github.com/driusan/dgit@v0.0.0-20221118233547-f39f0c15edbb/git/ignore.go (about) 1 package git 2 3 import ( 4 "bufio" 5 "fmt" 6 "io" 7 "log" 8 "os" 9 "path/filepath" 10 "sort" 11 "strconv" 12 "strings" 13 ) 14 15 // An ignore pattern declared in a pattern file (e.g. .gitignore) or provided as an input. 16 type IgnorePattern struct { 17 Pattern string // The pattern to match, relative to the scope 18 Source File // The path to the pattern file where this pattern is found, relative path for work dir resources, otherwise absolute path or blank for one that was provided as input to dgit 19 Scope File // The work directory relative scope that this pattern applies 20 LineNum int // The line number within the source pattern file where this pattern is located 21 } 22 23 // Represents a match (or non-match) of a particular path name against a set of ignore patterns 24 type IgnoreMatch struct { 25 IgnorePattern // A match has all of the pattern information, or empty pattern if no match was found 26 PathName File // The provided path name that was checked for the ignore 27 } 28 29 // Returns the standard representation of an ignore match (or non-match) 30 func (im IgnoreMatch) String() string { 31 return fmt.Sprintf("%s:%s:%s\t%s", im.Source, im.LineString(), im.Pattern, im.PathName) 32 } 33 34 func (im IgnoreMatch) LineString() string { 35 lineNum := "" 36 if im.LineNum != 0 { 37 lineNum = strconv.Itoa(im.LineNum) 38 } 39 return lineNum 40 } 41 42 func (ip IgnorePattern) Matches(ignorePath string, isDir bool) bool { 43 if !strings.HasPrefix(ignorePath, ip.Scope.String()) { 44 return false 45 } 46 47 rel, err := filepath.Rel("/"+ip.Scope.String(), "/"+ignorePath) 48 49 // Not in scope of this pattern 50 if err != nil || strings.HasPrefix(rel, ".") { 51 return false 52 } 53 54 pattern := ip.Pattern 55 if pattern[0] == '!' { 56 pattern = pattern[1:] 57 } 58 return matchesGlob("/"+rel, isDir, pattern) 59 } 60 61 func (ip IgnorePattern) Negates() bool { 62 return len(ip.Pattern) > 1 && ip.Pattern[0] == '!' 63 } 64 65 // Returns the standard ignores (.gitignore files and various global sources) that 66 // should be used to determine whether the provided files are ignored. These patterns 67 // can be used with these files with ApplyIgnore() to find whether each file is ignored 68 // or not. 69 func StandardIgnorePatterns(c *Client, paths []File) ([]IgnorePattern, error) { 70 alreadyParsed := make(map[string]bool) 71 finalPatterns := []IgnorePattern{} 72 73 for _, path := range paths { 74 abs, err := filepath.Abs(path.String()) 75 if err != nil { 76 return nil, err 77 } 78 79 // Let's check that this path is in the git work dir first 80 wdpath, err := filepath.Rel(c.WorkDir.String(), abs) 81 if err != nil || strings.HasPrefix(wdpath, "..") { 82 return nil, fmt.Errorf("Path %v is not in the git work directory.", path.String()) 83 } 84 85 // this could be the work dir specified as '.' so let's start with that. 86 dir := filepath.Dir(abs) 87 88 for { 89 gitignore := filepath.Join(dir, ".gitignore") 90 if _, ok := alreadyParsed[gitignore]; ok { 91 if dir == c.WorkDir.String() { 92 break 93 } 94 dir = filepath.Dir(dir) 95 continue 96 } 97 alreadyParsed[gitignore] = true 98 99 scope, err := filepath.Rel(c.WorkDir.String(), dir) 100 if err != nil { 101 return []IgnorePattern{}, err 102 } 103 104 if scope == "." { 105 scope = "" 106 } 107 108 ignorePatterns, err := ParseIgnorePatterns(c, File(gitignore), File(scope)) 109 if err != nil { 110 return []IgnorePattern{}, err 111 } 112 113 finalPatterns = append(finalPatterns, ignorePatterns...) 114 115 if dir == c.WorkDir.String() || dir == filepath.Dir(c.WorkDir.String()) { 116 break 117 } 118 dir = filepath.Dir(dir) 119 } 120 } 121 122 // Check for an info/exclude file in the .git directory 123 ignorePatterns, err := ParseIgnorePatterns(c, File(filepath.Join(c.GitDir.String(), "info/exclude")), File("")) 124 if err != nil { 125 return []IgnorePattern{}, err 126 } 127 finalPatterns = append(finalPatterns, ignorePatterns...) 128 129 // Check for a core.excludesfile config value 130 excludes := c.GetConfig("core.excludesfile") 131 if excludes != "" { 132 excludesFile := File(excludes) 133 ignorePatterns, err = ParseIgnorePatterns(c, excludesFile, File("")) 134 if err != nil { 135 return []IgnorePattern{}, err 136 } 137 138 finalPatterns = append(finalPatterns, ignorePatterns...) 139 } 140 141 return finalPatterns, err 142 } 143 144 func ParseIgnorePatterns(c *Client, patternFile File, scope File) ([]IgnorePattern, error) { 145 log.Printf("Parsing %s for ignore patterns\n", patternFile) 146 _, err := patternFile.Lstat() 147 if os.IsNotExist(err) { 148 return []IgnorePattern{}, nil 149 } 150 151 if err != nil { 152 return []IgnorePattern{}, err 153 } 154 155 file, err := patternFile.Open() 156 if err != nil { 157 return []IgnorePattern{}, err 158 } 159 defer file.Close() 160 161 ignorePatterns := []IgnorePattern{} 162 reader := bufio.NewReader(file) 163 lineNumber := 0 164 source := patternFile 165 rel, err := filepath.Rel(c.WorkDir.String(), patternFile.String()) 166 if err == nil { 167 source = File(rel) 168 } 169 170 for { 171 pattern := "" 172 isEof := false 173 174 for { 175 linebytes, isprefix, err := reader.ReadLine() 176 pattern = pattern + string(linebytes) 177 178 if isprefix { 179 continue 180 } 181 182 lineNumber++ 183 184 if err == io.EOF { 185 isEof = true 186 } else if err != nil { 187 return nil, err 188 } 189 break 190 } 191 192 if pattern != "" && !strings.HasPrefix(pattern, "#") { 193 lastEscape := strings.LastIndex(pattern, "\\") + 1 194 if len(pattern) > lastEscape { 195 pattern = pattern[:lastEscape+1] + strings.TrimSpace(pattern[lastEscape+1:]) 196 } 197 198 pattern = strings.Replace(pattern, "\\#", "#", 1) 199 pattern = strings.Replace(pattern, "\\ ", " ", -1) 200 pattern = strings.Replace(pattern, "\\\t", "\t", -1) 201 202 ignorePattern := IgnorePattern{Pattern: pattern, LineNum: lineNumber, Scope: scope, Source: source} 203 ignorePatterns = append(ignorePatterns, ignorePattern) 204 } 205 206 if isEof { 207 break 208 } 209 } 210 211 return ignorePatterns, nil 212 } 213 214 // Patterns are sorted by their scope so that the most specific scopes are first and the more 215 // broad scopes are later on. This is to allow broader scopes to negate previous pattern matches 216 // in the future. 217 func sortPatterns(patterns *[]IgnorePattern) { 218 sort.Slice(*patterns, func(i, j int) bool { 219 return len((*patterns)[i].Scope.String()) > len((*patterns)[j].Scope.String()) 220 }) 221 } 222 223 func MatchIgnores(c *Client, patterns []IgnorePattern, paths []File) ([]IgnoreMatch, error) { 224 log.Printf("Matching ignores for paths %v using patterns %+v\n", paths, patterns) 225 patternMatches := make([]IgnoreMatch, len(paths)) 226 227 sortPatterns(&patterns) 228 229 for idx, path := range paths { 230 abs, err := filepath.Abs(path.String()) 231 if err != nil { 232 return nil, err 233 } 234 235 stat, _ := os.Lstat(abs) 236 isDir := false 237 if stat != nil { 238 isDir = stat.IsDir() 239 } 240 241 // Let's check that this path is in the git work dir first 242 wdpath, err := filepath.Rel(c.WorkDir.String(), abs) 243 if err != nil || strings.HasPrefix(wdpath, "..") { 244 return nil, fmt.Errorf("Path %v is not in the git work directory.", path.String()) 245 } 246 247 for _, pattern := range patterns { 248 if pattern.Matches(wdpath, isDir) { 249 patternMatches[idx].Pattern = pattern.Pattern 250 patternMatches[idx].Source = pattern.Source 251 patternMatches[idx].Scope = pattern.Scope 252 patternMatches[idx].LineNum = pattern.LineNum 253 break // For now, until we support negation 254 } 255 } 256 257 // Be sure to assign the pathname in all cases so that clients can match the outputs to their inputs 258 patternMatches[idx].PathName = path 259 } 260 261 log.Printf("Ignore matches: %+v\n", patternMatches) 262 263 return patternMatches, nil 264 } 265 266 func matchesGlob(path string, isDir bool, pattern string) bool { 267 if !strings.HasPrefix(pattern, "/") { 268 pattern = "/**/" + pattern 269 } 270 271 pathSegs := strings.Split(path, "/") 272 patternSegs := strings.Split(pattern, "/") 273 274 for len(pathSegs) > 0 && len(patternSegs) > 0 { 275 if patternSegs[0] == "**" && len(patternSegs) > 1 { 276 if m, _ := filepath.Match(patternSegs[1], pathSegs[0]); m { 277 patternSegs = patternSegs[2:] 278 } 279 } else if m, _ := filepath.Match(patternSegs[0], pathSegs[0]); m { 280 patternSegs = patternSegs[1:] 281 } else { 282 break 283 } 284 285 pathSegs = pathSegs[1:] 286 } 287 288 if len(patternSegs) == 0 { 289 return true 290 } else if patternSegs[0] == "" && len(pathSegs) > 0 { 291 return true 292 } else if patternSegs[0] == "" && isDir { 293 return true 294 } else { 295 return false 296 } 297 }