/**
 * Thin wrappers around git shell operations.
 * Extracted so tests can mock them without mocking the built-in child_process.
 */
import { execFileSync, spawnSync } from 'child_process';
import { writeFileSync, chmodSync, unlinkSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
/**
 * Pull the latest changes in a local git repo using fetch + fast-forward merge.
 * Returns 'updated', 'up-to-date', or 'diverged' (local has commits the remote doesn't).
 * Throws on hard errors (no remote, auth failure, fetch error, etc.).
 *
 * Fast-forward only: if the branches have diverged the caller should ask the
 * user to resolve manually with `git pull --rebase`.
 */
export function gitPull(repoPath, opts) {
    const creds = opts?.credentials;
    let askpassPath;
    const env = { ...process.env };
    if (creds) {
        askpassPath = join(tmpdir(), `.studiograph-askpass-${process.pid}`);
        writeFileSync(askpassPath, [
            '#!/bin/sh',
            `case "$1" in`,
            `  *[Uu]sername*) echo "${creds.username}" ;;`,
            `  *) echo "${creds.password}" ;;`,
            'esac',
        ].join('\n'), 'utf-8');
        chmodSync(askpassPath, 0o700);
        env.GIT_ASKPASS = askpassPath;
        env.GIT_TERMINAL_PROMPT = '0';
    }
    // Fetch — clear credential.helper only when injecting our own credentials so that
    // macOS Keychain / stored tokens continue to work for GitHub remotes.
    const headerArgs = (opts?.extraHeaders ?? []).flatMap(h => ['-c', `http.extraHeader=${h}`]);
    const fetchArgs = creds
        ? ['-c', 'credential.helper=', ...headerArgs, 'fetch', '--prune']
        : [...headerArgs, 'fetch', '--prune'];
    const fetch = spawnSync('git', fetchArgs, { cwd: repoPath, encoding: 'utf-8', stdio: 'pipe', env });
    if (askpassPath) {
        try {
            unlinkSync(askpassPath);
        }
        catch { /* non-fatal */ }
    }
    if (fetch.status !== 0) {
        throw new Error((fetch.stderr || fetch.stdout || 'git fetch failed').trim());
    }
    // Fast-forward merge only — exits non-zero if branches have diverged
    const merge = spawnSync('git', ['merge', '--ff-only', 'FETCH_HEAD'], {
        cwd: repoPath, encoding: 'utf-8', stdio: 'pipe',
    });
    if (merge.status === 0) {
        return (merge.stdout + merge.stderr).toLowerCase().includes('already up to date')
            ? 'up-to-date'
            : 'updated';
    }
    return 'diverged';
}
/**
 * Push local commits in a git repo to its remote.
 * Returns 'pushed', 'nothing-to-push', or 'rejected' (remote has newer commits).
 * Throws on hard errors (no remote, auth failure, etc.).
 */
export function gitPush(repoPath) {
    const result = spawnSync('git', ['push'], { cwd: repoPath, encoding: 'utf-8' });
    if (result.status === 0) {
        const combined = result.stdout + result.stderr;
        return combined.includes('Everything up-to-date') ? 'nothing-to-push' : 'pushed';
    }
    const combined = result.stdout + result.stderr;
    if (combined.includes('non-fast-forward') || combined.includes('fetch first') || combined.includes('rejected')) {
        return 'rejected';
    }
    // See gitPushWithCredentials for the rationale on the soft `not-found` state.
    if (combined.includes('Repository not found') || combined.includes('not found') || combined.includes('404')) {
        return 'not-found';
    }
    throw new Error((combined || `git push failed with exit ${result.status}`).trim());
}
/**
 * Check if a git repo has uncommitted changes (staged or unstaged).
 * Returns true if the working tree is dirty.
 */
export function gitHasUncommittedChanges(repoPath) {
    const result = spawnSync('git', ['status', '--porcelain'], { cwd: repoPath, encoding: 'utf-8' });
    return result.status === 0 && result.stdout.trim().length > 0;
}
/**
 * Check if a directory is a git repository.
 */
export function isGitRepo(repoPath) {
    const result = spawnSync('git', ['rev-parse', '--git-dir'], { cwd: repoPath, encoding: 'utf-8' });
    return result.status === 0;
}
/**
 * Check if a git repo has a remote named "origin" configured.
 */
export function gitHasRemote(repoPath) {
    const result = spawnSync('git', ['remote', 'get-url', 'origin'], { cwd: repoPath, encoding: 'utf-8' });
    return result.status === 0 && result.stdout.trim().length > 0;
}
/**
 * Set or update the 'origin' remote URL on a local git repo.
 */
export function gitSetRemote(repoPath, url) {
    const hasOrigin = gitHasRemote(repoPath);
    if (hasOrigin) {
        spawnSync('git', ['remote', 'set-url', 'origin', url], { cwd: repoPath, encoding: 'utf-8' });
    }
    else {
        spawnSync('git', ['remote', 'add', 'origin', url], { cwd: repoPath, encoding: 'utf-8' });
    }
}
/**
 * Push local commits to origin with Basic Auth credentials via GIT_ASKPASS.
 * Used for pushing to the embedded git server.
 */
export function gitPushWithCredentials(repoPath, credentials, opts = {}) {
    const askpassPath = join(tmpdir(), `.studiograph-askpass-${process.pid}`);
    writeFileSync(askpassPath, [
        '#!/bin/sh',
        `case "$1" in`,
        `  *[Uu]sername*) echo "${credentials.username}" ;;`,
        `  *) echo "${credentials.password}" ;;`,
        'esac',
    ].join('\n'), 'utf-8');
    chmodSync(askpassPath, 0o700);
    const env = {
        ...process.env,
        GIT_ASKPASS: askpassPath,
        GIT_TERMINAL_PROMPT: '0',
    };
    const headerArgs = (opts.extraHeaders ?? []).flatMap(h => ['-c', `http.extraHeader=${h}`]);
    try {
        const result = spawnSync('git', ['-c', 'credential.helper=', ...headerArgs, 'push', '-u', 'origin', 'main'], {
            cwd: repoPath, encoding: 'utf-8', env,
        });
        if (result.status === 0) {
            const combined = result.stdout + result.stderr;
            return combined.includes('Everything up-to-date') ? 'nothing-to-push' : 'pushed';
        }
        const combined = result.stdout + result.stderr;
        if (combined.includes('non-fast-forward') || combined.includes('fetch first') || combined.includes('rejected')) {
            return 'rejected';
        }
        // `not-found` covers the case where the local clone carries a folder
        // that doesn't exist on the server for the current user — a stale
        // clone that pre-dates an access tightening, or a personal folder
        // owned by another team member that workspace.json still references.
        // Surface it as a soft state so callers can skip + suggest cleanup
        // instead of failing the whole push run.
        if (combined.includes('Repository not found') || combined.includes('not found') || combined.includes('404')) {
            return 'not-found';
        }
        throw new Error((combined || `git push failed with exit ${result.status}`).trim());
    }
    finally {
        try {
            unlinkSync(askpassPath);
        }
        catch { /* non-fatal */ }
    }
}
/**
 * Clone a remote repository to a local path.
 *
 * Automatically injects the stored GitHub PAT (from `studiograph auth` or
 * GITHUB_TOKEN env) via GIT_ASKPASS so Git never blocks on an interactive
 * credential prompt. If no token is available and `silent` is false (the
 * default), uses `stdio: 'inherit'` so Git's credential prompt can reach
 * the user's terminal.
 *
 * Throws if the git command fails.
 */
export function gitClone(remoteUrl, localPath, opts) {
    const silent = opts?.silent ?? false;
    const creds = opts?.credentials;
    const env = { ...process.env };
    let hasCredentials = false;
    if (creds) {
        // Basic Auth for embedded git server — use GIT_ASKPASS to supply credentials
        const askpassPath = join(tmpdir(), `.studiograph-askpass-${process.pid}`);
        // The script receives the prompt ("Username for ..." or "Password for ...") as $1
        writeFileSync(askpassPath, [
            '#!/bin/sh',
            `case "$1" in`,
            `  *[Uu]sername*) echo "${creds.username}" ;;`,
            `  *) echo "${creds.password}" ;;`,
            'esac',
        ].join('\n'), 'utf-8');
        chmodSync(askpassPath, 0o700);
        env.GIT_ASKPASS = askpassPath;
        env.GIT_TERMINAL_PROMPT = '0';
        hasCredentials = true;
    }
    else if (silent) {
        env.GIT_TERMINAL_PROMPT = '0';
    }
    // If !silent and no credentials/token, leave stdio inherited so Git can prompt the user
    const stdio = (hasCredentials || silent) ? 'pipe' : 'inherit';
    const headerArgs = (opts?.extraHeaders ?? []).flatMap(h => ['-c', `http.extraHeader=${h}`]);
    try {
        execFileSync('git', ['-c', 'credential.helper=', ...headerArgs, 'clone', remoteUrl, localPath], { stdio, env, timeout: 120_000 });
    }
    finally {
        // Clean up askpass script
        if (env.GIT_ASKPASS) {
            try {
                unlinkSync(env.GIT_ASKPASS);
            }
            catch { /* non-fatal */ }
        }
    }
}
/**
 * Read a git config value.
 * Returns undefined if the key is not set.
 */
export function getGitConfig(key) {
    try {
        return execFileSync('git', ['config', key], { stdio: 'pipe' }).toString().trim() || undefined;
    }
    catch {
        return undefined;
    }
}
//# sourceMappingURL=git.js.map