[PATCH] revert: add support for reverting subrepos

Angel Ezquerra angel.ezquerra at gmail.com
Tue Oct 4 10:58:38 CDT 2011


# HG changeset patch
# User Angel Ezquerra <angel.ezquerra at gmail.com>
# Date 1317712886 -7200
# Node ID 52bdfa484615d27cf82d537f7f09414c7a058c8c
# Parent  6dc67dced8c122f6139ae20ccdc03a6b11e8b765
revert: add support for reverting subrepos

Reverting a subrepo is done by updating it to the revision that is
selected on the parent repo .hgsubstate file.

* ISSUES/TODO:
- This version of this patch only allows reverting a subrepo if
the --no-backup flag is used, since no backups are performed on the
contents of the subrepo. It could be possible to add support for
backing up the subrepo contents by first performing a "revert --all"
on the subrepo, and then updating the subrepo to the proper revision.

- The behavior of the --all flag has not been changed:
The --all flag will not revert the state of the subrepos. This could
be changed as well, but it is left for a later patch (if considered
appropriate).

- I'm calling update() on the subrepos while a wlock is active.
I don't know if this is correct.

- I've used ui.status() to show a message while the subrepos are
being reverted. However TortoiseHg does not properly capture this
message (it shows a dialog box rather than showing the message on
its console).

diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -4422,8 +4422,36 @@
     ctx = scmutil.revsingle(repo, opts.get('rev'))
     node = ctx.node()
 
-    if not pats and not opts.get('all'):
-        msg = _("no files or directories specified")
+    # reverting files and subrepos is a very different operation
+    # separate subrepos from regular files by removing subrepo references
+    # from "pats" and adding them to a dictionary containing the corresponding
+    # substate entry
+    substate = {}
+    filepats = []
+
+    targetsubstate = repo[node].substate
+    for p in pats:
+        if p in targetsubstate:
+            stype = targetsubstate[p][2]
+            if stype == "hg":
+                substate[p] = targetsubstate[p]
+            else:
+                msg = _("cannot revert subrepo")
+                hint = _("reverting %s repositories is not supported") % stype
+                raise util.Abort(msg, hint=hint)
+        else:
+            filepats.append(p)
+    pats = filepats
+
+    if not opts.get('no_backup') and substate:
+        # we cannot revert subrepos unless the no_backup flag is set!
+        msg = _("cannot revert subrepos unless the no-backup flag is set")
+        hint = _("there are subrepos on the revert list, "
+            "use --no-backup to revert them")
+        raise util.Abort(msg, hint=hint)
+
+    if not pats and not substate and not opts.get('all'):
+        msg = _("no files, directories or subrepos specified")
         if p2 != nullid:
             hint = _("uncommitted merge, use --all to discard all changes,"
                      " or 'hg update -C .' to abort the merge")
@@ -4456,158 +4484,169 @@
 
     wlock = repo.wlock()
     try:
-        # walk dirstate.
-
-        m = scmutil.match(repo[None], pats, opts)
-        m.bad = lambda x, y: False
-        for abs in repo.walk(m):
-            names[abs] = m.rel(abs), m.exact(abs)
-
-        # walk target manifest.
-
-        def badfn(path, msg):
-            if path in names:
-                return
-            path_ = path + '/'
-            for f in names:
-                if f.startswith(path_):
+        if pats:
+            # walk dirstate.
+
+            m = scmutil.match(repo[None], pats, opts)
+            m.bad = lambda x, y: False
+            for abs in repo.walk(m):
+                names[abs] = m.rel(abs), m.exact(abs)
+
+            # walk target manifest.
+
+            def badfn(path, msg):
+                if path in names:
                     return
-            ui.warn("%s: %s\n" % (m.rel(path), msg))
-
-        m = scmutil.match(repo[node], pats, opts)
-        m.bad = badfn
-        for abs in repo[node].walk(m):
-            if abs not in names:
-                names[abs] = m.rel(abs), m.exact(abs)
-
-        m = scmutil.matchfiles(repo, names)
-        changes = repo.status(match=m)[:4]
-        modified, added, removed, deleted = map(set, changes)
-
-        # if f is a rename, also revert the source
-        cwd = repo.getcwd()
-        for f in added:
-            src = repo.dirstate.copied(f)
-            if src and src not in names and repo.dirstate[src] == 'r':
-                removed.add(src)
-                names[src] = (repo.pathto(src, cwd), True)
-
-        def removeforget(abs):
-            if repo.dirstate[abs] == 'a':
-                return _('forgetting %s\n')
-            return _('removing %s\n')
-
-        revert = ([], _('reverting %s\n'))
-        add = ([], _('adding %s\n'))
-        remove = ([], removeforget)
-        undelete = ([], _('undeleting %s\n'))
-
-        disptable = (
-            # dispatch table:
-            #   file state
-            #   action if in target manifest
-            #   action if not in target manifest
-            #   make backup if in target manifest
-            #   make backup if not in target manifest
-            (modified, revert, remove, True, True),
-            (added, revert, remove, True, False),
-            (removed, undelete, None, False, False),
-            (deleted, revert, remove, False, False),
-            )
-
-        for abs, (rel, exact) in sorted(names.items()):
-            mfentry = mf.get(abs)
-            target = repo.wjoin(abs)
-            def handle(xlist, dobackup):
-                xlist[0].append(abs)
-                if (dobackup and not opts.get('no_backup') and
-                    os.path.lexists(target)):
-                    bakname = "%s.orig" % rel
-                    ui.note(_('saving current version of %s as %s\n') %
-                            (rel, bakname))
-                    if not opts.get('dry_run'):
-                        util.rename(target, bakname)
-                if ui.verbose or not exact:
-                    msg = xlist[1]
-                    if not isinstance(msg, basestring):
-                        msg = msg(abs)
-                    ui.status(msg % rel)
-            for table, hitlist, misslist, backuphit, backupmiss in disptable:
-                if abs not in table:
-                    continue
-                # file has changed in dirstate
-                if mfentry:
-                    handle(hitlist, backuphit)
-                elif misslist is not None:
-                    handle(misslist, backupmiss)
-                break
-            else:
-                if abs not in repo.dirstate:
+                path_ = path + '/'
+                for f in names:
+                    if f.startswith(path_):
+                        return
+                ui.warn("%s: %s\n" % (m.rel(path), msg))
+
+            m = scmutil.match(repo[node], pats, opts)
+            m.bad = badfn
+            for abs in repo[node].walk(m):
+                if abs not in names:
+                    names[abs] = m.rel(abs), m.exact(abs)
+
+            m = scmutil.matchfiles(repo, names)
+            changes = repo.status(match=m)[:4]
+            modified, added, removed, deleted = map(set, changes)
+
+            # if f is a rename, also revert the source
+            cwd = repo.getcwd()
+            for f in added:
+                src = repo.dirstate.copied(f)
+                if src and src not in names and repo.dirstate[src] == 'r':
+                    removed.add(src)
+                    names[src] = (repo.pathto(src, cwd), True)
+
+            def removeforget(abs):
+                if repo.dirstate[abs] == 'a':
+                    return _('forgetting %s\n')
+                return _('removing %s\n')
+
+            revert = ([], _('reverting %s\n'))
+            add = ([], _('adding %s\n'))
+            remove = ([], removeforget)
+            undelete = ([], _('undeleting %s\n'))
+
+            disptable = (
+                # dispatch table:
+                #   file state
+                #   action if in target manifest
+                #   action if not in target manifest
+                #   make backup if in target manifest
+                #   make backup if not in target manifest
+                (modified, revert, remove, True, True),
+                (added, revert, remove, True, False),
+                (removed, undelete, None, False, False),
+                (deleted, revert, remove, False, False),
+                )
+
+            for abs, (rel, exact) in sorted(names.items()):
+                mfentry = mf.get(abs)
+                target = repo.wjoin(abs)
+                def handle(xlist, dobackup):
+                    xlist[0].append(abs)
+                    if (dobackup and not opts.get('no_backup') and
+                        os.path.lexists(target)):
+                        bakname = "%s.orig" % rel
+                        ui.note(_('saving current version of %s as %s\n') %
+                                (rel, bakname))
+                        if not opts.get('dry_run'):
+                            util.rename(target, bakname)
+                    if ui.verbose or not exact:
+                        msg = xlist[1]
+                        if not isinstance(msg, basestring):
+                            msg = msg(abs)
+                        ui.status(msg % rel)
+                for table, hitlist, misslist, backuphit, backupmiss in disptable:
+                    if abs not in table:
+                        continue
+                    # file has changed in dirstate
                     if mfentry:
-                        handle(add, True)
-                    elif exact:
-                        ui.warn(_('file not managed: %s\n') % rel)
-                    continue
-                # file has not changed in dirstate
+                        handle(hitlist, backuphit)
+                    elif misslist is not None:
+                        handle(misslist, backupmiss)
+                    break
+                else:
+                    if abs not in repo.dirstate:
+                        if mfentry:
+                            handle(add, True)
+                        elif exact:
+                            ui.warn(_('file not managed: %s\n') % rel)
+                        continue
+                    # file has not changed in dirstate
+                    if node == parent:
+                        if exact:
+                            ui.warn(_('no changes needed to %s\n') % rel)
+                        continue
+                    if pmf is None:
+                        # only need parent manifest in this unlikely case,
+                        # so do not read by default
+                        pmf = repo[parent].manifest()
+                    if abs in pmf:
+                        if mfentry:
+                            # if version of file is same in parent and target
+                            # manifests, do nothing
+                            if (pmf[abs] != mfentry or
+                                pmf.flags(abs) != mf.flags(abs)):
+                                handle(revert, False)
+                        else:
+                            handle(remove, False)
+
+            if not opts.get('dry_run'):
+                def checkout(f):
+                    fc = ctx[f]
+                    repo.wwrite(f, fc.data(), fc.flags())
+
+                audit_path = scmutil.pathauditor(repo.root)
+                for f in remove[0]:
+                    if repo.dirstate[f] == 'a':
+                        repo.dirstate.drop(f)
+                        continue
+                    audit_path(f)
+                    try:
+                        util.unlinkpath(repo.wjoin(f))
+                    except OSError:
+                        pass
+                    repo.dirstate.remove(f)
+
+                normal = None
                 if node == parent:
-                    if exact:
-                        ui.warn(_('no changes needed to %s\n') % rel)
-                    continue
-                if pmf is None:
-                    # only need parent manifest in this unlikely case,
-                    # so do not read by default
-                    pmf = repo[parent].manifest()
-                if abs in pmf:
-                    if mfentry:
-                        # if version of file is same in parent and target
-                        # manifests, do nothing
-                        if (pmf[abs] != mfentry or
-                            pmf.flags(abs) != mf.flags(abs)):
-                            handle(revert, False)
+                    # We're reverting to our parent. If possible, we'd like status
+                    # to report the file as clean. We have to use normallookup for
+                    # merges to avoid losing information about merged/dirty files.
+                    if p2 != nullid:
+                        normal = repo.dirstate.normallookup
                     else:
-                        handle(remove, False)
-
-        if not opts.get('dry_run'):
-            def checkout(f):
-                fc = ctx[f]
-                repo.wwrite(f, fc.data(), fc.flags())
-
-            audit_path = scmutil.pathauditor(repo.root)
-            for f in remove[0]:
-                if repo.dirstate[f] == 'a':
-                    repo.dirstate.drop(f)
-                    continue
-                audit_path(f)
-                try:
-                    util.unlinkpath(repo.wjoin(f))
-                except OSError:
-                    pass
-                repo.dirstate.remove(f)
-
-            normal = None
-            if node == parent:
-                # We're reverting to our parent. If possible, we'd like status
-                # to report the file as clean. We have to use normallookup for
-                # merges to avoid losing information about merged/dirty files.
-                if p2 != nullid:
-                    normal = repo.dirstate.normallookup
-                else:
+                        normal = repo.dirstate.normal
+                for f in revert[0]:
+                    checkout(f)
+                    if normal:
+                        normal(f)
+
+                for f in add[0]:
+                    checkout(f)
+                    repo.dirstate.add(f)
+
+                normal = repo.dirstate.normallookup
+                if node == parent and p2 == nullid:
                     normal = repo.dirstate.normal
-            for f in revert[0]:
-                checkout(f)
-                if normal:
+                for f in undelete[0]:
+                    checkout(f)
                     normal(f)
 
-            for f in add[0]:
-                checkout(f)
-                repo.dirstate.add(f)
-
-            normal = repo.dirstate.normallookup
-            if node == parent and p2 == nullid:
-                normal = repo.dirstate.normal
-            for f in undelete[0]:
-                checkout(f)
-                normal(f)
+        # Revert the subrepos on the revert list
+        # reverting a subrepo is done by updating it to the revision specified
+        # in the corresponding substate dictionary
+        for sname in substate:
+            ui.status(_('s reverting suprepo %s\n') % sname)
+            if not opts.get('dry_run'):
+                srepo = repo[None].sub(sname)._repo
+                update(ui, srepo,
+                    node=substate[sname][1], clean=True)
 
     finally:
         wlock.release()


More information about the Mercurial-devel mailing list