"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NativeGitRepositoryClient = exports.discoverGitRemoteUrl = exports.discoverGitRemoteNames = exports.discoverGitRemoteDefaultBranch = void 0;
const promises_1 = require("node:fs/promises");
const node_path_1 = __importDefault(require("node:path"));
const catalog_1 = require("../i18n/catalog");
const author_resolution_1 = require("./author-resolution");
const native_git_cli_1 = require("./native-git-cli");
const remote_discovery_1 = require("./remote-discovery");
Object.defineProperty(exports, "discoverGitRemoteDefaultBranch", { enumerable: true, get: function () { return remote_discovery_1.discoverGitRemoteDefaultBranch; } });
Object.defineProperty(exports, "discoverGitRemoteNames", { enumerable: true, get: function () { return remote_discovery_1.discoverGitRemoteNames; } });
Object.defineProperty(exports, "discoverGitRemoteUrl", { enumerable: true, get: function () { return remote_discovery_1.discoverGitRemoteUrl; } });
const remote_url_safety_1 = require("./remote-url-safety");
const types_1 = require("./types");
class NativeGitRepositoryClient {
    repositoryHost;
    authProvider;
    locale;
    hooks;
    constructor(repositoryHost, authProvider, locale = catalog_1.DEFAULT_APP_LOCALE, hooks = "publish") {
        this.repositoryHost = repositoryHost;
        this.authProvider = authProvider;
        this.locale = locale;
        this.hooks = hooks;
    }
    async ensureWorkspaceClone(input) {
        (0, remote_url_safety_1.assertRemoteUrlDoesNotContainCredentials)(input.remoteUrl);
        if (await exists(node_path_1.default.join(input.workspacePath, ".git"))) {
            await this.ensureWorkspaceRemote(input.workspacePath, input.remoteUrl);
            return;
        }
        await (0, promises_1.mkdir)(node_path_1.default.dirname(input.workspacePath), { recursive: true });
        await (0, native_git_cli_1.runNativeGit)(node_path_1.default.dirname(input.workspacePath), [
            "clone",
            "--origin",
            this.repositoryHost.remoteName,
            "--branch",
            input.baseBranch,
            "--single-branch",
            input.remoteUrl,
            input.workspacePath,
        ], { credentials: input.credentials, suppressHooks: true });
        await this.ensureWorkspaceRemote(input.workspacePath, input.remoteUrl);
    }
    async fetch(workspacePath) {
        const credentials = await this.authProvider.getGitCredentials(this.repositoryHost);
        await (0, native_git_cli_1.runNativeGit)(workspacePath, ["fetch", this.repositoryHost.remoteName, "--prune"], { credentials, suppressHooks: true });
    }
    async checkoutIssueBranch(input) {
        await this.fetch(input.workspacePath);
        if (await this.hasChanges(input.workspacePath)) {
            const currentBranch = await this.getCurrentBranch(input.workspacePath);
            if (currentBranch !== input.branchName) {
                throw new types_1.IssueWorkspaceSyncBlockedError("dirty_workspace");
            }
            return;
        }
        await this.checkoutExistingOrCreateIssueBranch(input);
        await this.syncCheckedOutIssueBranchWithRemote(input.workspacePath, input.branchName);
        await this.syncCheckedOutIssueBranchWithBase(input.workspacePath, input.baseBranch);
    }
    async hasChanges(workspacePath, pathspecs = []) {
        return ((await this.isMergeInProgress(workspacePath)) ||
            (await (0, native_git_cli_1.readNativeGitStatusPorcelain)(workspacePath, pathspecs)).length > 0);
    }
    async getHeadCommitOid(workspacePath) {
        return (await (0, native_git_cli_1.readNativeGit)(workspacePath, ["rev-parse", "HEAD"])).trim();
    }
    async getRemoteBranchHeadCommitOid(workspacePath, branchName) {
        await this.fetch(workspacePath);
        return (await (0, native_git_cli_1.readNativeGit)(workspacePath, [
            "rev-parse",
            this.remoteBranchRef(branchName),
        ])).trim();
    }
    async commitAllChanges(workspacePath, message, pathspecs = [], options = {}) {
        const author = await (0, author_resolution_1.resolveGitAuthor)(workspacePath, this.locale);
        await (0, native_git_cli_1.runNativeGit)(workspacePath, [
            "add",
            "-A",
            ...(pathspecs.length > 0 ? ["--", ...pathspecs] : []),
        ]);
        if (!(await this.hasStagedChanges(workspacePath, pathspecs))) {
            return;
        }
        await (0, native_git_cli_1.runNativeGit)(workspacePath, (0, native_git_cli_1.withGitIdentityArgs)([
            "commit",
            "--no-gpg-sign",
            ...(this.shouldRunHooks(options.hookContext) ? [] : ["--no-verify"]),
            "-m",
            message,
            ...(pathspecs.length > 0 ? ["--", ...pathspecs] : []),
        ], author), {
            env: (0, native_git_cli_1.withGitIdentityEnv)(author),
            suppressHooks: !this.shouldRunHooks(options.hookContext),
        });
    }
    async commitAllChangesAsMerge(input) {
        await this.writeMergeHead(input.workspacePath, input.parentCommitOid);
        await this.commitAllChanges(input.workspacePath, input.message, [], input.hookContext !== undefined ? { hookContext: input.hookContext } : {});
    }
    async pushBranch(workspacePath, branchName, options = {}) {
        const credentials = await this.authProvider.getGitCredentials(this.repositoryHost);
        await (0, native_git_cli_1.runNativeGit)(workspacePath, [
            "push",
            ...(this.shouldRunHooks(options.hookContext) ? [] : ["--no-verify"]),
            this.repositoryHost.remoteName,
            `${branchName}:${branchName}`,
        ], {
            credentials,
            suppressHooks: !this.shouldRunHooks(options.hookContext),
        });
    }
    async rebaseBranchOntoRemoteBranch(input) {
        await this.fetch(input.workspacePath);
        const remoteRef = this.remoteBranchRef(input.branchName);
        const remoteOid = (await (0, native_git_cli_1.readNativeGit)(input.workspacePath, ["rev-parse", remoteRef])).trim();
        await this.assertWorkspaceClean(input.workspacePath, {
            sourceCommitOid: remoteOid,
        });
        await (0, native_git_cli_1.runNativeGit)(input.workspacePath, ["checkout", input.branchName], {
            suppressHooks: true,
        });
        const author = await (0, author_resolution_1.resolveGitAuthor)(input.workspacePath, this.locale);
        try {
            await (0, native_git_cli_1.runNativeGit)(input.workspacePath, (0, native_git_cli_1.withGitIdentityArgs)(["rebase", "--no-verify", "--rebase-merges", remoteRef], author), { env: (0, native_git_cli_1.withGitIdentityEnv)(author), suppressHooks: true });
        }
        catch (error) {
            const conflictedFiles = await this.readConflictedFiles(input.workspacePath);
            await this.abortRebase(input.workspacePath);
            if (conflictedFiles.length > 0) {
                throw new types_1.IssueWorkspaceSyncBlockedError("merge_conflict", conflictedFiles, { sourceCommitOid: remoteOid });
            }
            throw error;
        }
    }
    async mergeSourceBranchIntoBranch(input) {
        await this.fetch(input.workspacePath);
        const sourceRef = this.remoteBranchRef(input.sourceBranchName);
        const sourceOid = (await (0, native_git_cli_1.readNativeGit)(input.workspacePath, ["rev-parse", sourceRef])).trim();
        const syncBlockOptions = { sourceCommitOid: sourceOid };
        await this.assertWorkspaceClean(input.workspacePath, syncBlockOptions);
        await this.checkoutCleanWorkspaceBranch(input.workspacePath, input.branchName, syncBlockOptions);
        if (await this.isCommitAncestorOfHead(input.workspacePath, sourceOid)) {
            return { kind: "already_merged", sourceCommitOid: sourceOid };
        }
        const author = await (0, author_resolution_1.resolveGitAuthor)(input.workspacePath, this.locale);
        try {
            await (0, native_git_cli_1.runNativeGit)(input.workspacePath, (0, native_git_cli_1.withGitIdentityArgs)([
                "merge",
                ...(this.shouldRunHooks("worker") ? [] : ["--no-verify"]),
                "--no-edit",
                "-m",
                `Merge branch '${input.sourceBranchName}' into ${input.branchName}`,
                sourceRef,
            ], author), {
                env: (0, native_git_cli_1.withGitIdentityEnv)(author),
                suppressHooks: !this.shouldRunHooks("worker"),
            });
            await this.resetCleanWorkspaceToHeadAndAssertClean(input.workspacePath, syncBlockOptions);
            return { kind: "merged", sourceCommitOid: sourceOid };
        }
        catch (error) {
            const conflictedFiles = await this.readConflictedFiles(input.workspacePath);
            if (conflictedFiles.length > 0) {
                await this.writeMergeHead(input.workspacePath, sourceOid);
                throw new types_1.IssueWorkspaceSyncBlockedError("merge_conflict", conflictedFiles, { sourceCommitOid: sourceOid });
            }
            throw error;
        }
    }
    async isBranchAheadOfBase(workspacePath, baseBranch) {
        await this.fetch(workspacePath);
        const remoteBaseRef = this.remoteBranchRef(baseBranch);
        const headOid = await this.getHeadCommitOid(workspacePath);
        const baseOid = (await (0, native_git_cli_1.readNativeGit)(workspacePath, ["rev-parse", remoteBaseRef])).trim();
        if (headOid === baseOid) {
            return false;
        }
        return !(await this.isAncestor(workspacePath, "HEAD", remoteBaseRef));
    }
    async isCommitAncestorOfHead(workspacePath, oid) {
        return this.isAncestor(workspacePath, oid, "HEAD");
    }
    async getCurrentBranch(workspacePath) {
        const branch = (await (0, native_git_cli_1.readNativeGit)(workspacePath, ["branch", "--show-current"])).trim();
        if (!branch) {
            throw new Error((0, catalog_1.translate)(this.locale, "git.error.detachedHead"));
        }
        return branch;
    }
    async ensureWorkspaceRemote(workspacePath, remoteUrl) {
        const existingRemoteUrl = await (0, remote_discovery_1.discoverGitRemoteUrl)(workspacePath, this.repositoryHost.remoteName);
        if (existingRemoteUrl === null) {
            await (0, native_git_cli_1.runNativeGit)(workspacePath, [
                "remote",
                "add",
                this.repositoryHost.remoteName,
                remoteUrl,
            ]);
        }
        else if (existingRemoteUrl !== remoteUrl) {
            await (0, native_git_cli_1.runNativeGit)(workspacePath, [
                "remote",
                "set-url",
                this.repositoryHost.remoteName,
                remoteUrl,
            ]);
        }
        await (0, native_git_cli_1.runNativeGit)(workspacePath, [
            "config",
            `remote.${this.repositoryHost.remoteName}.fetch`,
            `+refs/heads/*:refs/remotes/${this.repositoryHost.remoteName}/*`,
        ]);
    }
    async checkoutExistingOrCreateIssueBranch(input) {
        if (await this.refExists(input.workspacePath, `refs/heads/${input.branchName}`)) {
            await (0, native_git_cli_1.runNativeGit)(input.workspacePath, ["checkout", "-f", input.branchName], { suppressHooks: true });
            return;
        }
        if (await this.refExists(input.workspacePath, this.remoteBranchRef(input.branchName))) {
            await (0, native_git_cli_1.runNativeGit)(input.workspacePath, [
                "checkout",
                "-f",
                "-B",
                input.branchName,
                this.remoteBranchRef(input.branchName),
            ], { suppressHooks: true });
            return;
        }
        await (0, native_git_cli_1.runNativeGit)(input.workspacePath, [
            "checkout",
            "-f",
            "-B",
            input.branchName,
            this.remoteBranchRef(input.baseBranch),
        ], { suppressHooks: true });
    }
    async syncCheckedOutIssueBranchWithRemote(workspacePath, branchName) {
        const remoteRef = this.remoteBranchRef(branchName);
        if (!(await this.refExists(workspacePath, remoteRef))) {
            return;
        }
        const [headOid, remoteOid] = await Promise.all([
            this.getHeadCommitOid(workspacePath),
            (0, native_git_cli_1.readNativeGit)(workspacePath, ["rev-parse", remoteRef]).then((value) => value.trim()),
        ]);
        if (headOid === remoteOid) {
            return;
        }
        if (await this.isAncestor(workspacePath, "HEAD", remoteRef)) {
            await (0, native_git_cli_1.runNativeGit)(workspacePath, ["reset", "--hard", remoteRef], {
                suppressHooks: true,
            });
            await this.assertWorkspaceClean(workspacePath, {
                sourceCommitOid: remoteOid,
            });
            return;
        }
        if (await this.isAncestor(workspacePath, remoteRef, "HEAD")) {
            return;
        }
        throw new types_1.IssueWorkspaceSyncBlockedError("branch_diverged", [], {
            sourceCommitOid: remoteOid,
        });
    }
    async syncCheckedOutIssueBranchWithBase(workspacePath, baseBranch) {
        const baseRef = this.remoteBranchRef(baseBranch);
        const [headOid, baseOid] = await Promise.all([
            this.getHeadCommitOid(workspacePath),
            (0, native_git_cli_1.readNativeGit)(workspacePath, ["rev-parse", baseRef]).then((value) => value.trim()),
        ]);
        if (headOid === baseOid) {
            return;
        }
        if (await this.isAncestor(workspacePath, "HEAD", baseRef)) {
            await (0, native_git_cli_1.runNativeGit)(workspacePath, ["reset", "--hard", baseRef], {
                suppressHooks: true,
            });
            await this.assertWorkspaceClean(workspacePath);
        }
    }
    async refExists(workspacePath, ref) {
        try {
            await (0, native_git_cli_1.runNativeGit)(workspacePath, [
                "show-ref",
                "--verify",
                "--quiet",
                ref,
            ]);
            return true;
        }
        catch {
            return false;
        }
    }
    async isAncestor(workspacePath, ancestor, descendant) {
        try {
            await (0, native_git_cli_1.runNativeGit)(workspacePath, [
                "merge-base",
                "--is-ancestor",
                ancestor,
                descendant,
            ]);
            return true;
        }
        catch (error) {
            if ((0, native_git_cli_1.isNativeGitExitCode)(error, 1)) {
                return false;
            }
            throw error;
        }
    }
    async assertWorkspaceClean(workspacePath, options = {}) {
        if (await this.hasChanges(workspacePath)) {
            throw new types_1.IssueWorkspaceSyncBlockedError("dirty_workspace", [], options);
        }
    }
    async checkoutCleanWorkspaceBranch(workspacePath, branchName, options = {}) {
        await (0, native_git_cli_1.runNativeGit)(workspacePath, ["checkout", "-f", branchName], {
            suppressHooks: true,
        });
        await this.resetCleanWorkspaceToHeadAndAssertClean(workspacePath, options);
    }
    async resetCleanWorkspaceToHeadAndAssertClean(workspacePath, options = {}) {
        await (0, native_git_cli_1.runNativeGit)(workspacePath, ["reset", "--hard", "HEAD"], {
            suppressHooks: true,
        });
        await this.assertWorkspaceClean(workspacePath, options);
    }
    async hasStagedChanges(workspacePath, pathspecs) {
        try {
            await (0, native_git_cli_1.runNativeGit)(workspacePath, [
                "diff",
                "--cached",
                "--quiet",
                ...(pathspecs.length > 0 ? ["--", ...pathspecs] : []),
            ]);
            return false;
        }
        catch (error) {
            if ((0, native_git_cli_1.isNativeGitExitCode)(error, 1)) {
                return true;
            }
            throw error;
        }
    }
    async isMergeInProgress(workspacePath) {
        try {
            await (0, native_git_cli_1.readNativeGit)(workspacePath, [
                "rev-parse",
                "-q",
                "--verify",
                "MERGE_HEAD",
            ]);
            return true;
        }
        catch {
            return false;
        }
    }
    async writeMergeHead(workspacePath, sourceOid) {
        const stdout = await (0, native_git_cli_1.readNativeGit)(workspacePath, [
            "rev-parse",
            "--git-path",
            "MERGE_HEAD",
        ]);
        await (0, promises_1.writeFile)(node_path_1.default.resolve(workspacePath, stdout.trim()), `${sourceOid}\n`, "utf8");
    }
    async readConflictedFiles(workspacePath) {
        const status = await (0, native_git_cli_1.readNativeGitStatusPorcelain)(workspacePath);
        if (status.length === 0) {
            return [];
        }
        return status
            .split("\n")
            .flatMap((line) => {
            const code = line.slice(0, 2);
            if (!isConflictStatusCode(code)) {
                return [];
            }
            const pathPart = line.slice(3).trim();
            return pathPart ? [pathPart] : [];
        })
            .sort((left, right) => left.localeCompare(right));
    }
    async abortRebase(workspacePath) {
        try {
            await (0, native_git_cli_1.runNativeGit)(workspacePath, ["rebase", "--abort"], {
                suppressHooks: true,
            });
        }
        catch (error) {
            if (await this.isRebaseInProgress(workspacePath)) {
                throw error;
            }
        }
    }
    async isRebaseInProgress(workspacePath) {
        for (const stateDirectory of ["rebase-merge", "rebase-apply"]) {
            const stdout = await (0, native_git_cli_1.readNativeGit)(workspacePath, [
                "rev-parse",
                "--git-path",
                stateDirectory,
            ]);
            try {
                await (0, promises_1.access)(node_path_1.default.resolve(workspacePath, stdout.trim()));
                return true;
            }
            catch {
                // Continue checking the other rebase state directory.
            }
        }
        return false;
    }
    shouldRunHooks(context) {
        if (context === "publish") {
            return this.hooks !== "off";
        }
        if (context === "worker") {
            return this.hooks === "publish_and_worker";
        }
        return false;
    }
    remoteBranchRef(branchName) {
        return `refs/remotes/${this.repositoryHost.remoteName}/${branchName}`;
    }
}
exports.NativeGitRepositoryClient = NativeGitRepositoryClient;
async function exists(targetPath) {
    try {
        await (0, promises_1.access)(targetPath);
        return true;
    }
    catch {
        return false;
    }
}
function isConflictStatusCode(code) {
    return ["AA", "AU", "DD", "DU", "UA", "UD", "UU"].includes(code);
}
