Repository navigation
fix(git): keep merge-tree's throwaway trees out of the real object store - #22356
Conversation
The PR conflict summary and the squash-merged branch cleanup both run `git merge-tree --write-tree` only to read its output, but every divergent run left unreachable loose objects behind. Run it with a per-call scratch object directory (inside objects/, alternates pointing at the real store) that is deleted afterwards, on native, WSL and SSH relay hosts alike.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to
|
There was a problem hiding this comment.
Important
WSL Git on a Windows-drive repo with Git < 2.31 is handed a Windows-spelled GIT_OBJECT_DIRECTORY it cannot resolve, so merge-tree fails instead of falling back. Details inline.
Reviewed changes
- Core quarantine module — new
src/shared/git-object-quarantine.tsruns a Git command withGIT_OBJECT_DIRECTORY= a fresh scratch dir underobjects/andGIT_ALTERNATE_OBJECT_DIRECTORIES= the real object store, deleting the scratch dir in afinally; falls back to unquarantined when the objects dir or scratch dir is unavailable. - Local adapter —
src/main/git/local-git-object-quarantine.tsresolves the common dir viareadRepoCommonDirFromGit, translates Linux spellings to WSL UNC host paths, and adds both vars toWSLENVon Windows. - Relay adapter and env plumbing —
src/relay/relay-git-object-quarantine.tsplusopts.envmerged overbuildRelayGitEnv()ingit-handler.ts, with the type threaded throughgit-handler-ops.ts/git-handler-operation-context.ts. - Callers —
conflict-summary.tswraps both merge-tree invocations;git-branch-cleanup.tsmarks its merge-tree calldiscardWrittenObjects: true, honored byworktree-branch-removal.tsandgit-handler-branch-cleanup.ts. - Tests — real-Git fixture and loose-object-count tests (local + relay), plus unit tests for fallback/cleanup/quoting/WSLENV, and a branch-cleanup assertion that only
merge-treeasks for the scratch store.
ℹ️ Nitpicks
alternateObjectDirectoriesValueleaves a Windows path that itself contains;(the Windows separator) unquoted, because the shortcut keys offisWindowsAbsolutePathLikeand the special-char regex is only/[:"\\]/. A;-containing directory is legal NTFS and native Windows Git would then split it into two bogus entries. Rare enough that it may not be worth the added branching, but the shortcut assumes a separator it never checks.- The alternates-quoting branch is only asserted as the produced string; the real-Git fixtures live under
mkdtempintmpdir(no:), so that branch is never exercised against the real Git parser. The scheme itself checks out against Git 2.25+, so this is coverage rather than a live defect.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
|
…stores - A partial clone fetches missing blobs during merge-tree; those packs landed in the scratch store and were deleted, so every check re-downloaded them. Move fetched packs (index last) into the real store before removing the scratch dir. - Sweep this app's own scratch dirs older than an hour, once per objects dir per process, so a crash or failed delete cannot strand one forever in repos where gc never runs. - Remove with Windows transient-lock retries; the shared core now owns all filesystem work and host adapters only resolve the objects dir.
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: b406813f-8d2a-46b8-8b84-bf9fb07b35a6
📒 Files selected for processing (20)
src/main/git/local-git-object-quarantine.test.tssrc/main/git/local-git-object-quarantine.tssrc/main/git/merge-tree-object-quarantine-real-git.test.tssrc/main/git/worktree-branch-removal.tssrc/main/github/client-merge-queue-auto-merge.test.tssrc/main/github/client-pr-conflict-summary.test.tssrc/main/github/client-test-mocks.tssrc/main/github/conflict-summary.test.tssrc/main/github/conflict-summary.tssrc/relay/git-handler-branch-cleanup.tssrc/relay/git-handler-operation-context.tssrc/relay/git-handler-ops.tssrc/relay/git-handler.tssrc/relay/relay-git-object-quarantine-real-git.test.tssrc/relay/relay-git-object-quarantine.tssrc/shared/git-branch-cleanup.test.tssrc/shared/git-branch-cleanup.tssrc/shared/git-object-quarantine-real-git.test-fixture.tssrc/shared/git-object-quarantine.test.tssrc/shared/git-object-quarantine.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.
There was a problem hiding this comment.
ℹ️ No new issues in this delta.
Reviewed changes
- Partial-clone downloads preserved —
keepFetchedPacksmoves a pack Git lazily fetched into the scratch store (plus its.promisorfile) into the realobjects/pack, renaming.idxlast, so the next check does not re-download the same blobs. - Stranded scratch stores swept —
sweepStaleScratchDirectoriesdeletes this app's owntmp_objdir-orca-merge-tree-*directories older than an hour, once per objects dir per process, leaving recent ones and Git's owntmp_objdir-*alone. - Host adapters simplified — the shared core now owns the filesystem work (
mkdtemp/removeTreewith Windows lock retries); the local and relay adapters only resolve the objects dir. - Tests — a real-Git partial-clone test proving the second check does not re-fetch, unit tests for the pack move and the stale sweep, and fixture helpers (
createPartialClone,looseObjectCountIn,scratchDirectoriesIn).
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…ch cleanup failures
There was a problem hiding this comment.
ℹ️ No new issues in this delta.
Reviewed changes
- Windows alternates quoting —
alternateObjectDirectoriesValuenow quotes a Windows-spelled objects path containing;(Git for Windows' separator) or", so a legal-but-rare NTFS directory name is not split into two bogus alternate entries; POSIX paths keep the existing:/"/\rule. Verified against Git source:odb.c:parse_alternatessplits unquoted entries onPATH_SEP(:on POSIX,;on Git for Windows) but routes a leading"throughunquote_c_style, so a separator inside quotes is never split. - Cleanup failures now logged — a failed
keepFetchedPacksrename and a failed scratch-dirremoveTreenow emit aconsole.warnwith the path/pack and error, matching the neighbouring Git modules, instead of swallowing silently while the check result still stands. - Test coverage extended — a
C:\a;b\.git\objectscase pins the new quoted Windows spelling, while the plainC:\repo\...case still asserts an unquoted value; the old||shortcut left every Windows path unquoted, so the only behavior change for Windows paths is this;/"case.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
Status: ready for review (not merged)Problem this fixes: Orca's pull request conflict check and its branch cleanup after deleting a workspace ran What changes for the user: nothing visible. The conflict list, the merge check and branch cleanup give the same results; the repository simply stops growing from these checks. What changed in this round (head
Review:
Live QA on a second Mac, real app, Not verified:
Checks: CI green on |
…bly gone Another Orca process on the same repo (a second instance, dev and release builds) could delete a scratch object dir still in use once its mtime passed an hour. Each scratch dir now records its owner (pid, hostname, platform, Linux pid namespace); the sweep removes it only when that owner shares this pid space and has exited, or, with no checkable owner, at Git's own two-week tmp_ expiry. Also: - A fetched pack whose later rename fails is rolled back, so the real store never keeps an index-less pack; a pack already installed is left alone. - WSL Git gets the /mnt/<drive> spelling of a drive-path common dir (Git < 2.31 prints it relative, resolved against the C:\ repo path). - conflict-summary tests no longer create scratch dirs in the cwd.
… owner included A reused pid held by a long-lived process could keep a leftover scratch dir forever in a repo where git gc never runs. The two-week age (git gc's own tmp_ expiry) now removes any scratch dir; a younger one is still swept only when its owner shares this pid space and has exited.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- WSL drive-path translation —
localGitObjectsDirectorynow hands WSL GittoLinuxPath(commonDir), so a Windows-drive repo whose common dir arrives Windows-spelled (Git < 2.31's relative.git) gets/mnt/c/...instead of an unresolvableC:\...; the host spelling stays Windows. This resolves the prior blocking review thread. - Owner-recorded scratch sweep with a two-week cap — each scratch dir carries
orca-owner.json(pid,hostname,platform,pidNamespace?). A dir under the one-hour floor is kept; one at or pastMAX_GIT_OBJECT_QUARANTINE_AGE_MS(14 days, Git's owntmp_*expiry) is removed regardless of owner; otherwise it is removed only when the owner shares this pid space and its pid is dead. - Pack import rollback and skip —
keepFetchedPacksskips destination files that already exist and rolls back the files it installed if the.idxmove fails. - Tests — an owner-liveness matrix (including the 15-day live-owner cap), sweep-once coverage, pack rollback/existing-pack cases, the WSL drive-spelling case, and a
conflict-summarymock that prevents a real scratch dir being created.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Two-week hard cap on every scratch dir —
isStaleScratchDirectorynow short-circuits to stale once a dir reachesMAX_GIT_OBJECT_QUARANTINE_AGE_MS(14 days, matching Git's ownobjects/tmp_*expiry), before the owner-liveness branch. This closes the one open edge where a dead owner's pid reused by a long-lived process would otherwise keep a leftover directory in agc.auto=0repository indefinitely. A test pins the live-owner-but-15-days-old case.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
… path - The stale sweep now removes any scratch dir at least two weeks old, Git's own expiry for its objects/tmp_* dirs; no merge-tree runs that long. The owner file and pid-space probe are gone, and with them the cases where a sweeper misjudged another process's pid. - The sweep no longer delays the user's check: it starts without being awaited and can only touch dirs far older than the current run's. - A fetched pack's .keep never moves into the real store, so a transient one from an aborted lazy fetch cannot stop gc from repacking that pack. - Tests: the legacy merge-tree fallback gets the scratch env; the local quarantine's WSL decisions (distro from a UNC path, drvfs spelling, host-Git linked-worktree skip, WSLENV hand-off).
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Reviewed the delta since the prior pullfrog review (d3c2df63ff → 558f13e0cd): a single commit that simplifies the scratch object-store lifecycle.
- Stale sweep reduced to age alone — the owner record (
orca-owner.json, pid/hostname/pidNamespace) and its liveness probe were removed. Atmp_objdir-orca-merge-tree-*dir is now removed solely when its mtime is older thanSTALE_GIT_OBJECT_QUARANTINE_AGE_MS(14 days, matching Git's ownobjects/tmp_*expiry). An in-flight run's dir is always fresh, so the sweep can no longer delete a live one. - Sweep moved off the check's path —
startSweepOncefires the sweep without awaiting, tracked per objects dir in aMap<string, Promise>; a new test gates theobjects/listing to prove the Git command does not wait for it. - Fetched-pack import drops
.keep—keepFetchedPacksexcludes a transient.keep(an aborted lazy fetch can strand one, and gc never repacks a kept pack) while still moving.pack/.idx/.promisorwith.idxlast, skip-if-exists and rollback intact. - Test rework — the owner-liveness matrix became age-threshold/sweep-once cases;
local-git-object-quarantine.test.tsgained Windows-host routing tests (WSLENV forwarding, drive-path/mnt/cspelling, linked-worktree fallback) through a mocked shared module;conflict-summary.test.tsaccounts for the extrarev-parse --git-common-dirread and covers the legacy merge-tree fallback against a real scratch store.
Verified locally: the three changed suites pass (43 tests) and oxlint is clean.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
Status: ready for review (head f7d0cc0)The problem. Orca's pull request conflict check and its squash-merged-branch cleanup both run What changes for the user. Nothing visible: the pull request card and branch cleanup give the same results as Changed in this round: simpler, closer to Git's own handling. The description listed ten differences from the common pattern; three extra mechanisms were removed or cut down:
Verified
Not verified
|
…t sees without it The scratch object store must not change what merge-tree can read, or the conflict list and the squash-merged branch cleanup differ from main: - An inherited GIT_ALTERNATE_OBJECT_DIRECTORIES (from Orca's or the SSH relay's environment) was replaced; it is now kept and the real store appended after it, as Git's own temporary object stores do. - An inherited GIT_OBJECT_DIRECTORY is the store Git reads and writes, so the command now runs unquarantined. On the WSL route a Windows value only counts if WSLENV forwards it, and then the quarantine is skipped too. - The real store's own info/alternates entries are listed before it, so an alternates chain is linked at the same depth as without the quarantine (a 6-deep chain no longer hits Git's nesting limit). An alternates file that cannot be read or holds a quoted entry runs unquarantined.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Reviewed the delta since the prior pullfrog review (558f13e0cd → 710b17dc): one substantive commit plus two origin/main syncs.
- Quarantine now mirrors inherited object stores — a new
inheritedObjectStore(shared) andinheritedLocalObjectStore(local/WSL) decide whether the scratch store can see exactly what Git would. An inheritedGIT_OBJECT_DIRECTORYdisables quarantine (it is the store Git both reads and writes); an inheritedGIT_ALTERNATE_OBJECT_DIRECTORIESis passed through, and on Windows it is dropped whenWSLENVwould not forward it to the distro's Git. - The real store's own
info/alternatesare promoted into the scratch env —readRealStoreAlternatesresolves each entry as Git does (absolute kept, relative joined to the objects path, C-quoted or unreadable files fall back to unquarantined), andquarantineAlternateslists them ahead of the real store so an alternates chain keeps the same nesting depth Git allows without the quarantine. The relay adapter feeds the same inherited state from the relay process env. - Tests — three real-Git cases pin reading objects reachable only through inherited alternates, an inherited
GIT_OBJECT_DIRECTORYwriting where Git would, and a six-deepinfo/alternateschain; unit tests cover the two wrappers, the WSLENV-forwarding decision, and the new quoting/ordering. All five quarantine suites pass locally (52 tests).
The default (no inherited variables, no info/alternates) path is unchanged: quarantineAlternates reduces to the previous single quoted real-store entry.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…own alternates Mirroring Git's alternates resolution could not match it in every shape: a path back to the real store (mutual alternates, or an inherited alternates value naming the repo itself) still linked its chain one level deeper, and a NUL byte in info/alternates broke the spawn. When objects/info/alternates exists, or cannot be checked, the command now runs exactly as before. Repos cloned with --reference or --shared keep writing throwaway objects, as they always have. Inherited alternates are still appended to, and an inherited GIT_OBJECT_DIRECTORY still skips the quarantine.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Reviewed the delta since the prior pullfrog review (710b17dc → 3a96c33b): one commit that simplifies how the quarantine handles a real store with its own alternates.
- Quarantine is skipped for a store with
info/alternates—quarantineAlternatesno longer reads and flattens the real store'sinfo/alternatesinto the scratch env. It now onlystats the file: if it exists, orstatfails for any reason other thanENOENT/ENOTDIR, the command runs unquarantined (the exact pre-PR command). The removed mirroring could still link the chain one level deeper (mutual alternates, or an inherited alternates value naming the repo itself), and a NUL byte in the file broke the spawn. - Tradeoff is bounded and documented — repos cloned with
--reference/--shared(or any store carryinginfo/alternates) revert to leaving throwaway objects, as they did before this PR. The commit message states this. Inherited alternates are still appended to, and an inheritedGIT_OBJECT_DIRECTORYstill skips the quarantine. - Tests reworked around the new predicate — unit cases cover an existing file and an unreadable
info/(both unquarantined) plus a non-directoryinfo(ENOTDIR→ quarantined), alongside a UNC append case and aWSLENV-names-keys-without-values case; the real-Git six-deep chain test now asserts unquarantined writes instead of zero loose objects.
Verified locally: the four changed suites pass (65 tests) and oxlint is clean.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
Status update: same results as
|
…m merge-tree's scratch store A scratch folder left by a crash is a tmp_* dir inside objects/, which Git's own gc prunes after gc.pruneExpire, as it does Git's own leftovers, so Orca's 14-day sweep and its test hooks go. Packs a partial clone fetched into the scratch store are moved in Git's migration order: pack- files only, index last, existing names left in place, stopping at the first failed rename with no rollback. A failed move is logged and never stops the scratch dir's removal or the result. Adds a real-Git test that a partial clone lists its conflicts offline once an earlier check downloaded the contents. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Important
The pack-import rewrite drops two safeguards the PR description still guarantees: the per-pack rollback and the .keep exclusion. keepFetchedPacks still runs in finally (including after a failed or cancelled command) and no longer requires an .idx, so a failed .idx rename can leave an orphan pack pinned by its .keep in the real store. Details inline.
Reviewed changes
- Stale scratch sweep removed —
sweepStaleScratchDirectories/startSweepOnce/STALE_GIT_OBJECT_QUARANTINE_AGE_MSand the test hooks are gone; leftovertmp_objdir-orca-merge-tree-*dirs are now left togit gc. - Pack import simplified —
keepFetchedPacksmoves anypack-*file (.idxlast), skipping existing targets and stopping at the first failed rename with no rollback; the failure is logged and the scratch dir is still removed. - New real-Git coverage — a partial clone whose contents an earlier check downloaded now lists its conflicts offline after the remote URL is made unreachable.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…t --write-tree Git before 2.35 cannot prune a leftover scratch folder under objects/tmp_*, so a crash in the moment one exists makes every later `git gc` warn and stops automatic cleanup. On Git before 2.38 merge-tree --write-tree is rejected, yet the first check per host per session still made and removed a folder around it. Branch cleanup and the PR conflict list now use the scratch object store only when that host's capability cache already knows merge-tree --write-tree works. Until then the command runs exactly as it does on main: on a Git that rejects it no folder is ever created, and every Git that does get one is 2.38+, whose gc prunes it. Also reword two comments in the quarantine module. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
ℹ️ No critical issues — minor suggestions inline.
Reviewed changes
Reviewed the delta since the prior pullfrog review (14c3c5039 → 2fae54e5d): one commit that gates the scratch object store on the host's capability cache.
- Quarantine gated on known support — both callers now run
merge-tree --write-treeunquarantined untilisKnownSupported('merge-tree-write-tree')is true, so a Git that rejects--write-tree(Git < 2.38) or predatesgit gc'stmp_*pruning (Git < 2.35) never gets a scratch folder. conflict-summary.tsreworked — a per-commandrunMergeTreeclosure consults the capability cache and is threaded through the legacy fallback, replacing the previousLocalGitObjectQuarantineparameter.git-branch-cleanup.ts—branchMergesWithoutTreeChangespassesdiscardWrittenObjectsfrom the same capability check instead of alwaystrue.- Tests — capability priming in the real-Git fixtures, new first-run-unquarantined/later-runs-quarantined cases for
conflict-summaryand branch cleanup, a binary-compatibility case pinning one scratch folder across two checks, and comment rewording in the quarantine module.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…check Branch cleanup quarantines merge-tree only once the host's capability cache knows --write-tree works. A conflicting merge-tree exits 1, so the check threw and support was never recorded; on a host whose only checks conflict (likely over SSH, where the relay has no conflict summary to teach its cache) every check kept writing loose objects into the real store. A failure whose stdout starts with a tree ID proves --write-tree ran (old Git that rejects the option prints nothing there), so it now resolves to the same "not merged" result and lets the cache record support. Every other failure is rethrown unchanged. Also shorten two comments and soften a third. Co-Authored-By: Claude <noreply@anthropic.com>
…ee-objects-ready # Conflicts: # src/shared/git-binary-compatibility.test.ts
There was a problem hiding this comment.
ℹ️ No new issues in this delta — the new conflict-learning path is correct.
Reviewed changes
Reviewed the delta since the prior pullfrog review (2fae54e5d8 → b3ae921d94): one behavioral commit plus two origin/main syncs.
- A conflicting check now teaches the capability cache —
git-branch-cleanup.tscatchesmerge-tree --write-tree's conflict exit (exit 1) when its first stdout line is a 40/64-hex tree ID (printedMergedTreeId) and returnsnullinstead of rethrowing, sorunWithFallbackrecordsmerge-tree-write-treeas supported and the next branch-cleanup check runs under the scratch object store. This closes the prior minor finding where a session whose only merge-tree calls conflicted stayed permanently unquarantined. - Tests — unit cases pin that a conflict carrying a SHA-1/SHA-256 tree ID flips the second check to
discardWrittenObjects: true, while empty or non-tree stdout does not; a relay real-Git test removes a conflicting branch from a linked worktree twice and asserts the first check writes loose objects unquarantined, then the second makes exactly one scratch folder and writes none. - Comment rewording only — the capability-gating rationale in
conflict-summary.tsandgit-object-quarantine.tstightened; no behavior change.
Verified locally: git-branch-cleanup.test.ts (16 tests) and relay-git-object-quarantine-real-git.test.ts (3 tests) pass on this head.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…ee-objects-ready # Conflicts: # src/main/git/worktree-branch-removal.ts # src/relay/git-handler.ts # src/shared/git-binary-compatibility.test.ts
…ee-objects-ready # Conflicts: # src/main/git/worktree-branch-removal.ts # src/main/github/client-merge-queue-auto-merge.test.ts # src/main/github/client-pr-conflict-summary.test.ts # src/main/github/client-test-mocks.ts # src/relay/git-handler-branch-cleanup.ts # src/relay/git-handler-operation-context.ts # src/relay/git-handler-ops.ts # src/relay/git-handler.ts # src/shared/git-binary-compatibility.test.ts # src/wsl-guest/relay-git-object-quarantine-real-git.test.ts # src/wsl-guest/relay-git-object-quarantine.test.ts # src/wsl-guest/relay-git-object-quarantine.ts
A WSL login profile that set GIT_ALTERNATE_OBJECT_DIRECTORIES replaced the WSLENV-forwarded value, so a quarantined merge-tree lost the real store and failed where main succeeds. The WSL runner now passes both variables as command-line assignments after the profile, appending to its alternates.
… already pin The cold-capability gating, scratch-folder lifecycle and WSL path spelling are covered end to end by merge-tree-object-quarantine-real-git.test.ts, the real-binary compatibility contract (Git 2.25/2.38/2.49) and the localGitObjectsDirectory tests, so the unit copies only mirrored them.

What this fixes
Orca's background Git checks were permanently growing the user's repository with thousands of junk objects every hour. This PR stops that without changing anything the user sees.
ELI5
To list a pull request's conflicting files, and to check whether a deleted workspace's branch was already squash-merged, Orca does a "practice merge". Git saves every practice result into the repository's permanent storage, even though Orca throws it away, so the repository keeps growing with junk. Now each practice merge writes into a scratch folder instead, and Orca deletes that folder as soon as Git finishes.
What Changed
The problem. On 2026-09-28 the Orca repository on a maintainer's machine gained 18,048 new loose Git objects in one hour and 184,293 in 24 hours; 84% of the sampled new files contained conflict markers. That repository has automatic garbage collection off (
gc.auto=0, the user's choice), so nothing removed them.Why it happened. Both checks ask Git with
git merge-tree --write-tree. Git has no mode that reports the result without also saving the merged files and folder listings into.git/objects. Orca only reads the answer, so every check leaves objects nothing will ever reference.mainstill does this insrc/main/github/conflict-summary.tsandsrc/shared/git-branch-cleanup.ts; its cache only makes it happen less often.What is different for the user. Same results as before. The practice merges no longer add to the repository's permanent storage, apart from the first check per machine per Orca session, which still writes its few objects while Orca learns the Git version. Objects written before this change stay until Git's normal cleanup removes them.
Mechanism.
src/shared/git-object-quarantine.tsruns one Git command with two standard Git variables:GIT_OBJECT_DIRECTORYpoints at a fresh scratch folder,objects/tmp_objdir-orca-merge-tree-XXXXXX, so every write goes there.GIT_ALTERNATE_OBJECT_DIRECTORIESpoints at the realobjectsfolder, so Git still reads every existing object. An inherited value is kept, with the real folder added after it, as Git does for its own scratch folders.--filter, where Git downloads file contents on demand), contents downloaded during the check are moved into the realobjects/pack, index file last, leaving any file already there. Then the scratch folder is deleted (with the existing Windows retry). A failed move or delete is logged and never changes the check's result.src/main/git/local-git-object-quarantine.tsfinds the objects folder with the existingreadRepoCommonDirFromGit, for native Git and WSL Git (it creates the folder through the Windows form of the path and hands WSL Git the Linux form,/mnt/c/...for a Windows drive). Both callers use it: the conflict list (bothmerge-treeforms) and branch cleanup (worktree-branch-removal.ts, which marks only itsmerge-treecalldiscardWrittenObjects).WSL. WSL Git for these commands starts through the user's login shell. The runner now puts both variables on WSL Git's command line, the same way it already passes Git's output language, and appends the real folder to any alternates the shell profile set. Before this round they travelled through
WSLENV, so a profile that setGIT_ALTERNATE_OBJECT_DIRECTORIESreplaced Orca's value and the check failed wheremainsucceeds; that failure is gone.When the scratch folder is not used (the command runs exactly as on
main):merge-tree --write-tree(Git 2.38+), so Git older than 2.38 never gets a scratch folder, and Git older than 2.35, which cannot delete a leftover one, never sees one;GIT_OBJECT_DIRECTORY, or the repository borrows objects from another (objects/info/alternates);Leftover folders. A crash can leave one scratch folder (measured: 0 to 23 objects, under 320 KB), where
mainleaves that much on every check.git gcdeletes it after two weeks, as it does Git's own; in a repository with automatic cleanup turned off it stays until the user runsgit gc, like Git's own.This round: SSH relay part removed.
maindeleted the SSH relay and its Git handlers (#27215, #27644); SSH repositories now run Orca's main-process code on a remote Orca server, so they get this fix through the same code as local repositories. The relay adapter, its Git-handler plumbing and relay-only tests are gone from this PR; nothing moved elsewhere.Why
--name-onlystill writes objects;--quietprints neither the file list nor the merged folder ID the checks need.git gc.git pruneafterwards would walk every reachable object, race with the user's own Git work, and cost far more than the merge.Both variables exist in Git 2.25; the fallback for Git older than 2.38 is unchanged.
Differences from the common pattern
The common pattern is Git's own handling of objects it may throw away: a
tmp_objdir-*folder insideobjects/,GIT_OBJECT_DIRECTORYpointed at it, the real store appended toGIT_ALTERNATE_OBJECT_DIRECTORIES, the folder deleted after use, and crash leftovers deleted bygit gcafter two weeks. This PR uses that as is. The conflicting-file list before a merge and the squash-merge proof before deleting a branch are existing Orca features with no counterpart in the common pattern; this PR leaves both unchanged.GIT_QUARANTINE_PATH. That variable refuses branch and tag updates while objects sit in a scratch folder; these commands never update either.main; the cost is the first check per session leaving its few objects.objects/info/alternates) or an inheritedGIT_OBJECT_DIRECTORYrun as before. Git's chain of borrowed stores stops after five levels, and a scratch folder adds one; running as before keepsmain's result.mainsucceeds.GIT_ALTERNATE_OBJECT_DIRECTORIES. Previously labeled Intended (the check failed wheremainsucceeds). Now the variables are set on WSL Git's command line after the profile, appending to its value, so the profile no longer breaks the check.GIT_OBJECT_DIRECTORYis overridden for these two checks. Native Git skips the scratch folder when that variable is inherited; inside WSL the profile runs after Orca decides. Safe in practice because such a profile points every repository at one object store and breaks Git everywhere else.Not in this PR
main's cleanup decisions are kept exactly, including one known case wheremaindeletes a branch it should keep (a bare-repository project whose HEAD points at the branch being cleaned up); fixing that changes behaviour and is tracked separately.Linked Issue
No tracking issue. Found while investigating slow workspace creation (#23699). Related to #22344 (idle packing of loose objects).
Visual Proof
This round changes runtime behaviour only on the WSL route (how the two variables reach WSL Git), so the earlier macOS live QA on a second Mac stands and the WSL route was re-checked on a real Windows machine (see Testing). The screenshots below are from earlier heads.
No UI change: the app looks identical before and after; the difference is on disk. macOS (earlier round): live QA ran the real app on a second Mac (BEFORE =
main2d358df, AFTER = this PR 610627a, same test repository starting from 0 loose objects), deleting workspaces through the UI:feature-extra(unmerged changes)feature-squashed(squash-merged)feature-extrafeature-squashedA watcher sampling
.git/objectsevery 50-75 ms saw one short-lived scratch folder during each delete on this PR's build and none onmain.Not run live: the pull request card's conflict list, because it needs a GitHub login inside the test app, and test apps are not given credentials. It runs the same shared code, and the real-Git tests cover it.
main, deleting
feature-extra(repository gains 2 loose objects):this PR, deleting
feature-extra(same notice, no new objects):main, then this PR, deleting
feature-squashed(branch deleted, no notice):Windows and WSL (this round, PR head 558f13e vs
main3047353, Windows Git 2.55 and WSL Ubuntu 24.04 Git 2.43, each test repository starting from 0 loose objects):C:\...), deletefeature-extrafeature-squashed\\wsl.localhost\Ubuntu-24.04\...), deletefeature-extraobjectsfolder and was removedfeature-squashedBoth live rounds ran on earlier heads of this PR. The changes since then remove Orca's own leftover-folder cleanup, simplify the partial-clone move (which does nothing in these test repositories because they are not partial clones), and use the scratch folder only once Orca knows the Git supports the command, so the first check per machine per Orca session now writes its few objects as
maindoes and later checks add 0. The comparison harness under Testing measures that on the current head.Not run live on Windows: a
C:\repo whose project runtime is set to WSL. Deleting any workspace in that setup fails onmaintoo ("Access denied: path resolves outside allowed directories") before Git runs, so it is a separate existing bug. That path, and the Git older than 2.31 case, are covered by unit tests.Windows repo,
main(branch kept, 2 new loose objects):Windows repo, this PR (same notice, no new objects):
Repo inside WSL,
main:Repo inside WSL, this PR:
Testing
This round (head
240e830e819, after mergingmain6ade7a5):Comparison against
main, real Git (a harness run outside the repo, not committed): 11 repository shapes (plain; spaces, unicode,:,"and\in the path; linked worktree; inheritedGIT_ALTERNATE_OBJECT_DIRECTORIES; inheritedGIT_OBJECT_DIRECTORY;clone --shared; a 6-deep borrowed-object chain; read-only objects folder) plus a partial clone checked online, online again and offline. 104 branch-cleanup cases and 39 conflict-list cases per run, on Git 2.50.1 and Git 2.25.5, each with nothing yet known about the Git version and after one earlier check: 0 differences frommainin all four runs. Loose objects added after the first check:main10-11 per shape, this PR 0 (the three shapes that run as before by design: 10, same asmain). No scratch folder left behind. On Git 2.25.5 neither adds objects and no scratch folder is created.Real WSL on Windows (Ubuntu 24.04, Git 2.43, Orca's own Git runner through
wsl.exeand the login shell), a repo inside WSL and aC:\repo run with WSL Git: same conflict file list and the same branch-cleanup results asmain; after the first checkmainadded 1 loose object per repo, this PR 0, no scratch folder left.src/main/git/command-runner/wsl-command-resolution-object-store.test.ts(new): runs the WSL command line in a real POSIX shell with and without a profile-setGIT_ALTERNATE_OBJECT_DIRECTORIES; Git sees the real store appended. Fails with the command-line assignments removed.The PR's existing tests (real-Git loose-object counts for both checks from the main and a linked worktree, partial clone online and offline, cold capability cache; scratch-folder unit tests; Git 2.25/2.38/2.49 compatibility contract) pass; the relay suites were removed with the relay.
CI: green at
240e830e819(19 passed, 15 skipped, 0 failed).I manually tested these changes locally
Automated tests added/updated, or explained why not below
AI Disclosure
Written with Claude (Claude Code).
Review
Live app QA ran on macOS, Windows and WSL on earlier heads; this round re-checked the changed WSL route with Orca's real Git runner on Windows. Not tested by hand against a remote Orca server for an SSH repository; it runs the same main-process code as a local repository.
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
mkdtempinside the repository's ownobjects/folder, and only pathsmkdtempreturned are ever deleted.:(;for Git for Windows), so it is quoted when the path contains that separator or a"(and on POSIX also a\). WSL Git gets both variables as assignments on its command line, the same way Orca passes Git's output language. Deletion retries briefly locked files on Windows.rev-parse --git-common-dirper conflict summary or cleanup check, and a folder create and delete. The conflict summary is cached in memory per pair of head and base commits, so this normally runs again only when one of them moves (or after a failed check, which is retried after a minute).Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)