mirror of
https://github.com/jarrodwatts/claude-hud.git
synced 2026-05-21 15:52:37 +00:00
Add showFileStats option to display file change counts in Starship-compatible format: - ! = Modified files - + = Added/staged files - ✘ = Deleted files - ? = Untracked files Example: git:(main* !2 +1 ?3) Changes: - src/git.ts: Add FileStats interface and parseFileStats() function - src/config.ts: Add showFileStats config option (default: false) - src/render/session-line.ts: Add file stats rendering logic - commands/configure.md: Add "File stats" option to Git Style selection - tests: Add unit tests for fileStats parsing and rendering Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
119 lines
3.1 KiB
TypeScript
119 lines
3.1 KiB
TypeScript
import { execFile } from 'node:child_process';
|
|
import { promisify } from 'node:util';
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
export interface FileStats {
|
|
modified: number;
|
|
added: number;
|
|
deleted: number;
|
|
untracked: number;
|
|
}
|
|
|
|
export interface GitStatus {
|
|
branch: string;
|
|
isDirty: boolean;
|
|
ahead: number;
|
|
behind: number;
|
|
fileStats?: FileStats;
|
|
}
|
|
|
|
export async function getGitBranch(cwd?: string): Promise<string | null> {
|
|
if (!cwd) return null;
|
|
|
|
try {
|
|
const { stdout } = await execFileAsync(
|
|
'git',
|
|
['rev-parse', '--abbrev-ref', 'HEAD'],
|
|
{ cwd, timeout: 1000, encoding: 'utf8' }
|
|
);
|
|
return stdout.trim() || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function getGitStatus(cwd?: string): Promise<GitStatus | null> {
|
|
if (!cwd) return null;
|
|
|
|
try {
|
|
// Get branch name
|
|
const { stdout: branchOut } = await execFileAsync(
|
|
'git',
|
|
['rev-parse', '--abbrev-ref', 'HEAD'],
|
|
{ cwd, timeout: 1000, encoding: 'utf8' }
|
|
);
|
|
const branch = branchOut.trim();
|
|
if (!branch) return null;
|
|
|
|
// Check for dirty state and parse file stats
|
|
let isDirty = false;
|
|
let fileStats: FileStats | undefined;
|
|
try {
|
|
const { stdout: statusOut } = await execFileAsync(
|
|
'git',
|
|
['--no-optional-locks', 'status', '--porcelain'],
|
|
{ cwd, timeout: 1000, encoding: 'utf8' }
|
|
);
|
|
const trimmed = statusOut.trim();
|
|
isDirty = trimmed.length > 0;
|
|
if (isDirty) {
|
|
fileStats = parseFileStats(trimmed);
|
|
}
|
|
} catch {
|
|
// Ignore errors, assume clean
|
|
}
|
|
|
|
// Get ahead/behind counts
|
|
let ahead = 0;
|
|
let behind = 0;
|
|
try {
|
|
const { stdout: revOut } = await execFileAsync(
|
|
'git',
|
|
['rev-list', '--left-right', '--count', '@{upstream}...HEAD'],
|
|
{ cwd, timeout: 1000, encoding: 'utf8' }
|
|
);
|
|
const parts = revOut.trim().split(/\s+/);
|
|
if (parts.length === 2) {
|
|
behind = parseInt(parts[0], 10) || 0;
|
|
ahead = parseInt(parts[1], 10) || 0;
|
|
}
|
|
} catch {
|
|
// No upstream or error, keep 0/0
|
|
}
|
|
|
|
return { branch, isDirty, ahead, behind, fileStats };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse git status --porcelain output and count file stats (Starship-compatible format)
|
|
* Status codes: M=modified, A=added, D=deleted, ??=untracked
|
|
*/
|
|
function parseFileStats(porcelainOutput: string): FileStats {
|
|
const stats: FileStats = { modified: 0, added: 0, deleted: 0, untracked: 0 };
|
|
const lines = porcelainOutput.split('\n').filter(Boolean);
|
|
|
|
for (const line of lines) {
|
|
if (line.length < 2) continue;
|
|
|
|
const index = line[0]; // staged status
|
|
const worktree = line[1]; // unstaged status
|
|
|
|
if (line.startsWith('??')) {
|
|
stats.untracked++;
|
|
} else if (index === 'A') {
|
|
stats.added++;
|
|
} else if (index === 'D' || worktree === 'D') {
|
|
stats.deleted++;
|
|
} else if (index === 'M' || worktree === 'M' || index === 'R' || index === 'C') {
|
|
// M=modified, R=renamed (counts as modified), C=copied (counts as modified)
|
|
stats.modified++;
|
|
}
|
|
}
|
|
|
|
return stats;
|
|
}
|