agentws: check out --branch from origin when it exists
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline failed

`agentws new --branch` always forked from the base branch, so a worktree for a
branch that already existed on origin started at the base's HEAD with none of
the branch's commits, and callers had to reset --hard afterwards to recover.

- Check out an existing origin/<branch> and set the branch to track it
- Fast-forward a stale local branch onto origin, keeping unpushed commits
- Fork from --from or the remote default only when origin lacks the branch
- Name which of the two paths was taken in the command's output
- Ignore --from, with a note, when the branch is already on origin
This commit is contained in:
2026-09-19 16:11:59 +10:00
parent 72adebbf8b
commit 7510187243
4 changed files with 309 additions and 10 deletions
+70 -8
View File
@@ -161,13 +161,24 @@ func newNewCmd() *cobra.Command {
return err
}
// c. Base branch: --from or the remote default.
// c. A branch origin already has is work in progress and must be
// checked out as it stands; only a branch origin does not have is
// forked from a base.
onRemote := agent.GitRemoteBranchExists(srcDir, "origin", branch)
startPoint := "origin/" + branch
base := from
if base == "" {
base, err = agent.GitRemoteDefaultBranch(srcDir, "origin")
if err != nil {
return err
if onRemote {
if from != "" {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "note: --from %s ignored, origin/%s already exists\n", from, branch)
}
} else {
if base == "" {
base, err = agent.GitRemoteDefaultBranch(srcDir, "origin")
if err != nil {
return err
}
}
startPoint = "origin/" + base
}
// d. Create the worktree FROM the source checkout so the branch is
@@ -176,7 +187,7 @@ func newNewCmd() *cobra.Command {
if _, statErr := os.Stat(wtPath); statErr == nil {
return fmt.Errorf("worktree already exists at %s", wtPath)
}
if err := agent.GitWorktreeAdd(srcDir, wtPath, branch, "origin/"+base); err != nil {
if err := agent.GitWorktreeAdd(srcDir, wtPath, branch, startPoint); err != nil {
return err
}
@@ -199,9 +210,19 @@ func newNewCmd() *cobra.Command {
return err
}
// f. Report the worktree path and branch.
// f. A local branch left from an earlier run may sit behind origin,
// so reusing it is not enough on its own.
summary := fmt.Sprintf("branch %s (new, from origin/%s)", branch, base)
if onRemote {
summary, err = alignToRemote(cmd.OutOrStdout(), wtPath, branch)
if err != nil {
return err
}
}
// g. Report the worktree path and which of the two paths was taken.
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s\n", wtPath)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "branch %s (from origin/%s)\n", branch, base)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s\n", summary)
return nil
},
}
@@ -212,6 +233,47 @@ func newNewCmd() *cobra.Command {
return cmd
}
// alignToRemote puts the worktree on origin/<branch> and reports what that took.
// A reused local branch can be stale, and a fast-forward is the only move that
// adds no commit and drops none; a local branch carrying commits origin does not
// have is left where it stands, because those commits exist nowhere else.
func alignToRemote(out io.Writer, wtPath, branch string) (string, error) {
remoteRef := "origin/" + branch
want, err := agent.GitRevParse(wtPath, remoteRef)
if err != nil {
return "", err
}
head, err := agent.GitRevParse(wtPath, "HEAD")
if err != nil {
return "", err
}
if head != want {
ahead, err := agent.GitAheadCount(wtPath, remoteRef, "HEAD")
if err != nil {
return "", err
}
if ahead > 0 {
return fmt.Sprintf("branch %s (local, %s not on %s, left at %s)",
branch, commitCount(ahead), remoteRef, shortSHA(head)), nil
}
if err := agent.GitMergeFFOnly(wtPath, remoteRef); err != nil {
return "", err
}
_, _ = fmt.Fprintf(out, "fast-forwarded stale %s to %s\n", branch, remoteRef)
}
if err := agent.GitSetUpstream(wtPath, branch, remoteRef); err != nil {
return "", err
}
return fmt.Sprintf("branch %s (tracking %s at %s)", branch, remoteRef, shortSHA(want)), nil
}
func shortSHA(sha string) string {
if len(sha) > 7 {
return sha[:7]
}
return sha
}
// --- list -----------------------------------------------------------------
func newListCmd() *cobra.Command {