cf6c3851d091

Add preliminary .gitignore support.
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Thu, 27 Sep 2012 00:00:33 -0400
parents 5909877323bc
children d6c1bac54f7d 9626235f7fe1
branches/tags (none)
files ffind

Changes

--- a/ffind	Wed Sep 26 23:00:18 2012 -0400
+++ b/ffind	Thu Sep 27 00:00:33 2012 -0400
@@ -69,6 +69,7 @@
 
 # Regexes ---------------------------------------------------------------------
 SIZE_RE = re.compile(r'^(\d+(?:\.\d+)?)([bkmgtp])?[a-z]*$', re.IGNORECASE)
+
 AGO_RE = re.compile(r'''
                     (\d+(?:\.\d+)?)           # The number (float/int)
                     \s*                       # Optional whitespace
@@ -82,11 +83,16 @@
                       | s(?:ecs?(?:onds?)?)?  # s/sec/secs/second/seconds
                     )
                     ''', re.VERBOSE | re.IGNORECASE)
+
 IGNORE_SYNTAX_RE = re.compile(r'^\s*syntax:\s*(glob|regexp|regex|re|literal)\s*$',
                               re.IGNORECASE)
 IGNORE_COMMENT_RE = re.compile(r'^\s*#')
 IGNORE_BLANK_RE = re.compile(r'^\s*$')
 
+GITIGNORE_COMMENT_RE = re.compile(r'^\s*#')
+GITIGNORE_BLANK_RE = re.compile(r'^\s*$')
+GITIGNORE_NEGATE_RE = re.compile(r'^\s*!')
+
 
 # Global Options --------------------------------------------------------------
 # (it's a prototype, shut up)
@@ -125,7 +131,90 @@
     l = line
     return lambda s: l in s
 
-def parse_ignore_file(path):
+def compile_git(line):
+    pat = ''
+
+    # The following comments are (mostly) from gitignore(5).
+
+    # If the pattern ends with a slash, it is removed for the purpose of the
+    # following description, but it would only find a match with a directory. In
+    # other words, foo/ will match a directory foo and paths underneath it, but
+    # will not match a regular file or a symbolic link foo (this is consistent
+    # with the way how pathspec works in general in git).
+    # directories_only = line.endswith('/')
+
+    # A leading slash matches the beginning of the pathname. For example, "/*.c"
+    # matches "cat-file.c" but not "mozilla-sha1/sha1.c".
+    if line.startswith('/'):
+        pat += '^./'
+        line = line[1:]
+
+    def _eat_glob(chs):
+        pat = ''
+        while chs:
+            ch = chs.pop(0)
+            if ch == '?':
+                pat += '.'
+            elif ch == '*':
+                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
+
+    chs = list(line)
+    # I can't tell what the difference is between these two cases because git's
+    # documentation is fucking inscrutable.
+    if '/' not in line:
+        # If the pattern does not contain a slash /, git treats it as a shell
+        # glob pattern and checks for a match against the pathname relative to
+        # the location of the .gitignore file (relative to the toplevel of the
+        # work tree if not from a .gitignore file).
+        pat += _eat_glob(chs)
+    else:
+        # Otherwise, git treats the pattern as a shell glob suitable for
+        # consumption by fnmatch(3) with the FNM_PATHNAME flag: wildcards in the
+        # pattern will not match a / in the pathname. For example,
+        # "Documentation/*.html" matches "Documentation/git.html" but not
+        # "Documentation/ppc/ppc.html" or "tools/perf/Documentation/perf.html".
+        pat += _eat_glob(chs)
+
+    try:
+        regex = re.compile(pat)
+        return lambda s: regex.search(s)
+    except:
+        warn("could not parse gitignore pattern '%s'" % line)
+        return lambda s: True
+
+
+def parse_gitignore_file(path):
+    if not os.path.isfile(path):
+        return []
+
+    ignorers = []
+    with open(path) as f:
+        for line in f.readlines():
+            line = line.rstrip('\n')
+            if GITIGNORE_BLANK_RE.match(line):
+                continue
+            elif GITIGNORE_COMMENT_RE.match(line):
+                continue
+            elif GITIGNORE_NEGATE_RE.match(line):
+                # TODO: This bullshit feature.
+                continue
+            else:
+                # This line is a gitignore pattern.
+                ignorers.append(compile_git(line))
+
+    return ignorers
+
+def parse_ffignore_file(path):
     if not os.path.isfile(path):
         return []
 
@@ -160,19 +249,25 @@
 def parse_ignore_files(dir):
     ignorers = []
     for filename in options.ignore_files:
-        ignorers.extend(parse_ignore_file(os.path.join(dir, filename)))
+        target = os.path.join(dir, filename)
+        if filename == '.ffignore':
+            ignorers.extend(parse_ffignore_file(target))
+        elif filename == '.gitignore':
+            ignorers.extend(parse_gitignore_file(target))
     return ignorers
 
+
 def get_initial_ignorers():
     if '.ffignore' in options.ignore_files:
         home = os.environ.get('HOME')
         if home:
-            return parse_ignore_file(os.path.join(home, '.ffignore'))
+            return parse_ffignore_file(os.path.join(home, '.ffignore'))
         else:
             return []
     else:
         return []
 
+
 # Searching! ------------------------------------------------------------------
 def get_type(path):
     link = os.path.islink(path)
@@ -572,7 +667,7 @@
 
     # Ignore files
     if options.ignore_mode == IGNORE_MODE_RESTRICTED:
-        options.ignore_files = ['.ffignore']
+        options.ignore_files = ['.ffignore', '.gitignore']
         options.ignore_vcs_dirs = True
     elif options.ignore_mode == IGNORE_MODE_SEMI:
         options.ignore_files = ['.ffignore']