/** * Branch list helpers * * Listing, parsing, and enriching takt-managed branches * with metadata (diff stats, original instruction, task slug). * Used by the /list command. */ import { execFileSync } from 'node:child_process'; import { createLogger } from '../../shared/utils/index.js'; import type { BranchInfo, BranchListItem } from './types.js'; export type { BranchInfo, BranchListItem }; const log = createLogger('branchList'); const TAKT_BRANCH_PREFIX = 'takt/'; /** * Manages takt branch listing and metadata enrichment. */ export class BranchManager { /** Detect the default branch name (main or master) */ detectDefaultBranch(cwd: string): string { try { const ref = execFileSync( 'git', ['symbolic-ref', 'refs/remotes/origin/HEAD'], { cwd, encoding: 'utf-8', stdio: 'pipe' }, ).trim(); const parts = ref.split('/'); return parts[parts.length - 1] || 'main'; } catch { try { execFileSync('git', ['rev-parse', '--verify', 'main'], { cwd, encoding: 'utf-8', stdio: 'pipe', }); return 'main'; } catch { try { execFileSync('git', ['rev-parse', '--verify', 'master'], { cwd, encoding: 'utf-8', stdio: 'pipe', }); return 'master'; } catch { return 'main'; } } } } /** List all takt-managed branches */ listTaktBranches(projectDir: string): BranchInfo[] { try { const output = execFileSync( 'git', ['branch', '--list', 'takt/*', '--format=%(refname:short) %(objectname:short)'], { cwd: projectDir, encoding: 'utf-8', stdio: 'pipe' }, ); return BranchManager.parseTaktBranches(output); } catch (err) { log.error('Failed to list takt branches', { error: String(err) }); return []; } } /** Parse `git branch --list` formatted output into BranchInfo entries */ static parseTaktBranches(output: string): BranchInfo[] { const entries: BranchInfo[] = []; const lines = output.trim().split('\n'); for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; const spaceIdx = trimmed.lastIndexOf(' '); if (spaceIdx === -1) continue; const branch = trimmed.slice(0, spaceIdx); const commit = trimmed.slice(spaceIdx + 1); if (branch.startsWith(TAKT_BRANCH_PREFIX)) { entries.push({ branch, commit }); } } return entries; } /** Get the number of files changed between the default branch and a given branch */ getFilesChanged(cwd: string, defaultBranch: string, branch: string): number { try { const output = execFileSync( 'git', ['diff', '--numstat', `${defaultBranch}...${branch}`], { cwd, encoding: 'utf-8', stdio: 'pipe' }, ); return output.trim().split('\n').filter(l => l.length > 0).length; } catch { return 0; } } /** Extract a human-readable task slug from a takt branch name */ static extractTaskSlug(branch: string): string { const name = branch.replace(TAKT_BRANCH_PREFIX, ''); const withoutTimestamp = name.replace(/^\d{8,}T?\d{0,6}-?/, ''); return withoutTimestamp || name; } /** * Extract the original task instruction from the first commit message on a branch. * The first commit on a takt branch has the format: "takt: {original instruction}". */ getOriginalInstruction( cwd: string, defaultBranch: string, branch: string, ): string { try { const output = execFileSync( 'git', ['log', '--format=%s', '--reverse', `${defaultBranch}..${branch}`], { cwd, encoding: 'utf-8', stdio: 'pipe' }, ).trim(); if (!output) return ''; const firstLine = output.split('\n')[0] || ''; const TAKT_COMMIT_PREFIX = 'takt:'; if (firstLine.startsWith(TAKT_COMMIT_PREFIX)) { return firstLine.slice(TAKT_COMMIT_PREFIX.length).trim(); } return firstLine; } catch { return ''; } } /** Build list items from branch list, enriching with diff stats */ buildListItems( projectDir: string, branches: BranchInfo[], defaultBranch: string, ): BranchListItem[] { return branches.map(br => ({ info: br, filesChanged: this.getFilesChanged(projectDir, defaultBranch, br.branch), taskSlug: BranchManager.extractTaskSlug(br.branch), originalInstruction: this.getOriginalInstruction(projectDir, defaultBranch, br.branch), })); } } // ---- Backward-compatible module-level functions ---- const defaultManager = new BranchManager(); export function detectDefaultBranch(cwd: string): string { return defaultManager.detectDefaultBranch(cwd); } export function listTaktBranches(projectDir: string): BranchInfo[] { return defaultManager.listTaktBranches(projectDir); } export function parseTaktBranches(output: string): BranchInfo[] { return BranchManager.parseTaktBranches(output); } export function getFilesChanged(cwd: string, defaultBranch: string, branch: string): number { return defaultManager.getFilesChanged(cwd, defaultBranch, branch); } export function extractTaskSlug(branch: string): string { return BranchManager.extractTaskSlug(branch); } export function getOriginalInstruction(cwd: string, defaultBranch: string, branch: string): string { return defaultManager.getOriginalInstruction(cwd, defaultBranch, branch); } export function buildListItems( projectDir: string, branches: BranchInfo[], defaultBranch: string, ): BranchListItem[] { return defaultManager.buildListItems(projectDir, branches, defaultBranch); }