2

I'm updating an old feature branch with upstream and there's lots of merge conflicts. Resolving the conflicts in-place is not safe because I cannot reason about the actual change using only the text diff.

I need to find the commit in the upstream branch that made the change so that I can incorporate the update to my branch manually, considering the semantic differences (and to figure out what other parts of the branch need an adjustment).

How can I find such commit for each conflicting chunk?

  • It sounds like what you need is use `merge.conflictStyle=diff3` so that you are able to see what the _common ancestor_ looks like, beside the 2 tips. Try `git merge -c merge.conflictStyle=diff3 the-other-branch` – eftshift0 May 07 '21 at 16:56
  • Just in case, git does _not_ save metadata about conflicts when merging. In order to know if there is a conflict at a merge, you need to actually merge the parents to see. – eftshift0 May 07 '21 at 16:57
  • While `diff3` does help to see the original text, it doesn't point to the commit that actually made the conflicting change in upstream. The `||||||` marker doesn't give a useful pointer either. –  May 07 '21 at 17:09
  • I never said you would get the revision where it conflicted. Or the revisions that modified the content to what you have in both tips. Neither of those will be provided by git _out of the box_. If you want to know when/how code was changed, you might have to blame the file on both branches to know. And if there was a conflict before, git can't provide it. It's not info that is saved on a merge. You would have to re-attempt to merge (and you would need to know what to merge) again. – eftshift0 May 07 '21 at 17:13
  • Hmm.... if you want to know how code was changed in upstream, you _might_ want to try checking out the upstream branch and merge the local (or any other branch that you are working with). Then, in a conflicted file, if you run `git blame`, git will _probably_ provide you with history of the lines in upstream. – eftshift0 May 07 '21 at 17:15
  • Another way you _might_ want to try is to use a script I did to try to tackle this problem. https://github.com/eantoranz/difflame Run it against the two branches you are merging and you should get the revisions involved (no guarantees, though). – eftshift0 May 07 '21 at 17:16
  • Now that you mentioned it, git blame/log `-L` flag does a good job here, even though it's not completely convenient to use for this whole scenario. –  May 07 '21 at 17:26
  • This more recent, similar question has answers that might be relevant here too: https://stackoverflow.com/q/67566615/3216427 – joanis May 18 '21 at 13:39

3 Answers3

3

When a merge base is so far back the changes have become soup and you can't separate out what's what, there's git imerge, which automates walking forward from the merge base step by step, stopping at the pairs that introduce conflicts along the way. It's not often needed, but when you need it it does a whole lot of note-taking and note-consulting for you.

jthill
  • 55,082
  • 5
  • 77
  • 137
  • imerge is too big of a hammer. I think we can divide this into a few subtasks: (1) apply the merge (2) extract conflicting line ranges (3) run git log/blame -L (4) extract relevant commits. –  May 07 '21 at 18:37
  • `git blame` and `git log -L` on many files and long histories can involve a hell of a lot of diffing, how sure are you that winds up cheaper? If you don't have many files or long histories, I'd think `git log --first-parent -p` for the involved files on each tip would be enough to get the correct resolutions easier to sort out. – jthill May 07 '21 at 20:01
1

You could flip the problem around: instead of merging the feature branch into upstream, try rebasing it onto upstream.

A merge will apply all changes at once. Specifically, it will find the last common ancestor of the two branches, and compare each branch to that. The merge algorithm is then looking at those two changes, and doesn't care about commits in between. The question "which commit caused this conflict" has no meaning to the merge algorithm.

A rebase will instead apply each commit in turn, attempting to create a new commit as though you'd made the same changes based on the specified upstream. If a commit causes conflicts, it will stop at that particular commit for you to fix it, and the conflict view will show you the changes from that commit only. Once you've fixed it, running git rebase --continue will attempt to apply the next commit, and either succeed or stop again for you to fix more conflicts.

The downside is that you may have to fix very similar conflicts multiple times, or fix conflicts in code that wouldn't actually have conflicted in a straight merge. For instance, if you made a conflicting change and then reverted it in a later commit, it won't cause a conflict in a merge, but it may cause two conflicts with rebase (the first recreating the original change; and the second recreating the revert).

Once you've got a fully rebased branch, you can merge it into upstream with a guarantee that there will be no conflicts, because all the commits on upstream are already incorporated in its history.

IMSoP
  • 89,526
  • 13
  • 117
  • 169
  • rebase would be useful if you want to find the commit in the feature branch. I'm looking for the commit in the upstream branch. –  May 07 '21 at 17:25
  • 1
    @Alireza Hm, I see what you mean. I guess you could do the rebase in reverse, but not actually push the result anywhere - something like `git checkout upstream; git switch -c temp; git rebase feature` – IMSoP May 07 '21 at 17:27
  • @Alireza Or... I haven't quite figured out the details, but could you brute force the problem by rebasing or merging onto each commit of upstream in turn, and seeing which ones produced conflicts? – IMSoP May 07 '21 at 17:30
  • A reverse rebase is a pretty clever idea, however, it doesn't help in the process of the actual merge, so you'll have some back and forth to reach a stable state. –  May 07 '21 at 17:47
  • Also it only helps to find the first conflicting commit. You can't continue the rebase until you resolve it. –  May 07 '21 at 18:01
0

After investigating suggested solutions, I decided to reverse the merge by starting fresh on the tip of upstream in a new commit and merging in the feature branch with --squash. That way the merge is based on the upstream changes and you can better decide to how resolve conflicts or go with ours and adjust the code in a follow-up commit.