| /* |
| ignore is a library which returns a new ignorer object which can |
| test against various paths. This is particularly useful when trying |
| to filter files based on a .gitignore document |
| |
| The rules for parsing the input file are the same as the ones listed |
| in the Git docs here: http://git-scm.com/docs/gitignore |
| |
| The summarized version of the same has been copied here: |
| |
| 1. A blank line matches no files, so it can serve as a separator |
| for readability. |
| 2. A line starting with # serves as a comment. Put a backslash ("\") |
| in front of the first hash for patterns that begin with a hash. |
| 3. Trailing spaces are ignored unless they are quoted with backslash ("\"). |
| 4. An optional prefix "!" which negates the pattern; any matching file |
| excluded by a previous pattern will become included again. It is not |
| possible to re-include a file if a parent directory of that file is |
| excluded. Git doesn’t list excluded directories for performance reasons, |
| so any patterns on contained files have no effect, no matter where they |
| are defined. Put a backslash ("\") in front of the first "!" for |
| patterns that begin with a literal "!", for example, "\!important!.txt". |
| 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). |
| 6. 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). |
| 7. 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". |
| 8. A leading slash matches the beginning of the pathname. For example, |
| "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c". |
| 9. Two consecutive asterisks ("**") in patterns matched against full |
| pathname may have special meaning: |
| i. A leading "**" followed by a slash means match in all directories. |
| For example, "** /foo" matches file or directory "foo" anywhere, |
| the same as pattern "foo". "** /foo/bar" matches file or directory |
| "bar" anywhere that is directly under directory "foo". |
| ii. A trailing "/**" matches everything inside. For example, "abc/**" |
| matches all files inside directory "abc", relative to the location |
| of the .gitignore file, with infinite depth. |
| iii. A slash followed by two consecutive asterisks then a slash matches |
| zero or more directories. For example, "a/** /b" matches "a/b", |
| "a/x/b", "a/x/y/b" and so on. |
| iv. Other consecutive asterisks are considered invalid. */ |
| package ignore |
| |
| import ( |
| "io/ioutil" |
| "os" |
| "regexp" |
| "strings" |
| ) |
| |
| //////////////////////////////////////////////////////////// |
| |
| // An IgnoreParser is an interface which exposes two methods: |
| // MatchesPath() - Returns true if the path is targeted by the patterns compiled in the GitIgnore structure |
| type IgnoreParser interface { |
| MatchesPath(f string) bool |
| } |
| |
| //////////////////////////////////////////////////////////// |
| |
| // This function pretty much attempts to mimic the parsing rules |
| // listed above at the start of this file |
| func getPatternFromLine(line string) (*regexp.Regexp, bool) { |
| // Trim OS-specific carriage returns. |
| line = strings.TrimRight(line, "\r") |
| |
| // Strip comments [Rule 2] |
| if strings.HasPrefix(line, `#`) { |
| return nil, false |
| } |
| |
| // Trim string [Rule 3] |
| // TODO: Handle [Rule 3], when the " " is escaped with a \ |
| line = strings.Trim(line, " ") |
| |
| // Exit for no-ops and return nil which will prevent us from |
| // appending a pattern against this line |
| if line == "" { |
| return nil, false |
| } |
| |
| // TODO: Handle [Rule 4] which negates the match for patterns leading with "!" |
| negatePattern := false |
| if line[0] == '!' { |
| negatePattern = true |
| line = line[1:] |
| } |
| |
| // Handle [Rule 2, 4], when # or ! is escaped with a \ |
| // Handle [Rule 4] once we tag negatePattern, strip the leading ! char |
| if regexp.MustCompile(`^(\#|\!)`).MatchString(line) { |
| line = line[1:] |
| } |
| |
| // If we encounter a foo/*.blah in a folder, prepend the / char |
| if regexp.MustCompile(`([^\/+])/.*\*\.`).MatchString(line) && line[0] != '/' { |
| line = "/" + line |
| } |
| |
| // Handle escaping the "." char |
| line = regexp.MustCompile(`\.`).ReplaceAllString(line, `\.`) |
| |
| magicStar := "#$~" |
| |
| // Handle "/**/" usage |
| if strings.HasPrefix(line, "/**/") { |
| line = line[1:] |
| } |
| line = regexp.MustCompile(`/\*\*/`).ReplaceAllString(line, `(/|/.+/)`) |
| line = regexp.MustCompile(`\*\*/`).ReplaceAllString(line, `(|.`+magicStar+`/)`) |
| line = regexp.MustCompile(`/\*\*`).ReplaceAllString(line, `(|/.`+magicStar+`)`) |
| |
| // Handle escaping the "*" char |
| line = regexp.MustCompile(`\\\*`).ReplaceAllString(line, `\`+magicStar) |
| line = regexp.MustCompile(`\*`).ReplaceAllString(line, `([^/]*)`) |
| |
| // Handle escaping the "?" char |
| line = strings.Replace(line, "?", `\?`, -1) |
| |
| line = strings.Replace(line, magicStar, "*", -1) |
| |
| // Temporary regex |
| var expr = "" |
| if strings.HasSuffix(line, "/") { |
| expr = line + "(|.*)$" |
| } else { |
| expr = line + "(|/.*)$" |
| } |
| if strings.HasPrefix(expr, "/") { |
| expr = "^(|/)" + expr[1:] |
| } else { |
| expr = "^(|.*/)" + expr |
| } |
| pattern, _ := regexp.Compile(expr) |
| |
| return pattern, negatePattern |
| } |
| |
| //////////////////////////////////////////////////////////// |
| |
| // GitIgnore is a struct which contains a slice of regexp.Regexp |
| // patterns |
| type GitIgnore struct { |
| patterns []*regexp.Regexp // List of regexp patterns which this ignore file applies |
| negate []bool // List of booleans which determine if the pattern is negated |
| } |
| |
| // Accepts a variadic set of strings, and returns a GitIgnore object which |
| // converts and appends the lines in the input to regexp.Regexp patterns |
| // held within the GitIgnore objects "patterns" field |
| func CompileIgnoreLines(lines ...string) (*GitIgnore, error) { |
| g := new(GitIgnore) |
| for _, line := range lines { |
| pattern, negatePattern := getPatternFromLine(line) |
| if pattern != nil { |
| g.patterns = append(g.patterns, pattern) |
| g.negate = append(g.negate, negatePattern) |
| } |
| } |
| return g, nil |
| } |
| |
| // Accepts a ignore file as the input, parses the lines out of the file |
| // and invokes the CompileIgnoreLines method |
| func CompileIgnoreFile(fpath string) (*GitIgnore, error) { |
| buffer, error := ioutil.ReadFile(fpath) |
| if error == nil { |
| s := strings.Split(string(buffer), "\n") |
| return CompileIgnoreLines(s...) |
| } |
| return nil, error |
| } |
| |
| // Accepts a ignore file as the input, parses the lines out of the file |
| // and invokes the CompileIgnoreLines method with additional lines |
| func CompileIgnoreFileAndLines(fpath string, lines ...string) (*GitIgnore, error) { |
| buffer, error := ioutil.ReadFile(fpath) |
| if error == nil { |
| s := strings.Split(string(buffer), "\n") |
| return CompileIgnoreLines(append(s, lines...)...) |
| } |
| return nil, error |
| } |
| |
| //////////////////////////////////////////////////////////// |
| |
| // MatchesPath is an interface function for the IgnoreParser interface. |
| // It returns true if the given GitIgnore structure would target a given |
| // path string "f" |
| func (g GitIgnore) MatchesPath(f string) bool { |
| // Replace OS-specific path separator. |
| f = strings.Replace(f, string(os.PathSeparator), "/", -1) |
| |
| matchesPath := false |
| for idx, pattern := range g.patterns { |
| if pattern.MatchString(f) { |
| // If this is a regular target (not negated with a gitignore exclude "!" etc) |
| if !g.negate[idx] { |
| matchesPath = true |
| // Negated pattern, and matchesPath is already set |
| } else if matchesPath { |
| matchesPath = false |
| } |
| } |
| } |
| return matchesPath |
| } |
| |
| //////////////////////////////////////////////////////////// |