Spencer H
Spencer H

Reputation: 663

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

Upvotes: 1

Views: 306

Answers (1)

torek
torek

Reputation: 488183

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"

Upvotes: 1

Related Questions