Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Squash two Git commits in the middle of history without interactive rebase

Tags:

git

git-rebase

I'm in the process of converting an old SVN repository over to Git, which includes trying to get all of the branches/tags in all of the right places. That part is going pretty well, but there are times where I want to add a commit in history in my script that I later want to squash with the next commit. The problem is that I'm not grabbing the commits one by one, but as a large group so I can't squash them as I pull them out of the SVN repository. In the end, my repository looks like this:

* (branch_2, HEAD) commit 5
* commit 4
* commit 3
* SQUASH ME!
* (branch_1) commit 2
* commit 1

I want to be able to squash commit 3 with SQUASH ME!, which is obviously easy with an interactive rebase, but more challenging from within a script. The main problem I seem to have is that while it's easy to checkout branch_1 or any commit before it, it's hard to programmatically ask for the commit after it and it's hard for me to predict how many commits back from branch_2 I need to go. I really want to be able to do something like:

git checkout branch_1+2

Any pointers?

like image 454
Wesley Bland Avatar asked Oct 01 '15 13:10

Wesley Bland


2 Answers

What you are speaking about is not a squash, but a fixup, because the squash will ask you for the commit msg interactively, whereas the fixup uses the commit message from the HEAD commit.

Here's a script that does it without intervention.

The script : /usr/bin/git-fixup

#/bin/bash
# This command fixesup one commit onto his parent
set -e

# We need a commit from the first argument of that command
commit=${1:?No commit given as first argument}
startingbranch=$(git rev-parse --abbrev-ref HEAD)

# Checkout the parent of the asked commit
git checkout "$commit"
git checkout HEAD~

# Merge the commit into it's parent, keeping the commit message of the parent
git merge --squash "$commit"
git add .
git add --update
git commit --amend --no-edit

# Store the current commit
newcommit=$(git rev-parse HEAD)

# Rebase the starting branch onto the new commit
git checkout "$startingbranch"
git rebase "$newcommit"

Use it with

git fixup <commit-id>

For example, if your history is:

ce0e2fd (master, HEAD) commit 4
72ab3c4 commit 3
8150939 commit 2
301c1e1 commit 1

You can do git fixup 72ab3c4 which will merge together "commit 3" and "commit 2" as a commit with the message "commit 2", and place you back on the master branch.

like image 136
edi9999 Avatar answered Oct 23 '22 09:10

edi9999


From git rebase --help:

--autosquash

When the commit log message begins with "squash! ..." (or "fixup! ..."), and there is a commit whose title begins with the same ..., automatically modify the todo list of rebase -i so that the commit marked for squashing comes right after the commit to be modified, and change the action of the moved commit from pick to squash (or fixup).

This option is only valid when --interactive option is used.

That looks like it'll do half of what you want, if the input is in the correct form.

The other half is preventing it launching the interactive editor. Luckily, the editor is configurable, so we can just set it to something harmless.

Try this:

env EDITOR=true git rebase -i --autosquash <from-here>

Setting the editor to true (a tool that simply exits successfully) is enough to convince git to proceed with the default rebase settings, which the auto-squash should have set to something useful.


Alternatively, if --autosquash doesn't do what you want, you can set EDITOR to any script that you like:

env EDITOR=mysquasher.sh git rebase -i <from-here>

The script can do anything you need, but in your case it need only find every line that contains "SQUASHME!", and alter the "pick" on the following line to read "fixup". This can probably be most easily achieved with awk:

#!/bin/bash -e

awk -- 'BEGIN {dosquash=0}
        dosquash==1 {gsub(/^pick/, "fixup"); dosquash=0}
        /SQUASHME/ {dosquash=1}
        {print}' "$1" > /tmp/tmp.$$

mv /tmp/tmp.$$ "$1"
like image 43
ams Avatar answered Oct 23 '22 08:10

ams