--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/cmd_impls/graft.py Tue Nov 19 15:46:12 2024 +0100
@@ -0,0 +1,284 @@
+# graft.py - implementation of the graft command
+
+from ..i18n import _
+
+from .. import cmdutil, error, logcmdutil, merge as mergemod, state as statemod
+
+
+def cmd_graft(ui, repo, *revs, **opts):
+ """implement the graft command as defined in mercuria/commands.py"""
+ if revs and opts.get('rev'):
+ ui.warn(
+ _(
+ b'warning: inconsistent use of --rev might give unexpected '
+ b'revision ordering!\n'
+ )
+ )
+
+ revs = list(revs)
+ revs.extend(opts.get('rev'))
+ # a dict of data to be stored in state file
+ statedata = {}
+ # list of new nodes created by ongoing graft
+ statedata[b'newnodes'] = []
+
+ cmdutil.resolve_commit_options(ui, opts)
+
+ editor = cmdutil.getcommiteditor(editform=b'graft', **opts)
+
+ cmdutil.check_at_most_one_arg(opts, 'abort', 'stop', 'continue')
+
+ cont = False
+ if opts.get('no_commit'):
+ cmdutil.check_incompatible_arguments(
+ opts,
+ 'no_commit',
+ ['edit', 'currentuser', 'currentdate', 'log'],
+ )
+
+ graftstate = statemod.cmdstate(repo, b'graftstate')
+
+ if opts.get('stop'):
+ cmdutil.check_incompatible_arguments(
+ opts,
+ 'stop',
+ [
+ 'edit',
+ 'log',
+ 'user',
+ 'date',
+ 'currentdate',
+ 'currentuser',
+ 'rev',
+ ],
+ )
+ return _stopgraft(ui, repo, graftstate)
+ elif opts.get('abort'):
+ cmdutil.check_incompatible_arguments(
+ opts,
+ 'abort',
+ [
+ 'edit',
+ 'log',
+ 'user',
+ 'date',
+ 'currentdate',
+ 'currentuser',
+ 'rev',
+ ],
+ )
+ return cmdutil.abortgraft(ui, repo, graftstate)
+ elif opts.get('continue'):
+ cont = True
+ if revs:
+ raise error.InputError(_(b"can't specify --continue and revisions"))
+ # read in unfinished revisions
+ if graftstate.exists():
+ statedata = cmdutil.readgraftstate(repo, graftstate)
+ if statedata.get(b'date'):
+ opts['date'] = statedata[b'date']
+ if statedata.get(b'user'):
+ opts['user'] = statedata[b'user']
+ if statedata.get(b'log'):
+ opts['log'] = True
+ if statedata.get(b'no_commit'):
+ opts['no_commit'] = statedata.get(b'no_commit')
+ if statedata.get(b'base'):
+ opts['base'] = statedata.get(b'base')
+ nodes = statedata[b'nodes']
+ revs = [repo[node].rev() for node in nodes]
+ else:
+ cmdutil.wrongtooltocontinue(repo, _(b'graft'))
+ else:
+ if not revs:
+ raise error.InputError(_(b'no revisions specified'))
+ cmdutil.checkunfinished(repo)
+ cmdutil.bailifchanged(repo)
+ revs = logcmdutil.revrange(repo, revs)
+
+ skipped = set()
+ basectx = None
+ if opts.get('base'):
+ basectx = logcmdutil.revsingle(repo, opts['base'], None)
+ if basectx is None:
+ # check for merges
+ for rev in repo.revs(b'%ld and merge()', revs):
+ ui.warn(_(b'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.InputError(_(b'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
+ # skip would have been filtered out here, so they wouldn't have made their
+ # 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')) and basectx is None:
+ # check for ancestors of dest branch
+ ancestors = repo.revs(b'%ld & (::.)', revs)
+ for rev in ancestors:
+ ui.warn(_(b'skipping ancestor revision %d:%s\n') % (rev, repo[rev]))
+
+ revs = [r for r in revs if r not in ancestors]
+
+ if not revs:
+ return -1
+
+ # analyze revs for earlier grafts
+ ids = {}
+ for ctx in repo.set(b"%ld", revs):
+ ids[ctx.hex()] = ctx.rev()
+ n = ctx.extra().get(b'source')
+ if n:
+ ids[n] = ctx.rev()
+
+ # check ancestors for earlier grafts
+ ui.debug(b'scanning for duplicate grafts\n')
+
+ # 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):
+ ctx = repo[rev]
+ n = ctx.extra().get(b'source')
+ if n in ids:
+ try:
+ r = repo[n].rev()
+ except error.RepoLookupError:
+ r = None
+ if r in revs:
+ ui.warn(
+ _(
+ b'skipping revision %d:%s '
+ b'(already grafted to %d:%s)\n'
+ )
+ % (r, repo[r], rev, ctx)
+ )
+ revs.remove(r)
+ elif ids[n] in revs:
+ if r is None:
+ ui.warn(
+ _(
+ b'skipping already grafted revision %d:%s '
+ b'(%d:%s also has unknown origin %s)\n'
+ )
+ % (ids[n], repo[ids[n]], rev, ctx, n[:12])
+ )
+ else:
+ ui.warn(
+ _(
+ b'skipping already grafted revision %d:%s '
+ b'(%d:%s also has origin %d:%s)\n'
+ )
+ % (ids[n], repo[ids[n]], rev, ctx, r, n[:12])
+ )
+ revs.remove(ids[n])
+ elif ctx.hex() in ids:
+ r = ids[ctx.hex()]
+ if r in revs:
+ ui.warn(
+ _(
+ b'skipping already grafted revision %d:%s '
+ b'(was grafted from %d:%s)\n'
+ )
+ % (r, repo[r], rev, ctx)
+ )
+ revs.remove(r)
+ if not revs:
+ return -1
+
+ if opts.get('no_commit'):
+ statedata[b'no_commit'] = True
+ if opts.get('base'):
+ statedata[b'base'] = opts['base']
+ for pos, ctx in enumerate(repo.set(b"%ld", revs)):
+ desc = b'%d:%s "%s"' % (
+ ctx.rev(),
+ ctx,
+ ctx.description().split(b'\n', 1)[0],
+ )
+ names = repo.nodetags(ctx.node()) + repo.nodebookmarks(ctx.node())
+ if names:
+ desc += b' (%s)' % b' '.join(names)
+ ui.status(_(b'grafting %s\n') % desc)
+ if opts.get('dry_run'):
+ continue
+
+ source = ctx.extra().get(b'source')
+ extra = {}
+ if source:
+ extra[b'source'] = source
+ extra[b'intermediate-source'] = ctx.hex()
+ else:
+ extra[b'source'] = ctx.hex()
+ user = ctx.user()
+ if opts.get('user'):
+ user = opts['user']
+ statedata[b'user'] = user
+ date = ctx.date()
+ if opts.get('date'):
+ date = opts['date']
+ statedata[b'date'] = date
+ message = ctx.description()
+ if opts.get('log'):
+ message += b'\n(grafted from %s)' % ctx.hex()
+ statedata[b'log'] = True
+
+ # we don't merge the first commit when continuing
+ if not cont:
+ # perform the graft merge with p1(rev) as 'ancestor'
+ overrides = {(b'ui', b'forcemerge'): opts.get('tool', b'')}
+ base = ctx.p1() if basectx is None else basectx
+ with ui.configoverride(overrides, b'graft'):
+ stats = mergemod.graft(
+ repo, ctx, base, [b'local', b'graft', b'parent of graft']
+ )
+ # report any conflicts
+ if stats.unresolvedcount > 0:
+ # write out state for --continue
+ nodes = [repo[rev].hex() for rev in revs[pos:]]
+ statedata[b'nodes'] = nodes
+ stateversion = 1
+ graftstate.save(stateversion, statedata)
+ ui.error(_(b"abort: unresolved conflicts, can't continue\n"))
+ ui.error(_(b"(use 'hg resolve' and 'hg graft --continue')\n"))
+ return 1
+ else:
+ cont = False
+
+ # commit if --no-commit is false
+ if not opts.get('no_commit'):
+ node = repo.commit(
+ text=message, user=user, date=date, extra=extra, editor=editor
+ )
+ if node is None:
+ ui.warn(
+ _(b'note: graft of %d:%s created no changes to commit\n')
+ % (ctx.rev(), ctx)
+ )
+ # checking that newnodes exist because old state files won't have it
+ elif statedata.get(b'newnodes') is not None:
+ nn = statedata[b'newnodes']
+ assert isinstance(nn, list) # list of bytes
+ nn.append(node)
+
+ # remove state when we complete successfully
+ if not opts.get('dry_run'):
+ graftstate.delete()
+
+ return 0
+
+
+def _stopgraft(ui, repo, graftstate):
+ """stop the interrupted graft"""
+ if not graftstate.exists():
+ raise error.StateError(_(b"no interrupted graft found"))
+ pctx = repo[b'.']
+ mergemod.clean_update(pctx)
+ graftstate.delete()
+ ui.status(_(b"stopped the interrupted graft\n"))
+ ui.status(_(b"working directory is now at %s\n") % pctx.hex()[:12])
+ return 0