From 5344bed1ea6583db7f3f7efd8f995a51bd90a3e3 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Fri, 27 Mar 2026 21:52:41 -0700 Subject: [PATCH] git-sync: sync all remotes and push tags Pull, push, and push tags are now performed against every configured remote rather than just the default. Operations are grouped by type so all pulls complete before any push begins. Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/git-sync/main.go | 80 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 11 deletions(-) diff --git a/cmd/git-sync/main.go b/cmd/git-sync/main.go index cbd6a99..244db07 100644 --- a/cmd/git-sync/main.go +++ b/cmd/git-sync/main.go @@ -92,11 +92,26 @@ func isGitRepo(dir string) bool { // syncRepo performs the full sync sequence on a single repository: // -// fetch --prune stash (if dirty) pull --rebase stash pop push +// fetch --all --prune → stash (if dirty) → pull --rebase each remote → +// stash pop → push each remote → push tags each remote func syncRepo(r repo) error { fmt.Printf(" %s (%s)\n", r.name, r.path) - // 1. Fetch and prune. + rems, err := remotes(r.path) + if err != nil { + return fmt.Errorf("listing remotes: %w", err) + } + if len(rems) == 0 { + fmt.Println(" no remotes configured, skipping") + return nil + } + + branch, err := currentBranch(r.path) + if err != nil { + return fmt.Errorf("determining current branch: %w", err) + } + + // 1. Fetch all remotes. if err := git(r.path, "fetch", "--all", "--prune"); err != nil { return fmt.Errorf("fetch: %w", err) } @@ -108,35 +123,78 @@ func syncRepo(r repo) error { } if dirty { fmt.Println(" stashing uncommitted changes") - if err := git(r.path, "stash", "push", "-m", "sync-repos auto-stash"); err != nil { + if err := git(r.path, "stash", "push", "-m", "git-sync auto-stash"); err != nil { return fmt.Errorf("stash push: %w", err) } } - // 3. Pull with rebase. - pullErr := git(r.path, "pull", "--rebase") + // 3. Pull --rebase from each remote. + var pullErr error + for _, rem := range rems { + if err := git(r.path, "pull", "--rebase", rem, branch); err != nil { + fmt.Printf(" pull from %s failed: %v\n", rem, err) + pullErr = fmt.Errorf("pull from %s: %w", rem, err) + } + } // 4. Pop stash regardless of pull outcome (best effort to restore state). if dirty { fmt.Println(" restoring stashed changes") if popErr := git(r.path, "stash", "pop"); popErr != nil { - fmt.Printf(" stash pop failed: %v (changes remain in stash)\n", popErr) + fmt.Printf(" stash pop failed: %v (changes remain in stash)\n", popErr) } } if pullErr != nil { - return fmt.Errorf("pull: %w", pullErr) + return pullErr } - // 5. Push. - if err := git(r.path, "push"); err != nil { - return fmt.Errorf("push: %w", err) + // 5. Push to each remote. + for _, rem := range rems { + if err := git(r.path, "push", rem); err != nil { + return fmt.Errorf("push to %s: %w", rem, err) + } } - fmt.Println(" synced") + // 6. Push tags to each remote. + for _, rem := range rems { + if err := git(r.path, "push", "--tags", rem); err != nil { + return fmt.Errorf("push tags to %s: %w", rem, err) + } + } + + fmt.Println(" synced") return nil } +// remotes returns the list of git remote names for a repository. +func remotes(dir string) ([]string, error) { + cmd := exec.Command("git", "remote") + cmd.Dir = dir + out, err := cmd.Output() + if err != nil { + return nil, err + } + var names []string + for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { + if line != "" { + names = append(names, line) + } + } + return names, nil +} + +// currentBranch returns the current branch name. +func currentBranch(dir string) (string, error) { + cmd := exec.Command("git", "symbolic-ref", "--short", "HEAD") + cmd.Dir = dir + out, err := cmd.Output() + if err != nil { + return "", err + } + return strings.TrimSpace(string(out)), nil +} + // isDirty returns true if the working tree or index has uncommitted changes. func isDirty(dir string) (bool, error) { cmd := exec.Command("git", "status", "--porcelain")