/** @fileoverview Git operations including diff detection and file tracking. */
'use strict';
Object.defineProperty(exports, "__esModule", { value: true });
exports.getChangedFiles = getChangedFiles;
exports.getChangedFilesSync = getChangedFilesSync;
exports.getUnstagedFiles = getUnstagedFiles;
exports.getUnstagedFilesSync = getUnstagedFilesSync;
exports.getStagedFiles = getStagedFiles;
exports.getStagedFilesSync = getStagedFilesSync;
exports.isChanged = isChanged;
exports.isChangedSync = isChangedSync;
exports.isUnstaged = isUnstaged;
exports.isUnstagedSync = isUnstagedSync;
exports.isStaged = isStaged;
exports.isStagedSync = isStagedSync;
const node_path_1 = require("node:path");
const globs_1 = require("./globs");
const path_1 = require("./path");
const spawn_1 = require("./spawn");
const strings_1 = require("./strings");
const gitDiffCache = new Map();
let _fs;
/**
 * Lazily load the fs module to avoid Webpack errors.
 */
/*@__NO_SIDE_EFFECTS__*/
function getFs() {
    if (_fs === undefined) {
        // Use non-'node:' prefixed require to avoid Webpack errors.
        // eslint-disable-next-line n/prefer-node-protocol
        _fs = /*@__PURE__*/ require('fs');
    }
    return _fs;
}
/**
 * Get git executable path.
 */
function getGitPath() {
    return 'git';
}
/**
 * Get current working directory for git operations.
 * Returns the real path to handle symlinks like /tmp -> /private/tmp.
 */
function getCwd() {
    return getFs().realpathSync(process.cwd());
}
function getGitDiffSpawnArgs() {
    const cwd = getCwd();
    return {
        all: [
            getGitPath(),
            ['status', '--porcelain'],
            {
                cwd,
                shell: process.platform === 'win32',
            },
        ],
        unstaged: [
            getGitPath(),
            ['diff', '--name-only'],
            {
                cwd,
            },
        ],
        staged: [
            getGitPath(),
            ['diff', '--cached', '--name-only'],
            {
                cwd,
                shell: process.platform === 'win32',
            },
        ],
    };
}
async function innerDiff(args, options) {
    const { cache = true, ...parseOptions } = { __proto__: null, ...options };
    const cacheKey = cache ? JSON.stringify({ args, parseOptions }) : undefined;
    if (cache && cacheKey) {
        const result = gitDiffCache.get(cacheKey);
        if (result) {
            return result;
        }
    }
    let result;
    try {
        // Use stdioString: false to get raw Buffer, then convert ourselves to preserve exact output.
        const spawnResult = await (0, spawn_1.spawn)(args[0], args[1], {
            ...args[2],
            stdioString: false,
        });
        const stdout = Buffer.isBuffer(spawnResult.stdout)
            ? spawnResult.stdout.toString('utf8')
            : String(spawnResult.stdout);
        result = parseGitDiffStdout(stdout, parseOptions);
    }
    catch {
        return [];
    }
    if (cache && cacheKey) {
        gitDiffCache.set(cacheKey, result);
    }
    return result;
}
function innerDiffSync(args, options) {
    const { cache = true, ...parseOptions } = { __proto__: null, ...options };
    const cacheKey = cache ? JSON.stringify({ args, parseOptions }) : undefined;
    if (cache && cacheKey) {
        const result = gitDiffCache.get(cacheKey);
        if (result) {
            return result;
        }
    }
    let result;
    try {
        // Use stdioString: false to get raw Buffer, then convert ourselves to preserve exact output.
        const spawnResult = (0, spawn_1.spawnSync)(args[0], args[1], {
            ...args[2],
            stdioString: false,
        });
        const stdout = Buffer.isBuffer(spawnResult.stdout)
            ? spawnResult.stdout.toString('utf8')
            : String(spawnResult.stdout);
        result = parseGitDiffStdout(stdout, parseOptions);
    }
    catch {
        return [];
    }
    if (cache && cacheKey) {
        gitDiffCache.set(cacheKey, result);
    }
    return result;
}
function parseGitDiffStdout(stdout, options) {
    const rootPath = getCwd();
    const { absolute = false, cwd: cwdOption = rootPath, porcelain = false, ...matcherOptions } = { __proto__: null, ...options };
    // Resolve cwd to handle symlinks.
    const cwd = cwdOption === rootPath ? rootPath : getFs().realpathSync(cwdOption);
    // Split into lines without trimming to preserve leading spaces in porcelain format.
    let rawFiles = stdout
        ? (0, strings_1.stripAnsi)(stdout)
            .split('\n')
            .map(line => line.trimEnd())
            .filter(line => line)
        : [];
    // Parse porcelain format: strip status codes.
    // Git status --porcelain format is: XY filename
    // where X and Y are single characters and there's a space before the filename.
    if (porcelain) {
        rawFiles = rawFiles.map(line => {
            // Status is first 2 chars, then space, then filename.
            return line.length > 3 ? line.substring(3) : line;
        });
    }
    const files = absolute
        ? rawFiles.map(relPath => (0, path_1.normalizePath)(node_path_1.join(rootPath, relPath)))
        : rawFiles.map(relPath => (0, path_1.normalizePath)(relPath));
    if (cwd === rootPath) {
        return files;
    }
    const relPath = (0, path_1.normalizePath)(node_path_1.relative(rootPath, cwd));
    const matcher = (0, globs_1.getGlobMatcher)([`${relPath}/**`], {
        ...matcherOptions,
        absolute,
        cwd: rootPath,
    });
    const filtered = [];
    for (const filepath of files) {
        if (matcher(filepath)) {
            filtered.push(filepath);
        }
    }
    return filtered;
}
/**
 * Get all changed files including staged, unstaged, and untracked files.
 * Uses `git status --porcelain` which returns the full working tree status
 * with status codes (M=modified, A=added, D=deleted, ??=untracked, etc.).
 * This is the most comprehensive check - captures everything that differs
 * from the last commit.
 */
async function getChangedFiles(options) {
    const args = getGitDiffSpawnArgs().all;
    return await innerDiff(args, {
        __proto__: null,
        ...options,
        porcelain: true,
    });
}
/**
 * Get all changed files including staged, unstaged, and untracked files.
 * Uses `git status --porcelain` which returns the full working tree status
 * with status codes (M=modified, A=added, D=deleted, ??=untracked, etc.).
 * This is the most comprehensive check - captures everything that differs
 * from the last commit.
 */
function getChangedFilesSync(options) {
    const args = getGitDiffSpawnArgs().all;
    return innerDiffSync(args, {
        __proto__: null,
        ...options,
        porcelain: true,
    });
}
/**
 * Get unstaged modified files (changes not yet staged for commit).
 * Uses `git diff --name-only` which returns only unstaged modifications
 * to tracked files. Does not include untracked files or staged changes.
 * This is a focused check for uncommitted changes to existing files.
 */
async function getUnstagedFiles(options) {
    const args = getGitDiffSpawnArgs().unstaged;
    return await innerDiff(args, options);
}
/**
 * Get unstaged modified files (changes not yet staged for commit).
 * Uses `git diff --name-only` which returns only unstaged modifications
 * to tracked files. Does not include untracked files or staged changes.
 * This is a focused check for uncommitted changes to existing files.
 */
function getUnstagedFilesSync(options) {
    const args = getGitDiffSpawnArgs().unstaged;
    return innerDiffSync(args, options);
}
/**
 * Get staged files ready for commit (changes added with `git add`).
 * Uses `git diff --cached --name-only` which returns only staged changes.
 * Does not include unstaged modifications or untracked files.
 * This is a focused check for what will be included in the next commit.
 */
async function getStagedFiles(options) {
    const args = getGitDiffSpawnArgs().staged;
    return await innerDiff(args, options);
}
/**
 * Get staged files ready for commit (changes added with `git add`).
 * Uses `git diff --cached --name-only` which returns only staged changes.
 * Does not include unstaged modifications or untracked files.
 * This is a focused check for what will be included in the next commit.
 */
function getStagedFilesSync(options) {
    const args = getGitDiffSpawnArgs().staged;
    return innerDiffSync(args, options);
}
/**
 * Check if pathname has any changes (staged, unstaged, or untracked).
 */
async function isChanged(pathname, options) {
    const files = await getChangedFiles({
        __proto__: null,
        ...options,
        absolute: false,
    });
    // Resolve pathname to handle symlinks before computing relative path.
    const resolvedPathname = getFs().realpathSync(pathname);
    const relativePath = (0, path_1.normalizePath)(node_path_1.relative(getCwd(), resolvedPathname));
    return files.includes(relativePath);
}
/**
 * Check if pathname has any changes (staged, unstaged, or untracked).
 */
function isChangedSync(pathname, options) {
    const files = getChangedFilesSync({
        __proto__: null,
        ...options,
        absolute: false,
    });
    // Resolve pathname to handle symlinks before computing relative path.
    const resolvedPathname = getFs().realpathSync(pathname);
    const relativePath = (0, path_1.normalizePath)(node_path_1.relative(getCwd(), resolvedPathname));
    return files.includes(relativePath);
}
/**
 * Check if pathname has unstaged changes (modified but not staged).
 */
async function isUnstaged(pathname, options) {
    const files = await getUnstagedFiles({
        __proto__: null,
        ...options,
        absolute: false,
    });
    // Resolve pathname to handle symlinks before computing relative path.
    const resolvedPathname = getFs().realpathSync(pathname);
    const relativePath = (0, path_1.normalizePath)(node_path_1.relative(getCwd(), resolvedPathname));
    return files.includes(relativePath);
}
/**
 * Check if pathname has unstaged changes (modified but not staged).
 */
function isUnstagedSync(pathname, options) {
    const files = getUnstagedFilesSync({
        __proto__: null,
        ...options,
        absolute: false,
    });
    // Resolve pathname to handle symlinks before computing relative path.
    const resolvedPathname = getFs().realpathSync(pathname);
    const relativePath = (0, path_1.normalizePath)(node_path_1.relative(getCwd(), resolvedPathname));
    return files.includes(relativePath);
}
/**
 * Check if pathname is staged for commit.
 */
async function isStaged(pathname, options) {
    const files = await getStagedFiles({
        __proto__: null,
        ...options,
        absolute: false,
    });
    // Resolve pathname to handle symlinks before computing relative path.
    const resolvedPathname = getFs().realpathSync(pathname);
    const relativePath = (0, path_1.normalizePath)(node_path_1.relative(getCwd(), resolvedPathname));
    return files.includes(relativePath);
}
/**
 * Check if pathname is staged for commit.
 */
function isStagedSync(pathname, options) {
    const files = getStagedFilesSync({
        __proto__: null,
        ...options,
        absolute: false,
    });
    // Resolve pathname to handle symlinks before computing relative path.
    const resolvedPathname = getFs().realpathSync(pathname);
    const relativePath = (0, path_1.normalizePath)(node_path_1.relative(getCwd(), resolvedPathname));
    return files.includes(relativePath);
}
exports.default = {
    getChangedFiles,
    getChangedFilesSync,
    getStagedFiles,
    getStagedFilesSync,
    getUnstagedFiles,
    getUnstagedFilesSync,
    isChanged,
    isChangedSync,
    isStaged,
    isStagedSync,
    isUnstaged,
    isUnstagedSync,
};
