# Forest, an extension to work on a set of nested Mercurial trees. # # Copyright (C) 2006 by Robin Farine # # 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: 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.""" ctx = self.repo.workingctx() parents = ctx.parents() return [node.hex(parents[0].node())] def __repr__(self): return ("") % (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 (" ") % 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 ")), 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"