Search code examples
mercurial

Some hg changesets not merging after graft


I have two hg branches (dev and stable) that aren't merging like I'd expect.

  1. On stable: I grafted in a one-line commit from dev.
  2. On dev: Changed that one line that was grafted, committed change.
  3. On stable: merged dev into stable (no conflicts).

However after this merge stable still has the grafted version of the line (step 1). Not the latest changes to that same line from dev (step 2). Why is this?

hg log here


The file looks like:

This
file
to
be
merged

Changesets:

  1. Changes "to" to "might" on dev
  2. Grafts changeset 1 to stable
  3. Changes "might" back to "to" on dev
  4. Merges dev into stable. Result is "might" (not "to" like I'd expect to see from changeset 3).

Solution

  • Sorry about the delay here: As soon as you shrank the reproducer to the five commits, I knew what was going on, but I wanted to write my own reproducer before answering, and the priority of this dropped a lot. 😀 The script I used, mktest.hg, to create the commits, the graft, and the merge, appears at the end of this answer.

    The key issue here is the way merge actually works in Mercurial. It uses the same algorithm as Git does: that is, it completely ignores any of the branch information, and completely ignores any timing information. It looks only at three specific commits, as found by examining the commit graph, as shown in your image. Here's a text variant via my own reproducer:

    $ cd test-hg-graft/
    $ cat file.txt
    This
    file
    might
    be
    merged
    $ hg lga
    @    4:b027441200d2:draft  stable tip Chris Torek
    |\   merge dev into stable (9 minutes ago)
    | |
    | o  3:01c6cc386a08:draft  stable Chris Torek
    | |  back to "to" on stable (9 minutes ago)
    | |
    | o  2:ad954507e465:draft  stable Chris Torek
    | |  s/to/might/ (9 minutes ago)
    | |
    o |  1:f7521e4f0941:draft  dev Chris Torek
    |/   s/to/might/ (9 minutes ago)
    |
    o  0:a163d2c4874b:draft  stable Chris Torek
       initial (9 minutes ago)
    

    The lga alias is one I stole borrowed copied from someone else:

    lga = log -G --style ~/.hgstuff/map-cmdline.lg
    

    where map-cmdline.lg is in the link above. It's just log -G (aka glog) with a more-compact format.

    What's going on

    When we run:

    hg merge dev
    

    Mercurial locates three specific commits:

    • The current commit on stable, -r3 in this case (the SHA ID will vary), is one of the two endpoint commits.

    • The target commit on dev is the result of resolving dev to a revision. We can do this ourselves with hg id -r dev for instance:

      $ hg id -r dev
      f7521e4f0941 (dev)
      $ hg id -n -r dev
      1
      

      Note that we can do the same thing with @ to identify our current revision, although hg summary spills everything out more conveniently.

    • Last (or in some sense first, though we need the other two to get here), Mercurial locates a merge base commit from these two commits. The merge base is the first commit in the graph that is reachable from both of the other inputs to the merge. In our particular case, that's rev zero, since we split the branches apart right after -r0.

    Technically, the merge base is the output of a Lowest Common Ancestor algorithm as run on the Directed Acyclic Graph. See Wikipedia for some examples. There can be more than one LCA; Mercurial picks one at (apparent) random for this case. In our case there is only one LCA though.

    Having found the merge base, Mercurial now runs the equivalent of two diff operations:

    hg diff -r 0 -r 3
    

    to see what we changed, and:

    hg diff -r 0 -r 1
    

    to see what they changed, since the merge base snapshot.1 If we do this ourselves, we see what Mercurial sees:

    $ hg diff -r 0 -r 3
    $ hg diff -r 0 -r 1
    diff --git a/file.txt b/file.txt
    --- a/file.txt
    +++ b/file.txt
    @@ -1,5 +1,5 @@
     This
     file
    -to
    +might
     be
     merged
    

    (I have my hg diff configured with git = true so that I get diffs that I can feed to Git—long ago I was doing a lot of conversion work here.)

    As far as Mercurial is concerned, then, we did nothing on our branch. So it combines do nothing with make this change to file.txt and comes up with this one change to file.txt. That one change is applied to the files from the merge base commit. The resulting files—well, file, singular, in this case—are the ones that are ready to go into the final merge commit, even though they're not the ones you wanted.

    Because Mercurial has more information than Git—in particular, which branch something happened on—it would be possible for Mercurial to behave differently from Git here. But in fact, both do the same thing with this kind of operation. They both find a merge base snapshot, compare the snapshot to the two input commit snapshots, and apply the resulting combined changeset to the files from the merge base. Mercurial can do a better job of catching file renames (since it knows them, vs Git, which just has to guess) and could do a different job of merging here, but doesn't.


    1Some might object that Mercurial stores changesets, not snapshots. This is true—or rather, sort of true: every once in a while, Mercurial stores a new copy of a file, instead of a change for it. But as long as we have all the commits needed, storing changes vs storing snapshots is pretty much irrelevant. Given two adjacent snapshots, we can find a changeset, and given one snapshot and a changeset to move forward or backward, we can compute a new snapshot. That's how we can extract a snapshot in Mercurial (which stores changesets), or show a changeset in Git (which stores snapshots).


    Script: mktest.hg

    #! /bin/sh
    
    d=test-hg-graft
    
    test "$1" = replay && rm -rf $d
    if test -e $d; then
        echo "fatal: $d already exists" 1>&2
        exit 1
    fi
    set -e
    mkdir $d
    cd $d
    
    hg init
    hg branch stable
    
    cat << END > file.txt
    This
    file
    to
    be
    merged
    END
    hg add file.txt
    hg commit -m initial
    
    hg branch dev
    ed file.txt << END
    3s/to/might/
    w
    q
    END
    hg commit -m 's/to/might/'
    
    hg checkout stable
    hg graft -r 1 # pick up s/to/might/; graft makes its own commit
    
    ed file.txt << END
    3s/might/to/
    w
    q
    END
    hg commit -m 'back to "to" on stable'
    
    hg merge dev
    hg commit -m "merge dev into stable"