package main import ( "encoding/json" "flag" "fmt" "log" "os" "os/exec" "path/filepath" "regexp" "strings" ) type Result struct { FilteredChanges []string `json:"filteredChanges"` NextVersion string `json:"nextVersion"` } func main() { // Parse command-line arguments var patterns patternList var depth int flag.Var(&patterns, "p", "Path pattern to filter changed files (can be specified multiple times)") flag.Var(&patterns, "pattern", "Path pattern to filter changed files (can be specified multiple times)") flag.IntVar(&depth, "d", 2, "Depth of changeset (default is 2)") flag.IntVar(&depth, "depth", 2, "Depth of changeset (default is 2)") help := flag.Bool("h", false, "Display this help message") helpLong := flag.Bool("help", false, "Display this help message") flag.Parse() if *help || *helpLong { flag.Usage() os.Exit(1) } if len(patterns) == 0 { fmt.Println("Error: At least one path pattern must be specified.") flag.Usage() os.Exit(1) } // Get the current commit SHA currentSHA := getCurrentSHA() // Get parent commits parentSHAs := getParentSHAs(currentSHA) if len(parentSHAs) == 0 { fmt.Println("Single commit with no parents. Listing all changes.") parentSHAs = append(parentSHAs, currentSHA+"~1") } // Collect all changed files from all parents changedFiles := make(map[string]struct{}) for _, parentSHA := range parentSHAs { files := getChangedFiles(parentSHA, currentSHA) for _, file := range files { changedFiles[file] = struct{}{} } } // Filter and process changed files filteredChanges := filterAndProcessFiles(changedFiles, patterns, depth) // Get the next semantic version using git-sv nextVersion := getNextVersion() // Prepare the result result := Result{ FilteredChanges: uniqueStrings(filteredChanges), NextVersion: nextVersion, } // Output results as JSON output, err := json.Marshal(result) if err != nil { log.Fatalf("Error marshaling JSON: %v", err) } fmt.Println(string(output)) } // Custom flag type for multiple patterns type patternList []string func (p *patternList) String() string { return strings.Join(*p, ", ") } func (p *patternList) Set(value string) error { *p = append(*p, value) return nil } // Helper functions func getCurrentSHA() string { output, err := exec.Command("git", "rev-parse", "HEAD").Output() if err != nil { log.Fatalf("Error getting current SHA: %v", err) } return strings.TrimSpace(string(output)) } func getParentSHAs(commitSHA string) []string { output, err := exec.Command("git", "rev-list", "--parents", "-n", "1", commitSHA).Output() if err != nil { log.Fatalf("Error getting parent SHAs: %v", err) } parts := strings.Fields(string(output)) if len(parts) > 1 { return parts[1:] // Exclude the current commit SHA } return []string{} } func getChangedFiles(parentSHA, currentSHA string) []string { output, err := exec.Command("git", "diff", "--name-only", parentSHA, currentSHA).Output() if err != nil { log.Fatalf("Error getting changed files: %v", err) } lines := strings.Split(strings.TrimSpace(string(output)), "\n") if len(lines) == 1 && lines[0] == "" { return []string{} } return lines } func filterAndProcessFiles(changedFiles map[string]struct{}, patterns []string, depth int) []string { var filtered []string for file := range changedFiles { for _, pattern := range patterns { matched, err := regexp.MatchString(pattern, file) if err != nil { log.Fatalf("Invalid regex pattern: %v", err) } if matched { processedFile := processFilePath(file, depth) filtered = append(filtered, processedFile) break // Stop checking other patterns for this file } } } return filtered } func processFilePath(filePath string, depth int) string { parts := strings.Split(filePath, string(os.PathSeparator)) if len(parts) > depth { parts = parts[:depth] } return filepath.Join(parts...) } func uniqueStrings(input []string) []string { uniqueMap := make(map[string]struct{}) for _, item := range input { uniqueMap[item] = struct{}{} } var result []string for item := range uniqueMap { result = append(result, item) } return result } func getNextVersion() string { // Ensure that git-sv is available if _, err := exec.LookPath("git-sv"); err != nil { log.Fatalf("Error: git-sv is not installed or not in PATH.") } output, err := exec.Command("git-sv", "next-version").Output() if err != nil { log.Fatalf("Error getting next version: %v", err) } nextVersion := strings.TrimSpace(string(output)) return nextVersion }