changeset 52505:68dc6cecca32

graft: add a `--to` flag grafting in memory See inline documentation for details.
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Mon, 02 Dec 2024 02:45:41 +0100
parents de16800904f9
children 199b0e62b403
files mercurial/cmd_impls/graft.py mercurial/commands.py tests/test-completion.t tests/test-graft.t
diffstat 4 files changed, 438 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/cmd_impls/graft.py	Mon Dec 02 02:41:57 2024 +0100
+++ b/mercurial/cmd_impls/graft.py	Mon Dec 02 02:45:41 2024 +0100
@@ -11,7 +11,14 @@
 
 from ..i18n import _
 
-from .. import cmdutil, error, logcmdutil, merge as mergemod, state as statemod
+from .. import (
+    cmdutil,
+    context,
+    error,
+    logcmdutil,
+    merge as mergemod,
+    state as statemod,
+)
 
 
 if typing.TYPE_CHECKING:
@@ -33,8 +40,11 @@
         return _stopgraft(ui, repo, graftstate)
     elif action == "GRAFT":
         return _graft_revisions(ui, repo, graftstate, *args)
+    elif action == "GRAFT-TO":
+        return _graft_revisions_in_memory(ui, repo, graftstate, *args)
     else:
-        raise error.ProgrammingError('unknown action: %s' % action)
+        msg = b'unknown action: %s' % action.encode('ascii')
+        raise error.ProgrammingError(msg)
 
 
 def _process_args(
@@ -61,11 +71,20 @@
     statedata[b'newnodes'] = []
 
     # argument incompatible with followup from an interrupted operation
-    commit_args = ['edit', 'log', 'user', 'date', 'currentdate', 'currentuser']
+    commit_args = [
+        'edit',
+        'log',
+        'user',
+        'date',
+        'currentdate',
+        'currentuser',
+        'to',
+    ]
     nofollow_args = commit_args + ['base', 'rev']
 
     arg_compatibilities = [
         ('no_commit', commit_args),
+        ('continue', ['to']),
         ('stop', nofollow_args),
         ('abort', nofollow_args),
     ]
@@ -79,6 +98,12 @@
 
     graftstate = statemod.cmdstate(repo, b'graftstate')
 
+    if opts.get('to'):
+        toctx = logcmdutil.revsingle(repo, opts['to'], None)
+        statedata[b'to'] = toctx.hex()
+    else:
+        toctx = repo[None].p1()
+
     if opts.get('stop'):
         return "STOP", graftstate, None
     elif opts.get('abort'):
@@ -102,7 +127,8 @@
         raise error.InputError(_(b'no revisions specified'))
     else:
         cmdutil.checkunfinished(repo)
-        cmdutil.bailifchanged(repo)
+        if not opts.get('to'):
+            cmdutil.bailifchanged(repo)
         revs = logcmdutil.revrange(repo, revs)
 
     for o in (
@@ -142,7 +168,7 @@
     # already, they'd have been in the graftstate.
     if not (cont or opts.get('force')) and basectx is None:
         # check for ancestors of dest branch
-        ancestors = repo.revs(b'%ld & (::.)', revs)
+        ancestors = repo.revs(b'%ld & (::%d)', revs, toctx.rev())
         for rev in ancestors:
             ui.warn(_(b'skipping ancestor revision %d:%s\n') % (rev, repo[rev]))
 
@@ -164,7 +190,7 @@
 
         # The only changesets we can be sure doesn't contain grafts of any
         # revs, are the ones that are common ancestors of *all* revs:
-        for rev in repo.revs(b'only(%d,ancestor(%ld))', repo[b'.'].rev(), revs):
+        for rev in repo.revs(b'only(%d,ancestor(%ld))', toctx.rev(), revs):
             ctx = repo[rev]
             n = ctx.extra().get(b'source')
             if n in ids:
@@ -216,6 +242,8 @@
     editor = cmdutil.getcommiteditor(editform=b'graft', **opts)
     dry_run = bool(opts.get("dry_run"))
     tool = opts.get('tool', b'')
+    if opts.get("to"):
+        return "GRAFT-TO", graftstate, (statedata, revs, editor, dry_run, tool)
     return "GRAFT", graftstate, (statedata, revs, editor, cont, dry_run, tool)
 
 
@@ -247,6 +275,65 @@
     return (user, date, message, extra)
 
 
+def _graft_revisions_in_memory(
+    ui,
+    repo,
+    graftstate,
+    statedata,
+    revs,
+    editor,
+    dry_run,
+    tool=b'',
+):
+    """graft revisions in memory
+
+    Abort on unresolved conflicts.
+    """
+    with repo.lock(), repo.transaction(b"graft"):
+        target = repo[statedata[b"to"]]
+        for r in revs:
+            ctx = repo[r]
+            ui.status(_build_progress(ui, repo, ctx))
+            if dry_run:
+                # we might want to actually perform the grafting to detect
+                # potential conflict in the dry run.
+                continue
+            wctx = context.overlayworkingctx(repo)
+            wctx.setbase(target)
+            if b'base' in statedata:
+                base = repo[statedata[b'base']]
+            else:
+                base = ctx.p1()
+
+            (user, date, message, extra) = _build_meta(ui, repo, ctx, statedata)
+
+            # perform the graft merge with p1(rev) as 'ancestor'
+            try:
+                overrides = {(b'ui', b'forcemerge'): tool}
+                with ui.configoverride(overrides, b'graft'):
+                    mergemod.graft(
+                        repo,
+                        ctx,
+                        base,
+                        wctx=wctx,
+                    )
+            except error.InMemoryMergeConflictsError as e:
+                raise error.Abort(
+                    b'cannot graft in memory: merge conflicts',
+                    hint=_(bytes(e)),
+                )
+            mctx = wctx.tomemctx(
+                message,
+                user=user,
+                date=date,
+                extra=extra,
+                editor=editor,
+            )
+            node = repo.commitctx(mctx)
+            target = repo[node]
+        return 0
+
+
 def _graft_revisions(
     ui,
     repo,
--- a/mercurial/commands.py	Mon Dec 02 02:41:57 2024 +0100
+++ b/mercurial/commands.py	Mon Dec 02 02:45:41 2024 +0100
@@ -2971,6 +2971,12 @@
             _(b'base revision when doing the graft merge (ADVANCED)'),
             _(b'REV'),
         ),
+        (
+            b'',
+            b'to',
+            b'',
+            _(b'graft to this destination, in memory (EXPERIMENTAL)'),
+        ),
         (b'c', b'continue', False, _(b'resume interrupted graft')),
         (b'', b'stop', False, _(b'stop interrupted graft')),
         (b'', b'abort', False, _(b'abort interrupted graft')),
@@ -3061,6 +3067,16 @@
 
     .. container:: verbose
 
+        The experimental --to option allow to graft a revision in memory,
+        independently from the working copy. Merge conflict are not currenly
+        supported and the operation will be aborted if the configured tool
+        cannot handle the conflict that might be encountered.
+
+        As the operation is performence in memory, the on disk file will not be
+        modified and some hooks might not be run.
+
+    .. container:: verbose
+
       Examples:
 
       - copy a single change to the stable branch and edit its description::
--- a/tests/test-completion.t	Mon Dec 02 02:41:57 2024 +0100
+++ b/tests/test-completion.t	Mon Dec 02 02:45:41 2024 +0100
@@ -360,7 +360,7 @@
   export: bookmark, output, switch-parent, rev, text, git, binary, nodates, template
   files: rev, print0, include, exclude, template, subrepos
   forget: interactive, include, exclude, dry-run
-  graft: rev, base, continue, stop, abort, edit, log, no-commit, force, currentdate, currentuser, date, user, tool, dry-run
+  graft: rev, base, to, 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
--- a/tests/test-graft.t	Mon Dec 02 02:41:57 2024 +0100
+++ b/tests/test-graft.t	Mon Dec 02 02:45:41 2024 +0100
@@ -882,6 +882,8 @@
   grafting 23:72d9c7c75bcc "24"
   note: graft of 23:72d9c7c75bcc created no changes to commit
 
+  $ pwd
+  $TESTTMP/a
   $ cd ..
 
 Graft to duplicate a commit
@@ -921,3 +923,329 @@
   |/
   o  0
   
+  $ cd ../
+
+In memory graft with --to
+=========================
+
+
+setup a repository
+
+  $ hg init base-to
+  $ cd base-to
+  $ hg debugbuilddag -m ".:base..:dst*base.:src*base..:wc"
+  $ hg up "wc"
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg log -G -T '{rev}:{desc} {tags}\n'
+  @  7:r7 tip wc
+  |
+  o  6:r6
+  |
+  o  5:r5
+  |
+  | o  4:r4 src
+  | |
+  | o  3:r3
+  |/
+  | o  2:r2 dst
+  | |
+  | o  1:r1
+  |/
+  o  0:r0 base
+  
+
+  $ cd ..
+
+Simple test
+-----------
+
+As few special case as possible
+
+  $ cp -R base-to test-to-simple
+  $ cd test-to-simple
+  $ hg graft --rev src --to dst
+  grafting 4:4178b3134f52 "r4" (src)
+  merging mf
+  $ hg log -G -T '{rev}:{desc} {tags}\n'
+  o  8:r4 tip
+  |
+  | @  7:r7 wc
+  | |
+  | o  6:r6
+  | |
+  | o  5:r5
+  | |
+  | | o  4:r4 src
+  | | |
+  | | o  3:r3
+  | |/
+  o |  2:r2 dst
+  | |
+  o |  1:r1
+  |/
+  o  0:r0 base
+  
+  $ cd ..
+
+Single changeset, local changes
+-------------------------------
+
+Run "graft --to" with local changes
+
+  $ cp -R base-to test-to-local-change
+  $ cd test-to-local-change
+  $ hg st --all
+  C mf
+  $ echo foo >> mf
+  $ hg status
+  M mf
+  $ hg graft --rev src --to dst
+  grafting 4:4178b3134f52 "r4" (src)
+  merging mf
+
+local file should not have been touched.
+
+  $ hg status
+  M mf
+  $ hg log -G -T '{rev}:{desc} {tags}\n'
+  o  8:r4 tip
+  |
+  | @  7:r7 wc
+  | |
+  | o  6:r6
+  | |
+  | o  5:r5
+  | |
+  | | o  4:r4 src
+  | | |
+  | | o  3:r3
+  | |/
+  o |  2:r2 dst
+  | |
+  o |  1:r1
+  |/
+  o  0:r0 base
+  
+  $ cd ..
+
+Multiple linear changesets
+--------------------------
+
+grafting multiple linear changesets
+
+  $ cp -R base-to test-to-multiple-linear
+  $ cd test-to-multiple-linear
+  $ hg graft --rev 'src~1::src' --to dst
+  grafting 3:181578a106da "r3"
+  merging mf
+  grafting 4:4178b3134f52 "r4" (src)
+  merging mf
+  $ hg log -G -T '{rev}:{desc} {tags}\n'
+  o  9:r4 tip
+  |
+  o  8:r3
+  |
+  | @  7:r7 wc
+  | |
+  | o  6:r6
+  | |
+  | o  5:r5
+  | |
+  | | o  4:r4 src
+  | | |
+  | | o  3:r3
+  | |/
+  o |  2:r2 dst
+  | |
+  o |  1:r1
+  |/
+  o  0:r0 base
+  
+  $ cd ..
+
+Multiple unrelated changesets
+--------------------------
+
+Grafting multiple changesets on different branch
+
+The order specified on the command line should be preserved.
+The result should be linear.
+
+  $ cp -R base-to test-to-multiple-unrelated
+  $ cd test-to-multiple-unrelated
+  $ hg graft 'src' 'wc~1' 'src~1' --to dst
+  grafting 4:4178b3134f52 "r4" (src)
+  merging mf
+  grafting 6:735f0f7a080b "r6"
+  merging mf
+  grafting 3:181578a106da "r3"
+  merging mf
+  $ hg log -G -T '{rev}:{desc} {tags}\n'
+  o  10:r3 tip
+  |
+  o  9:r6
+  |
+  o  8:r4
+  |
+  | @  7:r7 wc
+  | |
+  | o  6:r6
+  | |
+  | o  5:r5
+  | |
+  | | o  4:r4 src
+  | | |
+  | | o  3:r3
+  | |/
+  o |  2:r2 dst
+  | |
+  o |  1:r1
+  |/
+  o  0:r0 base
+  
+  $ cd ..
+
+with base
+---------
+
+  $ cp -R base-to test-to-base
+  $ cd test-to-base
+  $ hg graft --base base src --to dst
+  grafting 4:4178b3134f52 "r4" (src)
+  merging mf
+  $ hg log -G -T '{rev}:{desc} {tags}\n'
+  o  8:r4 tip
+  |
+  | @  7:r7 wc
+  | |
+  | o  6:r6
+  | |
+  | o  5:r5
+  | |
+  | | o  4:r4 src
+  | | |
+  | | o  3:r3
+  | |/
+  o |  2:r2 dst
+  | |
+  o |  1:r1
+  |/
+  o  0:r0 base
+  
+  $ hg diff --from base --to src
+  diff -r 93cbaf5e6529 -r 4178b3134f52 mf
+  --- a/mf	Thu Jan 01 00:00:00 1970 +0000
+  +++ b/mf	Thu Jan 01 00:00:04 1970 +0000
+  @@ -4,9 +4,9 @@
+   3
+   4
+   5
+  -6
+  +6 r3
+   7
+  -8
+  +8 r4
+   9
+   10
+   11
+  $ hg export src
+  # HG changeset patch
+  # User debugbuilddag
+  # Date 4 0
+  #      Thu Jan 01 00:00:04 1970 +0000
+  # Node ID 4178b3134f5224d297d3b9e0e98b983f42e53d55
+  # Parent  181578a106daabea66d4465f4883f7f8552bbc9d
+  r4
+  
+  diff -r 181578a106da -r 4178b3134f52 mf
+  --- a/mf	Thu Jan 01 00:00:03 1970 +0000
+  +++ b/mf	Thu Jan 01 00:00:04 1970 +0000
+  @@ -6,7 +6,7 @@
+   5
+   6 r3
+   7
+  -8
+  +8 r4
+   9
+   10
+   11
+  $ hg export tip
+  # HG changeset patch
+  # User debugbuilddag
+  # Date 4 0
+  #      Thu Jan 01 00:00:04 1970 +0000
+  # Node ID 40112ab60ecb01882916c1a4439c798746e34165
+  # Parent  37d4c1cec295ddfa401f4a365e15a82a1974b056
+  r4
+  
+  diff -r 37d4c1cec295 -r 40112ab60ecb mf
+  --- a/mf	Thu Jan 01 00:00:02 1970 +0000
+  +++ b/mf	Thu Jan 01 00:00:04 1970 +0000
+  @@ -4,9 +4,9 @@
+   3
+   4 r2
+   5
+  -6
+  +6 r3
+   7
+  -8
+  +8 r4
+   9
+   10
+   11
+  $ cd ..
+
+with conflict
+-------------
+
+We should abort in case of conflict and rollback any grafted procress
+
+  $ cp -R base-to test-to-conflict
+  $ cd test-to-conflict
+  $ hg up src
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ echo this-will-conflict >> mf
+  $ hg ci -m 'this-will-conflict'
+  $ hg up dst
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ echo conflict-this-will-conflict >> mf
+  $ hg ci -m 'conflict-this-will'
+  $ hg up wc
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg graft --to 'max(dst::)' src:: --dry-run
+  grafting 4:4178b3134f52 "r4" (src)
+  grafting 8:9fa2d3fe2323 "this-will-conflict"
+  $ hg graft --to 'max(dst::)' src::
+  grafting 4:4178b3134f52 "r4" (src)
+  merging mf
+  grafting 8:9fa2d3fe2323 "this-will-conflict"
+  merging mf
+  transaction abort!
+  rollback completed
+  abort: cannot graft in memory: merge conflicts
+  (in-memory merge does not support merge conflicts)
+  [255]
+  $ hg log -G -T '{rev}:{desc} {tags}\n'
+  o  9:conflict-this-will tip
+  |
+  | o  8:this-will-conflict
+  | |
+  | | @  7:r7 wc
+  | | |
+  | | o  6:r6
+  | | |
+  | | o  5:r5
+  | | |
+  | o |  4:r4 src
+  | | |
+  | o |  3:r3
+  | |/
+  o |  2:r2 dst
+  | |
+  o |  1:r1
+  |/
+  o  0:r0 base
+  
+  $ cd ..
+
+