def revert()

in eden/scm/edenscm/mercurial/cmdutil.py [0:0]


def revert(ui, repo, ctx, parents, *pats, **opts):
    parent, p2 = parents
    node = ctx.node()

    mf = ctx.manifest()
    if node == p2:
        parent = p2

    # need all matching names in dirstate and manifest of target rev,
    # so have to walk both. do not print errors if files exist in one
    # but not other. in both cases, filesets should be evaluated against
    # workingctx to get consistent result (issue4497). this means 'set:**'
    # cannot be used to select missing files from target rev.

    # `names` is a mapping for all elements in working copy and target revision
    # The mapping is in the form:
    #   <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
    names = {}

    with repo.wlock():
        ## filling of the `names` mapping
        # walk dirstate to fill `names`

        interactive = opts.get("interactive", False)
        wctx = repo[None]
        m = scmutil.match(wctx, pats, opts)

        # we'll need this later
        badfiles = set()

        def badfn(path, msg):
            # We only report errors about paths that do not exist in the original
            # node.
            #
            # For other errors we normally will successfully revert the file anyway.
            # This includes situations where the file was replaced by an unsupported
            # file type (e.g., a FIFO, socket, or device node.
            if path not in ctx:
                badfiles.add(path)
                msg = _("no such file in rev %s") % short(node)
                ui.warn("%s: %s\n" % (m.rel(path), msg))

        changes = repo.status(node1=node, match=matchmod.badmatch(m, badfn))
        for kind in changes:
            for abs in kind:
                names[abs] = m.rel(abs), m.exact(abs)

        # Look for exact filename matches that were not returned in the results.
        # These will not be returned if they are clean, but we want to include them
        # to report them as not needing changes.
        for abs in m.files():
            if abs in names or abs in badfiles:
                continue
            # Check to see if this looks like a file or directory.
            # We don't need to report directories in the clean list.
            try:
                st = repo.wvfs.lstat(abs)
                if stat.S_ISDIR(st.st_mode):
                    continue
            except OSError:
                # This is can occur if the file was locally removed and is
                # untracked, and did not exist in the node we are reverting from,
                # but does exist in the current commit.
                # Continue on and report this file as clean.
                pass

            names[abs] = m.rel(abs), m.exact(abs)
            if abs in wctx:
                changes.clean.append(abs)
            else:
                # We don't really know if this file is unknown or ignored, but
                # fortunately this does not matter.  Revert treats unknown and
                # ignored exactly files the same.
                changes.unknown.append(abs)

        modified = set(changes.modified)
        added = set(changes.added)
        removed = set(changes.removed)
        _deleted = set(changes.deleted)
        unknown = set(changes.unknown)
        unknown.update(changes.ignored)
        clean = set(changes.clean)
        modadded = set()

        # We need to account for the state of the file in the dirstate,
        # even when we revert against something else than parent. This will
        # slightly alter the behavior of revert (doing back up or not, delete
        # or just forget etc).
        if parent == node:
            dsmodified = modified
            dsadded = added
            dsremoved = removed
            # store all local modifications, useful later for rename detection
            localchanges = dsmodified | dsadded
            modified, added, removed = set(), set(), set()
        else:
            exactfilesmatch = scmutil.matchfiles(repo, names)
            changes = repo.status(node1=parent, match=exactfilesmatch)
            dsmodified = set(changes.modified)
            dsadded = set(changes.added)
            dsremoved = set(changes.removed)
            # store all local modifications, useful later for rename detection
            localchanges = dsmodified | dsadded

            # only take into account for removes between wc and target
            clean |= dsremoved - removed
            dsremoved &= removed
            # distinct between dirstate remove and other
            removed -= dsremoved

            modadded = added & dsmodified
            added -= modadded

            # tell newly modified apart.
            dsmodified &= modified
            dsmodified |= modified & dsadded  # dirstate added may need backup
            modified -= dsmodified

            # We need to wait for some post-processing to update this set
            # before making the distinction. The dirstate will be used for
            # that purpose.
            dsadded = added

        # in case of merge, files that are actually added can be reported as
        # modified, we need to post process the result
        if p2 != nullid:
            mergeadd = set(dsmodified)
            for path in dsmodified:
                if path in mf:
                    mergeadd.remove(path)
            dsadded |= mergeadd
            dsmodified -= mergeadd

        # if f is a rename, update `names` to also revert the source
        cwd = repo.getcwd()
        for f in localchanges:
            src = repo.dirstate.copied(f)
            # XXX should we check for rename down to target node?
            if src and src not in names and repo.dirstate[src] == "r":
                dsremoved.add(src)
                names[src] = (repo.pathto(src, cwd), True)

        # determine the exact nature of the deleted changesets
        deladded = set(_deleted)
        for path in _deleted:
            if path in mf:
                deladded.remove(path)
        deleted = _deleted - deladded

        # distinguish between file to forget and the other
        added = set()
        for abs in dsadded:
            if repo.dirstate[abs] != "a":
                added.add(abs)
        dsadded -= added

        for abs in deladded:
            if repo.dirstate[abs] == "a":
                dsadded.add(abs)
        deladded -= dsadded

        # For files marked as removed, we check if an unknown file is present at
        # the same path. If a such file exists it may need to be backed up.
        # Making the distinction at this stage helps have simpler backup
        # logic.
        removunk = set()
        for abs in removed:
            target = repo.wjoin(abs)
            if os.path.lexists(target):
                removunk.add(abs)
        removed -= removunk

        dsremovunk = set()
        for abs in dsremoved:
            target = repo.wjoin(abs)
            if os.path.lexists(target):
                dsremovunk.add(abs)
        dsremoved -= dsremovunk

        # action to be actually performed by revert
        # (<list of file>, message>) tuple
        actions = {
            "revert": ([], _("reverting %s\n")),
            "add": ([], _("adding %s\n")),
            "remove": ([], _("removing %s\n")),
            "drop": ([], _("removing %s\n")),
            "forget": ([], _("forgetting %s\n")),
            "undelete": ([], _("undeleting %s\n")),
            "noop": (None, _("no changes needed to %s\n")),
            "unknown": (None, _("file not managed: %s\n")),
        }

        # "constant" that convey the backup strategy.
        # All set to `discard` if `no-backup` is set do avoid checking
        # no_backup lower in the code.
        # These values are ordered for comparison purposes
        backupinteractive = 3  # do backup if interactively modified
        backup = 2  # unconditionally do backup
        check = 1  # check if the existing file differs from target
        discard = 0  # never do backup
        if opts.get("no_backup"):
            backupinteractive = backup = check = discard
        if interactive:
            dsmodifiedbackup = backupinteractive
        else:
            dsmodifiedbackup = backup
        tobackup = set()

        backupanddel = actions["remove"]
        if not opts.get("no_backup"):
            backupanddel = actions["drop"]

        disptable = (
            # dispatch table:
            #   file state
            #   action
            #   make backup
            ## Sets that results that will change file on disk
            # Modified compared to target, no local change
            (modified, actions["revert"], discard),
            # Modified compared to target, but local file is deleted
            (deleted, actions["revert"], discard),
            # Modified compared to target, local change
            (dsmodified, actions["revert"], dsmodifiedbackup),
            # Added since target
            (added, actions["remove"], discard),
            # Added in working directory
            (dsadded, actions["forget"], discard),
            # Added since target, have local modification
            (modadded, backupanddel, backup),
            # Added since target but file is missing in working directory
            (deladded, actions["drop"], discard),
            # Removed since  target, before working copy parent
            (removed, actions["add"], discard),
            # Same as `removed` but an unknown file exists at the same path
            (removunk, actions["add"], check),
            # Removed since targe, marked as such in working copy parent
            (dsremoved, actions["undelete"], discard),
            # Same as `dsremoved` but an unknown file exists at the same path
            (dsremovunk, actions["undelete"], check),
            ## the following sets does not result in any file changes
            # File with no modification
            (clean, actions["noop"], discard),
            # Existing file, not tracked anywhere
            (unknown, actions["unknown"], discard),
        )

        for abs, (rel, exact) in sorted(names.items()):
            # target file to be touch on disk (relative to cwd)
            target = repo.wjoin(abs)
            # search the entry in the dispatch table.
            # if the file is in any of these sets, it was touched in the working
            # directory parent and we are sure it needs to be reverted.
            for table, (xlist, msg), dobackup in disptable:
                if abs not in table:
                    continue
                if xlist is not None:
                    xlist.append(abs)
                    if dobackup:
                        # If in interactive mode, don't automatically create
                        # .orig files (issue4793)
                        if dobackup == backupinteractive:
                            tobackup.add(abs)
                        elif backup <= dobackup or wctx[abs].cmp(ctx[abs]):
                            bakname = scmutil.origpath(ui, repo, rel)
                            ui.note(
                                _("saving current version of %s as %s\n")
                                % (rel, bakname)
                            )
                            if not opts.get("dry_run"):
                                # Don't backup symlinks, since they can
                                # interfere with future backup paths that
                                # overlap with the symlink path (like
                                # accidentally trying to move something
                                # into the symlink).
                                if not os.path.islink(target):
                                    if interactive:
                                        util.copyfile(target, bakname)
                                    else:
                                        util.rename(target, bakname)
                    if ui.verbose or not exact:
                        if not isinstance(msg, str):
                            msg = msg(abs)
                        ui.status(msg % rel)
                elif exact:
                    ui.warn(msg % rel)
                break

        if not opts.get("dry_run"):
            needdata = ("revert", "add", "undelete")
            _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
            _performrevert(
                repo,
                parents,
                ctx,
                actions,
                interactive,
                tobackup,
                forcecopytracing=opts.get("forcecopytracing"),
            )