#!/usr/bin/env bun /** * Git utilities for upstream merge automation */ import { $ } from "bun" export interface BranchInfo { current: string exists: boolean } export interface RemoteInfo { name: string url: string } export async function getCurrentBranch(): Promise { const result = await $`git rev-parse --abbrev-ref HEAD`.text() return result.trim() } export async function branchExists(name: string): Promise { const result = await $`git show-ref --verify --quiet refs/heads/${name}`.nothrow() return result.exitCode === 0 } export async function remoteBranchExists(remote: string, branch: string): Promise { const result = await $`git ls-remote --heads ${remote} ${branch}`.text() return result.trim().length > 0 } export async function getRemotes(): Promise { const result = await $`git remote -v`.text() const lines = result.trim().split("\n") const remotes: RemoteInfo[] = [] const seen = new Set() for (const line of lines) { const parts = line.split(/\s+/) const name = parts[0] ?? "" const url = parts[1] ?? "" if (name && !seen.has(name)) { seen.add(name) remotes.push({ name, url }) } } return remotes } export async function hasUpstreamRemote(): Promise { const remotes = await getRemotes() return remotes.some((r) => r.name === "upstream") } export async function fetchUpstream(): Promise { const result = await $`git fetch upstream`.quiet().nothrow() if (result.exitCode !== 0) { throw new Error(`Failed to fetch upstream: ${result.stderr.toString()}`) } } export async function checkout(ref: string): Promise { await $`git checkout ${ref}` } export async function createBranch(name: string, from?: string): Promise { if (from) { await $`git checkout -b ${name} ${from}` } else { await $`git checkout -b ${name}` } } export async function deleteBranch(name: string, force = false): Promise { if (force) { await $`git branch -D ${name}` } else { await $`git branch -d ${name}` } } export async function backupAndDeleteBranch(name: string): Promise { if (!(await branchExists(name))) return null const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19) const backupName = `backup/${name}-${timestamp}` const current = await getCurrentBranch() // Create backup from the existing branch await $`git branch ${backupName} ${name}` // Delete the old branch (must not be on it) if (current === name) { throw new Error(`Cannot backup and delete branch '${name}' while it is checked out`) } await deleteBranch(name, true) return backupName } export async function push(remote = "origin", branch?: string, setUpstream = false): Promise { const currentBranch = branch || (await getCurrentBranch()) if (setUpstream) { await $`git push -u ${remote} ${currentBranch}` } else { await $`git push ${remote} ${currentBranch}` } } export async function pull(remote = "origin", branch?: string): Promise { if (branch) { await $`git pull ${remote} ${branch}` } else { await $`git pull ${remote}` } } export async function commit(message: string): Promise { await $`git commit -am ${message}` } export async function merge(branch: string): Promise<{ success: boolean; conflicts: string[] }> { const result = await $`git merge ${branch}`.nothrow() if (result.exitCode === 0) { return { success: true, conflicts: [] } } // Get list of conflicted files const conflicts = await getConflictedFiles() return { success: false, conflicts } } export async function getConflictedFiles(): Promise { const result = await $`git diff --name-only --diff-filter=U`.text() return result .trim() .split("\n") .filter((f) => f.length > 0) } export async function hasUncommittedChanges(): Promise { const result = await $`git status --porcelain`.text() return result.trim().length > 0 } export async function stageAll(): Promise { await $`git add -A` } export async function stageFiles(files: string[]): Promise { for (const file of files) { await $`git add ${file}` } } export async function getCommitMessage(ref: string): Promise { const result = await $`git log -1 --format=%s ${ref}`.text() return result.trim() } export async function getCommitHash(ref: string): Promise { const result = await $`git rev-parse ${ref}`.text() return result.trim() } export async function getTagsForCommit(commit: string): Promise { const result = await $`git tag --points-at ${commit}`.text() return result .trim() .split("\n") .filter((t) => t.length > 0) } export async function getAllTags(): Promise { const result = await $`git tag -l`.text() return result .trim() .split("\n") .filter((t) => t.length > 0) } export async function getUpstreamTags(): Promise { const result = await $`git ls-remote --tags upstream`.quiet().nothrow() if (result.exitCode !== 0) { throw new Error(`Failed to list upstream tags: ${result.stderr.toString()}`) } const output = result.stdout.toString() const tags: string[] = [] for (const line of output.trim().split("\n")) { const match = line.match(/refs\/tags\/([^\^]+)$/) if (match && match[1]) tags.push(match[1]) } return tags } export async function abortMerge(): Promise { await $`git merge --abort` } export async function checkoutOurs(files: string[]): Promise { for (const file of files) { await $`git checkout --ours ${file}` } } export async function checkoutTheirs(files: string[]): Promise { for (const file of files) { await $`git checkout --theirs ${file}` } }