We've all been there... you have a project in source control and commit your changes thinking everything is all good. But then you realize you've got a typo, or you left a little file around. No big deal, what's one more commit?
Then you notice another...
Or maybe you've got a bunch of changes that you've submitted as a pull request to a repo you've forked previously. Everything seems good, until the owners of the upstream repo ask you to tweak a few things or make a few changes. Now your pretty little pull request that had just 2 commits with your changes well described has many more where the tiny fixup commits are in response to feedback. The owners of the upstream repo might ask you to squash them down into something more manageable to keep the history clean... makes sense right?
So how do you do that? Each tool does it differently, but sometimes you really want to fall bac to the command line where you have the most control.
Let's say you have 5 commits... A through E... get a list of all the commits in a nice clean single line using
git log --oneline:
4baeec1 E 670fb04 D 93a66f6 C 68c91e6 B d8e9f20 A
Let's say that we're good with commit A, but the changes in commits C-E shouldn't be there as individual commits. Let's think of those as the typo fixing commits... the changes within those commits should be included within commit B. So how do you fix this?
Before you do this, make sure you have your git environment setup correctly. When working in the interactive mode, git launches my default text editor. In my case this is Sublime.
The problem is that when I went into the interactive mode, I'd see git in the background run through all the commands and report Successfully rebased and updated refs/heads/master. What you want is for git to wait until you are finished with the changes in the interactive mode before it runs the commands. I found the issue in this StackOverflow answer. You basically want to run the following to tell Sublime to wait to hand control back to it's caller:
git config --global core.editor "subl -n -w"
Compressing Multiple Commits in Git with Rebase
The way you do this is with the git command rebase. You have a few options here but the two I'm interested in are squash & fixup. Squash will merge the changes in the commit as well as append each of those's commits into the target commit. Fixup does the same thing, but it leaves the commit messages behind.
Now, in our case because we want to work with the last 4 commits (B-D), I'm going to tell git that I only want to work with the last 4 commits from the current HEAD pointer:
git rebase -i HEAD~4. That opens something up that looks like the following:
pick 68c91e6 B pick 93a66f6 C pick 670fb04 D pick 4baeec1 E
There is some additional comment information below this but I'm only going to include the important stuff in this post. Notice that unlike the log, the oldest commit is at the top while the most recent commit is at the bottom. The way this works if you change the pick command with what you want to do. So our goal is to take everything in commits C, D & E and put them in B acting like C-E never happened. To do this, change the pick command with fixup or just the first letter like this:
pick 68c91e6 B f 93a66f6 C f 670fb04 D f 4baeec1 E
When you save your changes and close out your text editor, you'll see git apply your changes. Running the
git log --oneline command gives you what you were looking for:
68c91e6 B d8e9f20 A
If you look at the files, you'll notice all the stuff from commits C-D are in the B commit now. Perfect!
Wait... one last thing... let's tweak that last commit message on B to indicate it includes more. Do this by running
git commit --amend -e which says you way to edit the most recent commit. You will go back into the text editor for interactive mode where you can change the commit message to something like B changed. Save & close the text editor. Now the log will show this:
68c91e6 B changed d8e9f20 A
What if you had already pushed your changes up to a remote repo such as in the case of the pull request scenario I mentioned above? No worries, just repush but this time use the
--force flag to overwrite what you have in your forked repo:
git push --force. Works like a champ.
I've created a short video demonstrating this if you are a bit lost. While I did it in OSX, the same works in Windows: