Reputation: 1092
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
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