54

I have a very messy git history. I want to squash a bunch of older commits (not including the last one).

I know how to squash my last n commits. But this is different. Here I have commits consecutively n1 to n2 which I want to squash into one, while after n2 I have a history of commits that I want to preserve up to the last one.

So, if my current history looks like this:

---- n1 --- n2 -------- m

I want to squash n1 to n2 so it ends up looking like this:

---- n1n2 -------- m

where n1n2 is a single commit containing the squashed contents from n1 to n2.

How should I do this? What are the consequences on the history from n2 to m?

That is, will the hash of every commit from n2 to m change as a consequence of what I want to do?

5
  • 1
    1. Interactive rebase, see git-scm.com/book/en/v2/Git-Tools-Rewriting-History. 2. Every commit from n1, which is also rewritten, yes.
    – jonrsharpe
    Commented May 22, 2019 at 18:12
  • @jonrsharpe Just to confirm, ALL commits fromn2+1 to m will also get new hashes?
    – a06e
    Commented May 22, 2019 at 23:04
  • 1
    Yes, because they all have new parent commits now.
    – jonrsharpe
    Commented May 23, 2019 at 6:26
  • @jonrsharpe So commits 1,...,n1 will preserve their hash, even if some of them are included in the interactive rebase but with pick.
    – a06e
    Commented May 23, 2019 at 12:14
  • Not necessarily, a few things go into the commit including a timestamp: stackoverflow.com/a/28917694/3001761.
    – jonrsharpe
    Commented May 23, 2019 at 12:17

2 Answers 2

61

You can do an interactive rebase, per the docs and this blog post.

  1. Start an interactive rebase:

    git rebase -i HEAD~n
    

    (where n is how far do you want to go back in history)

  2. Your default editor will open. At the top, a list of your latest n commits will be displayed, in reverse order. Eg:

    pick a5f4a0d commit-1
    pick 19aab46 commit-2
    pick 1733ea4 commit-3
    pick 827a099 commit-4
    pick 10c3f38 commit-5
    pick d32d526 commit-6
    
  3. Specify squash (or the shortcut s) for all commits you want to squash. E.g.:

    pick a5f4a0d commit-1
    pick 19aab46 commit-2
    squash 1733ea4 commit-3
    squash 827a099 commit-4
    pick 10c3f38 commit-5
    pick d32d526 commit-6
    

    Git applies both that change and the change directly before it and makes you merge the commit messages together.

  4. Save and exit.

  5. Git will apply all changes and will open again your editor to merge the three commit messages. You can modify your commit messages or leave it as it is (if so, the commit messages of all commits will be concatenated).

  6. You're done! The commits you selected will all be squashed with the previous one.

4
  • 13
    Just to confirm. In this case example you put, commits 2, 3, 4 will be squashed into a single commit, let's call it commit 2*. My new history will look contain the commits 1, 2*, 5, 6. In addition, 1 will preserve its old SHA-1, but 2*, 5, 6 will all get new SHA-1 hashes. Am I correct?
    – a06e
    Commented May 22, 2019 at 23:08
  • 7
    @becko correct -- commit 2* gets a new hash because the contents of the commit are now different; commits 5 and 6 get a new hash because their parent commit IDs have changed. For more details: stackoverflow.com/a/32854964/11298742
    – ludovico
    Commented May 23, 2019 at 12:22
  • And then how do I push this, if the commits I have just squashed were already pushed to remote? After editing the commit message, I see "Your branch and 'origin/main' have diverged, and have 4 and 10 different commits each, respectively. (use "git pull" to merge the remote branch into yours) Commented Jul 3, 2022 at 18:42
  • @pretzlstyle an interactive rebase changes git commit history. If you have already pushed and you're working with other developers, you should avoid modifying git commit history as it can have important implications. If you understand those implications, --force would be an option (Warning: "Do not use the --force flag unless you’re absolutely sure you know what you’re doing")
    – ludovico
    Commented Jul 20, 2022 at 13:57
2

I wrote a bash function to automatically mark commit n2 and all commits between n1 and n2 as fixup. This way you don't have to manually mark every commit with fixup. It supports a DRY_RUN argument that prints the contents of the "interactive rebase file", so you can play around with the values of START and END. START and END indicate the index of the commits: e.g.: If this is the file you want

pick a5f4a0d commit-1
pick 19aab46 commit-2
fixup 1733ea4 commit-3
fixup 827a099 commit-4
pick 10c3f38 commit-5
pick d32d526 commit-6

START=3 and END=4.

The code:

function squash() {
# example use:
# DRY_RUN=1 START=2 END=4 squash HEAD~4
# START=2 END=4 squash HEAD~4
  mkdir -p /tmp/squash
  if [ -n "${DRY_RUN}" ] && [ "${DRY_RUN}" -eq 1 ]
  then
    echo "
sed  -i \"\"  "${START},${END}s/^[^[:space:]]*/fixup/" \$1
echo 
echo ***START***
echo \"\"
cat \$1
echo \"\"
echo ***END***
echo The error message \"error: there was a problem with the editor squash\" is normal when running DRY_RUN=1
exit 1 # force exit to not actually rebase
    " > /tmp/squash/squash
  else
    echo "
sed  -i \"\"  "${START},${END}s/^[^[:space:]]*/fixup/" \$1
    " > /tmp/squash/squash
  fi
  chmod +x /tmp/squash/squash
  OLDPATH=${PATH}
  export PATH="/tmp/squash:${PATH}"
  GIT_SEQUENCE_EDITOR=squash git rebase -i "$@"
  export PATH=${OLDPATH}
}
1
  • I'm on a mac, if anyone can confirm the sed is working on Linux that would be nice. Commented Jan 17 at 10:47

Not the answer you're looking for? Browse other questions tagged or ask your own question.