UNPKG

@vibe-validate/git

Version:

Git utilities for vibe-validate - tree hash calculation, branch sync, and post-merge cleanup

171 lines 5.79 kB
/** * Smart Branch Sync Checker * * Safely checks if the current branch is behind a remote branch without auto-merging. * Provides clear status reporting and next-step instructions. * * Key safety features: * - Never auto-merges (preserves conflict visibility) * - Clear exit codes for CI/agent integration * - Explicit instructions when manual intervention needed * - Cross-platform compatibility */ import { spawn } from 'node:child_process'; const GIT_TIMEOUT = 30000; // 30 seconds timeout for git operations /** * Execute git command safely using spawn (prevents command injection) * * @param args - Git command arguments (e.g., ['rev-parse', '--abbrev-ref', 'HEAD']) * @returns Promise resolving to stdout and stderr */ function execGit(args) { return new Promise((resolve, reject) => { const child = spawn('git', args, { timeout: GIT_TIMEOUT, stdio: ['ignore', 'pipe', 'pipe'], // No stdin (git commands shouldn't need interactive input), capture stdout/stderr }); let stdout = ''; let stderr = ''; if (child.stdout) { child.stdout.on('data', (data) => { stdout += data.toString(); }); } if (child.stderr) { child.stderr.on('data', (data) => { stderr += data.toString(); }); } child.on('error', (error) => { reject(error); }); child.on('close', (code) => { if (code === 0) { resolve({ stdout, stderr }); } else { reject(new Error(`git exited with code ${code}: ${stderr}`)); } }); }); } /** * Branch Sync Checker * * Checks if current branch is behind a remote branch */ export class BranchSyncChecker { remoteBranch; gitExecutor; constructor(options = {}) { this.remoteBranch = options.remoteBranch ?? 'origin/main'; this.gitExecutor = options.gitExecutor ?? execGit; } /** * Check if the current branch is synchronized with remote branch * * @returns Promise resolving to sync status information */ async checkSync() { try { // Get current branch name const currentBranch = await this.getCurrentBranch(); // Check if remote branch exists const hasRemote = await this.hasRemoteBranch(); if (!hasRemote) { return { isUpToDate: true, behindBy: 0, currentBranch, hasRemote: false, error: `No remote branch ${this.remoteBranch} found` }; } // Fetch latest from remote await this.fetchRemote(); // Check how many commits behind const behindBy = await this.getCommitsBehind(); return { isUpToDate: behindBy === 0, behindBy, currentBranch, hasRemote: true }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { isUpToDate: false, behindBy: -1, currentBranch: 'unknown', hasRemote: false, error: errorMessage }; } } async getCurrentBranch() { try { const { stdout } = await this.gitExecutor(['rev-parse', '--abbrev-ref', 'HEAD']); return stdout.trim(); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Not in a git repository or unable to determine current branch: ${errorMessage}`); } } async hasRemoteBranch() { try { await this.gitExecutor(['rev-parse', '--verify', this.remoteBranch]); return true; } catch { // Expected when remote branch doesn't exist return false; } } async fetchRemote() { try { const [remote, branch] = this.remoteBranch.split('/'); await this.gitExecutor(['fetch', '--quiet', remote, branch]); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to fetch from ${this.remoteBranch}: ${errorMessage}`); } } async getCommitsBehind() { try { const { stdout } = await this.gitExecutor(['rev-list', '--count', `HEAD..${this.remoteBranch}`]); const count = Number.parseInt(stdout.trim(), 10); return Number.isNaN(count) ? 0 : count; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to check commits behind: ${errorMessage}`); } } /** * Get appropriate exit code based on sync result * * @param result - The sync check result * @returns Exit code (0=success, 1=needs merge, 2=error) */ getExitCode(result) { if (result.error) return 2; // Error condition if (!result.hasRemote) return 0; // No remote, consider OK return result.isUpToDate ? 0 : 1; // 0 = up to date, 1 = needs merge } } /** * Convenience function for quick sync checking * * @param options - Sync check options * @returns Sync check result */ export async function checkBranchSync(options = {}) { const checker = new BranchSyncChecker(options); return checker.checkSync(); } //# sourceMappingURL=branch-sync.js.map