fix(source-control): resolve worktree-create fresh base from any default remote - #1730
Conversation
…ult remote `--base-ref fresh` probed `refs/remotes/origin/HEAD` and nothing else, so a clone made with `git clone -o upstream` — which has no `origin` at all — took the remoteless local-HEAD fallback even though `upstream/HEAD` was cached. A worktree created from a feature branch then carried unpushed commits into a base that `fresh` promises is the remote default branch. Resolve the effective default remote first, through a three-rung chain: the current branch's configured remote, then `origin` when it exists, then the sole remote when there is exactly one. The resolved remote's HEAD symref supplies the base; resolution stays symbolic, so no default branch name is hardcoded. Rung 1 accepts a configured remote only when it still exists, so stale config cannot shadow a healthy `origin`, and rejects git's `.` sentinel. The HEAD probe does not cascade back down the rungs: substituting a different remote's default branch is a worse failure than the caller-visible fallback. The local-HEAD fallback and its warning remain, now naming the cause. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude encountered an error after 1s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 1s —— View job I'll analyze this and get back to you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e059da050c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…tree tests The shell-portability gate scans every changed shell file in full, so touching this test file surfaced two pre-existing hits. Both are Windows path literals whose `\.claude\worktrees` tail contains a `\w` the GNU-regex token list matches — string data in a fixture, not a grep or sed construct. Annotated with the gate's own `portability-ok:` escape, matching the wording already used for the same shape in the guardrails fixtures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude encountered an error after 1s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 1s —— View job I'll analyze this and get back to you. |
…g entry The entry called the old fallback "silent". It was not: the pre-change path emitted a warning, which the issue text also states. What made it misleading was the wording of that warning, which named `origin` — the one remote a `git clone -o upstream` repository does not have — so it read as a misconfiguration rather than as the helper looking in the wrong place. The entry also scoped the bug to "any clone whose default remote is not named origin". A clone carrying both `upstream` and `origin` resolved `origin/HEAD` fine and never degraded, so the precondition is the absence of an `origin` remote, not the name of the preferred one. Same "silently" wording corrected in the matching test comment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
Claude finished @kyle-sexton's task in 2m 56s —— View job PR Review —
|
|
Claude finished @kyle-sexton's task in 2m 21s —— View job Security Review
Reviewed commit SUGGESTION — option-shaped remote name bypasses rung 1 validation silently (confidence: high)File: if [[ -n "$cfg" && "$cfg" != "." ]] && git -C "$repo_top" remote get-url "$cfg" >/dev/null 2>&1; thenGit permits remote names that begin with The practical impact is limited: the worst outcome is the wrong base commit being used for the new worktree. No code execution path exists here — Fix: add git -C "$repo_top" remote get-url -- "$cfg" >/dev/null 2>&1No other security findingsThe remainder of the changed surface is clean:
|
…emote name `resolve_default_remote` rung 1 probed the configured remote's existence with `git remote get-url "$cfg"`. That command takes options — its usage is `git remote get-url [--push] [--all] <name>` — so a name beginning with `-` is read as switches, not as a name. Such a name is legal git. `git remote add -- -foo <url>` accepts it, and `git clone -o -foo <url>` both creates the remote and writes `-foo` straight into `branch.<name>.remote`. On such a repository the probe failed with `unknown switch 'f'`, rung 1 judged a perfectly healthy remote to be missing, and resolution fell through to rung 2. With `origin` also present the fresh worktree was then based on `origin/HEAD` with no warning at all — the silently wrong base this branch exists to prevent, reached by a different route. Pass the name after `--` so the configured remote wins as documented. The two fixture helpers gain the same terminator on `git remote add`, without which an option-shaped remote is not constructible in a test at all; the change is inert for ordinary names. The new case pairs such a remote with a coexisting `origin` and three distinct tips, because with the dashed remote alone the sole-remote rung answers correctly however rung 1 behaves. Left alone deliberately: `check-ref-format --branch "$name"` parses no further options after `--branch`, so an option-shaped name already reaches it as a ref and a terminator there is a usage error instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Conflicts resolved in the source-control plugin's version surfaces. Main now carries 0.34.1 (#1731), so this branch's entry is renumbered from 0.33.3 to 0.34.2 and plugin.json follows. plugin.json's babysit_self_logins description takes main's #1731 wording, which this branch does not touch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TkLLDcB8Ht2tm3j688F5ZP
Main advanced to 0.35.0 while this branch sat at 0.34.2, so this branch's entry is renumbered to 0.35.1 and plugin.json follows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TkLLDcB8Ht2tm3j688F5ZP
|
Claude finished @kyle-sexton's task in 3m 9s —— View job PR Review —
|
|
Claude finished @kyle-sexton's task in 3m 21s —— View job Security Review —
|
Closes #904
Summary
worktree-create.sh --base-ref freshresolved the base ref by probingrefs/remotes/origin/HEADand nothing else. A repository cloned withgit clone -o upstreamhas nooriginat all, sofreshtook the remoteless local-HEADfallback even thoughupstream/HEADwas correctly cached — and a worktree created from a feature branch carried unpushed local commits into a base thatfreshpromises is the remote default branch.Fix
freshnow resolves the effective default remote before probing any symref, through the three-rung chain the issue prescribes:branch.<name>.remote)origin, when it existsThe resolved remote's
HEADsymref supplies the base. Nothing hardcodes a default branch name — resolution stays symbolic, as the portability lint requires.Details worth a reviewer's attention:
origin; and git's.sentinel (meaning "tracks a local branch", not a remote —refs/remotes/./HEADis nonsense) is refused. A detachedHEADhas no branch, so the rung is skipped rather than erroring.freshmeans the effective remote's default branch; quietly substituting a different remote's default branch is a worse failure than the fallback, because the caller cannot see it happen.HEADfallback and its loud warning remain for the genuinely unresolvable cases, and the warning now names the cause: the resolved remote whoseHEADis uncached (with thegit remote set-head <remote> --autofix), or the absence of any default remote.tr -d '\r'-trimmed. Undergit.exeon an MSYS/Cygwin shell the output carries CRLF, and an untrimmedupstream\rwould make each downstream lookup miss while still reading correctly in an error message.origin-clone case: rung 1 also applies in repos that do haveorigin. Whenbranch.<name>.remotenames a different existing remote,freshnow bases on that remote's default branch. That is the prescribed precedence; it is called out in the CHANGELOG so it is not a surprise.This closes a gap the helper shared with Claude Code's own native
fresh, which keepsorigin/HEADcurrent and falls back to localHEADwhenorigin/HEADis absent (fetched and verified while preparing this change). The helper is now deliberately more general than the native behavior in remote resolution — though not a strict superset, since it reads the cached ref rather than fetching;create.mdsays so explicitly.Test plan
plugins/source-control/scripts/worktree-create.test.sh— 128 assertions, exit 0, no failures or skips. The fixture gained--remote-nameplus helpers that seed a second remote at a distinguishable tip.New coverage: a sole non-
originremote; the uncached-<remote>/HEADwarning naming the resolved remote; branch-config precedence over a coexistingorigin; a stale branch-configured remote; the.sentinel; a detachedHEAD; several remotes with no resolvable default; the remoteless case.The new tests were checked against the pre-change helper to prove they are not vacuous. Running the updated suite against the old script (same fixtures, unmodified) produces five discriminating failures:
fresh bases on upstream/HEAD, not local HEAD— the exact bug in the issuewarning names the resolved remote's symrefwarning offers set-head for that remotebranch-configured remote wins over originambiguous remotes name the no-default-remote causeThe precedence test asserts against three distinct commits (origin tip, upstream tip, local
HEAD) so it cannot pass whichever remote is picked. Guard-only assertions (detachedHEAD, remoteless, stale/.fallthrough) pass under both scripts by design — they are regression guards, not discriminators.Other gates, all green:
shellcheck -xclean on both scripts;scripts/check-shell-portability.shintroduces no new hits (the two reported are pre-existing lines, unchanged and only shifted);scripts/check-skill-portability.shclean;scripts/check-changelog-parity.sh --checkand--check-bump origin/mainpass;scripts/validate-plugins.shpasses;markdownlint-cli2reports 0 errors.Independent fresh-context review ran with the implementation rationale withheld; every finding it raised was fixed before this PR. Two of its catches are worth naming: two new tests could have passed on a failing helper (an empty
$outdegrades the probed path to/UNPUSHED.md, which is absent) and now carry exit + positive-existence guards, and the new warning templating had no discriminating assertion at all — the case that covers it is theupstream/HEAD not set/set-head upstream --autopair above. Its shellcheck pass also caught a defect this change introduced: the added--helptext used backticks inside the unquotedusage()heredoc, which would have executedgit clone -o upstreamon every--help. Fixed;--helpnow renders the text literally.Related
parse_owner_repo's remote lookup in the same file still probesoriginonly, so in a-o upstreamclone the owner/repo directory name degrades to the repo-directory name. It is outside this issue's acceptance criteria, and changing it would move existing worktree directory names — a separate decision from the base-ref fix.<remote>/HEADsymref (present, but its target ref absent — e.g. after an upstream default-branch rename) makessymbolic-refsucceed, so git receives a non-existent ref and the helper exits 4 with a message about the branch already existing. The old code had the identical hole.