Repository navigation
Commit 6c962b3
feat(web-shell): manage git remotes from the workspace branch picker (#11163)
* feat(web-shell): manage git remotes from the workspace branch picker
The workspace git popover could pull, commit, push and switch branches,
but the remotes themselves were read-only context: a user who clones from
a zip (no origin), works triangular (fork + upstream), or wants to drop a
stale remote had to leave the Web Shell for a terminal.
Adds a Manage Remotes panel inside the popover — list, add, remove —
backed by new workspace-scoped daemon routes and the SDK methods for
them. The listing reads git through its structured accessors rather than
the rendered "remote -v" output, which git annotates for partial-clone
remotes and which a line parser silently misreads. Mutations answer with
the fresh list so the panel re-renders in one round trip, and a remove
also refreshes the branch listing and the tracking state of the chip,
because git deletes the remote-tracking refs and upstream config together
with the remote.
Names and URLs are validated before git is spawned (flag injection,
refname rules for add; removal stays as lenient as git itself so a
hand-edited config can always be cleaned up), and rendered through a
display sanitizer that strips the invisible-character set — a git config
the user did not author can carry bidi marks that spoof the displayed
URL.
Design doc: docs/design/git-manage-remotes.md
* fix(web-shell): harden git remotes management after review audits
The config-scope read, removal verification and client refusal handling
carried gaps the reverse audits surfaced: include.path/worktree scopes were
under-listed yet un-removable (destructive dead ends), GIT_ALLOW_PROTOCOL
was deleted outright (a fail-open swap against config-file policy), the
classifier let echoed names/URLs steal config-write failures, and several
guards shipped without witnesses. Fixes:
- list the repository-owned scope via `config --list --show-scope -z`
(local+worktree), completing worktree-scope removals git cannot edit
- normalize an inherited GIT_ALLOW_PROTOCOL (strip ext/fd) instead of
deleting it; keep deny-by-default force
- order sendGitError's anchored config-write shapes first; add
remote_config_unparsable for unparsable configured refspecs
- follow git's boolean/integer grammar exactly (valueless/empty/hex/kmg)
- keep the URL visible under row shrink; restore focus after in-flight
mutations; disarm confirms on refusal; silent re-reads stay silent
- witness every guard above in unit/e2e suites; sync the design doc
* fix(web-shell): close remotes edge cases from the second review round
Seven Critical threads from the review bot's second batch, all fixed with
witnesses and a ten-round reverse audit on top:
- add pre-flights all config scopes and refuses a same-name inherited
section (remote_shadows_inherited), after probing repository-ness;
removal verifies resolution across every scope so an inherited survivor
can never be reported as removed (fail-closed on killed reads)
- sendGitError shapes match git's message start only (line 1, or git's
two-line lock chain): config-chosen names/URLs can carry any keyword,
and config values can carry real newlines, so deeper line-initial text
is attacker-controllable; classification reads the full redacted detail
while the client message stays bounded
- killed config reads rethrow with the all-scope stdout dump stripped
(stderr diagnostics preserved) on every read path
- focus restores: add submit button, remove-row button when focused,
back-button fallback; restores are gated on lent focus, cleared on view
exit and workspace switch; refused removal awaits the silent re-read so
the restore lands on the converged list
- no_such_remote remove refusals also refresh branches and status
* fix(core): harden git remote removal verification and upstream cleanup
- iterConfigRecords: shared NUL-framed --show-scope record walk for all
four config-read consumers
- promisor bool grammar: match git's strtoimax exactly (C-isspace
leading class, attached sign, hex/octal/decimal, case-insensitive
k/m/g, [INT_MIN, INT_MAX] bound)
- removal: pre-removal pointingBranches snapshot (worktree-over-local,
last value) so mixed-scope upstream keys stay attributable
- removal: sweep worktree-scope branch.<b>.remote/merge/pushRemote and
remote.pushDefault that git's rm cannot write (--fixed-value
--unset-all for value-matched keys), re-verify and refuse
remote_still_configured
- removal: refuse when an upstream key still resolves to the removed
remote from a file this module will not edit (include.path'd file or
inherited global/system scope, incl. rm-unmasking), resolved with
git's effective last-value-wins semantics
- removal: converge a retry after a failed cleanup on the
no-such-remote path instead of dead-ending
* fix(web-shell,cli): close remotes edge cases from the fourth review round
- core: never fire the worktree completion when git died BEFORE
mutating — the refusal phrase matches at a line start only, and an
error:/fatal: invalid refspec at any line start vetoes it outright
(a config-chosen refspec value can carry the phrase on a deeper
line); the refusal keeps its remote_config_unparsable answer and the
row stays
- cli: redact gitdir paths git echoes outside the workspace tree — a
linked worktree's shared main .git dir, a submodule's gitdir, a
relocated admin dir (via the commondir file), the symlink-canonical
spelling of each (git realpaths at setup), and over-long targets
(head-bounded reads, truncated lines redacted as prefix tokens)
- web-shell: IME-owned Enter guard on the add form (isComposing /
keyCode 229, both inputs)
- web-shell: whitespace-lookalike marking — names/URLs differing only
by whitespace (CSS-collapsed) now flag, with escapes in the tooltip
and aria-labels; the search filter collapses both sides to match the
inked text
- web-shell: sticky stash warning survives the remotes round trip —
snapshot on entry, restore on back/Escape/dismiss, busy-gated so a
settling mutation's footer wins, a newer standing warning always
outranks a held snapshot, and re-entry never nulls a held one
- web-shell: focus capture/restore resolves from the popover content's
own root (shadow-portal embedding), and the settle restore bails
when the content is unmounted mid-mutation (no cross-instance focus
theft)
* fix(core,web-shell): close remotes edge cases from the R4 review batch
- core: sweep orphaned refs/remotes/<name>/* after a certified removal
(git's rm deletes them only through a parseable fetch refspec), with
--no-deref so a planted symref can never delete its target, plus the
no-such-remote converge arm so a retry after a failed sweep does not
dead-end; re-verified, refusing remote_still_configured on a survivor
- core: extraPushUrls falls back to the url list when no pushurl exists
(git pushes to every url then) so the badge no longer under-reports
git's push fan-out
- cli: route suite gains strict-mutation-gating (per-call + count),
mid-flight generation close → 503, runtime-env threading, both lock
chains, the ancestor-walk ceiling, and the host system/global config
precondition guards
- web-shell: armed remove confirm disarms on filter-out / Escape /
add-submit; the popover mock now unmounts on close and dismisses on
an un-prevented Escape (Radix DismissableLayer parity); Escape events
in the witnesses are cancelable
- web-shell: a successful removal no longer awaits the branch refresh
(busy/focus released immediately); a successful add clears the
remotes filter so the new row is visible; the removing row shows a
spinner; the armed confirm's aria-label names the consequence badge
- web-shell: search matches the extras badge text and sanitizes the
needle; the push/fetch tooltip labels are localized (en + zh-CN);
the e2e primary journey carries @smoke
- sdk: workspaceGitRemoteAdd/Remove accept an optional per-call
timeoutMs; the panel passes 600s (the mutation chains git spawns
that each carry their own 30s budget)
- docs + PR body updated to the shipped implementation (config-scope
reads, WRITES-vs-RESOLVES scope split, six classifier branches)
* fix(core,cli,web-shell): close remotes edge cases from the R5/R6 review batch
- core: refuse include-held/split remote sections BEFORE spawning
`git remote remove` (git rm deletes the tracking refs and upstream
keys before failing on a section it cannot write — every retry
destroyed again); record origins are resolved against the worktree
toplevel (git chdirs) with both literal and realpath spellings, and a
killed probe refuses instead of answering blind
- core: the upstream-key sweep now covers local scope too and matches
multi-valued keys by exact value (`--fixed-value`), since git's rm
skips multi-valued keys with a warning; the re-verification pins only
value-matched keys (an include-held inert merge key must not refuse a
completed removal)
- core: the survivor gate gains the unmask case — a pointed branch or
`remote.pushDefault` whose shadowing record the removal unset must
not fall back to an inherited record naming a remote with no section
(the pre-removal snapshot proves causation), while git's sectionless
forms (`.`, URLs, scp-like, local paths) and pre-existing dangling
values never refuse
- core: promisor values are resolved by the host git itself
(`--type=bool --get-all`, additive like git's registration) instead
of an in-module bool grammar whose integer bound tracks a moving git
target; a promisor record at any scope marks the badge, and a killed
badge read rethrows stripped instead of answering false
- core: Apple Git's runtime-prefix defaults file reports scope
`unknown`; the record walk recognizes it so the verification folds
stay sound (the listing and sweep still filter to repository scopes)
- cli: redaction covers the inherited config files git echoes by
absolute path ($HOME/.gitconfig, the XDG config — including empty-XDG,
no-HOME and trailing-slash verbatim-concatenation spellings — and the
build-time system gitconfig), NUL-truncated gitdir AND commondir
pointer targets, head-truncated targets keyed on the NUL-normalized
form, and the bad-config-line / unable-to-access payload shapes at
arbitrary include.path locations (transport URLs are not config
targets and stay verbatim)
- web-shell: the popover installs the house preserveImeEscape mask
while open, so a composition-cancelling Escape no longer tears down
the remotes view nor swallows the native IME cancel (the popover mock
now delivers Escape from document-capture like Radix's
DismissableLayer)
- e2e: remotes spec selects on data-testid instead of hashed
CSS-Modules class substrings
- kill suite: the mock chains pin exact call counts (a retargeted kill
read can no longer pass silently)
* fix(core,web-shell): close the slash-namespace and render-identity gaps
- core: isRemovableRemoteName now refuses names containing `/` — a
slashed remote's tracking namespace is a subdirectory of the prefix
remote's (refs/remotes/origin/staging/* lives inside
refs/remotes/origin/*), so no post-removal sweep can tell the remote's
own refs from the prefix remote's branch refs, and a never-configured
slashed name would 404 only after the converge-arm sweep destroyed
them; the panel answers 400 before anything runs and the terminal
remains the tool for the shape
- web-shell: the remotes row identity check gains the two structural
arms the strip list cannot enumerate — a name that is not NFC (a
canonical twin inks identically) and a Latin name mixing in another
script's letters (the Cyrillic-о homoglyph shape) — both marking the
unusual row with its distinguishing characters escaped in the tooltip
and aria-label, while a legitimate non-ASCII name stays unmarked
- docs: the design record's absolute render-identity claim and the
remove-route leniency claim are narrowed to the shipped mechanisms
* test(core): pin worktree-over-local upstream attribution on removal
* fix(cli,web-shell): bound the loose classifier keywords and keep git's sentence boundary
- cli: the legacy loose keyword branches (dirty / already exists / no
upstream / …) match the CAPPED 512-char slice again — an unbounded
scan reclassified long push/pull output whenever a path or URL past
the cap carried a keyword (a `dirty-cache.git` push rejection
answered 409 dirty_working_tree); the anchored remote branches keep
the full detail for the two-line lock chain
- web-shell: git error text in the single-line footer now collapses
whitespace before stripping the invisible class — a stripped newline
fused git's two-line lock chain into one unreadable sentence
* test(cli): allow the redaction's process-scoped HOME/XDG_CONFIG_HOME reads
* fix(core,cli,web-shell): close the R9-batch removal integrity gaps
- core: the upstream-key sweep now reaches a multi-valued key's
NON-effective entry naming the removed remote (residue whose later
surfacing would dangle), and the merge key is swept only when the
same scope's remote key holds no surviving entry — a branch that
keeps its upstream keeps its merge
- core: the tracking-refs sweep resolves ownership by longest-prefix
against the configured remote set (a configured slashed sibling's
namespace is no longer taken down with the prefix remote) and covers
the exact bare ref refs/remotes/<name>
- core: removal certification asks git's own resolver
(`ls-remote --get-url`, documented not to contact the remote), so a
legacy $GIT_DIR/remotes/<name> file or an insteadOf alias can no
longer keep a "removed" name fetchable
- core: the sweep now walks every linked worktree's config.worktree
(the invoking worktree's reads never see siblings' files), lazily,
fail-closed per worktree
- core: the pre-removal snapshot carries per-branch local value lists,
and a local [branch] section git rm destroyed while its shadowed copy
named a SURVIVING remote is restored — a worktree-shadowed fallback
upstream is no longer collateral damage
- cli: the unable-to-access redaction arm accepts drive-letter absolute
paths (the Windows lane runs the redaction suite)
- web-shell: a remote URL carrying non-ASCII bytes fails closed — URLs
are an ASCII-only surface, so the row marks and the tooltip spells
the homoglyph out
* fix(core,cli,web-shell): close the R10-batch removal and confusables gaps
- converge arm: ask git's own transport for the bare-word path leg
(bundles resolve; --resolve-git-dir called them too large), skip the
sweep for empty pushInsteadOf aliases, re-verify the ref sweep, and
carve win32 backslash spellings as sectionless values
- unmask gate: push-side arms count pushurl records and pushInsteadOf
aliases; the path leg answers through git's transport
- pre-flight: inherited refusal only when a repository half exists to
destroy; union gate and worktree completion keep their race backstops
(kill-suite witnesses)
- redaction: linear /etc/gitconfig token arm, open-family in-file arm,
fail-closed absolute-path sweep with the apostrophe-tail fragment drop
- confusables: table-first fold with NFC closure at generation time,
symmetric all-ASCII collision marking, skeleton as a search target,
U+2028/2029 escaped emission, self-map and empty-alias guards
- design docs: bilingual zh-CN twin plus the round's gloss corrections
* ci: run the platform lanes for the git remotes path handling
The win32 path-spelling carve and the local path-transport probes behave
differently per host and no POSIX-only lane can tell whether they work on
Windows; classify the two git-remotes files platform-sensitive (exact
paths, not a keyword) so both lanes see future changes to them.
* test(core): write the slashed sibling section instead of remote-adding it
git newer than 2.50 refuses `git remote add a/b` over an existing `a`
("remote name 'a/b' is a subset of existing remote 'a'"), which killed
the macOS and Windows lanes in fixture setup. A config-written section
is git-version independent and models the hand-edited config the lax
removal predicate exists for.
* Revert "ci: run the platform lanes for the git remotes path handling"
main retired the platform-lane pull_request trigger and froze
classify-platform-sensitivity.mjs untouched so the future restoration
revert stays clean (ci.yml comment). The GIT_PATH_HANDLING entries
extended a script no CI path invokes — the lanes actually ran through
workflow_dispatch event gating — and broke that freeze.
* fix(core,cli,web-shell,docs): close the R11 confusables split and deferred batch
- web-shell: the generator closed prototypes under NFC + table only,
but the runtime fold also NFKC-folds table-absent halves, so 29
emitted values split one ink-identical class across two skeletons
(R11-2). Close every value under the exact consumer fold map-wide,
delete self-maps inside the convergence loop so the fixed point is
computed against the final map, post-verify the invariant, and
regenerate (29 values, witness: U+1D52 vs U+00BA).
- core: escape the NUL separator in pushKey (a raw NUL byte made grep
and ripgrep treat the module as binary); rename the misnomer
repository-scope describe.
- cli: readHead opens workspace-controlled paths synchronously on the
shared error path; a FIFO commondir wedged the daemon event loop.
Guard on isFile (FIFO witness, POSIX-gated).
- web-shell test: replace the vacuous cross-workspace focus assertion
with an equality on the pre-settle active element.
- docs (en + zh-CN): consumer-fold closure clause, ls-remote probe
spelling, orphaned bullet fragment repaired, §6 witness rows.
* test(core): retry the tmp-dir teardown on Windows EBUSY
The windows lane lost an otherwise-passing not-a-repo test to a
transient EBUSY on the afterEach rmdir (a handle held by the indexer
or an exiting git child). Retry the teardown a few times instead of
failing a test whose assertions already passed; non-EBUSY errors and
exhausted retries still throw.
* docs(web-shell): state the consumer-fold example's mechanism exactly
The `º/₀` halves NFKC to `o/0`; the capital O comes from the per-half
table chance (0030 -> 004F), not from NFKC. The result string was
right, the stated mechanism was not — anyone re-predicting a fold
whose NFKC half is itself a table key would mis-derive it. Also name
the tooltip-escape consumer at the delete arm and print the code
point (not the raw char) in the fixed-point diagnostic.
* fix(core,web-shell,docs): close the R13 Criticals and their audit fallout
- R4-3: widen EXECUTING_HELPER_URL to admit git's empty-name helper
form (`::payload` execs a PATH-resolved `git-remote-`); rationale
corrected — the empty name is not on git's deny-by-default list, and
a protocol.allow policy that would cover it is overridable from
config files. Witness rows `::sh -c id`/`::0`/`::`; mutation of the
pattern turns them red while the IPv6 literals stay accepted.
- R12-1: restore the local upstream backups at the TOP of the
removal-failure path (above the !completed throw and the converge
classification), so a refusal following an already-destructive rm
cannot skip the rollback; a killed restore read rethrows instead of
letting git's original answer surface un-rolled-back (kill witness
pins the restore read's argv, separating it from the converge gate's
scope read).
- R13-2: the converge arm excludes the removed name's own
namespace-less top-level bare ref from sweep AND re-verify while
keeping the longest-prefix ownership resolution whole — a bare ref
exactly owned by a configured slashed sibling keeps that owner
(first attempt at this fix regressed exactly that; witnesses:
remote-HEAD symref, flat layout, slashed sibling bare ref).
Accepted residuals disclosed: a formerly-configured name's bare-ref
residue is orphaned, and a flat layout's slashed branch refs stay
sweepable.
- R13-3: mark BOTH rows when a collision group's skeleton is non-ASCII
and the variance is a table fold rather than canonical equivalence
(skeletonAscii/allCanonical on the group); witness öö × ةة; the
all-ASCII and pure-canonical polarities keep their pinned behavior.
- Audit fallout: the TR39 fold moves to
client/utils/remote-name-skeleton.ts beside its table integrity gate
(no self-maps, no value containing a table key, key folds to value,
value is a fold fixed point, NFC); the non-discriminating
lowercase-spoof completion test is removed (the exact-prefix test
carries the veto witness); design docs synced EN+ZH for the third
marking polarity, the error-path restore and its fail-closed
masking, the converge residuals, and the new test rows.
* fix(core,cli,web-shell,docs): close the R14 Criticals and their audit fallout
- R14-2: the TR39 fold iterates to a fixed point (capped) instead of
running one pass — the closing NFC can compose a table key and a
decomposed spelling only meets the table after composition; new
remote-name-skeleton.ts carries the fold beside its integrity gate
and fixed-point corner tests.
- R14-3: redaction gains a quoted-path arm (git's die(_('%s' …))
family owns a quoted space-bearing payload whole) plus a slash-
carrying fragment arm for apostrophe-bearing paths.
- R14-4: tracking-ref ownership skips refs inside a SURVIVING remote's
fetch-dest namespace (NUL-framed get-regexp read), on sweep and
re-verify alike; closes the flat-dest slashed-branch residual.
- R14-5: the restore's presence read is NUL-framed, so an empty value
reads as present instead of absent (no more doubled pushDefault).
- R14-6: isSectionlessUpstream narrows to `.`/colon-bearing values;
dangling slashed/backslash upstreams now refuse like their bare-word
twin; win32 UNC keeps a carve (network transport, offline-share
timeout); existing paths still certify through the path probe.
- R14-7: the post-refusal branch/status refresh runs in the same
synchronous block as the staleness guard, ahead of the awaited
re-read, so a mid-await workspace switch cannot seed the new
workspace's panel with the old closure's data.
- Audit fallout: stale comments corrected (sectionless shapes, flat
dests, probe classes), win32 UNC carve, NUL-framed dest read without
the speculative JSON.parse arm, prettier-clean test files, and EN/ZH
design-doc sync for all six changes plus matrix/Files-affected
attribution of the new skeleton module and its tests.
* fix(core,web-shell,docs): close the R15 Criticals and their audit fallout
- R15-1: foreign-namespace roots derived per wildcard position; mid-
wildcard dests (refs/remotes/*/main, or*/main) fail closed. The
reverse audit caught an unsatisfiable star-position conjunction that
disabled the orphan sweep in any repo with another default-refspec
remote; fixed and pinned by an origin + refspec-less orphan witness.
- R15-2: the sibling-worktree sweep verifies the sibling shares this
repository's common dir before any read or write, so a planted
.git/worktrees gitdir can no longer receive its unset writes;
planted-gitdir witness, real-sibling tests unchanged.
- R15-3: the remotes search folds the needle in three case forms (the
TR39 table is case-sensitive); Istanbul/lstanbul witness.
- R15-4: the fold is invariant under canonical equivalence — two-level
decomposition fallback at runtime, generator closure over NFD(key),
table regenerated (6564 entries, U+021A dropped, 27 values re-classed,
lunate sigma preserved); full invariance sweep pinned.
- Deferred-triage fixes: hermeticity precondition regex, win32 UNC
short-circuit witness, worktree-completion kill witness, dash-URL
client-guard witness, NFKC-fallback pin, timeout-ceiling and
mixed-scripts comment accuracy, zh predicate-bullet and UNC doc
parity, pushl marker-comment accuracy.
* refactor(core,web-shell,docs): land the R15 reverse-audit follow-ups
- The sibling sweep skips the invoking worktree BEFORE the ownership
probes, so a repo whose only worktree record is its own pays no
probe spawn (just the one lazy toplevel read, reused per sibling).
- The killed-sibling witness is re-aimed at the new spawn order and
pins the kill to the sibling's config.worktree read; under the old
queue the kill landed on an ownership probe while the test passed.
- The remotes search dedupes its case-folded needles and folds each
row's ink once instead of once per needle variant.
- EN design doc: two stranded fragments repaired; ZH counterpart gets
the missing subject in the marker sentence (parity).
* test(web-shell): route the remotes e2e through the post-#11700 picker entry
Main's context-overview rework removed the sidebar git pill and the
welcome-view branch chip this spec opened the picker through; the
merge-head smoke run failed waiting for a pill that no longer renders.
The spec now enters the scenario session and opens the picker from the
environment panel's branch row — the same route the branch-picker
visual spec uses — keeping every assertion unchanged.
* build: raise the tsc build heap ceiling to 4096 MiB
A fresh `tsc --build` of packages/cli on the merged tree peaks just
above the 3072 MiB ceiling: two consecutive CI Lint runs and a local
clean-tsbuildinfo build all OOM at ~3.0/3.1 GiB, while each parent tree
alone stays under the ceiling and incremental builds never showed it.
Workspace build children inherit NODE_OPTIONS from the root build
script, so raising it there is what fits the union program; the test
ceilings are untouched.
* test(web-shell): pin the remotes e2e to the scenario branch and armed state
- openBranchPicker derived its click target from a hardcoded branch
name while accepting scenario overrides; it now reads the scenario
branch, pinned by the search test whose fixture switches to feat/x
(RED without the derivation).
- The smoke test asserts the armed Confirm label between the two
remove clicks, so a single-click regression fails at the witness
instead of as a second-click timeout; pinned by a temporary
single-click mutation that turns the assertion RED.
* fix(cli): move the git routes off the core barrel per the #11798 rule
Main's new lint rule bans new cli value-imports from the core package
root; the remotes and branches serve routes imported their symbols from
the barrel. Point them at the defining core modules (utils/git-remotes,
utils/git-branches, utils/gitDirect, utils/gitUtils) and drop the
allowlist entry the branches route no longer needs, per the list's
shrink-only contract.
* fix(core,cli,web-shell,docs): address R16 review and convergence-audit findings
- Refuse removal up front when a repository-scope section's name stays
push-resolvable through a url.*.pushInsteadOf prefix: a
post-destruction refusal wedges the upstream keys the certify sweep
can no longer reach (the retry's 404 converge arm skips over the same
alias. Six shape witnesses: local-section, strict-prefix,
inherited-only 404 doctrine, worktree scope, and the included- and
inherited-refusal ordering ahead of the alias refusal.
- Convert the truncated-gitdir redaction from an unbounded \S* regex to
a linear per-token replace with identical semantics.
- Picker collision groups key on the casefolded skeleton; a per-pair
case-twin arm marks case-only pairs at any script (a non-ASCII third
member no longer disarms an ASCII pair); U+FFFC/U+FFFD placeholder
names mark with the (lookalike name) copy; every arm and polarity
carries a mutation-verified witness.
- Drop the core barrel re-export of utils/git-remotes.js: its only
consumer deep-imports the module per the #11798 rule.
- Sync EN+ZH design docs to the four structural arms, arms 1-4 witness
attribution, the six pre-flight witnesses, and the documented
fetch-side wedge as a known house limitation.
HEREDOC
)
* fix(cli,core,docs): address R17 verification findings and convergence-audit findings
- M6/M8 (four-time survivors): the bounded-slice test now pins the
not-a-repo keyword family past the 512 cap (deep-cap probes for
`not a git repository` and `invalid reference`), the `invalid
reference` alternation half gains its positive table row, and a new
table test pins the line-1 anchor of the remote-already-exists arm
(a line-2 occurrence falls to the loose keyword arm). Each pin is
mutation-verified: its mutant reddens exactly that test while every
sibling stays green.
- The inherited-scope-alias wedge (a global/system/unknown
pushInsteadOf alias prefix-matching a listed repository-scope
section) gains its repo-suite witness, and the pre-flight comment
plus both design docs name it, including that the 409 names no alias
at any scope so the UI never shows the cause (fail-closed, terminal
is the only escape).
- Verification-bot dispositions: the wire-visible distinct error code
for the pre-flight refusal is deferred as a follow-up; the leaked
HEREDOC commit-message trailer is declined (fixing it would require
rewriting published history).
* fix(remotes): close R18 review findings and local audit findings across rollback, converge and fold
R18-1: the ambient named-section guard matched across lines
(`[^.]+` spans newlines), so a host `remote.pushDefault` read as a
named section and skipped the whole route suite; exclude the newline,
stop the scan at the `=`, and pin the line boundary both ways
(two-component pushDefault plain / followed by a dotted key / carrying
a dot in its value must not match; dotted section names must).
R18-2: the no-such-remote converge arm ran the certify path's
destructive sweep without any unmask gate, so a removal could destroy
the local shadow of a dangling inherited upstream and still answer
404; run the snapshot-scoped half (sweptUpstreamResolving re-verify
plus unmaskedPointedUpstream) after the sweeps, keeping git's 404 for
inherited survivors the snapshot never pointed at.
R18-3: an include-held value equal to the removed name blocked the
collateral-damage restore; the new discountGate discounts it from the
presence reads when the section is gone from every scope AND the name
no longer resolves at all — resolver probe (legacy $GIT_DIR/remotes
file, insteadOf alias) plus local-path probe (same-named directory
repo or bundle file; a sectionless name short-circuits before the path
probe, which would otherwise put a scp-like spelling on the network),
and a probe that cannot answer yields no-discount inside the gate
rather than aborting the rollback ahead of the merge arm. The discount
spans the remote, pushRemote and pushDefault presence reads; the
merge pairing reads the undiscounted presence; the exit-0
split-section path restores a second time after its worktree-half
completion so the gate never sees a half the completion removes.
R18-4: isSectionlessUpstream treated any colon as a network transport,
failing the unmask gate open over colon-after-slash local paths;
apply git's colon-before-first-slash rule, keeping the dot and win32
UNC carves.
R18-5: an invisible character split the skeleton collision group
(group key folded the raw name while the search folded the sanitized
one); key the group, member flags and render lookup on the sanitized
fold, keeping the polarity arm on the raw name.
R18-6: the settle effect restored mutation-start focus without
reading settle-time focus, yanking focus out of a control the user
moved to mid-flight; record the start element and skip the restore
when in-content focus moved elsewhere.
R18-7: the skeleton fold was not invariant under canonical
equivalence (positional table walk before the closing NFC);
canonicalize at the entrance (NFC is a canonical function) and cover
fused code points with a longest-decomposed-prefix table lookup,
mirrored in the generator so the table stays closed.
Round-7 audit additions on top of the R18 fixes: the discount gate's
local-path leg and its probe-failure carve-out (a live directory-repo
or bundle upstream must not read as dangling residue; a wedged
resolver must not abort the rollback), the sectionless-name
short-circuit, the split-section second restore, the pushDefault
arm's dead conjunct, and witnesses for each: directory-repo and
bundle no-re-point pairs, probe-failure merge restore, pushRemote
write-back, split-section completion write-back, and the kill-suite
pin that a scp-like removed name never reaches the path probe.
Rounds 8-9 closed the remaining witness gaps: the pushurl-only
section scope-leg witness (the fetch-side resolver and the path probe
are both blind to a pushurl-only section), the kill-suite pin that a
killed gate probe yields no-discount while a later gate surfaces the
kill, the split-section witness now asserting certification, the
sanitized render-lookup witness (the skeleton-escape tail spells the
fold-covered code point), the pushDefault write-back comment
re-attributed to the unmask arm that actually refuses its fixture, and
the generator's vestigial NFD argument removed (old and new generator
produce byte-identical tables). Accepted residuals recorded in code
comments and both design docs: merge-key re-sweep on late refusals,
the sibling-worktree blind spot of the gate's section leg, the
swallowed discount when a surviving `[branch <b>]` section takes an
in-section insert ahead of a later `[include]` directive, the ambient
guard's `=`-in-name miss, and the six formerly-unified ink classes of
the regenerated table.
Mutation-verified: each mutant reddens only its target witness.
Rounds 10-11 closed the last witness gaps and comment drift: the
win32 drive-letter fail-open in isSectionlessUpstream (a drive-letter
prefix is a LOCAL path on win32 — the carve is win32-gated — while
every other platform reads it scp-like ssh, so the colon rule answers
it sectionless and keeps every probe off the wire; witnessed per
platform, with zero ssh spawns on POSIX), the converge-arm comment's
false claim that a legacy file never reaches the arm (git's rm exits 0
over it, so the resolver leg is what skips the sweep on a retry), the
per-arm sanitized member-flag witnesses (all-ASCII-only and
case-twin-only), the inventory rows' three omitted witnesses, the
comment counts ("Three refusal sources", "One gate decision per
pass"), the win32 drive-letter carve's missing no-separator spelling
(has_dos_drive_prefix needs only letter plus colon, so `C:x` probes
locally on win32 too), the merge-churn residual's code-comment record
at the post-certification cleanup, and the generator paragraph the
entrance-NFC rewrite orphaned (the ink-equal-value phenomenon is
systematic — 1010 of 6564 values — and its example is now a real
composition exclusion, U+FB30).
Rounds 13-14 rebuilt isSectionlessUpstream as a faithful mirror of
git's url_is_local_not_ssh: has_dos_drive_prefix (any non-NUL ASCII
character, or one whole non-ASCII code point, plus a colon), the
is_valid_win32_path conjunct (NTFS-forbidden characters, reserved
device-name segments, segments ending in space or period, and the
whole-path trailing-dot magic), and forward-slash UNC — a drive-letter
url whose tail NTFS rejects is routed by git to ssh, so probing it
would spawn ssh from a config string; the predicate is exported and
unit-tested under a stubbed platform (original descriptor restored),
because the win32 legs are not executable off-win32. Both design docs
enumerate the win32 classification correctly, and the accepted
residuals are numbered one to four.
Round 15 closed the last predicate divergences against git's
url_is_local_not_ssh / is_valid_win32_path: reserved device names match
as git's PREFIX rule (terminated by end, `.`, `:`, or a separator,
after optional spaces; LPT accepts any digit, COM 1-9), the
per-segment trailing-period rule exempts all-period segments of
length <= 2 (`C:/..` and `C:/.` are valid local paths, not ssh), and
the colon/slash legs are evaluated before the drive-prefix leg exactly
as git's expression orders them; the unit table covers the divergent
shapes and three mutants (old reserved regex, reordered legs, old
period rule) each redden only their unit tests.
Recorded deferrals from rounds 15-17: the per-pass gate memo is a
latency-only guard with no behavioral witness (spawn count, never
outcome); core.protectNTFS=false on win32 makes git read reserved-name
drive tails as local while the predicate calls them sectionless
(fail-safe direction, explicit opt-out only, same family as the UNC
over-approximation); and 1024 of the 6564 emitted confusables keys are
not NFC fixed points and therefore unreachable under the entrance NFC
(dead weight, behavior-preserving to drop). Round 17 also added the
kill-suite witness for the discount gate's scope leg sitting outside
its try (a killed scope read aborts before any destructive cleanup,
while a killed probe leg answers no-discount inside the gate).
Deferred follow-ups (recorded, not dropped): the merge key a refusal
landing after unsetUpstreamKeys can re-sweep when the local remote
key does not stand (include-held residue on a pointed branch); the
sibling-worktree blind spot of the discount gate's section leg; the
swallowed discount when git's rm leaves the local `[branch <b>]`
section standing and an `[include]` directive sits after it; the
ambient guard's `[remote "a=b"]` miss; the race-only pre-existing
TOCTOU window between the union gate and the tail surviving-keys
gate; the six formerly-unified ink
classes of the regenerated confusables table; the remoteInkKey helper
unification; the equal-combining-class skeleton limitation; the
converge-arm duplicate dump spawn; a wire-visible error code for the
pre-flight refusals.
* fix(remotes): certify removals push-side as well as fetch-side
The removal certification's resolver leg answered fetch-side only:
`ls-remote --get-url` applies insteadOf and never sees
url.<base>.pushInsteadOf, the rewrite git uses when pushing — a name
kept resolving push-side by an alias racing in after the pre-flight
read was certified as no longer resolving. The union gate and the
discount gate now read the push-side alias prefixes from the same
all-scope dump their section legs already take, at zero extra spawns
(the kill suite's spawn accounting is unchanged): a name kept
push-live refuses 409 remote_still_configured, and a residue equal to
a push-live name is not discounted into a silent re-point. The
pre-flight owns the steady-state shape (a section plus its alias
refuses before destruction; the converge arm skips the same alias on
retries); these legs are the backstops for the race window.
Witnesses: the union-gate push-alias refusal and the discount-gate
push-alias no-discount kill-suite cases (each mutant reddens only its
witness); the docs state the push-side leg in both languages.
---------
Co-authored-by: qwen-code-dev-bot <[email protected]>1 parent 3c774d1 commit 6c962b3
33 files changed
Lines changed: 23242 additions & 298 deletions
File tree
- docs/design
- packages
- cli/src/serve
- routes
- core/src/utils
- sdk-typescript
- src/daemon
- test/unit
- web-shell
- client
- components
- e2e
- utils
- utils
- scripts
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
62 | 67 | | |
63 | 68 | | |
64 | 69 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
152 | 152 | | |
153 | 153 | | |
154 | 154 | | |
155 | | - | |
156 | 155 | | |
157 | 156 | | |
158 | 157 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
289 | 289 | | |
290 | 290 | | |
291 | 291 | | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
292 | 301 | | |
293 | 302 | | |
294 | 303 | | |
| |||
0 commit comments