3

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 scenario:

  • 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.

Martin Geisler
  • 72,968
  • 25
  • 171
  • 229
Holstebroe
  • 4,993
  • 4
  • 29
  • 45
  • 3
    I have never used Plastic, but it sounds seriously confused. My experience is chiefly with `git`. If I understand your branching as described it would result in the changes to `foo.txt` being applied on the merge of C to A (with `git`). AFAIK the same holds for `hg`. – shelhamer Aug 17 '11 at 18:33
  • Yes, intuitively (my intuition) you would expect the changes made in B to be merged with A, when you merge a sub branch of B with A. Either I am missing some point about branching or Plastic has a deal breaking flaw. – Holstebroe Aug 17 '11 at 18:51
  • Mercurial would work fine with this. I'll try it in Plastic in a few hours when I get a chance, but Pablo will probably come answer, too. – Joel B Fant Aug 17 '11 at 19:30
  • On the Plastic forum, it is explained, that version 3.0 only merges changes made in C in a merge between C and A, not the whole tree. In 4.0 this will change. I am still curious about hg and git though. – Holstebroe Aug 17 '11 at 19:33
  • Hi, yes, I explained it here: http://www.plasticscm.net/forum193/default.aspx?g=posts&m=2109#post2109. In short: so far (<=3.0) we merged the "branch content" instead of the "branch status". Everyone else out there (Git, Hg, SVN (even with its flaws)) merges always "the status". I've to admit I prefer now the "simplest" 4.0 behavior but I also have to say that we had really few issues with this model so far (and it has been a while!), including teams bigger than 100 devs doing "task per branch",hence creating tons of branches on a daily basis. Maybe the reason is the "task per branch" pattern. – pablo Aug 17 '11 at 22:35
  • 1
    @pablo: Probably it didn't pop up because it is atypical to merge a branch to a branch that it didn't come from. Usually you merge back to the immediate parent (merging C back to B, then B back to A). – Joel B Fant Aug 18 '11 at 01:02
  • The last concrete case of this C to A merge was caused by a developer on the team who wanted to merge from A to B. After the merge he made a few commits to B, but eventually discovered that the merge had gone all wrong, so we wanted to roll back B to before the merge. As an alternative to a subtractive merge or similar, we branched the last good change set on B to a new branch C, which we merged with A expecting all the B changes to be included. – Holstebroe Aug 18 '11 at 07:36

2 Answers2

4

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.

Ryan Lundy
  • 204,559
  • 37
  • 180
  • 211
4

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).

Joel B Fant
  • 24,406
  • 4
  • 66
  • 67