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
@@ -303,6 +303,12 @@ export interface GitCommitFilesResponse {
|
||||
files: CommitFileEntry[];
|
||||
}
|
||||
|
||||
export interface CommitFileDiffResponse {
|
||||
original: string;
|
||||
modified: string;
|
||||
isBinary: boolean;
|
||||
}
|
||||
|
||||
export interface GitWorktreeInfo {
|
||||
head: string;
|
||||
name: string;
|
||||
@@ -447,6 +453,7 @@ export interface GitAPI {
|
||||
renameBranch(directory: string, oldName: string, newName: string): Promise<{ success: boolean; branch: string }>;
|
||||
getGitLog(directory: string, options?: GitLogOptions): Promise<GitLogResponse>;
|
||||
getCommitFiles(directory: string, hash: string): Promise<GitCommitFilesResponse>;
|
||||
getCommitFileDiff?(directory: string, hash: string, filePath: string, isBinary: boolean): Promise<CommitFileDiffResponse>;
|
||||
getCurrentGitIdentity(directory: string): Promise<GitIdentitySummary | null>;
|
||||
hasLocalIdentity?(directory: string): Promise<boolean>;
|
||||
setGitIdentity(directory: string, profileId: string): Promise<{ success: boolean; profile: GitIdentityProfile }>;
|
||||
|
||||
@@ -36,6 +36,7 @@ export type {
|
||||
GitMergeResult,
|
||||
GitRebaseResult,
|
||||
MergeConflictDetails,
|
||||
CommitFileDiffResponse,
|
||||
} from './api/types';
|
||||
|
||||
declare global {
|
||||
@@ -642,6 +643,17 @@ export async function getCommitFiles(
|
||||
return gitHttp.getCommitFiles(directory, hash);
|
||||
}
|
||||
|
||||
export async function getCommitFileDiff(
|
||||
directory: string,
|
||||
hash: string,
|
||||
filePath: string,
|
||||
isBinary: boolean
|
||||
): Promise<import('./api/types').CommitFileDiffResponse> {
|
||||
const runtime = getRuntimeGit();
|
||||
if (runtime?.getCommitFileDiff) return runtime.getCommitFileDiff(directory, hash, filePath, isBinary);
|
||||
return gitHttp.getCommitFileDiff(directory, hash, filePath, isBinary);
|
||||
}
|
||||
|
||||
export async function getGitIdentities(): Promise<import('./api/types').GitIdentityProfile[]> {
|
||||
const runtime = getRuntimeGit();
|
||||
if (runtime) return runtime.getGitIdentities();
|
||||
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
GitLogOptions,
|
||||
GitLogResponse,
|
||||
GitCommitFilesResponse,
|
||||
CommitFileDiffResponse,
|
||||
GitIdentityProfile,
|
||||
GitIdentitySummary,
|
||||
DiscoveredGitCredential,
|
||||
@@ -681,6 +682,25 @@ export async function getCommitFiles(
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getCommitFileDiff(
|
||||
directory: string,
|
||||
hash: string,
|
||||
filePath: string,
|
||||
isBinary: boolean
|
||||
): Promise<CommitFileDiffResponse> {
|
||||
const response = await fetch(
|
||||
buildUrl(`${API_BASE}/commit-file-diff`, directory, {
|
||||
hash,
|
||||
path: filePath,
|
||||
binary: isBinary ? 'true' : undefined,
|
||||
})
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get commit file diff: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getGitIdentities(): Promise<GitIdentityProfile[]> {
|
||||
const response = await fetch(buildUrl(`${API_BASE}/identities`, undefined));
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -483,14 +483,21 @@ export const dict = {
|
||||
'gitView.header.removeRemoteAria': 'Remove Remote aria label',
|
||||
'gitView.header.removeRemoteTitle': 'Remove Remote Title',
|
||||
'gitView.history.binary': 'Binary',
|
||||
'gitView.history.binaryNoDiff': 'Binary file — no diff available',
|
||||
'gitView.history.commitsPlaceholder': 'Commits Placeholder',
|
||||
'gitView.history.copySha': 'Copy SHA',
|
||||
'gitView.history.diffError': 'Failed to load diff. Click to retry.',
|
||||
'gitView.history.largeDiffDescription': 'Rendering may be slow. You can still view the diff by clicking below.',
|
||||
'gitView.history.largeDiffTitle': 'Large diff ({count} changed lines)',
|
||||
'gitView.history.loadingDiff': 'Loading diff...',
|
||||
'gitView.history.loadingFiles': 'Loading files...',
|
||||
'gitView.history.logSize100': 'Log Size100',
|
||||
'gitView.history.logSize25': 'Log Size25',
|
||||
'gitView.history.logSize50': 'Log Size50',
|
||||
'gitView.history.noCommits': 'No commits found',
|
||||
'gitView.history.noFiles': 'No files',
|
||||
'gitView.history.renamedNoDiff': 'Renamed file — diff not supported',
|
||||
'gitView.history.renderDiffAnyway': 'Render anyway',
|
||||
'gitView.history.title': 'History',
|
||||
'gitView.integrate.checking': 'Checking…',
|
||||
'gitView.integrate.cherryPickAbortedToast': 'Cherry Pick Aborted Toast',
|
||||
|
||||
@@ -484,14 +484,21 @@ export const dict: Record<I18nKey, string> = {
|
||||
"gitView.header.removeRemoteAria": "Eliminar remoto",
|
||||
"gitView.header.removeRemoteTitle": "Eliminar remoto",
|
||||
"gitView.history.binary": "Binario",
|
||||
"gitView.history.binaryNoDiff": "Archivo binario — no hay diff disponible",
|
||||
"gitView.history.commitsPlaceholder": "Buscar commits...",
|
||||
"gitView.history.copySha": "Copiar SHA",
|
||||
"gitView.history.diffError": "Error al cargar el diff. Haz clic para reintentar.",
|
||||
"gitView.history.largeDiffDescription": "El renderizado puede ser lento. Puedes ver el diff igualmente haciendo clic abajo.",
|
||||
"gitView.history.largeDiffTitle": "Diff grande ({count} líneas cambiadas)",
|
||||
"gitView.history.loadingDiff": "Cargando diff...",
|
||||
"gitView.history.loadingFiles": "Cargando archivos...",
|
||||
"gitView.history.logSize100": "100 commits",
|
||||
"gitView.history.logSize25": "25 commits",
|
||||
"gitView.history.logSize50": "50 commits",
|
||||
"gitView.history.noCommits": "No se encontraron commits",
|
||||
"gitView.history.noFiles": "No hay archivos",
|
||||
"gitView.history.renamedNoDiff": "Archivo renombrado — diff no soportado",
|
||||
"gitView.history.renderDiffAnyway": "Renderizar igualmente",
|
||||
"gitView.history.title": "Historial",
|
||||
"gitView.integrate.checking": "Verificando…",
|
||||
"gitView.integrate.cherryPickAbortedToast": "Cherry-pick abortado",
|
||||
|
||||
@@ -484,14 +484,21 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.header.removeRemoteAria': '리모트 제거',
|
||||
'gitView.header.removeRemoteTitle': '리모트 제거',
|
||||
'gitView.history.binary': '바이너리',
|
||||
'gitView.history.binaryNoDiff': '바이너리 파일 — diff 없음',
|
||||
'gitView.history.commitsPlaceholder': '커밋 검색',
|
||||
'gitView.history.copySha': 'SHA 복사',
|
||||
'gitView.history.diffError': 'diff 로드 실패. 클릭하여 재시도.',
|
||||
'gitView.history.largeDiffDescription': '렌더링이 느릴 수 있습니다. 아래를 클릭해 diff를 계속 볼 수 있습니다.',
|
||||
'gitView.history.largeDiffTitle': '큰 diff({count}개 변경된 줄)',
|
||||
'gitView.history.loadingDiff': 'diff 로드 중…',
|
||||
'gitView.history.loadingFiles': '파일 로드 중…',
|
||||
'gitView.history.logSize100': '최근 100개',
|
||||
'gitView.history.logSize25': '최근 25개',
|
||||
'gitView.history.logSize50': '최근 50개',
|
||||
'gitView.history.noCommits': '커밋 없음',
|
||||
'gitView.history.noFiles': '파일 없음',
|
||||
'gitView.history.renamedNoDiff': '이름 변경된 파일 — diff 미지원',
|
||||
'gitView.history.renderDiffAnyway': '그래도 렌더링',
|
||||
'gitView.history.title': '히스토리',
|
||||
'gitView.integrate.checking': '확인 중…',
|
||||
'gitView.integrate.cherryPickAbortedToast': 'Cherry-pick이 중단되었습니다',
|
||||
|
||||
@@ -1450,15 +1450,22 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.header.removeRemoteAria': 'Remove Remote aria label',
|
||||
'gitView.header.removeRemoteTitle': 'Remove Remote Title',
|
||||
'gitView.history.binary': 'Binary',
|
||||
'gitView.history.binaryNoDiff': 'Binary file — no diff available',
|
||||
'gitView.history.commitsPlaceholder': 'Commits Placeholder',
|
||||
'gitView.history.copySha': 'Copy SHA',
|
||||
'gitView.history.dialogDescription': 'Browse recent commits and inspect changed files.',
|
||||
'gitView.history.diffError': 'Failed to load diff. Click to retry.',
|
||||
'gitView.history.largeDiffDescription': 'Rendering may be slow. You can still view the diff by clicking below.',
|
||||
'gitView.history.largeDiffTitle': 'Large diff ({count} changed lines)',
|
||||
'gitView.history.loadingDiff': 'Loading diff...',
|
||||
'gitView.history.loadingFiles': 'Loading files...',
|
||||
'gitView.history.logSize100': 'Log Size100',
|
||||
'gitView.history.logSize25': 'Log Size25',
|
||||
'gitView.history.logSize50': 'Log Size50',
|
||||
'gitView.history.noCommits': 'No commits found',
|
||||
'gitView.history.noFiles': 'No files',
|
||||
'gitView.history.renamedNoDiff': 'Renamed file — diff not supported',
|
||||
'gitView.history.renderDiffAnyway': 'Render anyway',
|
||||
'gitView.history.title': 'History',
|
||||
'gitView.integrate.checking': 'Sprawdzanie…',
|
||||
'gitView.integrate.cherryPickAbortedToast': 'Cherry-pick został przerwany',
|
||||
|
||||
@@ -484,14 +484,21 @@ export const dict: Record<I18nKey, string> = {
|
||||
"gitView.header.removeRemoteAria": "Excluir remoto",
|
||||
"gitView.header.removeRemoteTitle": "Excluir remoto",
|
||||
"gitView.history.binary": "Binario",
|
||||
"gitView.history.binaryNoDiff": "Arquivo binário — diff não disponível",
|
||||
"gitView.history.commitsPlaceholder": "Buscar commits...",
|
||||
"gitView.history.copySha": "Copiar SHA",
|
||||
"gitView.history.diffError": "Falha ao carregar diff. Clique para tentar novamente.",
|
||||
"gitView.history.largeDiffDescription": "A renderização pode ser lenta. Você ainda pode ver o diff clicando abaixo.",
|
||||
"gitView.history.largeDiffTitle": "Diff grande ({count} linhas alteradas)",
|
||||
"gitView.history.loadingDiff": "Carregando diff...",
|
||||
"gitView.history.loadingFiles": "Carregando arquivos...",
|
||||
"gitView.history.logSize100": "100 commits",
|
||||
"gitView.history.logSize25": "25 commits",
|
||||
"gitView.history.logSize50": "50 commits",
|
||||
"gitView.history.noCommits": "Nenhum commit encontrado",
|
||||
"gitView.history.noFiles": "Não há arquivos",
|
||||
"gitView.history.renamedNoDiff": "Arquivo renomeado — diff não suportado",
|
||||
"gitView.history.renderDiffAnyway": "Renderizar mesmo assim",
|
||||
"gitView.history.title": "Histórico",
|
||||
"gitView.integrate.checking": "Verificando…",
|
||||
"gitView.integrate.cherryPickAbortedToast": "Cherry-pick abortado",
|
||||
|
||||
@@ -484,14 +484,21 @@ export const dict: Record<I18nKey, string> = {
|
||||
"gitView.header.removeRemoteAria": "Видалити remote",
|
||||
"gitView.header.removeRemoteTitle": "Видалити remote",
|
||||
"gitView.history.binary": "Бінарний",
|
||||
"gitView.history.binaryNoDiff": "Бінарний файл — diff недоступний",
|
||||
"gitView.history.commitsPlaceholder": "Пошук комітів",
|
||||
"gitView.history.copySha": "Скопіювати SHA",
|
||||
"gitView.history.diffError": "Не вдалося завантажити diff. Натисніть, щоб повторити.",
|
||||
"gitView.history.largeDiffDescription": "Рендеринг може бути повільним. Diff все одно можна переглянути нижче.",
|
||||
"gitView.history.largeDiffTitle": "Великий diff ({count} змінених рядків)",
|
||||
"gitView.history.loadingDiff": "Завантаження diff...",
|
||||
"gitView.history.loadingFiles": "Завантаження файлів...",
|
||||
"gitView.history.logSize100": "Розмір журналу 100",
|
||||
"gitView.history.logSize25": "Розмір журналу 25",
|
||||
"gitView.history.logSize50": "Розмір журналу 50",
|
||||
"gitView.history.noCommits": "Комітів не знайдено",
|
||||
"gitView.history.noFiles": "Немає файлів",
|
||||
"gitView.history.renamedNoDiff": "Перейменований файл — diff не підтримується",
|
||||
"gitView.history.renderDiffAnyway": "Показати все одно",
|
||||
"gitView.history.title": "Історія",
|
||||
"gitView.integrate.checking": "Перевірка…",
|
||||
"gitView.integrate.cherryPickAbortedToast": "Cherry-pick перервано",
|
||||
|
||||
@@ -484,14 +484,21 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.header.removeRemoteAria': '移除远程 {name}',
|
||||
'gitView.header.removeRemoteTitle': '移除 {name}',
|
||||
'gitView.history.binary': '二进制',
|
||||
'gitView.history.binaryNoDiff': '二进制文件 — 无可用 diff',
|
||||
'gitView.history.commitsPlaceholder': '提交数',
|
||||
'gitView.history.copySha': '复制 SHA',
|
||||
'gitView.history.diffError': '加载 diff 失败,点击重试。',
|
||||
'gitView.history.largeDiffDescription': '渲染可能较慢。你仍可点击下方查看 diff。',
|
||||
'gitView.history.largeDiffTitle': '大型 diff({count} 行变更)',
|
||||
'gitView.history.loadingDiff': '正在加载 diff...',
|
||||
'gitView.history.loadingFiles': '正在加载文件...',
|
||||
'gitView.history.logSize100': '100 个提交',
|
||||
'gitView.history.logSize25': '25 个提交',
|
||||
'gitView.history.logSize50': '50 个提交',
|
||||
'gitView.history.noCommits': '未找到提交',
|
||||
'gitView.history.noFiles': '没有文件',
|
||||
'gitView.history.renamedNoDiff': '已重命名文件 — 不支持 diff',
|
||||
'gitView.history.renderDiffAnyway': '仍然渲染',
|
||||
'gitView.history.title': '历史',
|
||||
'gitView.integrate.checking': '检查中…',
|
||||
'gitView.integrate.cherryPickAbortedToast': '已中止 cherry-pick',
|
||||
|
||||
Reference in New Issue
Block a user