UNPKG

worktree-tool

Version:

A command-line tool for managing Git worktrees with integrated tmux/shell session management

249 lines 10.2 kB
import chalk from "chalk"; // Status symbols - easy to change between emoji and text symbols const STATUS_SYMBOLS = { add: "(+)", mod: "(*)", del: "(-)", ren: "(\u2192)", // → arrow copy: "(\u00bb)", // » double chevron conflict: "(!)", untracked: "(?)", }; // Commit status arrows (vs main branch) const COMMIT_ARROWS = { ahead: "\u2191", // ↑ up arrow behind: "\u2193", // ↓ down arrow }; // Color constants const STAGED_COLOR = chalk.green; const MIXED_COLOR = chalk.yellow; const UNSTAGED_COLOR = chalk.white; const UNTRACKED_COLOR = chalk.gray; const CONFLICT_COLOR = chalk.red; const POTENTIAL_CONFLICT_COLOR = chalk.hex("#FFA500"); // orange const DEFAULT_COLOR = (text) => text; /** * Format a worktree status into a colorful string for display */ export function formatWorktreeStatus(status, maxNameLength) { const paddedName = status.name.padEnd(maxNameLength); const coloredName = DEFAULT_COLOR(`[${paddedName}]`); const statusParts = []; // Conflicts first if (status.counts.conflicts > 0) { statusParts.push(CONFLICT_COLOR(`${STATUS_SYMBOLS.conflict}${String(status.counts.conflicts)}`)); } else if (status.hasConflicts) { // Potential conflicts (orange) const count = status.potentialConflictCount ?? 1; statusParts.push(POTENTIAL_CONFLICT_COLOR(`${STATUS_SYMBOLS.conflict}${String(count)}`)); } // Check for each file type - determine if staged only, unstaged only, or mixed const fileTypes = [ { symbol: STATUS_SYMBOLS.add, stagedCount: status.counts.staged.add, unstagedCount: status.counts.unstaged.add }, { symbol: STATUS_SYMBOLS.mod, stagedCount: status.counts.staged.mod, unstagedCount: status.counts.unstaged.mod }, { symbol: STATUS_SYMBOLS.del, stagedCount: status.counts.staged.del, unstagedCount: status.counts.unstaged.del }, { symbol: STATUS_SYMBOLS.ren, stagedCount: status.counts.staged.ren, unstagedCount: 0 }, { symbol: STATUS_SYMBOLS.copy, stagedCount: status.counts.staged.copy, unstagedCount: 0 }, ]; for (const fileType of fileTypes) { const totalCount = fileType.stagedCount + fileType.unstagedCount; if (totalCount > 0) { let color; if (fileType.stagedCount > 0 && fileType.unstagedCount > 0) { // Mixed staged and unstaged color = MIXED_COLOR; } else if (fileType.stagedCount > 0) { // Only staged color = STAGED_COLOR; } else { // Only unstaged color = UNSTAGED_COLOR; } statusParts.push(color(`${fileType.symbol}${String(totalCount)}`)); } } // Untracked files (grey) if (status.counts.untracked > 0) { statusParts.push(UNTRACKED_COLOR(`${STATUS_SYMBOLS.untracked}${String(status.counts.untracked)}`)); } // Commit status vs main branch (default color) if (status.ahead > 0 && status.behind > 0) { statusParts.push(DEFAULT_COLOR(`${COMMIT_ARROWS.ahead}${String(status.ahead)}${COMMIT_ARROWS.behind}${String(status.behind)}`)); } else if (status.ahead > 0) { statusParts.push(DEFAULT_COLOR(`${COMMIT_ARROWS.ahead}${String(status.ahead)}`)); } else if (status.behind > 0) { statusParts.push(DEFAULT_COLOR(`${COMMIT_ARROWS.behind}${String(status.behind)}`)); } return `${coloredName} ${statusParts.join(" ")}`; } /** * Parse a single line from git status --porcelain output */ export function parseStatusLine(line) { const stagedStatus = !line.startsWith(" ") ? line[0] ?? null : null; const unstagedStatus = line[1] !== " " ? line[1] ?? null : null; const path = line.substring(3); return { stagedStatus, unstagedStatus, path }; } /** * Map git status codes to our categories */ export function categorizeStatus(statusCode) { switch (statusCode) { case "A": return "add"; case "M": return "mod"; case "D": return "del"; case "R": return "ren"; case "C": return "copy"; case "U": return "conflict"; case "?": return "untracked"; default: return null; } } /** * Display the legend for status symbols */ export function displayLegend() { // eslint-disable-next-line no-console console.log("\nLegend:"); // Symbols section // eslint-disable-next-line no-console console.log(` ${STATUS_SYMBOLS.add} Added files`); // eslint-disable-next-line no-console console.log(` ${STATUS_SYMBOLS.mod} Modified files`); // eslint-disable-next-line no-console console.log(` ${STATUS_SYMBOLS.del} Deleted files`); // eslint-disable-next-line no-console console.log(` ${STATUS_SYMBOLS.ren} Renamed files`); // eslint-disable-next-line no-console console.log(` ${STATUS_SYMBOLS.copy} Copied files`); // eslint-disable-next-line no-console console.log(` ${STATUS_SYMBOLS.conflict} Conflicts`); // eslint-disable-next-line no-console console.log(` ${STATUS_SYMBOLS.untracked} Untracked files`); // eslint-disable-next-line no-console console.log(` ${COMMIT_ARROWS.ahead}N Commits ahead of main`); // eslint-disable-next-line no-console console.log(` ${COMMIT_ARROWS.behind}N Commits behind main`); // eslint-disable-next-line no-console console.log(""); // Colors section // eslint-disable-next-line no-console console.log(` ${STAGED_COLOR("green")}: staged changes`); // eslint-disable-next-line no-console console.log(` ${MIXED_COLOR("yellow")}: mix of staged and unstaged changes`); // eslint-disable-next-line no-console console.log(` ${UNSTAGED_COLOR("white")}: unstaged changes`); // eslint-disable-next-line no-console console.log(` ${UNTRACKED_COLOR("grey")}: untracked changes`); // eslint-disable-next-line no-console console.log(` ${CONFLICT_COLOR("red")}: active conflicts`); // eslint-disable-next-line no-console console.log(` ${POTENTIAL_CONFLICT_COLOR("orange")}: potential conflicts\n`); } /** * Format and display verbose file listing */ export function displayVerboseFiles(lines) { // Sort lines to put conflicts first const sortedLines = [...lines].sort((a, b) => { const { stagedStatus: aStaged, unstagedStatus: aUnstaged } = parseStatusLine(a); const { stagedStatus: bStaged, unstagedStatus: bUnstaged } = parseStatusLine(b); const aIsConflict = aStaged === "U" || (aStaged === "A" && aUnstaged === "A") || (aStaged === "D" && aUnstaged === "D"); const bIsConflict = bStaged === "U" || (bStaged === "A" && bUnstaged === "A") || (bStaged === "D" && bUnstaged === "D"); if (aIsConflict && !bIsConflict) { return -1; } if (!aIsConflict && bIsConflict) { return 1; } return 0; }); for (const line of sortedLines) { if (!line.trim()) { continue; } const { stagedStatus, unstagedStatus, path } = parseStatusLine(line); // Handle different file states if (stagedStatus === "?" && unstagedStatus === "?") { // Untracked files (grey) // eslint-disable-next-line no-console console.log(`${UNTRACKED_COLOR(STATUS_SYMBOLS.untracked)} ${path}`); } else if (stagedStatus === "U" || (stagedStatus === "A" && unstagedStatus === "A") || (stagedStatus === "D" && unstagedStatus === "D")) { // Conflicts (red) // eslint-disable-next-line no-console console.log(`${CONFLICT_COLOR(STATUS_SYMBOLS.conflict)} ${path}`); } else if (stagedStatus && stagedStatus !== " ") { // Staged changes (green) const category = categorizeStatus(stagedStatus); if (category && category !== "conflict" && category !== "untracked") { const symbol = STATUS_SYMBOLS[category]; // eslint-disable-next-line no-console console.log(`${STAGED_COLOR(symbol)} ${path}`); } } else if (unstagedStatus && unstagedStatus !== " ") { // Unstaged changes (gray) const category = categorizeStatus(unstagedStatus); if (category && category !== "conflict" && category !== "untracked") { const symbol = STATUS_SYMBOLS[category]; // eslint-disable-next-line no-console console.log(`${UNSTAGED_COLOR(symbol)} ${path}`); } } } } /** * Count statuses from porcelain output lines */ export function countStatuses(lines) { const counts = { staged: { add: 0, mod: 0, del: 0, ren: 0, copy: 0, untracked: 0 }, unstaged: { add: 0, mod: 0, del: 0, ren: 0, copy: 0, untracked: 0 }, conflicts: 0, untracked: 0, }; for (const line of lines) { if (!line.trim()) { continue; } const { stagedStatus, unstagedStatus } = parseStatusLine(line); // Check for conflicts (UU, AA, DD) if (stagedStatus === "U" || (stagedStatus === "A" && unstagedStatus === "A") || (stagedStatus === "D" && unstagedStatus === "D")) { counts.conflicts++; continue; } // Handle untracked files specially (they appear as "?? filename") if (stagedStatus === "?" && unstagedStatus === "?") { counts.untracked++; continue; } // Count staged changes if (stagedStatus) { const category = categorizeStatus(stagedStatus); if (category && category !== "conflict" && category !== "untracked") { counts.staged[category]++; } } // Count unstaged changes if (unstagedStatus) { const category = categorizeStatus(unstagedStatus); if (category && category !== "conflict" && category !== "untracked") { counts.unstaged[category]++; } } } return counts; } //# sourceMappingURL=status-formatter.js.map