Reputation: 1
I have three branches, master
, feature
, and integration
. integration
represents the software configuration deployed to a specific development server. Feature branches are first merged into an integration
branch, tested on the corresponding server, and then merged into master
.
\ \
o----------------o----------o (master)
\ \
\ o----o-------o--o---o----o (integration)
\ / / / /
o-----o-------o--o---o----o--o (feature)
My goal is to update my integration
branch to be identical to master
, and then merge my feature
branch into it so that the integration
branch is the master
branch plus the changes in my feature
branch.
To do this first merged my integration
branch into master with strategy=ours
and then I fast-forwarded the integration
branch to be the same as the master
branch.
\ \
o----------------o----------o--------X (master, integration)
\ \ /
\ o----o-------o--o---o----o
\ / / / /
o-----o-------o--o---o----o--o (feature)
And then my plan was to merge the feature
branch into the newly updated integration
branch.
\ \
o----------------o----------o--------X- (master)
\ \ / \
\ o----o-------o--o---o----o Y (integration)
\ / / / / /
o-----o---o---o--o---o----o---o (feature)
However, when I do that my updated integration
branch is identical to the previous commit on the integration branch, e.g. X and Y are identical, instead of being the sum of the master
branch plus my changes in the feature branch.
So, can anyone recommend a way for me to get the branches configured in the way I want them?
Upvotes: 0
Views: 962
Reputation: 488013
[Edit: fixed in question, strike-out in answer now] Aside from the slight glitch in the question (the diagram shows the effect of Git seems to be behaving the way Git is supposed to.git checkout master && git merge -s ours integration && git checkout integration && git merge --ff-only master
, which uses integration
everywhere that you said feature
),
My goal is to update my integration branch to be identical to master, and then merge my feature branch into it so that the integration branch is the master branch plus the changes in my feature branch.
This is not necessarily a good idea, because Git doesn't do merges the way I think you think it does merges. :-)
What git merge
does is complicated because it's full of tiny details, but the way it does it is simple enough, and the graphs you have drawn here are a great start. Let's start with the "before" one. I will modify it just a tiny bit to add the HEAD
notation and to pick out three commits, L
, R
, and B
:
\ \
B----------------o----------L <-- master (HEAD)
\ \
\ o----o-------o--o---o----R <-- integration
\ / / / /
o-----o-------o--o---o----o--o <-- feature
Each merge strategy (-s recursive
being the default, and the -s ours
that you used being another one) has total control over what happens next. But let's assume, for a moment, that you run git merge -s recursive integration
rather than git merge -s ours integration
. The first step of this merge strategy would be to locate the merge base commit. Git does this by walking the graph backwards (in the direction of all the internal arrows, which always point backwards) to find the common ancestor between the current branch's tip commit and the named branch's tip commit.
The current branch is HEAD
which is master
which locates commit L
(L here stands for Left, Local, or --ours
). The named commit is integration
which locates commit R
(R stands for Right, Remote, otheR, or --theirs
). B
, the merge base, is the first shared commit we (or Git) will find if we walk the graph backwards simultaneously from both L
and R
.
So now, in an ordinary merge, Git would run the equivalent of:
git diff --find-renames B L > /tmp/left
git diff --find-renames B R > /tmp/right
It would then do its best to compute a new set of files that take the union of the two change-sets found in /tmp/left and /tmp/right and apply those to the files in B
.
If all goes well, Git would then make a new commit with two parents, L
first and then R
, using the merged files. This is in fact the new graph you get:
\ \
o----------------o----------L--------X <-- master (HEAD)
\ \ /
\ o----o-------o--o---o----o <-- integration
\ / / / /
o-----o-------o--o---o----o--o <-- feature
(I've un-named B
and R
as they are no longer special, but left L
for just a moment.) But you used -s ours
, and this strategy doesn't actually run git diff
at all. It just uses the same tree as the previous commit, i.e., L
itself. So commits L
and X
represent the same source ... but not the same commit history, as now integration
has been merged into master
, throwing away any difference that would have been introduced in the process.
Inasmuch as anything in Git has "meaning"—Git just follows a set of programmed procedures, so the only meaning is that we assign to it, or the ideas we use when we write these programmed procedures—the "meaning" of -s ours
is: everything in the branch I just merged is terrible; never use it again. The intent, as it were, is to kill off the branch.
Let's see how this works now when we run git checkout integration && git merge --ff-only master
(or the same without the --ff-only
, by default, if we have not messed with various Git configuration variables). A fast-forward operation, which is not actually a merge at all, just moves a branch label. The git checkout
step alters our HEAD
to point to integration
so that it's integration
that gets moved. This results in your second graph:
\ \
o----------------o----------o--------X <-- master, integration (HEAD)
\ \ /
\ o----o-------o--o---o----o
\ / / / /
o-----o-------o--o---o----o--o <-- feature
Now we might run git merge feature
(with or without an explicit -s recursive
). That will walk the graph to find the merge base, which is the first commit reachable from both X
and the tip of feature
. Starting from X
, we move down-and-left twice to arrive at this:
\ \
o----------------o----------o--------X <-- master, integration (HEAD)
\ \ /
\ o----o-------o--o---o----I
\ / / / /
o-----o-------o--o---o----B--R <-- feature
I've labeled the base and right-side commits again, but kept the left-side commit named X
. I've added one more single letter, I
, which is the old tip of integration
, before it got forwarded.
Now Git will run:
git diff --find-renames B X
git diff --find-renames B R
and combine the changes.
The change from B
to X
is: throw away most of the stuff that was on integration
, which is obvious enough because the change from I
to X
is throw away everything that was done on integration
—go back to what was in master
.
We might, depending on I
vs X
, get to keep whatever happened from B
to I
. We definitely keep whatever happened from B
to R
, unless it conflicts with the B
-to-X
changes. These combined changes, B
to X
plus B
to R
, form the merge result, and if all seems well, Git makes your merge commit Y
with first-parent X
and second-parent R
.
The big problem is the git merge -s ours
, which records that the merge happened, but uses the tree (source code) from master
only. The later merge uses the recorded previous merge to find a new, better, simpler merge base ... and working from that merge base demonstrates that when merging feature
, it's important to un-do (from that base) all the changes that had been on integration
, as apparently they were terrible and should be thrown out.
It is actually possible to construct a different tree: you can run git merge --no-commit
, which performs the tree-merging (diff combining) work and sets everything up so that the next git commit
will make a merge commit, with the usual two parents, without actually making that commit yet. Next, you can make whatever changes you like to the work-tree and git add
the resulting files back into the index. For instance, you could run git diff <hash-of-X> <hash-of-I> | git apply
, to yank all those un-done changes right back into the work-tree. (This assumes they will apply well. Add --full-index
to the diff and -3
to the apply to get full three-way merging if needed. I don't think Git will be able to deal well with renames here.)
In the end, though, you're working against the design: the behavior of merging, with newly computed merge bases after previous merges, is designed to work well as long as you don't use -s ours
. If you do use -s ours
, it's probably meant to "kill off" a branch, not to keep working with it, and the merge base algorithm behaves that way.
Upvotes: 2