diff --git a/cmd/dev/dev_push.go b/cmd/dev/dev_push.go index b079da9..6a401c9 100644 --- a/cmd/dev/dev_push.go +++ b/cmd/dev/dev_push.go @@ -133,16 +133,48 @@ func runPush(registryPath string, force bool) error { results := git.PushMultiple(ctx, pushPaths, names) var succeeded, failed int + var divergedRepos []git.PushResult + for _, r := range results { if r.Success { fmt.Printf(" %s %s\n", successStyle.Render("v"), r.Name) succeeded++ } else { - fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), r.Name, r.Error) + // Check if this is a non-fast-forward error (diverged branch) + if git.IsNonFastForward(r.Error) { + fmt.Printf(" %s %s: %s\n", warningStyle.Render("!"), r.Name, i18n.T("cmd.dev.push.diverged")) + divergedRepos = append(divergedRepos, r) + } else { + fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), r.Name, r.Error) + } failed++ } } + // Handle diverged repos - offer to pull and retry + if len(divergedRepos) > 0 { + fmt.Println() + fmt.Printf("%s\n", i18n.T("cmd.dev.push.diverged_help")) + if shared.Confirm(i18n.T("cmd.dev.push.pull_and_retry")) { + fmt.Println() + for _, r := range divergedRepos { + fmt.Printf(" %s %s...\n", dimStyle.Render("↓"), r.Name) + if err := git.Pull(ctx, r.Path); err != nil { + fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), r.Name, err) + continue + } + fmt.Printf(" %s %s...\n", dimStyle.Render("↑"), r.Name) + if err := git.Push(ctx, r.Path); err != nil { + fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), r.Name, err) + continue + } + fmt.Printf(" %s %s\n", successStyle.Render("v"), r.Name) + succeeded++ + failed-- + } + } + } + // Summary fmt.Println() fmt.Printf("%s", successStyle.Render(i18n.T("cmd.dev.push.done_pushed", map[string]interface{}{"Count": succeeded}))) diff --git a/pkg/git/git.go b/pkg/git/git.go index 92ae4ad..0081737 100644 --- a/pkg/git/git.go +++ b/pkg/git/git.go @@ -146,6 +146,23 @@ func Push(ctx context.Context, path string) error { return gitInteractive(ctx, path, "push") } +// Pull pulls changes for a single repository. +// Uses interactive mode to support SSH passphrase prompts. +func Pull(ctx context.Context, path string) error { + return gitInteractive(ctx, path, "pull", "--rebase") +} + +// IsNonFastForward checks if an error is a non-fast-forward rejection. +func IsNonFastForward(err error) bool { + if err == nil { + return false + } + msg := err.Error() + return strings.Contains(msg, "non-fast-forward") || + strings.Contains(msg, "fetch first") || + strings.Contains(msg, "tip of your current branch is behind") +} + // gitInteractive runs a git command with terminal attached for user interaction. func gitInteractive(ctx context.Context, dir string, args ...string) error { cmd := exec.CommandContext(ctx, "git", args...) diff --git a/pkg/i18n/locales/en_AU.json b/pkg/i18n/locales/en_AU.json index bcefb8b..b4ae017 100644 --- a/pkg/i18n/locales/en_AU.json +++ b/pkg/i18n/locales/en_AU.json @@ -187,6 +187,9 @@ "cmd.dev.push.commits_count": "{{.Count}} commit(s)", "cmd.dev.push.confirm_push": "Push {{.Commits}} commit(s) to {{.Repos}} repo(s)?", "cmd.dev.push.done_pushed": "Done: {{.Count}} pushed", + "cmd.dev.push.diverged": "branch has diverged from remote", + "cmd.dev.push.diverged_help": "Some repos have diverged (local and remote have different commits).", + "cmd.dev.push.pull_and_retry": "Pull changes and retry push?", "cmd.dev.pull.all_up_to_date": "All repos up to date. Nothing to pull.", "cmd.dev.pull.pulling_repos": "Pulling {{.Count}} repo(s):",