Skip to content
46 changes: 46 additions & 0 deletions apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,52 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
assert.equal(status.aheadOfDefaultCount, 1);
}),
);

it.effect("reports combined staged and unstaged edits to the same file", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
yield* initRepoWithCommit(cwd);
yield* writeTextFile(cwd, "feature.ts", "// line one\n");
yield* git(cwd, ["add", "feature.ts"]);
yield* git(cwd, ["commit", "-m", "add feature"]);
yield* writeTextFile(cwd, "feature.ts", "// line one\n// line two\n");
yield* git(cwd, ["add", "feature.ts"]);
yield* writeTextFile(cwd, "feature.ts", "// line one\n// line two\n// line three\n");

const status = yield* (yield* GitVcsDriver.GitVcsDriver).statusDetails(cwd);

assert.equal(status.isRepo, true);
assert.equal(status.hasWorkingTreeChanges, true);
const file = status.workingTree.files.find((f) => f.path === "feature.ts");
assert.ok(file);
// HEAD has 1 line. Staged has 2 lines (+1). Unstaged has 3 lines (+2 from HEAD).
// Combined net from HEAD: +2 insertions.
assert.equal(file.insertions, 2);
assert.equal(file.deletions, 0);
}),
);

it.effect("reports staged file counts on unborn HEAD without failing", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* driver.initRepo({ cwd });
yield* git(cwd, ["config", "user.email", "test@test.com"]);
yield* git(cwd, ["config", "user.name", "Test"]);
yield* writeTextFile(cwd, "initial.ts", "// first file\n");
yield* git(cwd, ["add", "initial.ts"]);

const status = yield* driver.statusDetails(cwd);

assert.equal(status.isRepo, true);
assert.equal(status.workingTree.files.length, 1);
const file = status.workingTree.files[0];
if (file) {
assert.equal(file.path, "initial.ts");
assert.equal(file.insertions, 1);
}
}),
);
});

describe("refName operations", () => {
Expand Down
104 changes: 76 additions & 28 deletions apps/server/src/vcs/GitVcsDriverCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,12 @@ function isMissingGitCwdError(error: GitCommandError): boolean {
function isNonRepositoryGitStderr(stderr: string): boolean {
return stderr.toLowerCase().includes("not a git repository");
}
function isUnbornHeadStderr(stderr: string): boolean {
return (
stderr.toLowerCase().includes("unknown revision") &&
stderr.toLowerCase().includes("path not in the working tree")
);
}

interface Trace2Monitor {
readonly env: NodeJS.ProcessEnv;
Expand Down Expand Up @@ -1511,27 +1517,73 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
});
}

const [unstagedNumstatStdout, stagedNumstatStdout, defaultRefResult, hasPrimaryRemote] =
yield* Effect.all(
[
runGitStdout("GitVcsDriver.statusDetails.unstagedNumstat", cwd, ["diff", "--numstat"]),
runGitStdout("GitVcsDriver.statusDetails.stagedNumstat", cwd, [
"diff",
"--cached",
"--numstat",
]),
executeGit(
"GitVcsDriver.statusDetails.defaultRef",
cwd,
["symbolic-ref", "refs/remotes/origin/HEAD"],
{
allowNonZeroExit: true,
},
),
originRemoteExists(cwd).pipe(Effect.orElseSucceed(() => false)),
],
{ concurrency: "unbounded" },
);
const [numstatStdout, defaultRefResult, hasPrimaryRemote] = yield* Effect.all(
[
executeGitWithStableDiagnostics(
"GitVcsDriver.statusDetails.numstat",
cwd,
["diff", "HEAD", "--numstat"],
{ allowNonZeroExit: true },
).pipe(
Effect.flatMap((result) => {
if (result.exitCode === 0) return Effect.succeed(result.stdout);
if (isUnbornHeadStderr(result.stderr)) {
return Effect.map(
Effect.all([
runGitStdout("GitVcsDriver.statusDetails.numstat.unborn", cwd, [
"diff",
"--numstat",
]),
runGitStdout("GitVcsDriver.statusDetails.numstat.unborn.staged", cwd, [
"diff",
"--cached",
"--numstat",
]),
]),
([unstagedStdout, stagedStdout]) => {
const staged = parseNumstatEntries(stagedStdout);
const unstaged = parseNumstatEntries(unstagedStdout);
const map = new Map<string, { insertions: number; deletions: number }>();
for (const entry of [...staged, ...unstaged]) {
const existing = map.get(entry.path) ?? {
insertions: 0,
deletions: 0,
};
existing.insertions += entry.insertions;
existing.deletions += entry.deletions;
map.set(entry.path, existing);
}
return Array.from(map.entries())
.map(([p, s]) => `${s.insertions}\t${s.deletions}\t${p}`)
.join("\n");
},
);
}
return Effect.fail(
new GitCommandError({
...gitCommandContext({
operation: "GitVcsDriver.statusDetails.numstat",
cwd,
args: ["diff", "HEAD", "--numstat"],
}),
detail: "git diff HEAD --numstat failed.",
exitCode: result.exitCode,
stdoutLength: result.stdout.length,
stderrLength: result.stderr.length,
}),
);
Comment thread
cursor[bot] marked this conversation as resolved.
}),
),
executeGit(
"GitVcsDriver.statusDetails.defaultRef",
cwd,
["symbolic-ref", "refs/remotes/origin/HEAD"],
{ allowNonZeroExit: true },
),
originRemoteExists(cwd).pipe(Effect.orElseSucceed(() => false)),
],
{ concurrency: "unbounded" },
);
const statusStdout = statusResult.stdout;
const defaultBranch =
defaultRefResult.exitCode === 0
Expand Down Expand Up @@ -1592,14 +1644,10 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
: yield* computeAheadCountAgainstBase(cwd, refName).pipe(Effect.orElseSucceed(() => 0));
}

const stagedEntries = parseNumstatEntries(stagedNumstatStdout);
const unstagedEntries = parseNumstatEntries(unstagedNumstatStdout);
const numstatEntries = parseNumstatEntries(numstatStdout);
const fileStatMap = new Map<string, { insertions: number; deletions: number }>();
for (const entry of [...stagedEntries, ...unstagedEntries]) {
const existing = fileStatMap.get(entry.path) ?? { insertions: 0, deletions: 0 };
existing.insertions += entry.insertions;
existing.deletions += entry.deletions;
fileStatMap.set(entry.path, existing);
for (const entry of numstatEntries) {
fileStatMap.set(entry.path, { insertions: entry.insertions, deletions: entry.deletions });
Comment thread
cursor[bot] marked this conversation as resolved.
}

let insertions = 0;
Expand Down
Loading