package main import ( "bufio" "fmt" "os" "os/exec" "path/filepath" "strings" ) func runCommand(name string, args ...string) error { cmd := exec.Command(name, args...) fmt.Fprintf(os.Stderr, "+ %s %s\n", name, strings.Join(args, " ")) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } func runCommandSilent(name string, args ...string) error { cmd := exec.Command(name, args...) fmt.Fprintf(os.Stderr, "+ %s %s\n", name, strings.Join(args, " ")) cmd.Stdout = nil cmd.Stderr = nil return cmd.Run() } func runCommandOutput(name string, args ...string) (string, error) { cmd := exec.Command(name, args...) fmt.Fprintf(os.Stderr, "+ %s %s\n", name, strings.Join(args, " ")) output, err := cmd.Output() if err != nil { return "", err } return strings.TrimSpace(string(output)), nil } // GitIgnore represents a .gitignore parser type GitIgnore struct { patterns []string } // NewGitIgnore creates a new GitIgnore parser by reading .gitignore file func NewGitIgnore() (*GitIgnore, error) { gi := &GitIgnore{} file, err := os.Open(".gitignore") if err != nil { if os.IsNotExist(err) { return gi, nil // No .gitignore file is fine } return nil, err } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line != "" && !strings.HasPrefix(line, "#") { gi.patterns = append(gi.patterns, line) } } return gi, scanner.Err() } // IsIgnored checks if a file path should be ignored according to .gitignore patterns func (gi *GitIgnore) IsIgnored(path string) bool { for _, pattern := range gi.patterns { if gi.matchPattern(pattern, path) { return true } } return false } // matchPattern implements basic gitignore pattern matching func (gi *GitIgnore) matchPattern(pattern, path string) bool { // Handle negation patterns (starting with !) if strings.HasPrefix(pattern, "!") { return false // Negation patterns would require more complex logic } // Convert gitignore pattern to filepath.Match compatible pattern // Handle directory patterns (ending with /) if strings.HasSuffix(pattern, "/") { pattern = strings.TrimSuffix(pattern, "/") // Check if any directory component matches parts := strings.Split(path, string(filepath.Separator)) for _, part := range parts { if matched, _ := filepath.Match(pattern, part); matched { return true } } return false } // Handle patterns with path separators if strings.Contains(pattern, "/") { // Exact path match if matched, _ := filepath.Match(pattern, path); matched { return true } // Check if pattern matches any suffix of the path pathParts := strings.Split(path, string(filepath.Separator)) patternParts := strings.Split(pattern, "/") if len(patternParts) <= len(pathParts) { for i := 0; i <= len(pathParts)-len(patternParts); i++ { match := true for j, patternPart := range patternParts { if matched, _ := filepath.Match(patternPart, pathParts[i+j]); !matched { match = false break } } if match { return true } } } return false } // Simple filename pattern - check against basename and any path component basename := filepath.Base(path) if matched, _ := filepath.Match(pattern, basename); matched { return true } // Check if pattern matches any directory component parts := strings.Split(path, string(filepath.Separator)) for _, part := range parts { if matched, _ := filepath.Match(pattern, part); matched { return true } } return false }