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  }