Search code examples
gitgit-rebasegit-reset

Find rebase point


Say I have this:

I1 -- I2 -- I3 
 \            \
  F1 -- F2 -- ? -- F3 -- F4 -- F5

I created a new feature branch, F, I made commit F1 and F2, and then after that I wanted to incorporate changes from the integration branch to prevent conflicts.

So I do:

git fetch origin I
git checkout F
git rebase origin/I

So what it should look like now is something like this:

I1 -- I2 -- I3
               \
               F1 -- F2 -- F3 -- F4 -- F5

my question is - now I want to squash commits F1-F5, into one commit. How can I safely squash the commits?

I can do git reset --soft x, but what is x? How can I find which commit to go back to? I don't want to lose any of the history from the integration branch.

In English, I suppose it would be the oldest commit on my branch that isn't in the integration branch, but also where that commit is younger than any commits from the integration branch. (?) The latter scenario might occur if I do a git merge instead of git rebase (by accident).


Solution

  • TL;DR

    Try git fetch && GIT_SEQUENCE_EDITOR="sed -i '' 's/^pick/squash/'" git rebase -i @{u} (you can squeeze this into an alias, shell or Git-to-shell).

    Long

    Consider the following: When you run git rebase, you tell git rebase where to put the copies. That is, you run:

    git checkout $branch
    git rebase $onto
    

    where $branch is the name of your branch, and $onto is the thing that specifies how to do the rebase. Since the copies go after the commit specified by $onto, $onto is automatically the correct point for your git reset --soft.

    If you have git pull --rebase run git rebase for you, you give up direct control of $onto here. The git pull command first runs git fetch $remote for some $remote, then uses the fetched information that git fetch records (in $GIT_DIR/FETCH_HEAD) as the $onto parameter.

    There is a bit of extra complication here due to the fork-point code, but in modern Git, you have the advantage that git fetch will update $upstream where $upstream is the upstream of the current branch, after which git rebase will use $upstream and do all the fork-point stuff automatically, so that $upstream is still correct. In this case, using:

    . git-sh-setup
    branch=$(git symbolic-ref -q --short HEAD) || die 'not currently on a branch'
    remote=$(git config --get branch.$branch.remote) || die 'current branch has no upstream'
    git fetch $remote && git rebase || die 'fetch or rebase failed'
    git reset --soft $upstream && git commit
    

    as your script (named, e.g., git-rebase-and-squash in your $PATH and run as git rebase-and-squash) would mostly do the trick.

    Rather than using reset --soft and git commit directly, though, you might want to make the last line read:

    GIT_SEQUENCE_EDITOR="sed -i '' 's/^pick/squash/'" git rebase -i $upstream
    

    which would convert all the picks to squashes for you, and in this case you would not even need the earlier git rebase, so that the whole script collapses down to git fetch followed by git rebase -i @{upstream}. (Whether you still want to automate the sequence editing is up to you.)

    If you don't mind slightly misleading error messages, you don't need the checking either. Running git fetch will fetch from origin by default, and @upstream will fail noisily if there is no current branch or if the current branch has no upstream, so this can be reduced to a two-command-long script or alias:

    git fetch && GIT_SEQUENCE_EDITOR="sed -i '' 's/^pick/squash/'" git rebase -i @{u}