// Branch utilities — delegates to gitStatus.ts for branch/uncommitted state
/**
* gitAutoCommit - Feature branch management and auto-commit utilities
*
* Provides functions for:
* - Auto-creating feature branches (only when needed)
* - Branch detection and validation
* - Feature workflow support
*/
import { log } from './logger.js';
import { execGitCommand } from './gitCommands.js';
import { getCurrentBranchName, getUncommittedStatus } from './gitStatus.js';
/**
* Result of ensuring feature branch exists
*/
export interface FeatureBranchResult {
branch: string;
created: boolean;
}
/**
* Get current git branch name
*
* @deprecated Delegates to getCurrentBranchName() from gitStatus.ts.
* Returns null for detached HEAD ('HEAD') or any error state ('unknown').
*
* @param projectPath - Path to git repository
* @returns Current branch name, or null if not in a git repo, detached HEAD, or error
*/
export async function getCurrentBranch(projectPath: string): Promise<string | null> {
const branch = await getCurrentBranchName(projectPath);
// Map sentinel values to null: 'HEAD' = detached HEAD, 'unknown' = error/not a repo
return (branch === 'HEAD' || branch === 'unknown') ? null : branch;
}
/**
* Check if there are uncommitted changes in the repository
*
* @deprecated Delegates to getUncommittedStatus() from gitStatus.ts.
*
* @param projectPath - Path to git repository root
* @returns true if there are uncommitted changes
*/
export async function hasUncommittedChanges(projectPath: string): Promise<boolean> {
const status = await getUncommittedStatus(projectPath);
return status.count > 0;
}
/**
* Check if branch name matches feature branch pattern
*
* Feature branches start with 'llm-feature-' prefix.
* Examples:
* - llm-feature-user-auth ✓
* - llm-feature-auto-20250121143022 ✓
* - main ✗
* - develop ✗
* - feature/user-auth ✗
*
* @param branchName - Branch name to check
* @returns true if branch is a feature branch
*/
export function isFeatureBranch(branchName: string): boolean {
return branchName.startsWith('llm-feature-');
}
/**
* Generate auto-generated feature branch name with timestamp
*
* Format: llm-feature-auto-YYYYMMDDHHmmss
* Example: llm-feature-auto-20250121143022
*
* @returns Auto-generated branch name
*/
function generateAutoBranchName(): string {
const timestamp = new Date()
.toISOString()
.replace(/[:-]/g, '')
.replace(/\..+$/, '') // Remove milliseconds and Z
.slice(0, 14); // YYYYMMDDHHmmss (14 chars)
return `llm-feature-auto-${timestamp}`;
}
/**
* Ensure feature branch exists before committing
*
* Auto-creates feature branch ONLY if:
* - Not already on a feature branch
* - Git repository exists and is accessible
*
* If already on a feature branch (e.g., llm-feature-user-auth), uses that branch.
* If on non-feature branch (e.g., main), creates new auto-generated branch.
*
* Branch naming:
* - Auto-created: llm-feature-auto-{timestamp}
* - Explicit (via git_feature start): llm-feature-{featureName}
*
* @param projectPath - Path to git repository root
* @returns Branch name and whether it was created
* @throws Error if git operations fail
*
* @example
* // Already on feature branch - reuse it
* const result = await ensureFeatureBranch('/path/to/repo');
* // { branch: 'llm-feature-user-auth', created: false }
*
* @example
* // On main branch - create auto branch
* const result = await ensureFeatureBranch('/path/to/repo');
* // { branch: 'llm-feature-auto-20250121143022', created: true }
*/
export async function ensureFeatureBranch(projectPath: string): Promise<FeatureBranchResult> {
try {
// Get current branch
const currentBranch = await getCurrentBranch(projectPath);
if (!currentBranch) {
throw new Error('Could not determine current git branch - not in a git repository or in detached HEAD state');
}
// Already on feature branch - use it
if (isFeatureBranch(currentBranch)) {
log.info(`[GIT-AUTO-COMMIT] Using existing feature branch: ${currentBranch}`);
return {
branch: currentBranch,
created: false
};
}
// Not on feature branch - create new auto-generated branch
const newBranchName = generateAutoBranchName();
log.info(`[GIT-AUTO-COMMIT] Creating new feature branch: ${newBranchName}`);
// SECURITY: Use spawn with array args to prevent shell injection
await execGitCommand(['checkout', '-b', newBranchName], projectPath);
log.info(`[GIT-AUTO-COMMIT] ✓ Feature branch created: ${newBranchName}`);
return {
branch: newBranchName,
created: true
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
log.error('[GIT-AUTO-COMMIT] Error ensuring feature branch:', errorMsg);
throw new Error(`Failed to ensure feature branch: ${errorMsg}`);
}
}
/**
* Get list of all branches in repository
*
* @param projectPath - Path to git repository root
* @returns Array of branch names
* @throws Error if git command fails
*/
export async function getAllBranches(projectPath: string): Promise<string[]> {
try {
const result = await execGitCommand(['branch'], projectPath);
return result
.split('\n')
.map((line: string) => line.trim().replace(/^[*+]\s+/, '')) // Remove * (current) and + (worktree) markers
.filter(Boolean); // Remove empty lines
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
log.error('[GIT-AUTO-COMMIT] Error listing branches:', errorMsg);
throw new Error(`Failed to list branches: ${errorMsg}`);
}
}
/**
* Get list of feature branches only
*
* @param projectPath - Path to git repository root
* @returns Array of feature branch names
*/
export async function getFeatureBranches(projectPath: string): Promise<string[]> {
const allBranches = await getAllBranches(projectPath);
return allBranches.filter(isFeatureBranch);
}