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()