import { exec, execFile } from "child_process";
import { spawnProcess } from "./spawn.js";
import { promisify } from "util";
import { resolve, dirname, basename } from "path";
import { existsSync, realpathSync } from "fs";
import { open as openFile, stat as statFile } from "fs/promises";
import { TTLCache } from "@isaacs/ttlcache";
import { parseAndHighlightDiff } from "../server/utils/diff-highlighter.js";
import { findExecutable } from "./executable.js";
import { isSeaworkOwnedWorktreeCwd } from "./worktree.js";
import { requireSeaworkWorktreeBaseRefName } from "./worktree-metadata.js";
const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);
const READ_ONLY_GIT_ENV = {
    ...process.env,
    GIT_OPTIONAL_LOCKS: "0",
};
const SMALL_OUTPUT_MAX_BUFFER = 20 * 1024 * 1024; // 20MB
const DEFAULT_PULL_REQUEST_STATUS_CACHE_TTL_MS = 30000;
const PULL_REQUEST_STATUS_CACHE_MAX = 1000;
let pullRequestStatusCacheTtlMs = DEFAULT_PULL_REQUEST_STATUS_CACHE_TTL_MS;
let pullRequestStatusCache = createPullRequestStatusCache(pullRequestStatusCacheTtlMs);
const pullRequestStatusInFlight = new Map();
let cachedGhPath = undefined;
function createPullRequestStatusCache(ttlMs) {
    return new TTLCache({
        ttl: ttlMs,
        max: PULL_REQUEST_STATUS_CACHE_MAX,
        checkAgeOnGet: true,
    });
}
function getPullRequestStatusCacheKey(cwd) {
    return resolve(cwd);
}
export function __resetPullRequestStatusCacheForTests() {
    pullRequestStatusCache.clear();
    pullRequestStatusCache.cancelTimer();
    pullRequestStatusCacheTtlMs = DEFAULT_PULL_REQUEST_STATUS_CACHE_TTL_MS;
    pullRequestStatusCache = createPullRequestStatusCache(pullRequestStatusCacheTtlMs);
    pullRequestStatusInFlight.clear();
}
export function __setPullRequestStatusCacheTtlForTests(ttlMs) {
    pullRequestStatusCache.clear();
    pullRequestStatusCache.cancelTimer();
    pullRequestStatusCacheTtlMs = ttlMs;
    pullRequestStatusCache = createPullRequestStatusCache(ttlMs);
    pullRequestStatusInFlight.clear();
}
export function __resetGhPathCacheForTests() {
    cachedGhPath = undefined;
}
export function __setGhPathForTests(path) {
    cachedGhPath = path;
}
async function execGit(command, options) {
    return execAsync(command, { ...options, maxBuffer: SMALL_OUTPUT_MAX_BUFFER });
}
async function spawnLimitedText(params) {
    const accept = new Set(params.acceptExitCodes ?? [0]);
    return new Promise((resolvePromise, rejectPromise) => {
        const child = spawnProcess(params.cmd, params.args, {
            cwd: params.cwd,
            env: params.env,
            stdio: ["ignore", "pipe", "pipe"],
        });
        const stdoutChunks = [];
        let stdoutBytes = 0;
        let truncated = false;
        const stop = () => {
            if (child.killed)
                return;
            try {
                child.kill("SIGKILL");
            }
            catch {
                // ignore
            }
        };
        child.stdout.on("data", (chunk) => {
            if (truncated)
                return;
            stdoutBytes += chunk.length;
            if (stdoutBytes > params.maxBytes) {
                truncated = true;
                stop();
                return;
            }
            stdoutChunks.push(chunk);
        });
        // We don't buffer stderr (it can be large too). Keep it minimal for debugging.
        let stderrPreview = "";
        child.stderr.on("data", (chunk) => {
            if (stderrPreview.length > 2048)
                return;
            stderrPreview += chunk.toString("utf8");
        });
        child.on("error", (error) => {
            rejectPromise(error);
        });
        child.on("close", (code, signal) => {
            if (code !== null && !accept.has(code) && !truncated) {
                rejectPromise(new Error(`Command failed: ${params.cmd} ${params.args.join(" ")} (code ${code})\n${stderrPreview}`));
                return;
            }
            resolvePromise({
                text: Buffer.concat(stdoutChunks).toString("utf8"),
                truncated,
                exitCode: code,
                signal,
            });
        });
    });
}
function normalizeBranchSuggestionName(raw) {
    const trimmed = raw.trim();
    if (!trimmed) {
        return null;
    }
    let normalized = trimmed;
    if (normalized.startsWith("refs/heads/")) {
        normalized = normalized.slice("refs/heads/".length);
    }
    else if (normalized.startsWith("refs/remotes/")) {
        normalized = normalized.slice("refs/remotes/".length);
    }
    if (normalized.startsWith("origin/")) {
        normalized = normalized.slice("origin/".length);
    }
    if (!normalized || normalized === "HEAD" || normalized === "origin") {
        return null;
    }
    return normalized;
}
async function listGitRefs(cwd, refPrefix) {
    const { stdout } = await execGit(`git for-each-ref --sort=-committerdate --format="%(refname)%09%(committerdate:unix)" ${refPrefix}`, { cwd, env: READ_ONLY_GIT_ENV });
    return stdout
        .split("\n")
        .map((line) => {
        const trimmed = line.trim();
        if (!trimmed)
            return null;
        const [name, dateStr] = trimmed.split("\t");
        if (!name)
            return null;
        return { name, committerDate: Number(dateStr) || 0 };
    })
        .filter((ref) => ref !== null);
}
function sortBranchSuggestions(branchNames, branchMeta, query) {
    const normalizedQuery = query.trim().toLowerCase();
    const hasQuery = normalizedQuery.length > 0;
    return branchNames.sort((a, b) => {
        if (hasQuery) {
            const aPrefix = a.toLowerCase().startsWith(normalizedQuery);
            const bPrefix = b.toLowerCase().startsWith(normalizedQuery);
            if (aPrefix !== bPrefix) {
                return aPrefix ? -1 : 1;
            }
        }
        const aMeta = branchMeta.get(a);
        const bMeta = branchMeta.get(b);
        const aDate = aMeta?.committerDate ?? 0;
        const bDate = bMeta?.committerDate ?? 0;
        if (aDate !== bDate) {
            return bDate - aDate;
        }
        return a.localeCompare(b);
    });
}
export async function listBranchSuggestions(cwd, options) {
    await requireGitRepo(cwd);
    const requestedLimit = options?.limit ?? 50;
    const limit = Math.max(1, Math.min(200, requestedLimit));
    const query = options?.query?.trim().toLowerCase() ?? "";
    const [localRefs, remoteRefs] = await Promise.all([
        listGitRefs(cwd, "refs/heads"),
        listGitRefs(cwd, "refs/remotes/origin"),
    ]);
    const branchMeta = new Map();
    for (const ref of localRefs) {
        const normalized = normalizeBranchSuggestionName(ref.name);
        if (!normalized)
            continue;
        const existing = branchMeta.get(normalized);
        branchMeta.set(normalized, {
            isLocal: true,
            committerDate: Math.max(ref.committerDate, existing?.committerDate ?? 0),
        });
    }
    for (const ref of remoteRefs) {
        const normalized = normalizeBranchSuggestionName(ref.name);
        if (!normalized)
            continue;
        const existing = branchMeta.get(normalized);
        if (!existing) {
            branchMeta.set(normalized, { isLocal: false, committerDate: ref.committerDate });
        }
        else {
            branchMeta.set(normalized, {
                ...existing,
                committerDate: Math.max(ref.committerDate, existing.committerDate),
            });
        }
    }
    const filteredNames = Array.from(branchMeta.keys()).filter((name) => query ? name.toLowerCase().includes(query) : true);
    if (filteredNames.length === 0) {
        return [];
    }
    const ordered = sortBranchSuggestions(filteredNames, branchMeta, query);
    return ordered.slice(0, limit);
}
async function listCheckoutFileChanges(cwd, ref, ignoreWhitespace = false) {
    const changes = [];
    const { stdout: nameStatusOut } = await execFileAsync("git", buildGitDiffArgs({
        ignoreWhitespace,
        extra: ["--name-status", ref],
    }), {
        cwd,
        env: READ_ONLY_GIT_ENV,
        maxBuffer: SMALL_OUTPUT_MAX_BUFFER,
    });
    for (const line of nameStatusOut
        .split("\n")
        .map((l) => l.trim())
        .filter(Boolean)) {
        // `--name-status` uses TAB separators, which preserves filenames with spaces.
        const tabParts = line.split("\t");
        const rawStatus = (tabParts[0] ?? "").trim();
        if (!rawStatus)
            continue;
        if (rawStatus.startsWith("R") || rawStatus.startsWith("C")) {
            const oldPath = tabParts[1];
            const newPath = tabParts[2];
            if (newPath) {
                changes.push({
                    path: newPath,
                    ...(oldPath ? { oldPath } : {}),
                    status: rawStatus,
                    isNew: false,
                    isDeleted: false,
                });
            }
            continue;
        }
        const path = tabParts[1];
        if (!path)
            continue;
        const code = rawStatus[0];
        changes.push({
            path,
            status: rawStatus,
            isNew: code === "A",
            isDeleted: code === "D",
        });
    }
    const { stdout: untrackedOut } = await execGit("git ls-files --others --exclude-standard", {
        cwd,
        env: READ_ONLY_GIT_ENV,
    });
    for (const file of untrackedOut
        .split("\n")
        .map((l) => l.trim())
        .filter(Boolean)) {
        changes.push({
            path: file,
            status: "U",
            isNew: true,
            isDeleted: false,
            isUntracked: true,
        });
    }
    // Deduplicate by path (prefer tracked status over untracked marker if both appear).
    const byPath = new Map();
    for (const change of changes) {
        const existing = byPath.get(change.path);
        if (!existing) {
            byPath.set(change.path, change);
            continue;
        }
        if (existing.isUntracked && !change.isUntracked) {
            byPath.set(change.path, change);
        }
    }
    return Array.from(byPath.values());
}
async function readGitFileContentAtRef(cwd, ref, path) {
    try {
        const { stdout } = await execFileAsync("git", ["show", `${ref}:${path}`], {
            cwd,
            env: READ_ONLY_GIT_ENV,
            maxBuffer: SMALL_OUTPUT_MAX_BUFFER,
        });
        return stdout;
    }
    catch {
        return null;
    }
}
async function tryResolveMergeBase(cwd, baseRef) {
    try {
        const { stdout } = await execGit(`git merge-base ${baseRef} HEAD`, {
            cwd,
            env: READ_ONLY_GIT_ENV,
        });
        const sha = stdout.trim();
        return sha.length > 0 ? sha : null;
    }
    catch {
        return null;
    }
}
function normalizeNumstatPath(pathField) {
    const braceRenameMatch = pathField.match(/^(.*)\{(.*) => (.*)\}(.*)$/);
    if (braceRenameMatch) {
        const [, prefix, , renamed, suffix] = braceRenameMatch;
        return `${prefix}${renamed}${suffix}`;
    }
    const inlineRenameMatch = pathField.match(/^(.*) => (.*)$/);
    if (inlineRenameMatch) {
        return inlineRenameMatch[2] ?? pathField;
    }
    return pathField;
}
function buildGitDiffArgs(args) {
    return ["diff", ...(args.ignoreWhitespace ? ["-w"] : []), ...args.extra];
}
const TRACKED_DIFF_NUMSTAT_MAX_BYTES = 2 * 1024 * 1024; // 2MB
const TRACKED_MAX_CHANGED_LINES = 40000;
async function getTrackedNumstatByPath(cwd, ref, ignoreWhitespace = false) {
    const result = await spawnLimitedText({
        cmd: "git",
        args: buildGitDiffArgs({
            ignoreWhitespace,
            extra: ["--numstat", ref],
        }),
        cwd,
        env: READ_ONLY_GIT_ENV,
        maxBytes: TRACKED_DIFF_NUMSTAT_MAX_BYTES,
        acceptExitCodes: [0],
    });
    const stats = new Map();
    const lines = result.text
        .split("\n")
        .map((line) => line.trim())
        .filter(Boolean);
    for (const line of lines) {
        const parts = line.split("\t");
        if (parts.length < 3) {
            continue;
        }
        const additionsField = parts[0] ?? "";
        const deletionsField = parts[1] ?? "";
        const rawPath = parts.slice(2).join("\t");
        const path = normalizeNumstatPath(rawPath);
        if (!path) {
            continue;
        }
        if (additionsField === "-" || deletionsField === "-") {
            stats.set(path, { additions: 0, deletions: 0, isBinary: true });
            continue;
        }
        const additions = Number.parseInt(additionsField, 10);
        const deletions = Number.parseInt(deletionsField, 10);
        if (Number.isNaN(additions) || Number.isNaN(deletions)) {
            stats.set(path, null);
            continue;
        }
        stats.set(path, { additions, deletions, isBinary: false });
    }
    return stats;
}
function isTrackedDiffTooLarge(stat) {
    if (!stat || stat.isBinary) {
        return false;
    }
    return stat.additions + stat.deletions > TRACKED_MAX_CHANGED_LINES;
}
export class NotGitRepoError extends Error {
    constructor(cwd) {
        super(`Not a git repository: ${cwd}`);
        this.code = "NOT_GIT_REPO";
        this.name = "NotGitRepoError";
        this.cwd = cwd;
    }
}
export class MergeConflictError extends Error {
    constructor(options) {
        super(`Merge conflict while merging ${options.currentBranch} into ${options.baseRef}`);
        this.name = "MergeConflictError";
        this.baseRef = options.baseRef;
        this.currentBranch = options.currentBranch;
        this.conflictFiles = options.conflictFiles;
    }
}
export class MergeFromBaseConflictError extends Error {
    constructor(options) {
        super(`Merge conflict while merging ${options.baseRef} into ${options.currentBranch}. Please merge manually.`);
        this.name = "MergeFromBaseConflictError";
        this.baseRef = options.baseRef;
        this.currentBranch = options.currentBranch;
        this.conflictFiles = options.conflictFiles;
    }
}
function isGitError(error) {
    if (!(error instanceof Error)) {
        return false;
    }
    return /not a git repository/i.test(error.message) || /git repository/i.test(error.message);
}
async function requireGitRepo(cwd) {
    try {
        await execAsync("git rev-parse --git-dir", { cwd, env: READ_ONLY_GIT_ENV });
    }
    catch (error) {
        throw new NotGitRepoError(cwd);
    }
}
export async function getCurrentBranch(cwd) {
    const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
        cwd,
        env: READ_ONLY_GIT_ENV,
    });
    const branch = stdout.trim();
    return branch.length > 0 ? branch : null;
}
async function getWorktreeRoot(cwd) {
    try {
        const { stdout } = await execAsync("git rev-parse --path-format=absolute --show-toplevel", {
            cwd,
            env: READ_ONLY_GIT_ENV,
        });
        const root = stdout.trim();
        return root.length > 0 ? root : null;
    }
    catch {
        return null;
    }
}
export async function getMainRepoRoot(cwd) {
    const { stdout: commonDirOut } = await execAsync("git rev-parse --path-format=absolute --git-common-dir", { cwd, env: READ_ONLY_GIT_ENV });
    const commonDir = commonDirOut.trim();
    const normalized = realpathSync(commonDir);
    if (basename(normalized) === ".git") {
        return dirname(normalized);
    }
    const { stdout: worktreeOut } = await execAsync("git worktree list --porcelain", {
        cwd,
        env: READ_ONLY_GIT_ENV,
    });
    const worktrees = parseWorktreeList(worktreeOut);
    const nonBareNonSeawork = worktrees.filter((wt) => !wt.isBare && !isSeaworkWorktreePath(wt.path));
    const childrenOfBareRepo = nonBareNonSeawork.filter((wt) => isDescendantPath(wt.path, normalized));
    const mainChild = childrenOfBareRepo.find((wt) => basename(wt.path) === "main");
    return mainChild?.path ?? childrenOfBareRepo[0]?.path ?? nonBareNonSeawork[0]?.path ?? normalized;
}
/** Check whether a path contains a `.seawork/worktrees/` segment (both `/` and `\`). */
export function isSeaworkWorktreePath(p) {
    return /[/\\]\.seawork[/\\]worktrees[/\\]/.test(p);
}
/** True when `child` is strictly inside `parent` (handles both `/` and `\`). */
export function isDescendantPath(child, parent) {
    let c = child.replace(/\\/g, "/").replace(/\/+$/, "");
    let p = parent.replace(/\\/g, "/").replace(/\/+$/, "");
    // Case-insensitive on Windows (drive letter like C: or D:)
    if (/^[A-Za-z]:/.test(c) || /^[A-Za-z]:/.test(p)) {
        c = c.toLowerCase();
        p = p.toLowerCase();
    }
    if (!c.startsWith(p))
        return false;
    if (c.length === p.length)
        return false;
    return c[p.length] === "/";
}
export function parseWorktreeList(output) {
    const entries = [];
    let current = null;
    for (const line of output.split("\n")) {
        const trimmed = line.trim();
        if (!trimmed) {
            continue;
        }
        if (trimmed.startsWith("worktree ")) {
            if (current) {
                entries.push(current);
            }
            current = { path: trimmed.slice("worktree ".length).trim() };
            continue;
        }
        if (current && trimmed.startsWith("branch ")) {
            current.branchRef = trimmed.slice("branch ".length).trim();
        }
        if (current && trimmed === "bare") {
            current.isBare = true;
        }
    }
    if (current) {
        entries.push(current);
    }
    return entries;
}
async function getWorktreePathForBranch(cwd, branchName) {
    try {
        const { stdout } = await execAsync("git worktree list --porcelain", {
            cwd,
            env: READ_ONLY_GIT_ENV,
        });
        const entries = parseWorktreeList(stdout);
        const ref = branchName.startsWith("refs/heads/") ? branchName : `refs/heads/${branchName}`;
        return entries.find((entry) => entry.branchRef === ref)?.path ?? null;
    }
    catch {
        return null;
    }
}
export async function renameCurrentBranch(cwd, newName) {
    await requireGitRepo(cwd);
    const previousBranch = await getCurrentBranch(cwd);
    if (!previousBranch || previousBranch === "HEAD") {
        throw new Error("Cannot rename branch in detached HEAD state");
    }
    await execAsync(`git branch -m "${newName}"`, {
        cwd,
    });
    const currentBranch = await getCurrentBranch(cwd);
    return { previousBranch, currentBranch };
}
async function getConfiguredBaseRefForCwd(cwd, context) {
    // Fast-path reject: non-worktree paths do not need expensive ownership checks.
    if (!/[\\/]worktrees[\\/]/.test(cwd)) {
        return { baseRef: null, isSeaworkOwnedWorktree: false };
    }
    const ownership = await isSeaworkOwnedWorktreeCwd(cwd, { seaworkHome: context?.seaworkHome });
    if (!ownership.allowed) {
        return { baseRef: null, isSeaworkOwnedWorktree: false };
    }
    const worktreeRoot = (await getWorktreeRoot(cwd)) ?? cwd;
    return {
        baseRef: requireSeaworkWorktreeBaseRefName(worktreeRoot),
        isSeaworkOwnedWorktree: true,
    };
}
async function isWorkingTreeDirty(cwd) {
    const { stdout } = await execAsync("git status --porcelain", {
        cwd,
        env: READ_ONLY_GIT_ENV,
    });
    return stdout.trim().length > 0;
}
export async function getOriginRemoteUrl(cwd) {
    try {
        const { stdout } = await execAsync("git config --get remote.origin.url", {
            cwd,
            env: READ_ONLY_GIT_ENV,
        });
        const url = stdout.trim();
        return url.length > 0 ? url : null;
    }
    catch {
        return null;
    }
}
export async function hasOriginRemote(cwd) {
    const url = await getOriginRemoteUrl(cwd);
    return url !== null;
}
export async function resolveAbsoluteGitDir(cwd) {
    try {
        const { stdout } = await execAsync("git rev-parse --absolute-git-dir", {
            cwd,
            env: READ_ONLY_GIT_ENV,
        });
        const gitDir = stdout.trim();
        return gitDir.length > 0 ? gitDir : null;
    }
    catch {
        return null;
    }
}
async function abortGitPullConflictState(cwd) {
    const gitDir = await resolveAbsoluteGitDir(cwd);
    if (!gitDir) {
        return;
    }
    const mergeHeadPath = resolve(gitDir, "MERGE_HEAD");
    const rebaseMergePath = resolve(gitDir, "rebase-merge");
    const rebaseApplyPath = resolve(gitDir, "rebase-apply");
    if (existsSync(mergeHeadPath)) {
        try {
            await execAsync("git merge --abort", { cwd });
        }
        catch {
            // ignore
        }
    }
    if (existsSync(rebaseMergePath) || existsSync(rebaseApplyPath)) {
        try {
            await execAsync("git rebase --abort", { cwd });
        }
        catch {
            // ignore
        }
    }
}
export async function resolveRepositoryDefaultBranch(repoRoot) {
    try {
        const { stdout } = await execAsync("git symbolic-ref --quiet refs/remotes/origin/HEAD", {
            cwd: repoRoot,
            env: READ_ONLY_GIT_ENV,
        });
        const ref = stdout.trim();
        if (ref) {
            // Prefer a local branch name (e.g. "main") over the remote-tracking ref (e.g. "origin/main")
            // so that status/diff/merge all operate against the same base ref.
            const remoteShort = ref.replace(/^refs\/remotes\//, "");
            const localName = remoteShort.startsWith("origin/")
                ? remoteShort.slice("origin/".length)
                : remoteShort;
            try {
                await execAsync(`git show-ref --verify --quiet refs/heads/${localName}`, {
                    cwd: repoRoot,
                    env: READ_ONLY_GIT_ENV,
                });
                return localName;
            }
            catch {
                return remoteShort;
            }
        }
    }
    catch {
        // ignore
    }
    const { stdout } = await execAsync("git branch --format='%(refname:short)'", {
        cwd: repoRoot,
        env: READ_ONLY_GIT_ENV,
    });
    const branches = stdout
        .split("\n")
        .map((line) => line.trim())
        .filter((line) => line.length > 0);
    if (branches.includes("main")) {
        return "main";
    }
    if (branches.includes("master")) {
        return "master";
    }
    return null;
}
async function resolveBaseRef(repoRoot) {
    return resolveRepositoryDefaultBranch(repoRoot);
}
function normalizeLocalBranchRefName(input) {
    return input.startsWith("origin/") ? input.slice("origin/".length) : input;
}
async function doesGitRefExist(cwd, fullRef) {
    try {
        await execAsync(`git show-ref --verify --quiet ${fullRef}`, {
            cwd,
            env: READ_ONLY_GIT_ENV,
        });
        return true;
    }
    catch {
        return false;
    }
}
async function resolveBestComparisonBaseRef(cwd, normalizedBaseRef) {
    const [hasLocal, hasOrigin] = await Promise.all([
        doesGitRefExist(cwd, `refs/heads/${normalizedBaseRef}`),
        doesGitRefExist(cwd, `refs/remotes/origin/${normalizedBaseRef}`),
    ]);
    if (hasLocal && !hasOrigin) {
        return normalizedBaseRef;
    }
    if (!hasLocal && hasOrigin) {
        return `origin/${normalizedBaseRef}`;
    }
    if (!hasLocal && !hasOrigin) {
        throw new Error(`Base branch not found locally or on origin: ${normalizedBaseRef}`);
    }
    // Both exist: choose the ref with more unique commits compared to the other.
    try {
        const { stdout } = await execAsync(`git rev-list --left-right --count ${normalizedBaseRef}...origin/${normalizedBaseRef}`, { cwd, env: READ_ONLY_GIT_ENV });
        const [localOnlyRaw, originOnlyRaw] = stdout.trim().split(/\s+/);
        const localOnly = Number.parseInt(localOnlyRaw ?? "0", 10);
        const originOnly = Number.parseInt(originOnlyRaw ?? "0", 10);
        if (!Number.isNaN(localOnly) && !Number.isNaN(originOnly) && originOnly > localOnly) {
            return `origin/${normalizedBaseRef}`;
        }
    }
    catch {
        // ignore and fall back to local
    }
    return normalizedBaseRef;
}
async function getAheadBehind(cwd, baseRef, currentBranch) {
    const normalizedBaseRef = normalizeLocalBranchRefName(baseRef);
    if (!normalizedBaseRef || !currentBranch || normalizedBaseRef === currentBranch) {
        return null;
    }
    const comparisonBaseRef = await resolveBestComparisonBaseRef(cwd, normalizedBaseRef);
    const { stdout } = await execAsync(`git rev-list --left-right --count ${comparisonBaseRef}...${currentBranch}`, { cwd, env: READ_ONLY_GIT_ENV });
    const [behindRaw, aheadRaw] = stdout.trim().split(/\s+/);
    const behind = Number.parseInt(behindRaw ?? "0", 10);
    const ahead = Number.parseInt(aheadRaw ?? "0", 10);
    if (Number.isNaN(behind) || Number.isNaN(ahead)) {
        return null;
    }
    return { ahead, behind };
}
async function getAheadOfOrigin(cwd, currentBranch) {
    if (!currentBranch) {
        return null;
    }
    try {
        const { stdout } = await execAsync(`git rev-list --count origin/${currentBranch}..${currentBranch}`, { cwd, env: READ_ONLY_GIT_ENV });
        const count = Number.parseInt(stdout.trim(), 10);
        return Number.isNaN(count) ? null : count;
    }
    catch {
        try {
            const { stdout } = await execAsync(`git rev-list --count ${currentBranch}`, {
                cwd,
                env: READ_ONLY_GIT_ENV,
            });
            const count = Number.parseInt(stdout.trim(), 10);
            return Number.isNaN(count) ? null : count;
        }
        catch {
            return null;
        }
    }
}
async function getBehindOfOrigin(cwd, currentBranch) {
    if (!currentBranch) {
        return null;
    }
    try {
        const { stdout } = await execAsync(`git rev-list --count ${currentBranch}..origin/${currentBranch}`, { cwd, env: READ_ONLY_GIT_ENV });
        const count = Number.parseInt(stdout.trim(), 10);
        return Number.isNaN(count) ? null : count;
    }
    catch {
        return null;
    }
}
async function inspectCheckoutContext(cwd, context) {
    try {
        const root = await getWorktreeRoot(cwd);
        if (!root) {
            return null;
        }
        const [currentBranch, remoteUrl, configured] = await Promise.all([
            getCurrentBranch(cwd),
            getOriginRemoteUrl(cwd),
            getConfiguredBaseRefForCwd(cwd, context),
        ]);
        return {
            worktreeRoot: root,
            currentBranch,
            remoteUrl,
            configured,
        };
    }
    catch (error) {
        if (isGitError(error)) {
            return null;
        }
        throw error;
    }
}
const PER_FILE_DIFF_MAX_BYTES = 1024 * 1024; // 1MB
const TOTAL_DIFF_MAX_BYTES = 2 * 1024 * 1024; // 2MB
const UNTRACKED_BINARY_SNIFF_BYTES = 16 * 1024;
async function isLikelyBinaryFile(absolutePath) {
    const handle = await openFile(absolutePath, "r");
    try {
        const buffer = Buffer.allocUnsafe(UNTRACKED_BINARY_SNIFF_BYTES);
        const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
        if (bytesRead === 0) {
            return false;
        }
        let suspicious = 0;
        for (let i = 0; i < bytesRead; i += 1) {
            const byte = buffer[i];
            if (byte === 0) {
                return true;
            }
            // Treat control bytes as suspicious while allowing common whitespace.
            if (byte < 7 || (byte > 14 && byte < 32) || byte === 127) {
                suspicious += 1;
            }
        }
        return suspicious / bytesRead > 0.3;
    }
    finally {
        await handle.close();
    }
}
async function inspectUntrackedFile(cwd, relativePath) {
    const absolutePath = resolve(cwd, relativePath);
    const metadata = await statFile(absolutePath);
    if (!metadata.isFile()) {
        return { stat: null, truncated: false };
    }
    if (await isLikelyBinaryFile(absolutePath)) {
        return {
            stat: { additions: 0, deletions: 0, isBinary: true },
            truncated: false,
        };
    }
    if (metadata.size > PER_FILE_DIFF_MAX_BYTES) {
        return {
            stat: { additions: 0, deletions: 0, isBinary: false },
            truncated: true,
        };
    }
    return {
        stat: { additions: 0, deletions: 0, isBinary: false },
        truncated: false,
    };
}
function buildPlaceholderParsedDiffFile(change, options) {
    return {
        path: change.path,
        isNew: change.isNew,
        isDeleted: change.isDeleted,
        additions: options.stat?.additions ?? 0,
        deletions: options.stat?.deletions ?? 0,
        hunks: [],
        status: options.status,
    };
}
async function getUntrackedDiffText(cwd, change, ignoreWhitespace = false) {
    try {
        const inspected = await inspectUntrackedFile(cwd, change.path);
        if (inspected.stat?.isBinary || inspected.truncated) {
            return { text: "", truncated: inspected.truncated, stat: inspected.stat };
        }
    }
    catch {
        // Fall through to git diff path if metadata probing fails.
    }
    const result = await spawnLimitedText({
        cmd: "git",
        args: buildGitDiffArgs({
            ignoreWhitespace,
            extra: ["--no-index", "/dev/null", "--", change.path],
        }),
        cwd,
        env: READ_ONLY_GIT_ENV,
        maxBytes: PER_FILE_DIFF_MAX_BYTES,
        acceptExitCodes: [0, 1],
    });
    return {
        text: result.text,
        truncated: result.truncated,
        stat: { additions: 0, deletions: 0, isBinary: false },
    };
}
export async function getCheckoutStatus(cwd, context) {
    const inspected = await inspectCheckoutContext(cwd, context);
    if (!inspected) {
        return { isGit: false };
    }
    const worktreeRoot = inspected.worktreeRoot;
    const currentBranch = inspected.currentBranch;
    const remoteUrl = inspected.remoteUrl;
    const configured = inspected.configured;
    const isDirty = await isWorkingTreeDirty(cwd);
    const hasRemote = remoteUrl !== null;
    const baseRef = configured.baseRef ?? (await resolveBaseRef(cwd));
    const [aheadBehind, aheadOfOrigin, behindOfOrigin] = await Promise.all([
        baseRef && currentBranch ? getAheadBehind(cwd, baseRef, currentBranch) : Promise.resolve(null),
        hasRemote && currentBranch ? getAheadOfOrigin(cwd, currentBranch) : Promise.resolve(null),
        hasRemote && currentBranch ? getBehindOfOrigin(cwd, currentBranch) : Promise.resolve(null),
    ]);
    if (configured.isSeaworkOwnedWorktree) {
        const mainRepoRoot = await getMainRepoRoot(cwd);
        return {
            isGit: true,
            repoRoot: worktreeRoot,
            mainRepoRoot,
            currentBranch,
            isDirty,
            baseRef: configured.baseRef,
            aheadBehind,
            aheadOfOrigin,
            behindOfOrigin,
            hasRemote,
            remoteUrl,
            isSeaworkOwnedWorktree: true,
        };
    }
    return {
        isGit: true,
        repoRoot: worktreeRoot,
        currentBranch,
        isDirty,
        baseRef,
        aheadBehind,
        aheadOfOrigin,
        behindOfOrigin,
        hasRemote,
        remoteUrl,
        isSeaworkOwnedWorktree: false,
    };
}
export async function getCheckoutStatusLite(cwd, context) {
    const inspected = await inspectCheckoutContext(cwd, context);
    if (!inspected) {
        return {
            isGit: false,
            currentBranch: null,
            remoteUrl: null,
            worktreeRoot: null,
            isSeaworkOwnedWorktree: false,
            mainRepoRoot: null,
        };
    }
    if (inspected.configured.isSeaworkOwnedWorktree) {
        return {
            isGit: true,
            currentBranch: inspected.currentBranch,
            remoteUrl: inspected.remoteUrl,
            worktreeRoot: inspected.worktreeRoot,
            isSeaworkOwnedWorktree: true,
            mainRepoRoot: await getMainRepoRoot(cwd),
        };
    }
    return {
        isGit: true,
        currentBranch: inspected.currentBranch,
        remoteUrl: inspected.remoteUrl,
        worktreeRoot: inspected.worktreeRoot,
        isSeaworkOwnedWorktree: false,
        mainRepoRoot: null,
    };
}
export async function getCheckoutShortstat(cwd, context) {
    try {
        await requireGitRepo(cwd);
    }
    catch {
        return null;
    }
    const configured = await getConfiguredBaseRefForCwd(cwd, context);
    const localBaseRef = configured.baseRef ?? (await resolveBaseRef(cwd));
    const currentBranch = await getCurrentBranch(cwd);
    let diffTarget;
    if (currentBranch && localBaseRef && currentBranch !== localBaseRef) {
        // Feature branch: diff against the merge-base with the base branch
        const comparisonBaseRef = await resolveBestComparisonBaseRef(cwd, normalizeLocalBranchRefName(localBaseRef));
        try {
            const { stdout } = await execAsync(`git merge-base HEAD ${comparisonBaseRef}`, {
                cwd,
                env: READ_ONLY_GIT_ENV,
            });
            const mergeBase = stdout.trim();
            if (!mergeBase) {
                return null;
            }
            diffTarget = mergeBase;
        }
        catch {
            return null;
        }
    }
    else if (currentBranch) {
        // On the base branch (or no base ref configured): diff against remote tracking branch
        const hasOrigin = await doesGitRefExist(cwd, `refs/remotes/origin/${currentBranch}`);
        if (!hasOrigin) {
            return null;
        }
        diffTarget = `origin/${currentBranch}`;
    }
    else {
        return null;
    }
    try {
        // Omit HEAD so the diff includes uncommitted (staged + unstaged) changes
        const { stdout } = await execAsync(`git diff --shortstat ${diffTarget}`, {
            cwd,
            env: READ_ONLY_GIT_ENV,
        });
        const text = stdout.trim();
        if (!text) {
            return null;
        }
        let additions = 0;
        let deletions = 0;
        const addMatch = text.match(/(\d+)\s+insertion/);
        if (addMatch) {
            additions = Number.parseInt(addMatch[1], 10);
        }
        const delMatch = text.match(/(\d+)\s+deletion/);
        if (delMatch) {
            deletions = Number.parseInt(delMatch[1], 10);
        }
        if (additions === 0 && deletions === 0) {
            return null;
        }
        return { additions, deletions };
    }
    catch {
        return null;
    }
}
export async function getCheckoutDiff(cwd, compare, context) {
    await requireGitRepo(cwd);
    let refForDiff;
    if (compare.mode === "uncommitted") {
        refForDiff = "HEAD";
    }
    else {
        const configured = await getConfiguredBaseRefForCwd(cwd, context);
        const baseRef = configured.baseRef ?? compare.baseRef ?? (await resolveBaseRef(cwd));
        if (!baseRef) {
            return { diff: "" };
        }
        if (configured.isSeaworkOwnedWorktree && compare.baseRef && compare.baseRef !== baseRef) {
            throw new Error(`Base ref mismatch: expected ${baseRef}, got ${compare.baseRef}`);
        }
        const normalizedBaseRef = normalizeLocalBranchRefName(baseRef);
        const bestBaseRef = await resolveBestComparisonBaseRef(cwd, normalizedBaseRef);
        refForDiff = (await tryResolveMergeBase(cwd, bestBaseRef)) ?? bestBaseRef;
    }
    const ignoreWhitespace = compare.ignoreWhitespace === true;
    const changes = await listCheckoutFileChanges(cwd, refForDiff, ignoreWhitespace);
    changes.sort((a, b) => {
        if (a.path === b.path)
            return 0;
        return a.path < b.path ? -1 : 1;
    });
    const structured = [];
    let diffText = "";
    let diffBytes = 0;
    const appendDiff = (text) => {
        if (!text)
            return;
        if (diffBytes >= TOTAL_DIFF_MAX_BYTES)
            return;
        const buf = Buffer.from(text, "utf8");
        if (diffBytes + buf.length <= TOTAL_DIFF_MAX_BYTES) {
            diffText += text;
            diffBytes += buf.length;
            return;
        }
        const remaining = TOTAL_DIFF_MAX_BYTES - diffBytes;
        if (remaining > 0) {
            diffText += buf.subarray(0, remaining).toString("utf8");
            diffBytes = TOTAL_DIFF_MAX_BYTES;
        }
    };
    const trackedChanges = changes.filter((change) => !change.isUntracked);
    const untrackedChanges = changes.filter((change) => change.isUntracked === true);
    const trackedChangeByPath = new Map(trackedChanges.map((change) => [change.path, change]));
    const trackedNumstatByPath = trackedChanges.length > 0
        ? await getTrackedNumstatByPath(cwd, refForDiff, ignoreWhitespace)
        : new Map();
    const trackedDiffPaths = [];
    const trackedPlaceholderByPath = new Map();
    for (const change of trackedChanges) {
        const stat = trackedNumstatByPath.get(change.path) ?? null;
        if (stat?.isBinary) {
            trackedPlaceholderByPath.set(change.path, { status: "binary", stat });
            continue;
        }
        if (isTrackedDiffTooLarge(stat)) {
            trackedPlaceholderByPath.set(change.path, { status: "too_large", stat });
            continue;
        }
        trackedDiffPaths.push(change.path);
    }
    let trackedDiffText = "";
    let trackedDiffTruncated = false;
    if (trackedDiffPaths.length > 0) {
        const trackedDiffResult = await spawnLimitedText({
            cmd: "git",
            args: buildGitDiffArgs({
                ignoreWhitespace,
                extra: [refForDiff, "--", ...trackedDiffPaths],
            }),
            cwd,
            env: READ_ONLY_GIT_ENV,
            maxBytes: TOTAL_DIFF_MAX_BYTES,
        });
        trackedDiffText = trackedDiffResult.text;
        trackedDiffTruncated = trackedDiffResult.truncated;
        appendDiff(trackedDiffText);
        if (trackedDiffTruncated) {
            appendDiff("# tracked diff truncated\n");
        }
    }
    const appendTrackedPlaceholderComment = (change, status) => {
        if (status === "binary") {
            appendDiff(`# ${change.path}: binary diff omitted\n`);
            return;
        }
        appendDiff(`# ${change.path}: diff too large omitted\n`);
    };
    if (compare.includeStructured) {
        const parsedTrackedFiles = trackedDiffText.length > 0
            ? await parseAndHighlightDiff(trackedDiffText, cwd, {
                getOldFileContent: async (file) => {
                    const change = trackedChangeByPath.get(file.path);
                    if (!change || change.isNew) {
                        return null;
                    }
                    const refPath = change.oldPath ?? change.path;
                    return readGitFileContentAtRef(cwd, refForDiff, refPath);
                },
            })
            : [];
        const parsedTrackedByPath = new Map(parsedTrackedFiles.map((file) => [file.path, file]));
        for (const change of trackedChanges) {
            const placeholder = trackedPlaceholderByPath.get(change.path);
            if (placeholder) {
                structured.push(buildPlaceholderParsedDiffFile(change, {
                    status: placeholder.status,
                    stat: placeholder.stat,
                }));
                appendTrackedPlaceholderComment(change, placeholder.status);
                continue;
            }
            const stat = trackedNumstatByPath.get(change.path) ?? null;
            const parsedFile = parsedTrackedByPath.get(change.path);
            if (parsedFile) {
                structured.push({
                    ...parsedFile,
                    path: change.path,
                    isNew: change.isNew,
                    isDeleted: change.isDeleted,
                    status: "ok",
                });
                continue;
            }
            // `git diff -w --name-status` can still report a modified path even when the
            // whitespace-filtered patch and numstat are both empty. Skip emitting a
            // structured placeholder in that case so whitespace-only edits truly disappear.
            if (ignoreWhitespace && !trackedDiffTruncated && stat === null) {
                continue;
            }
            structured.push({
                path: change.path,
                isNew: change.isNew,
                isDeleted: change.isDeleted,
                additions: stat?.additions ?? 0,
                deletions: stat?.deletions ?? 0,
                hunks: [],
                status: trackedDiffTruncated ? "too_large" : "ok",
            });
        }
    }
    else {
        for (const change of trackedChanges) {
            const placeholder = trackedPlaceholderByPath.get(change.path);
            if (placeholder) {
                appendTrackedPlaceholderComment(change, placeholder.status);
            }
        }
    }
    for (const change of untrackedChanges) {
        if (diffBytes >= TOTAL_DIFF_MAX_BYTES) {
            break;
        }
        const { text, truncated, stat } = await getUntrackedDiffText(cwd, change, ignoreWhitespace);
        if (!compare.includeStructured) {
            if (stat?.isBinary) {
                appendDiff(`# ${change.path}: binary diff omitted\n`);
            }
            else if (truncated) {
                appendDiff(`# ${change.path}: diff too large omitted\n`);
            }
            else {
                appendDiff(text);
            }
            continue;
        }
        if (stat?.isBinary) {
            structured.push(buildPlaceholderParsedDiffFile(change, { status: "binary", stat }));
            appendDiff(`# ${change.path}: binary diff omitted\n`);
            continue;
        }
        if (truncated) {
            structured.push(buildPlaceholderParsedDiffFile(change, { status: "too_large", stat }));
            appendDiff(`# ${change.path}: diff too large omitted\n`);
            continue;
        }
        appendDiff(text);
        const parsed = await parseAndHighlightDiff(text, cwd);
        const parsedFile = parsed[0] ??
            {
                path: change.path,
                isNew: change.isNew,
                isDeleted: change.isDeleted,
                additions: stat?.additions ?? 0,
                deletions: stat?.deletions ?? 0,
                hunks: [],
            };
        structured.push({
            ...parsedFile,
            path: change.path,
            isNew: change.isNew,
            isDeleted: change.isDeleted,
            status: "ok",
        });
    }
    if (compare.includeStructured) {
        return { diff: diffText, structured };
    }
    return { diff: diffText };
}
export async function commitChanges(cwd, options) {
    await requireGitRepo(cwd);
    if (options.addAll ?? true) {
        await execFileAsync("git", ["add", "-A"], { cwd });
    }
    await execFileAsync("git", ["-c", "commit.gpgsign=false", "commit", "-m", options.message], {
        cwd,
    });
}
export async function commitAll(cwd, message) {
    await commitChanges(cwd, { message, addAll: true });
}
export async function mergeToBase(cwd, options = {}, context) {
    await requireGitRepo(cwd);
    const currentBranch = await getCurrentBranch(cwd);
    const configured = await getConfiguredBaseRefForCwd(cwd, context);
    const baseRef = configured.baseRef ?? options.baseRef ?? (await resolveBaseRef(cwd));
    if (!baseRef) {
        throw new Error("Unable to determine base branch for merge");
    }
    if (configured.isSeaworkOwnedWorktree && options.baseRef && options.baseRef !== baseRef) {
        throw new Error(`Base ref mismatch: expected ${baseRef}, got ${options.baseRef}`);
    }
    if (!currentBranch) {
        throw new Error("Unable to determine current branch for merge");
    }
    let normalizedBaseRef = baseRef;
    normalizedBaseRef = normalizeLocalBranchRefName(normalizedBaseRef);
    if (normalizedBaseRef === currentBranch) {
        return;
    }
    const currentWorktreeRoot = (await getWorktreeRoot(cwd)) ?? cwd;
    const baseWorktree = await getWorktreePathForBranch(cwd, normalizedBaseRef);
    const operationCwd = baseWorktree ?? currentWorktreeRoot;
    const isSameCheckout = resolve(operationCwd) === resolve(currentWorktreeRoot);
    const originalBranch = await getCurrentBranch(operationCwd);
    const mode = options.mode ?? "merge";
    try {
        await execAsync(`git checkout ${normalizedBaseRef}`, { cwd: operationCwd });
        if (mode === "squash") {
            await execAsync(`git merge --squash ${currentBranch}`, { cwd: operationCwd });
            const message = options.commitMessage ?? `Squash merge ${currentBranch} into ${normalizedBaseRef}`;
            await execFileAsync("git", ["-c", "commit.gpgsign=false", "commit", "-m", message], {
                cwd: operationCwd,
            });
        }
        else {
            await execAsync(`git merge ${currentBranch}`, { cwd: operationCwd });
        }
    }
    catch (error) {
        const errorDetails = error instanceof Error
            ? `${error.message}\n${error.stderr ?? ""}\n${error.stdout ?? ""}`
            : String(error);
        try {
            const [unmergedOutput, lsFilesOutput, statusOutput] = await Promise.all([
                execAsync("git diff --name-only --diff-filter=U", { cwd: operationCwd }),
                execAsync("git ls-files -u", { cwd: operationCwd }),
                execAsync("git status --porcelain", { cwd: operationCwd }),
            ]);
            const statusConflicts = statusOutput.stdout
                .split("\n")
                .map((line) => line.trim())
                .filter(Boolean)
                .filter((line) => /^(UU|AA|DD|AU|UA|UD|DU)\s/.test(line))
                .map((line) => line.slice(3).trim());
            const conflicts = [
                ...unmergedOutput.stdout
                    .split("\n")
                    .map((line) => line.trim())
                    .filter(Boolean),
                ...lsFilesOutput.stdout
                    .split("\n")
                    .map((line) => line.trim())
                    .filter(Boolean)
                    .map((line) => line.split("\t").pop()),
                ...statusConflicts,
            ].filter(Boolean);
            const conflictDetected = conflicts.length > 0 || /CONFLICT|Automatic merge failed/i.test(errorDetails);
            if (conflictDetected) {
                try {
                    await execAsync("git merge --abort", { cwd: operationCwd });
                }
                catch {
                    // ignore
                }
                throw new MergeConflictError({
                    baseRef: normalizedBaseRef,
                    currentBranch,
                    conflictFiles: conflicts.length > 0 ? conflicts : [],
                });
            }
        }
        catch (innerError) {
            if (innerError instanceof MergeConflictError) {
                throw innerError;
            }
            // ignore detection failures
        }
        throw error;
    }
    finally {
        if (isSameCheckout && originalBranch && originalBranch !== normalizedBaseRef) {
            try {
                await execAsync(`git checkout ${originalBranch}`, { cwd: operationCwd });
            }
            catch {
                // ignore
            }
        }
    }
}
export async function mergeFromBase(cwd, options = {}, context) {
    await requireGitRepo(cwd);
    const currentBranch = await getCurrentBranch(cwd);
    if (!currentBranch || currentBranch === "HEAD") {
        throw new Error("Unable to determine current branch for merge");
    }
    const configured = await getConfiguredBaseRefForCwd(cwd, context);
    const baseRef = configured.baseRef ?? options.baseRef ?? (await resolveBaseRef(cwd));
    if (!baseRef) {
        throw new Error("Unable to determine base branch for merge");
    }
    if (configured.isSeaworkOwnedWorktree && options.baseRef && options.baseRef !== baseRef) {
        throw new Error(`Base ref mismatch: expected ${baseRef}, got ${options.baseRef}`);
    }
    const requireCleanTarget = options.requireCleanTarget ?? true;
    if (requireCleanTarget) {
        const { stdout } = await execAsync("git status --porcelain", {
            cwd,
            env: READ_ONLY_GIT_ENV,
        });
        if (stdout.trim().length > 0) {
            throw new Error("Working directory has uncommitted changes.");
        }
    }
    const normalizedBaseRef = normalizeLocalBranchRefName(baseRef);
    const bestBaseRef = await resolveBestComparisonBaseRef(cwd, normalizedBaseRef);
    if (bestBaseRef === currentBranch) {
        return;
    }
    try {
        await execAsync(`git merge ${bestBaseRef}`, { cwd });
    }
    catch (error) {
        const errorDetails = error instanceof Error
            ? `${error.message}\n${error.stderr ?? ""}\n${error.stdout ?? ""}`
            : String(error);
        try {
            const [unmergedOutput, lsFilesOutput, statusOutput] = await Promise.all([
                execAsync("git diff --name-only --diff-filter=U", { cwd }),
                execAsync("git ls-files -u", { cwd }),
                execAsync("git status --porcelain", { cwd }),
            ]);
            const statusConflicts = statusOutput.stdout
                .split("\n")
                .map((line) => line.trim())
                .filter(Boolean)
                .filter((line) => /^(UU|AA|DD|AU|UA|UD|DU)\s/.test(line))
                .map((line) => line.slice(3).trim());
            const conflicts = [
                ...unmergedOutput.stdout
                    .split("\n")
                    .map((line) => line.trim())
                    .filter(Boolean),
                ...lsFilesOutput.stdout
                    .split("\n")
                    .map((line) => line.trim())
                    .filter(Boolean)
                    .map((line) => line.split("\t").pop()),
                ...statusConflicts,
            ].filter(Boolean);
            const conflictDetected = conflicts.length > 0 || /CONFLICT|Automatic merge failed/i.test(errorDetails);
            if (conflictDetected) {
                try {
                    await execAsync("git merge --abort", { cwd });
                }
                catch {
                    // ignore
                }
                throw new MergeFromBaseConflictError({
                    baseRef: bestBaseRef,
                    currentBranch,
                    conflictFiles: conflicts.length > 0 ? conflicts : [],
                });
            }
        }
        catch (innerError) {
            if (innerError instanceof MergeFromBaseConflictError) {
                throw innerError;
            }
            // ignore detection failures
        }
        throw error;
    }
}
export async function pullCurrentBranch(cwd) {
    await requireGitRepo(cwd);
    const currentBranch = await getCurrentBranch(cwd);
    if (!currentBranch || currentBranch === "HEAD") {
        throw new Error("Unable to determine current branch for pull");
    }
    const hasRemote = await hasOriginRemote(cwd);
    if (!hasRemote) {
        throw new Error("Remote 'origin' is not configured.");
    }
    try {
        await execAsync("git pull", { cwd });
    }
    catch (error) {
        await abortGitPullConflictState(cwd);
        throw error;
    }
}
export async function pushCurrentBranch(cwd) {
    await requireGitRepo(cwd);
    const currentBranch = await getCurrentBranch(cwd);
    if (!currentBranch || currentBranch === "HEAD") {
        throw new Error("Unable to determine current branch for push");
    }
    const hasRemote = await hasOriginRemote(cwd);
    if (!hasRemote) {
        throw new Error("Remote 'origin' is not configured.");
    }
    await execAsync(`git push -u origin ${currentBranch}`, { cwd });
}
export async function resolveGhPath() {
    if (cachedGhPath === undefined) {
        cachedGhPath = await findExecutable("gh");
    }
    if (cachedGhPath === null) {
        throw new Error("GitHub CLI (gh) is not installed or not in PATH");
    }
    return cachedGhPath;
}
function getCommandErrorText(error) {
    if (!(error instanceof Error)) {
        return String(error);
    }
    const stderr = typeof error?.stderr === "string" ? error.stderr : "";
    const stdout = typeof error?.stdout === "string" ? error.stdout : "";
    return `${error.message}\n${stderr}\n${stdout}`.toLowerCase();
}
function isGhAuthError(error) {
    const text = getCommandErrorText(error);
    return (text.includes("gh auth login") ||
        text.includes("not logged into any github hosts") ||
        text.includes("authentication failed") ||
        text.includes("authentication required") ||
        text.includes("bad credentials") ||
        text.includes("http 401"));
}
async function resolveGitHubRepo(cwd) {
    try {
        const { stdout } = await execAsync("git config --get remote.origin.url", {
            cwd,
            env: READ_ONLY_GIT_ENV,
        });
        const url = stdout.trim();
        if (!url) {
            return null;
        }
        let cleaned = url;
        if (cleaned.startsWith("git@github.com:")) {
            cleaned = cleaned.slice("git@github.com:".length);
        }
        else if (cleaned.startsWith("https://github.com/")) {
            cleaned = cleaned.slice("https://github.com/".length);
        }
        else if (cleaned.startsWith("http://github.com/")) {
            cleaned = cleaned.slice("http://github.com/".length);
        }
        else {
            const marker = "github.com/";
            const index = cleaned.indexOf(marker);
            if (index !== -1) {
                cleaned = cleaned.slice(index + marker.length);
            }
            else {
                return null;
            }
        }
        if (cleaned.endsWith(".git")) {
            cleaned = cleaned.slice(0, -".git".length);
        }
        if (!cleaned.includes("/")) {
            return null;
        }
        return cleaned;
    }
    catch {
        // ignore
    }
    return null;
}
export async function createPullRequest(cwd, options) {
    await requireGitRepo(cwd);
    const ghPath = await resolveGhPath();
    const repo = await resolveGitHubRepo(cwd);
    if (!repo) {
        throw new Error("Unable to determine GitHub repo from git remote");
    }
    const head = options.head ?? (await getCurrentBranch(cwd));
    const configured = await getConfiguredBaseRefForCwd(cwd);
    const base = configured.baseRef ?? options.base ?? (await resolveBaseRef(cwd));
    if (!head) {
        throw new Error("Unable to determine head branch for PR");
    }
    if (!base) {
        throw new Error("Unable to determine base branch for PR");
    }
    const normalizedBase = normalizeLocalBranchRefName(base);
    if (configured.isSeaworkOwnedWorktree && options.base && options.base !== base) {
        throw new Error(`Base ref mismatch: expected ${base}, got ${options.base}`);
    }
    await execAsync(`git push -u origin ${head}`, { cwd });
    const ghEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
    const args = ["api", "-X", "POST", `repos/${repo}/pulls`, "-f", `title=${options.title}`];
    args.push("-f", `head=${head}`);
    args.push("-f", `base=${normalizedBase}`);
    if (options.body) {
        args.push("-f", `body=${options.body}`);
    }
    const { stdout } = await execFileAsync(ghPath, args, { cwd, env: ghEnv });
    const parsed = JSON.parse(stdout.trim());
    if (!parsed?.url || !parsed?.number) {
        throw new Error("GitHub CLI did not return PR url/number");
    }
    return { url: parsed.url, number: parsed.number };
}
export async function getPullRequestStatus(cwd) {
    const cacheKey = getPullRequestStatusCacheKey(cwd);
    const cached = pullRequestStatusCache.get(cacheKey);
    if (cached) {
        return cached;
    }
    const existing = pullRequestStatusInFlight.get(cacheKey);
    if (existing) {
        return existing;
    }
    const lookup = getPullRequestStatusUncached(cwd)
        .then((status) => {
        pullRequestStatusCache.set(cacheKey, status);
        return status;
    })
        .finally(() => {
        pullRequestStatusInFlight.delete(cacheKey);
    });
    pullRequestStatusInFlight.set(cacheKey, lookup);
    return lookup;
}
async function getPullRequestStatusUncached(cwd) {
    await requireGitRepo(cwd);
    const head = await getCurrentBranch(cwd);
    if (!head) {
        return {
            status: null,
            githubFeaturesEnabled: false,
        };
    }
    let ghPath;
    try {
        ghPath = await resolveGhPath();
    }
    catch {
        return {
            status: null,
            githubFeaturesEnabled: false,
        };
    }
    try {
        const { stdout } = await execFileAsync(ghPath, ["pr", "view", "--json", "url,title,state,baseRefName,headRefName,mergedAt"], { cwd, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
        const pr = JSON.parse(stdout.trim());
        if (!pr || typeof pr !== "object" || !pr.url || !pr.title) {
            return { status: null, githubFeaturesEnabled: true };
        }
        const mergedAt = typeof pr.mergedAt === "string" && pr.mergedAt.trim().length > 0 ? pr.mergedAt : null;
        const state = mergedAt !== null
            ? "merged"
            : typeof pr.state === "string" && pr.state.trim().length > 0
                ? pr.state.toLowerCase()
                : "";
        return {
            status: {
                url: pr.url,
                title: pr.title,
                state,
                baseRefName: pr.baseRefName ?? "",
                headRefName: pr.headRefName ?? head,
                isMerged: mergedAt !== null,
            },
            githubFeaturesEnabled: true,
        };
    }
    catch (error) {
        if (isGhAuthError(error)) {
            return { status: null, githubFeaturesEnabled: false };
        }
        // gh pr view exits non-zero when no PR exists for the branch
        const message = error instanceof Error ? error.message : String(error);
        if (message.includes("no pull requests found") || message.includes("Could not resolve")) {
            return { status: null, githubFeaturesEnabled: true };
        }
        throw error;
    }
}
//# sourceMappingURL=checkout-git.js.map