mercurial/cmdutil.py
author Matt Mackall <mpm@selenic.com>
Tue, 01 Jul 2014 23:32:11 -0500
branchstable
changeset 21830 0cc5187b95bc
parent 21825 3666331164bb
child 21832 4b93e19cd6e6
child 21876 584bbfd1b50d
permissions -rw-r--r--
Added tag 3.0.2 for changeset 2d8cd3d0e83c

# cmdutil.py - help for command processing in mercurial
#
# Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.

from node import hex, nullid, nullrev, short
from i18n import _
import os, sys, errno, re, tempfile
import util, scmutil, templater, patch, error, templatekw, revlog, copies
import match as matchmod
import context, repair, graphmod, revset, phases, obsolete, pathutil
import changelog
import bookmarks
import lock as lockmod

def parsealiases(cmd):
    return cmd.lstrip("^").split("|")

def findpossible(cmd, table, strict=False):
    """
    Return cmd -> (aliases, command table entry)
    for each matching command.
    Return debug commands (or their aliases) only if no normal command matches.
    """
    choice = {}
    debugchoice = {}

    if cmd in table:
        # short-circuit exact matches, "log" alias beats "^log|history"
        keys = [cmd]
    else:
        keys = table.keys()

    for e in keys:
        aliases = parsealiases(e)
        found = None
        if cmd in aliases:
            found = cmd
        elif not strict:
            for a in aliases:
                if a.startswith(cmd):
                    found = a
                    break
        if found is not None:
            if aliases[0].startswith("debug") or found.startswith("debug"):
                debugchoice[found] = (aliases, table[e])
            else:
                choice[found] = (aliases, table[e])

    if not choice and debugchoice:
        choice = debugchoice

    return choice

def findcmd(cmd, table, strict=True):
    """Return (aliases, command table entry) for command string."""
    choice = findpossible(cmd, table, strict)

    if cmd in choice:
        return choice[cmd]

    if len(choice) > 1:
        clist = choice.keys()
        clist.sort()
        raise error.AmbiguousCommand(cmd, clist)

    if choice:
        return choice.values()[0]

    raise error.UnknownCommand(cmd)

def findrepo(p):
    while not os.path.isdir(os.path.join(p, ".hg")):
        oldp, p = p, os.path.dirname(p)
        if p == oldp:
            return None

    return p

def bailifchanged(repo):
    if repo.dirstate.p2() != nullid:
        raise util.Abort(_('outstanding uncommitted merge'))
    modified, added, removed, deleted = repo.status()[:4]
    if modified or added or removed or deleted:
        raise util.Abort(_('uncommitted changes'))
    ctx = repo[None]
    for s in sorted(ctx.substate):
        if ctx.sub(s).dirty():
            raise util.Abort(_("uncommitted changes in subrepo %s") % s)

def logmessage(ui, opts):
    """ get the log message according to -m and -l option """
    message = opts.get('message')
    logfile = opts.get('logfile')

    if message and logfile:
        raise util.Abort(_('options --message and --logfile are mutually '
                           'exclusive'))
    if not message and logfile:
        try:
            if logfile == '-':
                message = ui.fin.read()
            else:
                message = '\n'.join(util.readfile(logfile).splitlines())
        except IOError, inst:
            raise util.Abort(_("can't read commit message '%s': %s") %
                             (logfile, inst.strerror))
    return message

def loglimit(opts):
    """get the log limit according to option -l/--limit"""
    limit = opts.get('limit')
    if limit:
        try:
            limit = int(limit)
        except ValueError:
            raise util.Abort(_('limit must be a positive integer'))
        if limit <= 0:
            raise util.Abort(_('limit must be positive'))
    else:
        limit = None
    return limit

def makefilename(repo, pat, node, desc=None,
                  total=None, seqno=None, revwidth=None, pathname=None):
    node_expander = {
        'H': lambda: hex(node),
        'R': lambda: str(repo.changelog.rev(node)),
        'h': lambda: short(node),
        'm': lambda: re.sub('[^\w]', '_', str(desc))
        }
    expander = {
        '%': lambda: '%',
        'b': lambda: os.path.basename(repo.root),
        }

    try:
        if node:
            expander.update(node_expander)
        if node:
            expander['r'] = (lambda:
                    str(repo.changelog.rev(node)).zfill(revwidth or 0))
        if total is not None:
            expander['N'] = lambda: str(total)
        if seqno is not None:
            expander['n'] = lambda: str(seqno)
        if total is not None and seqno is not None:
            expander['n'] = lambda: str(seqno).zfill(len(str(total)))
        if pathname is not None:
            expander['s'] = lambda: os.path.basename(pathname)
            expander['d'] = lambda: os.path.dirname(pathname) or '.'
            expander['p'] = lambda: pathname

        newname = []
        patlen = len(pat)
        i = 0
        while i < patlen:
            c = pat[i]
            if c == '%':
                i += 1
                c = pat[i]
                c = expander[c]()
            newname.append(c)
            i += 1
        return ''.join(newname)
    except KeyError, inst:
        raise util.Abort(_("invalid format spec '%%%s' in output filename") %
                         inst.args[0])

def makefileobj(repo, pat, node=None, desc=None, total=None,
                seqno=None, revwidth=None, mode='wb', modemap=None,
                pathname=None):

    writable = mode not in ('r', 'rb')

    if not pat or pat == '-':
        fp = writable and repo.ui.fout or repo.ui.fin
        if util.safehasattr(fp, 'fileno'):
            return os.fdopen(os.dup(fp.fileno()), mode)
        else:
            # if this fp can't be duped properly, return
            # a dummy object that can be closed
            class wrappedfileobj(object):
                noop = lambda x: None
                def __init__(self, f):
                    self.f = f
                def __getattr__(self, attr):
                    if attr == 'close':
                        return self.noop
                    else:
                        return getattr(self.f, attr)

            return wrappedfileobj(fp)
    if util.safehasattr(pat, 'write') and writable:
        return pat
    if util.safehasattr(pat, 'read') and 'r' in mode:
        return pat
    fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
    if modemap is not None:
        mode = modemap.get(fn, mode)
        if mode == 'wb':
            modemap[fn] = 'ab'
    return open(fn, mode)

def openrevlog(repo, cmd, file_, opts):
    """opens the changelog, manifest, a filelog or a given revlog"""
    cl = opts['changelog']
    mf = opts['manifest']
    msg = None
    if cl and mf:
        msg = _('cannot specify --changelog and --manifest at the same time')
    elif cl or mf:
        if file_:
            msg = _('cannot specify filename with --changelog or --manifest')
        elif not repo:
            msg = _('cannot specify --changelog or --manifest '
                    'without a repository')
    if msg:
        raise util.Abort(msg)

    r = None
    if repo:
        if cl:
            r = repo.unfiltered().changelog
        elif mf:
            r = repo.manifest
        elif file_:
            filelog = repo.file(file_)
            if len(filelog):
                r = filelog
    if not r:
        if not file_:
            raise error.CommandError(cmd, _('invalid arguments'))
        if not os.path.isfile(file_):
            raise util.Abort(_("revlog '%s' not found") % file_)
        r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False),
                          file_[:-2] + ".i")
    return r

def copy(ui, repo, pats, opts, rename=False):
    # called with the repo lock held
    #
    # hgsep => pathname that uses "/" to separate directories
    # ossep => pathname that uses os.sep to separate directories
    cwd = repo.getcwd()
    targets = {}
    after = opts.get("after")
    dryrun = opts.get("dry_run")
    wctx = repo[None]

    def walkpat(pat):
        srcs = []
        badstates = after and '?' or '?r'
        m = scmutil.match(repo[None], [pat], opts, globbed=True)
        for abs in repo.walk(m):
            state = repo.dirstate[abs]
            rel = m.rel(abs)
            exact = m.exact(abs)
            if state in badstates:
                if exact and state == '?':
                    ui.warn(_('%s: not copying - file is not managed\n') % rel)
                if exact and state == 'r':
                    ui.warn(_('%s: not copying - file has been marked for'
                              ' remove\n') % rel)
                continue
            # abs: hgsep
            # rel: ossep
            srcs.append((abs, rel, exact))
        return srcs

    # abssrc: hgsep
    # relsrc: ossep
    # otarget: ossep
    def copyfile(abssrc, relsrc, otarget, exact):
        abstarget = pathutil.canonpath(repo.root, cwd, otarget)
        if '/' in abstarget:
            # We cannot normalize abstarget itself, this would prevent
            # case only renames, like a => A.
            abspath, absname = abstarget.rsplit('/', 1)
            abstarget = repo.dirstate.normalize(abspath) + '/' + absname
        reltarget = repo.pathto(abstarget, cwd)
        target = repo.wjoin(abstarget)
        src = repo.wjoin(abssrc)
        state = repo.dirstate[abstarget]

        scmutil.checkportable(ui, abstarget)

        # check for collisions
        prevsrc = targets.get(abstarget)
        if prevsrc is not None:
            ui.warn(_('%s: not overwriting - %s collides with %s\n') %
                    (reltarget, repo.pathto(abssrc, cwd),
                     repo.pathto(prevsrc, cwd)))
            return

        # check for overwrites
        exists = os.path.lexists(target)
        samefile = False
        if exists and abssrc != abstarget:
            if (repo.dirstate.normalize(abssrc) ==
                repo.dirstate.normalize(abstarget)):
                if not rename:
                    ui.warn(_("%s: can't copy - same file\n") % reltarget)
                    return
                exists = False
                samefile = True

        if not after and exists or after and state in 'mn':
            if not opts['force']:
                ui.warn(_('%s: not overwriting - file exists\n') %
                        reltarget)
                return

        if after:
            if not exists:
                if rename:
                    ui.warn(_('%s: not recording move - %s does not exist\n') %
                            (relsrc, reltarget))
                else:
                    ui.warn(_('%s: not recording copy - %s does not exist\n') %
                            (relsrc, reltarget))
                return
        elif not dryrun:
            try:
                if exists:
                    os.unlink(target)
                targetdir = os.path.dirname(target) or '.'
                if not os.path.isdir(targetdir):
                    os.makedirs(targetdir)
                if samefile:
                    tmp = target + "~hgrename"
                    os.rename(src, tmp)
                    os.rename(tmp, target)
                else:
                    util.copyfile(src, target)
                srcexists = True
            except IOError, inst:
                if inst.errno == errno.ENOENT:
                    ui.warn(_('%s: deleted in working copy\n') % relsrc)
                    srcexists = False
                else:
                    ui.warn(_('%s: cannot copy - %s\n') %
                            (relsrc, inst.strerror))
                    return True # report a failure

        if ui.verbose or not exact:
            if rename:
                ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
            else:
                ui.status(_('copying %s to %s\n') % (relsrc, reltarget))

        targets[abstarget] = abssrc

        # fix up dirstate
        scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
                             dryrun=dryrun, cwd=cwd)
        if rename and not dryrun:
            if not after and srcexists and not samefile:
                util.unlinkpath(repo.wjoin(abssrc))
            wctx.forget([abssrc])

    # pat: ossep
    # dest ossep
    # srcs: list of (hgsep, hgsep, ossep, bool)
    # return: function that takes hgsep and returns ossep
    def targetpathfn(pat, dest, srcs):
        if os.path.isdir(pat):
            abspfx = pathutil.canonpath(repo.root, cwd, pat)
            abspfx = util.localpath(abspfx)
            if destdirexists:
                striplen = len(os.path.split(abspfx)[0])
            else:
                striplen = len(abspfx)
            if striplen:
                striplen += len(os.sep)
            res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
        elif destdirexists:
            res = lambda p: os.path.join(dest,
                                         os.path.basename(util.localpath(p)))
        else:
            res = lambda p: dest
        return res

    # pat: ossep
    # dest ossep
    # srcs: list of (hgsep, hgsep, ossep, bool)
    # return: function that takes hgsep and returns ossep
    def targetpathafterfn(pat, dest, srcs):
        if matchmod.patkind(pat):
            # a mercurial pattern
            res = lambda p: os.path.join(dest,
                                         os.path.basename(util.localpath(p)))
        else:
            abspfx = pathutil.canonpath(repo.root, cwd, pat)
            if len(abspfx) < len(srcs[0][0]):
                # A directory. Either the target path contains the last
                # component of the source path or it does not.
                def evalpath(striplen):
                    score = 0
                    for s in srcs:
                        t = os.path.join(dest, util.localpath(s[0])[striplen:])
                        if os.path.lexists(t):
                            score += 1
                    return score

                abspfx = util.localpath(abspfx)
                striplen = len(abspfx)
                if striplen:
                    striplen += len(os.sep)
                if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
                    score = evalpath(striplen)
                    striplen1 = len(os.path.split(abspfx)[0])
                    if striplen1:
                        striplen1 += len(os.sep)
                    if evalpath(striplen1) > score:
                        striplen = striplen1
                res = lambda p: os.path.join(dest,
                                             util.localpath(p)[striplen:])
            else:
                # a file
                if destdirexists:
                    res = lambda p: os.path.join(dest,
                                        os.path.basename(util.localpath(p)))
                else:
                    res = lambda p: dest
        return res


    pats = scmutil.expandpats(pats)
    if not pats:
        raise util.Abort(_('no source or destination specified'))
    if len(pats) == 1:
        raise util.Abort(_('no destination specified'))
    dest = pats.pop()
    destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
    if not destdirexists:
        if len(pats) > 1 or matchmod.patkind(pats[0]):
            raise util.Abort(_('with multiple sources, destination must be an '
                               'existing directory'))
        if util.endswithsep(dest):
            raise util.Abort(_('destination %s is not a directory') % dest)

    tfn = targetpathfn
    if after:
        tfn = targetpathafterfn
    copylist = []
    for pat in pats:
        srcs = walkpat(pat)
        if not srcs:
            continue
        copylist.append((tfn(pat, dest, srcs), srcs))
    if not copylist:
        raise util.Abort(_('no files to copy'))

    errors = 0
    for targetpath, srcs in copylist:
        for abssrc, relsrc, exact in srcs:
            if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
                errors += 1

    if errors:
        ui.warn(_('(consider using --after)\n'))

    return errors != 0

def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
    runargs=None, appendpid=False):
    '''Run a command as a service.'''

    def writepid(pid):
        if opts['pid_file']:
            mode = appendpid and 'a' or 'w'
            fp = open(opts['pid_file'], mode)
            fp.write(str(pid) + '\n')
            fp.close()

    if opts['daemon'] and not opts['daemon_pipefds']:
        # Signal child process startup with file removal
        lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
        os.close(lockfd)
        try:
            if not runargs:
                runargs = util.hgcmd() + sys.argv[1:]
            runargs.append('--daemon-pipefds=%s' % lockpath)
            # Don't pass --cwd to the child process, because we've already
            # changed directory.
            for i in xrange(1, len(runargs)):
                if runargs[i].startswith('--cwd='):
                    del runargs[i]
                    break
                elif runargs[i].startswith('--cwd'):
                    del runargs[i:i + 2]
                    break
            def condfn():
                return not os.path.exists(lockpath)
            pid = util.rundetached(runargs, condfn)
            if pid < 0:
                raise util.Abort(_('child process failed to start'))
            writepid(pid)
        finally:
            try:
                os.unlink(lockpath)
            except OSError, e:
                if e.errno != errno.ENOENT:
                    raise
        if parentfn:
            return parentfn(pid)
        else:
            return

    if initfn:
        initfn()

    if not opts['daemon']:
        writepid(os.getpid())

    if opts['daemon_pipefds']:
        lockpath = opts['daemon_pipefds']
        try:
            os.setsid()
        except AttributeError:
            pass
        os.unlink(lockpath)
        util.hidewindow()
        sys.stdout.flush()
        sys.stderr.flush()

        nullfd = os.open(os.devnull, os.O_RDWR)
        logfilefd = nullfd
        if logfile:
            logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
        os.dup2(nullfd, 0)
        os.dup2(logfilefd, 1)
        os.dup2(logfilefd, 2)
        if nullfd not in (0, 1, 2):
            os.close(nullfd)
        if logfile and logfilefd not in (0, 1, 2):
            os.close(logfilefd)

    if runfn:
        return runfn()

def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
    """Utility function used by commands.import to import a single patch

    This function is explicitly defined here to help the evolve extension to
    wrap this part of the import logic.

    The API is currently a bit ugly because it a simple code translation from
    the import command. Feel free to make it better.

    :hunk: a patch (as a binary string)
    :parents: nodes that will be parent of the created commit
    :opts: the full dict of option passed to the import command
    :msgs: list to save commit message to.
           (used in case we need to save it when failing)
    :updatefunc: a function that update a repo to a given node
                 updatefunc(<repo>, <node>)
    """
    tmpname, message, user, date, branch, nodeid, p1, p2 = \
        patch.extract(ui, hunk)

    editor = commiteditor
    if opts.get('edit'):
        editor = commitforceeditor
    update = not opts.get('bypass')
    strip = opts["strip"]
    sim = float(opts.get('similarity') or 0)
    if not tmpname:
        return (None, None)
    msg = _('applied to working directory')

    try:
        cmdline_message = logmessage(ui, opts)
        if cmdline_message:
            # pickup the cmdline msg
            message = cmdline_message
        elif message:
            # pickup the patch msg
            message = message.strip()
        else:
            # launch the editor
            message = None
        ui.debug('message:\n%s\n' % message)

        if len(parents) == 1:
            parents.append(repo[nullid])
        if opts.get('exact'):
            if not nodeid or not p1:
                raise util.Abort(_('not a Mercurial patch'))
            p1 = repo[p1]
            p2 = repo[p2 or nullid]
        elif p2:
            try:
                p1 = repo[p1]
                p2 = repo[p2]
                # Without any options, consider p2 only if the
                # patch is being applied on top of the recorded
                # first parent.
                if p1 != parents[0]:
                    p1 = parents[0]
                    p2 = repo[nullid]
            except error.RepoError:
                p1, p2 = parents
        else:
            p1, p2 = parents

        n = None
        if update:
            if p1 != parents[0]:
                updatefunc(repo, p1.node())
            if p2 != parents[1]:
                repo.setparents(p1.node(), p2.node())

            if opts.get('exact') or opts.get('import_branch'):
                repo.dirstate.setbranch(branch or 'default')

            files = set()
            patch.patch(ui, repo, tmpname, strip=strip, files=files,
                        eolmode=None, similarity=sim / 100.0)
            files = list(files)
            if opts.get('no_commit'):
                if message:
                    msgs.append(message)
            else:
                if opts.get('exact') or p2:
                    # If you got here, you either use --force and know what
                    # you are doing or used --exact or a merge patch while
                    # being updated to its first parent.
                    m = None
                else:
                    m = scmutil.matchfiles(repo, files or [])
                n = repo.commit(message, opts.get('user') or user,
                                opts.get('date') or date, match=m,
                                editor=editor)
        else:
            if opts.get('exact') or opts.get('import_branch'):
                branch = branch or 'default'
            else:
                branch = p1.branch()
            store = patch.filestore()
            try:
                files = set()
                try:
                    patch.patchrepo(ui, repo, p1, store, tmpname, strip,
                                    files, eolmode=None)
                except patch.PatchError, e:
                    raise util.Abort(str(e))
                memctx = context.makememctx(repo, (p1.node(), p2.node()),
                                            message,
                                            opts.get('user') or user,
                                            opts.get('date') or date,
                                            branch, files, store,
                                            editor=commiteditor)
                repo.savecommitmessage(memctx.description())
                n = memctx.commit()
            finally:
                store.close()
        if opts.get('exact') and hex(n) != nodeid:
            raise util.Abort(_('patch is damaged or loses information'))
        if n:
            # i18n: refers to a short changeset id
            msg = _('created %s') % short(n)
        return (msg, n)
    finally:
        os.unlink(tmpname)

def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
           opts=None):
    '''export changesets as hg patches.'''

    total = len(revs)
    revwidth = max([len(str(rev)) for rev in revs])
    filemode = {}

    def single(rev, seqno, fp):
        ctx = repo[rev]
        node = ctx.node()
        parents = [p.node() for p in ctx.parents() if p]
        branch = ctx.branch()
        if switch_parent:
            parents.reverse()
        prev = (parents and parents[0]) or nullid

        shouldclose = False
        if not fp and len(template) > 0:
            desc_lines = ctx.description().rstrip().split('\n')
            desc = desc_lines[0]    #Commit always has a first line.
            fp = makefileobj(repo, template, node, desc=desc, total=total,
                             seqno=seqno, revwidth=revwidth, mode='wb',
                             modemap=filemode)
            if fp != template:
                shouldclose = True
        if fp and fp != sys.stdout and util.safehasattr(fp, 'name'):
            repo.ui.note("%s\n" % fp.name)

        if not fp:
            write = repo.ui.write
        else:
            def write(s, **kw):
                fp.write(s)


        write("# HG changeset patch\n")
        write("# User %s\n" % ctx.user())
        write("# Date %d %d\n" % ctx.date())
        write("#      %s\n" % util.datestr(ctx.date()))
        if branch and branch != 'default':
            write("# Branch %s\n" % branch)
        write("# Node ID %s\n" % hex(node))
        write("# Parent  %s\n" % hex(prev))
        if len(parents) > 1:
            write("# Parent  %s\n" % hex(parents[1]))
        write(ctx.description().rstrip())
        write("\n\n")

        for chunk, label in patch.diffui(repo, prev, node, opts=opts):
            write(chunk, label=label)

        if shouldclose:
            fp.close()

    for seqno, rev in enumerate(revs):
        single(rev, seqno + 1, fp)

def diffordiffstat(ui, repo, diffopts, node1, node2, match,
                   changes=None, stat=False, fp=None, prefix='',
                   listsubrepos=False):
    '''show diff or diffstat.'''
    if fp is None:
        write = ui.write
    else:
        def write(s, **kw):
            fp.write(s)

    if stat:
        diffopts = diffopts.copy(context=0)
        width = 80
        if not ui.plain():
            width = ui.termwidth()
        chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
                            prefix=prefix)
        for chunk, label in patch.diffstatui(util.iterlines(chunks),
                                             width=width,
                                             git=diffopts.git):
            write(chunk, label=label)
    else:
        for chunk, label in patch.diffui(repo, node1, node2, match,
                                         changes, diffopts, prefix=prefix):
            write(chunk, label=label)

    if listsubrepos:
        ctx1 = repo[node1]
        ctx2 = repo[node2]
        for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
            tempnode2 = node2
            try:
                if node2 is not None:
                    tempnode2 = ctx2.substate[subpath][1]
            except KeyError:
                # A subrepo that existed in node1 was deleted between node1 and
                # node2 (inclusive). Thus, ctx2's substate won't contain that
                # subpath. The best we can do is to ignore it.
                tempnode2 = None
            submatch = matchmod.narrowmatcher(subpath, match)
            sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
                     stat=stat, fp=fp, prefix=prefix)

class changeset_printer(object):
    '''show changeset information when templating not requested.'''

    def __init__(self, ui, repo, patch, diffopts, buffered):
        self.ui = ui
        self.repo = repo
        self.buffered = buffered
        self.patch = patch
        self.diffopts = diffopts
        self.header = {}
        self.hunk = {}
        self.lastheader = None
        self.footer = None

    def flush(self, rev):
        if rev in self.header:
            h = self.header[rev]
            if h != self.lastheader:
                self.lastheader = h
                self.ui.write(h)
            del self.header[rev]
        if rev in self.hunk:
            self.ui.write(self.hunk[rev])
            del self.hunk[rev]
            return 1
        return 0

    def close(self):
        if self.footer:
            self.ui.write(self.footer)

    def show(self, ctx, copies=None, matchfn=None, **props):
        if self.buffered:
            self.ui.pushbuffer()
            self._show(ctx, copies, matchfn, props)
            self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
        else:
            self._show(ctx, copies, matchfn, props)

    def _show(self, ctx, copies, matchfn, props):
        '''show a single changeset or file revision'''
        changenode = ctx.node()
        rev = ctx.rev()

        if self.ui.quiet:
            self.ui.write("%d:%s\n" % (rev, short(changenode)),
                          label='log.node')
            return

        log = self.repo.changelog
        date = util.datestr(ctx.date())

        hexfunc = self.ui.debugflag and hex or short

        parents = [(p, hexfunc(log.node(p)))
                   for p in self._meaningful_parentrevs(log, rev)]

        # i18n: column positioning for "hg log"
        self.ui.write(_("changeset:   %d:%s\n") % (rev, hexfunc(changenode)),
                      label='log.changeset changeset.%s' % ctx.phasestr())

        branch = ctx.branch()
        # don't show the default branch name
        if branch != 'default':
            # i18n: column positioning for "hg log"
            self.ui.write(_("branch:      %s\n") % branch,
                          label='log.branch')
        for bookmark in self.repo.nodebookmarks(changenode):
            # i18n: column positioning for "hg log"
            self.ui.write(_("bookmark:    %s\n") % bookmark,
                    label='log.bookmark')
        for tag in self.repo.nodetags(changenode):
            # i18n: column positioning for "hg log"
            self.ui.write(_("tag:         %s\n") % tag,
                          label='log.tag')
        if self.ui.debugflag and ctx.phase():
            # i18n: column positioning for "hg log"
            self.ui.write(_("phase:       %s\n") % _(ctx.phasestr()),
                          label='log.phase')
        for parent in parents:
            # i18n: column positioning for "hg log"
            self.ui.write(_("parent:      %d:%s\n") % parent,
                          label='log.parent changeset.%s' % ctx.phasestr())

        if self.ui.debugflag:
            mnode = ctx.manifestnode()
            # i18n: column positioning for "hg log"
            self.ui.write(_("manifest:    %d:%s\n") %
                          (self.repo.manifest.rev(mnode), hex(mnode)),
                          label='ui.debug log.manifest')
        # i18n: column positioning for "hg log"
        self.ui.write(_("user:        %s\n") % ctx.user(),
                      label='log.user')
        # i18n: column positioning for "hg log"
        self.ui.write(_("date:        %s\n") % date,
                      label='log.date')

        if self.ui.debugflag:
            files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
            for key, value in zip([# i18n: column positioning for "hg log"
                                   _("files:"),
                                   # i18n: column positioning for "hg log"
                                   _("files+:"),
                                   # i18n: column positioning for "hg log"
                                   _("files-:")], files):
                if value:
                    self.ui.write("%-12s %s\n" % (key, " ".join(value)),
                                  label='ui.debug log.files')
        elif ctx.files() and self.ui.verbose:
            # i18n: column positioning for "hg log"
            self.ui.write(_("files:       %s\n") % " ".join(ctx.files()),
                          label='ui.note log.files')
        if copies and self.ui.verbose:
            copies = ['%s (%s)' % c for c in copies]
            # i18n: column positioning for "hg log"
            self.ui.write(_("copies:      %s\n") % ' '.join(copies),
                          label='ui.note log.copies')

        extra = ctx.extra()
        if extra and self.ui.debugflag:
            for key, value in sorted(extra.items()):
                # i18n: column positioning for "hg log"
                self.ui.write(_("extra:       %s=%s\n")
                              % (key, value.encode('string_escape')),
                              label='ui.debug log.extra')

        description = ctx.description().strip()
        if description:
            if self.ui.verbose:
                self.ui.write(_("description:\n"),
                              label='ui.note log.description')
                self.ui.write(description,
                              label='ui.note log.description')
                self.ui.write("\n\n")
            else:
                # i18n: column positioning for "hg log"
                self.ui.write(_("summary:     %s\n") %
                              description.splitlines()[0],
                              label='log.summary')
        self.ui.write("\n")

        self.showpatch(changenode, matchfn)

    def showpatch(self, node, matchfn):
        if not matchfn:
            matchfn = self.patch
        if matchfn:
            stat = self.diffopts.get('stat')
            diff = self.diffopts.get('patch')
            diffopts = patch.diffopts(self.ui, self.diffopts)
            prev = self.repo.changelog.parents(node)[0]
            if stat:
                diffordiffstat(self.ui, self.repo, diffopts, prev, node,
                               match=matchfn, stat=True)
            if diff:
                if stat:
                    self.ui.write("\n")
                diffordiffstat(self.ui, self.repo, diffopts, prev, node,
                               match=matchfn, stat=False)
            self.ui.write("\n")

    def _meaningful_parentrevs(self, log, rev):
        """Return list of meaningful (or all if debug) parentrevs for rev.

        For merges (two non-nullrev revisions) both parents are meaningful.
        Otherwise the first parent revision is considered meaningful if it
        is not the preceding revision.
        """
        parents = log.parentrevs(rev)
        if not self.ui.debugflag and parents[1] == nullrev:
            if parents[0] >= rev - 1:
                parents = []
            else:
                parents = [parents[0]]
        return parents


class changeset_templater(changeset_printer):
    '''format changeset information.'''

    def __init__(self, ui, repo, patch, diffopts, tmpl, mapfile, buffered):
        changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
        formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
        defaulttempl = {
            'parent': '{rev}:{node|formatnode} ',
            'manifest': '{rev}:{node|formatnode}',
            'file_copy': '{name} ({source})',
            'extra': '{key}={value|stringescape}'
            }
        # filecopy is preserved for compatibility reasons
        defaulttempl['filecopy'] = defaulttempl['file_copy']
        self.t = templater.templater(mapfile, {'formatnode': formatnode},
                                     cache=defaulttempl)
        if tmpl:
            self.t.cache['changeset'] = tmpl

        self.cache = {}

    def _meaningful_parentrevs(self, ctx):
        """Return list of meaningful (or all if debug) parentrevs for rev.
        """
        parents = ctx.parents()
        if len(parents) > 1:
            return parents
        if self.ui.debugflag:
            return [parents[0], self.repo['null']]
        if parents[0].rev() >= ctx.rev() - 1:
            return []
        return parents

    def _show(self, ctx, copies, matchfn, props):
        '''show a single changeset or file revision'''

        showlist = templatekw.showlist

        # showparents() behaviour depends on ui trace level which
        # causes unexpected behaviours at templating level and makes
        # it harder to extract it in a standalone function. Its
        # behaviour cannot be changed so leave it here for now.
        def showparents(**args):
            ctx = args['ctx']
            parents = [[('rev', p.rev()), ('node', p.hex())]
                       for p in self._meaningful_parentrevs(ctx)]
            return showlist('parent', parents, **args)

        props = props.copy()
        props.update(templatekw.keywords)
        props['parents'] = showparents
        props['templ'] = self.t
        props['ctx'] = ctx
        props['repo'] = self.repo
        props['revcache'] = {'copies': copies}
        props['cache'] = self.cache

        # find correct templates for current mode

        tmplmodes = [
            (True, None),
            (self.ui.verbose, 'verbose'),
            (self.ui.quiet, 'quiet'),
            (self.ui.debugflag, 'debug'),
        ]

        types = {'header': '', 'footer':'', 'changeset': 'changeset'}
        for mode, postfix  in tmplmodes:
            for type in types:
                cur = postfix and ('%s_%s' % (type, postfix)) or type
                if mode and cur in self.t:
                    types[type] = cur

        try:

            # write header
            if types['header']:
                h = templater.stringify(self.t(types['header'], **props))
                if self.buffered:
                    self.header[ctx.rev()] = h
                else:
                    if self.lastheader != h:
                        self.lastheader = h
                        self.ui.write(h)

            # write changeset metadata, then patch if requested
            key = types['changeset']
            self.ui.write(templater.stringify(self.t(key, **props)))
            self.showpatch(ctx.node(), matchfn)

            if types['footer']:
                if not self.footer:
                    self.footer = templater.stringify(self.t(types['footer'],
                                                      **props))

        except KeyError, inst:
            msg = _("%s: no key named '%s'")
            raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
        except SyntaxError, inst:
            raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))

def gettemplate(ui, tmpl, style):
    """
    Find the template matching the given template spec or style.
    """

    # ui settings
    if not tmpl and not style:
        tmpl = ui.config('ui', 'logtemplate')
        if tmpl:
            try:
                tmpl = templater.parsestring(tmpl)
            except SyntaxError:
                tmpl = templater.parsestring(tmpl, quoted=False)
            return tmpl, None
        else:
            style = util.expandpath(ui.config('ui', 'style', ''))

    if style:
        mapfile = style
        if not os.path.split(mapfile)[0]:
            mapname = (templater.templatepath('map-cmdline.' + mapfile)
                       or templater.templatepath(mapfile))
            if mapname:
                mapfile = mapname
        return None, mapfile

    if not tmpl:
        return None, None

    # looks like a literal template?
    if '{' in tmpl:
        return tmpl, None

    # perhaps a stock style?
    if not os.path.split(tmpl)[0]:
        mapname = (templater.templatepath('map-cmdline.' + tmpl)
                   or templater.templatepath(tmpl))
        if mapname and os.path.isfile(mapname):
            return None, mapname

    # perhaps it's a reference to [templates]
    t = ui.config('templates', tmpl)
    if t:
        try:
            tmpl = templater.parsestring(t)
        except SyntaxError:
            tmpl = templater.parsestring(t, quoted=False)
        return tmpl, None

    # perhaps it's a path to a map or a template
    if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
        # is it a mapfile for a style?
        if os.path.basename(tmpl).startswith("map-"):
            return None, os.path.realpath(tmpl)
        tmpl = open(tmpl).read()
        return tmpl, None

    # constant string?
    return tmpl, None

def show_changeset(ui, repo, opts, buffered=False):
    """show one changeset using template or regular display.

    Display format will be the first non-empty hit of:
    1. option 'template'
    2. option 'style'
    3. [ui] setting 'logtemplate'
    4. [ui] setting 'style'
    If all of these values are either the unset or the empty string,
    regular display via changeset_printer() is done.
    """
    # options
    patch = None
    if opts.get('patch') or opts.get('stat'):
        patch = scmutil.matchall(repo)

    tmpl, mapfile = gettemplate(ui, opts.get('template'), opts.get('style'))

    if not tmpl and not mapfile:
        return changeset_printer(ui, repo, patch, opts, buffered)

    try:
        t = changeset_templater(ui, repo, patch, opts, tmpl, mapfile, buffered)
    except SyntaxError, inst:
        raise util.Abort(inst.args[0])
    return t

def showmarker(ui, marker):
    """utility function to display obsolescence marker in a readable way

    To be used by debug function."""
    ui.write(hex(marker.precnode()))
    for repl in marker.succnodes():
        ui.write(' ')
        ui.write(hex(repl))
    ui.write(' %X ' % marker._data[2])
    ui.write('{%s}' % (', '.join('%r: %r' % t for t in
                                 sorted(marker.metadata().items()))))
    ui.write('\n')

def finddate(ui, repo, date):
    """Find the tipmost changeset that matches the given date spec"""

    df = util.matchdate(date)
    m = scmutil.matchall(repo)
    results = {}

    def prep(ctx, fns):
        d = ctx.date()
        if df(d[0]):
            results[ctx.rev()] = d

    for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
        rev = ctx.rev()
        if rev in results:
            ui.status(_("found revision %s from %s\n") %
                      (rev, util.datestr(results[rev])))
            return str(rev)

    raise util.Abort(_("revision matching date not found"))

def increasingwindows(windowsize=8, sizelimit=512):
    while True:
        yield windowsize
        if windowsize < sizelimit:
            windowsize *= 2

class FileWalkError(Exception):
    pass

def walkfilerevs(repo, match, follow, revs, fncache):
    '''Walks the file history for the matched files.

    Returns the changeset revs that are involved in the file history.

    Throws FileWalkError if the file history can't be walked using
    filelogs alone.
    '''
    wanted = set()
    copies = []
    minrev, maxrev = min(revs), max(revs)
    def filerevgen(filelog, last):
        """
        Only files, no patterns.  Check the history of each file.

        Examines filelog entries within minrev, maxrev linkrev range
        Returns an iterator yielding (linkrev, parentlinkrevs, copied)
        tuples in backwards order
        """
        cl_count = len(repo)
        revs = []
        for j in xrange(0, last + 1):
            linkrev = filelog.linkrev(j)
            if linkrev < minrev:
                continue
            # only yield rev for which we have the changelog, it can
            # happen while doing "hg log" during a pull or commit
            if linkrev >= cl_count:
                break

            parentlinkrevs = []
            for p in filelog.parentrevs(j):
                if p != nullrev:
                    parentlinkrevs.append(filelog.linkrev(p))
            n = filelog.node(j)
            revs.append((linkrev, parentlinkrevs,
                         follow and filelog.renamed(n)))

        return reversed(revs)
    def iterfiles():
        pctx = repo['.']
        for filename in match.files():
            if follow:
                if filename not in pctx:
                    raise util.Abort(_('cannot follow file not in parent '
                                       'revision: "%s"') % filename)
                yield filename, pctx[filename].filenode()
            else:
                yield filename, None
        for filename_node in copies:
            yield filename_node

    for file_, node in iterfiles():
        filelog = repo.file(file_)
        if not len(filelog):
            if node is None:
                # A zero count may be a directory or deleted file, so
                # try to find matching entries on the slow path.
                if follow:
                    raise util.Abort(
                        _('cannot follow nonexistent file: "%s"') % file_)
                raise FileWalkError("Cannot walk via filelog")
            else:
                continue

        if node is None:
            last = len(filelog) - 1
        else:
            last = filelog.rev(node)


        # keep track of all ancestors of the file
        ancestors = set([filelog.linkrev(last)])

        # iterate from latest to oldest revision
        for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
            if not follow:
                if rev > maxrev:
                    continue
            else:
                # Note that last might not be the first interesting
                # rev to us:
                # if the file has been changed after maxrev, we'll
                # have linkrev(last) > maxrev, and we still need
                # to explore the file graph
                if rev not in ancestors:
                    continue
                # XXX insert 1327 fix here
                if flparentlinkrevs:
                    ancestors.update(flparentlinkrevs)

            fncache.setdefault(rev, []).append(file_)
            wanted.add(rev)
            if copied:
                copies.append(copied)

    return wanted

def walkchangerevs(repo, match, opts, prepare):
    '''Iterate over files and the revs in which they changed.

    Callers most commonly need to iterate backwards over the history
    in which they are interested. Doing so has awful (quadratic-looking)
    performance, so we use iterators in a "windowed" way.

    We walk a window of revisions in the desired order.  Within the
    window, we first walk forwards to gather data, then in the desired
    order (usually backwards) to display it.

    This function returns an iterator yielding contexts. Before
    yielding each context, the iterator will first call the prepare
    function on each context in the window in forward order.'''

    follow = opts.get('follow') or opts.get('follow_first')

    if opts.get('rev'):
        revs = scmutil.revrange(repo, opts.get('rev'))
    elif follow:
        revs = repo.revs('reverse(:.)')
    else:
        revs = revset.spanset(repo)
        revs.reverse()
    if not revs:
        return []
    wanted = set()
    slowpath = match.anypats() or (match.files() and opts.get('removed'))
    fncache = {}
    change = repo.changectx

    # First step is to fill wanted, the set of revisions that we want to yield.
    # When it does not induce extra cost, we also fill fncache for revisions in
    # wanted: a cache of filenames that were changed (ctx.files()) and that
    # match the file filtering conditions.

    if not slowpath and not match.files():
        # No files, no patterns.  Display all revs.
        wanted = revs

    if not slowpath and match.files():
        # We only have to read through the filelog to find wanted revisions

        try:
            wanted = walkfilerevs(repo, match, follow, revs, fncache)
        except FileWalkError:
            slowpath = True

            # We decided to fall back to the slowpath because at least one
            # of the paths was not a file. Check to see if at least one of them
            # existed in history, otherwise simply return
            for path in match.files():
                if path == '.' or path in repo.store:
                    break
            else:
                return []

    if slowpath:
        # We have to read the changelog to match filenames against
        # changed files

        if follow:
            raise util.Abort(_('can only follow copies/renames for explicit '
                               'filenames'))

        # The slow path checks files modified in every changeset.
        # This is really slow on large repos, so compute the set lazily.
        class lazywantedset(object):
            def __init__(self):
                self.set = set()
                self.revs = set(revs)

            # No need to worry about locality here because it will be accessed
            # in the same order as the increasing window below.
            def __contains__(self, value):
                if value in self.set:
                    return True
                elif not value in self.revs:
                    return False
                else:
                    self.revs.discard(value)
                    ctx = change(value)
                    matches = filter(match, ctx.files())
                    if matches:
                        fncache[value] = matches
                        self.set.add(value)
                        return True
                    return False

            def discard(self, value):
                self.revs.discard(value)
                self.set.discard(value)

        wanted = lazywantedset()

    class followfilter(object):
        def __init__(self, onlyfirst=False):
            self.startrev = nullrev
            self.roots = set()
            self.onlyfirst = onlyfirst

        def match(self, rev):
            def realparents(rev):
                if self.onlyfirst:
                    return repo.changelog.parentrevs(rev)[0:1]
                else:
                    return filter(lambda x: x != nullrev,
                                  repo.changelog.parentrevs(rev))

            if self.startrev == nullrev:
                self.startrev = rev
                return True

            if rev > self.startrev:
                # forward: all descendants
                if not self.roots:
                    self.roots.add(self.startrev)
                for parent in realparents(rev):
                    if parent in self.roots:
                        self.roots.add(rev)
                        return True
            else:
                # backwards: all parents
                if not self.roots:
                    self.roots.update(realparents(self.startrev))
                if rev in self.roots:
                    self.roots.remove(rev)
                    self.roots.update(realparents(rev))
                    return True

            return False

    # it might be worthwhile to do this in the iterator if the rev range
    # is descending and the prune args are all within that range
    for rev in opts.get('prune', ()):
        rev = repo[rev].rev()
        ff = followfilter()
        stop = min(revs[0], revs[-1])
        for x in xrange(rev, stop - 1, -1):
            if ff.match(x):
                wanted = wanted - [x]

    # Now that wanted is correctly initialized, we can iterate over the
    # revision range, yielding only revisions in wanted.
    def iterate():
        if follow and not match.files():
            ff = followfilter(onlyfirst=opts.get('follow_first'))
            def want(rev):
                return ff.match(rev) and rev in wanted
        else:
            def want(rev):
                return rev in wanted

        it = iter(revs)
        stopiteration = False
        for windowsize in increasingwindows():
            nrevs = []
            for i in xrange(windowsize):
                try:
                    rev = it.next()
                    if want(rev):
                        nrevs.append(rev)
                except (StopIteration):
                    stopiteration = True
                    break
            for rev in sorted(nrevs):
                fns = fncache.get(rev)
                ctx = change(rev)
                if not fns:
                    def fns_generator():
                        for f in ctx.files():
                            if match(f):
                                yield f
                    fns = fns_generator()
                prepare(ctx, fns)
            for rev in nrevs:
                yield change(rev)

            if stopiteration:
                break

    return iterate()

def _makelogfilematcher(repo, pats, followfirst):
    # When displaying a revision with --patch --follow FILE, we have
    # to know which file of the revision must be diffed. With
    # --follow, we want the names of the ancestors of FILE in the
    # revision, stored in "fcache". "fcache" is populated by
    # reproducing the graph traversal already done by --follow revset
    # and relating linkrevs to file names (which is not "correct" but
    # good enough).
    fcache = {}
    fcacheready = [False]
    pctx = repo['.']
    wctx = repo[None]

    def populate():
        for fn in pats:
            for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
                for c in i:
                    fcache.setdefault(c.linkrev(), set()).add(c.path())

    def filematcher(rev):
        if not fcacheready[0]:
            # Lazy initialization
            fcacheready[0] = True
            populate()
        return scmutil.match(wctx, fcache.get(rev, []), default='path')

    return filematcher

def _makelogrevset(repo, pats, opts, revs):
    """Return (expr, filematcher) where expr is a revset string built
    from log options and file patterns or None. If --stat or --patch
    are not passed filematcher is None. Otherwise it is a callable
    taking a revision number and returning a match objects filtering
    the files to be detailed when displaying the revision.
    """
    opt2revset = {
        'no_merges':        ('not merge()', None),
        'only_merges':      ('merge()', None),
        '_ancestors':       ('ancestors(%(val)s)', None),
        '_fancestors':      ('_firstancestors(%(val)s)', None),
        '_descendants':     ('descendants(%(val)s)', None),
        '_fdescendants':    ('_firstdescendants(%(val)s)', None),
        '_matchfiles':      ('_matchfiles(%(val)s)', None),
        'date':             ('date(%(val)r)', None),
        'branch':           ('branch(%(val)r)', ' or '),
        '_patslog':         ('filelog(%(val)r)', ' or '),
        '_patsfollow':      ('follow(%(val)r)', ' or '),
        '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
        'keyword':          ('keyword(%(val)r)', ' or '),
        'prune':            ('not (%(val)r or ancestors(%(val)r))', ' and '),
        'user':             ('user(%(val)r)', ' or '),
        }

    opts = dict(opts)
    # follow or not follow?
    follow = opts.get('follow') or opts.get('follow_first')
    followfirst = opts.get('follow_first') and 1 or 0
    # --follow with FILE behaviour depends on revs...
    it = iter(revs)
    startrev = it.next()
    try:
        followdescendants = startrev < it.next()
    except (StopIteration):
        followdescendants = False

    # branch and only_branch are really aliases and must be handled at
    # the same time
    opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
    opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
    # pats/include/exclude are passed to match.match() directly in
    # _matchfiles() revset but walkchangerevs() builds its matcher with
    # scmutil.match(). The difference is input pats are globbed on
    # platforms without shell expansion (windows).
    pctx = repo[None]
    match, pats = scmutil.matchandpats(pctx, pats, opts)
    slowpath = match.anypats() or (match.files() and opts.get('removed'))
    if not slowpath:
        for f in match.files():
            if follow and f not in pctx:
                raise util.Abort(_('cannot follow file not in parent '
                                   'revision: "%s"') % f)
            filelog = repo.file(f)
            if not filelog:
                # A zero count may be a directory or deleted file, so
                # try to find matching entries on the slow path.
                if follow:
                    raise util.Abort(
                        _('cannot follow nonexistent file: "%s"') % f)
                slowpath = True

        # We decided to fall back to the slowpath because at least one
        # of the paths was not a file. Check to see if at least one of them
        # existed in history - in that case, we'll continue down the
        # slowpath; otherwise, we can turn off the slowpath
        if slowpath:
            for path in match.files():
                if path == '.' or path in repo.store:
                    break
            else:
                slowpath = False

    if slowpath:
        # See walkchangerevs() slow path.
        #
        if follow:
            raise util.Abort(_('can only follow copies/renames for explicit '
                               'filenames'))
        # pats/include/exclude cannot be represented as separate
        # revset expressions as their filtering logic applies at file
        # level. For instance "-I a -X a" matches a revision touching
        # "a" and "b" while "file(a) and not file(b)" does
        # not. Besides, filesets are evaluated against the working
        # directory.
        matchargs = ['r:', 'd:relpath']
        for p in pats:
            matchargs.append('p:' + p)
        for p in opts.get('include', []):
            matchargs.append('i:' + p)
        for p in opts.get('exclude', []):
            matchargs.append('x:' + p)
        matchargs = ','.join(('%r' % p) for p in matchargs)
        opts['_matchfiles'] = matchargs
    else:
        if follow:
            fpats = ('_patsfollow', '_patsfollowfirst')
            fnopats = (('_ancestors', '_fancestors'),
                       ('_descendants', '_fdescendants'))
            if pats:
                # follow() revset interprets its file argument as a
                # manifest entry, so use match.files(), not pats.
                opts[fpats[followfirst]] = list(match.files())
            else:
                opts[fnopats[followdescendants][followfirst]] = str(startrev)
        else:
            opts['_patslog'] = list(pats)

    filematcher = None
    if opts.get('patch') or opts.get('stat'):
        if follow:
            filematcher = _makelogfilematcher(repo, pats, followfirst)
        else:
            filematcher = lambda rev: match

    expr = []
    for op, val in opts.iteritems():
        if not val:
            continue
        if op not in opt2revset:
            continue
        revop, andor = opt2revset[op]
        if '%(val)' not in revop:
            expr.append(revop)
        else:
            if not isinstance(val, list):
                e = revop % {'val': val}
            else:
                e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
            expr.append(e)

    if expr:
        expr = '(' + ' and '.join(expr) + ')'
    else:
        expr = None
    return expr, filematcher

def getgraphlogrevs(repo, pats, opts):
    """Return (revs, expr, filematcher) where revs is an iterable of
    revision numbers, expr is a revset string built from log options
    and file patterns or None, and used to filter 'revs'. If --stat or
    --patch are not passed filematcher is None. Otherwise it is a
    callable taking a revision number and returning a match objects
    filtering the files to be detailed when displaying the revision.
    """
    if not len(repo):
        return [], None, None
    limit = loglimit(opts)
    # Default --rev value depends on --follow but --follow behaviour
    # depends on revisions resolved from --rev...
    follow = opts.get('follow') or opts.get('follow_first')
    possiblyunsorted = False # whether revs might need sorting
    if opts.get('rev'):
        revs = scmutil.revrange(repo, opts['rev'])
        # Don't sort here because _makelogrevset might depend on the
        # order of revs
        possiblyunsorted = True
    else:
        if follow and len(repo) > 0:
            revs = repo.revs('reverse(:.)')
        else:
            revs = revset.spanset(repo)
            revs.reverse()
    if not revs:
        return revset.baseset(), None, None
    expr, filematcher = _makelogrevset(repo, pats, opts, revs)
    if possiblyunsorted:
        revs.sort(reverse=True)
    if expr:
        # Revset matchers often operate faster on revisions in changelog
        # order, because most filters deal with the changelog.
        revs.reverse()
        matcher = revset.match(repo.ui, expr)
        # Revset matches can reorder revisions. "A or B" typically returns
        # returns the revision matching A then the revision matching B. Sort
        # again to fix that.
        revs = matcher(repo, revs)
        revs.sort(reverse=True)
    if limit is not None:
        limitedrevs = revset.baseset()
        for idx, rev in enumerate(revs):
            if idx >= limit:
                break
            limitedrevs.append(rev)
        revs = limitedrevs

    return revs, expr, filematcher

def getlogrevs(repo, pats, opts):
    """Return (revs, expr, filematcher) where revs is an iterable of
    revision numbers, expr is a revset string built from log options
    and file patterns or None, and used to filter 'revs'. If --stat or
    --patch are not passed filematcher is None. Otherwise it is a
    callable taking a revision number and returning a match objects
    filtering the files to be detailed when displaying the revision.
    """
    limit = loglimit(opts)
    # Default --rev value depends on --follow but --follow behaviour
    # depends on revisions resolved from --rev...
    follow = opts.get('follow') or opts.get('follow_first')
    if opts.get('rev'):
        revs = scmutil.revrange(repo, opts['rev'])
    elif follow:
        revs = revset.baseset(repo.revs('reverse(:.)'))
    else:
        revs = revset.spanset(repo)
        revs.reverse()
    if not revs:
        return revset.baseset([]), None, None
    expr, filematcher = _makelogrevset(repo, pats, opts, revs)
    if expr:
        # Revset matchers often operate faster on revisions in changelog
        # order, because most filters deal with the changelog.
        if not opts.get('rev'):
            revs.reverse()
        matcher = revset.match(repo.ui, expr)
        # Revset matches can reorder revisions. "A or B" typically returns
        # returns the revision matching A then the revision matching B. Sort
        # again to fix that.
        revs = matcher(repo, revs)
        if not opts.get('rev'):
            revs.sort(reverse=True)
    if limit is not None:
        count = 0
        limitedrevs = revset.baseset([])
        it = iter(revs)
        while count < limit:
            try:
                limitedrevs.append(it.next())
            except (StopIteration):
                break
            count += 1
        revs = limitedrevs

    return revs, expr, filematcher

def displaygraph(ui, dag, displayer, showparents, edgefn, getrenamed=None,
                 filematcher=None):
    seen, state = [], graphmod.asciistate()
    for rev, type, ctx, parents in dag:
        char = 'o'
        if ctx.node() in showparents:
            char = '@'
        elif ctx.obsolete():
            char = 'x'
        copies = None
        if getrenamed and ctx.rev():
            copies = []
            for fn in ctx.files():
                rename = getrenamed(fn, ctx.rev())
                if rename:
                    copies.append((fn, rename[0]))
        revmatchfn = None
        if filematcher is not None:
            revmatchfn = filematcher(ctx.rev())
        displayer.show(ctx, copies=copies, matchfn=revmatchfn)
        lines = displayer.hunk.pop(rev).split('\n')
        if not lines[-1]:
            del lines[-1]
        displayer.flush(rev)
        edges = edgefn(type, char, lines, seen, rev, parents)
        for type, char, lines, coldata in edges:
            graphmod.ascii(ui, state, type, char, lines, coldata)
    displayer.close()

def graphlog(ui, repo, *pats, **opts):
    # Parameters are identical to log command ones
    revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
    revdag = graphmod.dagwalker(repo, revs)

    getrenamed = None
    if opts.get('copies'):
        endrev = None
        if opts.get('rev'):
            endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
        getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
    displayer = show_changeset(ui, repo, opts, buffered=True)
    showparents = [ctx.node() for ctx in repo[None].parents()]
    displaygraph(ui, revdag, displayer, showparents,
                 graphmod.asciiedges, getrenamed, filematcher)

def checkunsupportedgraphflags(pats, opts):
    for op in ["newest_first"]:
        if op in opts and opts[op]:
            raise util.Abort(_("-G/--graph option is incompatible with --%s")
                             % op.replace("_", "-"))

def graphrevs(repo, nodes, opts):
    limit = loglimit(opts)
    nodes.reverse()
    if limit is not None:
        nodes = nodes[:limit]
    return graphmod.nodes(repo, nodes)

def add(ui, repo, match, dryrun, listsubrepos, prefix, explicitonly):
    join = lambda f: os.path.join(prefix, f)
    bad = []
    oldbad = match.bad
    match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
    names = []
    wctx = repo[None]
    cca = None
    abort, warn = scmutil.checkportabilityalert(ui)
    if abort or warn:
        cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
    for f in repo.walk(match):
        exact = match.exact(f)
        if exact or not explicitonly and f not in repo.dirstate:
            if cca:
                cca(f)
            names.append(f)
            if ui.verbose or not exact:
                ui.status(_('adding %s\n') % match.rel(join(f)))

    for subpath in sorted(wctx.substate):
        sub = wctx.sub(subpath)
        try:
            submatch = matchmod.narrowmatcher(subpath, match)
            if listsubrepos:
                bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
                                   False))
            else:
                bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
                                   True))
        except error.LookupError:
            ui.status(_("skipping missing subrepository: %s\n")
                           % join(subpath))

    if not dryrun:
        rejected = wctx.add(names, prefix)
        bad.extend(f for f in rejected if f in match.files())
    return bad

def forget(ui, repo, match, prefix, explicitonly):
    join = lambda f: os.path.join(prefix, f)
    bad = []
    oldbad = match.bad
    match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
    wctx = repo[None]
    forgot = []
    s = repo.status(match=match, clean=True)
    forget = sorted(s[0] + s[1] + s[3] + s[6])
    if explicitonly:
        forget = [f for f in forget if match.exact(f)]

    for subpath in sorted(wctx.substate):
        sub = wctx.sub(subpath)
        try:
            submatch = matchmod.narrowmatcher(subpath, match)
            subbad, subforgot = sub.forget(ui, submatch, prefix)
            bad.extend([subpath + '/' + f for f in subbad])
            forgot.extend([subpath + '/' + f for f in subforgot])
        except error.LookupError:
            ui.status(_("skipping missing subrepository: %s\n")
                           % join(subpath))

    if not explicitonly:
        for f in match.files():
            if f not in repo.dirstate and not os.path.isdir(match.rel(join(f))):
                if f not in forgot:
                    if os.path.exists(match.rel(join(f))):
                        ui.warn(_('not removing %s: '
                                  'file is already untracked\n')
                                % match.rel(join(f)))
                    bad.append(f)

    for f in forget:
        if ui.verbose or not match.exact(f):
            ui.status(_('removing %s\n') % match.rel(join(f)))

    rejected = wctx.forget(forget, prefix)
    bad.extend(f for f in rejected if f in match.files())
    forgot.extend(forget)
    return bad, forgot

def cat(ui, repo, ctx, matcher, prefix, **opts):
    err = 1

    def write(path):
        fp = makefileobj(repo, opts.get('output'), ctx.node(),
                         pathname=os.path.join(prefix, path))
        data = ctx[path].data()
        if opts.get('decode'):
            data = repo.wwritedata(path, data)
        fp.write(data)
        fp.close()

    # Automation often uses hg cat on single files, so special case it
    # for performance to avoid the cost of parsing the manifest.
    if len(matcher.files()) == 1 and not matcher.anypats():
        file = matcher.files()[0]
        mf = repo.manifest
        mfnode = ctx._changeset[0]
        if mf.find(mfnode, file)[0]:
            write(file)
            return 0

    # Don't warn about "missing" files that are really in subrepos
    bad = matcher.bad

    def badfn(path, msg):
        for subpath in ctx.substate:
            if path.startswith(subpath):
                return
        bad(path, msg)

    matcher.bad = badfn

    for abs in ctx.walk(matcher):
        write(abs)
        err = 0

    matcher.bad = bad

    for subpath in sorted(ctx.substate):
        sub = ctx.sub(subpath)
        try:
            submatch = matchmod.narrowmatcher(subpath, matcher)

            if not sub.cat(ui, submatch, os.path.join(prefix, sub._path),
                           **opts):
                err = 0
        except error.RepoLookupError:
            ui.status(_("skipping missing subrepository: %s\n")
                           % os.path.join(prefix, subpath))

    return err

def duplicatecopies(repo, rev, fromrev, skiprev=None):
    '''reproduce copies from fromrev to rev in the dirstate

    If skiprev is specified, it's a revision that should be used to
    filter copy records. Any copies that occur between fromrev and
    skiprev will not be duplicated, even if they appear in the set of
    copies between fromrev and rev.
    '''
    exclude = {}
    if skiprev is not None:
        exclude = copies.pathcopies(repo[fromrev], repo[skiprev])
    for dst, src in copies.pathcopies(repo[fromrev], repo[rev]).iteritems():
        # copies.pathcopies returns backward renames, so dst might not
        # actually be in the dirstate
        if dst in exclude:
            continue
        if repo.dirstate[dst] in "nma":
            repo.dirstate.copy(src, dst)

def commit(ui, repo, commitfunc, pats, opts):
    '''commit the specified files or all outstanding changes'''
    date = opts.get('date')
    if date:
        opts['date'] = util.parsedate(date)
    message = logmessage(ui, opts)

    # extract addremove carefully -- this function can be called from a command
    # that doesn't support addremove
    if opts.get('addremove'):
        scmutil.addremove(repo, pats, opts)

    return commitfunc(ui, repo, message,
                      scmutil.match(repo[None], pats, opts), opts)

def amend(ui, repo, commitfunc, old, extra, pats, opts):
    ui.note(_('amending changeset %s\n') % old)
    base = old.p1()

    wlock = lock = newid = None
    try:
        wlock = repo.wlock()
        lock = repo.lock()
        tr = repo.transaction('amend')
        try:
            # See if we got a message from -m or -l, if not, open the editor
            # with the message of the changeset to amend
            message = logmessage(ui, opts)
            # ensure logfile does not conflict with later enforcement of the
            # message. potential logfile content has been processed by
            # `logmessage` anyway.
            opts.pop('logfile')
            # First, do a regular commit to record all changes in the working
            # directory (if there are any)
            ui.callhooks = False
            currentbookmark = repo._bookmarkcurrent
            try:
                repo._bookmarkcurrent = None
                opts['message'] = 'temporary amend commit for %s' % old
                node = commit(ui, repo, commitfunc, pats, opts)
            finally:
                repo._bookmarkcurrent = currentbookmark
                ui.callhooks = True
            ctx = repo[node]

            # Participating changesets:
            #
            # node/ctx o - new (intermediate) commit that contains changes
            #          |   from working dir to go into amending commit
            #          |   (or a workingctx if there were no changes)
            #          |
            # old      o - changeset to amend
            #          |
            # base     o - parent of amending changeset

            # Update extra dict from amended commit (e.g. to preserve graft
            # source)
            extra.update(old.extra())

            # Also update it from the intermediate commit or from the wctx
            extra.update(ctx.extra())

            if len(old.parents()) > 1:
                # ctx.files() isn't reliable for merges, so fall back to the
                # slower repo.status() method
                files = set([fn for st in repo.status(base, old)[:3]
                             for fn in st])
            else:
                files = set(old.files())

            # Second, we use either the commit we just did, or if there were no
            # changes the parent of the working directory as the version of the
            # files in the final amend commit
            if node:
                ui.note(_('copying changeset %s to %s\n') % (ctx, base))

                user = ctx.user()
                date = ctx.date()
                # Recompute copies (avoid recording a -> b -> a)
                copied = copies.pathcopies(base, ctx)

                # Prune files which were reverted by the updates: if old
                # introduced file X and our intermediate commit, node,
                # renamed that file, then those two files are the same and
                # we can discard X from our list of files. Likewise if X
                # was deleted, it's no longer relevant
                files.update(ctx.files())

                def samefile(f):
                    if f in ctx.manifest():
                        a = ctx.filectx(f)
                        if f in base.manifest():
                            b = base.filectx(f)
                            return (not a.cmp(b)
                                    and a.flags() == b.flags())
                        else:
                            return False
                    else:
                        return f not in base.manifest()
                files = [f for f in files if not samefile(f)]

                def filectxfn(repo, ctx_, path):
                    try:
                        fctx = ctx[path]
                        flags = fctx.flags()
                        mctx = context.memfilectx(fctx.path(), fctx.data(),
                                                  islink='l' in flags,
                                                  isexec='x' in flags,
                                                  copied=copied.get(path))
                        return mctx
                    except KeyError:
                        raise IOError
            else:
                ui.note(_('copying changeset %s to %s\n') % (old, base))

                # Use version of files as in the old cset
                def filectxfn(repo, ctx_, path):
                    try:
                        return old.filectx(path)
                    except KeyError:
                        raise IOError

                user = opts.get('user') or old.user()
                date = opts.get('date') or old.date()
            editmsg = False
            if not message:
                editmsg = True
                message = old.description()
            elif opts.get('edit'):
                editmsg = True

            pureextra = extra.copy()
            extra['amend_source'] = old.hex()

            new = context.memctx(repo,
                                 parents=[base.node(), old.p2().node()],
                                 text=message,
                                 files=files,
                                 filectxfn=filectxfn,
                                 user=user,
                                 date=date,
                                 extra=extra)
            if editmsg:
                new._text = commitforceeditor(repo, new, [])
            repo.savecommitmessage(new.description())

            newdesc =  changelog.stripdesc(new.description())
            if ((not node)
                and newdesc == old.description()
                and user == old.user()
                and date == old.date()
                and pureextra == old.extra()):
                # nothing changed. continuing here would create a new node
                # anyway because of the amend_source noise.
                #
                # This not what we expect from amend.
                return old.node()

            ph = repo.ui.config('phases', 'new-commit', phases.draft)
            try:
                if opts.get('secret'):
                    commitphase = 'secret'
                else:
                    commitphase = old.phase()
                repo.ui.setconfig('phases', 'new-commit', commitphase, 'amend')
                newid = repo.commitctx(new)
            finally:
                repo.ui.setconfig('phases', 'new-commit', ph, 'amend')
            if newid != old.node():
                # Reroute the working copy parent to the new changeset
                repo.setparents(newid, nullid)

                # Move bookmarks from old parent to amend commit
                bms = repo.nodebookmarks(old.node())
                if bms:
                    marks = repo._bookmarks
                    for bm in bms:
                        marks[bm] = newid
                    marks.write()
            #commit the whole amend process
            if obsolete._enabled and newid != old.node():
                # mark the new changeset as successor of the rewritten one
                new = repo[newid]
                obs = [(old, (new,))]
                if node:
                    obs.append((ctx, ()))

                obsolete.createmarkers(repo, obs)
            tr.close()
        finally:
            tr.release()
        if (not obsolete._enabled) and newid != old.node():
            # Strip the intermediate commit (if there was one) and the amended
            # commit
            if node:
                ui.note(_('stripping intermediate changeset %s\n') % ctx)
            ui.note(_('stripping amended changeset %s\n') % old)
            repair.strip(ui, repo, old.node(), topic='amend-backup')
    finally:
        if newid is None:
            repo.dirstate.invalidate()
        lockmod.release(lock, wlock)
    return newid

def commiteditor(repo, ctx, subs):
    if ctx.description():
        return ctx.description()
    return commitforceeditor(repo, ctx, subs)

def commitforceeditor(repo, ctx, subs):
    edittext = []
    modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
    if ctx.description():
        edittext.append(ctx.description())
    edittext.append("")
    edittext.append("") # Empty line between message and comments.
    edittext.append(_("HG: Enter commit message."
                      "  Lines beginning with 'HG:' are removed."))
    edittext.append(_("HG: Leave message empty to abort commit."))
    edittext.append("HG: --")
    edittext.append(_("HG: user: %s") % ctx.user())
    if ctx.p2():
        edittext.append(_("HG: branch merge"))
    if ctx.branch():
        edittext.append(_("HG: branch '%s'") % ctx.branch())
    if bookmarks.iscurrent(repo):
        edittext.append(_("HG: bookmark '%s'") % repo._bookmarkcurrent)
    edittext.extend([_("HG: subrepo %s") % s for s in subs])
    edittext.extend([_("HG: added %s") % f for f in added])
    edittext.extend([_("HG: changed %s") % f for f in modified])
    edittext.extend([_("HG: removed %s") % f for f in removed])
    if not added and not modified and not removed:
        edittext.append(_("HG: no files changed"))
    edittext.append("")
    # run editor in the repository root
    olddir = os.getcwd()
    os.chdir(repo.root)
    text = repo.ui.edit("\n".join(edittext), ctx.user(), ctx.extra())
    text = re.sub("(?m)^HG:.*(\n|$)", "", text)
    os.chdir(olddir)

    if not text.strip():
        raise util.Abort(_("empty commit message"))

    return text

def commitstatus(repo, node, branch, bheads=None, opts={}):
    ctx = repo[node]
    parents = ctx.parents()

    if (not opts.get('amend') and bheads and node not in bheads and not
        [x for x in parents if x.node() in bheads and x.branch() == branch]):
        repo.ui.status(_('created new head\n'))
        # The message is not printed for initial roots. For the other
        # changesets, it is printed in the following situations:
        #
        # Par column: for the 2 parents with ...
        #   N: null or no parent
        #   B: parent is on another named branch
        #   C: parent is a regular non head changeset
        #   H: parent was a branch head of the current branch
        # Msg column: whether we print "created new head" message
        # In the following, it is assumed that there already exists some
        # initial branch heads of the current branch, otherwise nothing is
        # printed anyway.
        #
        # Par Msg Comment
        # N N  y  additional topo root
        #
        # B N  y  additional branch root
        # C N  y  additional topo head
        # H N  n  usual case
        #
        # B B  y  weird additional branch root
        # C B  y  branch merge
        # H B  n  merge with named branch
        #
        # C C  y  additional head from merge
        # C H  n  merge with a head
        #
        # H H  n  head merge: head count decreases

    if not opts.get('close_branch'):
        for r in parents:
            if r.closesbranch() and r.branch() == branch:
                repo.ui.status(_('reopening closed branch head %d\n') % r)

    if repo.ui.debugflag:
        repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
    elif repo.ui.verbose:
        repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))

def revert(ui, repo, ctx, parents, *pats, **opts):
    parent, p2 = parents
    node = ctx.node()

    mf = ctx.manifest()
    if node == parent:
        pmf = mf
    else:
        pmf = None

    # need all matching names in dirstate and manifest of target rev,
    # so have to walk both. do not print errors if files exist in one
    # but not other.

    names = {}

    wlock = repo.wlock()
    try:
        # walk dirstate.

        m = scmutil.match(repo[None], pats, opts)
        m.bad = lambda x, y: False
        for abs in repo.walk(m):
            names[abs] = m.rel(abs), m.exact(abs)

        # walk target manifest.

        def badfn(path, msg):
            if path in names:
                return
            if path in ctx.substate:
                return
            path_ = path + '/'
            for f in names:
                if f.startswith(path_):
                    return
            ui.warn("%s: %s\n" % (m.rel(path), msg))

        m = scmutil.match(ctx, pats, opts)
        m.bad = badfn
        for abs in ctx.walk(m):
            if abs not in names:
                names[abs] = m.rel(abs), m.exact(abs)

        # get the list of subrepos that must be reverted
        targetsubs = sorted(s for s in ctx.substate if m(s))
        m = scmutil.matchfiles(repo, names)
        changes = repo.status(match=m)[:4]
        modified, added, removed, deleted = map(set, changes)

        # if f is a rename, also revert the source
        cwd = repo.getcwd()
        for f in added:
            src = repo.dirstate.copied(f)
            if src and src not in names and repo.dirstate[src] == 'r':
                removed.add(src)
                names[src] = (repo.pathto(src, cwd), True)

        def removeforget(abs):
            if repo.dirstate[abs] == 'a':
                return _('forgetting %s\n')
            return _('removing %s\n')

        revert = ([], _('reverting %s\n'))
        add = ([], _('adding %s\n'))
        remove = ([], removeforget)
        undelete = ([], _('undeleting %s\n'))

        disptable = (
            # dispatch table:
            #   file state
            #   action if in target manifest
            #   action if not in target manifest
            #   make backup if in target manifest
            #   make backup if not in target manifest
            (modified, revert, remove, True, True),
            (added, revert, remove, True, False),
            (removed, undelete, None, True, False),
            (deleted, revert, remove, False, False),
            )

        for abs, (rel, exact) in sorted(names.items()):
            mfentry = mf.get(abs)
            target = repo.wjoin(abs)
            def handle(xlist, dobackup):
                xlist[0].append(abs)
                if (dobackup and not opts.get('no_backup') and
                    os.path.lexists(target) and
                    abs in ctx and repo[None][abs].cmp(ctx[abs])):
                    bakname = "%s.orig" % rel
                    ui.note(_('saving current version of %s as %s\n') %
                            (rel, bakname))
                    if not opts.get('dry_run'):
                        util.rename(target, bakname)
                if ui.verbose or not exact:
                    msg = xlist[1]
                    if not isinstance(msg, basestring):
                        msg = msg(abs)
                    ui.status(msg % rel)
            for table, hitlist, misslist, backuphit, backupmiss in disptable:
                if abs not in table:
                    continue
                # file has changed in dirstate
                if mfentry:
                    handle(hitlist, backuphit)
                elif misslist is not None:
                    handle(misslist, backupmiss)
                break
            else:
                if abs not in repo.dirstate:
                    if mfentry:
                        handle(add, True)
                    elif exact:
                        ui.warn(_('file not managed: %s\n') % rel)
                    continue
                # file has not changed in dirstate
                if node == parent:
                    if exact:
                        ui.warn(_('no changes needed to %s\n') % rel)
                    continue
                if pmf is None:
                    # only need parent manifest in this unlikely case,
                    # so do not read by default
                    pmf = repo[parent].manifest()
                if abs in pmf and mfentry:
                    # if version of file is same in parent and target
                    # manifests, do nothing
                    if (pmf[abs] != mfentry or
                        pmf.flags(abs) != mf.flags(abs)):
                        handle(revert, False)
                else:
                    handle(remove, False)
        if not opts.get('dry_run'):
            _performrevert(repo, parents, ctx, revert, add, remove, undelete)

            if targetsubs:
                # Revert the subrepos on the revert list
                for sub in targetsubs:
                    ctx.sub(sub).revert(ui, ctx.substate[sub], *pats, **opts)
    finally:
        wlock.release()

def _performrevert(repo, parents, ctx, revert, add, remove, undelete):
    """function that actually perform all the action computed for revert

    This is an independent function to let extension to plug in and react to
    the imminent revert.

    Make sure you have the working directory locked when calling this function.
    """
    parent, p2 = parents
    node = ctx.node()
    def checkout(f):
        fc = ctx[f]
        repo.wwrite(f, fc.data(), fc.flags())

    audit_path = pathutil.pathauditor(repo.root)
    for f in remove[0]:
        if repo.dirstate[f] == 'a':
            repo.dirstate.drop(f)
            continue
        audit_path(f)
        try:
            util.unlinkpath(repo.wjoin(f))
        except OSError:
            pass
        repo.dirstate.remove(f)

    normal = None
    if node == parent:
        # We're reverting to our parent. If possible, we'd like status
        # to report the file as clean. We have to use normallookup for
        # merges to avoid losing information about merged/dirty files.
        if p2 != nullid:
            normal = repo.dirstate.normallookup
        else:
            normal = repo.dirstate.normal
    for f in revert[0]:
        checkout(f)
        if normal:
            normal(f)

    for f in add[0]:
        checkout(f)
        repo.dirstate.add(f)

    normal = repo.dirstate.normallookup
    if node == parent and p2 == nullid:
        normal = repo.dirstate.normal
    for f in undelete[0]:
        checkout(f)
        normal(f)

    copied = copies.pathcopies(repo[parent], ctx)

    for f in add[0] + undelete[0] + revert[0]:
        if f in copied:
            repo.dirstate.copy(copied[f], f)

def command(table):
    '''returns a function object bound to table which can be used as
    a decorator for populating table as a command table'''

    def cmd(name, options=(), synopsis=None):
        def decorator(func):
            if synopsis:
                table[name] = func, list(options), synopsis
            else:
                table[name] = func, list(options)
            return func
        return decorator

    return cmd

# a list of (ui, repo, otherpeer, opts, missing) functions called by
# commands.outgoing.  "missing" is "missing" of the result of
# "findcommonoutgoing()"
outgoinghooks = util.hooks()

# a list of (ui, repo) functions called by commands.summary
summaryhooks = util.hooks()

# a list of (ui, repo, opts, changes) functions called by commands.summary.
#
# functions should return tuple of booleans below, if 'changes' is None:
#  (whether-incomings-are-needed, whether-outgoings-are-needed)
#
# otherwise, 'changes' is a tuple of tuples below:
#  - (sourceurl, sourcebranch, sourcepeer, incoming)
#  - (desturl,   destbranch,   destpeer,   outgoing)
summaryremotehooks = util.hooks()

# A list of state files kept by multistep operations like graft.
# Since graft cannot be aborted, it is considered 'clearable' by update.
# note: bisect is intentionally excluded
# (state file, clearable, allowcommit, error, hint)
unfinishedstates = [
    ('graftstate', True, False, _('graft in progress'),
     _("use 'hg graft --continue' or 'hg update' to abort")),
    ('updatestate', True, False, _('last update was interrupted'),
     _("use 'hg update' to get a consistent checkout"))
    ]

def checkunfinished(repo, commit=False):
    '''Look for an unfinished multistep operation, like graft, and abort
    if found. It's probably good to check this right before
    bailifchanged().
    '''
    for f, clearable, allowcommit, msg, hint in unfinishedstates:
        if commit and allowcommit:
            continue
        if repo.vfs.exists(f):
            raise util.Abort(msg, hint=hint)

def clearunfinished(repo):
    '''Check for unfinished operations (as above), and clear the ones
    that are clearable.
    '''
    for f, clearable, allowcommit, msg, hint in unfinishedstates:
        if not clearable and repo.vfs.exists(f):
            raise util.Abort(msg, hint=hint)
    for f, clearable, allowcommit, msg, hint in unfinishedstates:
        if clearable and repo.vfs.exists(f):
            util.unlink(repo.join(f))