Compare commits

...
Author SHA1 Message Date
Mohamed Boudra 00870740ef Fix committed diff excluding dirty worktree 2026-04-21 15:00:08 +07:00
2 changed files with 116 additions and 29 deletions
@@ -615,6 +615,41 @@ const x = 1;
expect(diff.diff).toContain("feature.txt"); expect(diff.diff).toContain("feature.txt");
}); });
it("does not include dirty working tree changes in base mode", async () => {
writeFileSync(join(repoDir, "file.txt"), "dirty\n");
writeFileSync(join(repoDir, "untracked.txt"), "untracked\n");
const diff = await getCheckoutDiff(repoDir, {
mode: "base",
baseRef: "main",
includeStructured: true,
});
expect(diff.diff).toBe("");
expect(diff.structured).toEqual([]);
});
it("shows committed branch changes without dirty working tree changes in base mode", async () => {
execSync("git checkout -b feature", { cwd: repoDir });
writeFileSync(join(repoDir, "feature.txt"), "feature\n");
execSync("git add feature.txt", { cwd: repoDir });
execSync("git -c commit.gpgsign=false commit -m 'feature commit'", { cwd: repoDir });
writeFileSync(join(repoDir, "file.txt"), "dirty\n");
writeFileSync(join(repoDir, "untracked.txt"), "untracked\n");
const diff = await getCheckoutDiff(repoDir, {
mode: "base",
baseRef: "main",
includeStructured: true,
});
expect(diff.diff).toContain("feature.txt");
expect(diff.diff).not.toContain("file.txt");
expect(diff.diff).not.toContain("untracked.txt");
expect(diff.structured?.map((file) => file.path)).toEqual(["feature.txt"]);
});
it("warms shortstat cache in the background without blocking listing callers", async () => { it("warms shortstat cache in the background without blocking listing callers", async () => {
expect(getCachedCheckoutShortstat(repoDir)).toBeUndefined(); expect(getCachedCheckoutShortstat(repoDir)).toBeUndefined();
@@ -1524,6 +1559,36 @@ const x = 1;
expect(baseDiff.diff).not.toContain("file.txt"); expect(baseDiff.diff).not.toContain("file.txt");
}); });
it("excludes dirty working tree changes from Paseo worktree base diffs", async () => {
const worktree = await createLegacyWorktreeForTest({
branchName: "feature",
cwd: repoDir,
baseBranch: "main",
worktreeSlug: "dirty-feature",
paseoHome,
});
writeFileSync(join(worktree.worktreePath, "feature.txt"), "feature\n");
execSync("git add feature.txt", { cwd: worktree.worktreePath });
execSync("git -c commit.gpgsign=false commit -m 'feature commit'", {
cwd: worktree.worktreePath,
});
writeFileSync(join(worktree.worktreePath, "file.txt"), "dirty\n");
writeFileSync(join(worktree.worktreePath, "untracked.txt"), "untracked\n");
const baseDiff = await getCheckoutDiff(
worktree.worktreePath,
{ mode: "base", includeStructured: true },
{ paseoHome },
);
expect(baseDiff.diff).toContain("feature.txt");
expect(baseDiff.diff).not.toContain("file.txt");
expect(baseDiff.diff).not.toContain("untracked.txt");
expect(baseDiff.structured?.map((file) => file.path)).toEqual(["feature.txt"]);
});
it("resolves the repository default branch from origin HEAD", async () => { it("resolves the repository default branch from origin HEAD", async () => {
execSync("git checkout -b develop", { cwd: repoDir }); execSync("git checkout -b develop", { cwd: repoDir });
execSync("git checkout main", { cwd: repoDir }); execSync("git checkout main", { cwd: repoDir });
+51 -29
View File
@@ -103,6 +103,16 @@ type CheckoutFileChange = {
isUntracked?: boolean; isUntracked?: boolean;
}; };
type CheckoutDiffRefs = {
baseRef: string;
targetRef?: string;
includeUntracked: boolean;
};
function getCheckoutDiffRefArgs(refs: CheckoutDiffRefs): string[] {
return [refs.baseRef, ...(refs.targetRef ? [refs.targetRef] : [])];
}
function normalizeBranchSuggestionName(raw: string): string | null { function normalizeBranchSuggestionName(raw: string): string | null {
const trimmed = raw.trim(); const trimmed = raw.trim();
if (!trimmed) { if (!trimmed) {
@@ -358,7 +368,7 @@ export async function checkoutResolvedBranch(
async function listCheckoutFileChanges( async function listCheckoutFileChanges(
cwd: string, cwd: string,
ref: string, refs: CheckoutDiffRefs,
ignoreWhitespace = false, ignoreWhitespace = false,
): Promise<CheckoutFileChange[]> { ): Promise<CheckoutFileChange[]> {
const changes: CheckoutFileChange[] = []; const changes: CheckoutFileChange[] = [];
@@ -366,7 +376,7 @@ async function listCheckoutFileChanges(
const { stdout: nameStatusOut } = await runGitCommand( const { stdout: nameStatusOut } = await runGitCommand(
buildGitDiffArgs({ buildGitDiffArgs({
ignoreWhitespace, ignoreWhitespace,
extra: ["--name-status", ref], extra: ["--name-status", ...getCheckoutDiffRefArgs(refs)],
}), }),
{ cwd, env: READ_ONLY_GIT_ENV }, { cwd, env: READ_ONLY_GIT_ENV },
); );
@@ -405,24 +415,26 @@ async function listCheckoutFileChanges(
}); });
} }
const { stdout: untrackedOut } = await runGitCommand( if (refs.includeUntracked) {
["ls-files", "--others", "--exclude-standard"], const { stdout: untrackedOut } = await runGitCommand(
{ ["ls-files", "--others", "--exclude-standard"],
cwd, {
env: READ_ONLY_GIT_ENV, cwd,
}, env: READ_ONLY_GIT_ENV,
); },
for (const file of untrackedOut );
.split("\n") for (const file of untrackedOut
.map((l) => l.trim()) .split("\n")
.filter(Boolean)) { .map((l) => l.trim())
changes.push({ .filter(Boolean)) {
path: file, changes.push({
status: "U", path: file,
isNew: true, status: "U",
isDeleted: false, isNew: true,
isUntracked: true, isDeleted: false,
}); isUntracked: true,
});
}
} }
// Deduplicate by path (prefer tracked status over untracked marker if both appear). // Deduplicate by path (prefer tracked status over untracked marker if both appear).
@@ -495,13 +507,13 @@ const TRACKED_MAX_CHANGED_LINES = 40_000;
async function getTrackedNumstatByPath( async function getTrackedNumstatByPath(
cwd: string, cwd: string,
ref: string, refs: CheckoutDiffRefs,
ignoreWhitespace = false, ignoreWhitespace = false,
): Promise<Map<string, FileStat>> { ): Promise<Map<string, FileStat>> {
const result = await runGitCommand( const result = await runGitCommand(
buildGitDiffArgs({ buildGitDiffArgs({
ignoreWhitespace, ignoreWhitespace,
extra: ["--numstat", ref], extra: ["--numstat", ...getCheckoutDiffRefArgs(refs)],
}), }),
{ {
cwd, cwd,
@@ -1507,10 +1519,10 @@ export async function getCheckoutDiff(
): Promise<CheckoutDiffResult> { ): Promise<CheckoutDiffResult> {
await requireGitRepo(cwd); await requireGitRepo(cwd);
let refForDiff: string; let refsForDiff: CheckoutDiffRefs;
if (compare.mode === "uncommitted") { if (compare.mode === "uncommitted") {
refForDiff = "HEAD"; refsForDiff = { baseRef: "HEAD", includeUntracked: true };
} else { } else {
const configured = await getConfiguredBaseRefForCwd(cwd, context); const configured = await getConfiguredBaseRefForCwd(cwd, context);
const baseRef = configured.baseRef ?? compare.baseRef ?? (await resolveBaseRef(cwd)); const baseRef = configured.baseRef ?? compare.baseRef ?? (await resolveBaseRef(cwd));
@@ -1522,11 +1534,15 @@ export async function getCheckoutDiff(
} }
const bestBaseRef = await resolveBestComparisonBaseRef(cwd, baseRef); const bestBaseRef = await resolveBestComparisonBaseRef(cwd, baseRef);
refForDiff = (await tryResolveMergeBase(cwd, bestBaseRef)) ?? bestBaseRef; refsForDiff = {
baseRef: (await tryResolveMergeBase(cwd, bestBaseRef)) ?? bestBaseRef,
targetRef: "HEAD",
includeUntracked: false,
};
} }
const ignoreWhitespace = compare.ignoreWhitespace === true; const ignoreWhitespace = compare.ignoreWhitespace === true;
const changes = await listCheckoutFileChanges(cwd, refForDiff, ignoreWhitespace); const changes = await listCheckoutFileChanges(cwd, refsForDiff, ignoreWhitespace);
changes.sort((a, b) => { changes.sort((a, b) => {
if (a.path === b.path) return 0; if (a.path === b.path) return 0;
return a.path < b.path ? -1 : 1; return a.path < b.path ? -1 : 1;
@@ -1557,7 +1573,7 @@ export async function getCheckoutDiff(
const trackedNumstatByPath = const trackedNumstatByPath =
trackedChanges.length > 0 trackedChanges.length > 0
? await getTrackedNumstatByPath(cwd, refForDiff, ignoreWhitespace) ? await getTrackedNumstatByPath(cwd, refsForDiff, ignoreWhitespace)
: new Map<string, FileStat>(); : new Map<string, FileStat>();
const trackedDiffPaths: string[] = []; const trackedDiffPaths: string[] = [];
const trackedPlaceholderByPath = new Map< const trackedPlaceholderByPath = new Map<
@@ -1584,7 +1600,7 @@ export async function getCheckoutDiff(
const trackedDiffResult = await runGitCommand( const trackedDiffResult = await runGitCommand(
buildGitDiffArgs({ buildGitDiffArgs({
ignoreWhitespace, ignoreWhitespace,
extra: [refForDiff, "--", ...trackedDiffPaths], extra: [...getCheckoutDiffRefArgs(refsForDiff), "--", ...trackedDiffPaths],
}), }),
{ {
cwd, cwd,
@@ -1621,7 +1637,13 @@ export async function getCheckoutDiff(
return null; return null;
} }
const refPath = change.oldPath ?? change.path; const refPath = change.oldPath ?? change.path;
return readGitFileContentAtRef(cwd, refForDiff, refPath); return readGitFileContentAtRef(cwd, refsForDiff.baseRef, refPath);
},
getNewFileContent: async (file) => {
if (!refsForDiff.targetRef) {
return null;
}
return readGitFileContentAtRef(cwd, refsForDiff.targetRef, file.path);
}, },
}) })
: []; : [];