@vibe-validate/git
Version:
Git utilities for vibe-validate - tree hash calculation, branch sync, and post-merge cleanup
171 lines • 5.79 kB
JavaScript
/**
* 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