Search code examples
gitmercurialdvcsplasticscm

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


Solution

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