How do criss-cross merges arise in Git?
Asked Answered
S

4

9

I have been going through the, "git merge-base", man page and I can't understand how multiple merge bases develop. Specifically, I'm hung up on the following illustration in the man page:

When the history involves criss-cross merges, there can be more than one best common
ancestor for two commits. For example, with this topology:
  ---1---o---A
      \ /
       X
      / \
  ---2---o---o---B
both 1 and 2 are merge-bases of A and B. Neither one is better than the other (both
are best merge bases). When the --all option is not given, it is unspecified which
best one is output.

I just can't understand how such a situation could be created. I have attempted to recreate this criss-cross merge situation between branches using a test repository but I cannot replicate it. In all cases, I always wind up with 1 merge commit to which both A and B point to (instead of A and B pointing to independent merge commits as the diagram illustrates).

Can anyone illustrate how this situation can arise? Is this a common situation or an error situation?

Sterilize answered 14/10, 2014 at 20:57 Comment(2)
Related concept: the "Foxtrot Merge". Also see this SO questionHolmium
Also related: How do I create a criss-cross merge successfully in Git?.Determine
B
12

I just can't understand how such a situation could be created. I have attempted to recreate this criss-cross merge situation between branches using a test repository but I cannot replicate it. In all cases, I always wind up with 1 merge commit to which both A and B point to (instead of A and B pointing to independent merge commits as the diagram illustrates).

Can anyone illustrate how this situation can arise?

Criss-cross merges can arise in different ways. On example that springs to mind is when you have two branch references pointing to the same merge commit (one of those branches being checked out) and you run git commit --amend. The following toy example does just that:

# set things up
cd ~/Desktop
mkdir crisscross
cd crisscross
git init

# make an initial commit
printf "bar\n" > README.md
git add README.md
git commit -m "add README"

# add a line at the top of the file and commit
sed -i '' '1s/^/foo\n/' README.md
git commit -am "add foo line"

# create and checkout a branch pointing at the initial commit
git checkout -b other master^

# add a line at the bottom of the file and commit
printf "baz\n" >> README.md
git commit -am "add baz line"

# do a merge and make both branches point to this merge commit
git merge master
git checkout master
git merge other

At this stage, the output of git log --graph --oneline --decorate --all is

*   30b9175 (HEAD, master, other) Merge branch 'master' into other
|\  
| * f318ead add foo line
* | 31875d9 add baz line
|/  
* 8b313a0 add README

Now amend the last commit (see How does git commit --amend work, exactly? for more details):

git commit --amend

After that, the output of git log --graph --oneline --decorate --all will be

*   69bbcbe (HEAD, master) Amended merge commit
|\  
| | *   30b9175 (other) Merge branch 'master' into other
| | |\  
| |/ /  
|/| /   
| |/    
| * f318ead add foo line
* | 31875d9 add baz line
|/  
* 8b313a0 add README

There you go: the history now contains criss-cross merges.

Is this a common situation or an error situation?

As illustrated above, the situation can arise. It may be undesirable, but it shouldn't be considered an "error state".

Benildas answered 14/10, 2014 at 22:6 Comment(2)
Thanks for the example, that was exactly the sort of thing I was looking for! The issue is becoming clearer for me now.Sterilize
@Sterilize Yes, you're right; --all is missing. I'll edit my answer.Benildas
M
3

Here is a sequence of commits that would generate a criss-cross merge. More steps than the one involving the --amend, but only uses the basics. Run this script to see that you do indeed get a criss-cross merge.

#!/bin/sh
# Initialize 
git init .                                                                                                                       
echo date > file.txt; git add file.txt
git commit -m "Initial commit"

# Make branches                                                                                                                     
git branch A; git branch B

# Make change in A                                                                                                                  
git checkout A
echo date > fileA.txt; git add fileA.txt
git commit -m "first commit along branch A"

# Make change in B; add tag for later                                                                                                               
git checkout B
echo date > fileB.txt; git add fileB.txt
git commit -m "first commit along branch B"
git tag "tag-B"

# Merge A into B (still on B)                                                                                                       
git merge --no-edit A; git tag "M"

# Add another commit on B whose parent is M                                                                                         
echo date > fileB2.txt; git add fileB2.txt
git commit -m "second commit along B"

# Switch to A and add a commit; note that
# M is not an ancestor of this commit                                                                          
git checkout A
echo date > fileA2.txt; git add fileA2.txt
git commit -m "second commit along A"

echo "Best common ancestors (before criss-cross):"
# Should be only one
git merge-base --all A B

# Merge the commit tagged "tag-B" 
# into A generating the cris-cross-merge                                                            
git merge --no-edit tag-B

echo "Best common ancestors (after criss-cross):"
# Should be two
git merge-base --all A B
Monah answered 30/4, 2017 at 13:26 Comment(2)
Thanks for the detailed reply. I will try this on my end and look at the revision history to see it in action. :)Sterilize
For me, this script does not create a criss-cross merge.Photozincography
N
2

This happens when the other developer fetches commits from you, you fetch from them, and then you both merge each other's branches into your own after you've both fetched. That can happen, it's not an error state.

Nonfeasance answered 14/10, 2014 at 22:16 Comment(2)
Thanks for the reply. I think I will have to setup an example repo to test your scenario so I can see it unfold.Sterilize
possibly we fetch from the same server not from each other, if there is no problemIncardinate
I
1

When the history involves criss-cross merges, there can be more than one best common ancestor for two commits. For example, with this topology:

---1---o---A
    \ /
     X
    / \
---2---o---o---B

The above comes from the git website, I guess it forgets about adding an arrow.

Let's look at the pic below, it is obvious how to make a criss-cross situation then. when branch A needs some code from branch B, it merges from branch B; when branch B needs some code from branch A, it mergers from branch A; here we come across the criss-cross situations then.

Even more, It is easy to figure out that branch A and branch B share ancestors 1 and 2.

enter image description here

Incardinate answered 6/11, 2018 at 16:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.