github.com/driusan/dgit@v0.0.0-20221118233547-f39f0c15edbb/git/lsfiles.go (about) 1 package git 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "log" 7 "path" 8 "path/filepath" 9 "sort" 10 "strings" 11 ) 12 13 // Finds things that aren't tracked, and creates fake IndexEntrys for them to be merged into 14 // the output if --others is passed. 15 func findUntrackedFilesFromDir(c *Client, opts LsFilesOptions, root, parent, dir File, tracked map[IndexPath]bool, recursedir bool, ignorePatterns []IgnorePattern) (untracked []*IndexEntry) { 16 files, err := ioutil.ReadDir(dir.String()) 17 if err != nil { 18 return nil 19 } 20 for _, ignorefile := range opts.ExcludePerDirectory { 21 ignoreInDir := ignorefile 22 if dir != "" { 23 ignoreInDir = dir + "/" + ignorefile 24 } 25 26 if ignoreInDir.Exists() { 27 log.Println("Adding excludes from", ignoreInDir) 28 29 patterns, err := ParseIgnorePatterns(c, ignoreInDir, dir) 30 if err != nil { 31 continue 32 } 33 ignorePatterns = append(ignorePatterns, patterns...) 34 } 35 } 36 files: 37 for _, fi := range files { 38 fname := File(fi.Name()) 39 if fi.Name() == ".git" { 40 continue 41 } 42 for _, pattern := range ignorePatterns { 43 var name File 44 if parent == "" { 45 name = fname 46 } else { 47 name = parent + "/" + fname 48 } 49 if pattern.Matches(name.String(), fi.IsDir()) { 50 continue files 51 } 52 } 53 if fi.IsDir() { 54 if !recursedir { 55 // This isn't very efficient, but lets us implement git ls-files --directory 56 // without too many changes. 57 indexPath, err := (parent + "/" + fname).IndexPath(c) 58 if err != nil { 59 panic(err) 60 } 61 dirHasTracked := false 62 for path := range tracked { 63 if strings.HasPrefix(path.String(), indexPath.String()) { 64 dirHasTracked = true 65 break 66 } 67 } 68 if !dirHasTracked { 69 if opts.Directory { 70 if opts.NoEmptyDirectory { 71 if files, err := ioutil.ReadDir(fname.String()); len(files) == 0 && err == nil { 72 continue 73 } 74 } 75 indexPath += "/" 76 } 77 untracked = append(untracked, &IndexEntry{PathName: indexPath}) 78 continue 79 } 80 } 81 var newparent, newdir File 82 if parent == "" { 83 newparent = fname 84 } else { 85 newparent = parent + "/" + fname 86 } 87 if dir == "" { 88 newdir = fname 89 } else { 90 newdir = dir + "/" + fname 91 } 92 93 recurseFiles := findUntrackedFilesFromDir(c, opts, root, newparent, newdir, tracked, recursedir, ignorePatterns) 94 untracked = append(untracked, recurseFiles...) 95 } else { 96 var filePath File 97 if parent == "" { 98 filePath = File(strings.TrimPrefix(fname.String(), root.String())) 99 100 } else { 101 filePath = File(strings.TrimPrefix((parent + "/" + fname).String(), root.String())) 102 } 103 indexPath, err := filePath.IndexPath(c) 104 if err != nil { 105 panic(err) 106 } 107 indexPath = IndexPath(filePath) 108 109 if _, ok := tracked[indexPath]; !ok { 110 untracked = append(untracked, &IndexEntry{PathName: indexPath}) 111 } 112 } 113 } 114 return 115 } 116 117 // Describes the options that may be specified on the command line for 118 // "git diff-index". Note that only raw mode is currently supported, even 119 // though all the other options are parsed/set in this struct. 120 type LsFilesOptions struct { 121 // Types of files to show 122 Cached, Deleted, Modified, Others bool 123 124 // Invert exclusion logic 125 Ignored bool 126 127 // Show stage status instead of just file name 128 Stage bool 129 130 // Show files which are unmerged. Implies Stage. 131 Unmerged bool 132 133 // Show files which need to be removed for checkout-index to succeed 134 Killed bool 135 136 // If a directory is classified as "other", show only its name, not 137 // its contents 138 Directory bool 139 140 // Do not show empty directories with --others 141 NoEmptyDirectory bool 142 143 // Exclude standard patterns (ie. .gitignore and .git/info/exclude) 144 ExcludeStandard bool 145 146 // Exclude using the provided patterns 147 ExcludePatterns []string 148 149 // Exclude using the provided file with the patterns 150 ExcludeFiles []File 151 152 // Exclude using additional patterns from each directory 153 ExcludePerDirectory []File 154 155 ErrorUnmatch bool 156 157 // Equivalent to the -t option to git ls-files 158 Status bool 159 } 160 161 type LsFilesResult struct { 162 *IndexEntry 163 StatusCode rune 164 } 165 166 // LsFiles implements the git ls-files command. It returns an array of files 167 // that match the options passed. 168 func LsFiles(c *Client, opt LsFilesOptions, files []File) ([]LsFilesResult, error) { 169 var fs []LsFilesResult 170 index, err := c.GitDir.ReadIndex() 171 if err != nil { 172 return nil, err 173 } 174 175 // We need to keep track of what's in the index if --others is passed. 176 // Keep a map instead of doing an O(n) search every time. 177 var filesInIndex map[IndexPath]bool 178 if opt.Others || opt.ErrorUnmatch { 179 filesInIndex = make(map[IndexPath]bool) 180 } 181 182 for _, entry := range index.Objects { 183 f, err := entry.PathName.FilePath(c) 184 if err != nil { 185 return nil, err 186 } 187 if opt.Killed { 188 // We go through each parent to check if it exists on the filesystem 189 // until we find a directory (which means there's no more files getting 190 // in the way of os.MkdirAll from succeeding in CheckoutIndex) 191 pathparent := filepath.Clean(path.Dir(f.String())) 192 193 for pathparent != "" && pathparent != "." { 194 f := File(pathparent) 195 if f.IsDir() { 196 // We found a directory, so there's nothing 197 // getting in the way 198 break 199 } else if f.Exists() { 200 // It's not a directory but it exists, 201 // so we need to delete it 202 indexPath, err := f.IndexPath(c) 203 if err != nil { 204 return nil, err 205 } 206 fs = append(fs, LsFilesResult{ 207 &IndexEntry{PathName: indexPath}, 208 'K', 209 }) 210 } 211 // check the next level of the directory path 212 pathparent, _ = filepath.Split(filepath.Clean(pathparent)) 213 } 214 if f.IsDir() { 215 indexPath, err := f.IndexPath(c) 216 if err != nil { 217 return nil, err 218 } 219 fs = append(fs, LsFilesResult{ 220 &IndexEntry{PathName: indexPath}, 221 'K', 222 }) 223 } 224 } 225 226 if opt.Others || opt.ErrorUnmatch { 227 filesInIndex[entry.PathName] = true 228 } 229 230 if strings.HasPrefix(f.String(), "../") || len(files) > 0 { 231 skip := true 232 for _, explicit := range files { 233 eAbs, err := filepath.Abs(explicit.String()) 234 if err != nil { 235 return nil, err 236 } 237 fAbs, err := filepath.Abs(f.String()) 238 if err != nil { 239 return nil, err 240 } 241 if fAbs == eAbs || strings.HasPrefix(fAbs, eAbs+"/") { 242 skip = false 243 break 244 } 245 if f.MatchGlob(explicit.String()) { 246 skip = false 247 break 248 } 249 } 250 if skip { 251 continue 252 } 253 } 254 255 if opt.Cached { 256 if entry.SkipWorktree() { 257 fs = append(fs, LsFilesResult{entry, 'S'}) 258 } else { 259 fs = append(fs, LsFilesResult{entry, 'H'}) 260 } 261 continue 262 } 263 if opt.Deleted { 264 if !f.Exists() { 265 fs = append(fs, LsFilesResult{entry, 'R'}) 266 continue 267 } 268 } 269 270 if opt.Unmerged && entry.Stage() != Stage0 { 271 fs = append(fs, LsFilesResult{entry, 'M'}) 272 continue 273 } 274 275 if opt.Modified { 276 if f.IsDir() { 277 fs = append(fs, LsFilesResult{entry, 'C'}) 278 continue 279 } 280 // If we couldn't stat it, we assume it was deleted and 281 // is therefore modified. (It could be because the file 282 // was deleted, or it could be bcause a parent directory 283 // was deleted and we couldn't stat it. The latter means 284 // that os.IsNotExist(err) can't be used to check if it 285 // really was deleted, so for now we just assume.) 286 if _, err := f.Stat(); err != nil { 287 fs = append(fs, LsFilesResult{entry, 'C'}) 288 continue 289 } 290 291 // We've done everything we can to avoid hashing the file, but now 292 // we need to to avoid the case where someone changes a file, then 293 // changes it back to the original contents 294 hash, _, err := HashFile("blob", f.String()) 295 if err != nil { 296 return nil, err 297 } 298 if hash != entry.Sha1 { 299 fs = append(fs, LsFilesResult{entry, 'C'}) 300 } 301 } 302 } 303 304 if opt.ErrorUnmatch { 305 for _, file := range files { 306 indexPath, err := file.IndexPath(c) 307 if err != nil { 308 return nil, err 309 } 310 if _, ok := filesInIndex[indexPath]; !ok { 311 return nil, fmt.Errorf("error: pathspec '%v' did not match any file(s) known to git", file) 312 } 313 } 314 } 315 316 if opt.Others { 317 wd := File(c.WorkDir) 318 319 ignorePatterns := []IgnorePattern{} 320 321 if opt.ExcludeStandard { 322 opt.ExcludeFiles = append(opt.ExcludeFiles, File(filepath.Join(c.GitDir.String(), "info/exclude"))) 323 opt.ExcludePerDirectory = append(opt.ExcludePerDirectory, ".gitignore") 324 } 325 326 for _, file := range opt.ExcludeFiles { 327 patterns, err := ParseIgnorePatterns(c, file, "") 328 if err != nil { 329 return nil, err 330 } 331 ignorePatterns = append(ignorePatterns, patterns...) 332 } 333 334 for _, pattern := range opt.ExcludePatterns { 335 ignorePatterns = append(ignorePatterns, IgnorePattern{Pattern: pattern, Source: "", LineNum: 1, Scope: ""}) 336 } 337 338 others := findUntrackedFilesFromDir(c, opt, wd+"/", wd, wd, filesInIndex, !opt.Directory, ignorePatterns) 339 for _, file := range others { 340 f, err := file.PathName.FilePath(c) 341 if err != nil { 342 return nil, err 343 } 344 345 if strings.HasPrefix(f.String(), "../") || len(files) > 0 { 346 skip := true 347 for _, explicit := range files { 348 eAbs, err := filepath.Abs(explicit.String()) 349 if err != nil { 350 return nil, err 351 } 352 fAbs, err := filepath.Abs(f.String()) 353 if err != nil { 354 return nil, err 355 } 356 if fAbs == eAbs || strings.HasPrefix(fAbs, eAbs+"/") { 357 skip = false 358 break 359 } 360 } 361 if skip { 362 continue 363 } 364 } 365 fs = append(fs, LsFilesResult{file, '?'}) 366 } 367 } 368 369 sort.Sort(lsByPath(fs)) 370 return fs, nil 371 } 372 373 // Implement the sort interface on *GitIndexEntry, so that 374 // it's easy to sort by name. 375 type lsByPath []LsFilesResult 376 377 func (g lsByPath) Len() int { return len(g) } 378 func (g lsByPath) Swap(i, j int) { g[i], g[j] = g[j], g[i] } 379 func (g lsByPath) Less(i, j int) bool { 380 if g[i].PathName == g[j].PathName { 381 return g[i].Stage() < g[j].Stage() 382 } 383 ibytes := []byte(g[i].PathName) 384 jbytes := []byte(g[j].PathName) 385 for k := range ibytes { 386 if k >= len(jbytes) { 387 // We reached the end of j and there was stuff 388 // leftover in i, so i > j 389 return false 390 } 391 392 // If a character is not equal, return if it's 393 // less or greater 394 if ibytes[k] < jbytes[k] { 395 return true 396 } else if ibytes[k] > jbytes[k] { 397 return false 398 } 399 } 400 // Everything equal up to the end of i, and there is stuff 401 // left in j, so i < j 402 return true 403 }