package lib import ( "fmt" "log" "os/exec" "regexp" "strings" "sync" "time" ) var gitMutex sync.Mutex func GitAddAndCommit(dir, message string, lockMutex bool) error { if lockMutex { gitMutex.Lock() defer gitMutex.Unlock() } err := GitAdd(dir, ".", false) if err != nil { return fmt.Errorf("error adding files to git repository for dir: %s, err: %v", dir, err) } err = GitCommit(dir, message, nil, false) if err != nil { return fmt.Errorf("error committing files to git repository for dir: %s, err: %v", dir, err) } return nil } func GitAddAndCommitPaths(dir, message string, paths []string, lockMutex bool) error { if len(paths) == 0 { return nil } if lockMutex { gitMutex.Lock() defer gitMutex.Unlock() } for _, path := range paths { err := GitAdd(dir, path, false) if err != nil { return fmt.Errorf("error adding file %s to git repository for dir: %s, err: %v", path, dir, err) } } err := GitCommit(dir, message, paths, false) if err != nil { return fmt.Errorf("error committing files to git repository for dir: %s, err: %v", dir, err) } return nil } func GitAdd(repoDir, path string, lockMutex bool) error { if lockMutex { gitMutex.Lock() defer gitMutex.Unlock() } res, err := exec.Command("git", "-C", repoDir, "add", path).CombinedOutput() if err != nil { return fmt.Errorf("error adding files to git repository for dir: %s, err: %v, output: %s", repoDir, err, string(res)) } return nil } func GitCommit(repoDir, commitMsg string, paths []string, lockMutex bool) error { if lockMutex { gitMutex.Lock() defer gitMutex.Unlock() } args := []string{"-C", repoDir, "commit", "-m", commitMsg, "--allow-empty"} if len(paths) > 0 { args = append(args, paths...) } res, err := exec.Command("git", args...).CombinedOutput() if err != nil { return fmt.Errorf("error committing files to git repository for dir: %s, err: %v, output: %s", repoDir, err, string(res)) } return nil } func CheckUncommittedChanges() (bool, error) { gitMutex.Lock() defer gitMutex.Unlock() // Check if there are any changes res, err := exec.Command("git", "status", "--porcelain").CombinedOutput() if err != nil { return false, fmt.Errorf("error checking for uncommitted changes: %v, output: %s", err, string(res)) } // If there's output, there are uncommitted changes return strings.TrimSpace(string(res)) != "", nil } func GitStashCreate(message string) error { gitMutex.Lock() defer gitMutex.Unlock() res, err := exec.Command("git", "stash", "push", "--include-untracked", "-m", message).CombinedOutput() if err != nil { return fmt.Errorf("error creating git stash: %v, output: %s", err, string(res)) } return nil } // this matches output for git version 2.39.3 // need to test on other versions and check for more variations // there isn't any structured way to get stash conflicts from git, unfortunately const PopStashConflictMsg = "overwritten by merge" const ConflictMsgFilesEnd = "commit your changes" func GitStashPop(forceOverwrite bool) error { gitMutex.Lock() defer gitMutex.Unlock() res, err := exec.Command("git", "stash", "pop").CombinedOutput() // we should no longer have conflicts since we are forcing an update before // running the 'apply' command as well as resetting any files with uncommitted change // still leaving this though in case something goes wrong if err != nil { log.Println("Error popping git stash:", string(res)) if strings.Contains(string(res), PopStashConflictMsg) { log.Println("Conflicts detected") if !forceOverwrite { return fmt.Errorf("conflict popping git stash: %s", string(res)) } // Parse the output to find which files have conflicts conflictFiles := parseConflictFiles(string(res)) log.Println("Conflicting files:", conflictFiles) for _, file := range conflictFiles { // Reset each conflicting file individually checkoutRes, err := exec.Command("git", "checkout", "--ours", file).CombinedOutput() if err != nil { return fmt.Errorf("error resetting file %s: %v", file, string(checkoutRes)) } } dropRes, err := exec.Command("git", "stash", "drop").CombinedOutput() if err != nil { return fmt.Errorf("error dropping git stash: %v", string(dropRes)) } return nil } else { log.Println("No conflicts detected") return fmt.Errorf("error popping git stash: %v", string(res)) } } return nil } func GitClearUncommittedChanges() error { gitMutex.Lock() defer gitMutex.Unlock() // Reset staged changes res, err := exec.Command("git", "reset", "--hard").CombinedOutput() if err != nil { return fmt.Errorf("error resetting staged changes | err: %v, output: %s", err, string(res)) } // Clean untracked files res, err = exec.Command("git", "clean", "-d", "-f").CombinedOutput() if err != nil { return fmt.Errorf("error cleaning untracked files | err: %v, output: %s", err, string(res)) } return nil } func GitFileHasUncommittedChanges(path string) (bool, error) { gitMutex.Lock() defer gitMutex.Unlock() res, err := exec.Command("git", "status", "--porcelain", path).CombinedOutput() if err != nil { return false, fmt.Errorf("error checking for uncommitted changes for file %s | err: %v, output: %s", path, err, string(res)) } return strings.TrimSpace(string(res)) != "", nil } func GitCheckoutFile(path string) error { gitMutex.Lock() defer gitMutex.Unlock() res, err := exec.Command("git", "checkout", path).CombinedOutput() if err != nil { log.Println("Error checking out file:", string(res)) return fmt.Errorf("error checking out file %s | err: %v, output: %s", path, err, string(res)) } return nil } const GitLogTimestampFormat = "Mon Jan 2, 2006 | 3:04:05pm" var GitLogTimestampRegex = regexp.MustCompile(`\w{3} \w{3} \d{1,2}, \d{4} \| \d{1,2}:\d{2}:\d{2}(am|pm) UTC`) func GetGitLogTimestamp(log string) (time.Time, error) { matches := GitLogTimestampRegex.FindStringSubmatch(log) if len(matches) < 2 { return time.Time{}, fmt.Errorf("no timestamp found in log") } return time.Parse(GitLogTimestampFormat, strings.TrimSuffix(matches[0], " UTC")) } func parseConflictFiles(gitOutput string) []string { var conflictFiles []string lines := strings.Split(gitOutput, "\n") inFilesSection := false for _, line := range lines { if inFilesSection { file := strings.TrimSpace(line) if file == "" { continue } conflictFiles = append(conflictFiles, strings.TrimSpace(line)) } else if strings.Contains(line, PopStashConflictMsg) { inFilesSection = true } else if strings.Contains(line, ConflictMsgFilesEnd) { break } } return conflictFiles }