feat(git): inline file diffs in commit history rows (#1291)
* chore: add .worktrees/ to gitignore for worktree workflow * feat(git): add getCommitFileDiff service function * docs(git): document getCommitFileDiff in module docs * feat(git): add GET /api/git/commit-file-diff route * feat(git): add CommitFileDiffResponse type and GitAPI method signature * feat(git): add getCommitFileDiff HTTP client function * feat(git): add getCommitFileDiff API facade * feat(git): add getCommitFileDiff stub to VS Code bridge * feat(git): add getCommitFileDiff to VS Code gitService and bridge handler * feat(git): add inline file diff to history commit rows * fix(git): consolidate CommitFileDiffResponse import to gitApi facade * fix(git): pass directory through history, validate hash, propagate git errors * fix(git): use exit code check for VS Code getCommitFileDiff error detection * fix(git): VS Code rename detection, hash validation parity, retry on error * fix(git): register scroll container as virtualizer root to fix empty space in history diffs * fix(git): address greptile review — rename key extraction, directory guard, language detection, isBinary cleanup * fix(git): harden history inline diffs --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
fa8fac2590
commit
631905764e
@@ -279,6 +279,27 @@ async function execGit(args: string[], cwd: string): Promise<{ stdout: string; s
|
||||
});
|
||||
}
|
||||
|
||||
function extractGitStatusPath(status: string, pathPart: string): string {
|
||||
if ((status === 'R' || status === 'C') && pathPart.includes('\t')) {
|
||||
return pathPart.split('\t').pop() || pathPart;
|
||||
}
|
||||
return pathPart;
|
||||
}
|
||||
|
||||
function extractGitNumstatDestinationPath(filePath: string): string {
|
||||
if (!filePath.includes(' => ')) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
const braceMatch = filePath.match(/^(.*)\{([^{}]*)\s=>\s([^{}]*)\}(.*)$/);
|
||||
if (braceMatch) {
|
||||
const [, prefix, , destination, suffix] = braceMatch;
|
||||
return `${prefix}${destination}${suffix}`.replace(/\/+/g, '/');
|
||||
}
|
||||
|
||||
return filePath.split(' => ').pop()?.trim() || filePath;
|
||||
}
|
||||
|
||||
// ============== Repository Operations ==============
|
||||
|
||||
/**
|
||||
@@ -2631,31 +2652,84 @@ export async function getCommitFiles(
|
||||
directory: string,
|
||||
hash: string
|
||||
): Promise<{ files: Array<{ path: string; insertions: number; deletions: number; isBinary: boolean; changeType: string }> }> {
|
||||
const result = await execGit(['show', '--numstat', '--format=', hash], directory);
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
const numstatResult = await execGit(['show', '--numstat', '--format=', hash], directory);
|
||||
|
||||
if (numstatResult.exitCode !== 0) {
|
||||
return { files: [] };
|
||||
}
|
||||
|
||||
const files: Array<{ path: string; insertions: number; deletions: number; isBinary: boolean; changeType: string }> = [];
|
||||
|
||||
for (const line of result.stdout.trim().split('\n').filter(Boolean)) {
|
||||
const lines = numstatResult.stdout.trim().split('\n').filter(Boolean);
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.split('\t');
|
||||
if (parts.length >= 3) {
|
||||
const isBinary = parts[0] === '-' && parts[1] === '-';
|
||||
files.push({
|
||||
path: parts[2] || '',
|
||||
insertions: isBinary ? 0 : parseInt(parts[0] || '0', 10),
|
||||
deletions: isBinary ? 0 : parseInt(parts[1] || '0', 10),
|
||||
isBinary,
|
||||
changeType: 'M', // Would need additional parsing for actual change type
|
||||
});
|
||||
if (parts.length < 3) continue;
|
||||
|
||||
const [insertionsRaw, deletionsRaw, ...pathParts] = parts;
|
||||
const filePath = pathParts.join('\t');
|
||||
if (!filePath) continue;
|
||||
|
||||
const isBinary = insertionsRaw === '-' && deletionsRaw === '-';
|
||||
const insertions = isBinary ? 0 : (parseInt(insertionsRaw, 10) || 0);
|
||||
const deletions = isBinary ? 0 : (parseInt(deletionsRaw, 10) || 0);
|
||||
|
||||
let changeType = 'M';
|
||||
if (filePath.includes(' => ')) {
|
||||
changeType = 'R';
|
||||
}
|
||||
|
||||
files.push({ path: filePath, insertions, deletions, isBinary, changeType });
|
||||
}
|
||||
|
||||
// Get accurate change types from --name-status
|
||||
const nameStatusResult = await execGit(['show', '--name-status', '--format=', hash], directory);
|
||||
if (nameStatusResult.exitCode === 0) {
|
||||
const statusMap = new Map<string, string>();
|
||||
for (const line of nameStatusResult.stdout.trim().split('\n').filter(Boolean)) {
|
||||
const match = line.match(/^([AMDRC])\d*\t(.+)$/);
|
||||
if (match) {
|
||||
const [, status, pathPart] = match;
|
||||
statusMap.set(extractGitStatusPath(status, pathPart), status);
|
||||
}
|
||||
}
|
||||
for (const file of files) {
|
||||
const basePath = extractGitNumstatDestinationPath(file.path);
|
||||
const status = statusMap.get(basePath) ?? statusMap.get(file.path);
|
||||
if (status) {
|
||||
file.changeType = status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { files };
|
||||
}
|
||||
|
||||
export async function getCommitFileDiff(
|
||||
directory: string,
|
||||
hash: string,
|
||||
filePath: string,
|
||||
isBinary: boolean
|
||||
): Promise<{ original: string; modified: string; isBinary: boolean }> {
|
||||
if (isBinary) {
|
||||
return { original: '', modified: '', isBinary: true };
|
||||
}
|
||||
|
||||
const [originalResult, modifiedResult] = await Promise.all([
|
||||
execGit(['show', `${hash}^:${filePath}`], directory),
|
||||
execGit(['show', `${hash}:${filePath}`], directory),
|
||||
]);
|
||||
|
||||
if (originalResult.exitCode !== 0 && modifiedResult.exitCode !== 0) {
|
||||
throw new Error(`Failed to read file content at commit ${hash}`);
|
||||
}
|
||||
|
||||
return {
|
||||
original: originalResult.exitCode === 0 ? originalResult.stdout : '',
|
||||
modified: modifiedResult.exitCode === 0 ? modifiedResult.stdout : '',
|
||||
isBinary: false,
|
||||
};
|
||||
}
|
||||
|
||||
// ============== Git Identity Operations ==============
|
||||
|
||||
export interface GitIdentitySummary {
|
||||
|
||||
Reference in New Issue
Block a user