diff --git a/mercurial/cmd_impls/graft.py b/mercurial/cmd_impls/graft.py --- a/mercurial/cmd_impls/graft.py +++ b/mercurial/cmd_impls/graft.py @@ -11,7 +11,14 @@ from typing import ( 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 @@ def cmd_graft(ui, repo, *revs, **opts) - 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 @@ def _process_args( 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 @@ def _process_args( 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 @@ def _process_args( 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 @@ def _process_args( # 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 @@ def _process_args( # 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 @@ def _process_args( 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 @@ def _build_meta(ui, repo, ctx, statedata 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, diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -2971,6 +2971,12 @@ def forget(ui, repo, *pats, **opts): _(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 @@ def graft(ui, repo, *revs, **opts): .. 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:: diff --git a/tests/test-completion.t b/tests/test-completion.t --- a/tests/test-completion.t +++ b/tests/test-completion.t @@ -360,7 +360,7 @@ Show all commands + options 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 diff --git a/tests/test-graft.t b/tests/test-graft.t --- a/tests/test-graft.t +++ b/tests/test-graft.t @@ -882,6 +882,8 @@ Empty graft 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 @@ Graft to duplicate a commit twice |/ 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 .. + +