Red Green Repeat Adventures of a Spec Driven Junkie

How-to Dig Out of git reset "git hole"

I want to share a git hole situation I had a hard time digging myself out of.

I show how I dug myself into the situation by resetting changes more than I wanted and show to easily fix the problem by using git reflog.

If you are in or run into this situation, you won’t have to agonize how to dig yourself out of the git hole and can elegantly put the repository into a desired state.

This article will take you about six minutes to read.

Nasca - Drum source and more information

Introduction

Ever get yourself into a “git hole”, where the repository’s state is not what you want by running the wrong command?

Most of the time, just “pulling down the repository again” will fix it (as with almost all problems related to software).

What if there’s actually work you want to keep or worse, other people’s commits you want to save?

What Happened?!

In my situation, a team member fat-fingered a command and put the repository into weird state. A state he didn’t want it to be in.

This article, I share the same situation using a practice repository.

If you want to follow along code in this article:

  1. Clone repository: git clone https://github.com/a-leung/tdd_indepth_callbacks.git
  2. Change to branch: git checkout fix/update_rails_1

Starting Out

I was looking at recent git history, say before pushing up changes and making sure I have a clean history when pushing up changes:

$ git log
commit 78323e0a89d12e486310ffffb3d90d65699fd2bd
Author: Andrew Leung <git commit email address>
Date:   Sat Jul 4 00:02:05 2020 +0000

	FIX: running bin/rails app:update

commit eaa73c18028ef149083921b517dc539b79630498
Author: Andrew Leung <git commit email address>
Date:   Fri Jul 3 23:57:58 2020 +0000

	FIX: Update Rails to 6.0.3.2

...
commit cfa3b5b90f93780404a033f16374c2d9e9c7e428
Author: Andrew Leung <git commit email address>
Date:   Fri Jan 18 23:15:08 2019 +0000

	Code: Order is received - only after create

commit 5bd384ba4152e1284523b0aec4cf12023131c83c
Author: Andrew Leung <git commit email address>
Date:   Fri Jan 18 23:11:39 2019 +0000

	Code: Order is received - simple

commit 5bcbbe5742fd84a37751b79979ca533b9cba0002
Author: Andrew Leung <git commit email address>
Date:   Fri Jan 18 23:10:06 2019 +0000

	Test: Order is received

Well, the running bin/rails app:update can be part of the Update Rails to 6.0.3.2 commit because they’re the same process.

Clean up History

In this case, the easy thing to do: reset the last commit and amend it into the commit before that.

Step 1, run: git reset HEAD~1

$ git reset HEAD~11 Unstaged changes after reset: M Gemfile M Gemfile.lock M app/models/order.rb M bin/rails M bin/rake M bin/setup M config/cable.yml M config/environments/development.rb M config/environments/production.rb M config/environments/test.rb M config/initializers/content_security_policy.rb M config/locales/en.yml M config/puma.rb M config/routes.rb M config/spring.rb M db/schema.rb M spec/models/order_spec.rb

What the?! What did I do???

I typed: git reset HEAD~11 instead of git reset HEAD~1!? I jumped back 11 commits instead of 1!!!!

AUGH!

What’s the git status???

Is this situation true??? Let’s confirm using git status

$ git status
On branch fix/upgrade_rails_1
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

		modified:   Gemfile
		modified:   Gemfile.lock
		modified:   app/models/order.rb
		modified:   bin/rails
		modified:   bin/rake
		modified:   bin/setup
		modified:   config/cable.yml
		modified:   config/environments/development.rb
		modified:   config/environments/production.rb
		modified:   config/environments/test.rb
		modified:   config/initializers/content_security_policy.rb
		modified:   config/locales/en.yml
		modified:   config/puma.rb
		modified:   config/routes.rb
		modified:   config/spring.rb
		modified:   db/schema.rb
		modified:   spec/models/order_spec.rb

Untracked files:
  (use "git add <file>..." to include in what will be committed)

		config/initializers/new_framework_defaults_6_0.rb
		db/migrate/20200704000022_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.active_storage.rb

no changes added to commit (use "git add" and/or "git commit -a")

Yeah, there are files that are yet to be commited that are not part of the commit I want to squash. Crap. Did I just lose commit history too?!

What does git log say??

There’s gotta be a mistake, what’s the current log???

$ git log
commit 5bd384ba4152e1284523b0aec4cf12023131c83c
Author: Andrew Leung <git commit email address>
Date:   Fri Jan 18 23:11:39 2019 +0000

	Code: Order is received - simple

commit 5bcbbe5742fd84a37751b79979ca533b9cba0002
Author: Andrew Leung <git commit email address>
Date:   Fri Jan 18 23:10:06 2019 +0000

	Test: Order is received

commit bb5c31e9b78bb0ecf88b46471ff367591b8be481
Author: Andrew Leung <git commit email address>
Date:   Fri Jan 18 23:09:19 2019 +0000

	Code: Order is pending - callback

commit db84373577f704e18a971b40089c66b60ce544f3
Author: Andrew Leung <git commit email address>
Date:   Fri Jan 18 23:08:47 2019 +0000

	Code: Order is pending - simple

The first entry of git log is from January 18, not July 3, which is what I wanted.

There’s definitely commits missing. In this case, they’re only my commits. If I had commits from team members lost, it would be even worse.

How do I fix this?!

After catching my breath and realizing: well, at least I didn’t lose actual work. That would be even worse. I’ll take the extra “git blame” on those changes. Recreating and extra 10 commits of work, oh boy. Things can definitely be worse. :-)

Enter: git reflog

git has a tool that tracks all the changes of the current HEAD, just run: git reflog. When I run it in my case:

$ git reflog
5bd384b HEAD@{0}: reset: moving to HEAD~11
78323e0 HEAD@{1}: checkout: moving from fix/upgrade_rails_2 to fix/upgrade_rails_1
595a6f1 HEAD@{2}: checkout: moving from master to fix/upgrade_rails_2
95769b4 HEAD@{3}: checkout: moving from fix/upgrade_rails_2 to master
595a6f1 HEAD@{4}: commit: FIX: Upgrade to Rails 6.0.3
95769b4 HEAD@{5}: checkout: moving from master to fix/upgrade_rails_2
95769b4 HEAD@{6}: checkout: moving from fix/upgrade_rails_1 to master
78323e0 HEAD@{7}: commit: FIX: running bin/rails app:update

This is the last set of actions I took on the repository. git tracks all changes made to the HEAD reference of the repository, stating the SHA value of the change, the HEAD history from 0, and a description of the action.

How can I use this??

To fix the problem of git reset HEAD~11, which is in the reflog as:

5bd384b HEAD@{0}: reset: moving to HEAD~11

The solution is the entry just before it:

78323e0 HEAD@{1}: checkout: moving from fix/upgrade_rails_2 to fix/upgrade_rails_1

To have git change HEAD to that entry, just do a git reset to the HEAD by running command:

$ git reset HEAD@{1}

Did that work?!

Let’s check the repository status now, what’s staged?

$ git status
On branch fix/upgrade_rails_1
nothing to commit, working directory clean

All clean… how about the git history?? Let’s check that using git log:

$ git log
commit 78323e0a89d12e486310ffffb3d90d65699fd2bd
Author: Andrew Leung <git commit email address>
Date:   Sat Jul 4 00:02:05 2020 +0000

	FIX: running bin/rails app:update

commit eaa73c18028ef149083921b517dc539b79630498
Author: Andrew Leung <git commit email address>
Date:   Fri Jul 3 23:57:58 2020 +0000

	FIX: Update Rails to 6.0.3.2

commit 8d3db208eba4de995d6b632b1766617403c3a709
Author: Andrew Leung <git commit email address>
Date:   Fri Jul 3 01:14:36 2020 +0000

	FIX: Update Ruby to 2.6.0

Everything’s there again, just like it was at the beginning. All work committed with the right commit history.

More information

If you want to find out more, the best place to look is the official documentation on reflog.

How it works

Although git presents a linear time line of work on the tracked items, under the hood, git is tracking every change in a non-linear manner. This is why you can even recover squashed commits.

The reflog is another entry to git’s guts without exposing all the details.

Why it’s important to know

If you want to be better at your craft, knowing your tools better improves your craft.

git is an essential tool for software development as it’s one of the most used code versioning systems today.

Another Solution

Given this knowledge of using the reflog - there is another way of solving the same problem without using the reflog:

If you notice, the entry:

78323e0 HEAD@{1}: checkout: moving from fix/upgrade_rails_2 to fix/upgrade_rails_1

has the same SHA as:

78323e0 HEAD@{7}: commit: FIX: running bin/rails app:update

Which refers to this commit:

commit 78323e0a89d12e486310ffffb3d90d65699fd2bd
Author: Andrew Leung <git commit email address>
Date:   Sat Jul 4 00:02:05 2020 +0000

	FIX: running bin/rails app:update

The original commit I wanted to “reset” to.

This means running, just running git reset <sha> would have solved the same problem:

$ git reset 7832

Will have the same effect.

reflog won’t be necessary if you had the commit SHA you want to return to. Either in your screen’s buffer or even on the upstream repository.

Conclusion

I never knew the reflog was there and now that I know it, I couldn’t believe why didn’t I know it sooner!

It would have saved me a lot of stress earlier in my development career and awkward conversations.

With reflog, I see it’s a clean way into the guts of git as it’s how git is tracking what to point to inside it’s own database of stuff.