view 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 source

# 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