Time Travel in Your Project: Undo Changes with Git
In life, undoing our mistakes is something we've always wished was possible. While life might not always present us with a chance to undo our mistakes, Git provides us ample opportunities to do just that. Lucky us! Undoing things in Git isn't just for mistakes. There are many other valid reasons you'd want to travel in time between different versions of a project. For example, maybe you're working on a new feature that isn't ready yet, and then you need to rollback to a previous commit.
In this post, we'll explore ways to undo changes in Git. So, next time you think you've made a commit in error, you'll have no reason at all to panic. As you know, Git stores snapshots of a repo at different points and then creates a timeline history. We'll be taking advantage of this timeline to move back and forth as we please. Let's jump right into it!
To make this post practical and relatable, I'll be creating a new git folder to use as a demo. I advise you do the same so you can easily follow along. We'll be doing everything from the terminal, so get ready to do some typing.
Setting Things Up
From my terminal, I'll run the following commands to create the repo folder, add some files and create some commits to work with:
mkdir git-undo && cd git-undo
touch page1.txt && echo "Let us put something here" > page1.txt
git add page.txt
git commit -m "create page1"
Now, we have a repo set up with one file added and one commit in the git history. Let's add a few more files with separate commits so we can have more commits to work with.
touch page2.txt && echo "Content for page2" > page2.txt
git add page2.txt
git commit -m "create page2"
touch page3.txt && echo "Content for page3" > page3.txt
git add page3.txt
git commit -m "create page3"
Checking Git History
To be able to travel back and forth in time, we need a way to know where we are. We also need a list of possible places and times we can travel to. And that's where the Git history comes in handy. There are two major ways to view the git history of a repo: git reflog and git log. While these two commands serve a similar purpose (viewing history), there are subtle differences in how they work and the result they show. Either of the two will serve us just fine, however, for the scope of this tutorial. So let's use git reflog. To read more about their differences, check out this StackOverflow answer. When you run the git reflog command, you should have something like this:
Git reflog shows a list of all commits that have been made in this repo, as well as some other key events, like when switching between branches (checkout). Each commit is identified by a SHA-1 hash (before the commit message). You can use these SHA-1 IDs to travel to any commit whenever you want.
Undoing Uncommitted Changes
The first approach we're going to look at in undoing changes is how to undo changes you've made but not yet committed. Whether you've staged these changes or not, what matters is that you haven't committed them. Let's make some changes to the third file we created earlier (page3.txt).
echo "making some changes to page3" > page3.txt
After running the above command, run git status. You should see something like this:
Let's assume you made the above change in error. Luckily, you realized the problem before making the commit. Now you want to restore the repo to how it was before you made the change. There are three ways to go about it:
git stash: The git stash command will discard all your untracked files, as well as staged and unstaged modifications. However, Git will temporarily save them, in case you want to retrieve them later.
git checkout --<file>: This works similarly to git stash, except that it discards changes to a file permanently.
git reset --hard: This also discards all changes permanently.
Which option is best? I mostly use git stash because I can reapply the discarded changes later. When I'm absolutely sure I won't ever need those changes, I use one of the other options.
Undoing Committed Changes (Git Reset)
Now, let's assume you didn't realize you made an error before you committed. Fret not! You can easily go back to a previous commit from where you're sure there's no error. Let's create a new page and commit it to see how to do this.
echo "some new content for the page 4" > page4.txt
git add page4.txt
git commit -m "create page 4"
We want to restore our repo to when we had just three pages. Here's how to do that:
Run git reflog to see a history of all commits made.
Then note down the ID (SHA-1 hash) of the commit we want to go back to.
Now run git reset --hard <commit id>.
As you can see in the image above, the commit hash when we had 3 pages was 6b6271a. We then passed this hash to the git reset --hard command to restore the repo to that commit.
Undoing Your Last Commit
What if after committing a series of changes, you make some changes that really should have been a part of the last commit? There's a way to undo—or more accurately, amend—the most recent commit. We can amend the last commit by running the git commit --amend command. Running this command will open your configured text editor so that you can edit the last commit message. Before running git command --amend, remember to first stage your new changes with git add. Close your text editor after typing in the new commit message. When you come back to your terminal, you'll see something like this:
A Note on Shared Repos
The three undo methods we've looked at above are best applied to local repos you're working on alone, when you haven't pushed your commits to a remote repo. A shared repo introduces a different dynamic to how you undo changes. Beyond Git, it's important that you communicate with other members sharing the repo so they're all aware and on the same page as you. One thing to consider when working on a shared repo is to use git branches for your local work. That way your work doesn't interfere with the main branch. You merge into the main shared branch only when you're sure your work contains no errors. That said, what if for any reason you have to do a git reset on a shared branch and you try to git push? Git will refuse the push, complaining that your local copy is out of date and missing some commits.
A brute force solution to this problem is to use the git push --force command. This command pushes your local copy to the remote repo and overwrites the history of the remote repo to be in sync with yours. However, this creates problems for other people sharing the repo. Also, if someone else also uses the git push --force command, it overwrites what you've done. That's why communication with team members is important. Git push --force is something I always avoid. I advise you to avoid it too, unless it's absolutely necessary.
Undoing Changes in a Shared Repo
If you must undo changes in a shared branch of a shared repo, the best tool to use is git revert <commit id>. It reverts the changes done by the commit you specified, and then it creates a new commit for it. In other words, the git revert inverts the changes of a specified commit as a new commit. This way, the history of the repo keeps going forward, and other collaborators don't pull a messed-up repo.
Recap of All Commands
Let's do a quick summary, since we've covered a lot in this post.
Git reflogto check commits history.
Git stashlets you discard changes and save them for later reuse.
Git checkout --<file>to discard uncommitted changes to a file.
Git reset --hardis for when you want to discard all uncommitted changes.
Git reset --hard <commit id>to point the repo to a previous commit.
Now you've seen the various methods Git provides for undoing changes. The next time you need to time travel in your project and undo things, always remember that Git has you covered. Check out the CloudBees blog to learn more about Git, GitOps, Git Switch Branch, Advanced Git with Jenkins and much more.