# HG changeset patch # User Ben Kehoe # Date 1385562479 18000 # Node ID 503d403fc0404b8cb2df568da6fcb14cb2879b15 # Parent 5f00e72ac3dfce6d9e627c11392b6441ee48b858 Fix for #68 | Use .gitignore files (with proper semantics) diff --git a/hggit/__init__.py b/hggit/__init__.py --- a/hggit/__init__.py +++ b/hggit/__init__.py @@ -24,10 +24,12 @@ from mercurial import bundlerepo from mercurial import commands from mercurial import demandimport +from mercurial import dirstate from mercurial import discovery from mercurial import extensions from mercurial import help from mercurial import hg +from mercurial import ignore from mercurial import localrepo from mercurial import revset from mercurial import templatekw @@ -39,7 +41,7 @@ 'collections', ]) -import gitrepo, hgrepo +import gitrepo, hgrepo, gitdirstate from git_handler import GitHandler testedwith = '1.9.3 2.0.2 2.1.2 2.2.3 2.3.1' @@ -128,6 +130,9 @@ repo.ui.status(_("clearing out the git cache data\n")) git = GitHandler(repo, ui) git.clear() + +extensions.wrapfunction(ignore, 'ignore', gitdirstate.gignore) +dirstate.dirstate = gitdirstate.gitdirstate def git_cleanup(ui, repo): new_map = [] diff --git a/hggit/gitdirstate.py b/hggit/gitdirstate.py new file mode 100644 --- /dev/null +++ b/hggit/gitdirstate.py @@ -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() diff --git a/tests/test-gitignore.t b/tests/test-gitignore.t new file mode 100644 --- /dev/null +++ b/tests/test-gitignore.t @@ -0,0 +1,87 @@ +Load commonly used test logic + $ . "$TESTDIR/testutil" + + $ hg init + + $ touch foo + $ touch foobar + $ touch bar + $ echo 'foo*' > .gitignore + $ hg status + ? .gitignore + ? bar + + $ echo '*bar' > .gitignore + $ hg status + ? .gitignore + ? foo + + $ mkdir dir + $ touch dir/foo + $ echo 'foo' > .gitignore + $ hg status + ? .gitignore + ? bar + ? foobar + + $ echo '/foo' > .gitignore + $ hg status + ? .gitignore + ? bar + ? dir/foo + ? foobar + + $ rm .gitignore + $ echo 'foo' > dir/.gitignore + $ hg status + ? bar + ? dir/.gitignore + ? foo + ? foobar + + $ touch dir/bar + $ echo 'bar' > .gitignore + $ hg status + ? .gitignore + ? dir/.gitignore + ? foo + ? foobar + + $ echo '/bar' > .gitignore + $ hg status + ? .gitignore + ? dir/.gitignore + ? dir/bar + ? foo + ? foobar + + $ echo 'foo*' > .gitignore + $ echo '!*bar' >> .gitignore + $ hg status + .gitignore: unsupported ignore pattern '!*bar' + ? .gitignore + ? bar + ? dir/.gitignore + ? dir/bar + + $ touch .hgignore + $ hg status + ? .gitignore + ? .hgignore + ? bar + ? dir/.gitignore + ? dir/bar + ? dir/foo + ? foo + ? foobar + + $ echo 'syntax: re' > .hgignore + $ echo 'foo.*$(?> .hgignore + $ echo 'dir/foo' >> .hgignore + $ hg status + ? .gitignore + ? .hgignore + ? bar + ? dir/.gitignore + ? dir/bar + ? foobar