2

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.

Timmmm
  • 88,195
  • 71
  • 364
  • 509
  • 1
    The short answer is, no, there is no fool-proof way to do this, because if you were doing a manual rebase then after each commit you might go in and resolve other conflicts, make changes, etc. How can you know what those would be. Could you accept finding the _first_ commit which causes a conflict? – Tim Biegeleisen May 17 '21 at 08:54
  • Yes that's exactly what I want. – Timmmm May 17 '21 at 09:04
  • Also keep in mind how a rebase work. First, the `foo` branch is rolled back until reaching the _common_ commit with `master`. Then, the commits occurring on `master` after that point are applied to `foo` (which cannot cause any conflict). Finally, your new unique commits are applied to the _new_ base (hence the name "rebase") one-by-one. The question here is which of the `foo` commits being applied causes a conflict. The conflict would always be with the HEAD commit of the new base. – Tim Biegeleisen May 17 '21 at 09:07
  • That's not really how it works. It just resets `foo` to `master` and then re-applies the patches from `foo`. And the questions *isn't* which of the `foo` commits causes a conflict! That's what the other StackOverflow questions ask. I want to know which of the **`master`** commits caused the conflict. – Timmmm May 17 '21 at 09:10
  • The conflict is caused by the application of one of the `foo` commits against the new base. I'm not sure what you're asking even makes sense here. – Tim Biegeleisen May 17 '21 at 09:11
  • The conflict is caused because some commit on `master` changed the code to be different from what the patch in `foo` was expecting. I want to know which commit did that. – Timmmm May 17 '21 at 09:21
  • Well maybe `git bisect` could help there, but I don't know how it works on a merge commit, because you would need to tell it which parent to follow going back. – Tim Biegeleisen May 17 '21 at 09:22
  • Yeah I initially considered bisecting but I don't think it's the best approach because conflicting commits can be discontiguous (e.g. imagine if the conflicting commit was reverted). In any case `git blame` can give a decent answer directly which would be much faster. I guess I could distil the question to "how do you do git blame of files with merge markers?". – Timmmm May 17 '21 at 09:27
  • Aha maybe something like this: https://stackoverflow.com/a/41320247/265521 – Timmmm May 17 '21 at 09:27
  • 1
    If I understand the task correctly, the problem seems symmetric to me: I would create a new branch on master, then rebase that branch onto foo. Whichever commit gets a conflict first in that operation is likely the one that is causing foo to have a conflict when it is rebased onto master. This operation should be simple enough to script: create the branch, rebase it, parse the output (this is the flakiest part), abort the rebase and delete the branch. – joanis May 17 '21 at 13:02
  • I think this is a duplicate of this recent question: [How can I find the upstream commit hash that caused a specific conflict?](https://stackoverflow.com/questions/67438870/how-can-i-find-the-upstream-commit-hash-that-caused-a-specific-conflict) – IMSoP May 17 '21 at 13:10
  • 1
    @IMSoP That's a partially duplicate question, but I don't think the answers there really answer the question here. – joanis May 17 '21 at 15:27
  • @joanis I agree that there isn't yet a good *answer*, but I think the *question* is basically the same, and a good answer to one would be a good answer to both. – IMSoP May 17 '21 at 17:07
  • @joanis: That's a really clever solution! – Timmmm May 17 '21 at 19:40

3 Answers3

1

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"
joanis
  • 10,635
  • 14
  • 30
  • 40
1

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.

torek
  • 448,244
  • 59
  • 642
  • 775
  • Yes I'm aware that a theoretically correct solution is more or less impossible (e.g. due to commits that revert conflicting changes). But you can still have a good guess. – Timmmm May 17 '21 at 19:44
0

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.

LeGEC
  • 46,477
  • 5
  • 57
  • 104