[42223] trunk/dports/devel

blb at macports.org blb at macports.org
Sun Nov 16 16:52:45 PST 2008


Revision: 42223
          http://trac.macports.org/changeset/42223
Author:   blb at macports.org
Date:     2008-11-16 16:52:44 -0800 (Sun, 16 Nov 2008)
Log Message:
-----------
New port - devel/hg-forest, mercurial extension for nested repositories

Added Paths:
-----------
    trunk/dports/devel/hg-forest/
    trunk/dports/devel/hg-forest/Portfile
    trunk/dports/devel/hg-forest/files/
    trunk/dports/devel/hg-forest/files/forest.py

Added: trunk/dports/devel/hg-forest/Portfile
===================================================================
--- trunk/dports/devel/hg-forest/Portfile	                        (rev 0)
+++ trunk/dports/devel/hg-forest/Portfile	2008-11-17 00:52:44 UTC (rev 42223)
@@ -0,0 +1,39 @@
+# $Id$
+
+PortSystem          1.0
+PortGroup           python25 1.0
+name                hg-forest
+version             20081025
+categories          devel mercurial
+maintainers         blb openmaintainer
+description         Mercurial extension for nested repositories
+long_description \
+   Operations on trees with nested Mercurial repositories. \
+   This extension provides commands that apply to a composite tree called \
+   a forest. Some commands simply wrap standard Mercurial commands, such \
+   as 'clone' or 'status', and others involve a snapshot file.
+
+platforms           darwin
+
+homepage            http://www.selenic.com/mercurial/wiki/index.cgi/ForestExtension
+# This is only here since master_sites is a required key
+master_sites        macports
+
+depends_lib         port:mercurial
+
+fetch               {}
+checksum            {}
+extract             {}
+use_configure       no
+build               {}
+destroot {
+   xinstall -m 755 -d ${destroot}${python.pkgd}/hgext
+   xinstall -m 644 -W ${filespath} forest.py ${destroot}${python.pkgd}/hgext
+}
+post-activate {
+   ui_msg "To setup forest to run, edit your .hgrc to add:"
+   ui_msg "   \[extensions\]"
+   ui_msg "   hgext.forest="
+   ui_msg "See ${homepage} for more explanation"
+}
+


Property changes on: trunk/dports/devel/hg-forest/Portfile
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: trunk/dports/devel/hg-forest/files/forest.py
===================================================================
--- trunk/dports/devel/hg-forest/files/forest.py	                        (rev 0)
+++ trunk/dports/devel/hg-forest/files/forest.py	2008-11-17 00:52:44 UTC (rev 42223)
@@ -0,0 +1,1400 @@
+# Forest, an extension to work on a set of nested Mercurial trees.
+#
+# Copyright (C) 2006 by Robin Farine <robin.farine at terminus.org>
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+
+# Repository path representation
+#
+# Repository paths stored in the filesystem representation are stored
+# in variables named 'rpath'. Repository roots in the mercurial
+# representation, stored in variables named 'root', are used in
+# snapshot files and in command output.
+
+"""Operations on trees with nested Mercurial repositories.
+
+This extension provides commands that apply to a composite tree called
+a forest. Some commands simply wrap standard Mercurial commands, such
+as 'clone' or 'status', and others involve a snapshot file.
+
+A snapshot file represents the state of a forest at a given time. It
+has the format of a ConfigParser file and lists the trees in a forest,
+each tree with the following attributes:
+
+  root          path relative to the top-level tree
+  revision      the revision the working directory is based on
+  paths         a list of (alias, location) pairs
+
+The 'fsnap' command generates or updates such a file based on a forest
+in the file system. Other commands use this information to populate a
+forest or to pull/push changes.
+
+
+Configuration
+
+This extension recognizes the following item in the forest
+configuration section:
+
+walkhg = (0|no|false|1|yes|true)
+
+  Whether repositories directly under a .hg directory should be
+  skipped (0|no|false) or not (1|yes|true). The default value is true.
+  Some commands accept the --walkhg command-line option to override
+  the behavior selected by this item.
+
+partial = (0|no|false|1|yes|true)
+
+  Whether fpull should default to partial. The default value is 0.
+
+"""
+
+import ConfigParser
+import errno
+import os
+import re
+import shutil
+
+from mercurial import cmdutil, commands, hg, hgweb, node, util
+from mercurial import localrepo, sshrepo, sshserver, httprepo, statichttprepo
+from mercurial.i18n import gettext as _
+from mercurial.repo import RepoError
+
+# For backwards compatibility, we need the following function definition.
+# If we didn't want that, we'd have just written:
+#     from mercurial.commands import 
+def findcmd(ui, cmd, table):
+    """Find and execute mercurial.*.findcmd(ui, cmd[, table])."""
+    try:
+        # ui argument was removed as of b4c035057d34
+        return findcmd.findcmd(cmd, table)
+    except TypeError:
+        try:
+            return findcmd.findcmd(ui, cmd, table)
+        except TypeError:
+            return findcmd.findcmd(ui, cmd)
+try:
+    findcmd.findcmd = cmdutil.findcmd
+    findcmd.__doc__ = cmdutil.findcmd.__doc__
+    findcmd.UnknownCommand = cmdutil.UnknownCommand
+except AttributeError:
+    findcmd.findcmd = commands.findcmd
+    findcmd.__doc__ = commands.findcmd.__doc__
+    findcmd.UnknownCommand = commands.UnknownCommand
+
+# For backwards compatibility, find the parseurl() function that splits
+# urls and revisions.  Mercurial 0.9.3 doesn't have this, so we need
+# to provide a stub.
+try:
+    parseurl = cmdutil.parseurl
+except:
+    try:
+        parseurl = hg.parseurl
+    except:
+        def parseurl(url, revs):
+            """Mercurial <= 0.9.3 doesn't have this feature."""
+            return url, (revs or None)
+
+
+# For backwards compatibility, find the HTTP protocol.
+if not hasattr(hgweb, 'protocol'):
+    hgweb.protocol = hgweb.hgweb_mod.hgweb
+
+def cmd_options(ui, cmd, remove=None, table=commands.table):
+    aliases, spec = findcmd(ui, cmd, table)
+    res = list(spec[1])
+    if remove is not None:
+        res = [opt for opt in res
+               if opt[0] not in remove and opt[1] not in remove]
+    return res
+
+def walkhgenabled(ui, walkhg):
+    if not walkhg:
+        walkhg = ui.config('forest', 'walkhg', 'true')
+    try:
+        res = { '0' : False, 'false' : False, 'no' : False,
+                '1' : True, 'true' : True, 'yes' : True }[walkhg.lower()]
+    except KeyError:
+        raise util.Abort(_("invalid value for 'walkhg': %s" % walkhg))
+    return res
+
+def partialenabled(ui, partial):
+    if partial:
+        return partial
+    else:
+        partial = ui.config('forest', 'partial', 'false')
+    try:
+        res = { '0' : False, 'false' : False, 'no' : False,
+                '1' : True, 'true' : True, 'yes' : True }[partial.lower()]
+    except KeyError:
+        raise util.Abort(_("invalid value for 'partial': %s" % partial))
+    return res
+
+def _localrepo_forests(self, walkhg):
+    """Shim this function into mercurial.localrepo.localrepository so
+    that it gives you the list of subforests.
+
+    Return a list of roots in filesystem representation relative to
+    the self repository.  This list is lexigraphically sorted.
+    """
+
+    def errhandler(err):
+        if err.filename == self.root:
+            raise err
+
+    def normpath(path):
+        if path:
+            return util.normpath(path)
+        else:
+            return '.'
+
+    res = {}
+    paths = [self.root]
+    while paths:
+        path = paths.pop()
+        if os.path.realpath(path) in res:
+            continue
+        for root, dirs, files in os.walk(path, onerror=errhandler):
+            hgdirs = dirs[:]  # Shallow-copy to protect d from dirs.remove() 
+            for d in hgdirs:
+                if d == '.hg':
+                    res[os.path.realpath(root)] = root
+                    if not walkhg:
+                        dirs.remove(d)
+                else:
+                    p = os.path.join(root, d)
+                    if os.path.islink(p) and os.path.abspath(p) not in res:
+                        paths.append(p)
+    res = res.values()
+    res.sort()
+    # Turn things into relative paths
+    pfx = len(self.root) + 1
+    return [normpath(r[pfx:]) for r in res]
+
+localrepo.localrepository.forests = _localrepo_forests
+
+
+def _sshrepo_forests(self, walkhg):
+    """Shim this function into mercurial.sshrepo.sshrepository so
+    that it gives you the list of subforests.
+
+    Return a list of roots as ssh:// URLs.
+    """
+
+    if 'forests' not in self.capabilities:
+        raise util.Abort(_("Remote forests cannot be cloned because the "
+                           "other repository doesn't support the forest "
+                           "extension."))
+    data = self.call("forests", walkhg=("", "True")[walkhg])
+    return data.splitlines()
+
+sshrepo.sshrepository.forests = _sshrepo_forests
+
+
+def _sshserver_do_hello(self):
+    '''the hello command returns a set of lines describing various
+    interesting things about the server, in an RFC822-like format.
+    Currently the only one defined is "capabilities", which
+    consists of a line in the form:
+    
+    capabilities: space separated list of tokens
+    '''
+
+    caps = ['unbundle', 'lookup', 'changegroupsubset', 'forests']
+    if self.ui.configbool('server', 'uncompressed'):
+        if hasattr(self.repo, "revlogversion"):
+            version = self.repo.revlogversion
+        else:
+            version = self.repo.changelog.version
+        caps.append('stream=%d' % version)
+    self.respond("capabilities: %s\n" % (' '.join(caps),))
+
+sshserver.sshserver.do_hello = _sshserver_do_hello
+
+
+def _sshserver_do_forests(self):
+    """Shim this function into the sshserver so that it responds to
+    the forests command.  It gives a list of roots relative to the
+    self.repo repository, sorted lexigraphically.
+    """
+    
+    key, walkhg = self.getarg()
+    forests = self.repo.forests(bool(walkhg))
+    self.respond("\n".join(forests))
+
+sshserver.sshserver.do_forests = _sshserver_do_forests
+
+
+
+def _httprepo_forests(self, walkhg):
+    """Shim this function into mercurial.httprepo.httprepository so
+    that it gives you the list of subforests.
+
+    Return a list of roots as http:// URLs.
+    """
+
+    if 'forests' not in self.capabilities:
+        raise util.Abort(_("Remote forests cannot be cloned because the "
+                           "other repository doesn't support the forest "
+                           "extension."))
+    data = self.do_read("forests", walkhg=("", "True")[walkhg])
+    return data.splitlines()
+
+httprepo.httprepository.forests = _httprepo_forests
+
+
+def _httpserver_do_capabilities(self, req):
+    caps = ['lookup', 'changegroupsubset', 'forests']
+    if self.configbool('server', 'uncompressed'):
+        if hasattr(self.repo, "revlogversion"):
+            version = self.repo.revlogversion
+        else:
+            version = self.repo.changelog.version
+        caps.append('stream=%d' % version)
+    # XXX: make configurable and/or share code with do_unbundle:
+    unbundleversions = ['HG10GZ', 'HG10BZ', 'HG10UN']
+    if unbundleversions:
+        caps.append('unbundle=%s' % ','.join(unbundleversions))
+    resp = ' '.join(caps)
+    req.httphdr("application/mercurial-0.1", length=len(resp))
+    req.write(resp)
+
+hgweb.protocol.do_capabilities = _httpserver_do_capabilities
+
+
+def _httpserver_do_forests(self, req):
+    """Shim this function into the httpserver so that it responds to
+    the forests command.  It gives a list of roots relative to the
+    self.repo repository, sorted lexigraphically.
+    """
+
+    resp = ""
+    if req.form.has_key('walkhg'):
+        forests = self.repo.forests(bool(req.form['walkhg'][0]))
+        resp = "\n".join(forests)
+    req.httphdr("application/mercurial-0.1", length=len(resp))
+    req.write(resp)
+
+
+hgweb.protocol.do_forests = _httpserver_do_forests
+
+
+def _statichttprepo_forests(self, walkhg):
+    """Shim this function into
+    mercurial.statichttprepo.statichttprepository so that it gives you
+    the list of subforests.
+
+    It depends on the fact that most directory indices have directory
+    names followed by a slash.  There is no reliable way of telling
+    whether a link leads into a subdirectory.
+
+    Return a list of roots in filesystem representation relative to
+    the self repository.  This list is lexigraphically sorted.
+    """
+
+    import HTMLParser
+    import string
+    import urllib
+    import urllib2
+    import urlparse
+
+    class HtmlIndexParser(HTMLParser.HTMLParser):
+        def __init__(self, ui, paths, walkhg):
+            self._paths = paths
+            self._ui = ui
+            self._walkhg = walkhg
+            self.current = None
+        def handle_starttag(self, tag, attrs):
+            if string.lower(tag) == "a":
+                for attr in attrs:
+                    if (string.lower(attr[0]) == "href" and
+                        attr[1].endswith('/')):
+                        link = urlparse.urlsplit(attr[1])
+                        if (not self._walkhg and
+                            link[2].rstrip('/').split('/')[-1] == '.hg'):
+                            break
+                        if not link[0] and not link[2].startswith('/'):
+                            self._ui.debug(_("matched on '%s'") % attr[1])
+                            self._paths.append(urlparse.urljoin(self.current,
+                                                                attr[1]))
+
+    if self._url.endswith('/'):
+        url = self._url
+    else:
+        url = self._url + '/'
+
+    res = []
+    paths = [url]
+    seen = {}
+
+    parser = HtmlIndexParser(self.ui, paths, walkhg)
+    while paths:
+        path = paths.pop()
+        if not seen.has_key(path):
+            seen[path] = True
+            parser.current = path
+            index = None
+            try:
+                self.ui.debug(_("retrieving '%s'\n") % path)
+                index = urllib2.urlopen(path)
+                parser.reset()
+                parser.feed(index.read())
+                parser.close()
+                hg_path = urlparse.urljoin(path, '.hg')
+                self.ui.debug(_("retrieving '%s'\n") % hg_path)
+                hg = urllib2.urlopen(hg_path)
+                res.append(path)
+            except urllib2.HTTPError, inst:
+                pass
+                #raise IOError(None, inst)
+            except urllib2.URLError, inst:
+                pass
+                #raise IOError(None, inst.reason[1])
+
+    res.sort()
+    # Turn things into relative paths
+    return [root[len(url):].rstrip('/') or "." for root in res]
+
+statichttprepo.statichttprepository.forests = _statichttprepo_forests
+
+
+def die_on_numeric_revs(revs):
+    """Check to ensure that the revs passed in are not numeric.
+
+    Numeric revisions make no sense when searching a forest.  You want
+    only named branches and tags.  The only special exception is
+    revision -1, which occurs before the first checkin.
+    """
+    if revs is None:
+        return
+    for strrev in revs:
+        try:
+            intrev = int(strrev)
+        except:
+            continue                    # String-based revision
+        if intrev == 0 and strrev.startswith("00"):
+            continue                    # Revision -1
+        raise util.Abort(_("numeric revision '%s'") % strrev)
+
+
+def relpath(root, pathname):
+    """Returns the relative path of a local pathname from a local root."""
+    root = os.path.abspath(root)
+    pathname = os.path.abspath(pathname)
+    if root == pathname or pathname.startswith(root + os.sep):
+        pathname = os.path.normpath(pathname[len(root)+1:])
+    return pathname
+
+
+def urltopath(url):
+    if url and hg.islocal(url):
+        if url.startswith("file://"):
+            url = url[7:]
+        elif url.startswith("file:"):
+            url = url[5:]
+    return url
+
+
+class Forest(object):
+    """Describes the state of the forest within the current repository.
+
+    This data structure describes the Forest contained within the
+    current repository.  It contains a list of Trees that describe
+    each sub-repository.
+    """
+
+    class SnapshotError(ConfigParser.NoSectionError):
+        pass
+
+    class Tree(object):
+        """Describe a local sub-repository within a forest."""
+
+        class Skip(Warning):
+            """Exception that signals this tree should be skipped."""
+            pass
+
+        __slots__ = ('_repo', '_root', 'revs', 'paths')
+
+        def __init__(self, repo=None, root=None, revs=[], paths={}):
+            """Create a Tree object.
+
+            repo may be any mercurial.localrepo object
+            root is the absolute path of this repo object
+            rev is the desired revision for this repository, None meaning the tip
+            paths is a dictionary of path aliases to real paths
+            """
+            self._repo = repo
+            self.revs = revs
+            if repo:
+                self.paths = {}
+                self.setrepo(repo)
+                self.paths.update(paths)
+            else:
+                self.setroot(root)
+                self.paths = paths
+
+        def die_on_mq(self, rootpath=None):
+            """Raises a util.Abort exception if self has mq patches applied."""
+            if self.mq_applied():
+                rpath = self.root
+                if rootpath:
+                    if not isinstance(rootpath, str):
+                        rootpath = rootpath.root
+                    rpath = relpath(rootpath, rpath)
+                raise util.Abort(_("'%s' has mq patches applied") % rpath)
+
+        def mq_applied(self):
+            rpath = urltopath(self.root)
+            if not hg.islocal(rpath):
+                raise util.Abort(_("'%s' is not a local repository") % rpath)
+            rpath = util.localpath(rpath)
+            rpath = os.path.join(rpath, ".hg")
+            if not os.path.isdir(rpath):
+                return False
+            for entry in os.listdir(rpath):
+                path = os.path.join(rpath, entry)
+                if (os.path.isdir(path) and
+                    os.path.isfile(os.path.join(path, 'series'))):
+                    try:
+                        s = os.stat(os.path.join(path, "status"))
+                        if s.st_size > 0:
+                            return path
+                    except OSError, err:
+                        if err.errno != errno.ENOENT:
+                            raise
+            return False
+
+        def getpath(self, paths):
+            assert(type(paths) != str)
+            if paths is None:
+                return None
+            for path in paths:
+                if not hg.islocal(path):
+                    return path
+                result = urltopath(path)
+                if os.path.isdir(result):
+                    return result
+                result = urltopath(self.paths.get(path, None))
+                if result is not None:
+                    return result
+            return None
+
+        def getrepo(self, ui=False):
+            if not self._repo and self._root:
+                if ui is False:
+                    raise AttributeError("getrepo() requires 'ui' parameter")
+                self._repo = hg.repository(ui, self._root)
+            return self._repo
+
+        def setrepo(self, repo):
+            self._root = None
+            self._repo = repo
+            if repo.ui:
+                self.paths.update(dict(repo.ui.configitems('paths')))
+
+        def getroot(self):
+            if self._repo:
+                return self._repo.root
+            else:
+                return self._root
+
+        def setroot(self, root):
+            self._repo = None
+            self._root = root
+
+        @staticmethod
+        def skip(function):
+            """Decorator that turns any exception into a Forest.Tree.Skip"""
+            def skipme(*args, **keywords):
+                try:
+                    function(*args, **keywords)
+                except Exception, err:
+                    raise Forest.Tree.Skip(err)
+            return skipme
+
+        @staticmethod
+        def warn(function):
+            """Decorator that turns any exception into a Warning"""
+            def warnme(*args, **keywords):
+                try:
+                    function(*args, **keywords)
+                except Exception, err:
+                    raise Warning(err)
+            return warnme
+
+        def working_revs(self):
+            """Returns the revision of the working copy."""
+            try:
+                ctx = self.repo[None]
+            except TypeError:
+                ctx = self.repo.workingctx()
+            parents = ctx.parents()
+            return [node.hex(parents[0].node())]
+
+        def __repr__(self):
+            return ("<forest.Tree object "
+                    "- repo: %s "
+                    "- revs: %s "
+                    "- root: %s "
+                    "- paths: %s>") % (self.repo, self.revs,
+                                       self.root, self.paths)
+
+        repo = property(getrepo, setrepo, None, None)
+        root = property(getroot, setroot, None, None)
+        
+    __slots__ = ('trees', 'snapfile')
+
+    def __init__(self, error=None, top=None, snapfile=None, walkhg=True):
+        """Create a Forest object.
+
+        top is the mercurial.localrepo object at the top of the forest.
+        snapfile is the filename of the snapshot file.
+        walkhg controls if we descend into .hg directories.
+
+        If you provide no snapfile, the top repo will be searched for
+        sub-repositories.
+
+        If you do provide a snapfile, then the snapfile will be read
+        for sub-repositories and no searching of the filesystem will
+        be done.  The top repository is queried for the root of all
+        relative paths, but if it's missing, then the current
+        directory will be assumed.
+        """
+        if error:
+            raise AttributeError("__init__() takes only named arguments")
+        self.trees = []
+        self.snapfile = None
+        if snapfile:
+            self.snapfile = snapfile
+            if top is None:
+                toppath = ""
+            else:
+                toppath = top.root
+            self.read(snapfile, toppath)
+        elif top:
+            self.trees.append(Forest.Tree(repo=top))
+            if top.ui:
+                top.ui.note(_("searching for repos in %s\n") % top.root)
+            self.scan(walkhg)
+
+    def collate_files(self, pats):
+        """Returns a dictionary of absolute file paths, keyed Tree.
+
+        This lets us iterate over repositories with only the files
+        that belong to them.
+        """
+        result = {}
+        files = [os.path.abspath(path) for path in list(pats)]
+        if files:
+            files.sort(reverse=True)
+            trees = self.trees[:]
+            trees.sort(reverse=True, key=(lambda tree: tree.root))
+            for tree in trees:
+                paths = []
+                for path in files[:]:
+                    if not os.path.exists(path):
+                        raise util.Abort(_("%s not under root") % path)
+                    if path.startswith(tree.root):
+                        paths.append(path)
+                        files.remove(path)
+                if paths:
+                    result[tree] = paths
+        return result
+
+
+    def apply(self, ui, function, paths, opts, prehooks=[]):
+        """Apply function(repo, targetpath, opts) to the entire forest.
+
+        path is a path provided on the command line.
+        function is a function that should be called for every repository.
+        opts is a list of options provided to the function
+        prehooks is a list of hook(tree) that are run before function()
+
+        Useful for the vast majority of commands that scan a local
+        forest and perform some command on each sub-repository.
+        
+        Skips a sub-repository skipping it isn't actually a repository
+        or if it has mq patches applied.
+
+        In function(), targetpath will be /-separated.  You may have
+        to util.localpath() it.
+        """
+        opts['force'] = None                # Acting on unrelated repos is BAD
+        if paths:
+            # Extract revisions from # syntax in path.
+            paths[0], revs = parseurl(paths[0], opts['rev'])[0:2]
+        elif 'rev' in opts:
+            revs = opts['rev']
+        else:
+            revs = None
+        die_on_numeric_revs(revs)
+        for tree in self.trees:
+            rpath = relpath(self.top().root, tree.root)
+            ui.status("[%s]\n" % rpath)
+            try:
+                for hook in prehooks:
+                    try:
+                        hook(tree)
+                    except Forest.Tree.Skip:
+                        raise
+                    except Warning, message:
+                        ui.warn(_("warning: %s\n") % message)
+            except Forest.Tree.Skip, message:
+                ui.warn(_("skipped: %s\n") % message)
+                ui.status("\n")
+                continue
+            except util.Abort:
+                raise
+            if revs:
+                opts['rev'] = revs
+            else:
+                opts['rev'] = tree.revs
+            targetpath = paths or None
+            if paths:
+                targetpath = tree.getpath(paths)
+                if targetpath:
+                    if targetpath == paths[0] and rpath != os.curdir:
+                        targetpath = '/'.join((targetpath, util.pconvert(rpath)))
+            function(tree, targetpath, opts)
+            ui.status("\n")
+
+    def read(self, snapfile, toppath="."):
+        """Loads the information in snapfile into this forest.
+
+        snapfile is the filename of a snapshot file
+        toppath is the path of the top of this forest
+        """
+        if not toppath:
+            toppath = "."
+        cfg = ConfigParser.RawConfigParser()
+        if not cfg.read([snapfile]):
+            raise util.Abort("%s: %s" % (snapfile, os.strerror(errno.ENOENT)))
+        seen_root = False
+        sections = {}
+        for section in cfg.sections():
+            if section.endswith('.paths'):
+                # Compatibility with old Forest snapshot files
+                paths = dict(cfg.items(section))
+                section = section[:-6]
+                if section in sections:
+                    sections[section].paths.update(paths)
+                else:
+                    sections[section] = Forest.Tree(paths=paths)
+            else:
+                root = cfg.get(section, 'root')
+                if root == '.':
+                    seen_root = True
+                    root = toppath
+                else:
+                    root = os.path.join(toppath, util.localpath(root))
+                root = os.path.normpath(root)
+                rev = cfg.get(section, 'revision')
+                if not rev:
+                    rev = []
+                paths = dict([(k[5:], v)
+                              for k, v in cfg.items(section)
+                              if k.startswith('path')])
+                if section in sections:
+                    sections[section].root = root
+                    sections[section].revs = [rev]
+                    sections[section].paths.update(paths)
+                else:
+                    sections[section] = Forest.Tree(root=root,
+                                                    revs=[rev],
+                                                    paths=paths)
+        if not seen_root:
+            raise Forest.SnapshotError("Could not find 'root = .' in '%s'" %
+                                       snapfile)
+        self.trees = sections.values()
+        self.trees.sort(key=(lambda tree: tree.root))
+
+    def scan(self, walkhg):
+        """Scans for sub-repositories within this forest.
+
+        This method modifies this forest in-place.  It searches within the
+        forest's directories and enumerates all the repositories it finds.
+        """
+        trees = []
+        top = self.top()
+        ui = top.repo.ui
+        for relpath in top.repo.forests(walkhg):
+            if relpath != '.':
+                abspath = os.path.join(top.root, util.localpath(relpath))
+                trees.append(Forest.Tree(hg.repository(ui, abspath)))
+        trees.sort(key=(lambda tree: tree.root))
+        trees.insert(0, Forest.Tree(hg.repository(ui, top.root)))
+        self.trees = trees
+
+    def top(self):
+        """Returns the top Forest.Tree in this forest."""
+        if len(self.trees):
+            return self.trees[0]
+        else:
+            return None
+
+    def update(self, ui=None):
+        """Gets the most recent information about repos."""
+        try:
+            if not ui:
+                ui = self.top().repo.ui
+        except:
+            pass
+        for tree in self.trees:
+            try:
+                repo = hg.repository(ui, tree.root)
+            except RepoError:
+                repo = None
+            tree.repo = repo
+
+    def write(self, fd, oldstyle=False):
+        """Writes a snapshot file to a file descriptor."""
+        counter = 1
+        for tree in self.trees:
+            fd.write("[tree%s]\n" % counter)
+            root = relpath(self.top().root, tree.root)
+            if root == os.curdir:
+                root = '.'
+            root = util.normpath(root)
+            fd.write("root = %s\n" % root)
+            if tree.revs:
+                fd.write("revision = %s\n" % tree.revs[0])
+            else:
+                fd.write("revision = None\n")
+            if not oldstyle:
+                for name, path in tree.paths.items():
+                    fd.write("path.%s = %s\n" % (name, path))
+            else:
+                fd.write("\n[tree%s.paths]\n" % counter)
+                for name, path in tree.paths.items():
+                    fd.write("%s = %s\n" % (name, path))
+            fd.write("\n")
+            counter += 1
+
+
+    def __repr__(self):
+        return ("<forest.Forest object - trees: %s> ") % self.trees
+
+
+def qclone(ui, source, sroot, dest, rpath, opts):
+    """Helper function to clone from a remote repository.
+
+    source is the URL of the source of this repository
+    dest is the directory of the destination
+    rpath is the relative path of the destination
+    opts are a list of options to be passed into the clone
+    """
+    ui.status("[%s]\n" % rpath)
+    assert(dest is not None)
+    destpfx = os.path.normpath(os.path.dirname(dest))
+    if not os.path.exists(destpfx):
+        os.makedirs(destpfx)
+    repo = hg.repository(ui, source)
+    mqdir = None
+    assert(source is not None)
+    if hg.islocal(source):
+        Forest.Tree(repo=repo).die_on_mq(sroot)
+    url = urltopath(repo.url())
+    ui.note(_("cloning %s to %s\n") % (url, dest))
+    commands.clone(ui, url, dest, **opts)
+    repo = None
+
+
+def clone(ui, source, dest=None, **opts):
+    """make a clone of an existing forest of repositories
+
+    Create a clone of an existing forest in a new directory.
+
+    Look at the help text for the clone command for more information.
+    """
+    die_on_numeric_revs(opts['rev'])
+    source = ui.expandpath(source) or source
+    islocalsrc = hg.islocal(source)
+    if islocalsrc:
+        source = os.path.abspath(urltopath(source))
+    if dest:
+        if hg.islocal(dest):
+            dest = os.path.normpath(dest)
+        else:
+            pass
+    else:
+        dest = hg.defaultdest(source)
+    toprepo = hg.repository(ui, source)
+    forests = toprepo.forests(walkhgenabled(ui, opts['walkhg']))
+    for rpath in forests:
+        if rpath == '.':
+            rpath = ''
+        if islocalsrc:
+            srcpath = source
+            srcpath = os.path.join(source, util.localpath(rpath))
+        else:
+            srcpath = '/'.join((source, rpath))
+        if rpath:
+            destpath = os.path.join(dest, util.localpath(rpath))
+        else:
+            destpath = dest
+        try:
+            qclone(ui=ui,
+                   source=srcpath, sroot=source,
+                   dest=destpath, rpath=os.path.normpath(rpath),
+                   opts=opts)
+        except util.Abort, err:
+            ui.warn(_("skipped: %s\n") % err)
+        ui.status("\n")
+
+
+def fetch(ui, top, source="default", **opts):
+    """pull changes from a remote forest, merge new changes if needed.
+
+    This finds all changes from the forest at the specified path or
+    URL and adds them to the local forest.
+
+    Look at the help text for the fetch command for more information.
+    """
+
+    snapfile = opts['snapfile']
+    forest = Forest(top=top, snapfile=snapfile,
+                    walkhg=walkhgenabled(ui, opts['walkhg']))
+    source = [source]
+    try:
+        import hgext.fetch as fetch
+    except ImportError:
+        raise util.Abort(_("could not import fetch module\n"))
+
+    def function(tree, srcpath, opts):
+        if not srcpath:
+            srcpath = forest.top().getpath(source)
+            if srcpath:
+                rpath = util.pconvert(relpath(forest.top().root, tree.root))
+                srcpath = '/'.join((srcpath, rpath))
+            else:
+                ui.warn(_("skipped: %s\n") %
+                        _("repository %s not found") % source[0])
+                return
+        try:
+            fetch.fetch(ui, tree.getrepo(ui), srcpath, **opts)
+        except Exception, err:
+            ui.warn(_("skipped: %s\n") % err)
+            try:
+                tree.repo.transaction().__del__()
+            except AttributeError:
+                pass
+
+    @Forest.Tree.skip
+    def check_mq(tree):
+        tree.die_on_mq(top.root)
+
+    forest.apply(ui, function, source, opts,
+                 prehooks=[lambda tree: check_mq(tree)])
+
+
+def incoming(ui, top, source="default", **opts):
+    """show new changesets found in source forest
+
+    Show new changesets found in the specified path/URL or the default
+    pull location for each repository in the source forest.
+
+    Look at the help text for the incoming command for more information.
+    """
+    die_on_numeric_revs(opts['rev'])
+    forest = Forest(top=top, snapfile=opts['snapfile'],
+                    walkhg=walkhgenabled(ui, opts['walkhg']))
+    source = [source]
+    opts["bundle"] = ""
+
+    def function(tree, srcpath, opts):
+        if not srcpath:
+            srcpath = forest.top().getpath(source)
+            if srcpath:
+                rpath = util.pconvert(relpath(forest.top().root, tree.root))
+                srcpath = '/'.join((srcpath, rpath))
+            else:
+                ui.warn(_("skipped: %s\n") %
+                        _("repository %s not found") % source[0])
+                return
+        try:
+            commands.incoming(ui, tree.repo, srcpath, **opts)
+        except Exception, err:
+            ui.warn(_("skipped: %s\n") % err)
+
+    @Forest.Tree.warn
+    def check_mq(tree):
+        tree.die_on_mq(top.root)
+
+    forest.apply(ui, function, source, opts,
+                 prehooks=[lambda tree: check_mq(tree)])
+
+
+def outgoing(ui, top, dest=None, **opts):
+    """show changesets not found in destination forest
+
+    Show changesets not found in the specified destination forest or
+    the default push location.
+
+    Look at the help text for the outgoing command for more information.
+    """
+    die_on_numeric_revs(opts['rev'])
+    forest = Forest(top=top, snapfile=opts['snapfile'],
+                    walkhg=walkhgenabled(ui, opts['walkhg']))
+    if dest == None:
+        dest = ["default-push", "default"]
+    else:
+        dest = [dest]
+
+    def function(tree, destpath, opts):
+        if not destpath:
+            destpath = forest.top().getpath(dest)
+            if destpath:
+                rpath = util.pconvert(relpath(forest.top().root, tree.root))
+                destpath = '/'.join((destpath, rpath))
+            else:
+                ui.warn(_("skipped: %s\n") %
+                        _("repository %s not found") % dest[0])
+                return
+        try:
+            commands.outgoing(ui, tree.repo, destpath, **opts)
+        except Exception, err:
+            ui.warn(_("skipped: %s\n") % err)
+
+    @Forest.Tree.warn
+    def check_mq(tree):
+        tree.die_on_mq(top.root)
+
+    forest.apply(ui, function, dest, opts,
+                 prehooks=[lambda tree: check_mq(tree)])
+
+
+def pull(ui, top, source="default", pathalias=None, **opts):
+    """pull changes from the specified forest
+
+    Pull changes from a remote forest to a local one.
+
+    You may specify a snapshot file, which is generated by the fsnap
+    command.  For each tree in this file, pull the specified revision
+    from the specified source path.
+
+    By default, pull new remote repositories that it discovers.  If
+    you use the -p option, pull only the repositories available locally.
+
+    Look at the help text for the pull command for more information.
+    """
+
+    die_on_numeric_revs(opts['rev'])
+    if pathalias:
+        # Compatibility with old 'hg fpull SNAPFILE PATH-ALIAS' syntax
+        snapfile = source
+        source = pathalias
+    else:
+        snapfile = opts['snapfile']
+    source = [source]
+    walkhg = walkhgenabled(ui, opts['walkhg'])
+    forest = Forest(top=top, snapfile=snapfile, walkhg=walkhg)
+    toproot = forest.top().root
+    if not snapfile:
+        # Look for new remote paths from source
+        srcpath = forest.top().getpath(source) or ""
+        srcrepo = hg.repository(ui, srcpath)
+        srcforests = None
+        try:
+            srcforests = srcrepo.forests(walkhg)
+        except util.Abort, err:
+            ui.note(_("skipped new forests: %s\n") % err)
+        if srcforests:
+            ui.note(_("looking for new forests\n"))
+            newrepos = [util.localpath(root) for root in srcforests]
+            for tree in forest.trees:
+                try:
+                    newrepos.remove(relpath(toproot, tree.root))
+                except Exception, err:
+                    pass
+            ui.note(_("found new forests: %s\n") % newrepos)
+            forest.trees.extend([Forest.Tree(root=os.path.join(toproot, new))
+                                 for new in newrepos])
+            forest.trees.sort(key=(lambda tree: tree.root))
+    opts['pull'] = True
+    opts['uncompressed'] = None
+    opts['noupdate'] = not opts['update']
+    partial = partialenabled(ui, opts['partial'])
+
+    def function(tree, srcpath, opts):
+        if snapfile:
+            opts['rev'] = tree.revs
+        else:
+            destpath = relpath(os.path.abspath(os.curdir), tree.root)
+            rpath = util.pconvert(relpath(toproot, tree.root))
+            if not srcpath:
+                srcpath = forest.top().getpath(source)
+                if srcpath:
+                    srcpath = '/'.join((srcpath, rpath))
+                else:
+                    ui.warn(_("warning: %s\n") %
+                            _("repository %s not found") % source[0])
+                    return
+            try:
+                tree.getrepo(ui)
+            except RepoError:
+                if partial:
+                    ui.warn(_("skipped: new remote repository\n"))
+                else:
+                    # Need to clone
+                    quiet = ui.quiet
+                    try:
+                        ui.quiet = True # Hack to shut up qclone's ui.status()
+                        qclone(ui=ui,
+                               source=srcpath, sroot=source,
+                               dest=destpath, rpath=rpath,
+                               opts=opts)
+                    except util.Abort, err:
+                        ui.warn(_("skipped: %s\n") % err)
+                    ui.quiet = quiet
+                return
+        try:
+            commands.pull(ui, tree.getrepo(ui), srcpath, **opts)
+        except Exception, err:
+            ui.warn(_("skipped: %s\n") % err)
+            if tree._repo:
+                tree.repo.transaction().__del__()
+
+    @Forest.Tree.skip
+    def check_mq(tree):
+        tree.die_on_mq(top.root)
+
+    forest.apply(ui, function, source, opts,
+                 prehooks=[lambda tree: check_mq(tree)])
+
+def push(ui, top, dest=None, pathalias=None, **opts):
+    """push changes to the specified forest.
+
+    Push changes from the local forest to the given destination.
+
+    You may specify a snapshot file, which is generated by the fsnap
+    command.  For each tree in this file, push the specified revision
+    to the specified destination path.
+
+    Look at the help text for the push command for more information.
+    """
+
+    if pathalias:
+        # Compatibility with old 'hg fpush SNAPFILE PATH-ALIAS' syntax
+        snapfile = dest
+        dest = [pathalias]
+        opts['rev'] = ['tip']           # Force a push from tip
+    else:
+        snapfile = opts['snapfile']
+        if dest:
+            dest = [dest]
+        else:
+            dest = ["default-push", "default"]
+    forest = Forest(top=top, snapfile=snapfile,
+                    walkhg=walkhgenabled(ui, opts['walkhg']))
+
+    def function(tree, destpath, opts):
+        try:
+            commands.push(ui, tree.getrepo(ui), destpath, **opts)
+        except Exception, err:
+            ui.warn(_("skipped: %s\n") % err)
+            try:
+                tree.repo.transaction().__del__()
+            except AttributeError:
+                pass
+
+    @Forest.Tree.skip
+    def check_mq(tree):
+        tree.die_on_mq(top.root)
+
+    forest.apply(ui, function, dest, opts,
+                 prehooks=[lambda tree: check_mq(tree)])
+
+
+def seed(ui, snapshot=None, source='default', **opts):
+    """populate a forest according to a snapshot file.
+
+    Populate an empty local forest according to a snapshot file.
+
+    Given a snapshot file, clone any non-existant directory from the
+    provided path-alias.  This defaults to cloning from the 'default'
+    path.
+
+    Unless the --tip option is set, this command will clone the
+    revision specified in the snapshot file.
+
+    Look at the help text for the clone command for more information.
+    """
+
+    snapfile = snapshot or opts['snapfile']
+    if not snapfile:
+        raise cmdutil.ParseError("fseed", _("invalid arguments"))
+    forest = Forest(snapfile=snapfile)
+    tip = opts['tip']
+    dest = opts['root']
+    if not dest:
+        dest = os.curdir
+        forest.trees.remove(forest.top())
+    dest = os.path.normpath(dest)
+    for tree in forest.trees:
+        srcpath = tree.getpath([source])
+        if not srcpath:
+            ui.status("[%s]\n" % util.pconvert(tree.root))
+            ui.warn(_("skipped: path alias %s not defined\n") % source)
+            ui.status("\n")
+            continue
+        srcpath = urltopath(srcpath)
+        if tree.root == ".":
+            destpath = dest
+        else:
+            destpath = os.path.join(dest, tree.root)
+        opts['rev'] = tree.revs
+        try:
+            qclone(ui=ui,
+                   source=srcpath, sroot=None,
+                   dest=destpath, rpath=util.pconvert(tree.root),
+                   opts=opts)
+        except util.Abort, err:
+            ui.warn(_("skipped: %s\n") % err)
+        ui.status("\n")
+
+
+def snap(ui, top, snapshot=None, **opts):
+    """take a snapshot of the forest and show it
+
+    Shows the current state of the forest.
+
+    You can use the output of this command as with the --snapfile
+    option of other forest commands.
+
+    When you provide a snapshot file, only the trees mentioned in that
+    file will be shown.
+    """
+
+    snapfile = snapshot or opts['snapfile']
+    tip = opts['tip']
+    forest = Forest(top=top, snapfile=snapfile,
+                    walkhg=walkhgenabled(ui, opts['walkhg']))
+    if snapfile:
+        forest.update(ui)
+    for tree in forest.trees:
+        tree.die_on_mq(top.root)
+        if not tip:
+            tree.revs = tree.working_revs()
+    forest.write(ui, opts['compatible'])
+
+
+def status(ui, top, *pats, **opts):
+    """show changed files in the working forest
+
+    Show status of files in this forest's repositories.
+
+    Look at the help text for the status command for more information.
+    """
+    forest = Forest(top=top, walkhg=walkhgenabled(ui, opts['walkhg']))
+    die_on_numeric_revs(opts['rev'])
+    # Figure out which paths are relative to which roots
+    files = forest.collate_files(pats)
+    if files:
+        # Trim which trees we're going to look at
+        forest.trees = files.keys()
+
+    class munge_ui(object):
+        """This wrapper class allows us to munge the mercurial.ui.write() """
+        def __init__(self, transform, ui):
+            self._transform = transform
+            self._ui = ui
+        def write(self, output):
+            self._ui.write(self._transform(output))
+        def __getattr__(self, attrname):
+            return getattr(self._ui, attrname)
+
+    def function(tree, path, opts):
+        path = util.localpath(path)
+        if files:
+            pats = files[tree]
+        else:
+            pats = ()
+            if path == top.root:
+                path = ''
+            else:
+                path = relpath(top.root, path)
+            def prefix(output):
+                """This function shims the root in before the filename."""
+                if opts['no_status']:
+                    return os.path.join(path, output)
+                else:
+                    prefix, filename = output.split(' ', 1)
+                    return ' '.join((prefix, os.path.join(path, filename)))
+            localui = munge_ui(prefix, ui)
+        try:
+            commands.status(localui, tree.repo, *pats, **opts)
+        except RepoError, err:
+            ui.warn(_("skipped: %s\n") % err)
+
+    @Forest.Tree.warn
+    def check_mq(tree):
+        tree.die_on_mq(top.root)
+
+    forest.apply(ui, function, [top.root], opts,
+                 prehooks=[lambda tree: check_mq(tree)])
+
+
+def trees(ui, top, **opts):
+    """show the roots of the repositories
+
+    Show the roots of the trees in the forest.
+
+    By default, show the absolute path of each repository.  With
+    --convert, show the portable Mercurial path.
+    """
+
+    forest = Forest(top=top,
+                    walkhg=walkhgenabled(ui, opts['walkhg']))
+    convert = opts['convert']
+    for tree in forest.trees:
+        if convert:
+            ui.write("%s\n" % relpath(top.root, tree.root))
+        else:
+            ui.write("%s\n" % util.localpath(tree.root))
+
+
+def update(ui, top, revision=None, **opts):
+    """update working forest
+
+    Update the working forest to the specified revision, or the
+    tip of the current branch if none is specified.
+
+    You may specify a snapshot file, which is generated by the fsnap
+    command.  For each tree in this file, update to the revision
+    recorded for that tree.
+
+    Look at the help text for the update command for more information.
+    """
+
+    snapfile = None
+    if revision:
+        cp = ConfigParser.RawConfigParser()
+        try:
+            if cp.read([revision]):
+                # Compatibility with old 'hg fupdate SNAPFILE' syntax
+                snapfile = revision
+        except Exception, err:
+            if isinstance(err, ConfigParser.Error):
+                ui.warn(_("warning: %s\n") % err)
+            else:
+                raise err
+            snapfile = opts['snapfile']
+            opts['rev'] = revision
+    tip = opts['tip']
+    forest = Forest(top=top, snapfile=snapfile,
+                    walkhg=walkhgenabled(ui, opts['walkhg']))
+
+    def function(tree, ignore, opts):
+        if 'rev' in opts:
+            rev = opts['rev'] or None
+        else:
+            rev = None
+        if type(rev) is str:
+            rev = rev
+        elif rev:
+            rev = rev[0]
+        try:
+            if rev is not None:
+                commands.update(ui, tree.getrepo(ui),
+                                rev=rev, clean=opts['clean'], date=opts['date'])
+            else:
+                commands.update(ui, tree.getrepo(ui),
+                                clean=opts['clean'], date=opts['date'])
+        except Exception, err:
+            ui.warn(_("skipped: %s\n") % err)
+            tree.repo.transaction().__del__()
+
+    @Forest.Tree.skip
+    def check_mq(tree):
+        tree.die_on_mq(top.root)
+
+    forest.apply(ui, function, None, opts,
+                 prehooks=[lambda tree: check_mq(tree)])
+
+
+cmdtable = None
+
+def uisetup(ui):
+    global cmdtable
+    walkhgopts = ('', 'walkhg', '',
+                  _("walk repositories under '.hg' (yes/no)"))
+    snapfileopts = ('', 'snapfile', '',
+                    _("snapshot file generated by fsnap"))
+    cmdtable = {
+        "^fclone" :
+            (clone,
+             [walkhgopts] + cmd_options(ui, 'clone'),
+             _('hg fclone [OPTION]... SOURCE [DEST]')),
+        "fincoming|fin" :
+            (incoming,
+             [walkhgopts, snapfileopts]
+             + cmd_options(ui, 'incoming', remove=('f', 'bundle')),
+             _('hg fincoming [OPTION]... [SOURCE]')),
+        "foutgoing|fout" :
+            (outgoing,
+             [walkhgopts, snapfileopts]
+             + cmd_options(ui, 'outgoing', remove=('f',)),
+             _('hg foutgoing [OPTION]... [DEST]')),
+        "^fpull" :
+            (pull,
+             [('p', 'partial', False,
+               _("do not pull new remote repositories")),
+              walkhgopts, snapfileopts] + cmd_options(ui, 'pull', remove=('f',)),
+             _('hg fpull [OPTION]... [SOURCE]')),
+        "^fpush" :
+            (push,
+             [walkhgopts, snapfileopts] + cmd_options(ui, 'push', remove=('f',)),
+             _('hg fpush [OPTION]... [DEST]')),
+        "fseed" :
+            (seed,
+             [('', 'root', '',
+               _("create root as well as children under <root>")),
+              snapfileopts,
+              ('t', 'tip', False,
+               _("use tip instead of revisions stored in the snapshot file"))]
+             + cmd_options(ui, 'clone', remove=('r',)),
+             _('hg fseed [OPTION]... SNAPSHOT-FILE [PATH-ALIAS]')),
+        "fsnap" :
+            (snap,
+             [('', 'compatible', False,
+               _("write snapshot file compatible with older forest versions")),
+              snapfileopts,
+              ('t', 'tip', False,
+               _("record tip instead of actual child revisions")),
+              walkhgopts],
+             _('hg fsnap [OPTION]... [SNAPSHOT-FILE]')),
+        "^fstatus|fst" :
+            (status,
+             [walkhgopts] + cmd_options(ui, 'status'),
+             _('hg fstatus [OPTION]... [FILE]...')),
+        "ftrees" :
+            (trees,
+             [('c', 'convert', False,
+               _("convert paths to mercurial representation")),
+              walkhgopts],
+             _('hg ftrees [OPTIONS]')),
+        "^fupdate|fup|fcheckout|fco" :
+            (update,
+             [snapfileopts,
+              ('', 'tip', False,
+               _("use tip instead of revisions stored in the snapshot file")),
+              walkhgopts]
+             + cmd_options(ui, 'update'),
+             _('hg fupdate [OPTION]...'))
+        }
+
+    try:
+        import hgext.fetch
+    except ImportError:
+        return
+    try:
+        cmdtable.update({"ffetch": (fetch,
+                                    [walkhgopts, snapfileopts]
+                                    + cmd_options(ui, 'fetch',
+                                                  remove=('bundle',),
+                                                  table=hgext.fetch.cmdtable),
+                                    _('hg ffetch [OPTION]... [SOURCE]'))})
+    except findcmd.UnknownCommand:
+        return
+
+commands.norepo += " fclone fseed"
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/macports-changes/attachments/20081116/48c2ed84/attachment-0001.html>


More information about the macports-changes mailing list