gitgit-rewrite-history

How to split a commit into two without introducing merge conflicts?


I had lines like this to change:

service.audit(Message.delete()[…] [long line]

I had to add withMessage(…) to them where that was missing. At the same time I formatted the places where the lines were long:

service.audit(Message.
    .delete()
    .withMessage([…])
    .withUser(…)
    .withPayload(…)
    […]

Now I want to split this commit into two:

  1. Format
  2. Add new method

But if I do a split in the standard way, say with:

I get merge conflicts since I did changes in the same places.


Solution

  • We’re not gonna use git-rebase(1) and/or git-cherry-pick(1) since these turn commits into patches in order to make new commits. That’s why you get merge conflicts.

    Instead you want to make use of the fact that commits are snapshots.

    More concretely:

    #!/usr/bin/env bash
    
    hash=$(git rev-parse @)
    # Safely do `git reset`
    # See: https://stackoverflow.com/a/2659808/1725151
    git diff-index --quiet --cached HEAD -- \
        || { printf "staged changes: refusing to check out"; exit 1; }
    git diff-files --quiet \
        || { printf "unstaged changes: refusing to check out"; exit 1; }
    git reset --soft @~1
    # Manipulate the staged changes
    # Give a shell prompt to pause and allow that
    printf "Stage your changes and then type ‘exit’\n"
    sh
    # You can edit the commit message with an interactive rebase afterwards
    git commit -msplit
    # Add back the original commit
    git restore -SW -s "$hash" -- .
    git commit -C"$hash"
    

    Work backwards instead of forwards

    In this case it helps to keep the state of the starting point and then move one step backwards. Since you have the desired end-state already the only manual work is to remove things from what will become the first commit of the two.

    Contrast with a rebase session where you go back N commits, change to the state you want back at that point, and then deal with merge conflicts (if overlapping) as you apply the changes forwards.

    Use git-rebase(1) for touchups like changing the commit messages

    It’s natural that the commit messages will change after a split. We leave that for later:

    Since we can already change the commit messages easily in a rebase session: there are no possible conflicts any more so we can just focus on that part.

    Generalization

    This script splits the current commit you are on. What if you want to do a split N commits back?

    1. Do an interactive rebase
    2. Mark the commit you want to split for editing (edit)
    3. Do this procedure
    4. git rebase --continue

    (Thanks to j6t)