Reputation: 3
I have a primary development branch (A) which has a long history. All release commits in A are tagged as such. I checked out the root commit of A, and branched into test (B).
So I have a primary branch A, and the head of branch B pointing at the root commit of A. My goal is to create a history of all releases by merging each tagged commit from A to B.
The first merge from A into B works as expected, no conflicts.
$git checkout B
$git merge [release-commit-ID] --squash
$git commit -m "release#"
The first commit works great, but all other commits treat all merge commits as complete conflicts. I see that the root of B is the same as the root of A, but no shared history is recognized after the first squashed merge commit from the first release commit in A into B. What is causing the conflicts and how do I get shared history to be recognized?
Upvotes: 0
Views: 42
Reputation: 487755
What is causing the conflicts and how do I get shared history to be recognized?
There is no shared history (or rather, not enough). There's nothing to recognize, which is why there are conflicts.
The key is the --squash
flag:
$ git checkout B $ git merge [release-commit-ID] --squash
The first step attaches your HEAD
to the branch name B
, checking out whichever commit the name B
identifies:
...--*--C--D--E <-- B (HEAD)
\
F--G--H <-- somebranch
Here the name B
identifies commit E
(each of these letters stands in for the real hash IDs). Commit *
(which I would have named B
, but you used that name for your branch) is the point at which the two development streams diverge, which means that when we work backwards (as Git does) it's the point where they come together. You now run git merge --squash <hash>
where <hash>
identifies commit F
, or G
, or H
, so Git compares the contents of commit *
to the contents of commit E
to find out what you changed:
git diff --find-renames <hash-of-*> <hash-of-E> # what we changed
and repeats with, say, G
:
git diff --find-renames <hash-of-*> <hash-of-G> # what they changed
Git now combines these two sets of changes, applies the combined changes to commit *
's contents, and makes a new commit.
If you don't use --squash
, Git records the new commit with two parents:
...--*--C--D--E--I <-- B (HEAD)
\ /
F-------G--H <-- somebranch
and now the most recent common starting point between the two lines is commit G
. But if you do use --squash
, Git records the new commit with just one parent:
...--*--C--D--E--I <-- B (HEAD)
\
F--G--H <-- somebranch
and now the common starting point is unchanged. All the work through G
is in commit I
, but commit I
doesn't remember why that work is there. A future git merge
from commit H
has to start over at commit *
.
Git won't stop you from developing on branch somebranch
, but in general, after a git merge --squash
, you should consider the branch you merged from to be "dead" and just stop using it at all. Once we've merged G
with git merge --squash
we should stop using F
and G
entirely. This means we must also stop using H
entirely. If H
is useful, we should copy it to a new, different commit:
$ git checkout -b newbranch B
giving us:
...--*--C--D--E--I <-- B, newbranch (HEAD)
\ /
F-------G--H <-- somebranch
followed by:
$ git cherry-pick somebranch # or <hash of H>
to copy H
to a very similar, but not identical, commit H'
:
H' <-- newbranch (HEAD)
/
...--*--C--D--E--I <-- B
\ /
F-------G--H <-- somebranch
We can now discard somebranch
:
$ git branch -D somebranch
and rename newbranch
to somebranch
if we like.
(Note that we can do this copy-with-name-moving thing in one step using git rebase --onto
, with git checkout somebranch; git rebase --onto B <hash of G>
. No matter how you do it, though, be aware that git cherry-pick
cannot copy merge commits, and anyone who is using the commits we want to kill off—here the F-G-H
chain—must do this killing-off-with-copying-the-ones-to-save thing. So before using --squash
, be sure you understand all the implications.)
Upvotes: 1