elliotching
elliotching

Reputation: 1092

Create merge commit even if branches are identical

Is there a method to force create a commit even if 2 branches merging are exactly identical? Say I have git branch A and B (while the changes made are different), First step I merge A into B, a default commit of Merge branch A into B will be created (and solve conflicts if any).

Next, I want to merge branch B into A again (no merge commit will be created here), and I wish to create commit to annotate "B is now merged into A! guys!"

Upvotes: 2

Views: 432

Answers (1)

torek
torek

Reputation: 487725

This is easy to do with git merge --no-ff, as Anthony Sottile pointed out in a comment. It's not a totally crazy thing to do, and does not create the dreaded criss-cross merge that results in complicated future recursive merges, but it is usually an odd thing to do.

Note that it depends on the idea that the hash IDs selected by your two branch names are different hash IDs, even if the snapshots match up. Typically after merging, the snapshots don't match up. The usual exception to this rule is when one of the two snapshots was itself made with --no-ff.

That is, we start with, e.g.:

          I--J   <-- br1 (HEAD)
         /
...--G--H
         \
          K--L   <-- br2

We run git merge br2 in this state—with commit J being the current commit thanks to HEAD being attached to name br1 which points to J—and Git locates commit L through the name br2, then locates the merge base commit H. The merge process compares the snapshot in H to the one in J to see what we changed:

git diff --find-renames <hash-of-H> <hash-of-J>   # what we changed

Git repeats this diff for H-vs-L:

git diff --find-renames <hash-of-H> <hash-of-L>   # what they changed

Git then extracts all of H's files, combines the two sets of changes, applies the combined changes to H's files, and—if there are no conflicts—makes a new merge commit M, to which name br1 points. I tend to draw that like this:

          I--J
         /    \
...--G--H      M   <-- br1 (HEAD)
         \    /
          K--L   <-- br2

In this case, though, let's draw it this way:

          I--J---M   <-- br1 (HEAD)
         /      /
...--G--H      /
         \    /
          K--L   <-- br2

If we now git checkout br2 and run git merge without --no-ff, Git will, as you noted in the question, do a fast-forward operation: the name br2 will be modified to point directly to merge commit M. The identity of the separate branch br2 is lost. That's normally fine, because we don't need br2 any more at all: we can just delete it entirely, without bothering with this git merge step.

If we have some reason to keep the name around, though, we can do that; and if we want it to retain its identity by using a different commit hash ID, we need to run the merge with --no-ff. We're now in this position, and we need to find commits M and L and their merge base:

          I--J---M   <-- br1
         /      /
...--G--H      /
         \    /
          K--L   <-- br2 (HEAD)

The merge base of two commits is, loosely speaking, the "closest" commit that's on both branches. Commit L is of course on branch br2 as it is the tip of br2. Commit M is only on br1, and its first parent J is only on br1, but its second parent L is on br2. So commit L is the merge base.

The fact that the merge base L is the current commit is why this would normally be a fast-forward instead of a merge, but with --no-ff, Git goes ahead and runs the usual two diffs:

git diff --find-renames <hash-of-L> <hash-of-L>   # what we changed
git diff --find-renames <hash-of-L> <hash-of-M>   # what they changed

What we changed is, of course, nothing at all. So Git figures out what they changed—which is to say, whatever is different between L and M—and applies those changes to the files extracted from L. This snapshot matches the snapshot in M, but Git goes ahead and makes new merge commit N:

          I--J---M   <-- br1
         /      / \
...--G--H      /   \
         \    /     \
          K--L-------N   <-- br2 (HEAD)

New commits made after M or N produce, e.g.:

          I--J---M   <-- br1
         /      / \
...--G--H      /   \
         \    /     \
          K--L-------N--O--P   <-- br2 (HEAD)

Merging br2 into br1 at this point requires an explicit --no-ff because commit M is the merge base of M and P. An attempt to merge br1 into br2 does nothing at all (says there is nothing to merge). If, however, there is at least one commit on br1 that is not on br2 and vice versa, such as:

          I--J---M--O   <-- br1
         /      / \
...--G--H      /   \
         \    /     \
          K--L-------N--P   <-- br2

then there are commits to merge in "both directions", at least until the next merge is made (regardless of which direction we use when making it).

Upvotes: 2

Related Questions