view mercurial/ignore.py @ 16696:d1afbf03e69a

rebase: allow collapsing branches in place (issue3111) We allow rebase plus collapse, but not collapse only? I imagine people would rebase first then collapse once they are sure the rebase is correct and it is the right time to finish it. I was reluctant to submit this patch for reasons detailed below, but it improves rebase --collapse usefulness so much it is worth the ugliness. The fix is ugly because we should be fixing the collapse code path rather than the merge. Collapsing by merging changesets repeatedly is inefficient compared to what commit --amend does: commitctx(), update, strip. The problem with the latter is, to generate the synthetic changeset, copy records are gathered with copies.pathcopies(). copies.pathcopies() is still implemented with merging in mind and discards information like file replaced by the copy of another, criss-cross copies and so forth. I believe this information should not be lost, even if we decide not to interpret it fully later, at merge time. The second issue with improving rebase --collapse is the option should not be there to begin with. Rebasing and collapsing are orthogonal and a dedicated command would probably enable a better, simpler ui. We should avoid advertizing rebase --collapse, but with this fix it becomes the best shipped solution to collapse changesets. And for the record, available techniques are: - revert + commit + strip: lose copies - mq/qfold: repeated patching() (mostly correct, fragile) - rebase: repeated merges (mostly correct, fragile) - collapse: revert + tag rewriting wizardry, lose copies - histedit: repeated patching() (mostly correct, fragile) - amend: copies.pathcopies() + commitctx() + update + strip
author Patrick Mezard <patrick@mezard.eu>
date Thu, 03 May 2012 15:14:58 +0200
parents ee112eb69d2a
children b35e3364f94a
line wrap: on
line source

# ignore.py - ignored file handling for mercurial
#
# Copyright 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 i18n import _
import util, match
import re

_commentre = None

def ignorepats(lines):
    '''parse lines (iterable) of .hgignore text, returning a tuple of
    (patterns, parse errors). These patterns should be given to compile()
    to be validated and converted into a match function.'''
    syntaxes = {'re': 'relre:', 'regexp': 'relre:', 'glob': 'relglob:'}
    syntax = 'relre:'
    patterns = []
    warnings = []

    for line in lines:
        if "#" in line:
            global _commentre
            if not _commentre:
                _commentre = re.compile(r'((^|[^\\])(\\\\)*)#.*')
            # remove comments prefixed by an even number of escapes
            line = _commentre.sub(r'\1', line)
            # fixup properly escaped comments that survived the above
            line = line.replace("\\#", "#")
        line = line.rstrip()
        if not line:
            continue

        if line.startswith('syntax:'):
            s = line[7:].strip()
            try:
                syntax = syntaxes[s]
            except KeyError:
                warnings.append(_("ignoring invalid syntax '%s'") % s)
            continue
        pat = syntax + line
        for s, rels in syntaxes.iteritems():
            if line.startswith(rels):
                pat = line
                break
            elif line.startswith(s+':'):
                pat = rels + line[len(s)+1:]
                break
        patterns.append(pat)

    return patterns, warnings

def ignore(root, files, warn):
    '''return matcher covering patterns in 'files'.

    the files parsed for patterns include:
    .hgignore in the repository root
    any additional files specified in the [ui] section of ~/.hgrc

    trailing white space is dropped.
    the escape character is backslash.
    comments start with #.
    empty lines are skipped.

    lines can be of the following formats:

    syntax: regexp # defaults following lines to non-rooted regexps
    syntax: glob   # defaults following lines to non-rooted globs
    re:pattern     # non-rooted regular expression
    glob:pattern   # non-rooted glob
    pattern        # pattern of the current default type'''

    pats = {}
    for f in files:
        try:
            pats[f] = []
            fp = open(f)
            pats[f], warnings = ignorepats(fp)
            fp.close()
            for warning in warnings:
                warn("%s: %s\n" % (f, warning))
        except IOError, inst:
            if f != files[0]:
                warn(_("skipping unreadable ignore file '%s': %s\n") %
                     (f, inst.strerror))

    allpats = []
    for patlist in pats.values():
        allpats.extend(patlist)
    if not allpats:
        return util.never

    try:
        ignorefunc = match.match(root, '', [], allpats)
    except util.Abort:
        # Re-raise an exception where the src is the right file
        for f, patlist in pats.iteritems():
            try:
                match.match(root, '', [], patlist)
            except util.Abort, inst:
                raise util.Abort('%s: %s' % (f, inst[0]))

    return ignorefunc