How do I squash two non-consecutive commits?

You can run git rebase --interactive and reorder D before B and squash D into A.

Git will open an editor, and you see a file like this, ex: git rebase --interactive HEAD~4

pick aaaaaaa Commit A
pick bbbbbbb Commit B
pick ccccccc Commit C
pick ddddddd Commit D

# Rebase aaaaaaa..ddddddd onto 1234567 (4 command(s))
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

Now you change the file that it looks like this:

pick aaaaaaa Commit A
squash ddddddd Commit D
pick bbbbbbb Commit B
pick ccccccc Commit C

And git will now meld the changes of A and D together into one commit, and put B and C afterwards. When you don't want to keep the commit message of D, instead of squash, you would use the fixup keyword. For more on fixup, you can consult the git rebase docs, or check out this question which has some good answers.


Note: You should not change commits that have been pushed to another repo in any way unless you know the consequences.

git log --oneline -4

D commit_message_for_D
C commit_message_for_C
B commit_message_for_B
A commit_message_for_A

git rebase --interactive

pick D commit_message_for_D
pick C commit_message_for_C
pick B commit_message_for_B
pick A commit_message_for_A

Type i (Put VIM in insert mode)

Change the list to look like this (You don't have to remove or include the commit message). Do not misspell squash!:

pick C commit_message_for_C
pick B commit_message_for_B
pick A commit_message_for_A
squash D

Type Esc then ZZ (Save and exit VIM)

# This is a combination of 2 commits.
# The first commit's message is:

commit_message_for_D

# This is the 2nd commit message:

commit_message_for_A

Type i

Change the text to what you want the new commit message to look like. I recommend this be a description of the changes in commit A and D:

new_commit_message_for_A_and_D

Type Esc then ZZ

git log --oneline -4

E new_commit_message_for_A_and_D
C commit_message_for_C
B commit_message_for_B

git show E

(You should see a diff showing a combination of changes from A and D)

You have now created a new commit E. Commits A and D are no longer in your history but are not gone. You can still recover them at this point and for a while by git rebase --hard D (git rebase --hard will destroy any local changes!).


For those using SourceTree:

Make sure you haven't already pushed the commits.

  1. Repository > Interactive Rebase...
  2. Drag D (the newer commit) to be directly above A (the older commit)
  3. Make sure commit D is highlighted
  4. Click Squash with previous

Interactive rebase works well until you have big feature branch with 20-30 commits and/or couple of merges from master or/and fixing conflicts while you was commiting in your branch. Even with finding my commits through history and replacing pick with squash doesn't worked here. So i was looking for another way and found this article. I did my changes to work this on separate branch:

git checkout master
git fetch
git pull
git merge branch-name
git reset origin/master
git branch -D branch-name
git checkout -b branch-name
git add --all
#Do some commit
git push -f --set-upstream origin branch-name

Before this I got my pull request with about ~30 commits with 2-3 merges from master + fixing conflicts. And after this I got clear PR with one commit.

P.S. here is bash script to do this steps in automode.