loading...
Cover image for 3 ways to time travel in Git to undo destructive mistakes

3 ways to time travel in Git to undo destructive mistakes

zaklaughton profile image Zak Laughton ・Updated on ・6 min read

Raise your hand if you can relate:

πŸ™‹πŸ™‹πŸ™‹πŸ™‹πŸ™‹

We've all been there! It's easy to do a well-meaning git rebase or git reset only to find everything broken afterwards. Then, since your changes rewrote history, the only way to go back is to delete everything and re-clone the repository to start over again. I used to do this all the time!

But what if I told you there was a better way? What if you could time travel back to a point before you did those destructive changes without deleting anything? It turns out, git has built-in tools for this such as reflog, ORIG_HEAD, and gitrevisions, and they're surprisingly easy to use!

Back to the Future screenshot with caption "When this baby hits reflog, you're going to see some serious shit"

In this article, I'll show you how to use these nifty shortcuts to undo some of those history-changing moves and get back to the proper timeline!

Setup

If you only want to read the examples, you can skip to the next section. But if you want to follow along with the examples on your own terminal, run each line below as a command in your terminal to set up the demo repository:

mkdir reflog-demo && cd reflog-demo && git init
echo "file 1 content" > file1.txt && git add . && git commit -m "add file1"
echo "file 2 content" > file2.txt && git add . && git commit -m "add file2"
git checkout -b my-new-branch
echo "new branch file content" > newBranchFile.txt && git add . && git commit -m "add newBranchFile"
git checkout master
echo "file 3 content" > file3.txt && git add . && git commit -m "add file3"
git checkout my-new-branch

Examples

Here's an overview of the starting git history (using the command git log --oneline --graph --all):

git log screenshot

There is a master branch with 3 files, each added in a separate commit. There is also a separate my-new-branch branch. This branch also has the first two files, but then splits off from master and has its own third file.

And here are all the files in my-new-branch:
screenshot of ls -1 command

This is the desired state. When we mess it up in the examples below, our goal is to bring the repository back to this state.

1. Reset using a reflog reference

Let's say we're on my-new-branch and we rebase onto master:

Terminal screenshot running git rebase

But Agh! We realize this was a mistake! Maybe the code stopped working after the rebase. Maybe there were some weird merge conflicts during the rebase and everything got messy. Either way, we want things to be the way they were before, but here is what our new git log shows:

screenshot of rebased git log

...and all our my-new-branch files are combined with the master files:
screenshot of "ls -1" command after rebase on master

Apparently, our original commit (6cde040) is gone. How do we go back? We could delete everything and re-clone, but we probably don't have a remote copy of our most recent changes to clone. We could manually create a new branch off the old commit and re-add/delete the files that were there, but what a hassle!

Instead, try running git reflog to see some helpful points in history:
screenshot of git reflog output

Git reflog shows everywhere our current working HEAD was at, including commits that were removed with destructive commands such as git reset and git rebase. Simply pick the commit before our destructive changes, do a hard reset to the gitrevision number (the one with the curly braces), and everything is reset to the way it was before the rebase!

git reset --hard HEAD@{3}

After running this command, our git log shows us we're back to our original state with the original commits and the original files:

Original tree
Ahhhh, good ol' un-touched branches!

2. Reset to ORIG_HEAD

"But Zak," you might say, "that reflog output is kind of confusing, and it's hard to tell exactly which reflog line to go back to."

Agreed! Luckily, there's an EVEN EASIER way!

git reset --hard ORIG_HEAD

ORIG_HEAD automatically points to the state before the most recent destructive change, so we can easily undo our most recent rebase or reset with a single command! Running git reset --hard ORIG_HEAD here does the exact same thing as running git reset --hard HEAD@{3} in the example above without needing to hunt down a specific revision!

3. Reset to a relative time

🌟🌟🌟WARNING: THIS IS REALLY COOL

Resetting to ORIG_HEAD is great, but what if we've really gone down a rabbit hole of destructive changes. Let's take the rebase from the last example, and add a few more steps to it:

git rebase master
git reset --hard HEAD~2
git commit --amend -m 'shablagoo!'

Why did we do this? I don't know Β―\_(ツ)_/Β―. Someone on Stack Overflow probably said it was a good idea, so we tried it on a whim and, well, things got weird:
Screenshot of log after changes
Screenshot of files after changes

The commits are all out of order, and we are missing files. How can we undo this??? We made several destructive changes, so ORIG_HEAD isn't going to work. We can check git reflog, but there's so many changes that happened that it might be hard to find the right one. All we want is for our branch to look like it did 5 minutes ago before the changes.

Try this:

git reset --hard HEAD@{5.minutes.ago}

...and afterwards, you'll see the branch in the exact state it was 5 minutes ago:

Original tree
Ahhhh, good ol' un-touched branches!

Yes, you read that right: This command literally tells Git in plain English to return to some time in the past, and Git does it!

"MAGIC" gif

This is using a powerful concept in git called gitrevisions. Some other examples include: HEAD@{1.day.2.hours.ago}, HEAD@{yesterday}, HEAD@{2020-01-01 18:30:00}, HEAD@{12:43}. In other words: TIME TRAVEL IS POSSIBLE IN GIT!!!

Caveats

The techniques above are powerful, but there are a few limitations to keep in mind:

  • Only works for your local terminal – your reflog and gitrevisions are stored locally, but aren't shared when you push to a remote repository
  • Only works for committed files – if you've deleted uncommitted files, these aren't stored in Git anywhere. Good reason to commit early and often

Recap

So, to summarize, here are some ways to fix your repo after you reset or rebase your code into an unusable state:

  • Use git reflog to pick a point in history, then undo your recent changes git reset --hard HEAD@{<number>}
  • As a shortcut, use git reset --hard ORIG_HEAD to undo the most recent destructive operation
  • Reset to <refname>@<relative time> to time travel back to a happier state (e.g. git reset --hard HEAD@{10.minutes.ago})

I hope this helps someone out there! Let me know in the comments if you have any questions or any other go-to methods for undoing git mistakes!


Did you find this this article useful? Feel free to follow my profile below or follow me on Twitter for more developer tips and article announcements!

Discussion

pic
Editor guide
Collapse
slavius profile image
Slavius

Please note that you would only be able to achieve all this if your git repo was not garbage cleaned (yes, git has its own Garbage Collector).

There is an optional git config variable called gc.reflogExpireUnreachable that defaults to 30 days that clears all unreachable reflogs (not pointing to/from any live reflogs - a.k.a. lost commits).

Git garbage collection is run automatically on several frequently used commands:

  • git pull
  • git merge
  • git rebase
  • git commit

To prevent running automatic GC on your git repos run:
git config --global gc.auto 0

Similarly you could set to never expire reflog entries which would make your git repo to grow much larger than usually:
git config --global gc.pruneExpire never
git config --global gc.reflogExpire never

Collapse
zaklaughton profile image
Zak Laughton Author

Thanks for pointing this out! These tools are best used for resets to a recent state (within a few days at the most).

From my understanding, the default expiration is 90 days, but the impact is the same: if you have really old changes that you might want to use later, these should probably be reliably saved in a commit somewhere.

If you know ahead of time you want a reliable long-lasting backup of your branch state, you can create a backup branch before making changes.

git checkout my-new-branch
git branch my-new-branch-backup
# now reset, rebase, or otherwise destroy my-new-branch
# checkout my-new-branch-backup later to restore
Collapse
slavius profile image
Slavius

The thing is some tools enable/update/run git GC automatically (and sometimes even without you knowing). That might kill your whole reflog history and any attempts to restore... :(

Collapse
aleksandrhovhannisyan profile image
Collapse
aleksandrhovhannisyan profile image
Aleksandr Hovhannisyan

Came here thinking this was gonna cover generic things I already knew, but nope! I actually learned something :) That last one blew my mind:

git reset --hard HEAD@{5.minutes.ago}

I'll have to keep this in mind!

Collapse
yujiri8 profile image
Ryan Westlund

Wowww.... this stuff is incredible! I knew about reflog, but not about ORIG_HEAD and I would never have imagined that you could specify times!

Collapse
pinutz23 profile image
Jannik Wempe

Exactly the same here! Awesome article. Thanks for teaching me something new πŸ˜„

Collapse
michelemauro profile image
michelemauro

Or you could just use a sane SCM system, like Mercurial that, by design, does not destroy history so easily: it is impossible in hg to remotely damage a shared repository πŸ€·β€β™‚οΈ

Collapse
brownio profile image
Antonio Djigo

Git Bisect is a really powerful tool too when you don't really know when something started to fail.

Collapse
chathula profile image
Chathula Sampath

Great writeup. Learned new things that i never thought 😍😍 love the time traveling 😍

Collapse
leewarrickjr profile image
Lee Warrick

Great article! Well written and humorous, with great examples.

... Could you do one on how to rebase and squash commits?

Collapse
zaklaughton profile image
Zak Laughton Author

Glad you liked it! Rebasing and squashing is definitely on my list of things I'd like write about. Thanks for the request, and stay tuned...