From 738afcd12573604506f0dfb71626898aeab267c6 Mon Sep 17 00:00:00 2001 From: Yaacov Date: Thu, 12 Nov 2026 00:04:16 +0200 Subject: [PATCH 1/2] fix(git): keep commit-message staging out of the repository's object store Preparing a commit message stages with a temporary index, but `git add -A` still wrote every staged blob into the repository's object store. A message drafted without a commit left those objects behind until gc pruned them, including large generated files and datasets. Stage into a temporary object directory with the repository's own objects as an alternate. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 69 ++++++++++++++++++++ apps/server/src/vcs/GitVcsDriverCore.ts | 37 ++++++++++- 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 0ca6e092c2cc..36debe189b57 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -5907,6 +5907,75 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { }); describe("commit context", () => { + it.effect("reads history through environment alternates and a chain of shared clones", () => + Effect.gen(function* () { + const root = yield* makeTmpDir(); + const fileSystem = yield* FileSystem.FileSystem; + const driver = yield* GitVcsDriver.GitVcsDriver; + const seed = `${root}/clone-0`; + yield* fileSystem.makeDirectory(seed); + yield* initRepoWithCommit(seed); + let previous = seed; + for (let index = 1; index <= 6; index += 1) { + const next = `${root}/clone-${index}`; + yield* git(root, ["clone", "--quiet", "--shared", previous, next]); + previous = next; + } + // Six shared clones deep is within Git's alternate limit, not one level deeper. + yield* writeTextFile(previous, "README.md", "changed\n"); + const chained = yield* driver.prepareCommitContext(previous, ["README.md"]); + assert.include(chained?.stagedSummary ?? "", "M\tREADME.md"); + assert.include(chained?.stagedPatch ?? "", "-# test"); + + // A borrower whose alternates come only from the environment. + const borrower = `${root}/borrower`; + yield* git(root, ["clone", "--quiet", "--shared", seed, borrower]); + yield* fileSystem.remove(`${borrower}/.git/objects/info/alternates`); + yield* writeTextFile(borrower, "README.md", "changed\n"); + const environment = yield* HostProcess.Environment; + const borrowed = yield* driver.prepareCommitContext(borrower).pipe( + Effect.provideService(HostProcess.Environment, { + ...environment, + GIT_ALTERNATE_OBJECT_DIRECTORIES: `${seed}/.git/objects`, + }), + ); + assert.include(borrowed?.stagedSummary ?? "", "M\tREADME.md"); + assert.include(borrowed?.stagedPatch ?? "", "-# test"); + }), + ); + + it.effect("leaves no objects behind when no commit follows, in a linked worktree too", () => + Effect.gen(function* () { + const repo = yield* makeTmpDir(); + yield* initRepoWithCommit(repo); + const linked = `${repo}/linked`; + yield* git(repo, ["worktree", "add", "--quiet", linked]); + const driver = yield* GitVcsDriver.GitVcsDriver; + + for (const cwd of [repo, linked]) { + yield* writeTextFile(cwd, "README.md", `changed in ${cwd}\n`); + yield* writeTextFile(cwd, "draft.txt", `drafted in ${cwd}\n`); + const blobs = [ + yield* git(cwd, ["hash-object", "draft.txt"]), + yield* git(cwd, ["hash-object", "README.md"]), + ]; + const context = yield* driver.prepareCommitContext(cwd); + assert.include(context?.stagedPatch ?? "", `+drafted in ${cwd}`); + assert.include(context?.stagedPatch ?? "", "-# test"); + for (const blob of blobs) { + const probe = yield* driver.execute({ + operation: "GitVcsDriver.test.git", + cwd, + args: ["cat-file", "-e", blob], + allowNonZeroExit: true, + timeoutMs: 10_000, + }); + assert.notEqual(probe.exitCode, 0, `${blob} stayed in the repository`); + } + } + }), + ); + it.effect("prepares source patches without running textconv", () => Effect.gen(function* () { const cwd = yield* makeTmpDir(); diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index f48f4be26f65..bf227106c421 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -2534,7 +2534,42 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* prefix: "t3code-commit-index-", }); const tempIndexPath = path.join(directory, "index"); - const env = { GIT_INDEX_FILE: tempIndexPath } satisfies NodeJS.ProcessEnv; + // Staging hashes file contents; keep those objects in the temporary directory, read + // beside the repository's own, so a message drafted without a commit leaves nothing. + const objectsValue = yield* runGitStdout( + "GitVcsDriver.prepareCommitContext.objectsPath", + cwd, + ["rev-parse", "--git-path", "objects"], + ); + // Every object directory the repository reads, as Git resolved it (its own alternates, + // nested ones and the environment's), listed directly: one more level of nesting could + // pass Git's alternate depth limit. + const objectDirectories = yield* runGitStdout( + "GitVcsDriver.prepareCommitContext.alternates", + cwd, + ["count-objects", "-v"], + ); + // Git prints an entry C-quoted when it needs quoting; quote the rest the same way, so a + // path holding the list separator stays one entry (Git 2.11.1+). + const quoted = (entry: string) => + entry.startsWith('"') ? entry : `"${entry.replace(/[\\"]/g, "\\$&")}"`; + const alternates = [ + path.resolve(cwd, objectsValue.trim()), + ...objectDirectories + .split("\n") + .flatMap((line) => + line.startsWith("alternate: ") ? [line.slice("alternate: ".length)] : [], + ), + ]; + const tempObjectsPath = path.join(directory, "objects"); + yield* fileSystem.makeDirectory(tempObjectsPath); + const env = { + GIT_INDEX_FILE: tempIndexPath, + GIT_OBJECT_DIRECTORY: tempObjectsPath, + GIT_ALTERNATE_OBJECT_DIRECTORIES: alternates + .map(quoted) + .join(hostPlatform === "win32" ? ";" : ":"), + } satisfies NodeJS.ProcessEnv; const config = [ "-c", "core.splitIndex=false", From 10d10c270cebdb12ddc9ab5a524485b98a9948f3 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 22:25:02 +0000 Subject: [PATCH 2/2] refactor(server): resolve commit-context git paths in one call Read the index and objects paths with a single rev-parse, tighten the comments, and drop the environment-alternates test case, which the shared-clone chain already covers. AI-assisted; verified with the tests and tools listed above. --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 17 +--------------- apps/server/src/vcs/GitVcsDriverCore.ts | 21 +++++++------------- 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 5fbbb4a37fea..19b79d72748a 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -5929,7 +5929,7 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { }); describe("commit context", () => { - it.effect("reads history through environment alternates and a chain of shared clones", () => + it.effect("reads history through a chain of shared clones", () => Effect.gen(function* () { const root = yield* makeTmpDir(); const fileSystem = yield* FileSystem.FileSystem; @@ -5948,21 +5948,6 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { const chained = yield* driver.prepareCommitContext(previous, ["README.md"]); assert.include(chained?.stagedSummary ?? "", "M\tREADME.md"); assert.include(chained?.stagedPatch ?? "", "-# test"); - - // A borrower whose alternates come only from the environment. - const borrower = `${root}/borrower`; - yield* git(root, ["clone", "--quiet", "--shared", seed, borrower]); - yield* fileSystem.remove(`${borrower}/.git/objects/info/alternates`); - yield* writeTextFile(borrower, "README.md", "changed\n"); - const environment = yield* HostProcess.Environment; - const borrowed = yield* driver.prepareCommitContext(borrower).pipe( - Effect.provideService(HostProcess.Environment, { - ...environment, - GIT_ALTERNATE_OBJECT_DIRECTORIES: `${seed}/.git/objects`, - }), - ); - assert.include(borrowed?.stagedSummary ?? "", "M\tREADME.md"); - assert.include(borrowed?.stagedPatch ?? "", "-# test"); }), ); diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 64e52d95b6c9..3d1ef080f08d 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -2526,26 +2526,19 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* Effect.fn("prepareCommitContext")(function* (cwd, filePaths, options) { return yield* Effect.scoped( Effect.gen(function* () { - const indexValue = yield* runGitStdout( - "GitVcsDriver.prepareCommitContext.indexPath", + const [indexValue = "", objectsValue = ""] = (yield* runGitStdout( + "GitVcsDriver.prepareCommitContext.gitPaths", cwd, - ["rev-parse", "--git-path", "index"], - ); + ["rev-parse", "--git-path", "index", "--git-path", "objects"], + )).split("\n"); const indexPath = path.resolve(cwd, indexValue.trim()); const directory = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-commit-index-", }); const tempIndexPath = path.join(directory, "index"); - // Staging hashes file contents; keep those objects in the temporary directory, read - // beside the repository's own, so a message drafted without a commit leaves nothing. - const objectsValue = yield* runGitStdout( - "GitVcsDriver.prepareCommitContext.objectsPath", - cwd, - ["rev-parse", "--git-path", "objects"], - ); - // Every object directory the repository reads, as Git resolved it (its own alternates, - // nested ones and the environment's), listed directly: one more level of nesting could - // pass Git's alternate depth limit. + // Staging writes blobs; put them in the temp directory so a draft leaves nothing behind. + // List every object directory Git resolved (nested and environment alternates) flat, + // since nesting the repository's objects one level deeper can pass Git's depth limit. const objectDirectories = yield* runGitStdout( "GitVcsDriver.prepareCommitContext.alternates", cwd,