view trees.py @ 17:4287704dd0bf

config: write .hg/trees only when it has changed Also delete .hg/trees instead of writing an empty file.
author jcoomes
date Tue, 25 Feb 2014 06:42:56 -0800
parents a6ed24673925
children 6df509277779
line wrap: on
line source

#
# Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#
# This code is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 only, as
# published by the Free Software Foundation.
#
# This code is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# version 2 for more details (a copy is included in the LICENSE file that
# accompanied this code).
#
# You should have received a copy of the GNU General Public License version
# 2 along with this work; if not, write to the Free Software Foundation,
# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
# or visit www.oracle.com if you need additional information or have any
# questions.
#

"""manage loosely-coupled nested repositories

A 'tree' is simply a repository that may contain other repositories (or other
trees) nested within its working directory.  This extension provides commands
that operate on an entire tree, or on selected trees within it.

Each tree stores a list of the repositories contained within it in a
non-versioned file, .hg/trees.  The file lists the path to each contained tree,
relative to the root, one per line.  The file is created when a tree is cloned,
and can be modified using the tconfig command.  It is not updated when pushing
or pulling changesets (tpush, tpull).

The extension is usable on the client even if the hg server does not have the
trees extension enabled; it simply requires a bit more typing when cloning
repos.  Each repo in the tree maintains its own path information in .hg/hgrc, so
that repositories from different locations can be combined into a single tree.

The following example creates a tree called 'myproj' that includes four nested
repositories ('src', 'docs', 'images' and 'styles') with the last two coming
from a different server.  If the hg servers have the trees extension enabled::

    $ hg tclone http://abc/proj myproj
    $ hg tclone --skiproot http://xyz/pub myproj

If the hg servers do not have the trees extension enabled, then simply append
the desired contained repos (subtrees) to the command line::

    $ hg tclone http://abc/proj myproj src docs
    $ hg tclone --skiproot http://xyz/pub myproj images styles

The above is shorthand for five clone operations (three from the first command,
and two more from the second)::

    $ hg clone http://abc/proj       myproj
    $ hg clone http://abc/proj/src   myproj/src
    $ hg clone http://abc/proj/docs  myproj/docs
    $ hg clone http://xyz/pub/images myproj/images
    $ hg clone http://xyz/pub/styles myproj/styles

It also writes the tree configuration to myproj/.hg/trees.  Note that the same
tree can also be created with a single tclone::

    $ hg tclone http://abc/proj myproj src docs http://xyz/pub images styles

More examples::

Show the working directory status for each repo in the tree::

    $ hg tstatus

Pull upstream changes into each repo in the tree and update the working dir::

    $ hg tpull -u

Push local changes from each repo in the tree::

    $ hg tpush

List the tree configuration recursively::

    $ hg tlist
    /path/to/myproj
    /path/to/myproj/src
    /path/to/myproj/docs
    /path/to/myproj/images
    /path/to/myproj/styles

You can create abbreviations for tree lists by adding a [trees] section to your
hgrc file, e.g.::

    [trees]
    jdk-lt = jdk langtools
    ojdk-all = corba hotspot jaxp jaxws jdk-lt

which could be used like this:

    $ hg tclone http://hg.openjdk.java.net/jdk7/jdk7 myjdk7 ojdk-all

to create the myjdk7 tree which contains corba, hotspot, jaxp, jaxws, jdk and
langtools repos.

Show the working directory status, but only for the root repo plus the jdk and
langtools repos::

    $ hg tstatus --subtrees jdk-lt
"""

import __builtin__
import exceptions
import inspect
import os
import re
import subprocess

from mercurial import cmdutil
from mercurial import commands
from mercurial import extensions
from mercurial import hg
from mercurial import localrepo
from mercurial import pushkey
from mercurial import ui
from mercurial import util
from mercurial.i18n import _

testedwith = '''
1.1 1.1.2 1.2 1.2.1 1.3 1.3.1 1.4 1.4.3 1.5 1.5.4
1.6 1.6.4 1.7 1.7.5 1.8 1.8.4 1.9 1.9.3
2.0-rc 2.0 2.0.2 2.1-rc 2.1 2.1.2 2.2-rc 2.2 2.2.3
2.3-rc 2.3 2.3.2 2.4-rc 2.4 2.4.2 2.5-rc 2.5 2.5.4
2.6-rc 2.6 2.6.3 2.7-rc 2.7 2.7.2 2.8-rc 2.8 2.8.2
2.9-rc 2.9
'''

def _checklocal(repo):
    if not isinstance(repo, localrepo.localrepository):
        raise util.Abort(_('repository is not local'))

def _expandsubtrees(ui, subtrees):
    """Expand subtree aliases.

    Each string in subtrees that has a like-named config entry in the [trees]
    section is replaced by the right hand side of the config entry."""
    l = []
    for subtree in subtrees:
        if '/' in subtree:
            l += [subtree]
        else:
            cfglist = ui.configlist('trees', subtree)
            if cfglist:
                l += _expandsubtrees(ui, cfglist)
            else:
                l += [subtree]
    return l

def _nsnormalize(s):
    if s == 'trees' or s.startswith('trees.'):
        return s
    return 'trees.' + s

def _ns(ui, opts):
    return _nsnormalize(opts.get('tns') or
                        ui.config('trees', 'namespace', 'trees'))

_splitui = None

def _splitsubtrees(l):
    global _splitui
    res = []
    for s in l:
        if "'" in s or '"' in s:
            # Use ui.configlist() for quoted strings; requires hg 1.6 or later.
            if not _splitui:
                _splitui = ui.ui()
            _splitui.setconfig('x', 'x', s)
            res += _splitui.configlist('x', 'x')
        else:
            res += s.split()
    return res

def _subtreelist(ui, repo, opts):
    l = opts.get('subtrees')
    if l:
        del opts['subtrees']
        cansplit = ui.configbool('trees', 'splitargs', True)
        return _expandsubtrees(ui, cansplit and _splitsubtrees(l) or l)
    l = []
    try:
        keys = repo.listkeys(_ns(ui, opts))
        for i in xrange(0, len(keys)):
            l.append(keys[str(i)])
    except:
        pass
    return l

def hg_repo(ui, url, opts):
    parts = url.split(':', 2)
    if len(parts) == 1 or parts[0] == 'file':
        return hg.repository(ui, url)
    return hg.peer(ui, opts, url)

def _subtreegen_listkeys(ui, repo, opts, namespace):
    keys = repo.listkeys(namespace)
    for i in range(len(keys)):
        subtree = keys[("%d" % i)]
        yield repo, subtree

def _subtreegen(ui, repo, opts):
    """yields (repo, subtree) tuples"""
    l =  _subtreelist(ui, repo, opts)
    namespace = _ns(ui, opts)
    yielded = False
    if l:
        for subtree in l:
            # Look for file://..., http://..., ssh://..., etc.
            if subtree.split(':', 2)[0] in hg.schemes:
                if not yielded:
                    for r, st in _subtreegen_listkeys(ui, repo, opts, namespace):
                        yield r, st
                repo = hg_repo(ui, subtree, opts)
                yielded = False
            else:
                yielded = True
                yield repo, subtree
    if not yielded:
        for r, st in _subtreegen_listkeys(ui, repo, opts, namespace):
            yield r, st

def _docmd1(cmd, ui, repo, *args, **opts):
    """Call cmd for repo and each configured/specified subtree.

    This is for commands which operate on a single tree (e.g., tstatus,
    tupdate)."""

    ui.status('[%s]:\n' % repo.root)
    # XXX - should be done just once.
    cmdopts = dict(opts)
    for o in subtreesopts:
        if o[1] in cmdopts:
            del cmdopts[o[1]]
    trc = cmd(ui, repo, *args, **cmdopts)
    rc = trc != None and trc or 0
    for subtree in _subtreelist(ui, repo, opts):
        ui.status('\n')
        lr = hg.repository(ui, repo.wjoin(subtree))
        trc = _docmd1(cmd, lr.ui, lr, *args, **opts)
        rc += trc != None and trc or 0
    return rc

def _docmd2(cmd, ui, repo, remote, adjust, **opts):
    """Call cmd for repo and each configured/specified subtree.

    This is for commands which operate on two trees (e.g., tpull, tpush)."""

    ui.status('[%s]:\n' % repo.root)
    trc = cmd(ui, repo, remote, **opts)
    rc = trc != None and trc or 0
    for subtree in _subtreelist(ui, repo, opts):
        ui.status('\n')
        lr = hg.repository(ui, repo.wjoin(subtree))
        remote2 = adjust and os.path.join(remote, subtree) or remote
        trc = _docmd2(cmd, lr.ui, lr, remote2, adjust, **opts)
        rc += trc != None and trc or 0
    return rc

def _makeparentdir(path):
    if path:
        pdir = os.path.split(path.rstrip(os.sep + '/'))[0]
        if pdir and not os.path.exists(pdir):
            os.makedirs(pdir)

def _origcmd(name):
    """Return the callable mercurial will invoke for the given command name."""
    return cmdutil.findcmd(name, commands.table)[1][0]

def _shortpaths(root, subtrees):
    l = []
    if subtrees:
        n = len(root) + 1
        if subtrees[0] == root:
            l = ['.']
        else:
            l = [subtrees[0][n:]]
        for subtree in subtrees[1:]:
            l += [subtree[n:]]
    return l

def _stripfilescheme(url):
    if url.startswith('file:'):
        i = 5
        while url[i:i+1] == '//':
            i += 1
        url = url[i:]
    return url

def _subtreejoin(repo, subtree):
    """Return a string (url or path) referring to subtree within repo."""
    u = _stripfilescheme(repo.url()).rstrip('/')
    return u + '/' + subtree

def _walk(ui, repo, opts):
    l = []
    for dirpath, subdirs, files in os.walk(repo.root, True):
        if '.hg' in subdirs:
            subdirs.remove('.hg')
            l += [dirpath]
    return sorted(l)

def _readfile(path):
    f = None
    try:
        f = open(path, 'r')
        s = f.read()
        f.close()
        return s
    except:
        if f:
            f.close()
        return None

def _writeconfig(repo, namespace, subtrees, append = False):
    confpath = repo.join(namespace or 'trees')
    if subtrees:
        newconfig = '\n'.join(subtrees) + '\n'
        if append or newconfig != _readfile(confpath):
            f = open(confpath, append and 'a' or 'w')
            try:
                f.write(newconfig)
            finally:
                f.close()
    elif os.path.exists(confpath):
        os.remove(confpath)
    return 0

# ---------------- commands and associated recursion helpers -------------------

def _clonerepo(ui, source, dest, opts):
    _makeparentdir(dest)
    # Copied from mercurial/hg.py; need the returned dest repo.
    s, d = hg_clone(ui, opts, source, dest,
                    pull=opts.get('pull'),
                    stream=opts.get('uncompressed'),
                    rev=opts.get('rev'),
                    update=opts.get('updaterev') or not opts.get('noupdate'),
                    branch=opts.get('branch'))
    if isinstance(s, localrepo.localrepository) or isinstance(d.local(), bool):
        return (s, d)
    # peers; return the destination localrepo
    return (s, d.local())

def _skiprepo(ui, source, dest):
    src = None
    try:
        src = hg_repo(ui, source, {})
    except:
        class fakerepo(object):
            def __init__(self, ui, path):
                self.ui = ui
                self._path = path
            def peer(self):
                return self
            def local(self):
                return self._path
            def url(self):
                return self._path
            def wjoin(self, path):
                return os.path.join(self._path, path)
        src = fakerepo(ui, source)
    return (src, hg.repository(ui, dest))

def _clonesubtrees(ui, src, dst, opts):
    subtrees = []
    for src, subtree in _subtreegen(src.ui, src, opts):
        ui.status('\n')
        _clone(ui, _subtreejoin(src, subtree), dst.wjoin(subtree), opts)
        subtrees.append(subtree)
    return subtrees

def _clone(ui, source, dest, opts):
    ui.status('cloning %s\n' % source)
    src, dst = _clonerepo(ui, source, dest, opts)
    ui.status(_('created %s\n') % dst.root)
    subtrees = _clonesubtrees(ui, src, dst, opts)
    _writeconfig(dst, _ns(ui, opts), subtrees)

# Need to indirect through hg_clone for compatibility w/various hg versions.
hg_clone = None

def clone(ui, source, dest=None, *subtreeargs, **opts):
    '''copy one or more existing repositories to create a tree'''
    global hg_clone
    if not hg_clone:
        hg_clone = compatible_clone()
    if subtreeargs:
        s = __builtin__.list(subtreeargs)
        s.extend(opts.get('subtrees')) # Note:  extend does not return a value
        opts['subtrees'] = s
    if not opts.get('skiproot'):
        _clone(ui, source, dest, opts)
    else:
        ui.status('skipping root %s\n' % source)
        src, dst = _skiprepo(ui, source, dest)
        subtrees = _clonesubtrees(ui, src, dst, opts)
        _writeconfig(dst, _ns(ui, opts), subtrees, True)
    return 0

def _command(ui, repo, argv, stop, opts):
    ui.status('[%s]:\n' % repo.root)
    ui.flush()
    # Mercurial bug?  util.system() drops elements of argv after the first.
    # rc = util.system(argv, cwd=repo.root)
    rc = subprocess.call(argv, cwd=repo.root)
    if rc and stop:
        return rc
    for subtree in _subtreelist(ui, repo, opts):
        ui.status('\n')
        ui.flush()
        lr = hg.repository(ui, repo.wjoin(subtree))
        rc += _command(lr.ui, lr, argv, stop, opts)
        if rc and stop:
            return rc
    return rc

def command(ui, repo, cmd, *args, **opts):
    """Run a command in each repo in the tree.

    Change directory to the root of each repo and run the command.

    The command is executed directly (i.e., not using a shell), so if i/o
    redirection or other shell features are desired, include the shell
    invocation in the command, e.g.:  hg tcommand -- sh -c 'ls -l > ls.out'

    Mercurial parses all arguments that start with a dash, including those that
    follow the command name, which usually results in an error.  Prevent this by
    using '--' before the command or arguments, e.g.:  hg tcommand -- ls -l"""

    _checklocal(repo)
    l = __builtin__.list((cmd,) + args)
    return _command(ui, repo, l, opts.get('stop'), opts)

def commit(ui, repo, *pats, **opts):
    """commit all files"""
    _checklocal(repo)
    if pats:
        util.Abort('must commit all files')

    hgcommit = _origcmd('commit')
    def condcommit(ui, repo, *pats, **opts):
        '''commit conditionally - only if there is something to commit'''
        for l in repo.status()[:3]: # modified, added, removed
            if l:
                return hgcommit(ui, repo, *pats, **opts)
        ui.status('nothing to commit\n')
        return 0

    return _docmd1(condcommit, ui, repo, *pats, **opts)

def config(ui, repo, *subtrees, **opts):
    """list or change the subtrees configuration

    One of four operations can be selected:  --list, --add, --del, or --set.  If
    no operation is specified, --list is assumed.

    If the --walk option is used with --set, the filesystem rooted at REPO is
    scanned and the subtree configuration set to the discovered repos."""

    _checklocal(repo)

    opadd = opts.get('add')
    opdel = opts.get('del')
    oplst = opts.get('list')
    opset = opts.get('set')
    cnt = opadd + opdel + oplst + opset
    if cnt > 1:
        raise util.Abort(_('at most one of --add, --del, --list or --set is ' +
                           'allowed'))
    if cnt == 0 or oplst:
        l = _subtreelist(ui, repo, opts)
        for subtree in l:
            ui.write(subtree + '\n')
        return 0
    if opadd and subtrees:
        l = _subtreelist(ui, repo, opts)
        for subtree in subtrees:
            if subtree in l:
                raise util.Abort(_('subtree %s already configured' % subtree))
            l += [subtree]
        return _writeconfig(repo, _ns(ui, opts), l)
    if opdel:
        all = opts.get('all')
        if all + bool(subtrees) != 1:
            raise util.Abort(_('use either --all or subtrees (but not both)'))
        if all:
            return _writeconfig(repo, _ns(ui, opts), [])
        l = _subtreelist(ui, repo, opts)
        for subtree in subtrees:
            if not subtree in l:
                raise util.Abort(_('no subtree %s' % subtree))
            l.remove(subtree)
        return _writeconfig(repo, _ns(ui, opts), l)
    if opset:
        walk = opts.get('walk')
        if walk + bool(subtrees) != 1:
            raise util.Abort(_('use either --walk or subtrees (but not both)'))
        l = subtrees
        if walk:
            l = _shortpaths(repo.root, _walk(ui, repo, {}))[1:]
        return _writeconfig(repo, _ns(ui, opts), l)

def diff(ui, repo, *args, **opts):
    """diff repository (or selected files)"""
    _checklocal(repo)
    return _docmd1(_origcmd('diff'), ui, repo, *args, **opts)

def heads(ui, repo, *branchrevs, **opts):
    """show current repository heads or show branch heads"""
    _checklocal(repo)
    st = opts.get('subtrees')
    repocount = len(_list(ui, repo, st and {'subtrees': st} or {}))
    rc = _docmd1(_origcmd('heads'), ui, repo, *branchrevs, **opts)
    # return 0 if any of the repos have matching heads; 1 otherwise.
    return int(rc == repocount)

def incoming(ui, repo, remote="default", **opts):
    """show new changesets found in source"""
    _checklocal(repo)
    adjust = remote and not ui.config('paths', remote)
    st = opts.get('subtrees')
    repocount = len(_list(ui, repo, st and {'subtrees': st} or {}))
    rc = _docmd2(_origcmd('incoming'), ui, repo, remote, adjust, **opts)
    # return 0 if any of the repos have incoming changes; 1 otherwise.
    return int(rc == repocount)

def _list(ui, repo, opts):
    l = [repo.root]
    for subtree in _subtreelist(ui, repo, opts):
        dir = repo.wjoin(subtree)
        if os.path.exists(dir):
            lr = hg.repository(ui, dir)
            l += _list(lr.ui, lr, opts)
        else:
            ui.warn('repo %s is missing subtree %s\n' % (repo.root, subtree))
    return l

def list(ui, repo, **opts):
    """list the repo and configured subtrees, recursively

    The initial list of subtrees is obtained from the command line (if present)
    or from the repo configuration.

    If the --walk option is specified, search the filesystem instead of using
    the command line or repo configuration.

    If the --short option is specified, the listed paths are relative to
    the top-level repo."""

    _checklocal(repo)
    if opts.get('walk'):
        l = _walk(ui, repo, opts)
    else:
        l = _list(ui, repo, opts)
    if opts.get('short'):
        l = _shortpaths(repo.root, l)
    for subtree in l:
        ui.write(subtree + '\n')
    return 0

def log(ui, repo, *args, **opts):
    '''show revision history of entire repository or files'''
    _checklocal(repo)
    return _docmd1(_origcmd('log'), ui, repo, *args, **opts)

def merge(ui, repo, node=None, **opts):
    '''merge working directory with another revision'''
    _checklocal(repo)

    hgmerge = _origcmd('merge')
    def condmerge(ui, repo, node=None, **opts):
        if len(repo.heads()) > 1:
            return hgmerge(ui, repo, node, **opts)
        ui.status('nothing to merge\n')
        return 0

    return _docmd1(condmerge, ui, repo, node, **opts)

def outgoing(ui, repo, remote=None, **opts):
    '''show changesets not found in the destination'''
    _checklocal(repo)
    adjust = remote and not ui.config('paths', remote)
    st = opts.get('subtrees')
    repocount = len(_list(ui, repo, st and {'subtrees': st} or {}))
    rc = _docmd2(_origcmd('outgoing'), ui, repo, remote, adjust, **opts)
    # return 0 if any of the repos have outgoing changes; 1 otherwise.
    return int(rc == repocount)

def parents(ui, repo, filename=None, **opts):
    _checklocal(repo)
    return _docmd1(_origcmd('parents'), ui, repo, filename, **opts)

def _paths(cmd, ui, repo, search=None, **opts):
    ui.status('[%s]:\n' % repo.root)
    cmd(ui, repo, search)
    for subtree in _subtreelist(ui, repo, opts):
        ui.status('\n')
        lr = hg.repository(ui, repo.wjoin(subtree))
        _paths(cmd, lr.ui, lr, search, **opts)
    return 0

def paths(ui, repo, search=None, **opts):
    '''show aliases for remote repositories'''
    _checklocal(repo)
    return _paths(_origcmd('paths'), ui, repo, search, **opts)

def pull(ui, repo, remote="default", **opts):
    '''pull changes from the specified source'''
    _checklocal(repo)
    adjust = remote and not ui.config('paths', remote)
    st = opts.get('subtrees')
    repocount = len(_list(ui, repo, st and {'subtrees': st} or {}))
    rc = _docmd2(_origcmd('pull'), ui, repo, remote, adjust, **opts)
    # Sadly, pull returns 1 if there was nothing to pull *or* if there are
    # unresolved files on update.  No way to distinguish between them.
    # return 0 if any subtree pulled successfully.
    return int(rc == repocount)

def push(ui, repo, remote=None, **opts):
    '''push changes to the specified destination'''
    _checklocal(repo)
    adjust = remote and not ui.config('paths', remote)
    st = opts.get('subtrees')
    repocount = len(_list(ui, repo, st and {'subtrees': st} or {}))
    rc = _docmd2(_origcmd('push'), ui, repo, remote, adjust, **opts)
    # return 0 if all pushes were successful; 1 if none of the repos had
    # anything to push.
    return int(rc == repocount)

def status(ui, repo, *args, **opts):
    '''show changed files in the working directory'''
    _checklocal(repo)
    return _docmd1(_origcmd('status'), ui, repo, *args, **opts)

def summary(ui, repo, **opts):
    """summarize working directory state"""
    _checklocal(repo)
    return _docmd1(_origcmd('summary'), ui, repo, **opts)

def tag(ui, repo, name1, *names, **opts):
    '''add one or more tags for the current or given revision'''
    _checklocal(repo)
    return _docmd1(_origcmd('tag'), ui, repo, name1, *names, **opts)

def tip(ui, repo, **opts):
    '''show the tip revision'''
    _checklocal(repo)
    return _docmd1(_origcmd('tip'), ui, repo, **opts)

def _update(cmd, ui, repo, node=None, rev=None, clean=False, date=None,
            check=False, **opts):
    ui.status('[%s]:\n' % repo.root)
    if _newupdate:
        trc = cmd(ui, repo, node, rev, clean, date, check)
    else:
        trc = cmd(ui, repo, node, rev, clean, date)
    rc = trc != None and trc or 0
    for subtree in _subtreelist(ui, repo, opts):
        ui.status('\n')
        lr = hg.repository(ui, repo.wjoin(subtree))
        trc = _update(cmd, lr.ui, lr, node, rev, clean, date, check, **opts)
        rc += trc != None and trc or 0
    return rc

def update(ui, repo, node=None, rev=None, clean=False, date=None, check=False,
           **opts):
    '''update working directory (or switch revisions)'''
    _checklocal(repo)
    rc = _update(_origcmd('update'), ui, repo, node, rev, clean, date, check,
                 **opts)
    return rc and 1 or 0

def version(ui, **opts):
    '''show version information'''
    ui.status('trees extension (version 0.7)\n')

def defpath(ui, repo, peer=None, peer_push=None, **opts):
    '''examine and manipulate default path settings for a tree.'''
    def walker(r):
        return _list(ui, r, opts)
    return defpath_mod.defpath(ui, repo, peer, peer_push, walker, opts)

def debugkeys(ui, src, **opts):
    '''list the tree configuration using mercurial's pushkey mechanism.

    This works for remote repositories as long as the remote hg server has the
    trees extension enabled.'''
    d = hg_repo(ui, src, opts).listkeys(_ns(ui, opts))
    i = 0
    n = len(d)
    while i < n:
        istr = str(i)
        ui.write("%s: %s\n" % (istr, d[istr]))
        i += 1
    return 0

# ----------------------------- mercurial linkage ------------------------------

if not hasattr(hg, 'remoteui'):
    if hasattr(cmdutil, 'remoteui'):
        # hg < 1.5.4:  remoteui is in cmdutil instead of hg
        hg.remoteui = cmdutil.remoteui
    else:
        # hg < 1.3:  no remoteui
        def _remoteui(ui, opts):
            cmdutil.setremoteconfig(ui, opts)
            return ui
        hg.remoteui = _remoteui

# Tolerate changes to the signature of hg.clone().
def compatible_clone():
    clone_args = inspect.getargspec(hg.clone)[0]
    if not 'branch' in clone_args:
        # hg < 1.5:  no 'branch' parameter (a78bfaf988e1)
        def hg_clone(ui, peeropts, source, dest=None, pull=False, rev=None,
                     update=True, stream=False, branch=None):
            rui = hg.remoteui(ui, peeropts)
            return hg.clone(rui, source, dest, pull, rev, update, stream)
        return hg_clone
    if len(clone_args) < 9:
        # hg < 1.9:  no 'peeropts' parameter (d976542986d2, bd1acea552ff).
        def hg_clone(ui, peeropts, source, dest=None, pull=False, rev=None,
                     update=True, stream=False, branch=None):
            rui = hg.remoteui(ui, peeropts)
            return hg.clone(rui, source, dest, pull, rev, update, stream,
                            branch)
        return hg_clone
    return hg.clone

# hg < 2.3:  no peer() method
if getattr(hg, 'peer', None) is None:
    hg.peer = lambda ui, opts, url: hg.repository(ui, url)

_newupdate = len(inspect.getargspec(commands.update)[0]) >= 7
namespaceopt = [('', 'tns', '',
                 _('trees namespace to use'),
                 _('NAMESPACE'))]
subtreesopts = [('', 'subtrees', [],
                 _('path to subtree'),
                 _('SUBTREE'))] + namespaceopt

if len(commands.globalopts[0]) < 5:
    # hg < 1.5.4:  arg description (5th tuple element) is not supported
    def trimoptions(l):
        i = 0
        for opt in l:
            l[i] = opt[:4]
            i += 1
    trimoptions(namespaceopt)
    trimoptions(subtreesopts)

walkopt = [('w', 'walk', False,
            _('walk the filesystem to discover subtrees'))]

cloneopts = [('', 'skiproot', False,
              _('do not clone the root repo in the tree'))
            ] + subtreesopts
commandopts = [('', 'stop', False,
                _('stop if command returns non-zero'))
              ] + subtreesopts
listopts = [('s', 'short', False,
             _('list short paths (relative to repo root)'))
           ] + walkopt + subtreesopts
configopts = [('a', 'add', False,
               _('add the specified SUBTREEs to config')),
              ('',  'all', False,
               _('with --del, delete all subtrees from config')),
              ('d', 'del', False,
               _('delete the specified SUBTREEs from config')),
              ('l', 'list', False,
               _('list the configured subtrees')),
              ('s', 'set', False, _('set the subtree config to SUBTREEs'))
             ] + namespaceopt + walkopt

def _newcte(origcmd, newfunc, extraopts = [], synopsis = None):
    '''generate a cmdtable entry based on that for origcmd'''
    cte = cmdutil.findcmd(origcmd, commands.table)[1]
    # Filter out --exclude and --include, since those do not work across
    # repositories (mercurial converts them to abs paths).
    opts = [o for o in cte[1] if o[1] not in ('exclude', 'include')]
    if len(cte) > 2:
        return (newfunc, opts + extraopts, synopsis or cte[2])
    return (newfunc, opts + extraopts, synopsis)

def extsetup(ui = None):
    # The cmdtable is initialized here to pick up options added by other
    # extensions (e.g., rebase, bookmarks).
    #
    # Commands tagged with '^' are listed by 'hg help'.
    global defpath_mod
    defpath_mod = None
    defpath_opts = []
    try:
        defpath_mod = extensions.find('defpath')
        defpath_opts = __builtin__.list(defpath_mod.opts) + subtreesopts
        defpath_doc = getattr(defpath_mod, 'common_docstring', '')
        if defpath_doc:
            defpath.__doc__ += defpath_doc
    except:
        pass

    global cmdtable
    cmdtable = {
        '^tclone': _newcte('clone', clone, cloneopts,
                           _('[OPTION]... SOURCE [DEST [SUBTREE]...]')),
        'tcommand|tcmd': (command, commandopts, _('command [arg] ...')),
        'tcommit|tci': _newcte('commit', commit, subtreesopts),
        'tconfig': (config, configopts, _('[OPTION]... [SUBTREE]...')),
        'tdiff': _newcte('diff', diff, subtreesopts),
        'theads': _newcte('heads', heads, subtreesopts),
        'tincoming': _newcte('incoming', incoming, subtreesopts),
        'toutgoing': _newcte('outgoing', outgoing, subtreesopts),
        'tlist': (list, listopts, _('[OPTION]...')),
        '^tlog|thistory': _newcte('log', log, subtreesopts),
        'tmerge': _newcte('merge', merge, subtreesopts),
        'tparents': _newcte('parents', parents, subtreesopts),
        'tpaths': _newcte('paths', paths, subtreesopts),
        '^tpull': _newcte('pull', pull, subtreesopts),
        '^tpush': _newcte('push', push, subtreesopts),
        '^tstatus': _newcte('status', status, subtreesopts),
        '^tupdate': _newcte('update', update, subtreesopts),
        'ttag': _newcte('tag', tag, subtreesopts),
        'ttip': _newcte('tip', tip, subtreesopts),
        'tversion': (version, [], ''),
        'tdebugkeys': (debugkeys, namespaceopt, '')
    }
    if defpath_mod:
        cmdtable['tdefpath'] = (defpath, defpath_opts, _(''))
    if getattr(commands, 'summary', None):
        cmdtable['tsummary'] = _newcte('summary', summary, subtreesopts)

commands.norepo += ' tclone tversion tdebugkeys'

def genlistkeys(namespace):
    def _listkeys(repo):
        # trees are ordered, so the keys are the non-negative integers.
        d = {}
        i = 0
        try:
            for line in repo.opener(namespace):
                d[("%d" % i)] = line.rstrip('\n\r')
                i += 1
            return d
        except:
            return {}
    return _listkeys

def reposetup(ui, repo):
    # Pushing keys is disabled; unclear whether/how it should work.
    pushfunc = lambda *x: False
    x = [_nsnormalize(s) for s in ui.configlist('trees', 'namespaces', [])]
    try:
        for ns in [_ns(ui, {})] + x:
            pushkey.register(ns, pushfunc, genlistkeys(ns))
    except exceptions.ImportError:
        # hg < 1.6 - no pushkey.
        def _listkeys(self, namespace):
            # trees are ordered, so the keys are the non-negative integers.
            d = {}
            i = 0
            try:
                for line in self.opener(namespace):
                    d[("%d" % i)] = line.rstrip('\n\r')
                    i += 1
                return d
            except:
                return {}
        setattr(type(repo), 'listkeys', _listkeys)