In the previous post in this series, we learned about the command git reset
, together with its three modes – --soft
, --mixed
, and --hard
. We then applied our knowledge to a few “gitsasters,” and saw that by understanding git and how it works, we can feel confident fixing cases when we need to rewrite history.
In this post, you’ll acquire additional tools to rewrite history and deal with “gitsasters.” Be sure to read Part 1 first, so you have the relevant background for overcoming git disasters.
Getting started with a repo
To get started, let’s start by creating a small repository to play with.
As you can see, I’ve created a repository and created a file called 1.txt
, with the content hello
. I then added it to the index
, and created a commit. This commit was added to the main
branch, as we can verify using git log
.
Committing to the wrong branch
Great, now let’s create another branch…
As I explained in the post about creating a repository from scratch, this creates the branch feature_branch
, and also changes HEAD
to point to that branch.
Next, create a new file, and commit this addition to this feature branch.
Now, switch back to the main
branch using git checkout
, make changes to the file 1.txt
, and record these changes on main
branch:
This is the result:
Oh, wait! What if you actually wanted this change to be introduced in the feature_branch
, and not in main
? We dealt with a similar case where we accidentally committed to the main
branch, but actually wanted the commit to be recorded on a new branch. Similar to that instance, we now want the change introduced in this commit on an already existing branch.
A reminder, it’s useful to draw the current state versus the desired state.
There are technically a few changes between the current “gitsaster” state and the desired state:
- The commit which
main
is pointing to. - The commit which “Commit 3” is pointing to.
- The commit which
feature_branch
is pointing to.
Start by adding the changes introduced in “Commit 3” to feature_branch
. Note the changes this commit introduced using git diff
.
How to use ‘git cherry-pick’
If you want to apply these changes to the last commit on feature_branch
, we need to be on feature_branch
so we can use git checkout
again. Now use the git cherry-pick
command.
This basically takes the changes in “Commit 3” (with the SHA-1 value of c067afe7a50a54b1137aef0ed3b63f611b4ee8c7
). In this case, the changes to the file 1.txt
generate a new commit including these specific changes, and then changes the pointer of the active branch. Now the feature_branch
points to the new commit.
You can now see the new commit, and the changes it introduced using git diff
.
This is exactly what we wanted: we applied the same changes that we had seen earlier, and this time, we applied them on feature_branch
. In other words, the new commit introduced the same changes as “Commit 3”, and it also has the same commit message – that is “Commit 3.” But this is a different commit object introduced with a different timestamp and thus it has a different SHA-1 value. At this point, let’s call it “Commit 3.1”.
So now, our newly created commit points to “Commit 2” and feature_branch
points to this new commit.
What’s left for us to do is to change main
to point to “Commit 1”. For that, we can simply switch to main
branch again using git checkout main
. Then we can undo the last commit on this branch using git reset
.
Awesome! So we’ve successfully achieved the desired state with the power of the well-known git reset
command together with the new command we introduced, git cherry-pick
.
Continuing with another example…
Pushing a faulty commit
Let’s create another file 2.txt
, stage and commit it:
Now our reachable git history looks like this:
Next, add some content to the newly created file, and then stage and commit the change:
And now, let’s say I push
this to the remote repository.
And… Oh, oops, I actually had a typo there:I wrote tezt
instead of text
. I want to fix it. Remember you can use git reset
of some sort to undo this last commit. The problem is that it has already been push
ed to the remote. This means that someone might have already pull
ed this commit, and we don’t want to rewrite history so that it is inconsistent between our copy of the repository and someone else’s copy of the repository.
Just to clarify, I recommend not using git reset
to rewrite commits that have already been pushed to the remote unless you are completely sure that no one else has pulled the relevant commits.
If you are not sure if someone has pull
ed the relevant commits, I recommend creating another commit that will simply undo the last commit. Obviously in this case, you can also just change the content of 2.txt
, but we want to clarify that we are undoing the last commit as it was a mistake.
Next, take the changes introduced in the last commit, which you can see by using git diff
.
Apply them in reverse.
So in this case, add a new line to erase it. By introducing a commit in which we remove a line, reversing it means adding the line again to undo the faulty commit.
How to use ‘git revert’
To undo the faulty commit, use git revert
. This command reverses the changes that previous commits introduced, and records them in new commits.
In this example, you can just use git revert HEAD
which tells git to take the changes introduced at HEAD
(which before running this command pointed to “Commit 5”) and reverse those changes.
Note that we now have a new commit. Let’s take a closer look at the changes this commit has introduced.
You can see that this commit did exactly the reverse of the previous commit.
An interesting note by the way: you can also use git show
to show the changes introduced in a specific commit. So instead of using git diff HEAD~1 HEAD
, just use git show HEAD
.
Indeed, this is the reverse of the previous commit.
And what if we lost the commit?
We’ve now seen two new commands – git revert
and git cherry-pick
. Let’s move on to a more advanced scenario.
Let’s say, for example, you introduced a commit that took a really long time to create.
So obviously, this wasn’t really a lot of work. But imagine that it was an important commit that took a long time to create, and you hastily, by mistake, executed the wrong command.
Oh my! What a gitsaster! 😨😱
Remember, git reset
first goes through the “soft” step, changing whatever HEAD
was pointing to. In our case, git reset
changes the main
branch to point to what used to be HEAD~1
. That is, the commit that reverted “Commit 5”. This is where git reset --soft
would stop.
Here you can see this is indeed the state by using git log
.
Next, git reset
updates the index to be the same as what HEAD
currently points to, after the first step. So in our example, the index no longer includes changes introduced to 1.txt
. That’s where git reset --mixed
would stop.
In the third step, git reset
updates the working dir to be like the index. In this case, it means the working dir will no longer include the changes we introduced to the file 1.txt
. To verify, use git status
and look directly at the file system.
So, our precious changes are gone!
Or… are they? 🤔 The question is whether they are actually gone.
As we know, the commit object representing “Commit 6” was introduced to the system, so the git object is recorded within git’s internal object database. This commit object references a tree object which will in turn have a reference to the blob with the updated contents of 1.txt
.
Below you can see the commit object is there – it is just not reachable from any of the branches.
So if you have the SHA-1 of “Commit 6”, you can actually look at it!
To validate, assuming you’ve stored the SHA-1 of the created commit, look at this commit object by using git show
.
And the commit is there!
And actually, its parent is the commit that reverted “Commit 5”. So basically, all we need to do is just to have HEAD point to “Commit 6” again. And for that, use the same command:
Again, this command first updates whatever HEAD
is pointing to. In this case, it is the branch main
, now pointing to “Commit 6”. To verify:
You can see that indeed main
points to “Commit 6”.
Next, git reset
updates the index to resemble the state of “Commit 6” and then also updates the working dir.
Awesome, now, let’s remove this commit from the history again.
At this point, we’ve completely undone “Commit 6”, so that main
points to the commit that reverted “Commit 5.0”. This can be easily verified.
Also, note that the staging area and the working dir match the state of the commit that reverted “Commit 5”.
Now, let’s say we don’t have the SHA-1 value of “Commit 6.” Are we lost?
How to use ‘git reflog’
Luckily, no, and this is an opportunity to introduce yet another important tool: git reflog
🎉
Basically, without needing to do anything, git silently records what HEAD
points to every time you change it. So whenever we change the active branch, introduce a new commit or use git reset
, git updates its reflog
.
Note the SHA-1 of our lost commit, “Commit 6.”.To get a better view of these commits, use git log -g
which gives us a normal git log
output, but for our reflog.
You can now use git reset --hard
with that SHA-1 value. Another option is to create a branch pointing to this SHA-1 value.
To remove our commit again, let’s use git reset --hard HEAD~1
here.
If you look closely at the reflog
, you can see that it also uses “nicknames” for commits. Just as we could use HEAD~1
to reference the parent of HEAD
, git reflog
uses HEAD@1
to reference the last thing HEAD
pointed to. So instead of using git reset --hard <SHA-1>
, reference this commit by its nickname.
As you can see, we’ve gotten the same result. That is, we are back at “Commit 6”.
Nice, and the clear benefits of git reflog
!
Bottom line
In my previous post, we covered the command git reset
, together with its three modes – --soft
, that moves whatever HEAD
is pointing to, --mixed
, that goes on to update the index to what HEAD
is pointing to after the soft
step, and last – --hard
, which updates the working dir to match the state of the index.
In this post, we’ve added three new tools to our toolbox. First, git cherry-pick
, which applies the changes introduced in a commit or a range of commits. Second, git revert
, which reverses the changes introduced in a commit or a range of commits. And last, git reflog
, which shows us the history of what HEAD
points to, and comes in handy when you’ve lost a commit. We’ve also applied our knowledge to various cases, including committing to the wrong branch, pushing faulty commits to the remote, and losing a commit from our reachable history.
All in all, my hope is that you now feel confident when facing a wide range of gitsasters. The important thing is to understand that these are all pointers to commit objects. When you are not sure, just draw the current state as well as the desired state, go over the tools we have covered here, and you will be on your way to success.
Piggybacking on the theme of success, I encourage you to check out Swimm’s free platform. GitHub’s State of the Octoverse report showed how developers see about a 50% productivity boost with easy-to-source documentation. Swimm’s community of users is growing at an astounding rate, all benefiting from code-coupled documentation. Development work requires collaboration, and Continuous Documentation is the game changer.
Sign up for a 1:1 Swimm demo and see Swimm’s knowledge management tool for code up close.