Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";

import { GitCommandError } from "@t3tools/contracts";
import { ServerConfig } from "../config.ts";
import { splitNullSeparatedGitStdoutPaths } from "./GitVcsDriverCore.ts";
import {
isStaleGitTemporaryPackFile,
splitNullSeparatedGitStdoutPaths,
} from "./GitVcsDriverCore.ts";
import * as GitVcsDriver from "./GitVcsDriver.ts";

const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), {
Expand Down Expand Up @@ -95,6 +98,46 @@ const initRepoWithCommit = (
return { initialBranch };
});

describe("isStaleGitTemporaryPackFile", () => {
it("only selects temporary packs older than the cleanup grace period", () => {
const nowMs = 10_000;
const staleAfterMs = 1_000;

assert.isTrue(
isStaleGitTemporaryPackFile({
entry: "tmp_pack_stale",
modifiedAtMs: 9_000,
nowMs,
staleAfterMs,
}),
);
assert.isFalse(
isStaleGitTemporaryPackFile({
entry: "tmp_pack_active",
modifiedAtMs: 9_001,
nowMs,
staleAfterMs,
}),
);
assert.isFalse(
isStaleGitTemporaryPackFile({
entry: "pack-complete.pack",
modifiedAtMs: 1_000,
nowMs,
staleAfterMs,
}),
);
assert.isFalse(
isStaleGitTemporaryPackFile({
entry: "tmp_pack_unknown",
modifiedAtMs: null,
nowMs,
staleAfterMs,
}),
);
});
});

it.effect("uses stable diagnostics for every parsed non-repository command", () => {
const commands: Array<{ readonly args: ReadonlyArray<string>; readonly lcAll?: string }> = [];
const spawner = ChildProcessSpawner.make((command) =>
Expand Down Expand Up @@ -422,9 +465,26 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
refreshUpstream: false,
});
const refreshedStatus = yield* driver.statusDetailsRemote(cwd);
const fileSystem = yield* FileSystem.FileSystem;
const pathService = yield* Path.Path;
const gitCommonDirRaw = yield* git(cwd, ["rev-parse", "--git-common-dir"]);
const gitCommonDir = pathService.isAbsolute(gitCommonDirRaw)
? gitCommonDirRaw
: pathService.resolve(cwd, gitCommonDirRaw);
const isolatedFetchRoot = pathService.join(gitCommonDir, "objects", "t3-status-fetch");
const isolatedFetchEntries = (yield* fileSystem.exists(isolatedFetchRoot))
? yield* fileSystem.readDirectory(isolatedFetchRoot, { recursive: false })
: [];
const temporaryRefs = yield* git(cwd, [
"for-each-ref",
"--format=%(refname)",
"refs/t3-status-fetch",
]);

assert.equal(cachedStatus.behindCount, 0);
assert.equal(refreshedStatus.behindCount, 1);
assert.deepStrictEqual(isolatedFetchEntries, []);
assert.equal(temporaryRefs, "");
}),
);

Expand Down
256 changes: 241 additions & 15 deletions apps/server/src/vcs/GitVcsDriverCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ const STATUS_UPSTREAM_REFRESH_TIMEOUT = Duration.seconds(5);

const STATUS_UPSTREAM_REFRESH_FAILURE_COOLDOWN = Duration.seconds(5);
const STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY = 2_048;
const GIT_TEMPORARY_PACK_PREFIX = "tmp_pack_";
const GIT_TEMPORARY_PACK_STALE_AGE = Duration.hours(1);
const GIT_STATUS_FETCH_OBJECT_DIRECTORY = "t3-status-fetch";
const GIT_STATUS_FETCH_REF_PREFIX = "refs/t3-status-fetch";
const GIT_ZERO_OID = "0".repeat(40);
const STATUS_UPSTREAM_REFRESH_ENV = Object.freeze({
GCM_INTERACTIVE: "never",
GIT_ASKPASS: "",
Expand Down Expand Up @@ -93,6 +98,7 @@ type TraceTailState = {
class StatusRemoteRefreshCacheKey extends Data.Class<{
gitCommonDir: string;
remoteName: string;
branchName: string;
}> {}

interface ExecuteGitOptions {
Expand Down Expand Up @@ -227,6 +233,19 @@ export function splitNullSeparatedGitStdoutPaths(
return splitNullSeparatedPaths(result.stdout, result.stdoutTruncated);
}

export function isStaleGitTemporaryPackFile(input: {
entry: string;
modifiedAtMs: number | null;
nowMs: number;
staleAfterMs: number;
}): boolean {
return (
input.entry.startsWith(GIT_TEMPORARY_PACK_PREFIX) &&
input.modifiedAtMs !== null &&
input.nowMs - input.modifiedAtMs >= input.staleAfterMs
);
}

function sanitizeRemoteName(value: string): string {
const sanitized = value
.trim()
Expand Down Expand Up @@ -929,20 +948,226 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
const fetchRemoteForStatus = (
gitCommonDir: string,
remoteName: string,
): Effect.Effect<void, GitCommandError> => {
const fetchCwd =
path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir;
return executeGit(
"GitVcsDriver.fetchRemoteForStatus",
fetchCwd,
["--git-dir", gitCommonDir, "fetch", "--quiet", "--no-tags", remoteName],
{
allowNonZeroExit: true,
env: STATUS_UPSTREAM_REFRESH_ENV,
timeoutMs: Duration.toMillis(STATUS_UPSTREAM_REFRESH_TIMEOUT),
},
).pipe(Effect.asVoid);
};
branchName: string,
): Effect.Effect<void, GitCommandError> =>
Effect.gen(function* () {
const fetchCwd =
path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir;
const objectDirectory = path.join(gitCommonDir, "objects");
const packDirectory = path.join(objectDirectory, "pack");
// Older builds wrote interrupted fetches into the shared pack directory.
// New fetches use the isolated object directory below; this only reaps legacy leftovers.
const cleanupStaleTemporaryPacks = Effect.gen(function* () {
if (!(yield* fileSystem.exists(packDirectory))) return;
const now = yield* DateTime.now;
const nowMs = DateTime.toEpochMillis(now);
const entries = yield* fileSystem.readDirectory(packDirectory, { recursive: false });
yield* Effect.forEach(
entries,
(entry) => {
if (!entry.startsWith(GIT_TEMPORARY_PACK_PREFIX)) return Effect.void;
const temporaryPackPath = path.join(packDirectory, entry);
return fileSystem.stat(temporaryPackPath).pipe(
Effect.flatMap((info) => {
const modifiedAtMs = Option.getOrNull(info.mtime)?.getTime() ?? null;
return isStaleGitTemporaryPackFile({
entry,
modifiedAtMs,
nowMs,
staleAfterMs: Duration.toMillis(GIT_TEMPORARY_PACK_STALE_AGE),
})
? fileSystem.remove(temporaryPackPath, { force: true })
: Effect.void;
}),
Effect.ignore,
);
},
{ concurrency: "unbounded", discard: true },
);
}).pipe(
Effect.catch((cause) =>
Effect.logWarning("Failed to clean stale temporary Git pack files.", { cause }),
),
);

yield* cleanupStaleTemporaryPacks;
const operationId = yield* crypto.randomUUIDv4.pipe(
Effect.mapError(
(cause) =>
new GitCommandError({
operation: "GitVcsDriver.fetchRemoteForStatus",
command: "crypto.randomUUIDv4",
cwd: fetchCwd,
detail: "Failed to create an isolated status fetch identifier.",
cause,
}),
),
);
const temporaryRoot = path.join(
objectDirectory,
GIT_STATUS_FETCH_OBJECT_DIRECTORY,
operationId,
);
const temporaryObjectDirectory = path.join(temporaryRoot, "objects");
const temporaryPackDirectory = path.join(temporaryObjectDirectory, "pack");
const temporaryRef = `${GIT_STATUS_FETCH_REF_PREFIX}/${operationId}`;
const targetRef = `refs/remotes/${remoteName}/${branchName}`;

yield* fileSystem.makeDirectory(temporaryPackDirectory, { recursive: true }).pipe(
Effect.mapError(
(cause) =>
new GitCommandError({
operation: "GitVcsDriver.fetchRemoteForStatus",
command: "filesystem.makeDirectory",
cwd: fetchCwd,
detail: "Failed to create an isolated Git object directory for status fetch.",
cause,
}),
),
);

const cleanupIsolatedFetch = Effect.all(
[
executeGit(
"GitVcsDriver.fetchRemoteForStatus.cleanupRef",
fetchCwd,
["--git-dir", gitCommonDir, "update-ref", "-d", temporaryRef],
{ allowNonZeroExit: true, timeoutMs: 2_000 },
).pipe(Effect.ignore),
fileSystem.remove(temporaryRoot, { recursive: true, force: true }).pipe(Effect.ignore),
],
{ concurrency: "unbounded", discard: true },
);

const promoteFile = (source: string, destination: string) =>
fileSystem
.rename(source, destination)
.pipe(
Effect.catch((cause) =>
fileSystem
.exists(destination)
.pipe(
Effect.flatMap((exists) =>
exists ? fileSystem.remove(source, { force: true }) : Effect.fail(cause),
),
),
),
);
const promoteObjectFiles = Effect.gen(function* () {
const objectEntries = yield* fileSystem.readDirectory(temporaryObjectDirectory, {
recursive: false,
});
for (const entry of objectEntries) {
if (entry === "pack") {
const packEntries = yield* fileSystem.readDirectory(temporaryPackDirectory, {
recursive: false,
});
yield* Effect.forEach(
packEntries.filter((packEntry) => packEntry.startsWith("pack-")),
(packEntry) =>
promoteFile(
path.join(temporaryPackDirectory, packEntry),
path.join(packDirectory, packEntry),
),
{ concurrency: 1, discard: true },
);
continue;
}
if (!/^[0-9a-f]{2}$/u.test(entry)) continue;
const sourceDirectory = path.join(temporaryObjectDirectory, entry);
const destinationDirectory = path.join(objectDirectory, entry);
yield* fileSystem.makeDirectory(destinationDirectory, { recursive: true });
const looseObjects = yield* fileSystem.readDirectory(sourceDirectory, {
recursive: false,
});
yield* Effect.forEach(
looseObjects,
(looseObject) =>
promoteFile(
path.join(sourceDirectory, looseObject),
path.join(destinationDirectory, looseObject),
),
{ concurrency: 1, discard: true },
);
}
}).pipe(
Effect.mapError(
(cause) =>
new GitCommandError({
operation: "GitVcsDriver.fetchRemoteForStatus",
command: "filesystem.rename",
cwd: fetchCwd,
detail: "Failed to promote isolated Git objects.",
cause,
}),
),
);

yield* Effect.gen(function* () {
const previousTarget = yield* executeGit(
"GitVcsDriver.fetchRemoteForStatus.readTarget",
fetchCwd,
["--git-dir", gitCommonDir, "rev-parse", "--verify", targetRef],
{ allowNonZeroExit: true },
);
const expectedTarget =
previousTarget.exitCode === 0 ? previousTarget.stdout.trim() : GIT_ZERO_OID;
const result = yield* executeGit(
"GitVcsDriver.fetchRemoteForStatus",
fetchCwd,
[
"--git-dir",
gitCommonDir,
"-c",
"fetch.unpackLimit=0",
"fetch",
"--quiet",
"--no-tags",
"--no-write-fetch-head",
"--refmap=",
remoteName,
`+refs/heads/${branchName}:${temporaryRef}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong remote refspec assumed

Medium Severity

The new status fetch builds +refs/heads/${branchName}:… from the remote-tracking shorthand. That assumes a 1:1 refs/heads/* mapping, unlike the previous git fetch &lt;remote&gt; which honored configured refspecs, so custom upstreams (for example pull-request refs) can fail to refresh and leave behindCount stale.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 46ac662. Configure here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default-branch tip goes stale

Medium Severity

Status refresh now fetches only refs/heads/${branchName} into a temporary ref, instead of refreshing the whole upstream remote. On a feature branch, aheadOfDefaultCount still compares against local origin/main (via computeAheadCountAgainstBase), so that metric can stay wrong until some other full fetch updates the default remote-tracking tip.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 46ac662. Configure here.

],
{
allowNonZeroExit: true,
env: {
...STATUS_UPSTREAM_REFRESH_ENV,
GIT_OBJECT_DIRECTORY: temporaryObjectDirectory,
GIT_ALTERNATE_OBJECT_DIRECTORIES: objectDirectory,
},
timeoutMs: Duration.toMillis(STATUS_UPSTREAM_REFRESH_TIMEOUT),
},
);
if (result.exitCode !== 0) return;

yield* fileSystem.makeDirectory(packDirectory, { recursive: true }).pipe(
Effect.mapError(
(cause) =>
new GitCommandError({
operation: "GitVcsDriver.fetchRemoteForStatus",
command: "filesystem.makeDirectory",
cwd: fetchCwd,
detail: "Failed to prepare the Git pack directory.",
cause,
}),
),
);
yield* promoteObjectFiles;
const fetchedCommit = yield* runGitStdout(
"GitVcsDriver.fetchRemoteForStatus.resolve",
fetchCwd,
["--git-dir", gitCommonDir, "rev-parse", "--verify", temporaryRef],
).pipe(Effect.map((stdout) => stdout.trim()));
yield* runGit("GitVcsDriver.fetchRemoteForStatus.promoteRef", fetchCwd, [
"--git-dir",
gitCommonDir,
"update-ref",
targetRef,
fetchedCommit,
expectedTarget,
]);
}).pipe(Effect.ensuring(cleanupIsolatedFetch));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned fetch dirs never reaped

High Severity

The new isolated status-fetch mechanism creates temporary directories under objects/t3-status-fetch/. Its cleanup is best-effort (Effect.ignore) and lacks a stale-reaping mechanism for these new directories, unlike the legacy tmp_pack_* files. This can leave behind accumulated directories if cleanup fails, leading to disk space exhaustion.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 46ac662. Configure here.

});

const resolveGitCommonDir = Effect.fn("resolveGitCommonDir")(function* (cwd: string) {
const gitCommonDir = yield* runGitStdout("GitVcsDriver.resolveGitCommonDir", cwd, [
Expand All @@ -955,7 +1180,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
const refreshStatusRemoteCacheEntry = Effect.fn("refreshStatusRemoteCacheEntry")(function* (
cacheKey: StatusRemoteRefreshCacheKey,
) {
yield* fetchRemoteForStatus(cacheKey.gitCommonDir, cacheKey.remoteName);
yield* fetchRemoteForStatus(cacheKey.gitCommonDir, cacheKey.remoteName, cacheKey.branchName);
return true as const;
});

Expand All @@ -979,6 +1204,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
new StatusRemoteRefreshCacheKey({
gitCommonDir,
remoteName: upstream.remoteName,
branchName: upstream.branchName,
}),
);
});
Expand Down
Loading