| 1 | package sync |
| 2 | |
| 3 | import ( |
| 4 | "path" |
| 5 | "strings" |
| 6 | ) |
| 7 | |
| 8 | // matchIgnore reports whether rel (a slash-separated path relative to the sync |
| 9 | // root) is covered by a single gitignore-style pattern. |
| 10 | // |
| 11 | // This is a deliberate subset of gitignore semantics — the part deploy configs |
| 12 | // actually use — not a full implementation. Supported: |
| 13 | // |
| 14 | // - Trailing "/" marks a directory pattern: "pgdata/" matches the directory |
| 15 | // and everything beneath it, but never a plain file named "pgdata". |
| 16 | // - A pattern containing "/" is anchored to the sync root, as in gitignore: |
| 17 | // "master/*.log" matches "master/x.log" but not "a/master/x.log". |
| 18 | // - A pattern with no "/" matches at any depth: "__pycache__/" catches |
| 19 | // "a/b/__pycache__/x.pyc", and "*.pem" catches "certs/key.pem". |
| 20 | // |
| 21 | // Not supported (no deploy.yml in this repo uses them): "!" negation, a leading |
| 22 | // "/" anchor, and "**". A "!" pattern is ignored outright rather than silently |
| 23 | // treated as a literal — see ignoreSet. |
| 24 | func matchIgnore(pattern, rel string) bool { |
| 25 | pattern = strings.TrimSpace(pattern) |
| 26 | if pattern == "" || strings.HasPrefix(pattern, "#") { |
| 27 | return false |
| 28 | } |
| 29 | dirOnly := strings.HasSuffix(pattern, "/") |
| 30 | pattern = strings.Trim(pattern, "/") |
| 31 | if pattern == "" { |
| 32 | return false |
| 33 | } |
| 34 | |
| 35 | if strings.Contains(pattern, "/") { |
| 36 | // Anchored: match the whole path, or any ancestor directory of it so a |
| 37 | // directory pattern covers everything beneath it. |
| 38 | if !dirOnly { |
| 39 | if ok, _ := path.Match(pattern, rel); ok { |
| 40 | return true |
| 41 | } |
| 42 | } |
| 43 | for dir := path.Dir(rel); dir != "." && dir != "/"; dir = path.Dir(dir) { |
| 44 | if ok, _ := path.Match(pattern, dir); ok { |
| 45 | return true |
| 46 | } |
| 47 | } |
| 48 | return false |
| 49 | } |
| 50 | |
| 51 | // Unanchored: test every path segment. A directory pattern may only match a |
| 52 | // segment that actually is a directory — i.e. not the final one. |
| 53 | segs := strings.Split(rel, "/") |
| 54 | for i, seg := range segs { |
| 55 | isDir := i < len(segs)-1 |
| 56 | if dirOnly && !isDir { |
| 57 | continue |
| 58 | } |
| 59 | if ok, _ := path.Match(pattern, seg); ok { |
| 60 | return true |
| 61 | } |
| 62 | } |
| 63 | return false |
| 64 | } |
| 65 | |
| 66 | // ignoreSet is a compiled list of gitignore-style patterns. |
| 67 | type ignoreSet []string |
| 68 | |
| 69 | // newIgnoreSet keeps only the patterns matchIgnore can honour. Negation ("!") |
| 70 | // is dropped rather than misapplied: treating "!keep.log" as a literal would |
| 71 | // exclude the very path the author meant to re-include, so an unsupported |
| 72 | // pattern must never widen the exclusion. |
| 73 | func newIgnoreSet(patterns []string) ignoreSet { |
| 74 | out := make(ignoreSet, 0, len(patterns)) |
| 75 | for _, p := range patterns { |
| 76 | p = strings.TrimSpace(p) |
| 77 | if p == "" || strings.HasPrefix(p, "#") || strings.HasPrefix(p, "!") { |
| 78 | continue |
| 79 | } |
| 80 | out = append(out, p) |
| 81 | } |
| 82 | return out |
| 83 | } |
| 84 | |
| 85 | // match reports whether rel is covered by any pattern in the set. |
| 86 | func (s ignoreSet) match(rel string) bool { |
| 87 | for _, p := range s { |
| 88 | if matchIgnore(p, rel) { |
| 89 | return true |
| 90 | } |
| 91 | } |
| 92 | return false |
| 93 | } |
| 94 | |