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  }