Image of git reflog | Understanding Git's reflog feature

ADVERTISEMENT

Table of Contents

Introduction

Maintaining a comprehensive project history is one of the excellent features of using Git as your version control system (VCS). However, Git's ability to modify repositories in a variety of ways can result in some unintended consequences, so it's crucial to have a reliable history.

When working on a local copy of any repository, Git stores a detailed history of your changes in what is called the reflog. The reflog can be relied upon to traverse any changes made to the Git HEAD ref and other reference like branch tips, even if these changes were deleted or otherwise lost.

What is git reflog for?

Git reflog tracks every change that affects the location of your Git refs. The reflog is stored solely as a part of your local repository and is not shared with remotes.

The official Git documentation states that reference logs, known as reflogs for short, 'record when the tips of branches and other references were updated in the local repository.'

Since these changes can be reapplied once you have the corresponding ref, you can think of the reflog as an undo history for your entire local repo. Used in conjunction with other Git commands such as git reset, git checkout, and git cherry-pick, git reflog works great for recovering a previous state.

Git reflog basic syntax

Basic usage of reflog returns a list of all places that the Git HEAD ref has recently pointed to, which looks like this in the terminal:

$ git reflog
18f5f28 (HEAD -> master) HEAD@{0}: commit (initial): Initial commit.

As you can see from this example, the output is quite similar to a git log --oneline command, with a few twists. Currently, only 1 entry is returned because we only have one commit so far, and haven’t made any changes that affect the position of Git HEAD.

The output starts with the commit ID of the most recent commit that HEAD pointed to 18f5f28. It also tells us that Git HEAD is currently pointing to the tip of the master branch, via HEAD -> master.

Next, take special note of the HEAD@{0} part of the git reflog output above. Git's reflog uses the syntax HEAD@{x} to denote specific positions of the HEAD pointer (or whatever ref is being listed). You can think of this as "where HEAD used to be X changes ago".

So as in our example, HEAD@{0} denotes the most recent change to Git HEAD. Changes to Git HEAD made further in the past would be denoted by the reflog as HEAD@{1}, HEAD@{2}, ... HEAD@{n}, and so forth.

You can see the reflog for a different ref, such as a branch name like master, by simply specifying it like:

$ git reflog master
18f5f28 (HEAD -> master) master@{0}: commit (initial): Initial commit.

Note how this time we see master@{0} in the output, indicating that we're looking at the most recent commit the master branch ref has visited.

Git reflog subcommands

Here are a few subcommands that can be passed in with the git reflog command:

  • git reflog show: The default behavior for the reflog command, and omitting "show" it will result in identical behavior. This command can be used with all the options accepted by the git log command.
  • git reflog delete <ref>: Will delete any single entry from the reflog by passing the corresponding git revision.
  • git reflog expire <ref/time>: Used to purge old reflog entries. The syntax accepts time or refs, such as master@{5.days.ago}.

Git reflog example

Now let's put Git's reflog to use by recovering a deleted commit. First, let's create a new repository to use as an example. We'll name it reflogex, make an initial commit, and take the following steps:

  1. Add and commit some text to file1.ext in the master branch.
  2. Run git log and git reflog, and compare the output.
  3. Use git reset to delete the commit we just made.
  4. Run git log and git reflog again, and compare the output.
$ git add .
$ git commit -m 'Initial commit.'
[master (root-commit) d99f811] Initial commit.
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 file1.ext
 
$ git commit -am 'Added text in master branch.'
[master b949949] Added text in master branch.
 1 file changed, 1 insertion(+)
 
$ git log --oneline
b949949 (HEAD -> master) Added text in master branch.
d99f811 Initial commit.
 
$ git reflog
b949949 (HEAD -> master) HEAD@{0}: commit: Added text in master branch.
d99f811 HEAD@{1}: commit (initial): Initial commit.
 
$ git reset --hard HEAD~1
HEAD is now at d99f811 Initial commit.
$ git log --oneline
d99f811 (HEAD -> master) Initial commit.
 
$ git reflog
d99f811 (HEAD -> master) HEAD@{0}: reset: moving to HEAD~1
b949949 HEAD@{1}: commit: Added text in master branch.
d99f811 (HEAD -> master) HEAD@{2}: commit (initial): Initial commit.

As you can see from the output, only the reflog contains data for our deleted commit; it even shows and entry for the git reset command we issued to eliminate that commit.

Now that we have the commit SHA, recovery is trivial. Assuming we haven't made any new commits since the deletion took place, we could use git reset like so:

$ git reset --hard b949949
HEAD is now at b949949 Added text in master branch.
 
$ git log --oneline
b949949 (HEAD -> master) Added text in master branch.
d99f811 Initial commit.

In this case, we could also use the HEAD pointer along with the special syntax we explained above, which would achieve the same results. The syntax looks like this:

$ git reset --hard HEAD@{1}

If you have made commits since the deletion took place, but you still want to recover this commit to the tip of the current branch, you can use git cherry-pick:

git cherry-pick b949949

Cherry-picking the commit will take the diff from the given commit and its parent and apply it to your current HEAD.

How do I recover a deleted branch?

When working on a branch other than the master, it's common practice to only delete that branch after it has been merged with the master branch. However, Git does offer the option to force a delete on a branch that hasn't been merged by passing -D to the git branch command.

When a branch is deleted in this manner, the only way to access the associated commits is by accessing the reflog.

Similar to our last example, it's a matter of tracking down the location of HEAD at the desired point. However, in this case, we want to reestablish a branch rather than a commit, so we'll be using the checkout command instead of reset or cherry-pick.

Let's reinitialize our reflogex branch from scratch and take the following steps to simulate the recovery of a deleted branch:

  1. Create a feature branch and check it out.
  2. Make two separate commits to feature by adding random text to file1.ext.
  3. Switch back to the master branch and delete the feature branch using the -D flag.
  4. Use git reflog and git checkout to reinstate the deleted branch.
$ git checkout -b feature
Switched to a new branch 'feature'
 
$ git commit -am 'Feature A'
[feature 2a4329f] Feature A
 1 file changed, 3 insertions(+), 1 deletion(-)
 
$ git commit -am 'Feature B'
[feature 8fd8ba7] Feature B
 1 file changed, 3 insertions(+), 1 deletion(-)
 
$ git switch master
Switched to branch 'master.'
 
$ git branch -D feature
Deleted branch feature (was 8fd8ba7).
 
$ git log --oneline
e99485b (HEAD -> master) Initial commit.
 
$ git reflog
e99485b (HEAD -> master) HEAD@{0}: checkout: moving from feature to master
8fd8ba7 HEAD@{1}: commit: Feature B
2a4329f HEAD@{2}: commit: Feature A
e99485b (HEAD -> master) HEAD@{3}: checkout: moving from master to feature
e99485b (HEAD -> master) HEAD@{4}: commit (initial): Initial commit.
 
$ git checkout -b feature HEAD@{1}
Switched to a new branch 'feature.'
 
$ git log --oneline
8fd8ba7 (HEAD -> feature) Feature B
2a4329f Feature A
e99485b (master) Initial commit.

In the above example, notice that after we deleted the feature branch and ran a git log, there was no trace of our deleted branch. Only after running git reflog were we able to review the contents and reinstate the deleted branch.

Git reflog undo rebase

When you use Git rebase on your feature branch, only to realize it wasn't a good idea for some reason, reflog can be used to undo it.

Undoing a rebase is similar to recovering a deleted commit. We use the reflog to find the commit immediately before the rebase, then use git reset --hard to reset our branch to this point.

To simulate, delete and reinitialize your example repo and take the following steps:

  1. Create a feature branch and make a commit.
  2. Switch back to the master branch, and make a commit.
  3. Switch back to the feature branch and run a git rebase.
  4. Use reflog to reset the feature branch prior to the rebase.
$ git checkout -b feature
Switched to a new branch 'feature.'
 
$ git commit -am 'Feature commit.'
[feature 1538578] Feature commit.
 1 file changed, 3 insertions(+), 1 deletion(-)
 
$ git switch master
Switched to branch 'master'
 
$ git commit -am 'Master commit.'
[master 33d2884] Master commit.
 1 file changed, 1 insertion(+), 1 deletion(-)
 
$ git switch feature
Switched to branch 'feature.'
 
$ git rebase master
 
// You may run into conflicts here. If so, resolve the conflicts and continue rebasing.
 
$ git add .
$ git rebase --continue
[detached HEAD 377106e] Feature commit.
 1 file changed, 6 insertions(+), 1 deletion(-)
Successfully rebased and updated refs/heads/feature.
 
$ git reflog
377106e (HEAD -> feature) HEAD@{0}: rebase (continue) (finish): returning to refs/heads/feature
377106e (HEAD -> feature) HEAD@{1}: rebase (continue): Feature commit.
33d2884 (master) HEAD@{2}: rebase (start): checkout master
1538578 HEAD@{3}: checkout: moving from master to feature
33d2884 (master) HEAD@{4}: commit: Master commit.
f490ae7 HEAD@{5}: checkout: moving from feature to master
1538578 HEAD@{6}: commit: Feature commit.
f490ae7 HEAD@{7}: checkout: moving from master to feature
f490ae7 HEAD@{8}: commit (initial): Initial commit
 
$ git reset --hard HEAD@{6}
HEAD is now at 1538578 Feature commit.
 
$ git log --oneline
1538578 (HEAD -> feature) Feature commit.
f490ae7 Initial commit

A quick review of the above examples shows that we located the commit prior to our rebase, and utilized the git reset command to reverse our rebase.

Where is git reflog stored?

As we’ve already mentioned, the reflog is only located on your local copy of the repository. More specifically, the reflog is located in .git directory inside the following paths:

.git/logs/refs/heads/
.git/logs/HEAD
.git/logs/refs/stash (if stash has been utilized)

Because this data is scoped locally, it is helpful to think of the reflog as your local undo history.

What is the difference between git log and reflog?

Despite having some overlapping features with Git log, reflog provides the benefit of keeping a local account of all deleted refs throughout your entire repo history.

There are a few key differences to keep in mind when it comes to git log and git reflog. The reflog is essentially a local, workspace-only history of your repository. Git log, on the other hand, is a publicly shared accounting of the history and is transferred along with the repo any time it is cloned or otherwise copied.

Another important distinction is that the log only outputs from the current branch by default unless other parameters are passed to the command. Reflog defaults to giving you the available history of the Git HEAD ref.

Because git log is focused on the current branch history, deleted commits and branches are excluded from the log output. Reflog, on the other hand, will always keep an accounting of these deleted refs.

As with many commands in Git, there are some overlapping features. For instance, git log -g (-g is shorthand for --walk-reflogs) allows you to traverse the reflog history.

How far back does Git's reflog go?

Git's reflog stores entries going back 90 days by default. This is customizable using Git's configuration for garbage collection. But since garbage collection is run by default to delete unreachable commits and data structures older than 90 days, reflog entries will only be valid for that amount of time as well. Obviously the reflog can't be used to retrieve information for Git objects that have been deleted through garbage collection.

Summary

Git reflog is an isolated data store used to keep an accurate running history of changes made to the HEAD pointer of your repository. It remains intact regardless of destructive commands such as git reset --hard, and can be used to restore lost commits or branches.

As a tool for ensuring the integrity of your project, the reflog is a must-know feature for anyone who plans on using Git as their VCS. Hopefully, after reading this tutorial, you can now feel confident when it comes to reinstating history you previously thought was gone for good.

Next Steps

If you're interested in learning more about how Git works under the hood, check out our Baby Git Guidebook for Developers, which dives into Git's code in an accessible way. We wrote it for curious developers to learn how Git works at the code level. To do this, we documented the first version of Git's code and discuss it in detail.

We hope you enjoyed this post! Feel free to shoot me an email at jacob@initialcommit.io with any questions or comments.

References

  1. Git SCM Docs, git reflog - https://git-scm.com/docs/git-reflog

Final Notes