Reputation: 4609
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
Reputation: 488213
When git merge
does its job, it:
git diff
s) the merge base commit to your current (HEAD
) commit, andConflicts, 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