[PATCH v2] graft: introduce --base option for using custom base revision while merging

Mads Kiilerich mads at kiilerich.com
Sun Oct 14 15:15:57 UTC 2018


# HG changeset patch
# User Mads Kiilerich <mads at kiilerich.com>
# Date 1539529698 -7200
#      Sun Oct 14 17:08:18 2018 +0200
# Node ID 258029c642d97ef663396476c63ce34dbef89b13
# Parent  38ac525b44c93fcadb3680d4ded56f1e5a0029b2
graft: introduce --base option for using custom base revision while merging

The graft command usually performs an internal merge of the current parent
revision with the graft revision, using p1 of the grafted revision as base for
the merge.

As a trivial extension of this, we introduce the --base option to allow for
using another base revision.

This can be used as a building block for grafting and collapsing multiple
changesets at once, or for grafting the resulting change from a merge as a
single simple change. (This is kind of similar to backout --parent ... only
different: this graft base must be an ancestor, but is usually *not* a parent.)

This is probably an advanced use case, and we do thus not show it in the
non-verbose help.

diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -2223,6 +2223,8 @@ def forget(ui, repo, *pats, **opts):
 @command(
     'graft',
     [('r', 'rev', [], _('revisions to graft'), _('REV')),
+     ('', 'base', '',
+      _('base revision when doing the graft merge (ADVANCED)'), _('REV')),
      ('c', 'continue', False, _('resume interrupted graft')),
      ('', 'stop', False, _('stop interrupted graft')),
      ('', 'abort', False, _('abort interrupted graft')),
@@ -2267,6 +2269,35 @@ def graft(ui, repo, *revs, **opts):
 
     .. container:: verbose
 
+      The --base option exposes more of how graft internally uses merge with a
+      custom base revision. --base can be used to specify another ancestor than
+      the first and only parent.
+
+      The command::
+
+        hg graft -r 345 --base 234
+
+      is thus pretty much the same as::
+
+        hg diff -r 234 -r 345 | hg import
+
+      but using merge to resolve conflicts and track moved files.
+
+      The result of a merge can thus be backported as a single commit by
+      specifying one of the merge parents as base, and thus effectively
+      grafting the changes from the other side.
+
+      It is also possible to collapse multiple changesets and clean up history
+      by specifying another ancestor as base, much like rebase --collapse
+      --keep.
+
+      The commit message can be tweaked after the fact using commit --amend .
+
+      For using non-ancestors as the base to backout changes, see the backout
+      command and the hidden --parent option.
+
+    .. container:: verbose
+
       Examples:
 
       - copy a single change to the stable branch and edit its description::
@@ -2290,6 +2321,15 @@ def graft(ui, repo, *revs, **opts):
 
           hg log -r "sort(all(), date)"
 
+      - backport the result of a merge as a single commit::
+
+          hg graft -r 123 --base 123^
+
+      - land a feature branch as one changeset::
+
+          hg up -cr default
+          hg graft -r featureX --base "ancestor('featureX', 'default')"
+
     See :hg:`help revisions` for more about specifying revisions.
 
     Returns 0 on successful completion.
@@ -2305,6 +2345,9 @@ def _dograft(ui, repo, *revs, **opts):
 
     revs = list(revs)
     revs.extend(opts.get('rev'))
+    basectx = None
+    if opts.get('base'):
+        basectx = scmutil.revsingle(repo, opts['base'], None)
     # a dict of data to be stored in state file
     statedata = {}
     # list of new nodes created by ongoing graft
@@ -2384,13 +2427,16 @@ def _dograft(ui, repo, *revs, **opts):
         revs = scmutil.revrange(repo, revs)
 
     skipped = set()
-    # check for merges
-    for rev in repo.revs('%ld and merge()', revs):
-        ui.warn(_('skipping ungraftable merge revision %d\n') % rev)
-        skipped.add(rev)
+    if basectx is None:
+        # check for merges
+        for rev in repo.revs('%ld and merge()', revs):
+            ui.warn(_('skipping ungraftable merge revision %d\n') % rev)
+            skipped.add(rev)
     revs = [r for r in revs if r not in skipped]
     if not revs:
         return -1
+    if basectx is not None and len(revs) != 1:
+        raise error.Abort(_('only one revision allowed with --base '))
 
     # Don't check in the --continue case, in effect retaining --force across
     # --continues. That's because without --force, any revisions we decided to
@@ -2398,7 +2444,7 @@ def _dograft(ui, repo, *revs, **opts):
     # way to the graftstate. With --force, any revisions we would have otherwise
     # skipped would not have been filtered out, and if they hadn't been applied
     # already, they'd have been in the graftstate.
-    if not (cont or opts.get('force')):
+    if not (cont or opts.get('force')) and basectx is None:
         # check for ancestors of dest branch
         crev = repo['.'].rev()
         ancestors = repo.changelog.ancestors([crev], inclusive=True)
@@ -2494,8 +2540,9 @@ def _dograft(ui, repo, *revs, **opts):
         if not cont:
             # perform the graft merge with p1(rev) as 'ancestor'
             overrides = {('ui', 'forcemerge'): opts.get('tool', '')}
+            base = ctx.p1() if basectx is None else basectx
             with ui.configoverride(overrides, 'graft'):
-                stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'graft'])
+                stats = mergemod.graft(repo, ctx, base, ['local', 'graft'])
             # report any conflicts
             if stats.unresolvedcount > 0:
                 # write out state for --continue
diff --git a/tests/test-completion.t b/tests/test-completion.t
--- a/tests/test-completion.t
+++ b/tests/test-completion.t
@@ -318,7 +318,7 @@ Show all commands + options
   debugwireargs: three, four, five, ssh, remotecmd, insecure
   debugwireproto: localssh, peer, noreadstderr, nologhandshake, ssh, remotecmd, insecure
   files: rev, print0, include, exclude, template, subrepos
-  graft: rev, continue, stop, abort, edit, log, no-commit, force, currentdate, currentuser, date, user, tool, dry-run
+  graft: rev, base, continue, stop, abort, edit, log, no-commit, force, currentdate, currentuser, date, user, tool, dry-run
   grep: print0, all, diff, text, follow, ignore-case, files-with-matches, line-number, rev, all-files, user, date, template, include, exclude
   heads: rev, topo, active, closed, style, template
   help: extension, command, keyword, system
diff --git a/tests/test-graft.t b/tests/test-graft.t
--- a/tests/test-graft.t
+++ b/tests/test-graft.t
@@ -25,7 +25,7 @@ Create a repo with some stuff in it:
   $ echo b > e
   $ hg branch -q stable
   $ hg ci -m5
-  $ hg merge -q default --tool internal:local
+  $ hg merge -q default --tool internal:local # for conflicts in e, choose 5 and ignore 4
   $ hg branch -q default
   $ hg ci -m6
   $ hg phase --public 3
@@ -46,8 +46,40 @@ Create a repo with some stuff in it:
   |
   o  test at 0.public: 0
   
+Test --base for grafting the merge of 4 from the perspective of 5, thus only getting the change to d
+
+  $ hg up -cqr 3
+  $ hg graft -r 6 --base 5
+  grafting 6:25a2b029d3ae "6" (tip)
+  merging e
+  $ hg st --change .
+  M d
+
+  $ hg -q strip . --config extensions.strip=
+
+Test --base for collapsing changesets 2 and 3, thus getting both b and c
+
+  $ hg up -cqr 0
+  $ hg graft -r 3 --base 1
+  grafting 3:4c60f11aa304 "3"
+  merging a and b to b
+  merging a and c to c
+  $ hg st --change .
+  A b
+  A c
+  R a
+
+  $ hg -q strip . --config extensions.strip=
+
+Specifying child as --base revision fails safely (perhaps slightly confusing, but consistent)
+
+  $ hg graft -r 2 --base 3
+  grafting 2:5c095ad7e90f "2"
+  note: graft of 2:5c095ad7e90f created no changes to commit
+
 Can't continue without starting:
 
+  $ hg -q up -cr tip
   $ hg rm -q e
   $ hg graft --continue
   abort: no graft in progress


More information about the Mercurial-devel mailing list