Merging changes in intermediate branches with DVCS
I have been working with Plastic SCM for a while, but only recently discovered a flaw in my mental model of how change sets in branches work.
Consider this simple scenar开发者_StackOverflow中文版io:
- Assume I have a branch A with a file
foo.txt
- I branch latest change set of A to a child branch B.
- I switch workspace to B and checkout
foo.txt
and make some changes, which I check in. - I branch the last change set of B to C.
- I attempt to merge C to A
In short: A branch to B, change foo.txt
in B, branch to C, merge C to A.
To my surprise the changes made to foo.txt
in the B intermediate branch was ignored, because I didn't make changes to foo.txt
in C. So after the merge from C to A, A contains the original foo.txt before branching out B.
I would have expected my intermediate changes in B to be merged when performing a full merge from C to A, but obviously my understanding of change sets in branches have been wrong. This has caused quite some clean up from time to time when changes mysteriously were lost.
Does Git and Mercurial or other DVCS behave similarly?
Edit:
Plastic version <= 3 merges only changes in the source branch, not intermediate branches.
Plastic >= 4 merges the whole branch path.
Git doesn't work this way. In Git, a branch is really just a pointer pointing to the last commit in a series. When you're on that branch and you make a new commit, the pointer moves ahead to the new commit.
Because of this, when you create a new branch from an existing branch, you now simply have two pointers to the same commit. Both branches have the same history.
The scenario you're talking about would look like this in Git:
A
↓
a1 ← a2 ← a3 B
↖ ↓
a4 C
↖ ↓
a5
In Git you just have a series of commits, but some of them happen to have branch labels pointing to them. A points to a3 (which knows a2 is its parent); B points to a4, which has a3 as its parent, and so forth. So by default if you merged C into A, Git would do a "fast-forward" merge (which just means A has no changes) and you'd get this:
B A, C
↓ ↓
a1 ← a2 ← a3 ← a4 ← a5
Now you could get rid of the B changes if you wanted to. One way is with an interactive rebase:
git rebase -i
This shows you a list of commits, and you just delete the B commit from that list. Git replays your commits without that one. Another way is to start at A, then use
git cherry-pick a5
to put the a5 changes as the next change set after a3. But both of these are special things you have to do. By default B would be included in the merge.
Mercurial works differently with branches than git does. In Mercurial, the branch name of a changeset is actually part of the changeset. Bookmarks behave somewhat similarly to git's branches. There are still differences, and this article does a nice job covering hg bookmarks and git branches.
Whether using Mercurial bookmarks or named branches (or even just anonymous branches), you still get this:
@__ merged C into A (branch A again)
| \
| o add c.txt (branch C; a commit is necessary to start named branch)
| /
| o appended to a.txt (branch B)
|/
o added a.txt (branch A)
And after the merge, A contains: the a.txt that has the appended text done in the second changeset (as well as c.txt added in the third).
精彩评论