Automatic merge conflict resolution for git-rebase(1)

Suraj N. Kurapati

  1. Problem
    1. Solution
      1. ~/bin/git-rebase-autocon

      Problem

      Let’s say you have a copy of some software (a clone of a Git repository, to be precise) that you’ve modified in some way, locally. Some time later, that software is changed upstream, thereby rendering your local copy out of date. How can you bring your local copy up to date while also retaining your local modifications?

      The answer is to use git-rebase(1). However, if your local modifications conflict with any upstream changes, then you’ll need to resolve those conflicts manually: a cumbersome process that can be daunting for inexperienced users.

      This is particularly problematic for configurable software distributed through Git, such as my text editor and window manager configurations, where the occasional need for manual conflict resolution greatly impedes users who aren’t technically inclined or lack the time necessary to manually resolve such conflicts.

      Solution

      The following shell script automates conflict resolution for git-rebase(1) by simply setting aside local modifications that conflict with upstream changes and by offering users a means of easily undoing such actions, through git reset --hard.

      Afterwards, the user may choose to inspect the automatically set-aside conflicts, recorded in empty-tree commits labeled “fixup!”, and restore portions thereof: thereby enacting manual conflict resolution, but willingly performed at one’s leisure.

      ~/bin/git-rebase-autocon

      #!/bin/sh -e
      #
      # Usage: git-rebase-autocon [TARGET] [ARGUMENTS_FOR_GIT_REBASE...]
      #
      # Rebases the given TARGET while automatically resolving conflicts
      # by substituting empty-tree commits labeled "fixup!" that log all
      # conflicting hunks in their commit messages in git-diff(1) format.
      #
      # If TARGET is not specified, the upstream tracking branch is used.
      # Optional ARGUMENTS_FOR_GIT_REBASE... are passed to git-rebase(1).
      #
      # Written in 2010 by Suraj N. Kurapati <https://github.com/sunaku>
      # Documented at <https://sunaku.github.io/git-rebase-autocon.html>
      
      # ensure working tree is clean
      git rebase HEAD --quiet
      commit=$(git rev-parse --short HEAD)
      
      # parse command-line arguments
      test $# -gt 0 && target=$1 && shift || target='@{u}'
      target=$(git name-rev --name-only "$target")
      target=${target#remotes/}
      
      # rebase target and ensure that only merge conflicts made it fail
      git rebase --fork-point "$target" "$@" && exit || test -d .git/rebase-apply
      
      # solve merge conflicts by absorbing leftover commits from prior
      # versions of the target or by setting aside conflicting commits
      trap 'git rebase --abort' TERM INT
      while test -d .git/rebase-apply; do
        headline=$(head -1 .git/rebase-apply/final-commit)
        conflict=$(cat .git/rebase-apply/original-commit)
        shortish=$(git rev-parse --short "$conflict")
      
        # in place of each conflicting commit, record an empty commit whose
        # message contains the changes introduced by the conflicting commit
        git reset --mixed --quiet # empty the index so we can make a commit
        git commit --allow-empty --reuse-message="$conflict" --quiet
        {
          printf 'fixup! %s %s\n\n' "$shortish" "$headline"
          git format-patch --stdout "$conflict~..$conflict"
        } |
        git commit --amend --allow-empty --file=-
      
        git rebase --skip >/dev/null 2>&1 || :
      done
      
      cat <<END
      
          +-------------------------IMPORTANT!----------------------------+
          |                                                               |
          | Some of YOUR COMMITS WERE SET ASIDE to solve merge conflicts: |
          | empty commits labeled as "fixup!" have now taken their place. |
          | But rest assured, THEY STILL EXIST in Git history at $commit, |
          | and your working tree has all changes from those commits too! |
          |                                                               |
          | You can UNDO THIS REBASE at any time by running this command: |
          |                                                               |
          |                  git reset --hard $commit                     |
          |                                                               |
          | You can SEE WHAT CHANGED from before by running this command: |
          |                                                               |
          |                      git diff $commit                         |
          |                                                               |
          +-------------------------IMPORTANT!----------------------------+
      
      END
      

      Updates