Mercurial > hg-git
diff hggit/gitdirstate.py @ 615:503d403fc040
Fix for #68 | Use .gitignore files (with proper semantics)
author | Ben Kehoe <benk@berkeley.edu> |
---|---|
date | Wed, 27 Nov 2013 09:27:59 -0500 |
parents | |
children | 5998a6ddc704 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hggit/gitdirstate.py Wed Nov 27 09:27:59 2013 -0500 @@ -0,0 +1,238 @@ +import os, stat, re + +from mercurial import dirstate +from mercurial import hg +from mercurial import ignore +from mercurial import match as matchmod +from mercurial import osutil +from mercurial import scmutil +# pathauditor moved to pathutil in 2.8 +try: + from mercurial import pathutil + pathutil.pathauditor +except: + pathutil = scmutil +from mercurial import util +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 + 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(orig, root, files, warn, extrapatterns=None): + pats = ignore.readpats(root, files, warn) + allpats = [] + if extrapatterns: + allpats.extend(extrapatterns) + for f, patlist in pats: + allpats.extend(patlist) + 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: + 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) + 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 ignore.ignore(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 + full=True + + 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 + 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) + 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()