feat: git panel redesign (#535)

* feat(git-panel): add Branches tab, Fetch always visible, inline error banners

- Add dedicated Branches tab (local/remote sections, switch with confirmation, delete branch, create branch)
- Rename History tab to Commits; add change-count badge on Changes tab
- Fetch button always visible when remote exists (not only when both ahead & behind)
- Inline error banner below header for failed push/pull/fetch, with dismiss button
- Server: /api/git/branches now returns localBranches + remoteBranches separately
- Server: add /api/git/delete-branch endpoint (prevents deleting current branch)
- Controller: expose operationError, clearOperationError, deleteBranch, localBranches, remoteBranches
- Constants: add deleteBranch to all ConfirmActionType record maps

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: git log datetime

* feat(git-panel): add staged/unstaged sections and enhanced commit details

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Haile <118998054+blackmammoth@users.noreply.github.com>
This commit is contained in:
Simos Mikelatos
2026-03-13 15:38:53 +01:00
committed by GitHub
parent 1d31c3ec83
commit adb3a06d7e
13 changed files with 732 additions and 172 deletions

View File

@@ -24,3 +24,70 @@ export function getStatusLabel(status: FileStatusCode): string {
export function getStatusBadgeClass(status: FileStatusCode): string {
return FILE_STATUS_BADGE_CLASSES[status] || FILE_STATUS_BADGE_CLASSES.U;
}
// ---------------------------------------------------------------------------
// Parse `git show` output to extract per-file change info
// ---------------------------------------------------------------------------
export type CommitFileChange = {
path: string;
directory: string;
filename: string;
status: FileStatusCode;
insertions: number;
deletions: number;
};
export type CommitFileSummary = {
files: CommitFileChange[];
totalFiles: number;
totalInsertions: number;
totalDeletions: number;
};
export function parseCommitFiles(showOutput: string): CommitFileSummary {
const files: CommitFileChange[] = [];
// Split on file diff boundaries
const fileDiffs = showOutput.split(/^diff --git /m).slice(1);
for (const section of fileDiffs) {
const lines = section.split('\n');
// Extract path from "a/path b/path"
const header = lines[0] ?? '';
const match = header.match(/^a\/(.+?) b\/(.+)/);
if (!match) continue;
const pathA = match[1];
const pathB = match[2];
// Determine status
let status: FileStatusCode = 'M';
const joined = lines.slice(0, 6).join('\n');
if (joined.includes('new file mode')) status = 'A';
else if (joined.includes('deleted file mode')) status = 'D';
const filePath = status === 'D' ? pathA : pathB;
// Count insertions/deletions (lines starting with +/- but not +++/---)
let insertions = 0;
let deletions = 0;
for (const line of lines) {
if (line.startsWith('+++') || line.startsWith('---')) continue;
if (line.startsWith('+')) insertions++;
else if (line.startsWith('-')) deletions++;
}
const lastSlash = filePath.lastIndexOf('/');
const directory = lastSlash >= 0 ? filePath.substring(0, lastSlash + 1) : '';
const filename = lastSlash >= 0 ? filePath.substring(lastSlash + 1) : filePath;
files.push({ path: filePath, directory, filename, status, insertions, deletions });
}
return {
files,
totalFiles: files.length,
totalInsertions: files.reduce((sum, f) => sum + f.insertions, 0),
totalDeletions: files.reduce((sum, f) => sum + f.deletions, 0),
};
}