mercurial/cmd_impls/graft.py
changeset 52505 68dc6cecca32
parent 52504 de16800904f9
--- 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,