graft: add a `--to` flag grafting in memory
See inline documentation for details.
--- 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 ..
+
+