Question

I'm currently doing a massive refactoring of a Java application which splits service classes into service interfaces and service implementations, so for example

package de.abc.def;
public class AuthenticationService {
    public void doStuff() {
        System.out.println("doing stuff");
    }
}

becomes

package de.abc.def;
public interface AuthenticationService {
    void doStuff();
}

package de.abc.impl;
public class AuthenticationServiceImpl implements de.abc.def.AuthenticationService {
    @Override public void doStuff() {
        System.out.println("doing stuff");
    }
}

All this happend in master in several distinct operations (first move AuthenticationService to a new package and rename it, commit the changes, then use refactoring to introduce the interface thereby recreating AuthenticationService.java at the old place, and commit again).
When I now try to merge changes to the service classes made in another branch into master, git creates a lot of merge conflicts - basically I can manually resolve every single change to the service classes. I thought/hoped git would detect the rename operations and merge the changes made to the service classes into the service implementations (they are almost 100% identical to the old service classes), but it seems that git is happy with using the existing file and trying to merge it - failing completely.

Is there a way to force git to consider renames first? Or do I have to rename or move all service interfaces so that git doesn't find a candidate (which is something I'd like to avoid...)?

Thanks

Was it helpful?

Solution

Git merges based on endpoints (vs the merge-base). Let's draw the commit graph:

... - B - R1 - R2   <-- HEAD=master
        \
          S1 - S2   <-- service

Here B is the merge-base (the point at which the two branches diverge), commit R1 renames files, and commit R2 reintroduces the old names.

Meanwhile, commits S1 and S2 are ordinary non-rename-y commits on the service (service classes) branch.

When you are on master and do git merge service, git effectively does this underneath:

git diff -M B R2 > our_changes
git diff -M B S2 > their_changes
# now combine the two sets of changes and make a new commit

The -M tries to detect renames, but is fooled because commit R2 reintroduced the old file names.

There are two obvious (?) ways to make this merge automatically:

  1. find the renames, make the same ones in service, and make a commit S3 there so that the names match up (and then hope git doesn't accidentally detect those in comparing B to S3 and make a mess), or;
  2. do the merge incrementally.

I suspect (without trying it) that method 2 will work much better, plus you already did the work to separate out commits R1 and R2, so you might as well use it.

To do this, check out commit R1 (you can do this as a "detached HEAD" commit, or make a branch for it—let's call it temp, so you do git checkout -b temp master^), then git merge service to get a new merge commit. This time, when git does the diff from B to R1, it should detect the renames, and apply the changes in the service branch to the renamed files (these are of course the old, unrefactored, files but that's pretty much what you have to do anyway, to get the changes to apply). Now the graph looks like this:

               R2      <-- master
             /
... - B - R1 ---- M1   <-- HEAD=temp
        \        /
          S1 - S2      <-- service

You can now merge temp into master by doing git checkout master; git merge temp. The merge-base for this merge is R1: git will look at what changed between R1 and M1, and what changed between R1 and R2, and attempt to bring the R1 to M1 changes in on top of R2, making a new merge commit M2:

               R2 - M2 <-- HEAD=master
             /      /
... - B - R1 ---- M1   <-- temp
        \        /
          S1 - S2      <-- service

Since M1 "captures" the renames (well, we hope), the changes in S1 and S2 are likely to land in the correct (old, pre-refactoring) files now.


The above assumes a specific number of commits, but the general idea is this: help git by choosing an earlier "endpoint" for the merge, where the rename-detection will do what you want. Make a temporary branch for these merge results to go into. Then use the merge result as input to a second merge. (Repeat for more than two merges if needed.) Remember to delete the temp branch names once you're done with them.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top