mercurial/narrowspec.py
author Augie Fackler <augie@google.com>
Thu, 09 Aug 2018 10:11:10 -0400
changeset 38963 467b5c1df72d
parent 38872 576eef1ab43d
child 39525 2675d561f5cb
permissions -rw-r--r--
tests: make all the string constants in test-match.py be bytes Done with python3 contrib/byteify-strings.py tests/test-match.py -i # skip-blame just bytes prefixes Differential Revision: https://phab.mercurial-scm.org/D4171

# narrowspec.py - methods for working with a narrow view of a repository
#
# Copyright 2017 Google, Inc.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.

from __future__ import absolute_import

import errno

from .i18n import _
from . import (
    error,
    match as matchmod,
    repository,
    sparse,
    util,
)

FILENAME = 'narrowspec'

def parseserverpatterns(text):
    """Parses the narrowspec format that's returned by the server."""
    includepats = set()
    excludepats = set()

    # We get one entry per line, in the format "<key> <value>".
    # It's OK for value to contain other spaces.
    for kp in (l.split(' ', 1) for l in text.splitlines()):
        if len(kp) != 2:
            raise error.Abort(_('Invalid narrowspec pattern line: "%s"') % kp)
        key = kp[0]
        pat = kp[1]
        if key == 'include':
            includepats.add(pat)
        elif key == 'exclude':
            excludepats.add(pat)
        else:
            raise error.Abort(_('Invalid key "%s" in server response') % key)

    return includepats, excludepats

def normalizesplitpattern(kind, pat):
    """Returns the normalized version of a pattern and kind.

    Returns a tuple with the normalized kind and normalized pattern.
    """
    pat = pat.rstrip('/')
    _validatepattern(pat)
    return kind, pat

def _numlines(s):
    """Returns the number of lines in s, including ending empty lines."""
    # We use splitlines because it is Unicode-friendly and thus Python 3
    # compatible. However, it does not count empty lines at the end, so trick
    # it by adding a character at the end.
    return len((s + 'x').splitlines())

def _validatepattern(pat):
    """Validates the pattern and aborts if it is invalid.

    Patterns are stored in the narrowspec as newline-separated
    POSIX-style bytestring paths. There's no escaping.
    """

    # We use newlines as separators in the narrowspec file, so don't allow them
    # in patterns.
    if _numlines(pat) > 1:
        raise error.Abort(_('newlines are not allowed in narrowspec paths'))

    components = pat.split('/')
    if '.' in components or '..' in components:
        raise error.Abort(_('"." and ".." are not allowed in narrowspec paths'))

def normalizepattern(pattern, defaultkind='path'):
    """Returns the normalized version of a text-format pattern.

    If the pattern has no kind, the default will be added.
    """
    kind, pat = matchmod._patsplit(pattern, defaultkind)
    return '%s:%s' % normalizesplitpattern(kind, pat)

def parsepatterns(pats):
    """Parses a list of patterns into a typed pattern set."""
    return set(normalizepattern(p) for p in pats)

def format(includes, excludes):
    output = '[include]\n'
    for i in sorted(includes - excludes):
        output += i + '\n'
    output += '[exclude]\n'
    for e in sorted(excludes):
        output += e + '\n'
    return output

def match(root, include=None, exclude=None):
    if not include:
        # Passing empty include and empty exclude to matchmod.match()
        # gives a matcher that matches everything, so explicitly use
        # the nevermatcher.
        return matchmod.never(root, '')
    return matchmod.match(root, '', [], include=include or [],
                          exclude=exclude or [])

def needsexpansion(includes):
    return [i for i in includes if i.startswith('include:')]

def load(repo):
    try:
        spec = repo.svfs.read(FILENAME)
    except IOError as e:
        # Treat "narrowspec does not exist" the same as "narrowspec file exists
        # and is empty".
        if e.errno == errno.ENOENT:
            return set(), set()
        raise
    # maybe we should care about the profiles returned too
    includepats, excludepats, profiles = sparse.parseconfig(repo.ui, spec,
                                                            'narrow')
    if profiles:
        raise error.Abort(_("including other spec files using '%include' is not"
                            " suported in narrowspec"))
    return includepats, excludepats

def save(repo, includepats, excludepats):
    spec = format(includepats, excludepats)
    repo.svfs.write(FILENAME, spec)

def savebackup(repo, backupname):
    if repository.NARROW_REQUIREMENT not in repo.requirements:
        return
    vfs = repo.vfs
    vfs.tryunlink(backupname)
    util.copyfile(repo.svfs.join(FILENAME), vfs.join(backupname), hardlink=True)

def restorebackup(repo, backupname):
    if repository.NARROW_REQUIREMENT not in repo.requirements:
        return
    util.rename(repo.vfs.join(backupname), repo.svfs.join(FILENAME))

def clearbackup(repo, backupname):
    if repository.NARROW_REQUIREMENT not in repo.requirements:
        return
    repo.vfs.unlink(backupname)

def restrictpatterns(req_includes, req_excludes, repo_includes, repo_excludes):
    r""" Restricts the patterns according to repo settings,
    results in a logical AND operation

    :param req_includes: requested includes
    :param req_excludes: requested excludes
    :param repo_includes: repo includes
    :param repo_excludes: repo excludes
    :return: include patterns, exclude patterns, and invalid include patterns.

    >>> restrictpatterns({'f1','f2'}, {}, ['f1'], [])
    (set(['f1']), {}, [])
    >>> restrictpatterns({'f1'}, {}, ['f1','f2'], [])
    (set(['f1']), {}, [])
    >>> restrictpatterns({'f1/fc1', 'f3/fc3'}, {}, ['f1','f2'], [])
    (set(['f1/fc1']), {}, [])
    >>> restrictpatterns({'f1_fc1'}, {}, ['f1','f2'], [])
    ([], set(['path:.']), [])
    >>> restrictpatterns({'f1/../f2/fc2'}, {}, ['f1','f2'], [])
    (set(['f2/fc2']), {}, [])
    >>> restrictpatterns({'f1/../f3/fc3'}, {}, ['f1','f2'], [])
    ([], set(['path:.']), [])
    >>> restrictpatterns({'f1/$non_exitent_var'}, {}, ['f1','f2'], [])
    (set(['f1/$non_exitent_var']), {}, [])
    """
    res_excludes = set(req_excludes)
    res_excludes.update(repo_excludes)
    invalid_includes = []
    if not req_includes:
        res_includes = set(repo_includes)
    elif 'path:.' not in repo_includes:
        res_includes = []
        for req_include in req_includes:
            req_include = util.expandpath(util.normpath(req_include))
            if req_include in repo_includes:
                res_includes.append(req_include)
                continue
            valid = False
            for repo_include in repo_includes:
                if req_include.startswith(repo_include + '/'):
                    valid = True
                    res_includes.append(req_include)
                    break
            if not valid:
                invalid_includes.append(req_include)
        if len(res_includes) == 0:
            res_excludes = {'path:.'}
        else:
            res_includes = set(res_includes)
    else:
        res_includes = set(req_includes)
    return res_includes, res_excludes, invalid_includes