Mercurial > hg > hgforest
view forest.py @ 63:95a1f9aab6ab
`hg fpull` now behaves much like `hg pull`
author | Simon Law <simon@akoha.org> |
---|---|
date | Mon, 27 Aug 2007 13:43:06 -0400 |
parents | 5d770477eb1a |
children | a45213137945 |
line wrap: on
line source
# 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. # 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 0. Some commands accept the --walkhg command-line option to override the behavior selected by this item. """ import ConfigParser import errno import os import re import shutil from mercurial import cmdutil, commands, hg, node, util from mercurial import localrepo, sshrepo, sshserver, httprepo, statichttprepo from mercurial.hgweb import hgweb_mod 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__ except AttributeError: findcmd.findcmd = commands.findcmd findcmd.__doc__ = commands.findcmd.__doc__ try: parseurl = hg.parseurl except: parseurl = cmdutil.parseurl cmdtable = None commands.norepo += " fclone fseed" def cmd_options(ui, cmd, remove=None): aliases, spec = findcmd(ui, cmd, commands.table) res = list(spec[1]) if remove is not None: res = [opt for opt in res if opt[0] 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 _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_mod.hgweb.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_mod.hgweb.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 tree_section_re = re.compile(r"^tree(\w+)$") def tree_sections(cfg, withtop=True): """Return lexicographically sorted list of tree sections.""" allsecs = cfg.sections() secs = [] top = None for s in allsecs: if tree_section_re.match(s): secs.append(s) if cfg.get(s, "root") == ".": top = s if top is None: raise util.Abort(_("snapshot has no entry with root '.'")) secs.sort(lambda a,b: cmp(cfg.get(a, "root"), cfg.get(b, "root"))) # ensure that '.' comes first, regardless of sort secs.remove(top) if withtop: secs.insert(0, top) return secs 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 ("<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 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']) else: revs = opts['rev'] 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 class ForestSnapshot(object): __slots__ = ('forest') def __init__(self, snapfile=None): self.forest = Forest(snapfile=snapfile) def __call__(self, ui, toprepo, func, pathalias=None, mq_check=True): """Apply a function to trees matching a snapshot entry. Call func(repo, root, path, rev, mq_applied) for each repo in toprepo and its nested repositories where repo matches a snapshot entry. """ if self.forest.snapfile: self.forest = Forest(snapfile=self.forest.snapfile, top=toprepo) self.forest.update(ui) pfx = toprepo.url() for t in self.forest.trees: root = relpath(self.forest.top().root, t.root) ui.status("[%s]\n" % root) path = t.paths.get(pathalias, None) if pathalias is not None and path is None: ui.warn(_("skipped, no path alias '%s' defined\n\n") % pathalias) continue if not t.repo: ui.warn(_("skipped, no valid repo found\n\n")) rev = None if t.revs: rev = t.revs[0] func(t.repo, root, path, rev, (mq_check and t.mq_applied())) ui.status("\n") def update(self, ui, repo, mq_fatal, walkhg='', tip=False): """Update a snapshot by scanning a forest. If the ForestSnapshot instance to update was initialized from a snapshot file, this regenerates the list of trees with their current revisions but does not add any path alias to updated tree entries. Newly created tree entries get all the path aliases from the corresponding repository. """ if self.forest.top(): self.forest.update(ui) else: if repo: self.forest = Forest(top=repo) self.forest.scan(walkhg) if mq_fatal or not tip: for tree in self.forest.trees: if mq_fatal: tree.die_on_mq(self.forest.top()) if not tip: tree.revs = tree.working_revs() def write(self, ui): self.forest.write(ui, oldstyle=True) 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: dest = os.path.normpath(dest) 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 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. 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) if not snapfile: # Look for new remote paths from source srcrepo = hg.repository(ui, forest.top().getpath(source)) newrepos = [util.localpath(root) for root in srcrepo.forests(walkhg)] toproot = forest.top().root for tree in forest.trees: try: newrepos.remove(relpath(toproot, tree.root)) except Exception, err: pass 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'] 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]) try: tree.getrepo(ui) except RepoError: # 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, 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, root, path, rev, mq_applied): if mq_applied: ui.write(_("skipped, mq patches applied\n")) else: commands.push(repo.ui, repo, path, **opts) snapshot = ForestSnapshot(snapfile) snapshot(ui, toprepo, doit, pathalias) def seed(ui, snapshot, pathalias='default', root='', tip=False, **opts): """Populate a forest according to a snapshot file.""" cfg = ConfigParser.RawConfigParser() cfg.read(snapshot) pfx = root for section in tree_sections(cfg, bool(pfx)): root = cfg.get(section, 'root') ui.write("[%s]\n" % root) dest = os.path.normpath(os.path.join(pfx, util.localpath(root))) psect = section + '.paths' if not cfg.has_option(psect, pathalias): ui.write(_("skipped, no path alias '%s' defined\n\n") % pathalias) continue source = cfg.get(psect, pathalias) if os.path.exists(dest): ui.write(_("skipped, destination '%s' already exists\n\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 all remote repos, clone # everything and then use 'update' if necessary opts['rev'] = [] commands.clone(ui, source, dest, **opts) if not tip: rev = cfg.get(section, 'revision') if rev and rev != 'tip' and rev != node.nullid: repo = hg.repository(ui, dest) commands.update(repo.ui, repo, node=rev) ui.write("\n") def snapshot(ui, repo, snapfile=None, tip=False, walkhg='', **opts): """Generate a new or updated forest snapshot and display it.""" snapshot = ForestSnapshot(snapfile) snapshot.update(ui, repo, True, walkhgenabled(ui, walkhg), tip) snapshot.write(ui) def status(ui, repo, walkhg='', *pats, **opts): """Display the status of a forest of working directories.""" def doit(repo, root, path, rev, mq_applied): if mq_applied: ui.write("*mq*\n") commands.status(repo.ui, repo, *pats, **opts) snapshot = ForestSnapshot() snapshot.update(ui, repo, False, walkhgenabled(ui, walkhg)) snapshot(ui, repo, doit) def trees(ui, repo, convert=False, walkhg='', **opts): """List the roots of the repositories.""" walkhg = walkhgenabled(ui, walkhg) if convert: l = repo.forests(walkhg) else: root = repo.root l = [(f == "." and root) or os.path.join(root, f) for f in repo.forests(walkhg)] for t in l: ui.write(t + '\n') def update(ui, toprepo, snapfile=None, tip=False, walkhg='', **opts): """Update working directories to tip or according to a snapshot file. When the tip option is specified, the working directory of the toplevel repository and of each nested repository found in the local filesystem is updated to its tip. When a snapshot file is specified, the working directory of each repository listed in the snapshot file is updated to the revision recorded in the snapshot. The tip option or the snapshot file are exclusive. """ if snapfile is not None and tip or snapfile is None and not tip: raise util.Abort(_("need either --tip or SNAPSHOT-FILE")) if tip: snapshot = ForestSnapshot() snapshot.update(ui, toprepo, False, walkhgenabled(ui, walkhg), True) else: snapshot = ForestSnapshot(snapfile) def doit(repo, root, path, rev, mq_applied): if mq_applied: ui.write(_("skipped, mq patches applied\n")) else: commands.update(repo.ui, repo, node=rev, **opts) snapshot(ui, toprepo, doit) 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]')), "^fpull" : (pull, [walkhgopts, snapfileopts] + cmd_options(ui, 'pull', remove=('f',)), _('hg fpull [OPTION]... [SOURCE]')), "fpush" : (push, cmd_options(ui, 'push', remove=('f', 'r')), _('hg fpush [OPTIONS] SNAPSHOT-FILE PATH-ALIAS')), "fseed" : (seed, [('', 'root', '', _("create root as well as children under <root>")), ('t', 'tip', False, _("use tip instead of revisions stored in the snapshot file"))] + cmd_options(ui, 'clone', remove=('r',)), _('hg fseed [OPTIONS] SNAPSHOT-FILE [PATH-ALIAS]')), "fsnap" : (snapshot, [('t', 'tip', False, _("record tip instead of actual child revisions")), walkhgopts], _('hg fsnap [OPTIONS] [SNAPSHOT-FILE]')), "fstatus" : (status, [walkhgopts] + cmd_options(ui, 'status'), _('hg fstatus [OPTIONS]')), "ftrees" : (trees, [('c', 'convert', False, _("convert paths to mercurial representation")), walkhgopts], _('hg ftrees [OPTIONS]')), "fupdate" : (update, [('', 'tip', False, _("update working directories to a specified revision")), walkhgopts] + cmd_options(ui, 'update', remove=('d',)), _('hg fupdate [OPTIONS] (--tip | SNAPSHOT-FILE)')) }