[PATCH 10 of 11] graft: enable rotated-DAG copy tracing (issue4028)

Gábor STEFANIK Gabor.STEFANIK at nng.com
Tue Oct 4 10:48:43 EDT 2016


>


--------------------------------------------------------------------------
This message, including its attachments, is confidential. For more information please read NNG's email policy here:
http://www.nng.com/emailpolicy/
By responding to this email you accept the email policy.


-----Original Message-----
> From: Mercurial-devel [mailto:mercurial-devel-bounces at mercurial-scm.org]
> On Behalf Of Gábor Stefanik
> Sent: Tuesday, October 4, 2016 4:40 PM
> To: mercurial-devel at mercurial-scm.org
> Subject: [PATCH 10 of 11] graft: enable rotated-DAG copy tracing (issue4028)
>
> # HG changeset patch
> # User G?bor Stefanik <gabor.stefanik at nng.com> # Date 1475512693 -7200

Alright, that didn't work either, sorry for the spam...

> #      Mon Oct 03 18:38:13 2016 +0200
> # Node ID 9e59cd55604c5e30b38c66c502c8c982c01a4a01
> # Parent  5977d6569b2e22487134af7be281e5d60cc987f5
> graft: enable rotated-DAG copy tracing (issue4028)
>
> Graft performs a merge in a rotated DAG (with a false common ancestor),
> which must be taken into account when tracking copies. Find the real
> common ancestor in this case, and track copies between the real and false
> common ancestors in reverse.
>
> Using this change, when grafting a commit with a change to a file moved
> earlier on the graft's source branch, the change is merged as expected into
> the original
> (unmoved) file, rather than recreating it under its new name.
> It should also eventually make it possible to support cross-branch updates
> that preserve changes in a dirty working copy.
>
> diff -r 5977d6569b2e -r 9e59cd55604c mercurial/copies.py
> --- a/mercurial/copies.pyTue Oct 04 12:51:54 2016 +0200
> +++ b/mercurial/copies.pyMon Oct 03 18:38:13 2016 +0200
> @@ -321,7 +321,23 @@
>      if repo.ui.configbool('experimental', 'disablecopytrace'):
>          return {}, {}, {}, {}
>
> -    dirtyc1 = False # dummy bool for later use
> +    # In certain scenarios (e.g. graft, update or rebase), ca can be overridden
> +    # We still need to know a real common ancestor in this case
> +    # We can't just compute _c1.ancestor(_c2) and compare it to ca, because
> +    # there can be multiple common ancestors, e.g. in case of bidmerge.
> +    # Because our caller may not know if the revision passed in lieu of the CA
> +    # is a genuine common ancestor or not without explicitly checking it, it's
> +    # better to determine that here.
> +    tca = ca
> +    # ca.descendant(wc) and ca.descendant(ca) are False, work around that
> +    _c1 = c1.p1() if c1.rev() is None else c1
> +    _c2 = c2.p1() if c2.rev() is None else c2
> +    dirtyc1 = not (ca == _c1 or ca.descendant(_c1))
> +    dirtyc2 = not (ca == _c2 or ca.descendant(_c2))
> +    graft = dirtyc1 or dirtyc2
> +    if graft:
> +        tca = _c1.ancestor(_c2)
> +
>      limit = _findlimit(repo, c1.rev(), c2.rev())
>      if limit is None:
>          # no common ancestor, no copies @@ -331,6 +347,7 @@
>      m1 = c1.manifest()
>      m2 = c2.manifest()
>      ma = ca.manifest()
> +    mta = tca.manifest()
>
>      # see _checkcopies documentation below for these dicts
>      copy1, copy2 = {}, {}
> @@ -340,18 +357,26 @@
>      diverge, incompletediverge = {}, {}
>
>      # find interesting file sets from manifests
> +    if graft:
> +        repo.ui.debug("  computing unmatched files in rotated DAG\n")
>      addedinm1 = m1.filesnotin(ma)
>      addedinm2 = m2.filesnotin(ma)
>      u1r, u2r = _computenonoverlap(repo, c1, c2, addedinm1, addedinm2)
> -    u1u, u2u = u1r, u2r
> +    if not graft:
> +        u1u, u2u = u1r, u2r
> +    else: # need to recompute this for directory move handling when grafting
> +        repo.ui.debug("  computing unmatched files in unrotated DAG\n")
> +        u1u, u2u = _computenonoverlap(repo, c1, c2, m1.filesnotin(mta),
> +                                                  m2.filesnotin(mta))
> +
>      bothnew = sorted(addedinm1 & addedinm2)
>
>      for f in u1u:
> -        _checkcopies(c1, f, m1, m2, ca, ca, False, limit, diverge, copy1,
> +        _checkcopies(c1, f, m1, m2, ca, tca, dirtyc1, limit, diverge,
> + copy1,
>                       fullcopy1, incomplete1, incompletediverge)
>
>      for f in u2u:
> -        _checkcopies(c2, f, m2, m1, ca, ca, False, limit, diverge, copy2,
> +        _checkcopies(c2, f, m2, m1, ca, tca, dirtyc2, limit, diverge,
> + copy2,
>                       fullcopy2, incomplete2, incompletediverge)
>
>      copy = dict(copy1.items() + copy2.items()) @@ -401,9 +426,9 @@
>      # reset incomplete dicts for bothdiverge generation
>      incomplete1, incomplete2, incompletediverge = {}, {}, {}
>      for f in bothnew:
> -        _checkcopies(c1, f, m1, m2, ca, ca, False, limit, bothdiverge, _copy,
> +        _checkcopies(c1, f, m1, m2, ca, tca, dirtyc1, limit,
> + bothdiverge, _copy,
>                       _fullcopy, incomplete1, incompletediverge)
> -        _checkcopies(c2, f, m2, m1, ca, ca, False, limit, bothdiverge, _copy,
> +        _checkcopies(c2, f, m2, m1, ca, tca, dirtyc2, limit,
> + bothdiverge, _copy,
>                       _fullcopy, incomplete2, incompletediverge)
>      if dirtyc1:
>          assert incomplete2 == {}
> diff -r 5977d6569b2e -r 9e59cd55604c tests/test-graft.t
> --- a/tests/test-graft.tTue Oct 04 12:51:54 2016 +0200
> +++ b/tests/test-graft.tMon Oct 03 18:38:13 2016 +0200
> @@ -179,6 +179,13 @@
>    committing changelog
>    grafting 5:97f8bfe72746 "5"
>      searching for copies back to rev 1
> +    computing unmatched files in rotated DAG
> +    computing unmatched files in unrotated DAG
> +    unmatched files in other:
> +     c
> +    all copies found (* = to merge, ! = divergent, % = renamed and deleted):
> +     src: 'c' -> dst: 'b' *
> +    checking for directory renames
>    resolving manifests
>     branchmerge: True, force: True, partial: False
>     ancestor: 4c60f11aa304, local: 6b9e5368ca4e+, remote: 97f8bfe72746 @@ -
> 193,6 +200,13 @@
>    scanning for duplicate grafts
>    grafting 4:9c233e8e184d "4"
>      searching for copies back to rev 1
> +    computing unmatched files in rotated DAG
> +    computing unmatched files in unrotated DAG
> +    unmatched files in other:
> +     c
> +    all copies found (* = to merge, ! = divergent, % = renamed and deleted):
> +     src: 'c' -> dst: 'b' *
> +    checking for directory renames
>    resolving manifests
>     branchmerge: True, force: True, partial: False
>     ancestor: 4c60f11aa304, local: 1905859650ec+, remote: 9c233e8e184d @@ -
> 842,3 +856,431 @@
>    |/
>    o  0
>
> +Graft from behind a move or rename
> +==================================
> +
> +NOTE: This is affected by issue5343, and will need updating when it's
> +fixed
> +
> +Possible cases during a regular graft (when ca is between cta and c2):
> +
> +name | c1<-cta | cta<->ca | ca->c2
> +A.0  |         |          |
> +A.1  |    X    |          |
> +A.2  |         |     X    |
> +A.3  |         |          |   X
> +A.4  |    X    |     X    |
> +A.5  |    X    |          |   X
> +A.6  |         |     X    |   X
> +A.7  |    X    |     X    |   X
> +
> +A.0 is trivial, and doesn't need copy tracking.
> +For A.1, a forward rename is recorded in the c1 pass, to be followed later.
> +In A.2, the rename is recorded in the c2 pass and followed backwards.
> +A.3 is recorded in the c2 pass as a forward rename to be duplicated on
> target.
> +In A.4, both passes of checkcopies record incomplete renames, which are
> +then joined in mergecopies to record a rename to be followed.
> +In A.5 and A.7, the c1 pass records an incomplete rename, while the c2
> +pass records an incomplete divergence. The incomplete rename is then
> +joined to the appropriate side of the incomplete divergence, and the
> +result is recorded as a divergence. The code doesn't distinguish at all
> +between these two cases, since the end result of them is the same: an
> +incomplete divergence joined with an incomplete rename into a
> divergence.
> +Finally, A.6 records a divergence entirely in the c2 pass.
> +
> +A.4 has a degenerate case a<-b<-a->a, where checkcopies isn't needed at
> all.
> +A.5 has a special case a<-b<-b->a, which is treated like a<-b->a in a merge.
> +A.6 has a special case a<-a<-b->a. Here, checkcopies will find a
> +spurious incomplete divergence, which is in fact complete. This is
> +handled later in mergecopies.
> +A.7 has 4 special cases: a<-b<-a->b (the "ping-pong" case), a<-b<-c->b,
> +a<-b<-a->c and a<-b<-c->a. Of these, only the "ping-pong" case is
> +interesting, the others are fairly trivial (a<-b<-c->b and a<-b<-a->c
> +proceed like the base case, a<-b<-c->a is treated the same as a<-b<-b->a).
> +
> +f5a therefore tests the "ping-pong" rename case, where a file is
> +renamed to the same name on both branches, then the rename is backed
> +out on one branch, and the backout is grafted to the other branch. This
> +creates a challenging rename sequence of a<-b<-a->b in the graft
> +target, topological CA, graft CA and graft source, respectively. Since
> +rename detection will run on the c1 side for such a sequence (as for
> +technical reasons, we split the c1 and c2 sides not at the graft CA,
> +but rather at the topological CA), it will pick up a false rename, and
> +cause a spurious merge conflict. This false rename is always exactly
> +the reverse of the true rename that would be detected on the c2 side, so
> we can correct for it by detecting this condition and reversing as necessary.
> +
> +First, set up the repository with commits to be grafted
> +
> +  $ hg init ../graftmove
> +  $ cd ../graftmove
> +  $ echo c1a > f1a
> +  $ echo c2a > f2a
> +  $ echo c3a > f3a
> +  $ echo c4a > f4a
> +  $ echo c5a > f5a
> +  $ hg ci -qAm a
> +  $ hg mv f1a f1b
> +  $ hg mv f3a f3b
> +  $ hg mv f5a f5b
> +  $ hg ci -qAm b
> +  $ echo c1c > f1b
> +  $ hg mv f2a f2c
> +  $ hg mv f5b f5a
> +  $ echo c5c > f5a
> +  $ hg ci -qAm c
> +  $ hg mv f3b f3d
> +  $ echo c4d > f4a
> +  $ hg ci -qAm d
> +  $ hg log -G
> +  @  changeset:   3:aa2584f6dee9
> +  |  tag:         tip
> +  |  user:        test
> +  |  date:        Thu Jan 01 00:00:00 1970 +0000
> +  |  summary:     d
> +  |
> +  o  changeset:   2:c8d3926d7649
> +  |  user:        test
> +  |  date:        Thu Jan 01 00:00:00 1970 +0000
> +  |  summary:     c
> +  |
> +  o  changeset:   1:7e9aff31b586
> +  |  user:        test
> +  |  date:        Thu Jan 01 00:00:00 1970 +0000
> +  |  summary:     b
> +  |
> +  o  changeset:   0:3340a7726e9e
> +     user:        test
> +     date:        Thu Jan 01 00:00:00 1970 +0000
> +     summary:     a
> +
> +
> +Test the cases A.2 (f1x), A.3 (f2x) and a special case of A.6 (f5x)
> +where the two renames actually converge to the same name (thus no
> actual divergence).
> +
> +  $ hg up -q 0 # commit "a"
> +  $ hg graft -r 2
> +  grafting 2:c8d3926d7649 "c"
> +  merging f1a and f1b to f1a
> +  merging f5a
> +  warning: can't find ancestor for 'f5a' copied from 'f5b'!
> +  $ hg status --change .
> +  M f1a
> +  M f5a
> +  A f2c
> +  R f2a
> +  $ hg cat f1a
> +  c1c
> +  $ hg cat f1b
> +  f1b: no such file in rev 68af396ea7bf  [1]
> +
> +Test the cases A.0 (f4x) and A.6 (f3x)
> +
> +  $ hg graft -r 3
> +  grafting 3:aa2584f6dee9 "d"
> +  note: possible conflict - f3b was renamed multiple times to:
> +   f3d
> +   f3a
> +  warning: can't find ancestor for 'f3d' copied from 'f3b'!
> +
> +Set up the repository for some further tests
> +
> +  $ hg up -q 0
> +  $ hg mv f1a f1e
> +  $ echo c2e > f2a
> +  $ hg mv f3a f3e
> +  $ hg mv f4a f4e
> +  $ hg mv f5a f5b
> +  $ hg ci -qAm e
> +  $ hg log -G
> +  @  changeset:   6:52db7f4dcf33
> +  |  tag:         tip
> +  |  parent:      0:3340a7726e9e
> +  |  user:        test
> +  |  date:        Thu Jan 01 00:00:00 1970 +0000
> +  |  summary:     e
> +  |
> +  | o  changeset:   5:29f6ffdbca28
> +  | |  user:        test
> +  | |  date:        Thu Jan 01 00:00:00 1970 +0000
> +  | |  summary:     d
> +  | |
> +  | o  changeset:   4:68af396ea7bf
> +  |/   parent:      0:3340a7726e9e
> +  |    user:        test
> +  |    date:        Thu Jan 01 00:00:00 1970 +0000
> +  |    summary:     c
> +  |
> +  | o  changeset:   3:aa2584f6dee9
> +  | |  user:        test
> +  | |  date:        Thu Jan 01 00:00:00 1970 +0000
> +  | |  summary:     d
> +  | |
> +  | o  changeset:   2:c8d3926d7649
> +  | |  user:        test
> +  | |  date:        Thu Jan 01 00:00:00 1970 +0000
> +  | |  summary:     c
> +  | |
> +  | o  changeset:   1:7e9aff31b586
> +  |/   user:        test
> +  |    date:        Thu Jan 01 00:00:00 1970 +0000
> +  |    summary:     b
> +  |
> +  o  changeset:   0:3340a7726e9e
> +     user:        test
> +     date:        Thu Jan 01 00:00:00 1970 +0000
> +     summary:     a
> +
> +
> +Test the cases A.4 (f1x), the "ping-pong" special case of A.7 (f5x),
> +and A.3 with a local content change to be preserved (f2x).
> +
> +  $ hg graft -r 2
> +  grafting 2:c8d3926d7649 "c"
> +  merging f1e and f1b to f1e
> +  merging f2a and f2c to f2c
> +  merging f5b and f5a to f5a
> +
> +Test the cases A.1 (f4x) and A.7 (f3x).
> +
> +  $ hg graft -r 3
> +  grafting 3:aa2584f6dee9 "d"
> +  note: possible conflict - f3b was renamed multiple times to:
> +   f3e
> +   f3d
> +  merging f4e and f4a to f4e
> +  warning: can't find ancestor for 'f3d' copied from 'f3b'!
> +
> +Check the results of the grafts tested
> +
> +  $ hg log -CGv --patch --git
> +  @  changeset:   8:4c243d7a2f50
> +  |  tag:         tip
> +  |  user:        test
> +  |  date:        Thu Jan 01 00:00:00 1970 +0000
> +  |  files:       f3d f4e
> +  |  description:
> +  |  d
> +  |
> +  |
> +  |  diff --git a/f3d b/f3d
> +  |  new file mode 100644
> +  |  --- /dev/null
> +  |  +++ b/f3d
> +  |  @@ -0,0 +1,1 @@
> +  |  +c3a
> +  |  diff --git a/f4e b/f4e
> +  |  --- a/f4e
> +  |  +++ b/f4e
> +  |  @@ -1,1 +1,1 @@
> +  |  -c4a
> +  |  +c4d
> +  |
> +  o  changeset:   7:9b7ee4960a74
> +  |  user:        test
> +  |  date:        Thu Jan 01 00:00:00 1970 +0000
> +  |  files:       f1e f2a f2c f5a f5b
> +  |  copies:      f2c (f2a) f5a (f5b)
> +  |  description:
> +  |  c
> +  |
> +  |
> +  |  diff --git a/f1e b/f1e
> +  |  --- a/f1e
> +  |  +++ b/f1e
> +  |  @@ -1,1 +1,1 @@
> +  |  -c1a
> +  |  +c1c
> +  |  diff --git a/f2a b/f2c
> +  |  rename from f2a
> +  |  rename to f2c
> +  |  diff --git a/f5b b/f5a
> +  |  rename from f5b
> +  |  rename to f5a
> +  |  --- a/f5b
> +  |  +++ b/f5a
> +  |  @@ -1,1 +1,1 @@
> +  |  -c5a
> +  |  +c5c
> +  |
> +  o  changeset:   6:52db7f4dcf33
> +  |  parent:      0:3340a7726e9e
> +  |  user:        test
> +  |  date:        Thu Jan 01 00:00:00 1970 +0000
> +  |  files:       f1a f1e f2a f3a f3e f4a f4e f5a f5b
> +  |  copies:      f1e (f1a) f3e (f3a) f4e (f4a) f5b (f5a)
> +  |  description:
> +  |  e
> +  |
> +  |
> +  |  diff --git a/f1a b/f1e
> +  |  rename from f1a
> +  |  rename to f1e
> +  |  diff --git a/f2a b/f2a
> +  |  --- a/f2a
> +  |  +++ b/f2a
> +  |  @@ -1,1 +1,1 @@
> +  |  -c2a
> +  |  +c2e
> +  |  diff --git a/f3a b/f3e
> +  |  rename from f3a
> +  |  rename to f3e
> +  |  diff --git a/f4a b/f4e
> +  |  rename from f4a
> +  |  rename to f4e
> +  |  diff --git a/f5a b/f5b
> +  |  rename from f5a
> +  |  rename to f5b
> +  |
> +  | o  changeset:   5:29f6ffdbca28
> +  | |  user:        test
> +  | |  date:        Thu Jan 01 00:00:00 1970 +0000
> +  | |  files:       f3d f4a
> +  | |  description:
> +  | |  d
> +  | |
> +  | |
> +  | |  diff --git a/f3d b/f3d
> +  | |  new file mode 100644
> +  | |  --- /dev/null
> +  | |  +++ b/f3d
> +  | |  @@ -0,0 +1,1 @@
> +  | |  +c3a
> +  | |  diff --git a/f4a b/f4a
> +  | |  --- a/f4a
> +  | |  +++ b/f4a
> +  | |  @@ -1,1 +1,1 @@
> +  | |  -c4a
> +  | |  +c4d
> +  | |
> +  | o  changeset:   4:68af396ea7bf
> +  |/   parent:      0:3340a7726e9e
> +  |    user:        test
> +  |    date:        Thu Jan 01 00:00:00 1970 +0000
> +  |    files:       f1a f2a f2c f5a
> +  |    copies:      f2c (f2a)
> +  |    description:
> +  |    c
> +  |
> +  |
> +  |    diff --git a/f1a b/f1a
> +  |    --- a/f1a
> +  |    +++ b/f1a
> +  |    @@ -1,1 +1,1 @@
> +  |    -c1a
> +  |    +c1c
> +  |    diff --git a/f2a b/f2c
> +  |    rename from f2a
> +  |    rename to f2c
> +  |    diff --git a/f5a b/f5a
> +  |    --- a/f5a
> +  |    +++ b/f5a
> +  |    @@ -1,1 +1,1 @@
> +  |    -c5a
> +  |    +c5c
> +  |
> +  | o  changeset:   3:aa2584f6dee9
> +  | |  user:        test
> +  | |  date:        Thu Jan 01 00:00:00 1970 +0000
> +  | |  files:       f3b f3d f4a
> +  | |  copies:      f3d (f3b)
> +  | |  description:
> +  | |  d
> +  | |
> +  | |
> +  | |  diff --git a/f3b b/f3d
> +  | |  rename from f3b
> +  | |  rename to f3d
> +  | |  diff --git a/f4a b/f4a
> +  | |  --- a/f4a
> +  | |  +++ b/f4a
> +  | |  @@ -1,1 +1,1 @@
> +  | |  -c4a
> +  | |  +c4d
> +  | |
> +  | o  changeset:   2:c8d3926d7649
> +  | |  user:        test
> +  | |  date:        Thu Jan 01 00:00:00 1970 +0000
> +  | |  files:       f1b f2a f2c f5a f5b
> +  | |  copies:      f2c (f2a) f5a (f5b)
> +  | |  description:
> +  | |  c
> +  | |
> +  | |
> +  | |  diff --git a/f1b b/f1b
> +  | |  --- a/f1b
> +  | |  +++ b/f1b
> +  | |  @@ -1,1 +1,1 @@
> +  | |  -c1a
> +  | |  +c1c
> +  | |  diff --git a/f2a b/f2c
> +  | |  rename from f2a
> +  | |  rename to f2c
> +  | |  diff --git a/f5b b/f5a
> +  | |  rename from f5b
> +  | |  rename to f5a
> +  | |  --- a/f5b
> +  | |  +++ b/f5a
> +  | |  @@ -1,1 +1,1 @@
> +  | |  -c5a
> +  | |  +c5c
> +  | |
> +  | o  changeset:   1:7e9aff31b586
> +  |/   user:        test
> +  |    date:        Thu Jan 01 00:00:00 1970 +0000
> +  |    files:       f1a f1b f3a f3b f5a f5b
> +  |    copies:      f1b (f1a) f3b (f3a) f5b (f5a)
> +  |    description:
> +  |    b
> +  |
> +  |
> +  |    diff --git a/f1a b/f1b
> +  |    rename from f1a
> +  |    rename to f1b
> +  |    diff --git a/f3a b/f3b
> +  |    rename from f3a
> +  |    rename to f3b
> +  |    diff --git a/f5a b/f5b
> +  |    rename from f5a
> +  |    rename to f5b
> +  |
> +  o  changeset:   0:3340a7726e9e
> +     user:        test
> +     date:        Thu Jan 01 00:00:00 1970 +0000
> +     files:       f1a f2a f3a f4a f5a
> +     description:
> +     a
> +
> +
> +     diff --git a/f1a b/f1a
> +     new file mode 100644
> +     --- /dev/null
> +     +++ b/f1a
> +     @@ -0,0 +1,1 @@
> +     +c1a
> +     diff --git a/f2a b/f2a
> +     new file mode 100644
> +     --- /dev/null
> +     +++ b/f2a
> +     @@ -0,0 +1,1 @@
> +     +c2a
> +     diff --git a/f3a b/f3a
> +     new file mode 100644
> +     --- /dev/null
> +     +++ b/f3a
> +     @@ -0,0 +1,1 @@
> +     +c3a
> +     diff --git a/f4a b/f4a
> +     new file mode 100644
> +     --- /dev/null
> +     +++ b/f4a
> +     @@ -0,0 +1,1 @@
> +     +c4a
> +     diff --git a/f5a b/f5a
> +     new file mode 100644
> +     --- /dev/null
> +     +++ b/f5a
> +     @@ -0,0 +1,1 @@
> +     +c5a
> +
> +  $ hg cat f2c
> +  c2e
> diff -r 5977d6569b2e -r 9e59cd55604c tests/test-rebase-conflicts.t
> --- a/tests/test-rebase-conflicts.tTue Oct 04 12:51:54 2016 +0200
> +++ b/tests/test-rebase-conflicts.tMon Oct 03 18:38:13 2016 +0200
> @@ -238,6 +238,10 @@
>     merge against 9:e31216eec445
>       detach base 8:8e4e2c1a07ae
>      searching for copies back to rev 3
> +    computing unmatched files in rotated DAG
> +    computing unmatched files in unrotated DAG
> +    unmatched files in other:
> +     f2.txt
>    resolving manifests
>     branchmerge: True, force: True, partial: False
>     ancestor: 8e4e2c1a07ae, local: 4bc80088dc6b+, remote: e31216eec445 @@
> -255,6 +259,10 @@
>     merge against 10:2f2496ddf49d
>       detach base 9:e31216eec445
>      searching for copies back to rev 3
> +    computing unmatched files in rotated DAG
> +    computing unmatched files in unrotated DAG
> +    unmatched files in other:
> +     f2.txt
>    resolving manifests
>     branchmerge: True, force: True, partial: False
>     ancestor: e31216eec445, local: 19c888675e13+, remote: 2f2496ddf49d


More information about the Mercurial-devel mailing list