diff mercurial/cmd_impls/graft.py @ 52360:f2fc0a91faca

commands: create a "mercurial.cmd_impls" module to host graft The "mercurial.commands" have been overweight for a while. We create a namespace dedicated to host smaller modules containing code revelant to a specific command. This should result in more isolated snd manageable module. We start with moving the code for "hg graft" in "mercurial.cmd_impls.graft" before doing more work on it. Since that code was about 5% of "commands.py" this seems like a success.
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Tue, 19 Nov 2024 15:46:12 +0100
parents
children 5ab77b93567c
line wrap: on
line diff
--- /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