As a software engineer, you’ve probably wondered how Git’s reflog
works under the hood. You (hopefully) won’t need to use reflog
daily, but you’ve likely found yourself in a situation where your Git state is seemingly borked beyond repair at least once before. Engineers are (mostly) human, and Git mistakes can happen even with safeguards like pre-commit hooks in place. That’s where this guide comes in.
Today, we’ll be taking a look at ways to fix these mistakes with the power of git reflog
. With reflog
you can navigate the entire granular history of your Git repository to identify and fix any errors that find their way into your codebase.
What is Git reflog?
The Git reflog
is a reference log file that stores a chronological list of all changes made to the HEAD
pointer in your Git repository. HEAD
always points to the most recent commit, and the reflog
essentially tracks every previous commit ever made in the repo.
Think of the reflog
as a logbook for your Git branches and other references. It records every change made to these references, enabling you to rewind time and undo any potentially unwanted actions. This can be incredibly helpful when you:
Accidentally commit something you shouldn't have.
Delete a commit or branch by mistake.
Need to recall what happened to a specific reference at a particular point in time.
This list is, of course, not exhaustive - I’m sure there are thousands of other mistakes you could possibly fix using reflog
.
Locating the reflog:
To start, let’s actually take a look at the reflog
and its various components.
Every repo has its own reflog stored in the .git
folder at the root of your repository. In the .git
directory, the reflog is stored in logs
folder and is updated any time you make any changes to any branch (or other reference, like HEAD
)
While the raw log contains a lot of useful info, it’s pretty ugly. Let’s look at how we can sift through the log and navigate it a little better.
Time qualifiers
Every action you take in Git is timestamped in the reflog
, so, when you want to explore specific moments in your Git history, you can easily filter by various periods of time with “time qualifiers”.
These tokens serve as filters for navigating your reflog
and allow you to specify a timeframe and view changes made within that window.
Here are a few examples:
@{1.minute.ago}
: See the state of your repository just a minute ago.@{1.hour.ago}
: Rewind your Git journey by one hour.@{1.day.ago}
: Compare your current work with yesterday's progress.@{yesterday}
: Travel back to yesterday's snapshot of your repository.@{1.week.ago}
: Analyze changes made a week ago.@{1.month.ago}
: Recall the state of your project a month back.@{1.year.ago}
: Explore the evolution of your work over a year.@{2023-10-21.08:30:00}
: Jump to a specific date and time in your Git history.
Combining and stacking time qualifiers**:**
@{1.day.2.hours.ago}
: Refine your search to a specific time within the past day.@{5.minutes.ago}
: Plural forms allow for even more precise navigation.
Using time qualifiers with other Git commands**:**
@{git diff main@{0} main@{1.day.ago}}
: Compare your current main branch with its state one day ago.@{git log master@{1.month.ago..yesterday}}
: View all commits made to the master branch between one month ago and yesterday.
Using reflog: your guide to time travel
You can se the see a detailed history of changes made to the current branch (HEAD
) or any other reference you specify using this command in your terminal:
git reflog
To check if a particular entry exists in the reflog, you can use:
git reflog exists <rev>
Go back in time:
You can also specify a specific point in the reflog
using keywords like
HEAD@{2}
(two revisions back)master@{one.week.ago}
(master's state one week ago).
This allows you to checkout that specific point and view the state of the world at that point in time any subsequent changes.
Clean up the past:
Because the reflog adds entries for every git event in your repo, it can get large very quickly. Luckily however, git has garbage collection built right in. The way this works is that many common Git commands silently run additional plumbing commands under the hood to prune the reflog. Two of these plumbing subcommands are expire
and delete
.
Expire
Expire prunes older reflog entries.
git reflog expire
You can also manually remove old entries based on specified timeframes:
git reflog expire <time>orgit reflog expire --expire-unreachable <time>
You can use the following options to customize the usage of this command:
--all
: Processreflogs
of all references.--single-worktree
: Limit processing to the current working tree only.--expire=<time>
: Prune entries older than a specified time.--expire-unreachable=<time>
: Prune unreachable entries older than a specified time.--updateref
: Update the reference to the topreflog
entry if the previous top entry was pruned.--rewrite
: Adjust the "old" SHA-1 ofreflog
entries if their predecessors are pruned.--stale-fix
: Prune entries referencing "broken commits".--dry-run
: Show what would be pruned without actually deleting any entries.--verbose
: Display additional information on the screen.
Delete
To remove an individual entry from the reflog
:
git reflog delete <entry>
You can use the following options to customize the usage of this command:
--updateref
: Update the reference to the topreflog
entry if the previous top entry was pruned.--rewrite
: Adjust the "old" SHA-1 ofreflog
entries if their predecessors are pruned.--dry-run
: Show what would be pruned without actually deleting any entries.--verbose
: Display additional information on the screen.
While these commands are important to understand, as an enduser you will typically never run them yourself as Git will run them under the hood.
Scenario: Correcting a Bad Rebase with Git Reflog
Imagine you're working on a feature branch in your project, and you initiate a rebase onto the main branch. However, something goes wrong during the rebase process—perhaps due to conflicts or incorrect conflict resolutions—and you end up with a feature branch that's in a worse state than before. This is a common situation that many developers face. Fortunately, git reflog
can be a lifesaver in such instances, allowing you to restore your feature branch to its state before the rebase.
Here's how to fix a bad rebase:
1. Identify the Commit Before the Rebase:
To undo the effects of a bad rebase, first find the commit hash where your feature branch was before the rebase. The git reflog
command will help you with this:
git reflog show <your-feature-branch>
This command shows a list of recent commits on your feature branch, including the state before the rebase. Look for the commit right before the rebase started.
Example output of the reflog might look like this:
52896f49 (HEAD -> feature) feature@{0}: rebase: Added new feature post-rebase7896dbe feature@{1}: commit: Pre-rebase commit43672ac feature@{2}: commit: Another feature update987fd12 feature@{3}: commit: Initial feature work
Here, 7896dbe
is the commit before the rebase started.
2. Revert to Pre-Rebase State:
Once you’ve identified the correct commit, use the following command to reset your feature branch to that state, effectively undoing the rebase:
git reset --hard 7896dbe
Replace 7896dbe
with the actual commit hash you identified. This resets your feature branch to how it was before the problematic rebase.
3. Verify the Branch State:
After running the git reset
command, check that your feature branch is back to its desired state:
git log
The top commit should now be the one you identified as the last correct commit before the rebase.
Conclusion
Remember that while the reflog
is a powerful tool, it's not a substitute for good engineering practices. Practicing safe Git habits, and by making frequent small pull requests you can minimize the need for reflog
interventions.
In a worst case scenario however, know thatreflog
can be your saving grace.