46cb98ff7fe0 v0.2.0

Add .hgignore support and ffignore syntax: glob.
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Mon, 01 Oct 2012 13:41:38 -0400
parents 48df53581f1c
children d61398ed376f
branches/tags v0.2.0
files README.markdown ffind

Changes

--- a/README.markdown	Thu Sep 27 13:33:26 2012 -0400
+++ b/README.markdown	Mon Oct 01 13:41:38 2012 -0400
@@ -10,8 +10,7 @@
 notable exceptions:
 
 * Time filtering is unimplemented.
-* VCS ignore files aren't parsed (however: VCS data directories *are* skipped,
-  and the `.ffignore` file *is* parsed).
+* SVN ignores aren't parsed.
 * It's pretty slow (though pruning VCS data directories saves lots of time).
 
 Feedback is welcome, though remember that it's still a prototype, and is
--- a/ffind	Thu Sep 27 13:33:26 2012 -0400
+++ b/ffind	Mon Oct 01 13:41:38 2012 -0400
@@ -93,6 +93,11 @@
 GITIGNORE_BLANK_RE = re.compile(r'^\s*$')
 GITIGNORE_NEGATE_RE = re.compile(r'^\s*!')
 
+HGIGNORE_SYNTAX_RE = re.compile(r'^\s*syntax:\s*(glob|regexp|re)\s*$',
+                                re.IGNORECASE)
+HGIGNORE_COMMENT_RE = re.compile(r'^\s*#')
+HGIGNORE_BLANK_RE = re.compile(r'^\s*$')
+
 
 # Global Options --------------------------------------------------------------
 # (it's a prototype, shut up)
@@ -123,9 +128,33 @@
         warn('could not compile regular expression "%s"' % line)
         return lambda s: False
 
-def compile_glob(line):
-    # TODO
-    die('glob ignore patterns are not supported yet, sorry!')
+def glob_to_re(glob):
+    pat = ''
+
+    chs = list(glob)
+    while chs:
+        ch = chs.pop(0)
+        if ch == '\\':
+            pat += re.escape(chs.pop(0))
+        elif ch == '?':
+            pat += '.'
+        elif ch == '*':
+            if chs and chs[0] == '*':
+                chs.pop(0)
+                pat += '.*'
+            else:
+                pat += '[^/]*'
+        elif ch == '[':
+            pat += '['
+            ch = chs.pop(0)
+            while chs and ch != ']':
+                pat += ch
+                ch = chs.pop(0)
+            pat += ']'
+        else:
+            pat += re.escape(ch)
+
+    return pat
 
 def compile_literal(line):
     l = line
@@ -227,12 +256,37 @@
     pat += '$'
 
     try:
-        regex = re.compile(pat)
-        return lambda s: regex.search(s)
+        return compile_re(pat)
     except:
         warn("could not parse gitignore pattern '%s'" % original_line)
         return lambda s: True
 
+def compile_hg_glob(line):
+    pat = glob_to_re(line)
+
+    # Mercurial ignore globs are quasi-rooted at directory boundaries or the
+    # beginning of the pattern.
+    pat = '(^|/)' + pat
+
+    # Mercurial globs also have to match to the end of the pattern.
+    pat = pat + '$'
+
+    try:
+        regex = re.compile(pat)
+        return lambda s: regex.search(s[2:] if s.startswith('./') else s)
+    except:
+        warn("could not parse hgignore pattern '%s'" % line)
+        return lambda s: True
+
+def compile_ff_glob(line):
+    pat = glob_to_re(line)
+
+    try:
+        return compile_re(pat)
+    except:
+        warn("could not parse ffignore pattern '%s'" % line)
+        return lambda s: True
+
 
 def parse_gitignore_file(path):
     if not os.path.isfile(path):
@@ -255,6 +309,34 @@
 
     return ignorers
 
+def parse_hgignore_file(path):
+    if not os.path.isfile(path):
+        return []
+
+    syntax = IGNORE_SYNTAX_REGEX
+    ignorers = []
+    with open(path) as f:
+        for line in f.readlines():
+            line = line.rstrip('\n')
+            if HGIGNORE_BLANK_RE.match(line):
+                continue
+            elif HGIGNORE_COMMENT_RE.match(line):
+                continue
+            elif HGIGNORE_SYNTAX_RE.match(line):
+                s = HGIGNORE_SYNTAX_RE.match(line).groups()[0].lower()
+                if s == 'glob':
+                    syntax = IGNORE_SYNTAX_GLOB
+                elif s in ['re', 'regexp']:
+                    syntax = IGNORE_SYNTAX_REGEX
+            else:
+                # This line is a pattern.
+                if syntax == IGNORE_SYNTAX_REGEX:
+                    ignorers.append(compile_re(line))
+                elif syntax == IGNORE_SYNTAX_GLOB:
+                    ignorers.append(compile_hg_glob(line))
+
+    return ignorers
+
 def parse_ffignore_file(path):
     if not os.path.isfile(path):
         return []
@@ -283,7 +365,7 @@
                 elif syntax == IGNORE_SYNTAX_REGEX:
                     ignorers.append(compile_re(line))
                 elif syntax == IGNORE_SYNTAX_GLOB:
-                    ignorers.append(compile_glob(line))
+                    ignorers.append(compile_ff_glob(line))
 
     return ignorers
 
@@ -295,6 +377,8 @@
             ignorers.extend(parse_ffignore_file(target))
         elif filename == '.gitignore':
             ignorers.extend(parse_gitignore_file(target))
+        elif filename == '.hgignore':
+            ignorers.extend(parse_hgignore_file(target))
     return ignorers
 
 
@@ -709,7 +793,7 @@
 
     # Ignore files
     if options.ignore_mode == IGNORE_MODE_RESTRICTED:
-        options.ignore_files = ['.ffignore', '.gitignore']
+        options.ignore_files = ['.ffignore', '.gitignore', '.hgignore']
         options.ignore_vcs_dirs = True
     elif options.ignore_mode == IGNORE_MODE_SEMI:
         options.ignore_files = ['.ffignore']