import { exec } from "node:child_process";
import { promisify } from "node:util";
import type { WorktreeInfo } from "../types.js";
const execAsync = promisify(exec);
export interface ExecResult {
stdout: string;
stderr: string;
}
export async function execGit(
args: string,
cwd?: string
): Promise<ExecResult> {
const options = cwd ? { cwd } : {};
return execAsync(`git ${args}`, options);
}
export async function execCommand(
command: string,
cwd?: string,
env?: NodeJS.ProcessEnv
): Promise<ExecResult> {
const options: { cwd?: string; env?: NodeJS.ProcessEnv } = {};
if (cwd) options.cwd = cwd;
if (env) options.env = { ...process.env, ...env };
return execAsync(command, options);
}
export async function isGitAvailable(): Promise<boolean> {
try {
await execAsync("git --version");
return true;
} catch {
return false;
}
}
export async function getGitVersion(): Promise<{ major: number; minor: number; patch: number } | null> {
try {
const { stdout } = await execAsync("git --version");
// Parse "git version 2.39.1" format
const match = stdout.match(/git version (\d+)\.(\d+)\.(\d+)/);
if (match) {
return {
major: parseInt(match[1], 10),
minor: parseInt(match[2], 10),
patch: parseInt(match[3], 10),
};
}
return null;
} catch {
return null;
}
}
export function isGitVersionSupported(version: { major: number; minor: number; patch: number }): boolean {
// Worktrees require git 2.5+
if (version.major > 2) return true;
if (version.major === 2 && version.minor >= 5) return true;
return false;
}
export async function isGitRepository(cwd?: string): Promise<boolean> {
try {
await execGit("rev-parse --git-dir", cwd);
return true;
} catch {
return false;
}
}
export async function getRepoRoot(cwd?: string): Promise<string> {
const { stdout } = await execGit("rev-parse --show-toplevel", cwd);
return stdout.trim();
}
export async function getCurrentBranch(cwd?: string): Promise<string> {
const { stdout } = await execGit("symbolic-ref --short HEAD", cwd);
return stdout.trim();
}
export async function branchExists(
branch: string,
cwd?: string
): Promise<boolean> {
try {
await execGit(`rev-parse --verify refs/heads/${branch}`, cwd);
return true;
} catch {
return false;
}
}
export async function hasUncommittedChanges(cwd?: string): Promise<boolean> {
try {
await execGit("diff-index --quiet HEAD --", cwd);
return false;
} catch {
return true;
}
}
export async function hasMergeConflicts(cwd?: string): Promise<boolean> {
try {
const { stdout } = await execGit(
"diff --name-only --diff-filter=U",
cwd
);
return stdout.trim().length > 0;
} catch {
return false;
}
}
export async function getConflictedFiles(cwd?: string): Promise<string[]> {
try {
const { stdout } = await execGit(
"diff --name-only --diff-filter=U",
cwd
);
return stdout
.trim()
.split("\n")
.filter((f) => f.length > 0);
} catch {
return [];
}
}
export async function getWorktreeList(cwd?: string): Promise<WorktreeInfo[]> {
const { stdout } = await execGit("worktree list --porcelain", cwd);
const worktrees: WorktreeInfo[] = [];
let current: Partial<WorktreeInfo> = {};
for (const line of stdout.split("\n")) {
if (line.startsWith("worktree ")) {
if (current.path) {
worktrees.push(current as WorktreeInfo);
}
current = { path: line.replace("worktree ", "") };
} else if (line.startsWith("HEAD ")) {
current.head = line.replace("HEAD ", "");
} else if (line.startsWith("branch ")) {
current.branch = line.replace("branch ", "").replace("refs/heads/", "");
} else if (line === "bare") {
current.bare = true;
} else if (line.startsWith("locked")) {
current.locked = true;
const reason = line.replace("locked ", "").trim();
if (reason) current.lockReason = reason;
} else if (line === "prunable") {
current.prunable = true;
}
}
if (current.path) {
worktrees.push(current as WorktreeInfo);
}
return worktrees;
}
export async function createWorktree(
path: string,
branch: string,
createBranch: boolean,
cwd?: string
): Promise<void> {
if (createBranch) {
await execGit(`worktree add -b ${branch} "${path}"`, cwd);
} else {
await execGit(`worktree add "${path}" ${branch}`, cwd);
}
}
export async function removeWorktree(
path: string,
force: boolean,
cwd?: string
): Promise<void> {
const forceFlag = force ? " --force" : "";
await execGit(`worktree remove "${path}"${forceFlag}`, cwd);
}
export async function lockWorktree(
path: string,
reason?: string,
cwd?: string
): Promise<void> {
const reasonArg = reason ? ` --reason "${reason}"` : "";
await execGit(`worktree lock "${path}"${reasonArg}`, cwd);
}
export async function unlockWorktree(
path: string,
cwd?: string
): Promise<void> {
await execGit(`worktree unlock "${path}"`, cwd);
}
export async function pruneWorktrees(cwd?: string): Promise<string> {
const { stdout, stderr } = await execGit("worktree prune -v", cwd);
return stdout + stderr;
}
export async function merge(
branch: string,
squash: boolean,
cwd?: string
): Promise<void> {
if (squash) {
await execGit(`merge --squash ${branch}`, cwd);
} else {
await execGit(`merge ${branch} --no-edit`, cwd);
}
}
export async function abortMerge(cwd?: string): Promise<void> {
await execGit("merge --abort", cwd);
}
export async function checkout(branch: string, cwd?: string): Promise<void> {
await execGit(`checkout ${branch}`, cwd);
}
export async function pull(cwd?: string): Promise<void> {
await execGit("pull", cwd);
}
export async function push(cwd?: string): Promise<void> {
await execGit("push", cwd);
}
export async function commit(message: string, cwd?: string): Promise<void> {
await execGit(`commit -m "${message}"`, cwd);
}
export async function deleteBranch(
branch: string,
force: boolean,
cwd?: string
): Promise<void> {
const flag = force ? "-D" : "-d";
await execGit(`branch ${flag} ${branch}`, cwd);
}
export async function deleteRemoteBranch(
branch: string,
cwd?: string
): Promise<void> {
await execGit(`push origin --delete ${branch}`, cwd);
}
export async function resolveConflict(
file: string,
strategy: "ours" | "theirs",
cwd?: string
): Promise<void> {
await execGit(`checkout --${strategy} "${file}"`, cwd);
await execGit(`add "${file}"`, cwd);
}
export async function commitMerge(cwd?: string): Promise<void> {
await execGit("commit --no-edit", cwd);
}
export async function getStatus(cwd?: string): Promise<string> {
const { stdout } = await execGit("status", cwd);
return stdout;
}
export async function getLog(
count: number = 5,
cwd?: string
): Promise<string> {
const { stdout } = await execGit(`log --oneline -${count}`, cwd);
return stdout;
}