Undoing changes in Git — Part I

This will be a two part post on undoing changes in Git. I’m assuming if you’re reading this that you are familiar with Git and probably use it regularly. If not, you can find Git tutorials all over the interwebs to get you started. If you would like me to do a Git beginners tutorial series, please let me know in the comments.

Even if are a Git veteran, there’s a chance you might not be familiar with Git’s internal workings or how it stores ongoing project commits. This information will be relevant when we take a look at how you might ‘undo’ actual commits, which we’ll see is not really possible in a strict sense without drastic measures. First though, we will talk about undoing changes to your working copy and your staging index.

I’ll use a simple ‘project’ with is just a directory with 3 text files to demonstrate the concepts.

If you want to follow along, go ahead and create new directory anywhere you want, call it fox-project and cd into it. Once you do that, go ahead and do a git init to initialize our new Git repo:

mkdir fox-project
cd fox-project
git init

Now go ahead and create 3 new text files, you can call them whatever you want but I will call them file1, file2, and file3. We’re keeping it simple here, bear with me.

In each file create some text, again whatever you like, but I will be entering:

  • file1 — The quick brown fox
  • file2 — jumped over
  • file3 — the lazy dog.

Now, do a git status and you should see something along the lines of:

git status
On branch master

Initial commit

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


nothing added to commit but untracked files present (use "git add" to track)

So we have 3 files that are untracked right now. Let’s add and commit them separately so that we have some commits to view in our commit log.

git add file1.txt
➜ fox-project git:(master) ✗ git commit -m "Initial commit file1"
[master (root-commit) 6fd4c63] Initial commit file1
1 file changed, 1 insertion(+)
create mode 100644 file1.txt
➜ fox-project git:(master) ✗ git add file2.txt
➜ fox-project git:(master) ✗ git commit -m "Initial commit file2"
[master 223b018] Initial commit file2
1 file changed, 1 insertion(+)
create mode 100644 file2.txt
➜ fox-project git:(master) ✗ git add file3.txt
➜ fox-project git:(master) ✗ git commit -m "Initial commit file3"
[master 366a995] Initial commit file3
1 file changed, 1 insertion(+)
create mode 100644 file3.txt

Take a look at your commit log, and it should be like this:

commit 366a9950ff4558a35182ca4bcdd9526981c14b71
Author: Tom Casper <youremail@gmail.com>
Date: Wed Mar 22 21:01:51 2017 -0400

Initial commit file3

commit 223b01845630c4474adca00b553d3d8e7105b6b8
Author: Tom Casper <youremail@gmail.com>
Date: Wed Mar 22 21:01:19 2017 -0400

Initial commit file2

commit 6fd4c63a33f6b79bd37f020fdd27bd8a3455c663
Author: Tom Casper <youremail@gmail.com>
Date: Wed Mar 22 21:01:03 2017 -0400

Initial commit file1

Ok, great! So now we’ve got a simple little project to work with. By the way, it’s always a good idea to keep a directory somewhere that you can use as a git-sandbox just like this where you can try out different commands and concepts as you learn them and experiment a bit.

As you might already be aware, Git has the concept of a working copy, staging index, and repository.

Your working copy is basically the set of changes that are currently living in your git controlled project that are different from what’s in the repository and have not yet been added to the staging index. They are what’s called, untracked.

The staging index is the intermediate area between your working copy and the actual repository where you add changes from your working copy. Part of the reason for this staging index is to make better, more relevant commits using changesets. Once you add a file or change to this index it’s said to be tracked and ready for permanent commit to the repository.

The repository is the permanent storage area for all your files that are tracked by Git. Once you commit, it’s forever…for the most part. We’ll see in part II that in some cases commits can be modified or reverted, but it’s a lot more complicated in Git due to the way it stores commits.

Undoing a change in your working copy

So let’s say you make a change to one of your files. You’ve not yet added that change to the staging index, in fact you’ve done nothing except make the change to the file itself and that’s it. No Git involved whatsoever. That means the change is in your working copy and can be undone with a simple checkout command.

I am a little bored with brown foxes, so I think I want to change the fox’s color. I’m gonna edit file1.txt to make the fox red and do a git status:

On branch master
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: file1.txt

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

Come to think of it I think that looks kind of stupid and I want to get back what’s in the repo. You can see above that Git gives you little hints about what to do in this case.

git checkout -- file1.txt

What this does is checkout the last committed version from the current branch. It’s that simple. If you want to undo changes in your working copy, again, changes that have not been added yet to the staging index, just do a git checkout — filename, and you’re all good. Note that the — is not strictly necessary, but is useful in that git checkout can be used to checkout branches, files, projects, etc…Using the — is just a way of ensuring you’re checking out that file from the current branch.

Undoing a change that you’ve added to the staging index

Let’s say now I am unhappy calling our dog in this story lazy. I’m gonna change that to diligent:

On branch master
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: file3.txt

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

And I really like that change so I’m gonna go ahead and stage it for commit:

git add file3.txt
➜ fox-project git:(master) ✗ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

modified: file3.txt

You know what, nah…let’s keep the dog lazy. Otherwise he may be too diligent and leap up and hurt the heroine of our story, the quick brown fox!

Git already tells you how to do this, so what the heck are you reading this for? :) That’s right, issue a:

git reset HEAD file3.txt

And that will do the trick.

Now we will go in depth into the reset command in the next part, and that command can be very powerful but also very dangerous. It should be used with caution. Here it’s pretty straightforward when your’e dealing with clean cut cases such as this.

Now what is this HEAD business? The HEAD is Git’s pointer to the tip, that is the latest commit, of the current branch. So you can think of the HEAD almost as a tape recording play head for those of us old enough to remember cassette tapes. If you aren’t, google it :) We will talk a little more about the HEAD when we discuss the reset command in depth in part II.

So, if you want to undo a change in your working copy, use git checkout, and if you want to undo a change you’ve added to your staging index, use git reset HEAD for simple cases.

In part II we’ll dive into the powerful reset command which will allow us to manipulate commits, and we will also take a look at a couple other ways to do so. In order to do that, we will also take a quick tour of Git’s commit architecture to get a better idea of why it’s so much more complicated to undo commits in Git, vs. undoing working copy or staging index changes.

Till next time brothers and sisters, thanks for reading \,,/

Originally published at tcasper.com.