Mercurial > hg > hg-git
view hggit/gitdirstate.py @ 1047:cf982a23e15c
gitdirstate: show pattern error in hgignore file as expected
Before this revision, invalid pattern in hgignore file causes
unintentional failure for UnboundLocalError of ignorefunc, if hggit is
used with Mercurial 3.5 or later.
In such case:
- checking source of invalid pattern at failure uses "pats" list for
hgignore files, but
- "pats" list is empty, if ignoremod is None (= Mercurial 3.5 or later)
- therefore, checking with matchmod.match() overlooks invalid pattern
Then, "return ignorefunc" is executed without assignment to
ignorefunc, and causes UnboundLocalError.
To show pattern error in hgignore file as expected even with Mercurial
3.5 or later, this revision puts '(FILE, ["include: FILE"])' tuples
into "pats" (to avoid code duplication, putting into allpats is
shared, too).
This makes checking source of invalid pattern at failure work as
expected for hgignore files.
Fixes #197
author | FUJIWARA Katsunori <foozy@lares.dti.ne.jp> |
---|---|
date | Sat, 05 Aug 2017 02:13:11 +0900 |
parents | e3dab807e38c |
children |
line wrap: on
line source
import os import stat import re import errno from mercurial import ( dirstate, match as matchmod, scmutil, util, ) try: from mercurial import ignore ignore.readpats ignoremod = True except (AttributeError, ImportError): # ignore module was removed in Mercurial 3.5 ignoremod = False # pathauditor moved to pathutil in 2.9 try: from mercurial import pathutil pathutil.pathauditor except (AttributeError, ImportError): pathutil = scmutil 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 = [] if ignoremod: pats = ignore.readpats(root, files, warn) else: 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 util.Abort: for f, patlist in pats: try: matchmod.match(root, '', [], patlist) except util.Abort, inst: if not ignoremod: # in this case, patlist is ['include: FILE'], and # inst[0] should already include FILE raise raise util.Abort('%s: %s' % (f, inst[0])) if extrapatterns: try: matchmod.match(root, '', [], extrapatterns) except util.Abort, inst: raise util.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: 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, 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()