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>
This commit is contained in:
@@ -92,11 +92,26 @@ func isGitRepo(dir string) bool {
|
|||||||
|
|
||||||
// syncRepo performs the full sync sequence on a single repository:
|
// 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 {
|
func syncRepo(r repo) error {
|
||||||
fmt.Printf(" %s (%s)\n", r.name, r.path)
|
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 {
|
if err := git(r.path, "fetch", "--all", "--prune"); err != nil {
|
||||||
return fmt.Errorf("fetch: %w", err)
|
return fmt.Errorf("fetch: %w", err)
|
||||||
}
|
}
|
||||||
@@ -108,13 +123,19 @@ func syncRepo(r repo) error {
|
|||||||
}
|
}
|
||||||
if dirty {
|
if dirty {
|
||||||
fmt.Println(" stashing uncommitted changes")
|
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)
|
return fmt.Errorf("stash push: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Pull with rebase.
|
// 3. Pull --rebase from each remote.
|
||||||
pullErr := git(r.path, "pull", "--rebase")
|
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).
|
// 4. Pop stash regardless of pull outcome (best effort to restore state).
|
||||||
if dirty {
|
if dirty {
|
||||||
@@ -125,18 +146,55 @@ func syncRepo(r repo) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if pullErr != nil {
|
if pullErr != nil {
|
||||||
return fmt.Errorf("pull: %w", pullErr)
|
return pullErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Push.
|
// 5. Push to each remote.
|
||||||
if err := git(r.path, "push"); err != nil {
|
for _, rem := range rems {
|
||||||
return fmt.Errorf("push: %w", err)
|
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")
|
fmt.Println(" synced")
|
||||||
return nil
|
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.
|
// isDirty returns true if the working tree or index has uncommitted changes.
|
||||||
func isDirty(dir string) (bool, error) {
|
func isDirty(dir string) (bool, error) {
|
||||||
cmd := exec.Command("git", "status", "--porcelain")
|
cmd := exec.Command("git", "status", "--porcelain")
|
||||||
|
|||||||
Reference in New Issue
Block a user