What is the behavior of the "cousins" options in git rebase --rebase-merges?

Viewed 489

For Git v2.22, the following information regarding the --rebase-merges option can be found on the git rebase documentation page:

By default, or when no-rebase-cousins was specified, commits which do not have <upstream> as direct ancestor will keep their original branch point, i.e. commits that would be excluded by git-log's --ancestry-path option will keep their original ancestry by default. If the rebase-cousins mode is turned on, such commits are instead rebased onto <upstream> (or <onto>, if specified).

Unfortunately, the detailed documentation sections at the bottom of that page do not show any examples of the no-rebase-cousins or rebase-cousins options to --rebase-merges. I'm also not too familiar with git log --ancestry-path (I never use it), so that example doesn't help my understanding at all.

Can someone explain, with examples, the behavior of the rebase-cousins parameters to the --rebase-merges option?

1 Answers

This is probably best done by example.

Suppose you have:

...--o--*--o--o   <-- main
         \
          \      C--D
           \    /    \
            A--B      G--H   <-- branch
                \    /
                 E--F

You want to rebase branch onto main, with --rebase-merges. The enumeration of main..branch lists commits A through H. OK, the goal is obvious: Git should copy commits A-B-(C-D/E-F) to new commits to make A', B', etc., then run a new git merge to make G', and finally copy H to make H', all of which come after the tip of main instead of coming after commit *. You end up with:

                       C'-D'
                      /    \
                  A'-B'     G'-H'  <-- branch
                 /    \    /
                /      E'-F'
               /
...--o--*--o--o   <-- main
         \
          \      C--D
           \    /    \
            A--B      G--H   [abandoned]
                \    /
                 E--F

But suppose you have instead:

...--o--*--o---o   <-- main
      \  \
       \  A--B---F--G   <-- branch
        \       /
         C--D--E

You run git checkout branch; git rebase --rebase-merges main. The enumeration, main..branch, lists commits A-B-F-G and C-D-E too.

Which commits do you want copied? Do you want to keep C-D-E as they are, or copy them? You do want to re-perform the merge that produced F, merging your new copy B' with something, but is that with E, or is that with E', the new copy of E?

The option allows you to avoid copying C-D-E, which aren't in the ancestry path from * to H—git log --ancestry-path main..branch will exclude them—or to copy these "cousin" commits anyway. The default is to exclude them from the copying process, giving:

                 A'-B'---------F'-G'  <-- branch
                /             /
...--o--*--o---o   <-- main  /
      \  \                  /
       \  A--B---F--G      /  [abandoned]
        \       /_________/
         C--D--E

The rebase-cousins option includes them in the copying process, and puts C', the copy of C, after main, giving:

                  C'-D'-E'
                 /       \
                /         \
               / A'-B'-----F'-G'  <-- branch
               |/
...--o--*--o---o   <-- main
      \  \
       \  A--B---F--G   [abandoned]
        \       /
         C--D--E

Note that if there are branch names pointing to any of C, D, or E, they won't be moved, regardless of which option you choose.

Related