fix code diff view

This commit is contained in:
Arjun 2026-06-12 14:10:08 +05:30
parent 52772dd8dd
commit 65f0d518eb

View file

@ -67,6 +67,17 @@ export async function repoInfo(cwd: string): Promise<GitRepoInfo> {
return { isGitRepo: true, branch, hasCommits, dirtyCount };
}
// git status/diff report paths relative to the REPO ROOT, which is not the
// session cwd when the user opened a subdirectory of a repo as their project.
// Disk reads must resolve against the root, not cwd.
async function repoToplevel(cwd: string): Promise<string> {
try {
return (await git(cwd, ['rev-parse', '--show-toplevel'])).trim() || cwd;
} catch {
return cwd;
}
}
function stateFromPorcelain(xy: string): GitFileState {
if (xy === '??') return 'untracked';
if (xy.includes('R')) return 'renamed';
@ -75,10 +86,14 @@ function stateFromPorcelain(xy: string): GitFileState {
return 'modified';
}
// Working-tree changes vs HEAD with insertion/deletion counts. Untracked files
// get their line count from disk (capped) since numstat doesn't cover them.
// Working-tree changes vs HEAD with insertion/deletion counts, scoped to the
// session directory's subtree (`-- .`): a project opened inside a bigger repo
// only shows its own changes. Result paths are repo-root-relative (git's
// porcelain format). Untracked files get their line count from disk (capped)
// since numstat doesn't cover them.
export async function status(cwd: string): Promise<GitStatusFile[]> {
const out = await git(cwd, ['status', '--porcelain=v1', '-z']);
const root = await repoToplevel(cwd);
const out = await git(cwd, ['status', '--porcelain=v1', '-z', '--', '.']);
const entries: Array<{ path: string; state: GitFileState }> = [];
// -z format: "XY path\0" and for renames "XY newPath\0oldPath\0"
const parts = out.split('\0');
@ -94,7 +109,7 @@ export async function status(cwd: string): Promise<GitStatusFile[]> {
const counts = new Map<string, { insertions: number | null; deletions: number | null }>();
try {
const numstat = await git(cwd, ['diff', 'HEAD', '--numstat', '-z']);
const numstat = await git(cwd, ['diff', 'HEAD', '--numstat', '-z', '--', '.']);
// -z numstat rows: "ins\tdel\tpath\0" (renames: "ins\tdel\0old\0new\0")
const rows = numstat.split('\0');
for (let i = 0; i < rows.length; i++) {
@ -126,7 +141,7 @@ export async function status(cwd: string): Promise<GitStatusFile[]> {
deletions = counted.deletions;
} else if (entry.state === 'untracked') {
try {
const full = path.join(cwd, entry.path);
const full = path.join(root, entry.path);
const stat = await fs.stat(full);
if (stat.isFile() && stat.size <= MAX_TEXT_BYTES) {
const content = await fs.readFile(full, 'utf8');
@ -154,16 +169,33 @@ export interface FileDiff {
}
export async function fileDiff(cwd: string, relPath: string): Promise<FileDiff> {
// Paths from `status` are repo-root-relative; paths clicked in the chat
// timeline are cwd-relative. Resolve whichever interpretation points at a
// real file (deleted files fall back to the root interpretation, which is
// also what `git show` uses).
const root = await repoToplevel(cwd);
let gitPath = relPath;
let full = path.join(root, relPath);
const existsAt = async (p: string) => fs.stat(p).then((s) => s.isFile()).catch(() => false);
if (!await existsAt(full)) {
const cwdFull = path.join(cwd, relPath);
if (await existsAt(cwdFull)) {
full = cwdFull;
// Realpath both sides — git reports the real toplevel, while the
// session cwd may reach it through a symlink (e.g. /tmp on macOS).
const realFull = await fs.realpath(cwdFull).catch(() => cwdFull);
gitPath = path.relative(root, realFull).split(path.sep).join('/');
}
}
let oldText = '';
try {
oldText = await git(cwd, ['show', `HEAD:${relPath}`]);
oldText = await git(cwd, ['show', `HEAD:${gitPath}`]);
} catch {
// untracked / newly added / no commits — diff against empty
oldText = '';
}
let newText = '';
try {
const full = path.join(cwd, relPath);
const stat = await fs.stat(full);
if (stat.size > MAX_TEXT_BYTES) {
return { oldText: '', newText: '', isBinary: false, tooLarge: true };