Mercurial > hg > hgforest
changeset 0:c499a5ffd657
forest extension version 0.1
author | Robin Farine <robin.farine@terminus.org> |
---|---|
date | Sat, 15 Jul 2006 02:44:21 +0200 |
parents | |
children | 001a058bb63c |
files | .hgignore forest.py |
diffstat | 2 files changed, 365 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /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
--- /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@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'), +}