feat(push): handle diverged branches with pull-and-retry
When push fails due to non-fast-forward rejection (local and remote have diverged), offer to pull with rebase and retry the push instead of just failing. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
dcb0871b61
commit
58596ea00e
3 changed files with 53 additions and 1 deletions
|
|
@ -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 {
|
||||
// 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})))
|
||||
|
|
|
|||
|
|
@ -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...)
|
||||
|
|
|
|||
|
|
@ -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):",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue