"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.LocalGitService = void 0;
const node_async_hooks_1 = require("node:async_hooks");
const simple_git_1 = require("simple-git");
const logger_1 = require("../utils/logger");
const RIVET_BOT_NAME = 'Rivet Bot';
const RIVET_BOT_EMAIL = 'noreply@rivet.dev';
const log = (0, logger_1.createLogger)('LocalGitService');
const GIT_OPERATION_TIMEOUT_MS = 30000; // 30 seconds
class Mutex {
    mutex = Promise.resolve();
    async lock() {
        let begin = () => { };
        const next = new Promise((resolve) => {
            begin = resolve;
        });
        const wait = this.mutex;
        this.mutex = next;
        await wait;
        return begin;
    }
}
class LocalGitService {
    cachedRootGit = new Map();
    static repoMutexes = new Map();
    static lockContext = new node_async_hooks_1.AsyncLocalStorage();
    getMutex(projectPath) {
        let mutex = LocalGitService.repoMutexes.get(projectPath);
        if (!mutex) {
            mutex = new Mutex();
            LocalGitService.repoMutexes.set(projectPath, mutex);
        }
        return mutex;
    }
    async withLock(projectPath, fn) {
        const existingLocks = LocalGitService.lockContext.getStore();
        if (existingLocks?.has(projectPath)) {
            // Re-entrant call, keep using existing lock
            return await fn();
        }
        const mutex = this.getMutex(projectPath);
        log.info(`[Mutex] Waiting for git lock...`);
        const unlock = await mutex.lock();
        log.info(`[Mutex] Acquired git lock`);
        try {
            const newLocks = new Set(existingLocks);
            newLocks.add(projectPath);
            return await LocalGitService.lockContext.run(newLocks, fn);
        }
        finally {
            log.info(`[Mutex] Releasing git lock`);
            unlock();
        }
    }
    /**
     * Execute multiple git operations within a single lock context.
     * Useful for atomic operations or to reduce log spam from repeated acquisitions.
     */
    async executeInLock(projectPath, fn) {
        return this.withLock(projectPath, fn);
    }
    getGit(projectPath) {
        return (0, simple_git_1.simpleGit)(projectPath, {
            timeout: {
                block: GIT_OPERATION_TIMEOUT_MS,
            },
        });
    }
    /** Resolves the git root for a project path. Caches the result since it never changes. */
    async getGitRootPath(projectPath) {
        const cached = this.cachedRootGit.get(projectPath);
        if (cached)
            return cached;
        const git = this.getGit(projectPath);
        try {
            const gitRoot = await git.revparse(['--show-toplevel']);
            const gitRootPath = gitRoot.trim();
            log.debug(`Git repository root: ${gitRootPath}`);
            log.debug(`Project path: ${projectPath}`);
            const rootGit = gitRootPath !== projectPath ? this.getGit(gitRootPath) : git;
            this.cachedRootGit.set(projectPath, rootGit);
            return rootGit;
        }
        catch (error) {
            log.warn('Could not determine git root, using project path:', error);
            return null;
        }
    }
    async configureDefaultIdentity(projectPath) {
        return this.withLock(projectPath, async () => {
            const git = this.getGit(projectPath);
            try {
                const config = await git.listConfig();
                const hasUserName = config.all['user.name'];
                const hasUserEmail = config.all['user.email'];
                if (hasUserName && hasUserEmail) {
                    log.debug(`Git identity already configured: ${hasUserName} <${hasUserEmail}>`);
                    return;
                }
                // Set fallback git identity for commits when no user authentication is available
                if (!hasUserName) {
                    await git.addConfig('user.name', RIVET_BOT_NAME);
                }
                if (!hasUserEmail) {
                    await git.addConfig('user.email', RIVET_BOT_EMAIL);
                }
                log.info(`Configured Rivet git identity because user identity was not found: ${RIVET_BOT_NAME} <${RIVET_BOT_EMAIL}>`);
            }
            catch (error) {
                log.warn('Failed to configure git identity:', error);
            }
        });
    }
    /** Checkout an existing local branch. */
    async checkoutBranch(projectPath, branchName) {
        return this.withLock(projectPath, async () => {
            const git = await this.getGitRootPath(projectPath);
            if (!git) {
                throw new Error('Could not determine git root');
            }
            // Check for uncommitted changes
            const status = await git.status();
            const allChangedFiles = [
                ...status.modified,
                ...status.created,
                ...status.deleted,
            ];
            if (allChangedFiles.length > 0) {
                log.info(`Stashing ${allChangedFiles.length} files before switching branch`);
                try {
                    await git.add(allChangedFiles);
                    await git.stash([
                        'push',
                        '-m',
                        `Rivet: Stash before switching to ${branchName}`,
                    ]);
                }
                catch (error) {
                    log.error('Failed to stash changes before branch switch:', error);
                    throw error;
                }
            }
            const currentBranch = status.current || 'main';
            // Checkout the existing branch
            await git.checkout(branchName);
            log.info(`Switched branch: ${branchName} (from ${currentBranch})`);
            return currentBranch;
        });
    }
    /** List all local branches sorted by most recently updated. */
    async listBranches(projectPath) {
        return this.withLock(projectPath, async () => {
            const rootGit = await this.getGitRootPath(projectPath);
            if (!rootGit) {
                return [];
            }
            try {
                // Sort by committerdate (most recent first)
                const output = await rootGit.raw([
                    'branch',
                    '--list',
                    '--sort=-committerdate',
                    '--format=%(refname:short)',
                ]);
                return output
                    .split('\n')
                    .map((b) => b.trim())
                    .filter(Boolean);
            }
            catch (error) {
                log.warn('Failed to list local branches:', error);
                return [];
            }
        });
    }
    /** Check whether a local branch with the given name already exists. */
    async branchExists(projectPath, branchName) {
        const branches = await this.listBranches(projectPath);
        return branches.includes(branchName);
    }
    async createBranch(projectPath, branchName) {
        return this.withLock(projectPath, async () => {
            const git = await this.getGitRootPath(projectPath);
            if (!git) {
                throw new Error('Could not determine git root');
            }
            // Check for uncommitted changes
            const status = await git.status();
            const allChangedFiles = [
                ...status.modified,
                ...status.created,
                ...status.deleted,
            ];
            if (allChangedFiles.length > 0) {
                log.info(`Stashing ${allChangedFiles.length} files before creating new branch`);
                try {
                    // Stash all files - we'll handle script cleaning at commit time
                    // Batch add operation to avoid multiple lock acquisitions
                    await git.add(allChangedFiles);
                    await git.stash([
                        'push',
                        '-m',
                        'Rivet: Stash before new branch creation',
                    ]);
                }
                catch (error) {
                    log.error('Failed to stash changes before branch creation:', error);
                    throw error;
                }
            }
            // Get current branch to create new branch from
            const currentBranch = status.current || 'main';
            // Create and checkout new branch from current branch
            await git.checkoutLocalBranch(branchName);
            log.info(`Created branch: ${branchName} from ${currentBranch}`);
            return currentBranch;
        });
    }
    async commitChange(projectPath, change, userInfo, commitMessage) {
        return this.withLock(projectPath, async () => {
            const git = await this.getGitRootPath(projectPath);
            if (!git) {
                throw new Error('Could not determine git root');
            }
            // Configure git identity if user info provided
            if (userInfo) {
                await git.addConfig('user.name', userInfo.name);
                await git.addConfig('user.email', userInfo.email);
                log.info(`Configured git identity: ${userInfo.name} <${userInfo.email}>`);
            }
            const status = await git.status();
            const allChangedFiles = [
                ...status.modified,
                ...status.created,
                ...status.deleted,
            ];
            if (allChangedFiles.length === 0) {
                log.warn('No files to commit - no changes detected');
                throw new Error('No files available to commit');
            }
            try {
                // Stage all changed files in a single batch operation
                log.debug(`Staging ${allChangedFiles.length} files: ${allChangedFiles.join(', ')}`);
                await git.add(allChangedFiles);
                // Use provided message or fall back to the change description
                const message = commitMessage || change.description;
                log.info(`Committing with message: ${message.split('\n')[0]}...`);
                const result = await git.commit(message);
                log.info(`Successfully committed ${allChangedFiles.length} files with SHA: ${result.commit}`);
                return result.commit;
            }
            catch (error) {
                log.error('Failed to commit changes:', error);
                throw error;
            }
        });
    }
    /**
     * Stage all tracked changes and commit with a user-provided message.
     * Uses `git commit -a` to stage and commit in a single subprocess.
     */
    async commitWithMessage(projectPath, message) {
        return this.withLock(projectPath, async () => {
            const git = await this.getGitRootPath(projectPath);
            if (!git) {
                throw new Error('Could not determine git root');
            }
            const result = await git.commit(message, { '-a': null });
            if (!result.commit) {
                throw new Error('No changes to commit');
            }
            log.info(`Committed with SHA: ${result.commit}`);
            return result.commit;
        });
    }
    async getCurrentBranch(projectPath) {
        return this.withLock(projectPath, async () => {
            const git = this.getGit(projectPath);
            const status = await git.status();
            return status.current || 'main';
        });
    }
    /**
     * Detect the default branch on origin (e.g. origin/main or origin/master)
     * Returns the full remote ref like "origin/main"
     */
    async getOriginDefaultBranch(projectPath) {
        return this.withLock(projectPath, async () => {
            const git = this.getGit(projectPath);
            try {
                const ref = await git.raw([
                    'symbolic-ref',
                    'refs/remotes/origin/HEAD',
                    '--short',
                ]);
                return ref.trim();
            }
            catch {
                log.debug('Could not resolve origin/HEAD, falling back to origin/main');
                return 'origin/main';
            }
        });
    }
    async getStatus(projectPath) {
        return this.withLock(projectPath, async () => {
            const git = this.getGit(projectPath);
            const status = await git.status();
            return {
                hasChanges: !status.isClean(),
                branch: status.current || 'main',
                files: [...status.modified, ...status.created, ...status.deleted],
            };
        });
    }
    async hasUncommittedChanges(projectPath) {
        const status = await this.getStatus(projectPath);
        return status.hasChanges;
    }
    async getRepositoryInfo(projectPath) {
        return this.withLock(projectPath, async () => {
            const git = this.getGit(projectPath);
            const remotes = await git.getRemotes(true);
            const origin = remotes.find((remote) => remote.name === 'origin');
            if (!origin?.refs?.fetch) {
                throw new Error('No origin remote found');
            }
            const remoteUrl = origin.refs.fetch;
            const repoInfo = this.parseRepoUrl(remoteUrl);
            return {
                ...repoInfo,
                remoteUrl,
            };
        });
    }
    parseRepoUrl(url) {
        // Handle HTTPS URLs (https://github.com/owner/repo.git)
        const httpsMatch = url.match(/https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
        if (httpsMatch) {
            const [, owner, repo] = httpsMatch;
            return {
                owner,
                repo,
                provider: 'github',
                baseUrl: 'https://github.com',
            };
        }
        // Handle SSH URLs (git@github.com:owner/repo.git)
        const sshMatch = url.match(/git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/);
        if (sshMatch) {
            const [, owner, repo] = sshMatch;
            return {
                owner,
                repo,
                provider: 'github',
                baseUrl: 'https://github.com',
            };
        }
        throw new Error(`Unsupported repository URL format: ${url}`);
    }
    async pushBranch(projectPath, branchName) {
        return this.withLock(projectPath, async () => {
            const git = this.getGit(projectPath);
            try {
                await git.push('origin', branchName, ['--set-upstream']);
                log.info(`Pushed branch to origin: ${branchName}`);
            }
            catch (error) {
                log.error('Failed to push branch:', error);
                throw new Error(`Failed to push branch ${branchName}: ${error}`);
            }
        });
    }
    async generatePullRequestUrl(projectPath, branchName, defaultBranch = 'main') {
        const repoInfo = await this.getRepositoryInfo(projectPath);
        return `${repoInfo.baseUrl}/${repoInfo.owner}/${repoInfo.repo}/compare/${defaultBranch}...${branchName}?expand=1`;
    }
    /** Get the diff of modified-but-not-staged files in the working tree. */
    async getUnstagedDiff(projectPath) {
        return this.withLock(projectPath, async () => {
            const git = this.getGit(projectPath);
            try {
                const diff = await git.diff();
                return diff;
            }
            catch (error) {
                log.error('Failed to get unstaged diff:', error);
                throw new Error(`Failed to get unstaged diff: ${error}`);
            }
        });
    }
    /** Get all uncommitted changes (staged + unstaged) relative to HEAD. */
    async getUncommittedDiff(projectPath) {
        return this.withLock(projectPath, async () => {
            const git = this.getGit(projectPath);
            try {
                const diff = await git.diff(['HEAD']);
                return diff;
            }
            catch (error) {
                log.error('Failed to get uncommitted diff:', error);
                throw new Error(`Failed to get uncommitted diff: ${error}`);
            }
        });
    }
    /**
     * Get git diff between two branches
     * @param projectPath Path to the git repository
     * @param baseBranch Base branch to compare from (e.g., 'main')
     * @param compareBranch Branch to compare (e.g., 'rivet-xxxx')
     * @returns The diff as a string
     */
    async getDiff(projectPath, baseBranch, compareBranch) {
        return this.withLock(projectPath, async () => {
            try {
                log.info(`Getting diff between ${baseBranch} and ${compareBranch}`);
                const git = this.getGit(projectPath);
                const diff = await git.diff([`${baseBranch}...${compareBranch}`]);
                log.info(`Generated diff between ${baseBranch} and ${compareBranch}`);
                return diff;
            }
            catch (error) {
                log.error('Failed to generate diff:', error);
                throw new Error(`Failed to generate diff: ${error}`);
            }
        });
    }
    /**
     * Create a pull request using the GitHub CLI
     * @param projectPath Path to the git repository
     * @param branchName Branch to create PR from
     * @param title PR title
     * @param body PR body/description
     * @param baseBranch Base branch for the PR (default: 'main')
     * @returns The PR URL
     */
    async createPullRequest(projectPath, branchName, title, body, baseBranch = 'main') {
        return this.withLock(projectPath, async () => {
            try {
                // Verify gh CLI is installed
                const { execFileSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
                try {
                    execFileSync('gh', ['--version'], { stdio: 'ignore', timeout: 5000 });
                }
                catch {
                    throw new Error('GitHub CLI (gh) is not installed. Install from: https://cli.github.com/');
                }
                // Create PR using gh CLI
                // Use execFileSync with argument array to avoid shell injection
                const result = execFileSync('gh', [
                    'pr',
                    'create',
                    '--title',
                    title,
                    '--body',
                    body,
                    '--base',
                    baseBranch,
                    '--head',
                    branchName,
                ], { encoding: 'utf-8', cwd: projectPath, timeout: 15000 });
                // Extract PR URL from output (gh returns the URL)
                const prUrl = result.trim();
                log.info(`Created PR: ${prUrl}`);
                return prUrl;
            }
            catch (error) {
                // If PR already exists, try to get the existing URL
                const stderr = error.stderr?.toString() || '';
                if (stderr.includes('already exists')) {
                    log.info('PR already exists, fetching existing URL');
                    const existingUrl = await this._getExistingPrUrl(projectPath, branchName);
                    if (existingUrl)
                        return existingUrl;
                }
                log.error('Failed to create pull request:', error);
                throw new Error(`Failed to create pull request: ${error}`);
            }
        });
    }
    /**
     * Check if an open pull request exists for the given branch.
     * Returns the PR URL if one exists, null otherwise.
     */
    async getExistingPrUrl(projectPath, branchName) {
        return this.withLock(projectPath, async () => {
            return this._getExistingPrUrl(projectPath, branchName);
        });
    }
    async _getExistingPrUrl(projectPath, branchName) {
        try {
            const { execFileSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
            const result = execFileSync('gh', ['pr', 'view', branchName, '--json', 'url', '--jq', '.url'], {
                encoding: 'utf-8',
                cwd: projectPath,
                timeout: 10000,
                stdio: ['pipe', 'pipe', 'pipe'],
            });
            const url = result.trim();
            return url || null;
        }
        catch {
            // gh pr view exits non-zero when no PR exists
            return null;
        }
    }
    /**
     * Undo the last commit by resetting HEAD to the previous commit.
     * This is a destructive operation that removes the last commit from history.
     */
    async undoLastCommit(projectPath) {
        return this.withLock(projectPath, async () => {
            const git = await this.getGitRootPath(projectPath);
            if (!git) {
                throw new Error('Could not determine git root');
            }
            try {
                // Check if there are any commits to undo
                const logResult = await git.log({ maxCount: 1 });
                if (!logResult.latest) {
                    throw new Error('No commits to undo');
                }
                // Reset to the previous commit, discarding changes
                await git.reset(['--hard', 'HEAD~1']);
                log.info('Successfully undid last commit');
                // Note: We rely on forceReload() in the frontend to refresh the iframe.
                // The dev server will rebuild on-demand when the iframe requests fresh content.
            }
            catch (error) {
                log.error('Failed to undo last commit:', error);
                throw error;
            }
        });
    }
    /**
     * Get insertion/deletion stats for all changes relative to a base branch.
     * Compares the working tree against the base, so both committed and uncommitted changes appear.
     */
    async getDiffStats(projectPath, baseBranch) {
        return this.withLock(projectPath, async () => {
            const git = this.getGit(projectPath);
            try {
                const summary = await git.diffSummary([baseBranch]);
                const files = summary.files.map((f) => ({
                    file: f.file,
                    insertions: f.insertions ?? 0,
                    deletions: f.deletions ?? 0,
                }));
                return {
                    files,
                    totalInsertions: summary.insertions,
                    totalDeletions: summary.deletions,
                };
            }
            catch (error) {
                log.error('Failed to get diff stats:', error);
                throw new Error(`Failed to get diff stats: ${error}`);
            }
        });
    }
    /**
     * Revert a single file to its state on the base branch
     */
    async revertFile(projectPath, baseBranch, filePath) {
        return this.withLock(projectPath, async () => {
            const git = await this.getGitRootPath(projectPath);
            if (!git) {
                throw new Error('Could not determine git root');
            }
            try {
                await git.checkout([baseBranch, '--', filePath]);
                log.info(`Reverted ${filePath} to ${baseBranch}`);
            }
            catch (error) {
                log.error(`Failed to revert file ${filePath}:`, error);
                throw new Error(`Failed to revert file ${filePath}: ${error}`);
            }
        });
    }
    /** Resolve the current HEAD commit SHA. */
    async getHeadSha(projectPath) {
        return this.withLock(projectPath, async () => {
            const git = await this.getGitRootPath(projectPath);
            if (!git)
                throw new Error('Git root not found');
            const sha = await git.revparse(['HEAD']);
            return sha.trim();
        });
    }
    /** Get commit messages between the base branch and the current branch head. */
    async getCommitLog(projectPath, baseBranch) {
        return this.withLock(projectPath, async () => {
            const git = this.getGit(projectPath);
            try {
                const logResult = await git.log({
                    from: baseBranch,
                    to: 'HEAD',
                });
                return logResult.all.map((entry) => entry.message);
            }
            catch (error) {
                log.warn('Failed to get commit log:', error);
                return [];
            }
        });
    }
    /**
     * Check whether there are commits on the branch that can be undone.
     * Compares HEAD against the SHA captured at session start so it works
     * even when branchName equals the base branch (e.g. working on main).
     * Pass `currentBranch` to skip an extra subprocess when already known.
     */
    async canUndo(projectPath, branchName, startingSha, currentBranch) {
        return this.withLock(projectPath, async () => {
            const git = await this.getGitRootPath(projectPath);
            if (!git) {
                return false;
            }
            try {
                // Resolve current branch if not provided by caller
                const resolvedBranch = currentBranch ??
                    (await git.revparse(['--abbrev-ref', 'HEAD'])).trim();
                if (resolvedBranch !== branchName) {
                    log.warn(`Not on expected branch. Current: ${resolvedBranch}, Expected: ${branchName}`);
                    return false;
                }
                log.debug(`Checking canUndo: branchName=${branchName}, startingSha=${startingSha}`);
                const count = await git.raw([
                    'rev-list',
                    '--count',
                    'HEAD',
                    `^${startingSha}`,
                ]);
                const commitCount = parseInt(count.trim(), 10);
                if (isNaN(commitCount)) {
                    log.warn('Failed to parse commit count, assuming cannot undo');
                    return false;
                }
                log.debug(`canUndo commit count: ${commitCount}`);
                return commitCount > 0;
            }
            catch (error) {
                log.warn('Failed to check if can undo:', error);
                return false;
            }
        });
    }
}
exports.LocalGitService = LocalGitService;
//# sourceMappingURL=LocalGitService.js.map