[cig-commits] commit: forest extension version 0.1

Mercurial hg at geodynamics.org
Mon Nov 24 11:26:45 PST 2008


changeset:   0:c499a5ffd657
user:        Robin Farine <robin.farine at terminus.org>
date:        Sat Jul 15 02:44:21 2006 +0200
files:       .hgignore forest.py
description:
forest extension version 0.1


diff -r 000000000000 -r c499a5ffd657 .hgignore
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Sat Jul 15 02:44:21 2006 +0200
@@ -0,0 +1,4 @@
+syntax: glob
+
+*~
+*.pyc
diff -r 000000000000 -r c499a5ffd657 forest.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/forest.py	Sat Jul 15 02:44:21 2006 +0200
@@ -0,0 +1,361 @@
+# 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.
+
+"""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 'forest-snapshot' 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.
+"""
+
+version = "0.1"
+
+import ConfigParser
+import os
+import sys
+
+import mercurial.node
+from mercurial import commands, util
+
+try: # 'find' renamed as 'findcmd' after Mercurial 0.9
+    from mercurial.commands import findcmd
+except:
+    from mercurial.commands import find as findcmd
+from mercurial.hg import repository
+from mercurial.i18n import gettext as _
+from mercurial.repo import RepoError
+
+commands.norepo += " forest-clone"
+
+
+def cmd_options(cmd, remove=None):
+    aliases, spec = findcmd(cmd)
+    res = list(spec[1])
+    if remove is not None:
+        res = [opt for opt in res if opt[0] not in remove]
+    return res
+
+
+def enumerate_repos(top=''):
+    """Generate a lexicographically sorted list of repository roots."""
+
+    dirs = ['.']
+    while dirs:
+        root = dirs.pop()
+        entries = os.listdir(os.path.join(top, root))
+        entries.sort(reverse=True)
+        for e in entries:
+            path = os.path.join(root, e)
+            if not os.path.isdir(os.path.join(top, path)):
+                continue
+            if e == '.hg':
+                yield util.normpath(root)
+            else:
+                dirs.append(path)
+
+
+def mq_patches_applied(rootpath):
+    rootpath = os.path.join(rootpath, ".hg")
+    entries = os.listdir(rootpath)
+    for e in entries:
+        path = os.path.join(rootpath, e)
+        if e == "data" or not os.path.isdir(path):
+            continue
+        series = os.path.join(path, "series")
+        status = os.path.join(path, "status")
+        if os.path.isfile(series):
+            s = os.stat(status)
+            if s.st_size > 0:
+                return True
+    return False
+
+
+class ForestSnapshot(object):
+
+    class Tree(object):
+
+        __slots__ = ('root', 'rev', 'paths')
+
+        def __init__(self, root, rev, paths={}):
+            self.root = root
+            self.rev = rev
+            self.paths = paths
+
+        def info(self, pathalias):
+            return self.root, self.rev, self.paths.get(pathalias, None)
+
+        def update(self, rev, paths):
+            self.rev = rev
+            for name, path in paths.items():
+                if not self.paths.has_key(name):
+                    self.paths[name] = path
+
+        def write(self, ui, section):
+            ui.write("root = %s\n" % self.root)
+            ui.write("revision = %s\n" % self.rev)
+            ui.write("\n[%s]\n" % (section + ".paths"))
+            for name, path in self.paths.items():
+                ui.write("%s = %s\n" % (name, path))
+
+
+    __slots__ = ('rootmap', 'trees')
+
+    def __init__(self, snapfile=None):
+        self.rootmap = {}
+        self.trees = []
+        if snapfile is not None:
+            cfg = ConfigParser.RawConfigParser()
+            cfg.read([snapfile])
+            index = 0
+            while True:
+                index += 1
+                section = "tree" + str(index)
+                if not cfg.has_section(section):
+                    break
+                root = cfg.get(section, 'root')
+                tree = ForestSnapshot.Tree(root, cfg.get(section, 'revision'),
+                                           dict(cfg.items(section + '.paths')))
+                self.rootmap[root] = tree
+                self.trees.append(tree)
+
+    def __call__(self, ui, toprepo, func, pathalias):
+        """Apply a function to trees matching a snapshot entry.
+
+        Call func(repo, rev, path) for each repo in toprepo and its
+        nested repositories where repo matches a snapshot entry.
+        """
+
+        repo = None
+        for t in self.trees:
+            root, rev, path = t.info(pathalias)
+            ui.write("\n[%s]\n" % root)
+            if path is None:
+                ui.warn(_("no path alias '%s' defined\n") % pathalias)
+                continue
+            if repo is None:
+                repo = toprepo
+            else:
+                try:
+                    repo = repository(ui, root)
+                except RepoError:
+                    ui.warn(_("no valid repo found\n"))
+                    continue
+            if mq_patches_applied(repo.root):
+                ui.warn(_("mq patches applied\n"))
+                continue
+            func(repo, rev, path)
+
+
+    def update(self, ui, repo):
+        """Update a snapshot by scanning a forest.
+
+        If the ForestSnapshot instance to update was initialized from
+        a snapshot file, this regenerate the list of trees with their
+        current revisions but existing path aliases are not touched.
+        """
+
+        rootmap = {}
+        self.trees = []
+        for root in enumerate_repos():
+            if mq_patches_applied(root):
+                raise util.Abort(_("'%s' has mq patches applied") % root)
+            if root != '.':
+                repo = repository(ui, root)
+            rev = mercurial.node.hex(repo.dirstate.parents()[0])
+            paths = dict(repo.ui.configitems('paths'))
+            if self.rootmap.has_key(root):
+                tree = self.rootmap[root]
+                tree.update(rev, paths)
+            else:
+                tree = ForestSnapshot.Tree(root, rev, paths)
+            rootmap[root] = tree
+            self.trees.append(tree)
+        self.rootmap = rootmap
+
+    def write(self, ui):
+        index = 1
+        for t in self.trees:
+            section = 'tree' + str(index)
+            ui.write("\n[%s]\n" % section)
+            t.write(ui, section)
+            index += 1
+
+
+def clone(ui, source, dest, **opts):
+    """Clone a local forest."""
+    source = os.path.normpath(source)
+    dest = os.path.normpath(dest)
+    opts['rev'] = []
+    roots = []
+    for root in enumerate_repos(source):
+        if root == '.':
+            srcpath = source
+            destpath = dest
+        else:
+            subdir = util.localpath(root)
+            srcpath = os.path.join(source, subdir)
+            destpath = os.path.join(dest, subdir)
+        if mq_patches_applied(srcpath):
+            raise util.Abort(_("'%s' has mq patches applied\n") % root)
+        roots.append((root, srcpath, destpath))
+    for root in roots:
+        destpfx = os.path.dirname(root[2])
+        if destpfx and not os.path.exists(destpfx):
+            os.makedirs(destpfx)
+        ui.write("[%s]\n" % root[0])
+        print root[0], root[1], root[2]
+        commands.clone(ui, root[1], root[2], **opts)
+
+
+def pull(ui, toprepo, snapfile, pathalias, **opts):
+    """Pull changes from remote repositories to a local forest.
+
+    Iterate over the entries in the snapshot file and, for each entry
+    matching an actual tree in the forest and with a location
+    associated with 'pathalias', pull changes from this location to
+    the tree.
+
+    Skip entries that do not match or trees for which there is no entry.
+    """
+
+    opts['force'] = None
+    opts['rev'] = []
+
+    def doit(repo, rev, path):
+        commands.pull(repo.ui, repo, path, **opts)
+
+    snapshot = ForestSnapshot(snapfile)
+    snapshot(ui, toprepo, doit, pathalias)
+
+
+def push(ui, toprepo, snapfile, pathalias, **opts):
+    """Push changes in a local forest to remote destinations.
+
+    Iterate over the entries in the snapshot file and, for each entry
+    matching an actual tree in the forest and with a location
+    associated with 'pathalias', push changes from this tree to the
+    location.
+
+    Skip entries that do not match or trees for which there is no entry.
+    """
+
+    opts['force'] = None
+    opts['rev'] = []
+
+    def doit(repo, rev, path):
+        commands.push(repo.ui, repo, path, **opts)
+
+    snapshot = ForestSnapshot(snapfile)
+    snapshot(ui, toprepo, doit, pathalias)
+
+
+def seed(ui, repo, snapshot, pathalias, **opts):
+    """Populate a forest according to a snapshot file."""
+
+    cfg = ConfigParser.RawConfigParser()
+    cfg.read(snapshot)
+    index = 1
+    while True:
+        index += 1
+        section = 'tree' + str(index)
+        if not cfg.has_section(section):
+            break
+        root = cfg.get(section, 'root')
+        psect = section + '.paths'
+        if not cfg.has_option(psect, pathalias):
+            ui.warn(_("no path alias '%s' defined for tree '%s'\n") %
+                    (pathalias, dest))
+            continue
+        source = cfg.get(psect, pathalias)
+        ui.write("\n[%s]\n" % root)
+        dest = util.localpath(root)
+        if os.path.exists(dest):
+            ui.warn(_("destination '%s' already exists, skipping\n") % dest)
+            continue
+        destpfx = os.path.dirname(dest)
+        if destpfx and not os.path.exists(destpfx):
+            os.makedirs(destpfx)
+        # 'clone -r rev' not implemented for remote repos (<= 0.9), use
+        # 'update' if necessary
+        opts['rev'] = []
+        commands.clone(ui, source, dest, **opts)
+        if not opts['tip']:
+            rev = cfg.get(section, 'revision')
+            if rev and rev != mercurial.node.nullid:
+                repo = repository(ui, dest)
+                commands.update(repo.ui, repo, node=rev)
+
+
+def snapshot(ui, repo, snapfile=None):
+    """Generate or update a forest snapshot and display it."""
+
+    snapshot = ForestSnapshot(snapfile)
+    snapshot.update(ui, repo)
+    snapshot.write(ui)
+
+
+def status(ui, repo, *pats, **opts):
+    """Display the status of a forest of working directories."""
+
+    for root in enumerate_repos():
+        mqflag = ""
+        if mq_patches_applied(repo.root):
+            mqflag = " *mq*"
+        ui.write("\n[%s]%s\n" % (root, mqflag))
+        repo = repository(ui, root)
+        commands.status(repo.ui, repo, *pats, **opts)
+
+
+def trees(ui, repo):
+    """List the roots of the repositories."""
+
+    for root in enumerate_repos():
+        ui.write(root + '\n')
+
+
+cmdtable = {
+    "forest-clone" :
+        (clone,
+         cmd_options('clone', remove=('r',)),
+         _('hg forest-clone [OPTIONS] SOURCE DESTINATION')),
+    "forest-pull" :
+        (pull,
+         cmd_options('pull', remove=('f', 'r')),
+         _('hg forest-pull [OPTIONS] SNAPSHOT-FILE PATH-ALIAS')),
+    "forest-push" :
+        (push,
+         cmd_options('push', remove=('f', 'r')),
+         _('hg forest-push [OPTIONS] SNAPSHOT-FILE PATH-ALIAS')),
+    "forest-seed" :
+        (seed,
+         [('', 'tip', None,
+           _("use tip instead of revisions stored in the snapshot file"))] +
+         cmd_options('clone', remove=('r',)),
+         _('hg forest-seed [OPTIONS] SNAPSHOT-FILE PATH-ALIAS')),
+    "forest-snapshot" :
+        (snapshot, [],
+         'hg forest-snapshot [SNAPSHOT-FILE]'),
+    "forest-status" :
+        (status,
+         cmd_options('status'),
+         _('hg forest-status [OPTIONS]')),
+    "forest-trees" :
+        (trees, [],
+         'hg forest-trees'),
+}



More information about the CIG-COMMITS mailing list