view mercurial/cmdutil.py @ 3292:764688cf51e5

merge: remember rename copies and parents properly on commit record copies in dirstate even if rename was remote this lets us record it properly at commit teach checkfilemerge about copies, including merge cases pull old copy code out of commit extend rename-merge1 test to show file index
author Matt Mackall <mpm@selenic.com>
date Sun, 08 Oct 2006 19:57:45 -0500
parents eeaf9bcdfa25
children a33a9af1ec03
line wrap: on
line source

# cmdutil.py - help for command processing in mercurial
#
# Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
#
# This software may be used and distributed according to the terms
# of the GNU General Public License, incorporated herein by reference.

from demandload import demandload
from node import *
from i18n import gettext as _
demandload(globals(), 'mdiff util')
demandload(globals(), 'os sys')

revrangesep = ':'

def revfix(repo, val, defval):
    '''turn user-level id of changeset into rev number.
    user-level id can be tag, changeset, rev number, or negative rev
    number relative to number of revs (-1 is tip, etc).'''
    if not val:
        return defval
    try:
        num = int(val)
        if str(num) != val:
            raise ValueError
        if num < 0:
            num += repo.changelog.count()
        if num < 0:
            num = 0
        elif num >= repo.changelog.count():
            raise ValueError
    except ValueError:
        try:
            num = repo.changelog.rev(repo.lookup(val))
        except KeyError:
            raise util.Abort(_('invalid revision identifier %s') % val)
    return num

def revpair(ui, repo, revs):
    '''return pair of nodes, given list of revisions. second item can
    be None, meaning use working dir.'''
    if not revs:
        return repo.dirstate.parents()[0], None
    end = None
    if len(revs) == 1:
        start = revs[0]
        if revrangesep in start:
            start, end = start.split(revrangesep, 1)
            start = revfix(repo, start, 0)
            end = revfix(repo, end, repo.changelog.count() - 1)
        else:
            start = revfix(repo, start, None)
    elif len(revs) == 2:
        if revrangesep in revs[0] or revrangesep in revs[1]:
            raise util.Abort(_('too many revisions specified'))
        start = revfix(repo, revs[0], None)
        end = revfix(repo, revs[1], None)
    else:
        raise util.Abort(_('too many revisions specified'))
    if end is not None: end = repo.lookup(str(end))
    return repo.lookup(str(start)), end

def revrange(ui, repo, revs):
    """Yield revision as strings from a list of revision specifications."""
    seen = {}
    for spec in revs:
        if revrangesep in spec:
            start, end = spec.split(revrangesep, 1)
            start = revfix(repo, start, 0)
            end = revfix(repo, end, repo.changelog.count() - 1)
            step = start > end and -1 or 1
            for rev in xrange(start, end+step, step):
                if rev in seen:
                    continue
                seen[rev] = 1
                yield str(rev)
        else:
            rev = revfix(repo, spec, None)
            if rev in seen:
                continue
            seen[rev] = 1
            yield str(rev)

def make_filename(repo, pat, node,
                  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),
        }
    expander = {
        '%': lambda: '%',
        'b': lambda: os.path.basename(repo.root),
        }

    try:
        if node:
            expander.update(node_expander)
        if node and revwidth is not None:
            expander['r'] = (lambda:
                    str(repo.changelog.rev(node)).zfill(revwidth))
        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 file name") %
                         inst.args[0])

def make_file(repo, pat, node=None,
              total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
    if not pat or pat == '-':
        return 'w' in mode and sys.stdout or sys.stdin
    if hasattr(pat, 'write') and 'w' in mode:
        return pat
    if hasattr(pat, 'read') and 'r' in mode:
        return pat
    return open(make_filename(repo, pat, node, total, seqno, revwidth,
                              pathname),
                mode)

def matchpats(repo, pats=[], opts={}, head=''):
    cwd = repo.getcwd()
    if not pats and cwd:
        opts['include'] = [os.path.join(cwd, i)
                           for i in opts.get('include', [])]
        opts['exclude'] = [os.path.join(cwd, x)
                           for x in opts.get('exclude', [])]
        cwd = ''
    return util.cmdmatcher(repo.root, cwd, pats or ['.'], opts.get('include'),
                           opts.get('exclude'), head)

def makewalk(repo, pats=[], opts={}, node=None, head='', badmatch=None):
    files, matchfn, anypats = matchpats(repo, pats, opts, head)
    exact = dict(zip(files, files))
    def walk():
        for src, fn in repo.walk(node=node, files=files, match=matchfn,
                                 badmatch=badmatch):
            yield src, fn, util.pathto(repo.getcwd(), fn), fn in exact
    return files, matchfn, walk()

def walk(repo, pats=[], opts={}, node=None, head='', badmatch=None):
    files, matchfn, results = makewalk(repo, pats, opts, node, head, badmatch)
    for r in results:
        yield r

def findrenames(repo, added=None, removed=None, threshold=0.5):
    if added is None or removed is None:
        added, removed = repo.status()[1:3]
    changes = repo.changelog.read(repo.dirstate.parents()[0])
    mf = repo.manifest.read(changes[0])
    for a in added:
        aa = repo.wread(a)
        bestscore, bestname = None, None
        for r in removed:
            rr = repo.file(r).read(mf[r])
            delta = mdiff.textdiff(aa, rr)
            if len(delta) < len(aa):
                myscore = 1.0 - (float(len(delta)) / len(aa))
                if bestscore is None or myscore > bestscore:
                    bestscore, bestname = myscore, r
        if bestname and bestscore >= threshold:
            yield bestname, a, bestscore

def addremove(repo, pats=[], opts={}, wlock=None, dry_run=None,
              similarity=None):
    if dry_run is None:
        dry_run = opts.get('dry_run')
    if similarity is None:
        similarity = float(opts.get('similarity') or 0)
    add, remove = [], []
    mapping = {}
    for src, abs, rel, exact in walk(repo, pats, opts):
        if src == 'f' and repo.dirstate.state(abs) == '?':
            add.append(abs)
            mapping[abs] = rel, exact
            if repo.ui.verbose or not exact:
                repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
        if repo.dirstate.state(abs) != 'r' and not os.path.exists(rel):
            remove.append(abs)
            mapping[abs] = rel, exact
            if repo.ui.verbose or not exact:
                repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
    if not dry_run:
        repo.add(add, wlock=wlock)
        repo.remove(remove, wlock=wlock)
    if similarity > 0:
        for old, new, score in findrenames(repo, add, remove, similarity):
            oldrel, oldexact = mapping[old]
            newrel, newexact = mapping[new]
            if repo.ui.verbose or not oldexact or not newexact:
                repo.ui.status(_('recording removal of %s as rename to %s '
                                 '(%d%% similar)\n') %
                               (oldrel, newrel, score * 100))
            if not dry_run:
                repo.copy(old, new, wlock=wlock)