"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GitRepository = void 0;
exports.cloneFromUpstream = cloneFromUpstream;
exports.getGithubSlugOrNull = getGithubSlugOrNull;
exports.extractUserAndRepoFromGitHubUrl = extractUserAndRepoFromGitHubUrl;
exports.commitChanges = commitChanges;
exports.getLatestCommitSha = getLatestCommitSha;
const child_process_1 = require("child_process");
const path_1 = require("path");
const devkit_exports_1 = require("../devkit-exports");
function execAsync(command, execOptions) {
    return new Promise((res, rej) => {
        (0, child_process_1.exec)(command, execOptions, (err, stdout, stderr) => {
            if (err) {
                return rej(err);
            }
            res(stdout);
        });
    });
}
async function cloneFromUpstream(url, destination, { originName, depth } = {
    originName: 'origin',
}) {
    await execAsync(`git clone ${url} ${destination} ${depth ? `--depth ${depth}` : ''} --origin ${originName}`, {
        cwd: (0, path_1.dirname)(destination),
        maxBuffer: 10 * 1024 * 1024,
    });
    return new GitRepository(destination);
}
class GitRepository {
    constructor(directory) {
        this.directory = directory;
        this.root = this.getGitRootPath(this.directory);
    }
    getGitRootPath(cwd) {
        return (0, child_process_1.execSync)('git rev-parse --show-toplevel', {
            cwd,
        })
            .toString()
            .trim();
    }
    async addFetchRemote(remoteName, branch) {
        return await this.execAsync(`git config --add remote.${remoteName}.fetch "+refs/heads/${branch}:refs/remotes/${remoteName}/${branch}"`);
    }
    async showStat() {
        return await this.execAsync(`git show --stat`);
    }
    async listBranches() {
        return (await this.execAsync(`git ls-remote --heads --quiet`))
            .trim()
            .split('\n')
            .map((s) => s
            .trim()
            .substring(s.indexOf('\t') + 1)
            .replace('refs/heads/', ''));
    }
    async getGitFiles(path) {
        // Use -z to return file names exactly as they are stored in git, separated by NULL (\x00) character.
        // This avoids problems with special characters in file names.
        return (await this.execAsync(`git ls-files -z ${path}`))
            .trim()
            .split('\x00')
            .map((s) => s.trim())
            .filter(Boolean);
    }
    async reset(ref) {
        return await this.execAsync(`git reset ${ref} --hard`);
    }
    async mergeUnrelatedHistories(ref, message) {
        return await this.execAsync(`git merge ${ref} -X ours --allow-unrelated-histories -m "${message}"`);
    }
    async fetch(remote, ref) {
        return await this.execAsync(`git fetch ${remote}${ref ? ` ${ref}` : ''}`);
    }
    async checkout(branch, opts) {
        return await this.execAsync(`git checkout ${opts.new ? '-b ' : ' '}${branch}${opts.base ? ' ' + opts.base : ''}`);
    }
    async move(path, destination) {
        return await this.execAsync(`git mv ${this.quotePath(path)} ${this.quotePath(destination)}`);
    }
    async push(ref, remoteName) {
        return await this.execAsync(`git push -u -f ${remoteName} ${ref}`);
    }
    async commit(message) {
        return await this.execAsync(`git commit -am "${message}"`);
    }
    async amendCommit() {
        return await this.execAsync(`git commit --amend -a --no-edit`);
    }
    async deleteGitRemote(name) {
        return await this.execAsync(`git remote rm ${name}`);
    }
    async addGitRemote(name, url) {
        return await this.execAsync(`git remote add ${name} ${url}`);
    }
    async hasFilterRepoInstalled() {
        try {
            await this.execAsync(`git filter-repo --help`);
            return true;
        }
        catch {
            return false;
        }
    }
    // git-filter-repo is much faster than filter-branch, but needs to be installed by user
    // Use `hasFilterRepoInstalled` to check if it's installed
    async filterRepo(source, destination) {
        // NOTE: filter-repo requires POSIX path to work
        const sourcePosixPath = source.split(path_1.sep).join(path_1.posix.sep);
        const destinationPosixPath = destination.split(path_1.sep).join(path_1.posix.sep);
        await this.execAsync(`git filter-repo -f ${source !== '' ? `--path ${this.quotePath(sourcePosixPath)}` : ''} ${source !== destination
            ? `--path-rename ${this.quotePath(sourcePosixPath, true)}:${this.quotePath(destinationPosixPath, true)}`
            : ''}`);
    }
    async filterBranch(source, destination, branchName) {
        // We need non-ASCII file names to not be quoted, or else filter-branch will exclude them.
        await this.execAsync(`git config core.quotepath false`);
        // NOTE: filter-repo requires POSIX path to work
        const sourcePosixPath = source.split(path_1.sep).join(path_1.posix.sep);
        const destinationPosixPath = destination.split(path_1.sep).join(path_1.posix.sep);
        // First, if the source is not a root project, then only include commits relevant to the subdirectory.
        if (source !== '') {
            await this.execAsync(`git filter-branch -f --index-filter 'node ${(0, path_1.join)(__dirname, 'git-utils.index-filter.js')} "${sourcePosixPath}"' --prune-empty -- ${branchName}`);
        }
        // Then, move files to their new location if necessary.
        if (source === '' || source !== destination) {
            await this.execAsync(`git filter-branch -f --tree-filter 'node ${(0, path_1.join)(__dirname, 'git-utils.tree-filter.js')} "${sourcePosixPath}" "${destinationPosixPath}"' -- ${branchName}`);
        }
    }
    execAsync(command) {
        return execAsync(command, {
            cwd: this.root,
            maxBuffer: 10 * 1024 * 1024,
        });
    }
    quotePath(_path, ensureTrailingSlash) {
        const path = ensureTrailingSlash && _path !== '' && !_path.endsWith('/')
            ? `${_path}/`
            : _path;
        return process.platform === 'win32'
            ? // Windows/CMD only understands double-quotes, single-quotes are treated as part of the file name
                // Bash and other shells will substitute `$` in file names with a variable value.
                `"${path}"`
            : // e.g. `git mv "$$file.txt" "libs/a/$$file.txt"` will not work since `$$` is swapped with the PID of the last process.
                // Using single-quotes prevents this substitution.
                `'${path}'`;
    }
}
exports.GitRepository = GitRepository;
/**
 * This is currently duplicated in Nx Console. Please let @MaxKless know if you make changes here.
 */
function getGithubSlugOrNull() {
    try {
        const gitRemote = (0, child_process_1.execSync)('git remote -v', {
            stdio: 'pipe',
        }).toString();
        // If there are no remotes, we default to github
        if (!gitRemote || gitRemote.length === 0) {
            return 'github';
        }
        return extractUserAndRepoFromGitHubUrl(gitRemote);
    }
    catch (e) {
        // Probably git is not set up, so we default to github
        return 'github';
    }
}
function extractUserAndRepoFromGitHubUrl(gitRemotes) {
    const regex = /^\s*(\w+)\s+(git@github\.com:|https:\/\/github\.com\/)([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\.git/gm;
    const remotesPriority = ['origin', 'upstream', 'base'];
    const foundRemotes = {};
    let firstGitHubUrl = null;
    let match;
    while ((match = regex.exec(gitRemotes)) !== null) {
        const remoteName = match[1];
        const url = match[2] + match[3] + '/' + match[4] + '.git';
        foundRemotes[remoteName] = url;
        if (!firstGitHubUrl) {
            firstGitHubUrl = url;
        }
    }
    for (const remote of remotesPriority) {
        if (foundRemotes[remote]) {
            return parseGitHubUrl(foundRemotes[remote]);
        }
    }
    return firstGitHubUrl ? parseGitHubUrl(firstGitHubUrl) : null;
}
function parseGitHubUrl(url) {
    const sshPattern = /git@github\.com:([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\.git/;
    const httpsPattern = /https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\.git/;
    let match = url.match(sshPattern) || url.match(httpsPattern);
    if (match) {
        return `${match[1]}/${match[2]}`;
    }
    return null;
}
function commitChanges(commitMessage, directory) {
    try {
        (0, child_process_1.execSync)('git add -A', { encoding: 'utf8', stdio: 'pipe' });
        (0, child_process_1.execSync)('git commit --no-verify -F -', {
            encoding: 'utf8',
            stdio: 'pipe',
            input: commitMessage,
            cwd: directory,
        });
    }
    catch (err) {
        if (directory) {
            // We don't want to throw during create-nx-workspace
            // because maybe there was an error when setting up git
            // initially.
            devkit_exports_1.logger.verbose(`Git may not be set up correctly for this new workspace.
        ${err}`);
        }
        else {
            throw new Error(`Error committing changes:\n${err.stderr}`);
        }
    }
    return getLatestCommitSha();
}
function getLatestCommitSha() {
    try {
        return (0, child_process_1.execSync)('git rev-parse HEAD', {
            encoding: 'utf8',
            stdio: 'pipe',
        }).trim();
    }
    catch {
        return null;
    }
}
