sapling/hgext/hggit/gitdirstate.py
Ryan McElroy 66bd74e558 hggit: internalize extension
Test Plan: run-tests-.py

Reviewers: mitrandir, #mercurial

Reviewed By: mitrandir

Subscribers: ps, terrelln

Differential Revision: https://phabricator.intern.facebook.com/D6675896

Tasks: T24908724

Signature: 6675896:1515448382:df8d80cd7356ae8f5fb04586dc4a0a651bc498fd
2018-01-09 06:08:01 -08:00

261 lines
9.2 KiB
Python

import os
import stat
import re
import errno
from mercurial import (
dirstate,
error,
match as matchmod,
util,
)
# ignore module was removed in Mercurial 3.5
ignoremod = False
# pathauditor moved to pathutil in 2.9
from mercurial import pathutil
from mercurial.i18n import _
def gignorepats(orig, lines, root=None):
'''parse lines (iterable) of .gitignore 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 = 'glob:'
patterns = []
warnings = []
for line in lines:
if "#" in line:
_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('!'):
warnings.append(_("unsupported ignore pattern '%s'") % line)
continue
if re.match(r'(:?.*/)?\.hg(:?/|$)', line):
continue
rootprefix = '%s/' % root if root else ''
if line.startswith('/'):
line = line[1:]
rootsuffixes = ['']
else:
rootsuffixes = ['', '**/']
for rootsuffix in rootsuffixes:
pat = syntax + rootprefix + rootsuffix + 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 gignore(root, files, warn, extrapatterns=None):
allpats = []
pats = []
pats = [(f, ['include:%s' % f]) for f in files]
for f, patlist in pats:
allpats.extend(patlist)
if extrapatterns:
allpats.extend(extrapatterns)
if not allpats:
return util.never
try:
ignorefunc = matchmod.match(root, '', [], allpats)
except error.Abort:
for f, patlist in pats:
try:
matchmod.match(root, '', [], patlist)
except error.Abort as inst:
if not ignoremod:
# in this case, patlist is ['include: FILE'], and
# inst[0] should already include FILE
raise
raise error.Abort('%s: %s' % (f, inst[0]))
if extrapatterns:
try:
matchmod.match(root, '', [], extrapatterns)
except error.Abort as inst:
raise error.Abort('%s: %s' % ('extra patterns', inst[0]))
return ignorefunc
class gitdirstate(dirstate.dirstate):
@dirstate.rootcache('.hgignore')
def _ignore(self):
files = [self._join('.hgignore')]
for name, path in self._ui.configitems("ui"):
if name == 'ignore' or name.startswith('ignore.'):
files.append(util.expandpath(path))
patterns = []
# Only use .gitignore if there's no .hgignore
try:
fp = open(files[0])
fp.close()
except Exception:
fns = self._finddotgitignores()
for fn in fns:
d = os.path.dirname(fn)
fn = self.pathto(fn)
if not os.path.exists(fn):
continue
fp = open(fn)
pats, warnings = gignorepats(None, fp, root=d)
for warning in warnings:
self._ui.warn("%s: %s\n" % (fn, warning))
patterns.extend(pats)
return gignore(self._root, files, self._ui.warn,
extrapatterns=patterns)
def _finddotgitignores(self):
"""A copy of dirstate.walk. This is called from the new _ignore method,
which is called by dirstate.walk, which would cause infinite recursion,
except _finddotgitignores calls the superclass _ignore directly."""
match = matchmod.match(self._root, self.getcwd(),
['relglob:.gitignore'])
# TODO: need subrepos?
subrepos = []
unknown = True
ignored = False
def fwarn(f, msg):
self._ui.warn('%s: %s\n' % (self.pathto(f), msg))
return False
ignore = super(gitdirstate, self)._ignore
dirignore = self._dirignore
if ignored:
ignore = util.never
dirignore = util.never
elif not unknown:
# if unknown and ignored are False, skip step 2
ignore = util.always
dirignore = util.always
matchfn = match.matchfn
matchalways = match.always()
matchtdir = match.traversedir
dmap = self._map
# osutil moved in hg 4.3, but util re-exports listdir
try:
listdir = util.listdir
except AttributeError:
from mercurial import osutil
listdir = osutil.listdir
lstat = os.lstat
dirkind = stat.S_IFDIR
regkind = stat.S_IFREG
lnkkind = stat.S_IFLNK
join = self._join
exact = skipstep3 = False
if matchfn == match.exact: # match.exact
exact = True
dirignore = util.always # skip step 2
elif match.files() and not match.anypats(): # match.match, no patterns
skipstep3 = True
if not exact and self._checkcase:
normalize = self._normalize
skipstep3 = False
else:
normalize = None
# step 1: find all explicit files
results, work, dirsnotfound = self._walkexplicit(match, subrepos)
skipstep3 = skipstep3 and not (work or dirsnotfound)
if work and isinstance(work[0], tuple):
# Mercurial >= 3.3.3
work = [nd for nd, d in work if not dirignore(d)]
else:
work = [d for d in work if not dirignore(d)]
wadd = work.append
# step 2: visit subdirectories
while work:
nd = work.pop()
skip = None
if nd == '.':
nd = ''
else:
skip = '.hg'
try:
entries = listdir(join(nd), stat=True, skip=skip)
except OSError as inst:
if inst.errno in (errno.EACCES, errno.ENOENT):
fwarn(nd, inst.strerror)
continue
raise
for f, kind, st in entries:
if normalize:
nf = normalize(nd and (nd + "/" + f) or f, True, True)
else:
nf = nd and (nd + "/" + f) or f
if nf not in results:
if kind == dirkind:
if not ignore(nf):
if matchtdir:
matchtdir(nf)
wadd(nf)
if nf in dmap and (matchalways or matchfn(nf)):
results[nf] = None
elif kind == regkind or kind == lnkkind:
if nf in dmap:
if matchalways or matchfn(nf):
results[nf] = st
elif (matchalways or matchfn(nf)) and not ignore(nf):
results[nf] = st
elif nf in dmap and (matchalways or matchfn(nf)):
results[nf] = None
for s in subrepos:
del results[s]
del results['.hg']
# step 3: report unseen items in the dmap hash
if not skipstep3 and not exact:
if not results and matchalways:
visit = dmap.keys()
else:
visit = [f for f in dmap if f not in results and matchfn(f)]
visit.sort()
if unknown:
# unknown == True means we walked the full directory tree
# above. So if a file is not seen it was either a) not matching
# matchfn b) ignored, c) missing, or d) under a symlink
# directory.
audit_path = pathutil.pathauditor(self._root)
for nf in iter(visit):
# Report ignored items in the dmap as long as they are not
# under a symlink directory.
if audit_path.check(nf):
try:
results[nf] = lstat(join(nf))
except OSError:
# file doesn't exist
results[nf] = None
else:
# It's either missing or under a symlink directory
results[nf] = None
else:
# We may not have walked the full directory tree above,
# so stat everything we missed.
nf = iter(visit).next
for st in util.statfiles([join(i) for i in visit]):
results[nf()] = st
return results.keys()