/**
 * Git 快照 API 路由
 *
 * 提供 Git Stash 快照的创建、列表、恢复和删除功能
 */
import { Router } from 'express';
import { spawn } from 'node:child_process';
import { promises as fs } from 'node:fs';
import path from 'node:path';
import { validateToken } from '../middleware/auth.js';
import { logger } from '../utils/logger.js';
export const gitRouter = Router();
const DEFAULT_GIT_COMMAND_TIMEOUT_MS = 10_000;
/**
 * 解析 Git 命令超时时间。
 * 主流程：优先使用环境变量，非法或过小配置回落到默认值，避免 Git 检测请求长期挂起。
 */
export function resolveGitCommandTimeoutMs() {
    const configured = Number(process.env.AWS_RUNTIME_GIT_COMMAND_TIMEOUT_MS);
    if (Number.isFinite(configured) && configured >= 1_000) {
        return configured;
    }
    return DEFAULT_GIT_COMMAND_TIMEOUT_MS;
}
export async function assertGitWorkspacePathAccessible(workspacePath) {
    let stat;
    try {
        stat = await fs.stat(workspacePath);
    }
    catch (error) {
        const err = error;
        if (err.code === 'ENOENT') {
            throw new Error(`workspace path does not exist: ${workspacePath}`);
        }
        throw new Error(`workspace path is not accessible: ${workspacePath}${err.message ? ` (${err.message})` : ''}`);
    }
    if (!stat.isDirectory()) {
        throw new Error(`workspace path is not a directory: ${workspacePath}`);
    }
}
/**
 * 判断 Git diff 汇总行是否应该展示在差异树中。
 * 普通 modified 且文本增删均为 0 通常是 filemode/元数据噪声，避免显示为“无差异内容”。
 */
export function shouldIncludeGitDiffSummaryFile(status, additions, deletions, isBinaryDiff) {
    if (status === 'added' || status === 'deleted' || status === 'renamed') {
        return true;
    }
    return isBinaryDiff || additions > 0 || deletions > 0;
}
/**
 * 判断 git stash 是否因仓库尚无初始提交而失败。
 * 主流程：兼容 Git 不同版本输出，将底层英文错误归一成可面向用户的诊断。
 */
export function isGitStashMissingInitialCommitError(output) {
    const normalized = String(output || '').toLowerCase();
    return normalized.includes('you do not have the initial commit yet')
        || (normalized.includes('bad revision') && normalized.includes('head'))
        || (normalized.includes('ambiguous argument') && normalized.includes('head'));
}
export function createMissingInitialCommitSnapshotError() {
    return '当前 Git 仓库还没有初始提交，无法创建快照。请先提交一次初始提交后再创建快照。';
}
/**
 * 判断 git stash push 是否明确表示没有本地变更。
 * 主流程：兼容不同 Git 版本把 no-change 信息输出到 stdout 或 stderr，且可能返回 0 或非 0。
 */
export function isGitStashNoChangesOutput(output) {
    const normalized = String(output || '').toLowerCase();
    return normalized.includes('no local changes to save')
        || normalized.includes('no changes to save')
        || normalized.includes('没有本地变更需要保存');
}
/**
 * 解析 git stash list --format 输出行。
 * 主流程：优先返回稳定 stash commit hash 作为 ref，同时保留 stash@{n} 供界面展示。
 */
export function parseGitStashListLine(line) {
    const rawLine = String(line || '').trim();
    if (!rawLine)
        return null;
    const fields = rawLine.split('\0');
    if (fields.length >= 3) {
        const stableRef = fields[0]?.trim();
        const stashName = fields[1]?.trim();
        const subject = fields[2]?.trim() || stashName;
        const createdAt = fields[3]?.trim() || null;
        if (!stableRef || !stashName)
            return null;
        const parsedSubject = parseGitStashSubject(subject || '');
        return {
            ref: stableRef,
            stashName,
            message: parsedSubject.message,
            branch: parsedSubject.branch,
            createdAt,
        };
    }
    const match = rawLine.match(/^(stash@\{\d+\})(?::\s*)?(.*)$/);
    if (!match)
        return null;
    const stashName = match[1];
    const parsedSubject = parseGitStashSubject(match[2]?.trim() || stashName);
    return {
        ref: stashName,
        stashName,
        message: parsedSubject.message,
        branch: parsedSubject.branch,
        createdAt: null,
    };
}
/**
 * 从 stash subject 中拆出分支和用户消息。
 */
function parseGitStashSubject(subject) {
    const normalized = String(subject || '').trim();
    const branchMatch = normalized.match(/^(?:On|WIP on)\s+([^:]+):\s*(.*)$/);
    if (branchMatch) {
        return {
            branch: branchMatch[1].trim(),
            message: branchMatch[2].trim() || normalized,
        };
    }
    return { branch: '', message: normalized };
}
/**
 * 从 git stash list -n 1 输出解析最近一次 stash。
 * 主流程：提取稳定 stash commit hash，供无变更快照复用最近快照引用。
 */
export function parseLatestGitStash(output) {
    const firstLine = String(output || '').split('\n').find(line => line.trim());
    if (!firstLine)
        return null;
    const parsed = parseGitStashListLine(firstLine);
    if (!parsed)
        return null;
    return {
        ref: parsed.ref,
        stashName: parsed.stashName || parsed.ref,
        message: parsed.message || parsed.ref,
    };
}
/**
 * 执行 git 命令并返回输出
 */
async function execGitCommand(workspacePath, args) {
    return new Promise((resolve) => {
        let settled = false;
        let timeout = null;
        const finish = (result) => {
            if (settled) {
                return;
            }
            settled = true;
            if (timeout) {
                clearTimeout(timeout);
            }
            resolve(result);
        };
        const child = spawn('git', args, {
            cwd: workspacePath,
            stdio: ['ignore', 'pipe', 'pipe'],
        });
        let stdout = '';
        let stderr = '';
        child.stdout?.on('data', (chunk) => {
            stdout += String(chunk || '');
        });
        child.stderr?.on('data', (chunk) => {
            stderr += String(chunk || '');
        });
        timeout = setTimeout(() => {
            const timeoutMs = resolveGitCommandTimeoutMs();
            logger.warn(`[runtime-bridge] git command timed out after ${timeoutMs}ms: git ${args.join(' ')} (cwd=${workspacePath})`);
            child.kill('SIGKILL');
            finish({
                stdout,
                stderr: stderr || `git command timed out after ${timeoutMs}ms`,
                exitCode: 124,
            });
        }, resolveGitCommandTimeoutMs());
        child.on('error', (error) => {
            finish({ stdout: '', stderr: error.message, exitCode: 1 });
        });
        child.on('close', (code) => {
            finish({ stdout, stderr, exitCode: code ?? 1 });
        });
    });
}
function normalizeGitPath(targetPath) {
    return String(targetPath || '').replace(/\\/g, '/').replace(/^\.\//, '').trim();
}
async function hasGitMetadataDir(targetPath) {
    const gitDir = path.join(targetPath, '.git');
    try {
        const stat = await fs.stat(gitDir);
        return stat.isDirectory() || stat.isFile();
    }
    catch {
        return false;
    }
}
async function resolveGitRepositoryContext(workspacePath) {
    const normalizedPath = path.resolve(String(workspacePath).trim());
    await assertGitWorkspacePathAccessible(normalizedPath);
    const isGitRepo = await hasGitMetadataDir(normalizedPath);
    if (isGitRepo) {
        return {
            workspacePath: normalizedPath,
            repositoryRootPath: normalizedPath,
            hasRepository: true,
            isGitRepo: true,
            usingParentGitRepo: false,
            relativeWorkspacePath: ''
        };
    }
    const repoRootResult = await execGitCommand(normalizedPath, ['rev-parse', '--show-toplevel']);
    if (repoRootResult.exitCode !== 0) {
        return {
            workspacePath: normalizedPath,
            repositoryRootPath: null,
            hasRepository: false,
            isGitRepo: false,
            usingParentGitRepo: false,
            relativeWorkspacePath: ''
        };
    }
    const repositoryRootPath = path.resolve(repoRootResult.stdout.trim());
    const relativeWorkspacePath = normalizeGitPath(path.relative(repositoryRootPath, normalizedPath));
    return {
        workspacePath: normalizedPath,
        repositoryRootPath,
        hasRepository: true,
        isGitRepo: false,
        usingParentGitRepo: repositoryRootPath !== normalizedPath,
        relativeWorkspacePath
    };
}
function createScopedGitArgs(baseArgs, context) {
    if (!context.relativeWorkspacePath) {
        return [...baseArgs];
    }
    return [
        ...baseArgs,
        `--relative=${context.relativeWorkspacePath}`,
        '--',
        context.relativeWorkspacePath
    ];
}
function resolveRepositoryRelativeFilePath(filePath, context) {
    const normalizedFilePath = normalizeGitPath(filePath);
    if (!context.relativeWorkspacePath) {
        return normalizedFilePath;
    }
    return normalizeGitPath(path.posix.join(context.relativeWorkspacePath, normalizedFilePath));
}
function resolveGitDiffStatus(statusToken) {
    const normalizedToken = String(statusToken || '').trim().toUpperCase();
    if (normalizedToken.startsWith('A')) {
        return 'added';
    }
    if (normalizedToken.startsWith('D')) {
        return 'deleted';
    }
    if (normalizedToken.startsWith('R')) {
        return 'renamed';
    }
    return 'modified';
}
function parseRenameDisplayPath(rawPath) {
    const normalizedPath = normalizeGitPath(rawPath);
    if (!normalizedPath.includes('=>')) {
        return normalizedPath;
    }
    const braceMatch = normalizedPath.match(/^(.*)\{([^{}]+)=>([^{}]+)\}(.*)$/);
    if (braceMatch) {
        const prefix = braceMatch[1];
        const renamedTarget = braceMatch[3];
        const suffix = braceMatch[4];
        return normalizeGitPath(`${prefix}${renamedTarget}${suffix}`);
    }
    const arrowIndex = normalizedPath.lastIndexOf('=>');
    if (arrowIndex >= 0) {
        return normalizeGitPath(normalizedPath.slice(arrowIndex + 2));
    }
    return normalizedPath;
}
/**
 * 解析 git diff 的 name-status 与 numstat 输出，生成前端文件树需要的汇总数据。
 */
export function parseGitStatusStagedPaths(output) {
    const stagedPaths = new Set();
    const lines = String(output || '').split('\n').filter(line => line.trim());
    for (const line of lines) {
        const indexStatus = line[0] || ' ';
        if (indexStatus === ' ' || indexStatus === '?') {
            continue;
        }
        const rawPath = line.slice(3).trim();
        const pathParts = rawPath.split(' -> ');
        const displayPath = parseRenameDisplayPath(pathParts[pathParts.length - 1] || rawPath);
        if (displayPath) {
            stagedPaths.add(displayPath);
        }
    }
    return stagedPaths;
}
/**
 * 解析 git diff 的 name-status 与 numstat 输出，生成前端文件树需要的汇总数据。
 */
export function parseGitDiffSummary(nameStatusOutput, numstatOutput, stagedPaths = new Set(), forceStaged = false) {
    const statusMap = new Map();
    const nameStatusLines = nameStatusOutput.split('\n').filter(line => line.trim());
    for (const line of nameStatusLines) {
        const parts = line.split('\t');
        if (parts.length < 2) {
            continue;
        }
        const status = resolveGitDiffStatus(parts[0]);
        const pathIndex = status === 'renamed' && parts.length >= 3 ? 2 : 1;
        const displayPath = parseRenameDisplayPath(parts[pathIndex]);
        if (!displayPath) {
            continue;
        }
        statusMap.set(displayPath, status);
    }
    const files = [];
    const lines = numstatOutput.split('\n').filter(line => line.trim());
    for (const line of lines) {
        const parts = line.split('\t');
        if (parts.length < 3)
            continue;
        const isBinaryDiff = parts[0] === '-' || parts[1] === '-';
        const additions = parts[0] === '-' ? 0 : parseInt(parts[0], 10) || 0;
        const deletions = parts[1] === '-' ? 0 : parseInt(parts[1], 10) || 0;
        const filePath = parseRenameDisplayPath(parts.slice(2).join('\t').trim());
        const status = statusMap.get(filePath) || 'modified';
        if (!filePath || !shouldIncludeGitDiffSummaryFile(status, additions, deletions, isBinaryDiff)) {
            continue;
        }
        files.push({ path: filePath, status, additions, deletions, staged: forceStaged || stagedPaths.has(filePath) });
    }
    return files;
}
/**
 * 合并工作区与暂存区差异汇总。
 * 主流程：以路径去重，保留工作区展示顺序；同一路径若也存在暂存变更则标记为 staged 并累加统计。
 */
export function mergeGitDiffSummaryFiles(unstagedFiles, stagedFiles) {
    const fileMap = new Map();
    for (const file of unstagedFiles) {
        fileMap.set(file.path, { ...file });
    }
    for (const stagedFile of stagedFiles) {
        const existingFile = fileMap.get(stagedFile.path);
        if (!existingFile) {
            fileMap.set(stagedFile.path, { ...stagedFile, staged: true });
            continue;
        }
        fileMap.set(stagedFile.path, {
            ...existingFile,
            status: existingFile.status === 'modified' ? stagedFile.status : existingFile.status,
            additions: existingFile.additions + stagedFile.additions,
            deletions: existingFile.deletions + stagedFile.deletions,
            staged: true,
        });
    }
    return [...fileMap.values()];
}
function normalizeCommitHash(commitHash) {
    return String(commitHash || '').trim();
}
export function normalizeGitCommitMessage(message) {
    return String(message || '').trim();
}
export function createScopedGitPathspecArgs(context) {
    return ['--', context.relativeWorkspacePath || '.'];
}
export function createScopedFilePathspecArgs(filePath, context) {
    return ['--', resolveRepositoryRelativeFilePath(filePath, context)];
}
/**
 * 解析 git log 的 NUL 分隔输出，避免提交标题中的特殊字符破坏字段切分。
 */
function parseGitCommitLog(output) {
    const fields = output.split('\0');
    const commits = [];
    for (let index = 0; index + 4 < fields.length; index += 5) {
        const hash = fields[index]?.trim();
        if (!hash) {
            continue;
        }
        commits.push({
            hash,
            shortHash: fields[index + 1]?.trim() || hash.slice(0, 8),
            authorName: fields[index + 2]?.trim() || 'unknown',
            authorDate: fields[index + 3]?.trim() || '',
            subject: fields[index + 4]?.trim() || hash,
        });
    }
    return commits;
}
/**
 * 检查路径是否为 git 仓库
 * POST /runtime/git/check
 */
gitRouter.post('/git/check', validateToken, async (req, res) => {
    const { workspacePath } = req.body || {};
    if (!workspacePath || !String(workspacePath).trim()) {
        res.status(400).json({ error: 'workspacePath is required' });
        return;
    }
    try {
        const context = await resolveGitRepositoryContext(String(workspacePath).trim());
        res.json({
            ok: true,
            isGitRepo: context.isGitRepo,
            hasRepository: context.hasRepository,
            usingParentGitRepo: context.usingParentGitRepo,
            workspacePath: context.workspacePath,
            repositoryRootPath: context.repositoryRootPath,
            relativeWorkspacePath: context.relativeWorkspacePath
        });
        return;
    }
    catch (error) {
        const err = error;
        res.status(500).json({ error: err.message || 'check git repo failed' });
    }
});
/**
 * 创建 git stash 快照
 * POST /runtime/git/stash/create
 */
gitRouter.post('/git/stash/create', validateToken, async (req, res) => {
    const { workspacePath, message } = req.body || {};
    if (!workspacePath || !String(workspacePath).trim()) {
        res.status(400).json({ error: 'workspacePath is required' });
        return;
    }
    try {
        const normalizedPath = path.resolve(String(workspacePath).trim());
        const defaultMessage = `快照（${new Date().toLocaleString('zh-CN', {
            year: 'numeric',
            month: '2-digit',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit'
        })}）`;
        const stashMessage = message && String(message).trim()
            ? `快照: ${String(message).trim()}`
            : defaultMessage;
        const result = await execGitCommand(normalizedPath, ['stash', 'push', '-m', stashMessage]);
        if (isGitStashNoChangesOutput(`${result.stdout}\n${result.stderr}`)) {
            const latestResult = await execGitCommand(normalizedPath, ['stash', 'list', '-n', '1', '--format=%H%x00%gd%x00%gs%x00%cr']);
            const latestStash = latestResult.exitCode === 0 ? parseLatestGitStash(latestResult.stdout) : null;
            res.json({
                ok: true,
                stashRef: latestStash?.ref ?? null,
                stashName: latestStash?.stashName ?? null,
                message: latestStash
                    ? `没有本地变更需要保存，已复用最近快照 ${latestStash.stashName}`
                    : '没有本地变更需要保存',
                noChanges: true,
                reusedLatestStash: Boolean(latestStash),
                workspacePath: normalizedPath
            });
            return;
        }
        if (result.exitCode !== 0) {
            if (isGitStashMissingInitialCommitError(`${result.stdout}\n${result.stderr}`)) {
                res.status(400).json({ error: createMissingInitialCommitSnapshotError() });
                return;
            }
            res.status(400).json({ error: result.stderr || 'git stash push failed' });
            return;
        }
        const listResult = await execGitCommand(normalizedPath, ['stash', 'list', '-n', '1', '--format=%H%x00%gd%x00%gs%x00%cr']);
        const latestStash = listResult.exitCode === 0 ? parseLatestGitStash(listResult.stdout) : null;
        const stashRef = latestStash?.ref || 'stash@{0}';
        res.json({
            ok: true,
            stashRef,
            stashName: latestStash?.stashName ?? null,
            message: stashMessage,
            workspacePath: normalizedPath
        });
    }
    catch (error) {
        const err = error;
        res.status(500).json({ error: err.message || 'create stash failed' });
    }
});
/**
 * 获取 git stash 列表
 * POST /runtime/git/stash/list
 */
gitRouter.post('/git/stash/list', validateToken, async (req, res) => {
    const { workspacePath, limit = 20 } = req.body || {};
    if (!workspacePath || !String(workspacePath).trim()) {
        res.status(400).json({ error: 'workspacePath is required' });
        return;
    }
    try {
        const normalizedPath = path.resolve(String(workspacePath).trim());
        const stashLimit = Math.min(Math.max(1, Number(limit) || 20), 100);
        const result = await execGitCommand(normalizedPath, ['stash', 'list', '-n', String(stashLimit), '--format=%H%x00%gd%x00%gs%x00%cr']);
        if (result.exitCode !== 0) {
            res.status(400).json({ error: result.stderr || 'git stash list failed' });
            return;
        }
        const stashes = result.stdout
            .split('\n')
            .map(parseGitStashListLine)
            .filter((stash) => stash !== null);
        res.json({
            ok: true,
            stashes,
            count: stashes.length,
            workspacePath: normalizedPath
        });
    }
    catch (error) {
        const err = error;
        res.status(500).json({ error: err.message || 'list stash failed' });
    }
});
/**
 * 应用或弹出 git stash 快照。
 * 主流程：恢复前先清理工作区未提交变更，再执行 apply/pop，并统一处理冲突和引用不存在错误。
 */
async function restoreGitStash(req, res, mode) {
    const { workspacePath, ref } = req.body || {};
    if (!workspacePath || !String(workspacePath).trim()) {
        res.status(400).json({ error: 'workspacePath is required' });
        return;
    }
    try {
        const normalizedPath = path.resolve(String(workspacePath).trim());
        const stashRef = ref && String(ref).trim() ? String(ref).trim() : 'stash@{0}';
        await execGitCommand(normalizedPath, ['checkout', '--', '.']);
        const result = await execGitCommand(normalizedPath, ['stash', mode, stashRef]);
        if (result.exitCode !== 0) {
            if (result.stdout.includes('CONFLICT') || result.stderr.includes('CONFLICT')) {
                res.json({
                    ok: false,
                    hasConflict: true,
                    message: '恢复时发生冲突，请手动解决',
                    details: result.stdout || result.stderr,
                    workspacePath: normalizedPath
                });
                return;
            }
            if (result.stderr.includes('is not a valid reference')) {
                res.status(404).json({ error: `快照 ${stashRef} 不存在` });
                return;
            }
            res.status(400).json({ error: result.stderr || `git stash ${mode} failed` });
            return;
        }
        res.json({
            ok: true,
            stashRef,
            message: mode === 'apply' ? `已应用 ${stashRef}` : `已恢复 ${stashRef}`,
            preservedStash: mode === 'apply',
            workspacePath: normalizedPath
        });
    }
    catch (error) {
        const err = error;
        res.status(500).json({ error: err.message || `${mode} stash failed` });
    }
}
/**
 * 应用 git stash 快照但不删除快照
 * POST /runtime/git/stash/apply
 */
gitRouter.post('/git/stash/apply', validateToken, async (req, res) => {
    await restoreGitStash(req, res, 'apply');
});
/**
 * 恢复 git stash 快照并删除快照
 * POST /runtime/git/stash/pop
 */
gitRouter.post('/git/stash/pop', validateToken, async (req, res) => {
    await restoreGitStash(req, res, 'pop');
});
/**
 * 删除指定的 git stash
 * POST /runtime/git/stash/drop
 */
gitRouter.post('/git/stash/drop', validateToken, async (req, res) => {
    const { workspacePath, ref } = req.body || {};
    if (!workspacePath || !String(workspacePath).trim()) {
        res.status(400).json({ error: 'workspacePath is required' });
        return;
    }
    try {
        const normalizedPath = path.resolve(String(workspacePath).trim());
        const stashRef = ref && String(ref).trim() ? String(ref).trim() : 'stash@{0}';
        const result = await execGitCommand(normalizedPath, ['stash', 'drop', stashRef]);
        if (result.exitCode !== 0) {
            if (result.stderr.includes('is not a valid reference')) {
                res.status(404).json({ error: `快照 ${stashRef} 不存在` });
                return;
            }
            res.status(400).json({ error: result.stderr || 'git stash drop failed' });
            return;
        }
        res.json({
            ok: true,
            stashRef,
            message: `已删除 ${stashRef}`,
            workspacePath: normalizedPath
        });
    }
    catch (error) {
        const err = error;
        res.status(500).json({ error: err.message || 'drop stash failed' });
    }
});
/**
 * 获取工作区未提交变更概览
 * POST /runtime/git/diff
 */
gitRouter.post('/git/diff', validateToken, async (req, res) => {
    const { workspacePath, repositoryPath } = req.body || {};
    if (!workspacePath || !String(workspacePath).trim()) {
        res.status(400).json({ error: 'workspacePath is required' });
        return;
    }
    try {
        const context = await resolveGitRepositoryContext(String(repositoryPath || workspacePath).trim());
        if (!context.hasRepository || !context.repositoryRootPath) {
            res.json({
                ok: true,
                isGitRepo: context.isGitRepo,
                hasRepository: false,
                usingParentGitRepo: false,
                hasChanges: false,
                files: [],
                workspacePath: context.workspacePath,
                repositoryRootPath: null,
                relativeWorkspacePath: ''
            });
            return;
        }
        const nameStatusResult = await execGitCommand(context.repositoryRootPath, createScopedGitArgs(['diff', '--name-status'], context));
        if (nameStatusResult.exitCode !== 0) {
            res.status(400).json({ error: nameStatusResult.stderr || 'git diff failed' });
            return;
        }
        const result = await execGitCommand(context.repositoryRootPath, createScopedGitArgs(['diff', '--numstat'], context));
        if (result.exitCode !== 0) {
            res.status(400).json({ error: result.stderr || 'git diff failed' });
            return;
        }
        const stagedNameStatusResult = await execGitCommand(context.repositoryRootPath, createScopedGitArgs(['diff', '--cached', '--name-status'], context));
        if (stagedNameStatusResult.exitCode !== 0) {
            res.status(400).json({ error: stagedNameStatusResult.stderr || 'git staged diff failed' });
            return;
        }
        const stagedNumstatResult = await execGitCommand(context.repositoryRootPath, createScopedGitArgs(['diff', '--cached', '--numstat'], context));
        if (stagedNumstatResult.exitCode !== 0) {
            res.status(400).json({ error: stagedNumstatResult.stderr || 'git staged diff failed' });
            return;
        }
        const statusResult = await execGitCommand(context.repositoryRootPath, createScopedGitArgs(['status', '--porcelain=v1'], context));
        if (statusResult.exitCode !== 0) {
            res.status(400).json({ error: statusResult.stderr || 'git status failed' });
            return;
        }
        const stagedPaths = parseGitStatusStagedPaths(statusResult.stdout);
        const files = mergeGitDiffSummaryFiles(parseGitDiffSummary(nameStatusResult.stdout, result.stdout, stagedPaths), parseGitDiffSummary(stagedNameStatusResult.stdout, stagedNumstatResult.stdout, stagedPaths, true));
        res.json({
            ok: true,
            isGitRepo: context.isGitRepo,
            hasRepository: true,
            usingParentGitRepo: context.usingParentGitRepo,
            hasChanges: files.length > 0,
            files,
            workspacePath: context.workspacePath,
            repositoryRootPath: context.repositoryRootPath,
            relativeWorkspacePath: context.relativeWorkspacePath
        });
    }
    catch (error) {
        const err = error;
        res.status(500).json({ error: err.message || 'git diff failed' });
    }
});
/**
 * 提交工作区未提交变更
 * POST /runtime/git/commit
 */
gitRouter.post('/git/commit', validateToken, async (req, res) => {
    const { workspacePath, repositoryPath, message } = req.body || {};
    if (!workspacePath || !String(workspacePath).trim()) {
        res.status(400).json({ error: 'workspacePath is required' });
        return;
    }
    const commitMessage = normalizeGitCommitMessage(message);
    if (!commitMessage) {
        res.status(400).json({ error: 'commit message is required' });
        return;
    }
    try {
        const context = await resolveGitRepositoryContext(String(repositoryPath || workspacePath).trim());
        if (!context.hasRepository || !context.repositoryRootPath) {
            res.status(400).json({ error: 'workspace is not inside a git repository' });
            return;
        }
        const pathspecArgs = createScopedGitPathspecArgs(context);
        const addResult = await execGitCommand(context.repositoryRootPath, ['add', '--all', ...pathspecArgs]);
        if (addResult.exitCode !== 0) {
            res.status(400).json({ error: addResult.stderr || 'git add failed' });
            return;
        }
        const stagedResult = await execGitCommand(context.repositoryRootPath, ['diff', '--cached', '--quiet', ...pathspecArgs]);
        if (stagedResult.exitCode === 0) {
            res.status(400).json({ error: '没有可提交的变更' });
            return;
        }
        if (stagedResult.exitCode !== 1) {
            res.status(400).json({ error: stagedResult.stderr || 'git staged diff failed' });
            return;
        }
        const commitResult = await execGitCommand(context.repositoryRootPath, ['commit', '-m', commitMessage, ...pathspecArgs]);
        if (commitResult.exitCode !== 0) {
            res.status(400).json({ error: commitResult.stderr || commitResult.stdout || 'git commit failed' });
            return;
        }
        res.json({
            ok: true,
            message: '提交完成',
            output: commitResult.stdout,
            workspacePath: context.workspacePath,
            repositoryRootPath: context.repositoryRootPath,
            relativeWorkspacePath: context.relativeWorkspacePath,
        });
    }
    catch (error) {
        const err = error;
        res.status(500).json({ error: err.message || 'git commit failed' });
    }
});
/**
 * 将单个文件加入暂存区
 * POST /runtime/git/add-file
 */
gitRouter.post('/git/add-file', validateToken, async (req, res) => {
    const { workspacePath, repositoryPath, filePath } = req.body || {};
    if (!workspacePath || !String(workspacePath).trim()) {
        res.status(400).json({ error: 'workspacePath is required' });
        return;
    }
    const normalizedFilePath = normalizeGitPath(String(filePath || '').trim());
    if (!normalizedFilePath) {
        res.status(400).json({ error: 'filePath is required' });
        return;
    }
    try {
        const context = await resolveGitRepositoryContext(String(repositoryPath || workspacePath).trim());
        if (!context.hasRepository || !context.repositoryRootPath) {
            res.status(400).json({ error: 'workspace is not inside a git repository' });
            return;
        }
        const addResult = await execGitCommand(context.repositoryRootPath, ['add', ...createScopedFilePathspecArgs(normalizedFilePath, context)]);
        if (addResult.exitCode !== 0) {
            res.status(400).json({ error: addResult.stderr || 'git add file failed' });
            return;
        }
        res.json({
            ok: true,
            filePath: normalizedFilePath,
            workspacePath: context.workspacePath,
            repositoryRootPath: context.repositoryRootPath,
            relativeWorkspacePath: context.relativeWorkspacePath,
        });
    }
    catch (error) {
        const err = error;
        res.status(500).json({ error: err.message || 'git add file failed' });
    }
});
/**
 * 获取工作区提交列表
 * POST /runtime/git/commits
 */
gitRouter.post('/git/commits', validateToken, async (req, res) => {
    const { workspacePath, repositoryPath, limit } = req.body || {};
    if (!workspacePath || !String(workspacePath).trim()) {
        res.status(400).json({ error: 'workspacePath is required' });
        return;
    }
    try {
        const context = await resolveGitRepositoryContext(String(repositoryPath || workspacePath).trim());
        if (!context.hasRepository || !context.repositoryRootPath) {
            res.json({ ok: true, commits: [], workspacePath: context.workspacePath, repositoryRootPath: null });
            return;
        }
        const parsedLimit = Number(limit);
        const safeLimit = Number.isInteger(parsedLimit) && parsedLimit > 0 && parsedLimit <= 100 ? parsedLimit : 30;
        const result = await execGitCommand(context.repositoryRootPath, [
            'log',
            `-${safeLimit}`,
            '--date=iso-strict',
            '--pretty=format:%H%x00%h%x00%an%x00%ad%x00%s%x00',
            '--',
            context.relativeWorkspacePath || '.',
        ]);
        if (result.exitCode !== 0) {
            res.status(400).json({ error: result.stderr || 'git log failed' });
            return;
        }
        res.json({
            ok: true,
            commits: parseGitCommitLog(result.stdout),
            workspacePath: context.workspacePath,
            repositoryRootPath: context.repositoryRootPath,
            relativeWorkspacePath: context.relativeWorkspacePath,
        });
    }
    catch (error) {
        const err = error;
        res.status(500).json({ error: err.message || 'git commits failed' });
    }
});
/**
 * 获取指定提交相对父提交的变更概览
 * POST /runtime/git/commit-diff
 */
gitRouter.post('/git/commit-diff', validateToken, async (req, res) => {
    const { workspacePath, repositoryPath, commitHash } = req.body || {};
    if (!workspacePath || !String(workspacePath).trim()) {
        res.status(400).json({ error: 'workspacePath is required' });
        return;
    }
    const normalizedCommitHash = normalizeCommitHash(commitHash);
    if (!normalizedCommitHash) {
        res.status(400).json({ error: 'commitHash is required' });
        return;
    }
    try {
        const context = await resolveGitRepositoryContext(String(repositoryPath || workspacePath).trim());
        if (!context.hasRepository || !context.repositoryRootPath) {
            res.status(400).json({ error: 'workspace is not inside a git repository' });
            return;
        }
        const diffBaseArgs = ['diff', `${normalizedCommitHash}^!`];
        const nameStatusResult = await execGitCommand(context.repositoryRootPath, createScopedGitArgs([...diffBaseArgs, '--name-status'], context));
        if (nameStatusResult.exitCode !== 0) {
            res.status(400).json({ error: nameStatusResult.stderr || 'git commit diff failed' });
            return;
        }
        const numstatResult = await execGitCommand(context.repositoryRootPath, createScopedGitArgs([...diffBaseArgs, '--numstat'], context));
        if (numstatResult.exitCode !== 0) {
            res.status(400).json({ error: numstatResult.stderr || 'git commit diff failed' });
            return;
        }
        const files = parseGitDiffSummary(nameStatusResult.stdout, numstatResult.stdout);
        res.json({
            ok: true,
            commitHash: normalizedCommitHash,
            isGitRepo: context.isGitRepo,
            hasRepository: true,
            usingParentGitRepo: context.usingParentGitRepo,
            hasChanges: files.length > 0,
            files,
            workspacePath: context.workspacePath,
            repositoryRootPath: context.repositoryRootPath,
            relativeWorkspacePath: context.relativeWorkspacePath,
        });
    }
    catch (error) {
        const err = error;
        res.status(500).json({ error: err.message || 'git commit diff failed' });
    }
});
/**
 * 获取单个文件的详细差异
 * POST /runtime/git/diff-file
 */
gitRouter.post('/git/diff-file', validateToken, async (req, res) => {
    const { workspacePath, filePath, commitHash } = req.body || {};
    if (!workspacePath || !String(workspacePath).trim()) {
        res.status(400).json({ error: 'workspacePath is required' });
        return;
    }
    if (!filePath || !String(filePath).trim()) {
        res.status(400).json({ error: 'filePath is required' });
        return;
    }
    try {
        // 必须使用 workspacePath 来正确计算 relativeWorkspacePath
        // 如果使用 repositoryPath（仓库根路径），会丢失工作目录信息，导致文件路径拼接错误
        const context = await resolveGitRepositoryContext(String(workspacePath).trim());
        const normalizedFilePath = normalizeGitPath(String(filePath).trim());
        if (!context.hasRepository || !context.repositoryRootPath) {
            res.status(400).json({ error: 'workspace is not inside a git repository' });
            return;
        }
        const repositoryRelativeFilePath = resolveRepositoryRelativeFilePath(normalizedFilePath, context);
        const normalizedCommitHash = normalizeCommitHash(commitHash);
        const gitArgs = normalizedCommitHash
            ? ['show', '--format=', '--no-ext-diff', normalizedCommitHash, '--', repositoryRelativeFilePath]
            : ['diff', 'HEAD', '--', repositoryRelativeFilePath];
        let result = await execGitCommand(context.repositoryRootPath, gitArgs);
        if (!normalizedCommitHash && result.exitCode !== 0) {
            result = await execGitCommand(context.repositoryRootPath, ['diff', '--', repositoryRelativeFilePath]);
        }
        if (result.exitCode !== 0) {
            res.status(400).json({ error: result.stderr || 'git diff file failed' });
            return;
        }
        if (!normalizedCommitHash && !result.stdout.trim()) {
            const stagedResult = await execGitCommand(context.repositoryRootPath, [
                'diff',
                '--cached',
                '--',
                repositoryRelativeFilePath,
            ]);
            if (stagedResult.exitCode !== 0) {
                res.status(400).json({ error: stagedResult.stderr || 'git staged diff file failed' });
                return;
            }
            result = stagedResult;
        }
        res.json({
            ok: true,
            filePath: normalizedFilePath,
            repositoryRelativeFilePath,
            commitHash: normalizedCommitHash || null,
            diffContent: result.stdout,
            workspacePath: context.workspacePath,
            repositoryRootPath: context.repositoryRootPath,
            usingParentGitRepo: context.usingParentGitRepo
        });
    }
    catch (error) {
        const err = error;
        res.status(500).json({ error: err.message || 'git diff file failed' });
    }
});
