Determine which commit in the target branch caused a conflict

Viewed 128

When encountering a conflict while rebasing foo onto master is there a way to programmatically determine which commit(s) on master cause the conflict?

To do this manually I would basically find the conflicting lines, checkout master, run git blame and then see which commits changed those lines after the merge base.

Is there a simple way to do that process programmatically?

Questions that this is not a duplicate of

Unlike these other questions I want to know which commits on the target branch cause the conflicts not the commits on the branch that I am rebasing.

3 Answers

Assuming you can abort the rebase to do this test, I would approach it by temporarily creating a new branch on master, try to rebase it onto foo, and extract the commit that first has a conflict during that operation.

#!/bin/bash
# Make these command line parameters if you want
dest=master
source=foo
# Find the conflict commit
git checkout $dest
tmp_branch=$(mktemp -u | sed 's/.*\///') # Not strictly safe, I know, but it works
git checkout -b $tmp_branch
if git rebase $source; then
   conflict_commit=None
else
   conflict_commit=$(git am --show-current-patch | head -1 | sed 's/ *commit *//')
   git rebase --abort
fi
git checkout $source
git branch -D $tmp_branch
# Use $conflict_commit as needed
echo Conflict at "$conflict_commit"

When encountering a conflict while rebasing foo onto master is there a way to programmatically determine which commit(s) on master cause the conflict?

As Tim Biegeleisen noted in a comment, the short answer is no. This is a bit too short since it's an in-theory answer, and in practice there's often a machine-find-able answer. ("In theory, theory and practice are the same. In practice, they are not.")

The root of the theoretical problem is that every commit in Git is just a full snapshot. As such, each commit is sort of idempotent, if I may abuse the terminology a bit. For instance, suppose we have the following sequence of commits:

...--B--C---------G--H   <-- master
         \
          D--E--F   <-- feature

We now decide to rebase feature onto master, i.e., after commit H. We may or may not get a conflict. If we choose to rebase after G instead of H, we may or may not get a completely different set of conflicts.

For instance, suppose commit G consists of reversing the text on every line of every file, and commit H is a revert of G. Then rebasing onto G is virtually guaranteed to have conflicts (no lines will match up), but rebasing onto H will have no conflicts (the snapshot in H matches that in C). The fact—whether happy, sad, or neutral—is that commit G is only historically related to commits C and H; it has entirely different source files.

Of course, we don't normally find a commit that totally messes up every file, followed by one that restores the previous state. We do occasionally find commits that have been reverted, so that we might, with some programmatic test for conflict, find irrelevant commits (such as commit G in our theory-example), but if so, that's usually not a big problem anyway since we'll see them being reverted later. So, if we're in the middle of some rebase, and some commit X has failed to apply, all (hah) we need to do is take this:

...--B--C---------[sequence]   <-- master
         \
          D--X--F   <-- feature

and try the rebase on each of the commits in the sequence. The tricky part is choosing which commits to try, for efficiency and to handle merge bubbles in the sequence (if any exist). A brute force git rev-list --reverse --topo-order feature..master would, however, give us a reasonable starting point: we would now try rebasing commits up through and including X on each commit in order, until one of them fails.

joanis' method makes use symmetry (not strictly guaranteed, especially in the presence of reverts) and might well be more efficient. We could examine the number of commits N that need to be rebased each way and pick whichever one is shorter. In general, though, for efficiency I'd use a bisection approach, as that will usually be able to identify the problem in log2 N steps, where N is the number of potentially-conflicting commits.

The first thing, though, would probably be to get the brute force method working. To avoid disturbing the existing working tree (with the conflicted rebase-so-far in it), I'd suggest that a tool that does this—as far as I know, it has yet to be written—work by creating a detached-HEAD working tree with git worktree add.

If you are in the middle of a rebase, you won't be able to easily call git merge (or perhaps using a separate index file ?).

You may have some luck using git merge-tree : you can successively call

# while rebaseing, REBASE_HEAD points to the commit being replayed :
git merge-tree REBASE_HEAD^ REBASE_HEAD <commit>

for each <commit> in the master branch, and check if the output mentions a conflict.

Related