Compare commits

..

1 Commits

Author SHA1 Message Date
5344bed1ea 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) <noreply@anthropic.com>
2026-03-27 21:52:41 -07:00

View File

@@ -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,13 +123,19 @@ 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 {
@@ -125,18 +146,55 @@ func syncRepo(r repo) error {
}
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)
}
}
// 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")