0

We have a rather complicated situation in our Git repository. We have two teams working on the same software. One team is doing maintenance on the 3.0 version, while another team is doing a long running project, which will lead to version 3.1 to be released anytime soon.

The 3.1 team started off on their own branch, and they decided to merge all changes from the 3.0-branch, every time there was a new version released from the 3.0 branch, wichs is typically every other week. So when the 3.0 team realeased their first version, commit R1 was made and that was merged into 3.1, resulting in M1.

  3.1---o---o---M1
 /             /
3.0---o---o---R1

Two weeks later, the 3.0-team released their second version at R2, and the 3.1 team merged that, resulting M2.

  3.1---o---o---M1---o---o---M2
 /             /            /
3.0---o---o---R1---o---o---R2

This went on for a while, but eventually the 3.0 team did a refactoring, and released this in commit R3. In this refactoring, the 3.0-team split up a large Constants.java-file, into two smaller files: Constants.java and MoreConstants.java. (These are not the actual file names.) The Constants.java file was previously changed by both teams and merged succesfully. However, this refactoring caused a conflict for the 3.1 team, when they tried merging R3 into their branch. The 3.1 team decided they didn't want to solve that conflict and they thought they could "postpone" the solving by ignoring the refactoring, sticking with their own version. This meant not only that they didn't add MoreConstants.java to their repository, but also that they didn't merge the changes in files that were now importing MoreConstants.java instead of Constants.java. What they did was:

$ git merge 3.0
$ git status

And then, for each of the listed conflicts, they decided if they wanted to solve it or not. If they didn't want to solve de conflict, they did

$ git checkout --ours <file they wanted to ignore>
$ git add <file they wanted to ignore>

This resulted in commit C3 on the 3.1-branch:

  3.1---o---o---M1---o---o---M2---o---o---C3
 /             /            /            /
3.0---o---o---R1---o---o---R2---o---o---R3

From that point on, they kept doing the "--ours"-trick every time, as there were conflicts they wished to ignore every time. It is also important to notice that the 3.1-team kept making changes to the Constants.java-file themselves. So over time, the situation became something like this:

  3.1---o---o---M1---o---o---M2---o---o---C3---o---o---C4---o---o---C5
 /             /            /            /            /            /
3.0---o---o---R1---o---o---R2---o---o---R3---o---o---R4---o---o---R5

Now both branches had different versions of Constants.java, with one shared ancestor, which is a commit somewhere before R2. At this point, we can see what that ancestor is, by running

$ git merge-base --all C5 R5

Now comes the complicated part. The 3.1-team is nearly finished creating version 3.1 of the product. The 3.0-team will be taking over. Immediately after 3.1 is released, the 3.0-team will release 3.2. So, a new 3.2-branch was created off of the 3.0-branch:

  3.1---o---o---M1---o---o---M2---o---o---C3---o---o---C4---o---o---C5
 /             /            /            /            /            /
3.0---o---o---R1---o---o---R2---o---o---R3---o---o---R4---o---o---R5
                                                                   \
                                                               3.2  A5

Of course, version 3.2 needs to have all changes of both 3.0 and 3.1 incorporated. So we did a merge:

$ git checkout 3.2
$ git merge 3.1

Leading to:

  3.1---o---o---M1---o---o---M2---o---x3--C3---o---o---C4---o---o---C5
 /             /            /            /            /            /  \
3.0---o---o---R1---o---o---R2---o---o---R3---o---o---R4---o---o---R5   \
                                                                   \    \
                                                               3.2  A5---A6

And now for the nasty surprise: the 3.0 team was missing lots of fixes in the A6 commit. It turns out that everything that was missing, could somehow be related to the Constants.java-refactoring. Of course the new MoreConstants.java is missing in 3.2, but also lots of changes on files that imported the new MoreConstants.java were missing on the 3.2-branch.

After reading How to revert a merge which used strategy=ours? and How to revert a faulty merge (especially the ADDENDUM), I think I do understand why that is: the 3.1-team solved a lot of conflicts by choosing "ours". That way, in fact they told Git that the conflicting changes from 3.0 were "wrong". So that's effectively some form of reverting a merge, as referred to in the mentioned documents. Be it that not the entire merge is reverted, but just a part of it.

Both documents contain a lot of clues on how to fix this. However, both focus on fixing one faulty merge. In this simplified example, we're talking about three faulty merges. And in reality, we have, I guess, 15 to 20 faulty merges.

I know that the 3.1-team shouldn't have ignored the conflicts, but they did. So we have to find a solution. The only things I could come up with so far are:

1. A lot of cherry picking

We could cherry-pick each and every commit on the 3.1-line to the 3.2-line, except the commits that were merges. Sounds simple, but as we have about 20 faulty merges in reality and there are far more commits in between merges than displayed in the simplified graphs, we're talking about hundreds of commits to cherry pick. Doing this by hand is tedious and error prone. I think this solution is only viable if there's a way to generate a list of all commits to cherry pick and feed that list to a (series of) git command(s) automatically.

2. Lots of rebases

A series of consecutive rebases. Given the number of faulty merges, this is still error prone and tedious, but perhaps a little less so than the previous suggestion. On the pro side, this probably leads to a cleaner history. I think the process would go as follows:

$ git checkout x3
$ git rebase --no-ff 3.0

  3.1---o---o---M1---o---o---M2---o---x3--C3---o---o---C4---o---o---C5
 /             /            /            /            /            /  \
3.0---o---o---R1---o---o---R2---o---o---R3---o---o---R4---o---o---R5   \
 \                                                                 \    \
  \                                                            3.2  A5---A6
   \
    3.1'--o--o--M1'--o--o--M2'---o---x3'

$ git merge R3

  3.1---o---o---M1---o---o---M2---o---x3--C3---o---x4--C4---o---o---C5
 /             /            /            /            /            /  \
3.0---o---o---R1---o---o---R2---o---o---R3---o---o---R4---o---o---R5   \
 \                                      \                          \    \
  \                                      \                     3.2  A5---A6
   \                                      \
    3.1'--o--o--M1'--o--o--M2'---o---x3'---D3

$ git checkout x4
$ git rebase --no-ff R3

  3.1---o---o---M1---o---o---M2---o---x3--C3---o---x4--C4---o---o---C5
 /             /            /            /            /            /  \
3.0---o---o---R1---o---o---R2---o---o---R3---o---o---R4---o---o---R5   \
 \                                      \ \                        \    \
  \                                      \ C3'--o--x4'         3.2  A5---A6
   \                                      \     
    3.1'--o--o--M1'--o--o--M2'---o---x3'---D3   

$ git merge D3

  3.1---o---o---M1---o---o---M2---o---x3--C3---o---x4--C4---o---x5--C5
 /             /            /            /            /            /  \
3.0---o---o---R1---o---o---R2---o---o---R3---o---o---R4---o---o---R5   \
 \                                      \ \                        \    \
  \                                      \ C3'--o--x4'--D4     3.2  A5---A6
   \                                      \             /
    3.1'--o--o--M1'--o--o--M2'---o---x3'---D3----------o   

Etcetera...

3. Generating patches

A third solution might be to generate "patches", containing only the changes made between certain commits. As we visualize the tree one more time:

  3.1---o---o---M1---o---o---M2--y2--o--x3--C3--y3--o--x4--C4--y4--o--x5--C5
 /             /            /              /              /              /  
3.0---o---o---R1---o---o---R2--o---o--o---R3--o---o---o--R4--o---o---o--R5   
                                                                         \    
                                                                     3.2  A5

This would mean creating patches containing all changes made in the following "ranges":

  • [y2 - x3],
  • [y3 - x4] and
  • [y4 - x5].

And then, instead of doing a merge, we could apply those patches on top of A5.

The question

So these are my questions:

  1. What's the best solution to this problem. It might be (a variant on) one of my proposed solutions, of something completely different.
  2. Is there a way to do solution 1 in a (more or less) automated way, to prevent errors and to speed things up?
  3. Is the plan sketched in solution 2 going to work? Or am I missing something here?
  4. What about solution 3? Would there be a way to automate that process?
Community
  • 1
  • 1
Bart Kummel
  • 662
  • 12
  • 18

1 Answers1

1

We could cherry-pick each and every commit on the 3.1-line to the 3.2-line, except the commits that were merges. ... I think this solution is only viable if there's a way to generate a list of all commits to cherry pick and feed that list to a (series of) git command(s) automatically.

You can pass a range of commits to cherry-pick, e.g. git cherry-pick 16234..351be, and you can also specify -m 1 to only follow the first parents, which allows you to pass correctly over merge commits, so you don't need to skip them. As you solve merge conflicts at each step, they should be solved going forward, so hopefully you'll just hit a regular merge conflict at each conflicting step, and things will be a lot more automated and less error-prone.

Gary Fixler
  • 5,632
  • 2
  • 23
  • 39
  • Thanks for the reply. I'm not sure if I understand exactly why this would work, but we're going to give it a try! – Bart Kummel Feb 26 '14 at 10:41
  • Good luck! I'm curious to hear if it does work. I *think* I followed along with what was going on in your repo over that long post, but there's certainly a chance I'm mistaken. – Gary Fixler Feb 26 '14 at 19:30
  • Ok. So I couldn't find time to dive into this for a while. But eventually, I found the time to finally resolve this. I went for the cherry-picking solution. However, the suggested `-m 1` option did not work out that good for us. The point is that git expects every commit within the range to be a merge commit, when you use `-m 1`. So I ended up identifying all merges and then cherry-picking all commits in between. So I did a lot of `git cherry-pick -x y2^..x3`. – Bart Kummel Mar 18 '14 at 13:30