sevo
sevo

Reputation: 4609

Repeat code moves to alleviate merge conflicts

When encountering conflicts during merging, git presents file-based conflicts instead of actual conflicts within moved blocks of code (across files).

It's often the case that we have simple modifications within the moved code. To perform the desired merge, we can apply refactoring that happened on the remote branch before performing the merge. This gives us much less and more meaningful conflicts to resolve.

Git can show such moves when given --color-moved option.

How can I reuse this functionality to apply the moves on my branch before the merge?

I'm fine with manually fixing the compilation before proceeding.

Upvotes: 0

Views: 88

Answers (1)

torek
torek

Reputation: 488213

When git merge does its job, it:

  • compares (git diffs) the merge base commit to your current (HEAD) commit, and
  • compares the merge base commit to the tip commit whose hash ID or name you supply.

Conflicts, if any, arise from the results of these comparisons.

Unfortunately git merge's internal merge algorithm does not support moved code: it only works with insert and delete operations.

Fortunately, you don't necessarily have to merge the two branch tips. I think that's what you're getting at here:

To perform the desired merge, we can apply refactoring that happened on the remote branch before performing the merge.

If the refactoring that involves moving code from one file to another is made as commits that, first, just move the code, then make any changes required as a result of the merge, you can merge in two steps.

Concretely, suppose we have this:

          A--B--C--D   <-- branch1
         /
...--o--*
         \
          E--F--G--H   <-- branch2

where branch1 has pre-refactoring code (inherited from merge base commit *) and branch2 has post-refactoring code.

If the refactoring occurs entirely in commit G, and includes moving and changing code from one file to another, you're stuck. But suppose the refactoring happens first with "move code" in commit F, then "change code" in commit G. To merge branch2 into branch1, you run:

git checkout branch1
git merge <hash-of-F>

At this point the merge requires only doing the code-moving, from one file to another, which as you've noted, is easier to merge manually.

Committing the result (after all the hand merging) gives:

          A--B--C--D
         /          \
...--o--*            I   <-- branch1
         \          /
          E--------F--G--H   <-- branch2

Now that that's done, you can do:

git merge branch2

to merge with commit H. The merge base of this operation is commit F, so the two diffs that must be combined are those from F to I, and those from F to H. This incorporates the changes required due to the refactoring and gives you:

          A--B--C--D
         /          \
...--o--*            I-----J   <-- branch1
         \          /     /
          E--------F--G--H   <-- branch2

where commit J is the result you really wanted in the first place.

It's probably wise to mark (via commit message text) both commits F and I as "do not use this commit to build". (You can make I and J, but then use history rewriting to make a final merge K that has parents D and H and the content from J, then force the name branch1 to point to final merge K, to hide the construction process. If you never push the intermediate commits, no one in the future will ever know that you did this—which will make the existence of commit F odder...)

Better tools—in particular, a merge strategy that can handle code motion—would be good, but very hard to write.

Upvotes: 1

Related Questions