1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
|
import os
import stat
import re
import errno
from mercurial import (
dirstate,
error,
exthelper,
match as matchmod,
pathutil,
pycompat,
util,
)
from mercurial.i18n import _
from . import git_handler
from . import gitrepo
eh = exthelper.exthelper()
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 = {b're': b'relre:', b'regexp': b'relre:', b'glob': b'relglob:'}
syntax = b'glob:'
patterns = []
warnings = []
for line in lines:
if b"#" in line:
_commentre = re.compile(br'((^|[^\\])(\\\\)*)#.*')
# remove comments prefixed by an even number of escapes
line = _commentre.sub(br'\1', line)
# fixup properly escaped comments that survived the above
line = line.replace(b"\\#", b"#")
line = line.rstrip()
if not line:
continue
if line.startswith(b'!'):
warnings.append(_(b"unsupported ignore pattern '%s'") % line)
continue
if re.match(br'(:?.*/)?\.hg(:?/|$)', line):
continue
rootprefix = b'%s/' % root if root else b''
if line and line[0] in br'\/':
line = line[1:]
rootsuffixes = [b'']
else:
rootsuffixes = [b'', b'**/']
for rootsuffix in rootsuffixes:
pat = syntax + rootprefix + rootsuffix + line
for s, rels in syntaxes.items():
if line.startswith(rels):
pat = line
break
elif line.startswith(s + b':'):
pat = rels + line[len(s) + 1 :]
break
patterns.append(pat)
return patterns, warnings
def gignore(root, files, ui, extrapatterns=None):
allpats = []
pats = [(f, [b'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, b'', [], allpats)
except error.Abort:
ui.traceback()
for f, patlist in pats:
matchmod.match(root, b'', [], patlist)
if extrapatterns:
matchmod.match(root, b'', [], extrapatterns)
return ignorefunc
class gitdirstate(dirstate.dirstate):
@dirstate.rootcache(b'.hgignore')
def _ignore(self):
files = [self._join(b'.hgignore')]
for name, path in self._ui.configitems(b"ui"):
if name == b'ignore' or name.startswith(b'ignore.'):
files.append(util.expandpath(path))
patterns = []
# Only use .gitignore if there's no .hgignore
if not os.access(files[0], os.R_OK):
for fn in self._finddotgitignores():
d = os.path.dirname(fn)
fn = self.pathto(fn)
if not os.path.exists(fn):
continue
fp = open(fn, 'rb')
pats, warnings = gignorepats(None, fp, root=d)
for warning in warnings:
self._ui.warn(b"%s: %s\n" % (fn, warning))
patterns.extend(pats)
return gignore(self._root, files, self._ui, 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(), [b'relglob:.gitignore']
)
# TODO: need subrepos?
subrepos = []
unknown = True
ignored = False
def fwarn(f, msg):
self._ui.warn(
b'%s: %s\n'
% (
self.pathto(f),
pycompat.sysbytes(msg),
)
)
return False
ignore = super()._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
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)
work = [nd for nd, d in work if not dirignore(d)]
wadd = work.append
# step 2: visit subdirectories
while work:
nd = work.pop()
skip = None
if nd != b'':
skip = b'.hg'
try:
entries = util.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 + b"/" + f) or f, True, True)
else:
nf = nd and (nd + b"/" + 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[b'.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 = next(iter(visit))
for st in util.statfiles([join(i) for i in visit]):
results[nf()] = st
return results.keys()
def _rust_status(self, *args, **kwargs):
# intercept a rust status call and force the fallback,
# otherwise our patching won't work
if not os.path.lexists(self._join(b'.hgignore')):
self._ui.debug(b'suppressing rust status to intercept gitignores\n')
raise dirstate.rustmod.FallbackError
else:
return super()._rust_status(*args, **kwargs)
@eh.reposetup
def reposetup(ui, repo):
if isinstance(repo, gitrepo.gitrepo):
return
if git_handler.has_gitrepo(repo):
dirstate.dirstate = gitdirstate
|