fix(source-control): resolve remote dynamically in pull-request create flow (#442) - #763
Conversation
…e flow (#442) The /pull-request create.md reference hardcoded the remote name `origin` in `git fetch origin` (§2.2 rebase) and `git push -u origin <branch>` (§2.4.1), breaking any consumer whose remote is not named `origin` (`git clone -o <name>`, fork-based multi-remote setups) — a baked repo assumption the convention-resolution ladder forbids. Both sites now resolve the remote with the same candidate-priority idiom the toolchain linters use: the current branch's configured remote (branch.<name>.remote, a local-only `.` upstream treated as unset), else `origin`, else the sole configured remote. Every `origin/$DEFAULT_BRANCH` occurrence in §2.2 (fetch, merge-base, rev-parse, rev-list, rebase, the progress echo, and the merge-vs-rebase / skip-condition prose) now reads `$REMOTE/$DEFAULT_BRANCH`, and ORIGIN_DEFAULT is renamed REMOTE_DEFAULT. On the common path (single remote, or a fresh branch with no upstream yet) both sites still resolve to `origin`, preserving current behavior exactly. Co-authored-by: Claude <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 31s —— View job Security Review
Scope: Shell injection — §2.2 and §2.4.1 remote-resolution blocksBoth new blocks use git config "branch.$(git branch --show-current | tr -d '\r').remote"This is safe. Bash Remote trust model —
|
|
Claude finished @kyle-sexton's task in 2m 15s —— View job PR Review: fix(source-control): resolve remote dynamically in pull-request create flow (#442)Tasks
This is a clean, focused doc-only fix that correctly addresses issue #442. The version bump ( Finding 1 — §2.2 and §2.4.1 use inconsistent fallback logic (narrow edge case)The two resolution sites use different strategies when
In a repo cloned with The LLM executing these in sequence would successfully rebase in §2.2 and then fail at the push in §2.4.1. The PR description states "both sites now resolve the remote with the same candidate-priority idiom" — that's accurate for the happy path and for repos with an The
See §2.2 resolution · See §2.4.1 resolution Finding 2 — CHANGELOG overstate: "same candidate-priority idiom the toolchain linters already use"The priority order (branch-remote → origin → sole remote) matches the toolchain, but the toolchain idiom is meaningfully different: # Toolchain (check/lint SKILL.md lines 60-72):
# Builds a deduplicated candidate list, then loops and validates each
# with git rev-parse --verify --quiet "refs/remotes/$CANDIDATE/$BRANCH"
# before accepting — skips remotes never fetched locally.
CANDIDATES=$( { [[ -n "$TRACKED" ]] && echo "$TRACKED"; git remote | grep -qx origin && echo origin; git remote; } | awk 'NF && !seen[$0]++' )
while IFS= read -r CANDIDATE; do
...
if [[ -n "$BRANCH" ]] && git rev-parse --verify --quiet "refs/remotes/$CANDIDATE/$BRANCH" >/dev/null; then
REMOTE=$CANDIDATE DEFAULT_BRANCH=$BRANCH; break
fi
done <<< "$CANDIDATES"The PR's §2.2 doesn't validate that the resolved remote has a locally cached tracking ref — it just picks the first viable name and calls Everything else looks good
Finding 1 is the only functional concern; Finding 2 is a documentation accuracy note. Neither blocks merge if the team is comfortable with the documented limitation on triangular setups (which the PR explicitly calls out of scope). |
|
Claude finished @kyle-sexton's task in 1m 51s —— View job PR Review: fix(source-control): resolve remote dynamically in pull-request create flow (#442)Tasks
Scope: The fix correctly addresses the hardcoded Finding 1 — §2.2 and §2.4.1 use asymmetric fallback chains (functional gap)The two resolution blocks diverge in the last fallback:
In a repo cloned with
The rebase in §2.2 would silently succeed, then the push would fail with a confusing The PR description states "both sites now resolve the remote with the same candidate-priority idiom" — that claim is only true on the happy path (repos that have PUSH_REMOTE=$(git config "branch.$(git branch --show-current | tr -d '\r').remote" 2>/dev/null | tr -d '\r')
[[ "$PUSH_REMOTE" == "." ]] && PUSH_REMOTE=""
if [[ -z "$PUSH_REMOTE" ]]; then
if git remote | grep -qx origin; then PUSH_REMOTE=origin; else PUSH_REMOTE=$(git remote | head -1); fi
fiFinding 2 —
|
|
Claude finished @kyle-sexton's task in 1m 33s —— View job Security Review — PR #763
Scope: Shell injection —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 466bdf3468
ℹ️ 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".
…with push path Bot review on PR #763 found two gaps in the remote-agnostic create flow: the sole-configured-remote fallback picked `git remote | head -1` when 2+ non-origin remotes existed and no origin/branch.<name>.remote was set, silently rebasing against an arbitrary remote instead of failing loudly like the hardcoded-origin code it replaced; and the §2.4.1 push step still hardcoded `origin`, so a non-origin single-remote clone (`git clone -o vendor`) could rebase but not push. Extract the resolution logic into a shared scripts/resolve-remote.sh (same pattern as scripts/parse-branch-issue.sh) used by both §2.2 (rebase) and §2.4.1 (push): branch.<name>.remote, else origin, else the sole other configured remote — fails loudly with a diagnostic when 2+ non-origin candidates exist and neither is set. Co-Authored-By: Claude Opus <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 17s —— View job PR Review: fix(source-control): resolve remote dynamically in pull-request create flow (#442)Tasks
Scope: The a78661d follow-up commit correctly addresses Finding 1 from earlier reviews — the asymmetric fallback between §2.2 and §2.4.1 — by extracting a shared Finding 1 — CHANGELOG entry doesn't reflect a78661d's behavioral changes (doc accuracy)The CHANGELOG entry was written to describe commit 466bdf3's behavior. Commit a78661d made two meaningful changes that the CHANGELOG doesn't capture: 1. Resolution is now delegated to a shared script. The CHANGELOG says "Both sites now resolve the remote with the same candidate-priority idiom" without mentioning that the logic was extracted to 2. The last-resort fallback behavior changed. The CHANGELOG says:
The actual implementation in elif [[ ${#REMOTES[@]} -eq 1 ]]; then
REMOTE="${REMOTES[0]}"
elif [[ ${#REMOTES[@]} -eq 0 ]]; then
# ... exit 1
else
echo "error: cannot resolve a remote for branch '${BRANCH}': no branch.${BRANCH}.remote, no 'origin', and ${#REMOTES[@]} other remotes exist ..." >&2
exit 1
fiWhen 2+ non-origin remotes exist with neither Suggested addition to the existing CHANGELOG entry (or an additional bullet under
See CHANGELOG L15 · See resolve-remote.sh L36-L44 Finding 2 — CHANGELOG wording overstates parity with toolchain idiom (minor, previously flagged)Still present: "the same candidate-priority idiom the Everything else is solid
Shell injection: plugin.json: Version bump Finding 1 (CHANGELOG stale on a78661d's changes) is a straightforward documentation fix — the |
|
Claude finished @kyle-sexton's task in 3m 31s —— View job Security Review — PR #763
Scope: Shell injection —
|
|
Claude finished @kyle-sexton's task in 2m 32s —— View job PR Review: fix(source-control): resolve remote dynamically in pull-request create flow (#442)Tasks
Scope: The a78661d follow-up addressed both Codex P2 findings — the asymmetric Finding 1 — CHANGELOG entry is stale after a78661d (doc accuracy)The 1a. Resolution is now delegated to a shared script, which isn't mentioned. 1b. The last-resort fallback behavior changed in a way the entry contradicts.
The actual implementation in elif [[ ${#REMOTES[@]} -eq 1 ]]; then
REMOTE="${REMOTES[0]}"
elif [[ ${#REMOTES[@]} -eq 0 ]]; then
echo "error: cannot resolve a remote ... no remotes are configured." >&2; exit 1
else
echo "error: cannot resolve a remote ... ${#REMOTES[@]} other remotes exist ..." >&2; exit 1
fiWith 2+ non- The script header comment at lines 8–14 already has the right wording and can be adapted directly: Resolution logic is extracted to `scripts/resolve-remote.sh` (shared by §2.2 and §2.4.1).
The last-resort fallback — when no `branch.<name>.remote` and no `origin` are configured —
applies only when exactly one other remote exists; two or more candidates without a
`branch.<name>.remote` or `origin` now exit 1 with a diagnostic listing the candidates,
rather than silently resolving to `git remote | head -1`.Finding 2 — CHANGELOG wording overstates parity with toolchain idiom (minor)
The priority order matches, but the toolchain idiom validates each candidate against a locally-cached tracking ref before accepting: git rev-parse --verify --quiet "refs/remotes/$CANDIDATE/$BRANCH"
Finding 3 —
|
Post-green review classification (rule 8)
Both repro cases verified directly (fork+upstream-no-origin fails loudly; vendor-only clone resolves `vendor` for both steps). New test file `resolve-remote.test.sh` (7/7) plus all other source-control tests pass. Both threads resolved. Flipping to ready and handing off to babysit — no further re-polling from this lane. |
|
Claude finished @kyle-sexton's task in 2m 57s —— View job Security Review — PR #763
Scope: Shell injection —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a78661d15c
ℹ️ 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".
… fail-loud fallback The [0.15.4] entry was authored against 466bdf3 and never updated for a78661d, so it misdescribed the behavior it documents: it said the last fallback is "the sole configured remote" unconditionally and did not mention the shared scripts/resolve-remote.sh extraction. That omitted the fail-loud-on-ambiguity guard that is the change's core safety property — 2+ non-origin candidates with neither branch.<name>.remote nor origin now exit 1 with a diagnostic instead of silently resolving via head -1. Also soften "idiom" to "ordering": the resolver shares the toolchain's candidate priority, not its per-candidate tracking-ref validation, so "idiom" overstated the structural parity. Co-Authored-By: Claude Opus <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 0s —— View job PR Review: fix(source-control): resolve remote dynamically in pull-request create flow (#442)Tasks
Scope: Reviewing at HEAD Prior-round findings — resolution status
Finding 1 —
|
|
Claude finished @kyle-sexton's task in 2m 52s —— View job Security Review — PR #763Tasks
Scope: Shell injection —
|
Babysit classification — PR Review findings (commit a78661d → 9a9f3bb)
Verified independently: Separately, a new codex P2 (push-precedence in triangular fork setups) is reproduced and left as an open blocker — see the inline thread on the §2.4.1 push line. Not marking ready this round. |
…erve triangular fetch remote (#442) Bot review on PR #763 (reproduced) found the shared resolver keyed only off branch.<name>.remote for BOTH fetch and push, so §2.4.1's push reused the fetch resolver. In a triangular fork setup — `git checkout -b feature upstream/main` sets branch.feature.remote=upstream, with pushRemote/pushDefault pointing at the fork — the push published the branch to `upstream` instead of the fork. Git's documented push precedence (git-config(1), git-push(1)) is branch.<name>.pushRemote -> remote.pushDefault -> branch.<name>.remote, none of whose first two the resolver honored. Add a --push mode to scripts/resolve-remote.sh that prepends pushRemote / pushDefault ahead of the shared fetch order (branch.<name>.remote -> origin -> sole other), treating a "." value as local/unset. §2.4.1 now calls the resolver with --push; §2.2 (fetch/rebase) is unchanged. The ambiguous-fallback error is push-aware. Push-precedence cases covered in resolve-remote.test.sh (15 tests). Also make §2.4.1's `git push -u` conditional: `git push -u` rewrites branch.<name>.remote to the push target, so an unconditional -u on a triangular fork would silently repoint the FETCH remote §2.2 reads to the fork and break the next rebase. Bootstrap tracking with -u only when the branch has no real branch.<name>.remote yet; otherwise push without -u to preserve it. Corrects the now-false "triangular fork out of scope" note in §2.2 and the [0.15.4] CHANGELOG. Co-Authored-By: Claude Opus <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 39s —— View job Security Review — PR #763 (HEAD: f890d5b)
Scope: Reviewing changes introduced in the HEAD commit Shell injection —
|
|
Claude finished @kyle-sexton's task in 3m 45s —— View job PR Review: fix(source-control): resolve remote dynamically in pull-request create flow (#442)Tasks
Scope: Reviewing at HEAD Prior-round findings — resolution status
Finding 1 —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f890d5ba8f
ℹ️ 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".
| fi | ||
|
|
||
| if [[ -z "$REMOTE" ]]; then | ||
| mapfile -t REMOTES < <(git remote) |
There was a problem hiding this comment.
Avoid Bash 4-only mapfile in remote resolver
In environments that run the plugin with macOS's stock Bash 3.2 (or any Bash 3), this mapfile builtin is unavailable, while source-control's documented runtime only requires “Bash”/Git Bash and does not declare Bash 4+. This path is hit for the common fresh-branch case where branch.<name>.remote is unset, so /pull-request create fails during remote resolution before the fetch/push that previously worked with hardcoded origin. Use a Bash-3-compatible while read loop or explicitly declare and enforce Bash 4+ for this plugin.
Useful? React with 👍 / 👎.
…e match (#442) (#783) No linked issue — fix-forward of merged #763; the original issue #442 is already closed, and this residual was caught by post-merge review rather than filed. ## Summary Fix-forward on just-merged **#763** (source-control `/pull-request` create flow, issue **#442**). An independent post-merge review empirically reproduced a **residual silent clobber** that #763 left behind. **The residual finding.** create.md §2.4.1's conditional `-u` gate keyed on the *literal* `branch.<name>.remote` config being set. In the `remote.pushDefault`-only triangular shape — `pushDefault=fork` globally, `branch.<name>.remote` unset, fetch falling back to `origin` — the gate read "unset", took the `-u` bootstrap path, and `git push -u <fork>` rewrote `branch.<name>.remote` to the fork. The next fetch/rebase then silently targeted the fork instead of `origin`. **Reproduced before and after** (two bare remotes, `remote.pushDefault=fork`, `branch.<name>.remote` unset): | Gate | `branch.<name>.remote` after push | next fetch resolves | |------|-----------------------------------|---------------------| | old (merged in #763) | `fork` (clobbered) | **fork** — wrong base | | this fix | unset (untouched) | `origin` — correct | **Root-cause fix.** The `-u` conditional moved out of the markdown prose into a new co-located `scripts/push-branch.sh` (§2.4.1 now delegates to it), so the resolve-fetch → resolve-push → conditional-push sequence is executable and testable rather than living only in a reference doc. The gate fires `-u` only when the branch has **no existing upstream** *and* its fetch and push remotes resolve to the same name; otherwise it pushes plain and writes no branch config. The normalized `.`-as-unset / `\r`-strip handling stays solely in `resolve-remote.sh` — the literal-unset probe reads the key raw (`.` is non-empty → "has an upstream"). **Divergence from the review's literal prescription (disclosed).** The review prescribed gating purely on resolved-remote equality (`-u` iff fetch == push). An independent Codex review of this change found that `git push -u` rewrites the branch's **whole** upstream — both `branch.<name>.remote` **and** `branch.<name>.merge` — so the pure-resolved gate still clobbered the merge ref of an already-tracked branch, and of a deliberate local-only `.` upstream (`git branch --track . <ref>`), whenever the resolved *names* happened to match. Requiring the upstream to be **absent** before bootstrapping (`unset AND resolved-equal`) closes that second corruption while still fixing the reported `pushDefault`-only case. This also improves #763's `.` handling: publishing a branch for a PR no longer mutates a deliberate local-only upstream. (Per the DEFER-FORBIDDEN carve-out, both Codex findings were folded in inline, not deferred.) ## Test plan - New `push-branch.test.sh` drives **real** bare-remote pushes across: pushRemote-triangular, `pushDefault`-only triangular, non-triangular (asserting the merge ref survives), fresh-branch bootstrap (asserting `-u` fired), local-only `.`, and fetch-ambiguous/push-determinate. **18/18 pass.** - **Non-vacuity proven both directions:** the suite fails **5** cases against the old literal-probe gate and **3** cases against the pure-resolved gate it replaces — so it pins the refined invariant, not merely the original bug. (This integration layer is exactly what `resolve-remote.test.sh`'s resolver-only cases could not catch, which is why the clobber escaped #763.) - `resolve-remote.test.sh` 15/15 (unchanged resolver), changelog parity (`--check` + `--check-bump`), markdownlint, shellcheck, shfmt — all green. ## Merge posture Fix-forward proceeding under **operator momentum**: the change is verified and ready, but the agent will **not** merge it. The merge decision is delegated to the operator. A **veto window** is open — object here before merge if the `unset AND resolved-equal` refinement (vs the review's pure-resolved prescription) warrants discussion; otherwise it may be merged. ## Related - Refs #442 — original low-severity finding (closed by #763); this fix-forward addresses the residual clobber that survived it. - #763 — the PR this follows forward; introduced the conditional `-u` gate whose literal-probe form this replaces. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
/pull-request'screate.mdreference hardcoded the remote nameoriginin the §2.2 rebase(
git fetch origin,origin/$DEFAULT_BRANCH) and the §2.4.1 push (git push -u origin <branch>),so any consumer whose remote is not named
origin— a repo cloned withgit clone -o <name>, or afork-based multi-remote setup — would break. That is a baked repo assumption the convention-resolution
ladder forbids (issue #442, work-readiness sweep vs
docs/PLUGIN-PHILOSOPHY.md).Fix
Both sites now resolve the remote with the same candidate-priority idiom the
toolchainlinters use:the current branch's configured remote (
branch.<name>.remote, a local-only.upstream treated asunset), else
origin, else the sole configured remote. In §2.2 everyorigin/$DEFAULT_BRANCHoccurrence (fetch,
merge-base,rev-parse,rev-list,rebase, the progress echo, and themerge-vs-rebase / skip-condition prose) now reads
$REMOTE/$DEFAULT_BRANCH, andORIGIN_DEFAULTisrenamed
REMOTE_DEFAULT. On the common path — a single-remote repo, or a fresh feature branch with nobranch.<name>.remoteyet — both sites still resolve toorigin, so current behavior is preservedexactly. Doc-only change to
create.md(no scripts touched);plugin.jsonbumped to0.15.4with amatching CHANGELOG entry.
Verification
origin/main(0.15.3); resolved theplugin.json+ CHANGELOG versionconflicts, re-bumping to
0.15.4from main's head rather than the stale branch-point value.create.mdauto-merged cleanly against main and correctly integrates the source-control: PR-body "Generated with Claude Code" attribution is unconditional — no trailer_policy-equivalent seam #439 attribution line.scripts/check-changelog-parity.sh --checkand--check-bump origin/main: green.plugin.jsonparses and reportsversion 0.15.4.#NNNissue-number references in the addedcreate.mdcode comments (comment-hygiene gate).plugins/source-control/before opening.this PR.
Closes #442
Related
originhardcoding still lives in thepull-requestmerge.mdand thebabysit-prsreferences; deferred to a follow-up (noted in the CHANGELOG entry). Triangular fork flows that fetch
a separate
upstreamwhile pushing to a fork remain out of scope —branch.<name>.remotetracksthe push remote, not upstream.