Customized Remote Work Solutions From the World’s Largest Fully Remote CompanyCustomized Remote Work SolutionsLearn More
11 minute read

The Advanced Git Guide: Git Stash, Reset, Rebase, and More

Ursula has over five years of experience in software development and specializes in front-end development, especially intricate UI.

Every developer should have a good understanding of version control, and Git has become the de-facto standard for version control in software development.

Often though, developers only learn a few simple commands and overlook the power of Git history and the other things that Git can do to make you much more efficient. For example, managing releases is very easy with Git using git tag.

I took an advanced course in Git online (with Github) and proceeded to teach a beginner’s Git class in conjunction with Github. When I noticed there were not many technical articles on my favorite Git features, I jumped at the chance to share it with my fellow developers. In this post, you’ll learn how to leverage the following advanced Git functions:

  • git stash, which makes a temporary, local save of your code
  • git reset, which lets you tidy up your code before doing a commit
  • git bisect, a function that allows you to hunt out bad commits
  • git squash, which allows you to combine your commits
  • git rebase, which allows for applying changes from one branch onto another

Git Stash

Git stash enables you to save your code without making a commit. How is this useful? Picture the following scenario:

You’ve already made three neat and tidy commits, but you also have some uncommitted code that’s quite messy; you won’t want to commit it without removing your debugging code first. Then, for some reason, you suddenly need to attend to another task and have to switch branches. This can often happen if you are on your master branch, and you have forgotten to create a new branch for your feature. Right now, your code looks like this:

Git status screenshot

Git diff screenshot

When you run git stash, the uncommitted code disappears without being committed. Stashing is like saving a temporary local commit to your branch. It is not possible to push a stash to a remote repository, so a stash is just for your own personal use.

Git stash screenshot

Your branch now appears as it was when you made your last commit. Now, you can safely change branches without losing your code or having a messy commit. When you switch back to your branch and run git stash list you’ll see a list of stashes that look something like this:

Git stash list screen shot

You can easily reapply the stashed content by running git stash apply. You can also apply a specific stash (if you have stashed more than once) by running git stash apply [email protected]{1} (the ‘1’ denotes the second before last stash). Here is an example of stashing more than one commit and applying a different stash:

Git diff screenshot

Git stash list screenshot

git stash apply [email protected]{2} has applied the oldest stashed code, when we changed the colour of the text to red.

Git diff screenshot

If you decide not to commit your work once you have restored the stash, you can run git checkout ., which resets all uncommitted code.

As another example of how to use Git stash: say you have some new files, one of which has a bug. Leave all but the suspected bug file unstaged (code must be staged to be stashed), then you can stash that file and troubleshoot the issue. If the stashed file was not the problem, you can restore the stash.

Git status screenshot

Git add screenshot

You can also carry over your stashed commits to a new feature branch or debugging branch by using git stash branch:

Git status screenshot

Note that when you have applied a stash, the stash is not deleted. You can remove stashes individually by using git drop, or remove all stashes by using git stash clear:

Git stash list screenshot

Git Reset

If you do find yourself in the situation where you’ve accidentally committed some messy code, you can do a “soft” reset. This means that the code appears as if it has not been committed yet. Then you can tidy up your code in your IDE before making a cleaner commit. To do this you can run git reset --soft HEAD~1. This will reset the most recent commit. You can reset back more than one commit by changing the number after ~ e.g. git reset --soft HEAD~2.

Git reset screenshot

Git reset is bit more confusing, especially when teaching new Git users. A soft reset should be reserved for a genuine mistake whereas a stash can be used to swap code in and out.

You can also perform a hard reset (git reset --hard HEAD~1). This type of reset essentially erases your last commit. You should be very careful about performing hard resets, especially if you push your branch, as there is no way to restore your commit.

Git Bisect

My favourite Git tool is git bisect. I’ve only needed it a handful of times but when I did, it was invaluable! I primarily used it on large codebases where there was an issue for which nobody found the root cause, even after some vigorous debugging.

git bisect essentially performs a binary search between two given commits and then presents you with a specific commit’s details. You first need to give Git a good commit, where you know your functionality was working, and a bad commit. Note that as long as you have one good commit and one bad, the commits can be years apart (although the farther back in time you go, the more difficult it becomes!).

The most fun thing about git bisect is that you usually don’t really know who wrote the buggy commit when you start. The excitement of finding out where the bug was introduced has more than once caused a few coworkers to huddle around my computer!

To get started, check out the buggy branch and find the good commits. You’ll need to go through your commit history and find the commit hash, then check out that specific commit and test your branch. Once you find a good and bad spot to work from, you can run a git bisect.

In this scenario, the text is red on this website we’ve made (though it could use a UI designer) but we don’t know how or when it was made red. This is a very simple example, in a real-life scenario you’d probably have a much less obvious problem e.g. a form not submitting/functioning.

When we run a git log, we can see the list of commits to pick from.

Git log screenshot

If I open my webpage on the most recent commit hash, the text is red, so I know I have a problem.

Bad commit screenshot.

Now we start the bisect and tell Git we have a bad commit.

Git bisect screenshot

Now we go back in time to try and find a commit where the text was not red. Here I try checking out my first commit…

Git checkout screenshot

…and refreshing the webpage…

Black text website screenshot.

The text is no longer red, so this is a good commit! The newer commit the commit- i.e., the closer to the bad commit, the better:

Git checkout screenshot

Git will now tell you how many commits it has to search before finding the right one. The number of commits Git will traverse depends on how many commits are in between the good and bad commit (the longer length of time, the more times Git needs to iterate).

Now, you’ll need to test your branch again and see if your issue has disappeared. Sometimes this can be a little cumbersome if you regularly update modules, since you may need to reinstall node modules on your front end repository. If there have been database updates you may also need to update these.

git bisect automatically checks out a commit in the middle of your good and bad commits. Here it is estimating one step to find the bad commit.

Git bisect screenshot

Refresh the page, and see if your problem is gone. The issue is still there, so we tell Git that this is still a bad commit. No need to reference the commit hash this time since Git will use the commit you have checked out. We’ll need to repeat this process until Git has traversed all the possible steps.

Git bisect screenshot

Refresh the page, and our issue is gone again, so this is a good commit:

Black text website screenshot.

At this stage Git has found the first bad commit:

Git bisect screenshot

Now we can use git show to show the commit itself and identify the issue:

Git show screenshot

When you’re finished, you can run git bisect reset to reset your branch to its normal working state.

The closer the commits are together, the easier Git will be able to find the problem, but I’ve had it take 10 steps before and still find the bad commit easily. It’s not guaranteed to work, but it has found the issue most of the time for me. Congratulations, now you’re a code archaeologist!

Squashing Your Commits

I previously worked full time on an open source project for a global organisation and I quickly learned how important it is to squash- or combine- your commits. I think it’s an excellent habit to get into, even if your employer doesn’t require it. It’s especially considerate for other developers who will need to review and edit features you have built later.

Why squash your commits?

  • It’s easier for contributors to your repository to read. Imagine if you have a commit list like so:
    • Implement carousel slider
    • Add styling to carousel
    • Add buttons to carousel
    • Fix strange issue in IE with carousel
    • Adjust margins in carousel

    It’s much easier to squash these into a single commit that says “Add carousel to homepage”.

  • It encourages you to keep your commit messages understandable and relevant if every time you make a pull request, you have to squash your commits into one. How many times have you seen a commit titled “WIP”, “bugfix for login page” or “fix typo”. It’s important to have relevant commit names e.g. “Bugfix for #444 login page - fix flicker due to missing $scope function”.

A reason you may not want to squash your commits could be because you are working on a very detailed and lengthy feature, and want to keep a daily history for yourself in case you come across bugs later on. Then your feature is easier to debug. However, when checking your feature into your master branch and you are confident it is bug free, it still makes sense to squash.

In this scenario, I have made five commits, but all of them are related to one feature. My commit messages are also too closely related to merit being separate - all of my commits are about styling the page for this new feature:

Git log screenshot

You can also use git merge --squash, but I think it is clearer to use rebase because when you cherry pick your commits it is easier to see the commit description. If you run a git merge --squash, you first have to do a hard reset of your commits (git reset --hard HEAD~1 ), and it’s easy to get confused with exactly how many commits you need to do this with. I find git rebase to be more visual.

Start by running git rebase -i and your default text editor on the command line will open with your list of commits:

Git squash screenshot

You may just want to squash your last few commits, in which case you could run git rebase -i HEAD~2 and be presented with your last three commits:

Git squash screenshot

Note that if you are only running Git locally, and have not pushed your branch to a remote repository then you will need to add --root to this command i.e. git rebase -i --root. This prevents your branch from being limited by requiring an upstream branch.

Now we can squash all commits into the first commit as shown below.

Git squash screenshot

When you save the file, Git will open your commit message to edit.

Git squash screenshot

While we are doing the rebase, we can also edit the commit description so that it is easier to read.

Git squash screenshot

Save this file again and you’re done! When we take another look at the Git log we can see there is just one clean commit.

Git squash screenshot

Git Rebase

Developers are usually hesitant to use git rebase because they know that a rebase can be used to permanently delete files from your codebase.

As we saw above, git rebase can be used to keep your code and tidy it as well as delete - but what if you really do want to permanently remove a file from history?

I once witnessed a scenario where a member of our development team had accidentally committed a very large file to the codebase. It was part of a much bigger branch so the large file went unnoticed in code review and was mistakenly checked into the master branch. This became an issue whenever anyone wanted to re-clone the repository - it took a very long time to download! And of course, the file in question was unnecessary. It wouldn’t have been a problem if the file was the last commit to the master branch - in that case you could just run a hard reset (git reset --hard~1) and force push the branch.

Likewise, if the file was the only change in a given commit, you could just remove the entire commit by running git reset --hard commit-id. In our scenario, though, the large file was committed alongside other code that we wanted to keep in history, as the second to last commit.

Git log screenshot

Once you find the troublesome commit, check it out using git checkout and the commit hash.

Git checkout screenshot

Remove the file, or edit your code, and leave the code (or files) you want to keep intact.

Git status screenshot

Make sure you run git add -A so that your deleted file is staged and Git knows to remove it. Now run git commit --amend -v and Git will ask you to edit your commit message.

After this, run git rebase --onto HEAD 69aa621184e458f61bf09327c4e104b3e9bc8b6b master with your own commit hash. This is where you could come across some merge conflicts, meaning there is a conflict between your new commit and the old code. Git will ask you to resolve the conflict:

Git add screenshot

If you open the file in your text editor, you’ll see that Git has marked out two versions of the index file. You simply need to remove one or edit one to keep the changes you like.

Git diff of index file.

The code between “«< HEAD” and the line of equals “===” is one version and the code between “====” and “»> Add index file” is the version from the “Add index file” commit. So you can see that one version has the additional paragraph of ‘Toptal connects the top 3% of freelance talent all over the world,’ while the other does not.

Save your edited file and run git add filename followed by git rebase --continue. If there are no changes you can also run git rebase --skip. It can take a while to go through the rebasing if there were a lot of commits between your “large file” commit and the latest commit on master.

Be patient, and if you’re in a large team make sure to get a second opinion! It’s especially important to consult with the person(s) who wrote the commit(s) you are merging, if possible.

Remember that the merge changes are relevant to the specific commit in history, and not your most up to date changes. I.e. if you are editing a commit from when the text on your site was Arial, and it is now Verdana, you should still keep that commit with the history of Arial as the font face.

Note also that if Git sees a space or an end of line character, it can cause a merge conflict, so be careful!

More Than Just Commit and Pull

Git is more powerful than a lot of developers think. If you happen to be introducing a newbie, make sure to give them some tips on these invaluable features. It makes for a more efficient workflow.

Looking to learn more about Git? Take a look at Toptal’s Git Tips and Practices page.