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
@@ -58,6 +58,7 @@ The following functions are exported and used by the web server:
|
||||
### Log Operations
|
||||
- `getLog(directory, options)`: Get commit history with stats (supports maxCount, from, to, file filters).
|
||||
- `getCommitFiles(directory, commitHash)`: Get file changes for a specific commit.
|
||||
- `getCommitFileDiff(directory, hash, filePath, isBinary)`: Get before/after content for a specific file in a commit. Returns `{ original, modified, isBinary }`. Runs `git show <hash>^:<path>` and `git show <hash>:<path>` in parallel; returns empty strings on failure (added/deleted/root-commit edge cases).
|
||||
|
||||
### Merge and Rebase Operations
|
||||
- `rebase(directory, options)`: Start a rebase onto a target branch.
|
||||
|
||||
@@ -943,4 +943,30 @@ export function registerGitRoutes(app) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/git/commit-file-diff', async (req, res) => {
|
||||
const { getCommitFileDiff } = await getGitLibraries();
|
||||
try {
|
||||
const { directory, hash, path: filePath } = req.query;
|
||||
if (!directory || typeof directory !== 'string') {
|
||||
return res.status(400).json({ error: 'directory parameter is required' });
|
||||
}
|
||||
if (!hash || typeof hash !== 'string') {
|
||||
return res.status(400).json({ error: 'hash parameter is required' });
|
||||
}
|
||||
if (!/^[0-9a-fA-F]{7,40}$/.test(hash)) {
|
||||
return res.status(400).json({ error: 'hash must be a valid commit SHA' });
|
||||
}
|
||||
if (!filePath || typeof filePath !== 'string') {
|
||||
return res.status(400).json({ error: 'path parameter is required' });
|
||||
}
|
||||
|
||||
const isBinary = req.query.binary === 'true';
|
||||
const result = await getCommitFileDiff(directory, hash, filePath, isBinary);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('Failed to get commit file diff:', error);
|
||||
res.status(500).json({ error: error.message || 'Failed to get commit file diff' });
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -1611,6 +1611,27 @@ const parseIsBinaryFromNumstat = (raw) => {
|
||||
return added === '-' || deleted === '-';
|
||||
};
|
||||
|
||||
const extractGitStatusPath = (status, pathPart) => {
|
||||
if ((status === 'R' || status === 'C') && pathPart.includes('\t')) {
|
||||
return pathPart.split('\t').pop() || pathPart;
|
||||
}
|
||||
return pathPart;
|
||||
};
|
||||
|
||||
const extractGitNumstatDestinationPath = (filePath) => {
|
||||
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;
|
||||
};
|
||||
|
||||
const looksBinaryBySniff = async (absolutePath) => {
|
||||
try {
|
||||
const handle = await fsp.open(absolutePath, 'r');
|
||||
@@ -3028,15 +3049,13 @@ export async function getCommitFiles(directory, commitHash) {
|
||||
for (const line of statusLines) {
|
||||
const match = line.match(/^([AMDRC])\d*\t(.+)$/);
|
||||
if (match) {
|
||||
const [, status, path] = match;
|
||||
statusMap.set(path, status);
|
||||
const [, status, pathPart] = match;
|
||||
statusMap.set(extractGitStatusPath(status, pathPart), status);
|
||||
}
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const basePath = file.path.includes(' => ')
|
||||
? file.path.split(' => ').pop()?.replace(/[{}]/g, '') || file.path
|
||||
: file.path;
|
||||
const basePath = extractGitNumstatDestinationPath(file.path);
|
||||
|
||||
const status = statusMap.get(basePath) || statusMap.get(file.path);
|
||||
if (status) {
|
||||
@@ -3379,3 +3398,29 @@ export async function getConflictDetails(directory) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCommitFileDiff(directory, hash, filePath, isBinary) {
|
||||
if (!directory || !hash || !filePath) {
|
||||
throw new Error('directory, hash, and path are required for getCommitFileDiff');
|
||||
}
|
||||
|
||||
if (isBinary) {
|
||||
return { original: '', modified: '', isBinary: true };
|
||||
}
|
||||
|
||||
const directoryPath = normalizeDirectoryPath(directory);
|
||||
|
||||
const [originalResult, modifiedResult] = await Promise.all([
|
||||
runGitCommand(directoryPath, ['show', `${hash}^:${filePath}`]),
|
||||
runGitCommand(directoryPath, ['show', `${hash}:${filePath}`]),
|
||||
]);
|
||||
|
||||
const original = originalResult.success ? originalResult.stdout : '';
|
||||
const modified = modifiedResult.success ? modifiedResult.stdout : '';
|
||||
|
||||
if (!originalResult.success && !modifiedResult.success) {
|
||||
throw new Error(`Failed to read file content at commit ${hash}: ${originalResult.stderr || modifiedResult.stderr}`);
|
||||
}
|
||||
|
||||
return { original, modified, isBinary: false };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user