Skip to content

fix(worktree): create branches with --no-track and auto-setup remote#1563

Merged
brennanb2025 merged 11 commits into
mainfrom
brennanb2025/fix-worktree-no-track
May 11, 2026
Merged

fix(worktree): create branches with --no-track and auto-setup remote#1563
brennanb2025 merged 11 commits into
mainfrom
brennanb2025/fix-worktree-no-track

Conversation

@brennanb2025
Copy link
Copy Markdown
Contributor

@brennanb2025 brennanb2025 commented May 8, 2026

Summary

  • New worktree branches are now created with git worktree add --no-track, so an unpublished branch isn't reported as "behind by N" against its base ref by git status or by tools/agents that read upstream state.
  • After creating the worktree, set push.autoSetupRemote=true (locally, only when not already set at any scope) so a plain git push from the terminal creates origin/<branch> and sets the upstream automatically — matching modern-git ergonomics without forcing -u.
  • Probe with git config --get before writing, and only treat exit code 1 ("key unset") as a green light to write. Any other read failure (corrupt config, locked file, parse error) re-throws to the warn handler so we don't silently overwrite a deliberate user value.
  • Config write failures are warn-only and the warning includes the worktree path; old git clients (<2.37) just fall back to the pre-2.37 "no upstream" error and git push -u once.
  • SSH-relay parity: the same --no-track + probe-and-write state machine now runs on the relay side (in addWorktreeOp in src/relay/git-handler-worktree-ops.ts, called from GitHandler.addWorktree in src/relay/git-handler.ts), so SSH-mounted repos get the same git status / git push UX as local repos. The optional track field is removed from the provider interface and the SSH IPC payload (one in-tree caller updated).
  • Tests cover: default unset path writes true, existing value is preserved (including blank-string values), write failure warns without throwing, a non-unset read error is treated as a real failure (no --local set), and the config block is skipped entirely when worktree add itself fails. Mirror coverage exists for both the local and relay handlers.

Verification

Unit tests: all passing.

  • src/main/git/worktree.test.ts (21 tests) — local addWorktree state machine.
  • src/main/git/remove-worktree.test.ts (11 tests) — removal path.
  • src/relay/git-handler.test.ts (29 tests) — relay handler, including 6 new addWorktree cases mirroring the local matrix (--no-track arg ordering, write-when-unset, preserve-existing, empty-stdout = "already set", non-unset read error short-circuit, write-failure warn-only, no-config-on-worktree-add-fail).
  • src/main/providers/ssh-git-provider.test.ts (20 tests) — SSH provider IPC payload (already omits track).

Combined run: 81/81 passing post-merge with main.

Live Electron verification — pre-merge (Orca dev build, repo test-repos/my-git-proj with push.autoSetupRemote initially unset at every scope):

Scenario Expected Result
Create worktree, autoSetupRemote unset New branch has no upstream; git status clean (no "behind"); push.autoSetupRemote=true written locally All three confirmed
Branch config after creation No branch.<name>.remote / branch.<name>.merge keys (i.e. --no-track took effect) Confirmed (git config --get-regexp empty)
Create worktree with push.autoSetupRemote=false preset locally User value preserved (still false); branch still has no upstream Confirmed
git status on new worktree No "behind by N" line Confirmed (## brennanb2025/... only)
Worktree removal Renderer state + filesystem clean; branches deleted Confirmed via removeWorktree(id, force=true)
Renderer console errors during flow None 0 errors

Live Electron re-verification — post-merge (same setup, after merging origin/main into the branch on 2026-05-11):

Scenario Expected Result
Create worktree, autoSetupRemote unset branch has no upstream; status clean (no "behind"); push.autoSetupRemote=true written locally All three confirmed
Branch config after creation no branch.<name>.remote / .merge keys Confirmed (config --get-regexp exit 1)
Pre-set push.autoSetupRemote=false, then create worktree preserved as false; branch still has no upstream Confirmed
git status -b first line ## <branch> only — no [behind N] Confirmed
removeWorktree(id, force=true) renderer state + filesystem clean; branches deleted Confirmed
Renderer console errors during flow None 0 errors / 189 benign warnings

Test artifacts cleaned up after each round (worktrees removed, branches deleted, push.autoSetupRemote restored to its initial unset state).

Live remote-push verification (against personal fork brennanb2025/orca, fresh worktree at /tmp/orca-fork-autosetup-test-*):

Step Expected Result
git worktree add --no-track -b <branch> <wt> origin/main branch created, no upstream Confirmed (git status -b shows no branch.upstream / branch.ab line)
git config --get push.autoSetupRemote exit 1 (unset) Confirmed (exit 1)
git config --local push.autoSetupRemote true written to shared common-dir <repo>/.git/config Confirmed (per the design comment — no extensions.worktreeConfig involved)
Plain git push (no -u) on the new commit creates origin/<branch> AND sets upstream Confirmed: [new branch] + branch '...' set up to track 'origin/...'
git rev-parse --abbrev-ref @{u} after push origin/<branch> Confirmed

Live SSH-relay verification (driving GitHandler.addWorktree directly against a real bare-origin + working-clone setup — same code that runs on the SSH host; an in-process invocation exercises the entire relay state machine end-to-end since the SSH layer is just message transport):

Step Expected Result
Pre-condition: push.autoSetupRemote unset locally exit 1 from git config --get Confirmed
git.addWorktree with base: 'origin/main' worktree dir created; relay invokes worktree add --no-track -b ... origin/main, then probe + --local set true Confirmed
Branch keys after creation no branch.<name>.remote / .merge keys (--get-regexp exits 1) Confirmed
push.autoSetupRemote after creation true (written by relay) Confirmed
git status -b --porcelain first line ## <branch> only — no [behind N] Confirmed
Pre-existing push.autoSetupRemote=false (local scope) preserved as false; relay does not overwrite Confirmed
worktree add itself fails (duplicate branch) rejection bubbles up; no config probe/write occurs Confirmed

No regressions observed.

AI-agent confusion check (Claude Code spawned in a fresh worktree off main created by Orca after this PR is in effect — the failure mode this PR is designed to fix):

Prompt What an agent did pre-fix What the agent did post-fix
"What's the state of this branch relative to main? Do I need to sync before making changes?" Reads git status -b → sees [behind N] → suggests git pull / git rebase before letting you start Runs git rev-list --left-right --count, reports "no local work, no divergence" — no spurious sync suggestion
"Run git status -b --porcelain=v2 --branch and tell me what branch.upstream reports. Is this branch tracking anything?" branch.upstream origin/main would be present → agent confidently reports "tracking origin/main" (wrong-shaped — branch was never pushed) No branch.upstream line → agent correctly reads "this branch is not tracking any remote branch" and self-corrects its earlier "1 behind origin/main" answer
"Will plain git push work here, or do I really need -u origin <branch>? Check the repo's git config for any relevant push settings before answering." No push.autoSetupRemote set → agent says you need -u origin <branch> Finds push.autoSetupRemote=true (local), correctly concludes plain git push will create the remote and set upstream automatically; calls out the git ≥2.37 requirement and notes git pull still needs an upstream until the first push

The agent reasons from observed git state rather than misreading git status as ground truth — both halves of the bug (false-positive "behind" framing and the -u ceremony) are invisible to a calm agent and explainable when probed directly.

Test plan

  • pnpm test src/main/git/worktree.test.ts
  • pnpm test src/main/git/remove-worktree.test.ts
  • pnpm test src/relay/git-handler.test.ts
  • pnpm test src/main/providers/ssh-git-provider.test.ts
  • Create a worktree from Orca, then in it run git status (no "behind" line) — verified pre-merge and post-merge.
  • Set git config --local push.autoSetupRemote false, create a worktree, confirm the local config is not overwritten — verified pre-merge and post-merge.
  • First git push from a new worktree creates origin/<branch> and sets upstream automatically — verified end-to-end against brennanb2025/orca.
  • SSH-relay path: relay addWorktree produces the same end state as local (--no-track took effect, push.autoSetupRemote=true written, git status shows no "behind", existing user value preserved, no config writes when worktree add fails) — verified end-to-end against real git.
  • AI-agent confusion check: spawned Claude Code in a fresh post-fix worktree, asked it to (a) frame branch state vs main, (b) inspect branch.upstream via porcelain v2, (c) decide whether git push needs -u. No spurious sync suggestions, correct read of "no upstream", correct read of push.autoSetupRemote=true — verified.

Made with Orca 🐋

brennanb2025 and others added 10 commits May 7, 2026 18:27
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Orca <help@stably.ai>
- Probe push.autoSetupRemote with `git config --get` before writing so a
  deliberate user value at any scope (local/global/system) is preserved.
- Include worktree path in the warn log for failed config writes.
- Add test pinning the preserve-existing-value behavior.
- Remove stray 00-review-context.md committed during review tooling.

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Orca <help@stably.ai>
Treat only exit code 1 from `git config --get push.autoSetupRemote`
as "key unset". Other read failures (corrupt config, locked file,
parse error) now re-throw to the outer warn handler instead of being
silently treated as unset and overwriting whatever value the user
actually has.

Also: add test for the non-unset read-error path; convert the
"preserves existing value" test from `.some()` predicates to a
full-array `toEqual` matching sibling-test style; explicitly mock
`config --get` (with code: 1) in the sparse-failure rollback test
so it exercises the intended branch instead of the helper's empty-
stdout fallthrough; document in the design notes that
addSparseWorktree's rollback intentionally does not unset
push.autoSetupRemote.

Co-authored-by: Orca <help@stably.ai>
Why: addWorktree's post-create config probe has two ordering
invariants worth pinning so a future refactor can't silently
regress them: (1) `git config --get` succeeding with empty stdout
still counts as "already set" so we don't overwrite an explicit
empty value, and (2) the entire config block is skipped when
`worktree add` itself rejects.

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Orca <help@stably.ai>
… version, add empty-stdout parity test

JSDoc on local addWorktree now flags the push.autoSetupRemote side
effect; both paths cross-reference each other so the next change keeps
them in lockstep. Relay comment clarifies that the git version that
matters is the SSH host's, not the client's. Adds the missing
empty-stdout-as-already-set parity test on the relay side.

Co-authored-by: Orca <help@stably.ai>
Stray file from local review workflow; should not ship in this PR.

Co-authored-by: Orca <help@stably.ai>
Resolved conflicts in src/relay/git-handler.ts and
src/relay/git-handler.test.ts: kept this PR's --no-track +
push.autoSetupRemote state machine but moved it into addWorktreeOp in
the extracted git-handler-worktree-ops.ts (per main's #1672 split),
dropped the unused `track` param, and dropped the stale
this.context.validatePath calls (context is _context post-#1672).

Co-authored-by: Orca <help@stably.ai>
@brennanb2025 brennanb2025 force-pushed the brennanb2025/fix-worktree-no-track branch from 677f7a7 to e46a686 Compare May 11, 2026 06:12
Co-authored-by: Orca <help@stably.ai>
@brennanb2025 brennanb2025 merged commit 8f3c783 into main May 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant