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
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");
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
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
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
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
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
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
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)
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
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
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
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 @thebuildguy 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)
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).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
BUT! you can't do it without good communication. (Lest the communication will be from your team towards you, and contain swear words).
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!