jpimentel jpimentel - 2 months ago 6x
Git Question

Github can't merge branch, no conflicts, manually it auto-merges

I have a pull request that Github says it can't automatically merge. This change is behind master a few commits, but without conflicts.

Manually merging I don't get any conflicts, and I get this output:

(on master)
$git merge otherbranch
[vim pops up for commit message, :wq]
Auto-merging <file>
Merge made by the 'recursive' strategy.
<file> | 1 +
1 file changed, 1 insertion(+)

Is that why Github can't merge automatically? It merged automatically from the command line anyway. Is this not automatic enough for Github?


The reason is that git merge uses a strategy called "recursive" merging by default. It is able to perform 3-way merging: taking the patch from one side, and applying that patch to the other side, to produce a new file. It also does this recursively, in more complicated situations where a lot of branching and merging has been happening and there are multiple merge bases for the two commits being merged.

I recently encountered this same situation:

$ git merge-base --all 90d64557 05b3dd8f

With 2 merge bases, a 3-way merge is not possible. The 'recursive' strategy therefore resolved this by first recursing into a merge of those two commits:

$ git merge-base 711d1ad9 f303c596

OK, only one merge base, so the 3-way merging can begin. what's the changes on either side?

$ git diff --stat 3f5db594 711d1ad9
 normalize/      | 116 ++++++++++++++++++++++++++++++++++++++-----------
 normalize/   |  49 ++++++++++-----------
 tests/ |  10 +++--
 3 files changed, 120 insertions(+), 55 deletions(-)
$ git diff --stat 3f5db594 f303c596
 normalize/  | 38 +++++++++++++++++++++++++++++++-------
 tests/ |  2 ++
 2 files changed, 33 insertions(+), 7 deletions(-)

These two diffs both made changes to the same file, so they can't be resolved using a simple strategy of just taking the newer version of each file from each side (an index merge). Instead, git takes the new file from one side, and tries to apply the patch from the other side to it. The result is a combined commit, which can be simulated like this:

$ git checkout -b tmp
Switched to a new branch 'tmp'
$ git reset --hard f303c59666877696feef62844cfbb7960d464fc1
HEAD is now at f303c59 record_id: throw a more helpful error if record identity is not hashable
$ git merge 711d1ad9772e42d64e5ecd592bee95fd63590b86
Auto-merging tests/
Merge made by the 'recursive' strategy.
 normalize/      | 116 ++++++++++++++++++++++++++++++++++++++-----------
 normalize/   |  49 ++++++++++-----------
 tests/ |  10 +++--
 3 files changed, 120 insertions(+), 55 deletions(-)
$ git diff --stat 3f5db594 HEAD tests/
 tests/ | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

It then returns to the original 3-way merge, using this merge result as its starting point; and this also involves changes to the same file:

$ git diff --stat HEAD 90d64557| grep selector
 normalize/          |  17 +--
 tests/         |  19 ++--
$ git diff --stat HEAD 05b3dd8f| grep selector
 normalize/  | 29 +++++++++++++++++------------
 tests/ |  9 +++++++++

However again the changes are to different sections of the file, and so taking the diff and applying it to the other side is successful.

So, C git was able to resolve this merge by first doing a 3-way merge on the two merge bases of the two starting points, and then doing another 3-way merge on the two original commits being merged and the intermediate result of the first merge.

Github's automatic resolution doesn't do this. It's not necessarily incapable, and I'm not sure how much of the recursive strategy it does implement, but it is erring on the side of caution, which is exactly what you expect a big green button like that to do :-).