Automatic merge conflict resolution for git-rebase(1)
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