Once familiar with git
, editing code becomes a breeze. Refactorings and deletions feel as natural and safe as adding code. git
is always there to back us up if something goes awfully wrong!
But what about editing the git history itself?
Could someone also have our back when we run rebase
or reset
?
The answer is "yes"! git
has just the right for this job: the reflog
!
Once you have built your own mental model of reflog
, history-modifying commands become as natural and safe as editing code.
Before we actually dive in into what reflog
is, we need to understand a key concept in git
: the HEAD
.
What is HEAD
?
The HEAD
in git
is a simple pointer of the commit that is currently checked out in your local work tree.
When digging a little further into the core of git, you realize that pretty much anything is a reference to a commit in git. branches
, HEAD
, stash
are all commit references.
A look into the .git/refs
folder will reveal all those references:
.git/refs/heads/<branchname>
→ points to the latest commit of the branch<branchname>
.git/refs/stash
→ points to the latest commit on thestash
(yes, the changes you stash are commits under the hood).git/refs/tags/<tagname>
→ points to the commit referenced by the tag<tagname>
Along with it, you will find 2 more special commit references in the .git/
folder
.git/HEAD
→ the reference of the commit that is currently checked out in your local repository.git/ORIG_HEAD
→ the reference of the previous position of the HEAD (somewhat deprecated,reflog
achieves the same goal and much more)
What is the reflog
?
The reflog
is a quite simple concept, actually! It is a log of all the moves the git HEAD
has ever made in your local repository.
As such, reflog
is a very personal log that will never be shared with the remote repository. You will share the same commit history with your coworkers (modulus the commits that are not pushed) but your reflog
is yours only.
Which commands are moving the HEAD
?
The rule of thumb is: if you move from one commit to another, then the HEAD
is changing and a new entry is appended to the reflog
.
To be slightly more exhaustive, let's explain a few git commands with regard to what it does to the HEAD
:
git commit
→ creates a new commit and moves theHEAD
to this commitgit checkout <branch>
→ moves theHEAD
from its current commit to the commit referenced by<branch>
git pull
→ pulls the missing commits from the remote and moves theHEAD
to the latest commitgit merge
→ creates a merge commit and movesHEAD
to point to this merge commitgit reset HEAD~1
→ moves theHEAD
to the previous commit- ...
Every single of those moves will be inserted into the reflog
.
Let's take a simple example in order to let the concept sink in.
Imagine you are on branch feature
and this is the initial state of the history:
A initial commit < (HEAD, feature)
Let's run a bunch of git
commands on top of it
git commit -m 'second commit'
: moves HEAD
and feature
to the newly created commit
B second commit < (HEAD, feature)
|
A initial commit
git checkout A
: moves HEAD
to commit A
(but feature
is still pointing to B
)
B second commit < (feature)
|
A initial commit < (HEAD)
git checkout feature
: moves HEAD
back to the commit referenced by feature
B second commit < (HEAD, feature)
|
A initial commit
git reset A
: moves both HEAD
and feature
to commit A
B second commit
|
A initial commit < (HEAD, feature)
Every single of those moves will be recorded in the reflog
.
Real life example: recovering from a reset
Let's say you ran the examples commands that we saw earlier. You are in that state
B
|
A < (HEAD, feature)
Both HEAD
and feature
are pointing to A
and you might feel that commit B
is lost forever.
Fear not, commit B
is all but lost. It has just become unreachable from the branches you have... But it still exists in git
!
Unfortunately, commit hashes usually do not look like A
but more something like f1fec78c3a05708d7cb55d9e213f1ac51292b52f
. This makes it impossible, for a human at least, to just recall that hash and run git reset <commithash>
.
That's where the reflog
comes into play.
As we said earlier, every single move you make between commits is recorded in the reflog
. So, let's have a look at that reflog
:
A HEAD@{0}: reset: moving to A
B HEAD@{1}: checkout: moving from A to feature
A HEAD@{2}: checkout: moving from feature to A
B HEAD@{3}: commit: second commit
The entire story of what happened in the previous example is just lying there. We can see everything we did in reverse chronology order (the most recent moves on the top). We can reconstruct the story just by reading this log from bottom to top. Most importantly, we now have the commit hash that we are interested in recovering: B
.
So we have a few options to recover our commit:
git reset B # reseting our current branch to the commit hash we want
git reset HEAD@{1} # reset our current branch to the previous state of `HEAD`
git cherry-pick B # create a new commit on top of `feature` with the content of commit `B`
# last option is only if there is a single commit you want to recover
Recovering from a failed rebase
is pretty much similar.
It is just a matter of finding the previous top commit of the branch in the reflog
and running git reset <previous-top-commit-hash>
. You will find all the initial commits exactly as they were before the rebase was initiated.
Conclusion
One thing you can keep in mind is: There are no lost commits in git, only harder-to-reach ones! Once something is committed to git
, it is here to stay! A hard-to-reach commit is just a git reflog
away!
So, do not fear playing with reset
or rebase
in your feature branches and enjoy confident history rewritings* 🤓! Git is here to back you up!
*But please in mind that rewriting history of branches shared with coworkers is never a good idea ;)