/** * Git status parsing utilities */ import { exec } from 'child_process'; import { promisify } from 'util'; import { GIT_STATUS_MAP, type FileStatus } from './types.js'; const execAsync = promisify(exec); /** * Get a readable status text from git status codes * Handles both single character and XY format status codes */ function getStatusText(indexStatus: string, workTreeStatus: string): string { // Untracked files if (indexStatus === '?' && workTreeStatus === '?') { return 'Untracked'; } // Ignored files if (indexStatus === '!' && workTreeStatus === '!') { return 'Ignored'; } // Prioritize staging area status, then working tree const primaryStatus = indexStatus !== ' ' && indexStatus !== '?' ? indexStatus : workTreeStatus; // Handle combined statuses if ( indexStatus !== ' ' && indexStatus !== '?' && workTreeStatus !== ' ' && workTreeStatus !== '?' ) { // Both staging and working tree have changes const indexText = GIT_STATUS_MAP[indexStatus] || 'Changed'; const workText = GIT_STATUS_MAP[workTreeStatus] || 'Changed'; if (indexText === workText) { return indexText; } return `${indexText} (staged), ${workText} (unstaged)`; } return GIT_STATUS_MAP[primaryStatus] || 'Changed'; } /** * Check if a path is a git repository */ export async function isGitRepo(repoPath: string): Promise { try { await execAsync('git rev-parse --is-inside-work-tree', { cwd: repoPath }); return true; } catch { return false; } } /** * Parse the output of `git status --porcelain` into FileStatus array * Git porcelain format: XY PATH where X=staging area status, Y=working tree status * For renamed files: XY ORIG_PATH -> NEW_PATH */ export function parseGitStatus(statusOutput: string): FileStatus[] { return statusOutput .split('\n') .filter(Boolean) .map((line) => { // Git porcelain format uses two status characters: XY // X = status in staging area (index) // Y = status in working tree const indexStatus = line[0] || ' '; const workTreeStatus = line[1] || ' '; // File path starts at position 3 (after "XY ") let filePath = line.slice(3); // Handle renamed files (format: "R old_path -> new_path") if (indexStatus === 'R' || workTreeStatus === 'R') { const arrowIndex = filePath.indexOf(' -> '); if (arrowIndex !== -1) { filePath = filePath.slice(arrowIndex + 4); // Use new path } } // Determine the primary status character for backwards compatibility // Prioritize staging area status, then working tree let primaryStatus: string; if (indexStatus === '?' && workTreeStatus === '?') { primaryStatus = '?'; // Untracked } else if (indexStatus !== ' ' && indexStatus !== '?') { primaryStatus = indexStatus; // Staged change } else { primaryStatus = workTreeStatus; // Working tree change } return { status: primaryStatus, path: filePath, statusText: getStatusText(indexStatus, workTreeStatus), }; }); }