package main import ( "context" "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "time" ) type RepoResult struct { Name string Path string URL string Pending bool UpToDate bool Offline bool NeedsClone bool Error string NewCommits int LocalChanges int } func runGit(args []string, cwd string, timeout time.Duration) (code int, stdout string, stderr string) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() fullArgs := append([]string{"-c", "safe.directory=*"}, args...) cmd := newGitCmd(ctx, fullArgs, cwd) var outBuf, errBuf strings.Builder cmd.Stdout = &outBuf cmd.Stderr = &errBuf err := cmd.Run() stdout = strings.TrimSpace(outBuf.String()) stderr = strings.TrimSpace(errBuf.String()) if err != nil { if ctx.Err() == context.DeadlineExceeded { return 1, stdout, "Timeout" } var exitErr *exec.ExitError if errors.As(err, &exitErr) { return exitErr.ExitCode(), stdout, stderr } return -1, stdout, err.Error() } return 0, stdout, stderr } func absRepoPath(rel string) string { if filepath.IsAbs(rel) { return rel } return filepath.Join(exeDir(), rel) } func checkRepo(cfg RepoConfig) RepoResult { res := RepoResult{Name: cfg.Name, URL: cfg.URL} local := absRepoPath(cfg.Path) res.Path = local if _, err := os.Stat(filepath.Join(local, ".git")); os.IsNotExist(err) { res.NeedsClone = true return res } runGit([]string{"remote", "set-url", "origin", cfg.URL}, local, 10*time.Second) code, _, stderr := runGit([]string{"fetch", "origin"}, local, 60*time.Second) if code != 0 { for _, kw := range []string{"could not resolve", "connection refused", "unable to connect", "timed out", "the remote end hung up"} { if strings.Contains(strings.ToLower(stderr), kw) { res.Offline = true res.Error = "Hors ligne" return res } } res.Error = "Fetch: " + stderr return res } _, branch, _ := runGit([]string{"rev-parse", "--abbrev-ref", "HEAD"}, local, 5*time.Second) if branch == "" { branch = "master" } _, countStr, _ := runGit([]string{"rev-list", "--count", "HEAD..origin/" + branch}, local, 5*time.Second) fmt.Sscanf(countStr, "%d", &res.NewCommits) _, status, _ := runGit([]string{"status", "--porcelain"}, local, 5*time.Second) if status != "" { res.LocalChanges = len(strings.Split(strings.TrimSpace(status), "\n")) } res.UpToDate = res.NewCommits == 0 && res.LocalChanges == 0 return res } func doClone(cfg RepoConfig) error { local := absRepoPath(cfg.Path) if err := os.MkdirAll(filepath.Dir(local), 0755); err != nil { return err } code, _, stderr := runGit([]string{"clone", cfg.URL, local}, "", 300*time.Second) if code != 0 { return fmt.Errorf("%s", stderr) } return nil } func doPull(res RepoResult) error { _, branch, _ := runGit([]string{"rev-parse", "--abbrev-ref", "HEAD"}, res.Path, 5*time.Second) if branch == "" { branch = "master" } code, _, stderr := runGit([]string{"pull", "origin", branch}, res.Path, 120*time.Second) if code != 0 { return fmt.Errorf("%s", stderr) } return nil } func doCheckout(res RepoResult) error { code, _, stderr := runGit([]string{"checkout", "--", "."}, res.Path, 30*time.Second) if code != 0 { return fmt.Errorf("%s", stderr) } return nil }