DEV Community

Cover image for Time traveling with Git Reflog
Tulsi Prasad
Tulsi Prasad

Posted on

Time traveling with Git Reflog

It's 2021 and I still used to fear before running those pesky git commands, git reset --hard yet another time. Each time I kept a copy of my local branch and even tried having two different projects to avoid silly shenanigans. But eventually, apart from adding tiring overhead, it made me type a bunch more keys than required which I was pretty OCD about.

Then I discovered, git reflog.

Structure:


Concept

Git is very good at keeping track of changes that are happening to its HEAD. So each you do things commonly including, git commit, git merge or git reset it tracks it using reference logs.

Every time your branch tip is updated, git automatically stores a ref of that information for you. But mind that, this is temporary storage. You can only reference your history up to a certain point (~3 months) so it's not always reliable.

Usage

It's as simple as it sounds. You can see your reflog using:

$ git reflog

3844466 (HEAD -> main, origin/main) HEAD@{0}: merge refs/remotes/origin/main: Fast-forward
f7acb57 HEAD@{1}: checkout: moving from feat-c to main
fc859f8 (origin/feat-c, feat-c) HEAD@{2}: commit: Add c
f7acb57 HEAD@{3}: checkout: moving from main to feat-c
f7acb57 HEAD@{4}: pull: Fast-forward
6932227 HEAD@{5}: checkout: moving from feat-b to main
07087ce (origin/feat-b, feat-b) HEAD@{6}: commit: add b
6932227 HEAD@{7}: checkout: moving from main to feat-b
6932227 HEAD@{8}: Branch: renamed refs/heads/master to refs/heads/main
6932227 HEAD@{10}: commit (initial): added a
Enter fullscreen mode Exit fullscreen mode

Note: References are local to each user, it'll only contain changes that you make on your local copy of the repository

As you can see each ref has the commit ID, we can use this to see what the state was like at that point in time and if need be, can also jump to that specific point altogether.

You can see constituents of a reference using:

$ git show HEAD@{2}

commit fc859f87f8af7e7eaea88280b8ff5c5406920581 (origin/feat-c, feat-c)
Author: Tulsi Prasad <heytulsiprasad@gmail.com>
Date:   Sat Sep 18 09:33:41 2021 +0530

    Add c

diff --git a/c.js b/c.js
new file mode 100644
index 0000000..76fbf09
--- /dev/null
+++ b/c.js
@@ -0,0 +1 @@
+console.log("c was here");
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can either use the commit ID or relative notations of HEAD too.

More on knowing various relative notations using HEAD.

Advanced use cases

Along with tips of HEADs, git also keeps track of stashes, other branches, tags, remotes via references too. But by default git reflog only shows the reflog of HEAD ref. HEAD is always the tip of the current branch.

Thus when you run, git reflog it implicitly means git reflog show HEAD.

To see reflog of the entire project, use git reflog --all

You can use reflogs for stash as such:

$ git reflog show stash

f0c040c (refs/stash) stash@{0}: WIP on feat-e: f0082bb add d
29e2513 stash@{1}: WIP on feat-e: f0082bb add d
Enter fullscreen mode Exit fullscreen mode

Note: You have to have used git stash before in your project to see the reflogs.

Similarly you can track the origin branches like this:

$ git reflog show origin/main

3844466 (origin/main) refs/remotes/origin/main@{0}: fetch origin: fast-forward
f7acb57 refs/remotes/origin/main@{1}: pull: fast-forward
6932227 refs/remotes/origin/main@{2}: update by push
Enter fullscreen mode Exit fullscreen mode

Access reflogs by time

Reflogs also have a time of the executed command attached to it, so you can basically leverage this to see the difference in history between any two points.

Some default time strings.

1.minute.ago
1.hour.ago
1.day.ago or yesterday
1.week.ago
1.month.ago
1.year.ago
Enter fullscreen mode Exit fullscreen mode

Using them to see the diff:

$ git diff HEAD@{0} HEAD@{4.hours.ago}

diff --git a/d.js b/d.js
deleted file mode 100644
index 81fe3ac..0000000
--- a/d.js
+++ /dev/null
@@ -1 +0,0 @@
-console.log("d was here")
\ No newline at end of file
Enter fullscreen mode Exit fullscreen mode

So this implies as reflogs keep track of all your changes to the tip, like merge, rebase, reset, etc thus all your commits are safely kept inside reflog even after you get rid of them. Although this by default stores for (~90 days), you can manually update that.

Aversing risk with Reflogs

Now that you've known much already about conventions of reflog, now we can discuss how you can make it your safety net and use Git super-confidently all the time.

Example

Git Reset has been the bermuda triangle of commits for me, especially when I used to copy-paste commands from StackOverflow (please don't do that).

Let's say this is my log at one point in time and for some reason I decided to go back where I had just a as only file.

$ git log --oneline

* 724281a (HEAD -> master) add d
* cf20425 add c
* e1f07f6 add b
* 4037ca4 add a

$ git reset --hard 4037ca4
HEAD is now at 4037ca4 add a

$ git log --oneline
* 4037ca4 add a
Enter fullscreen mode Exit fullscreen mode

Using Reflog to revive newer files

$ git reflog

4037ca4 (HEAD -> master) HEAD@{0}: reset: moving to 4037ca4
724281a HEAD@{1}: commit: add d
cf20425 HEAD@{2}: commit: add c
e1f07f6 HEAD@{3}: commit: add b
4037ca4 (HEAD -> master) HEAD@{4}: commit (initial): add a

$ git reset --hard HEAD@{1}
HEAD is now at 724281a add d

$ git log --oneline

* 724281a (HEAD -> master) add d
* cf20425 add c
* e1f07f6 add b
* 4037ca4 add a
Enter fullscreen mode Exit fullscreen mode

As simple as that. 🤷‍♂️

Force pushing branches after rebase

A few days back there was a tweet on why you should keep your branch's backup before rebasing and I instantly knew what can be done, as I was halfway through the post lol 🤣. So here's a common use case you can avoid with reflog.

Ideally, force pushes are not recommended but when you rewrite your history (knowingly), this leaves us with this only option. However, in case this leads to breaking changes and you want your previous state of repo back, you know where I'm going with this.

Hopefully, you don't need to read anymore, if you've come all the way. I'd like you to continue when you've thought of a solution in your head.

~/Desktop/dev/git-ref (ijkl) $ git log --oneline
e4719d6 (HEAD -> ijkl, origin/ijkl) add l
87552ac add k
63841dc add j
0446b52 add i
724281a (origin/master, master) add d
cf20425 add c
e1f07f6 add b
4037ca4 add a

~/Desktop/dev/git-ref (ijkl) $ git rebase -i HEAD~4
hint: Waiting for your editor to close the file... 
hint: Waiting for your editor to close the file... 
[detached HEAD 1c7df0b] add ijkl
 Date: Sun Sep 19 16:56:54 2021 +0530
 3 files changed, 3 insertions(+)
 create mode 100644 i.txt
 create mode 100644 j.txt
 create mode 100644 l.txt
Successfully rebased and updated refs/heads/ijkl.

~/Desktop/dev/git-ref (ijkl) $ git push -f
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (5/5), 423 bytes | 423.00 KiB/s, done.
Total 5 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/heytulsiprasad/git-ref.git
 + e4719d6...1c7df0b ijkl -> ijkl (forced update)
Enter fullscreen mode Exit fullscreen mode

See above, I created four commits and rebased them to squash them into one before making the PR. But I accidentally dropped a commit add k which had k.txt file in it. But I've already force-pushed the commits to remote.

~/Desktop/dev/git-ref (ijkl) $ git log --name-status -1
commit 1c7df0b3fbeb46dfafb19fb5740ecac19f1a470a (HEAD -> ijkl, origin/ijkl)
Author: Tulsi Prasad <tulsi.prasad50@gmail.com>
Date:   Sun Sep 19 16:56:54 2021 +0530

    add ijkl

A       i.txt
A       j.txt
A       l.txt
Enter fullscreen mode Exit fullscreen mode

Now let's do some disaster management.

~/Desktop/dev/git-ref (ijkl) $ git reflog
1c7df0b (HEAD -> ijkl, origin/ijkl) HEAD@{0}: rebase (finish): returning to refs/heads/ijkl
1c7df0b (HEAD -> ijkl, origin/ijkl) HEAD@{1}: rebase (squash): add ijkl
cb62728 HEAD@{2}: rebase (squash): # This is a combination of 2 commits.
0446b52 HEAD@{3}: rebase (start): checkout HEAD~4
e4719d6 HEAD@{4}: commit: add l
87552ac HEAD@{5}: commit: add k
63841dc HEAD@{6}: commit: add j
0446b52 HEAD@{7}: commit: add i
724281a (origin/master, master) HEAD@{8}: checkout: moving from master to ijkl

~/Desktop/dev/git-ref (ijkl) $ git reset --hard HEAD@{4}
HEAD is now at e4719d6 add l

~/Desktop/dev/git-ref (ijkl) $ git log --oneline
e4719d6 (HEAD -> ijkl) add l
87552ac add k
63841dc add j
0446b52 add i
724281a (origin/master, master) add d
cf20425 add c
e1f07f6 add b
4037ca4 add a
Enter fullscreen mode Exit fullscreen mode

This is how you can get your most important k.txt file back and leave work each day like an absolute winner. 😅

For more references

  • For more tragic fuckups
  • Atlassian has a nice article on reflog too

Conclusion

Not all disasters can be avoided but knowing the ones that can is better than nothing. This reminds me of a quote from Picasso, “Learn the rules like a pro, so you can break them like an artist”. Tweet me your favorite git tricks that saved your day for real. You'll find me @heytulsiprasad on Twitter.

Huge thanks to Sreetam Das, Pratham Krishna, and Aditya Raj for reviewing this post beforehand, it really helped to make this post better.

Top comments (2)

Collapse
 
xtofl profile image
xtofl

Thanks for writing about this! I to use it very often, because I like a nice git log. I'm cleaning up, git commit --fixup-ping, --amend-ing, rebase -i-ing all the time. Like you, I discovered that fear is not needed when using git: there is always a memory lying around of what you created (if not on your harddrive, then on the one of your teammate who pulled three hours ago).

Ideally, force pushes are not recommended but when you rewrite your history (knowingly), this leaves us with this only option.

I highly recommend using force-pushes: they are useful if you want to use your git log as a recipe instead of as a personal work history

  • want to keep your history useful (I mean, after a month or so)
  • want to continuously share your work with your team

BUT! you can't do it without good communication. (Lest the communication will be from your team towards you, and contain swear words).

Collapse
 
thebuildguy profile image
Tulsi Prasad • Edited

Really good post there!

Both work log and recipes have their down and upsides. I think if you follow a recipe standpoint you can be removing some essential points in your repo which fixed a bug let's say, and all of it when made recipe will just be enough to give an overview of what's the bigger picture.

However, I'd be forced to use recipe method when working in a huge team when everyone has their minuscule commits of changes which can spoil the mainline strucutre. Also adding more semantics to commits are appreciated!

Thanks for stopping by! Glad you liked my post!