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)
|
results := git.PushMultiple(ctx, pushPaths, names)
|
||||||
|
|
||||||
var succeeded, failed int
|
var succeeded, failed int
|
||||||
|
var divergedRepos []git.PushResult
|
||||||
|
|
||||||
for _, r := range results {
|
for _, r := range results {
|
||||||
if r.Success {
|
if r.Success {
|
||||||
fmt.Printf(" %s %s\n", successStyle.Render("v"), r.Name)
|
fmt.Printf(" %s %s\n", successStyle.Render("v"), r.Name)
|
||||||
succeeded++
|
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 {
|
} else {
|
||||||
fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), r.Name, r.Error)
|
fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), r.Name, r.Error)
|
||||||
|
}
|
||||||
failed++
|
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
|
// Summary
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s", successStyle.Render(i18n.T("cmd.dev.push.done_pushed", map[string]interface{}{"Count": succeeded})))
|
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")
|
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.
|
// gitInteractive runs a git command with terminal attached for user interaction.
|
||||||
func gitInteractive(ctx context.Context, dir string, args ...string) error {
|
func gitInteractive(ctx context.Context, dir string, args ...string) error {
|
||||||
cmd := exec.CommandContext(ctx, "git", args...)
|
cmd := exec.CommandContext(ctx, "git", args...)
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,9 @@
|
||||||
"cmd.dev.push.commits_count": "{{.Count}} commit(s)",
|
"cmd.dev.push.commits_count": "{{.Count}} commit(s)",
|
||||||
"cmd.dev.push.confirm_push": "Push {{.Commits}} commit(s) to {{.Repos}} repo(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.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.all_up_to_date": "All repos up to date. Nothing to pull.",
|
||||||
"cmd.dev.pull.pulling_repos": "Pulling {{.Count}} repo(s):",
|
"cmd.dev.pull.pulling_repos": "Pulling {{.Count}} repo(s):",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue