Home iOS & Swift Books Advanced Git

The Many Faces of Undo Written by Chris Belanger

One of the best aspects of Git is the fact that it remembers everything. You can always go back through the history of your commits with git log, see the history of your team’s activities and cherry-pick commits from other places.

But one of the most frustrating aspects of Git is also that it remembers everything. At some point, you’ll inevitably create a commit that you didn’t want or that contains something you didn’t intend to include.

While you can’t rewrite shared history, you can get your repository back into working order without a lot of hassle.

In this chapter, you’ll learn how to use the reset, reflog and revert commands to undo mistakes in your repository. While doing so, you’ll find a new appreciation for Git’s infallible memory.

Working with git reset

Developers quickly become familiar with the git reset command, usually out of frustration. Most people see git reset as a “scorched earth” approach to fix a repository that’s messed up beyond repair. But when you delve deeper into how the command works, you’ll find that reset can be useful for more than a last-ditch effort to get things working again.

To learn how reset works, it’s worth revisiting another command you’re intimately familiar with: checkout.

Comparing reset with checkout

Take the example below, where the branch mybranch is a straightforward branch off of master:

Working with the three flavors of reset

Remember that Git needs to track three things: your working directory, the staging area and the internal repository index. Depending on your needs, you can provide parameters to reset to roll back either all those things or just a selection:

Testing git reset –hard

git reset --hard is most people’s first introduction to “undoing” things in Git. The --hard option says to Git, “Please forget about the grievous things I’ve done to my repository and restore my entire environment to the commit I’ve specified”.

Removing an utterly useless directory

Start by going to the command line and navigating to the root directory of your repository. Execute the following command to get rid of that pesky js directory, which doesn’t look very important:

git rm -r js
git commit -m "Deletes the pesky js directory"

git log --all --decorate --oneline --graph
* 6c5ecf1 (HEAD -> master) Deletes the pesky js directory

Restoring your directory

In this case, you want to return to the last commit before you made your blunder. You don’t even need to know the commit hash; you can provide relative references to git reset instead.

git reset HEAD^ --hard

Trying out git reset –mixed

Imagine that you’re working on another software project. You’re up late, the coffee ran out hours ago and you’re tired. That never happens in real life, of course, but bear with me.

echo 'password=correcthorsebatterystaple' >> SECRETS
git add .
git commit -m "Adds final styling for website"

Removing your unwanted commit

You could use git reset HEAD^ --hard, as above, but that would blow away hours of hard work. Instead, use git reset --mixed to reset the commit index and the staging area, but leave your working directory alone.

git reset HEAD^ --mixed
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   SECRETS

echo SECRETS >> .gitignore
git add .gitignore
git commit -m "Updates .gitignore"

Using git reset –soft

If you like to build up commits bit by bit, staging changes as you make them, then you may encounter a situation where you’ve staged various changes and committed them prematurely.

touch setup.config
git add setup.config
echo "For configuration instructions, call Sam on 555-555-5309 any time" >> README.md

Making a mistake

Just before you add that to the staging area, Will and Xanthe call you excitedly with their plans for their next big project: to create a — wait for it — magic triangle generator. You humor them for a while, then turn your attention back to your project.

git commit -m "Adds configuration file and instructions"
[master c416751] Adds configuration file and instructions
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 setup.config

Cleaning up your commit

So now, you need to clean up that commit so it includes both the change to README.md and the addition of setup.config.

git reset HEAD^ --soft
git add README.md
git commit -m "Adds configuration file and instructions"
[master 297be58] Adds configuration file and instructions
 2 files changed, 2 insertions(+)
 create mode 100644 setup.config
git log -p -1
diff --git a/README.md b/README.md
index 331487d..fb18f7c 100644
+For configuration instructions, call Sam on 555-555-5309 any time
diff --git a/setup.config b/setup.config
new file mode 100644
index 0000000..e69de29

Using git reflog

You know that Git remembers everything, but you probably don’t realize just how deep Git’s memory goes.

git reflog
297be58 (HEAD -> master) HEAD@{0}: commit: Adds configuration file and instructions
6b51dc9 HEAD@{1}: reset: moving to HEAD^
c416751 HEAD@{2}: commit: Adds configuration file and instructions
6b51dc9 HEAD@{3}: reset: moving to HEAD^
9142192 HEAD@{4}: commit: Adds final styling for website
6b51dc9 HEAD@{5}: reset: moving to HEAD^
6c5ecf1 HEAD@{6}: commit: Deletes the pesky js directory
6b51dc9 HEAD@{7}: filter-branch: rewrite
1bc3d71 (refs/original/refs/heads/master) HEAD@{8}: filter-branch: rewrite
32281cf HEAD@{9}: filter-branch: rewrite
fdb857a HEAD@{10}: rebase -i (abort): updating HEAD
59f601b HEAD@{11}: rebase -i (pick): Linking to the main CSS file
e725307 HEAD@{12}: rebase -i (pick): Creating basic CSS file

Finding old commits

You’ve rethought your changes above. Putting configuration elements in a separate file in the repo along with instructions isn’t the best way to go about things. It obviously makes more sense to put those settings, along with Sam’s mobile number, on the main wiki page for this project.

git reset HEAD^ --hard
git log --all --oneline --graph
git reflog
6b51dc9 (HEAD -> master) HEAD@{0}: reset: moving to HEAD^
297be58 HEAD@{1}: commit: Adds configuration file and instructions

Recovering your commit with git checkout

Even though you usually use git checkout to switch between branches, as you saw way back at the beginning of this chapter, you can use git checkout and specify a commit hash, or in this case, a reflog entry, to create a detached HEAD state. You’ll do that now.

git checkout HEAD@{1}
Note: checking out 'HEAD@{1}'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

HEAD is now at 297be58 Adds configuration file and instructions
git log --all --oneline --graph
* 297be58 (HEAD) Adds configuration file and instructions
* 2401800 (master) Updates .gitignore
git checkout -b temp

Checking that your changes worked

Look at your commit tree again with git log --all --oneline --graph and you’ll see something like this:

* 297be58 (HEAD -> temp) Adds configuration file and instructions
* 2401800 (master) Updates .gitignore
git log --all --oneline --graph
* 297be58 (temp) Adds configuration file and instructions
* 2401800 (HEAD -> master) Updates .gitignore

Using git revert

In all of this work with git reset and git reflog, you haven’t pushed anything to a remote repository. That’s by design. Remember, you can’t change shared history. Once you’ve pushed something, it’s a lot harder to get rid of a commit since you have to synchronize with everyone else.

Setting up your merge

First, merge in that branch. Ensure you’re on master to start:

git checkout master
git merge temp
Updating 6b51dc9..297be58
 README.md    | 1 +
 setup.config | 0
 2 files changed, 1 insertion(+)
 create mode 100644 setup.config
* 297be58 (HEAD -> master, temp) Adds configuration file and instructions
* 2401800 Updates .gitignore

Reverting your changes

While you can’t change shared history, you can at least revert the changes you’ve made here to get back to the previous commit.

git revert HEAD --no-edit
[master 82cfe6d] Revert "Adds configuration file and instructions"
 2 files changed, 1 deletion(-)
 delete mode 100644 setup.config
[master 297be58] Adds configuration file and instructions
 2 files changed, 1 insertion(+)
 create mode 100644 setup.config
* 82cfe6d (HEAD -> master) Revert "Adds configuration file and instructions"
* 297be58 (temp) Adds configuration file and instructions
* 2401800 Updates .gitignore
diff --git a/README.md b/README.md
index fb18f7c..331487d 100644
--- a/README.md
+++ b/README.md
-For configuration instructions, call Sam on 555-555-5309 at anytime
diff --git a/setup.config b/setup.config
deleted file mode 100644
index e69de29..0000000

Key points

Congratulations on finishing this chapter! Here’s a quick recap of what you’ve covered:

Where to go from here?

You’ve already covered quite a lot in this chapter, but I recommend reading a bit more about how relative references work in Git.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.