# HG changeset patch # User Steve Losh # Date 1348718433 14400 # Node ID cf6c3851d091fcefa07977f59c5a91f9918b4403 # Parent 5909877323bc4350436ba0ceb98881ec3310da85 Add preliminary .gitignore support. diff -r 5909877323bc -r cf6c3851d091 ffind --- 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']