[PATCH] foreach: new command for traversing subrepositories

Martin Geisler mg at lazybytes.net
Tue Aug 31 09:52:59 CDT 2010


# HG changeset patch
# User Martin Geisler <mg at lazybytes.net>
# Date 1283266348 -7200
# Node ID 3875e9ddeb830ab19b85efb6b823cb85c0e80e7a
# Parent  36a65283c3afd6f957da54817b3b8c577aa78ec4
foreach: new command for traversing subrepositories

This should be a useful building block for people who make heavy use
of subrepositories. Inspired by a Git command of the same name.

diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -10,7 +10,7 @@
 import os, sys, errno, re, glob, tempfile
 import util, templater, patch, error, encoding, templatekw
 import match as matchmod
-import similar, revset
+import similar, revset, subrepo
 
 revrangesep = ':'
 
@@ -1224,6 +1224,34 @@
                 yield change(rev)
     return iterate()
 
+def foreach(ui, repo, cmd, depthfirst):
+    """execute cmd in repo.root and in each subrepository"""
+    ctx = repo['.']
+    work = [ctx.sub(subpath) for subpath in sorted(ctx.substate)]
+    if depthfirst:
+        work.reverse()
+
+    while work:
+        if depthfirst:
+            sub = work.pop()
+        else:
+            sub = work.pop(0)
+
+        relpath = subrepo.relpath(sub)
+
+        ui.note(_("executing '%s' in %s\n") % (cmd, relpath))
+        util.system(cmd, environ=dict(HG_SUBPATH=relpath,
+                                      HG_SUBURL=sub._path,
+                                      HG_SUBSTATE=sub._state[1],
+                                      HG_REPO=repo.root),
+                    cwd=os.path.join(repo.root, relpath), onerr=util.Abort,
+                    errprefix=_('terminated foreach in %s') % relpath)
+
+        w = sub.subrepos()
+        if depthfirst:
+            w.reverse()
+        work.extend(w)
+
 def commit(ui, repo, commitfunc, pats, opts):
     '''commit the specified files or all outstanding changes'''
     date = opts.get('date')
diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -1504,6 +1504,32 @@
                  switch_parent=opts.get('switch_parent'),
                  opts=patch.diffopts(ui, opts))
 
+def foreach(ui, repo, *args, **opts):
+    """execute a command in each subrepository
+
+    The command is executed with the current working directory set to
+    the root of each subrepository. It has access to the following
+    environment variables:
+
+    ``HG_REPO``:
+        Absolute path to the top-level repository in which the foreach
+        command was executed.
+
+    ``HG_SUBPATH``:
+        Relative path to the current subrepository from the top-level
+        repository.
+
+    ``HG_SUBURL``:
+        URL for the current subrepository as specified in the
+        containing repository's ``.hgsub`` file.
+
+    ``HG_SUBSTATE``:
+        State of the current subrepository as specified in the
+        containing repository's ``.hgsubstate`` file.
+    """
+    cmd = ' '.join(args)
+    cmdutil.foreach(ui, repo, cmd, not opts.get('breadth_first'))
+
 def forget(ui, repo, *pats, **opts):
     """forget the specified files on the next commit
 
@@ -4171,6 +4197,10 @@
         (forget,
          [] + walkopts,
          _('[OPTION]... FILE...')),
+    "foreach":
+        (foreach,
+         [('', 'breadth-first', None, _('use breadth-first traversal'))],
+         _('[--breadth-first] CMD')),
     "grep":
         (grep,
          [('0', 'print0', None, _('end fields with NUL')),
diff --git a/mercurial/subrepo.py b/mercurial/subrepo.py
--- a/mercurial/subrepo.py
+++ b/mercurial/subrepo.py
@@ -241,6 +241,8 @@
         """
         raise NotImplementedError
 
+    def subrepos(self):
+        return []
 
 class hgsubrepo(abstractsubrepo):
     def __init__(self, ctx, path, state):
@@ -271,6 +273,11 @@
                 addpathconfig('default-push', defpushpath)
             fp.close()
 
+    def subrepos(self):
+        rev = self._state[1]
+        ctx = self._repo[rev]
+        return [ctx.sub(subpath) for subpath in sorted(ctx.substate)]
+
     def dirty(self):
         r = self._state[1]
         if r == '':
diff --git a/tests/test-foreach.t b/tests/test-foreach.t
new file mode 100644
--- /dev/null
+++ b/tests/test-foreach.t
@@ -0,0 +1,69 @@
+
+Create some nicely nested subrepositories:
+
+  $ hg init
+  $ for d in a b; do hg init $d; echo "$d = $d" >> .hgsub; done
+  $ hg add .hgsub
+
+  $ cd a
+
+  $ for d in x y; do hg init $d; echo "$d = $d" >> .hgsub; done
+  $ hg add .hgsub
+
+  $ cd y
+  $ for d in r s t; do hg init $d; echo "$d = $d" >> .hgsub; done
+  $ hg add .hgsub
+  $ cd ..
+
+  $ cd ..
+
+  $ cd b
+  $ for d in u v; do hg init $d; echo "$d = $d" >> .hgsub; done
+  $ hg add .hgsub
+  $ cd ..
+
+  $ hg commit -m init
+  committing subrepository a
+  committing subrepository a/x
+  committing subrepository a/y
+  committing subrepository a/y/r
+  committing subrepository a/y/s
+  committing subrepository a/y/t
+  committing subrepository b
+  committing subrepository b/u
+  committing subrepository b/v
+
+The default depth-first traversal:
+
+  $ hg foreach 'echo $HG_SUBPATH'
+  a
+  a/x
+  a/y
+  a/y/r
+  a/y/s
+  a/y/t
+  b
+  b/u
+  b/v
+
+Breadth-first traversal:
+
+  $ hg foreach 'echo $HG_SUBPATH' --breadth-first
+  a
+  b
+  a/x
+  a/y
+  b/u
+  b/v
+  a/y/r
+  a/y/s
+  a/y/t
+
+Test aborting:
+
+  $ hg foreach -v 'test $HG_SUBPATH != "a/y/r"'
+  executing 'test $HG_SUBPATH != "a/y/r"' in a
+  executing 'test $HG_SUBPATH != "a/y/r"' in a/x
+  executing 'test $HG_SUBPATH != "a/y/r"' in a/y
+  executing 'test $HG_SUBPATH != "a/y/r"' in a/y/r
+  abort: terminated foreach in a/y/r: test exited with status 1


More information about the Mercurial-devel mailing list