feat(diff): add branch scope to context panel diff view
Show every change on the current branch relative to its base in the Changed/Staged/Last turn dropdown. The base comes from the branch's reflog record or an explicit per-branch user choice (persisted), never a main/master guess; when git has no record the user picks a base once from a searchable branch list. - server: GET /api/git/branch-base (reflog-derived base), GET /api/git/range-files (name-status -z with rename/copy destination paths and -C copy detection) - shared UI: optional getBranchBase/getGitRangeFiles runtime APIs with boundary parsing; persisted per-branch overrides keyed by runtime+directory+branch - DiffView: branch scope with confirmed-unavailability coercion of persisted tabs (detached HEAD, default-branch checkout, metadata settled without a default), range-invalidated diff cache guarded against stale completions, bounded branch-metadata retry, read-only diff actions in branch scope; hidden in VS Code - helper module branchDiffScope.ts with tests for coercion, availability, race conditions, and retry exhaustion
This commit is contained in:
@@ -157,6 +157,22 @@ export interface GetGitRangeDiffOptions {
|
||||
contextLines?: number;
|
||||
}
|
||||
|
||||
export interface GetGitRangeFilesOptions {
|
||||
base: string;
|
||||
head: string;
|
||||
}
|
||||
|
||||
/** One changed file in a `base...head` range, with its change letter (A/M/D/R/C). */
|
||||
export interface GitRangeFileEntry {
|
||||
path: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface GitBranchBaseResponse {
|
||||
/** Null when git has no authoritative record of where the branch started. */
|
||||
base: string | null;
|
||||
}
|
||||
|
||||
export interface GitFileDiffResponse {
|
||||
original: string;
|
||||
modified: string;
|
||||
@@ -466,6 +482,8 @@ export interface GitAPI {
|
||||
getGitDiff(directory: string, options: GetGitDiffOptions): Promise<GitDiffResponse>;
|
||||
getGitFileDiff(directory: string, options: GetGitFileDiffOptions): Promise<GitFileDiffResponse>;
|
||||
getGitRangeDiff?(directory: string, options: GetGitRangeDiffOptions): Promise<GitDiffResponse>;
|
||||
getGitRangeFiles?(directory: string, options: GetGitRangeFilesOptions): Promise<GitRangeFileEntry[]>;
|
||||
getBranchBase?(directory: string, branch: string): Promise<GitBranchBaseResponse>;
|
||||
revertGitFile(directory: string, filePath: string, options?: { scope?: 'all' | 'working' }): Promise<void>;
|
||||
stageGitFile(directory: string, filePath: string): Promise<void>;
|
||||
stageGitFiles?(directory: string, filePaths: string[]): Promise<void>;
|
||||
|
||||
@@ -119,6 +119,24 @@ export async function getGitRangeDiff(
|
||||
return gitHttp.getGitRangeDiff(directory, options);
|
||||
}
|
||||
|
||||
export async function getGitRangeFiles(
|
||||
directory: string,
|
||||
options: import('./api/types').GetGitRangeFilesOptions
|
||||
): Promise<import('./api/types').GitRangeFileEntry[]> {
|
||||
const runtime = getRuntimeGit();
|
||||
if (runtime?.getGitRangeFiles) return runtime.getGitRangeFiles(directory, options);
|
||||
return gitHttp.getGitRangeFiles(directory, options);
|
||||
}
|
||||
|
||||
export async function getBranchBase(
|
||||
directory: string,
|
||||
branch: string
|
||||
): Promise<import('./api/types').GitBranchBaseResponse> {
|
||||
const runtime = getRuntimeGit();
|
||||
if (runtime?.getBranchBase) return runtime.getBranchBase(directory, branch);
|
||||
return gitHttp.getBranchBase(directory, branch);
|
||||
}
|
||||
|
||||
export async function revertGitFile(
|
||||
directory: string,
|
||||
filePath: string,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
GitDiffResponse,
|
||||
GetGitDiffOptions,
|
||||
GetGitRangeDiffOptions,
|
||||
GetGitRangeFilesOptions,
|
||||
GitFileDiffResponse,
|
||||
GetGitFileDiffOptions,
|
||||
GitBranch,
|
||||
@@ -248,6 +249,51 @@ export async function getGitRangeDiff(
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getGitRangeFiles(
|
||||
directory: string,
|
||||
options: GetGitRangeFilesOptions
|
||||
): Promise<import('./api/types').GitRangeFileEntry[]> {
|
||||
const { base, head } = options;
|
||||
if (!base || !head) {
|
||||
throw new Error('base and head are required to fetch git range files');
|
||||
}
|
||||
|
||||
const response = await runtimeFetch(
|
||||
buildUrl(`${API_BASE}/range-files`, directory, { base, head })
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get git range files: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as { files?: unknown };
|
||||
if (!Array.isArray(payload.files)) return [];
|
||||
return payload.files.filter((entry): entry is import('./api/types').GitRangeFileEntry => {
|
||||
if (!entry || typeof entry !== 'object') return false;
|
||||
const candidate = entry as { path?: unknown; status?: unknown };
|
||||
return typeof candidate.path === 'string' && typeof candidate.status === 'string';
|
||||
});
|
||||
}
|
||||
|
||||
export async function getBranchBase(
|
||||
directory: string,
|
||||
branch: string
|
||||
): Promise<import('./api/types').GitBranchBaseResponse> {
|
||||
if (!branch) {
|
||||
throw new Error('branch is required to get branch base');
|
||||
}
|
||||
|
||||
const response = await runtimeFetch(
|
||||
buildUrl(`${API_BASE}/branch-base`, directory, { branch })
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get branch base: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getGitFileDiff(directory: string, options: GetGitFileDiffOptions): Promise<GitFileDiffResponse> {
|
||||
const { path, staged } = options;
|
||||
if (!path) {
|
||||
|
||||
@@ -1360,6 +1360,13 @@ export const dict = {
|
||||
'diffView.scope.changed': 'Geändert',
|
||||
'diffView.scope.staged': 'Staged',
|
||||
'diffView.scope.lastTurn': 'Letzter Zug',
|
||||
'diffView.scope.branch': 'Branch',
|
||||
'diffView.branch.resolvingBase': 'Basis-Branch wird ermittelt...',
|
||||
'diffView.branch.noBaseTitle': 'Kein Basis-Branch',
|
||||
'diffView.branch.noBaseDescription': 'Git enthält keinen Eintrag, wo dieser Branch entstanden ist. Wähle einen Basis-Branch für den Vergleich.',
|
||||
'diffView.branch.loadError': 'Branch-Änderungen konnten nicht geladen werden',
|
||||
'diffView.branch.loadingFiles': 'Branch-Änderungen werden geladen...',
|
||||
'diffView.branch.empty': 'Keine Änderungen in diesem Branch gegenüber {base}',
|
||||
'diffView.scope.selectorAria': 'Änderungsmodus auswählen',
|
||||
'diffView.actions.retry': 'Erneut versuchen',
|
||||
'diffView.actions.renderAnyway': 'Trotzdem rendern',
|
||||
|
||||
@@ -1517,6 +1517,13 @@ export const dict = {
|
||||
'diffView.scope.changed': 'Changed',
|
||||
'diffView.scope.staged': 'Staged',
|
||||
'diffView.scope.lastTurn': 'Last turn',
|
||||
'diffView.scope.branch': 'Branch',
|
||||
'diffView.branch.resolvingBase': 'Detecting base branch...',
|
||||
'diffView.branch.noBaseTitle': 'No base branch',
|
||||
'diffView.branch.noBaseDescription': 'Git has no record of where this branch started. Choose a base branch to compare against.',
|
||||
'diffView.branch.loadError': 'Failed to load branch changes',
|
||||
'diffView.branch.loadingFiles': 'Loading branch changes...',
|
||||
'diffView.branch.empty': 'No changes on this branch relative to {base}',
|
||||
'diffView.scope.selectorAria': 'Select change mode',
|
||||
'diffView.actions.retry': 'Retry',
|
||||
'diffView.actions.renderAnyway': 'Render anyway',
|
||||
|
||||
@@ -1483,6 +1483,13 @@ export const dict: Record<I18nKey, string> = {
|
||||
"diffView.scope.changed": "Cambiados",
|
||||
"diffView.scope.staged": "Staged",
|
||||
"diffView.scope.lastTurn": "Último turno",
|
||||
"diffView.scope.branch": "Rama",
|
||||
"diffView.branch.resolvingBase": "Detectando rama base...",
|
||||
"diffView.branch.noBaseTitle": "Sin rama base",
|
||||
"diffView.branch.noBaseDescription": "Git no tiene registro de dónde surgió esta rama. Elige una rama base para comparar.",
|
||||
"diffView.branch.loadError": "No se pudieron cargar los cambios de la rama",
|
||||
"diffView.branch.loadingFiles": "Cargando cambios de la rama...",
|
||||
"diffView.branch.empty": "No hay cambios en esta rama respecto a {base}",
|
||||
"diffView.scope.selectorAria": "Seleccionar modo de cambios",
|
||||
"diffView.actions.retry": "Volver a intentar",
|
||||
"diffView.actions.renderAnyway": "Renderizar de todos modos",
|
||||
|
||||
@@ -1282,6 +1282,13 @@ export const dict = {
|
||||
"diffView.scope.changed": "Modifiés",
|
||||
"diffView.scope.staged": "Staged",
|
||||
"diffView.scope.lastTurn": "Dernier tour",
|
||||
"diffView.scope.branch": "Branche",
|
||||
"diffView.branch.resolvingBase": "Détection de la branche de base...",
|
||||
"diffView.branch.noBaseTitle": "Aucune branche de base",
|
||||
"diffView.branch.noBaseDescription": "Git ne conserve aucune trace de la branche d’origine de cette branche. Choisissez une branche de base pour la comparaison.",
|
||||
"diffView.branch.loadError": "Échec du chargement des modifications de la branche",
|
||||
"diffView.branch.loadingFiles": "Chargement des modifications de la branche...",
|
||||
"diffView.branch.empty": "Aucune modification sur cette branche par rapport à {base}",
|
||||
"diffView.scope.selectorAria": "Sélectionner le mode de changements",
|
||||
'diffView.actions.retry': 'Réessayer',
|
||||
'diffView.actions.renderAnyway': 'Afficher quand même',
|
||||
|
||||
@@ -1513,6 +1513,13 @@ export const dict: Record<I18nKey, string> = {
|
||||
'diffView.scope.changed': '変更済み',
|
||||
'diffView.scope.staged': 'ステージ済み',
|
||||
'diffView.scope.lastTurn': '最後のターン',
|
||||
'diffView.scope.branch': 'ブランチ',
|
||||
'diffView.branch.resolvingBase': 'ベースブランチを検出中...',
|
||||
'diffView.branch.noBaseTitle': 'ベースブランチがありません',
|
||||
'diffView.branch.noBaseDescription': 'このブランチがどこから作られたかの記録がGitにありません。比較するベースブランチを選択してください。',
|
||||
'diffView.branch.loadError': 'ブランチの変更を読み込めませんでした',
|
||||
'diffView.branch.loadingFiles': 'ブランチの変更を読み込み中...',
|
||||
'diffView.branch.empty': 'このブランチには{base}に対する変更はありません',
|
||||
'diffView.scope.selectorAria': '変更モードを選択',
|
||||
'diffView.actions.retry': '再試行',
|
||||
'diffView.actions.renderAnyway': 'とにかくレンダリング',
|
||||
|
||||
@@ -1519,6 +1519,13 @@ export const dict: Record<I18nKey, string> = {
|
||||
"diffView.scope.changed": "Changed",
|
||||
"diffView.scope.staged": "Staged",
|
||||
"diffView.scope.lastTurn": "마지막 턴",
|
||||
"diffView.scope.branch": "브랜치",
|
||||
"diffView.branch.resolvingBase": "베이스 브랜치 감지 중...",
|
||||
"diffView.branch.noBaseTitle": "베이스 브랜치 없음",
|
||||
"diffView.branch.noBaseDescription": "이 브랜치가 어디서 시작되었는지 Git에 기록이 없습니다. 비교할 베이스 브랜치를 선택하세요.",
|
||||
"diffView.branch.loadError": "브랜치 변경 사항을 불러오지 못했습니다",
|
||||
"diffView.branch.loadingFiles": "브랜치 변경 사항 불러오는 중...",
|
||||
"diffView.branch.empty": "이 브랜치에는 {base}에 대한 변경 사항이 없습니다",
|
||||
"diffView.scope.selectorAria": "변경 모드 선택",
|
||||
'diffView.actions.retry': '다시 시도',
|
||||
'diffView.actions.renderAnyway': '그래도 렌더링',
|
||||
|
||||
@@ -1795,6 +1795,13 @@ export const dict: Record<I18nKey, string> = {
|
||||
"diffView.scope.changed": "Zmienione",
|
||||
"diffView.scope.staged": "Staged",
|
||||
"diffView.scope.lastTurn": "Ostatnia tura",
|
||||
"diffView.scope.branch": "Gałąź",
|
||||
"diffView.branch.resolvingBase": "Wykrywanie gałęzi bazowej...",
|
||||
"diffView.branch.noBaseTitle": "Brak gałęzi bazowej",
|
||||
"diffView.branch.noBaseDescription": "Git nie zapisuje, od której gałęzi ta gałąź powstała. Wybierz gałąź bazową do porównania.",
|
||||
"diffView.branch.loadError": "Nie udało się wczytać zmian gałęzi",
|
||||
"diffView.branch.loadingFiles": "Wczytywanie zmian gałęzi...",
|
||||
"diffView.branch.empty": "Brak zmian w tej gałęzi względem {base}",
|
||||
"diffView.scope.selectorAria": "Wybierz tryb zmian",
|
||||
'diffView.summary.changedFilesSingle': 'Zmieniono {count} plik',
|
||||
'directoryExplorerDialog.actions.addProject': 'Dodaj projekt',
|
||||
|
||||
@@ -1483,6 +1483,13 @@ export const dict: Record<I18nKey, string> = {
|
||||
"diffView.scope.changed": "Alteradas",
|
||||
"diffView.scope.staged": "Staged",
|
||||
"diffView.scope.lastTurn": "Último turno",
|
||||
"diffView.scope.branch": "Branch",
|
||||
"diffView.branch.resolvingBase": "Detectando branch base...",
|
||||
"diffView.branch.noBaseTitle": "Sem branch base",
|
||||
"diffView.branch.noBaseDescription": "O Git não tem registro de onde este branch começou. Escolha um branch base para comparar.",
|
||||
"diffView.branch.loadError": "Falha ao carregar as alterações do branch",
|
||||
"diffView.branch.loadingFiles": "Carregando alterações do branch...",
|
||||
"diffView.branch.empty": "Nenhuma alteração neste branch em relação a {base}",
|
||||
"diffView.scope.selectorAria": "Selecionar modo de alterações",
|
||||
"diffView.actions.retry": "Tentar novamente",
|
||||
"diffView.actions.renderAnyway": "Renderizar mesmo assim",
|
||||
|
||||
@@ -1483,6 +1483,13 @@ export const dict: Record<I18nKey, string> = {
|
||||
"diffView.scope.changed": "Змінені",
|
||||
"diffView.scope.staged": "Індексовані",
|
||||
"diffView.scope.lastTurn": "Останній хід",
|
||||
"diffView.scope.branch": "Гілка",
|
||||
"diffView.branch.resolvingBase": "Визначаємо базову гілку...",
|
||||
"diffView.branch.noBaseTitle": "Немає базової гілки",
|
||||
"diffView.branch.noBaseDescription": "Git не зберігає, від якої гілки почалася ця гілка. Виберіть базову гілку для порівняння.",
|
||||
"diffView.branch.loadError": "Не вдалося завантажити зміни гілки",
|
||||
"diffView.branch.loadingFiles": "Завантаження змін гілки...",
|
||||
"diffView.branch.empty": "Немає змін у цій гілці відносно {base}",
|
||||
"diffView.scope.selectorAria": "Вибрати режим змін",
|
||||
"diffView.actions.retry": "Повторити спробу",
|
||||
"diffView.actions.renderAnyway": "Все одно відрендерити",
|
||||
|
||||
@@ -1483,6 +1483,13 @@ export const dict: Record<I18nKey, string> = {
|
||||
"diffView.scope.changed": "已更改",
|
||||
"diffView.scope.staged": "已暂存",
|
||||
"diffView.scope.lastTurn": "上一轮",
|
||||
"diffView.scope.branch": "分支",
|
||||
"diffView.branch.resolvingBase": "正在检测基础分支...",
|
||||
"diffView.branch.noBaseTitle": "没有基础分支",
|
||||
"diffView.branch.noBaseDescription": "Git 中没有记录此分支的起点。请选择一个基础分支进行比较。",
|
||||
"diffView.branch.loadError": "加载分支更改失败",
|
||||
"diffView.branch.loadingFiles": "正在加载分支更改...",
|
||||
"diffView.branch.empty": "此分支相对于 {base} 没有更改",
|
||||
"diffView.scope.selectorAria": "选择更改模式",
|
||||
'diffView.actions.retry': '重试',
|
||||
'diffView.actions.renderAnyway': '仍然渲染',
|
||||
|
||||
@@ -1493,6 +1493,13 @@ export const dict: Record<I18nKey, string> = {
|
||||
"diffView.scope.changed": "已變更",
|
||||
"diffView.scope.staged": "已暫存",
|
||||
"diffView.scope.lastTurn": "上一輪",
|
||||
"diffView.scope.branch": "分支",
|
||||
"diffView.branch.resolvingBase": "正在偵測基礎分支...",
|
||||
"diffView.branch.noBaseTitle": "沒有基礎分支",
|
||||
"diffView.branch.noBaseDescription": "Git 中沒有記錄此分支的起點。請選擇基礎分支進行比較。",
|
||||
"diffView.branch.loadError": "載入分支變更失敗",
|
||||
"diffView.branch.loadingFiles": "正在載入分支變更...",
|
||||
"diffView.branch.empty": "此分支相對於 {base} 沒有變更",
|
||||
"diffView.scope.selectorAria": "選擇變更模式",
|
||||
'diffView.actions.retry': '重試',
|
||||
'diffView.actions.renderAnyway': '仍然渲染',
|
||||
|
||||
Reference in New Issue
Block a user