import { CONFIG_VALIDATION, INVALID_PATH, REPOSITORY_CHANGED, REPOSITORY_DISABLED, REPOSITORY_EMPTY, SYSTEM_INSUFFICIENT_DISK_SPACE, TEMPORARY_ERROR, UNKNOWN_ERROR } from "../../constants/error-messages.js";
import { getEnv } from "../env.js";
import { newlineRegex, regEx } from "../regex.js";
import { getConfigFileNames } from "../../config/app-strings.js";
import { GlobalConfig } from "../../config/global.js";
import { matchRegexOrGlobList } from "../string-match.js";
import { logger } from "../../logger/index.js";
import { getChildEnv } from "../exec/utils.js";
import { ExternalHostError } from "../../types/errors/external-host-error.js";
import { instrument } from "../../instrumentation/index.js";
import { logWarningIfUnicodeHiddenCharactersInPackageFile } from "../unicode.js";
import { getGitEnvironmentVariables } from "./auth.js";
import { getNoVerify, setNoVerify, simpleGitConfig } from "./config.js";
import { getCache } from "../cache/repository/index.js";
import { withInstrumenting } from "../../instrumentation/with-instrumenting.js";
import { incCountValue, incLimitedValue } from "../../workers/global/limits.js";
import { parseGitAuthor } from "./author.js";
import { getCachedBehindBaseResult, setCachedBehindBaseResult } from "./behind-base-branch-cache.js";
import { getCachedConflictResult, setCachedConflictResult } from "./conflicts-cache.js";
import { bulkChangesDisallowed, checkForPlatformFailure, handleCommitError } from "./error.js";
import { instrumentGit } from "./instrument.js";
import { getCachedModifiedResult, setCachedModifiedResult } from "./modified-cache.js";
import { configSigningKey, setPrivateKey, writePrivateKey } from "./private-key.js";
import fs from "fs-extra";
import { isBoolean, isNonEmptyObject, isString } from "@sindresorhus/is";
import { DateTime } from "luxon";
import upath from "upath";
import { setTimeout } from "timers/promises";
import { ResetMode, simpleGit } from "simple-git";
import semver from "semver";
import URL from "node:url";
//#region lib/util/git/index.ts
const retryCount = 5;
const delaySeconds = 3;
const delayFactor = 2;
const RENOVATE_FORK_UPSTREAM = "renovate-fork-upstream";
async function gitRetry(gitFunc) {
	let round = 0;
	let lastError;
	while (round <= retryCount) {
		if (round > 0) logger.debug(`gitRetry round ${round}`);
		try {
			const res = await gitFunc();
			if (round > 1) logger.debug("Successful retry of git function");
			return res;
		} catch (err) {
			lastError = err;
			logger.debug({ err }, `Git function thrown`);
			const errChecked = checkForPlatformFailure(err);
			if (errChecked instanceof ExternalHostError) logger.debug({ err: errChecked }, `ExternalHostError thrown in round ${round + 1} of ${retryCount} - retrying in the next round`);
			else throw err;
		}
		const nextDelay = delayFactor ^ (round - 1) * delaySeconds;
		logger.trace({ nextDelay }, `Delay next round`);
		await setTimeout(1e3 * nextDelay);
		round++;
	}
	throw lastError;
}
async function isDirectory(dir) {
	try {
		return (await fs.stat(dir)).isDirectory();
	} catch {
		return false;
	}
}
async function getDefaultBranch(git) {
	logger.debug("getDefaultBranch()");
	try {
		let res = await git.raw([
			"rev-parse",
			"--abbrev-ref",
			"origin/HEAD"
		]);
		/* v8 ignore next -- TODO: add test #40625 */
		if (!res) {
			logger.debug("Could not determine default branch using git rev-parse");
			const headPrefix = "HEAD branch: ";
			res = (await git.raw([
				"remote",
				"show",
				"origin"
			])).split("\n").map((line) => line.trim()).find((line) => line.startsWith(headPrefix)).replace(headPrefix, "");
		}
		return res.replace("origin/", "").trim();
	} catch (err) 	/* v8 ignore next -- TODO: add test #40625 */ {
		logger.debug({ err }, "Error getting default branch");
		const errChecked = checkForPlatformFailure(err);
		if (errChecked) throw errChecked;
		if (err.message.startsWith("fatal: ref refs/remotes/origin/HEAD is not a symbolic ref")) throw new Error(REPOSITORY_EMPTY);
		if (err.message.includes("fatal: ambiguous argument 'origin/HEAD'")) {
			logger.warn("Error getting default branch");
			throw new Error(TEMPORARY_ERROR);
		}
		throw err;
	}
}
let config = {};
let git;
let gitInitialized;
let submodulesInitizialized;
let privateKeySet = false;
const GIT_MINIMUM_VERSION = "2.33.0";
async function validateGitVersion() {
	let version;
	const globalGit = instrumentGit(simpleGit());
	try {
		const { major, minor, patch, installed } = await globalGit.version();
		/* v8 ignore if -- TODO: add test #40625 */
		if (!installed) {
			logger.error("Git not installed");
			return false;
		}
		version = `${major}.${minor}.${patch}`;
	} catch (err) 	/* v8 ignore next -- TODO: add test #40625 */ {
		logger.error({ err }, "Error fetching git version");
		return false;
	}
	/* v8 ignore if -- TODO: add test #40625 */
	if (!(version && semver.gte(version, "2.33.0"))) {
		logger.error({
			detectedVersion: version,
			minimumVersion: GIT_MINIMUM_VERSION
		}, "Git version needs upgrading");
		return false;
	}
	logger.debug(`Found valid git version: ${version}`);
	return true;
}
async function fetchBranchCommits(preferUpstream = true) {
	config.branchCommits = {};
	const url = preferUpstream && config.upstreamUrl ? config.upstreamUrl : config.url;
	logger.debug(`fetchBranchCommits(): url=${url}`);
	const opts = [
		"ls-remote",
		"--heads",
		url
	];
	if (config.extraCloneOpts) Object.entries(config.extraCloneOpts).forEach((e) => opts.unshift(e[0], `${e[1]}`));
	try {
		const lsRemoteRes = await gitRetry(() => git.raw(opts));
		logger.trace({ lsRemoteRes }, "git ls-remote result");
		lsRemoteRes.split(newlineRegex).filter(Boolean).map((line) => line.trim().split(regEx(/\s+/))).forEach(([sha, ref]) => {
			config.branchCommits[ref.replace("refs/heads/", "")] = sha;
		});
		logger.trace({ branchCommits: config.branchCommits }, "branch commits");
	} catch (err) 	/* v8 ignore next -- TODO: add test #40625 */ {
		const errChecked = checkForPlatformFailure(err);
		if (errChecked) throw errChecked;
		logger.debug({ err }, "git error");
		if (err.message?.includes("Please ask the owner to check their account")) throw new Error(REPOSITORY_DISABLED);
		throw err;
	}
}
async function fetchRevSpec(revSpec) {
	await gitRetry(() => git.fetch(["origin", revSpec]));
}
async function initRepo(args) {
	config = { ...args };
	config.ignoredAuthors = [];
	config.additionalBranches = [];
	config.branchIsModified = {};
	git = instrumentGit(simpleGit(GlobalConfig.get("localDir"), simpleGitConfig()).env({
		...getEnv(),
		LANG: "C.UTF-8",
		LC_ALL: "C.UTF-8"
	}));
	gitInitialized = false;
	submodulesInitizialized = false;
	await fetchBranchCommits();
}
async function resetToBranch(branchName) {
	logger.debug(`resetToBranch(${branchName})`);
	await git.raw(["reset", "--hard"]);
	await gitRetry(() => git.checkout(branchName));
	await git.raw([
		"reset",
		"--hard",
		"origin/" + branchName
	]);
	await git.raw(["clean", "-fd"]);
}
/* v8 ignore next -- TODO: add test #40625 */
async function resetToCommit(commit) {
	logger.debug(`resetToCommit(${commit})`);
	await git.raw([
		"reset",
		"--hard",
		commit
	]);
}
async function deleteLocalBranch(branchName) {
	await git.branch(["-D", branchName]);
}
async function cleanLocalBranches() {
	const existingBranches = (await git.raw(["branch"])).split(newlineRegex).map((branch) => branch.trim()).filter((branch) => branch.length > 0 && !branch.startsWith("* "));
	logger.debug({ existingBranches });
	for (const branchName of existingBranches) await deleteLocalBranch(branchName);
}
function setGitAuthor(gitAuthor) {
	const gitAuthorParsed = parseGitAuthor(gitAuthor ?? "Renovate Bot <renovate@whitesourcesoftware.com>");
	if (!gitAuthorParsed) {
		const error = new Error(CONFIG_VALIDATION);
		error.validationSource = "None";
		error.validationError = "Invalid gitAuthor";
		error.validationMessage = `\`gitAuthor\` is not parsed as valid RFC5322 format: \`${gitAuthor}\``;
		throw error;
	}
	config.gitAuthorName = gitAuthorParsed.name;
	config.gitAuthorEmail = gitAuthorParsed.address;
}
async function writeGitAuthor() {
	const { gitAuthorName, gitAuthorEmail, writeGitDone } = config;
	/* v8 ignore if -- TODO: add test #40625 */
	if (writeGitDone) return;
	config.writeGitDone = true;
	try {
		// v8 ignore else -- TODO: add test #40625
		if (gitAuthorName) {
			logger.debug(`Setting git author name: ${gitAuthorName}`);
			await git.addConfig("user.name", gitAuthorName);
		}
		// v8 ignore else -- TODO: add test #40625
		if (gitAuthorEmail) {
			logger.debug(`Setting git author email: ${gitAuthorEmail}`);
			await git.addConfig("user.email", gitAuthorEmail);
		}
	} catch (err) 	/* v8 ignore next -- TODO: add test #40625 */ {
		const errChecked = checkForPlatformFailure(err);
		if (errChecked) throw errChecked;
		logger.debug({
			err,
			gitAuthorName,
			gitAuthorEmail
		}, "Error setting git author config");
		throw new Error(TEMPORARY_ERROR);
	}
}
function setUserRepoConfig({ gitIgnoredAuthors, gitAuthor }) {
	config.ignoredAuthors = gitIgnoredAuthors ?? [];
	setGitAuthor(gitAuthor);
}
async function getSubmodules() {
	try {
		return (await git.raw([
			"config",
			"--file",
			".gitmodules",
			"--get-regexp",
			"\\.path"
		]) || "").trim().split(regEx(/[\n\s]/)).filter((_e, i) => i % 2);
	} catch (err) 	/* v8 ignore next -- TODO: add test #40625 */ {
		logger.warn({ err }, "Error getting submodules");
		return [];
	}
}
async function cloneSubmodules(shouldClone, cloneSubmodulesFilter) {
	if (!shouldClone || submodulesInitizialized) return;
	submodulesInitizialized = true;
	const gitEnv = getChildEnv({ env: getGitEnvironmentVariables() });
	await syncGit();
	const submodules = await getSubmodules();
	for (const submodule of submodules) {
		if (!matchRegexOrGlobList(submodule, cloneSubmodulesFilter ?? ["*"])) {
			logger.debug({ cloneSubmodulesFilter }, `Skipping submodule ${submodule}`);
			continue;
		}
		try {
			logger.debug(`Cloning git submodule at ${submodule}`);
			await gitRetry(() => git.env(gitEnv).submoduleUpdate([
				"--init",
				"--recursive",
				submodule
			]));
		} catch (err) {
			logger.warn({
				err,
				submodule
			}, `Unable to initialise git submodule`);
		}
	}
}
function isCloned() {
	return gitInitialized;
}
const syncGit = withInstrumenting({ name: "syncGit" }, async function() {
	if (gitInitialized) {
		if (getEnv().RENOVATE_X_CLEAR_HOOKS) await git.raw([
			"config",
			"core.hooksPath",
			"/dev/null"
		]);
		return;
	}
	/* v8 ignore if -- TODO: add test #40625 */
	if (GlobalConfig.get("platform") === "local") throw new Error("Cannot sync git when platform=local");
	gitInitialized = true;
	const localDir = GlobalConfig.get("localDir");
	logger.debug(`syncGit(): Initializing git repository into ${localDir}`);
	const gitHead = upath.join(localDir, ".git/HEAD");
	let clone = true;
	if (await fs.pathExists(gitHead)) await instrument("fetch", async () => {
		logger.debug(`syncGit(): Found existing git repository, attempting git fetch`);
		try {
			await git.raw([
				"remote",
				"set-url",
				"origin",
				config.url
			]);
			const fetchStart = Date.now();
			await gitRetry(() => git.fetch(["--prune", "origin"]));
			config.currentBranch = config.currentBranch || await getDefaultBranch(git);
			await resetToBranch(config.currentBranch);
			await cleanLocalBranches();
			const durationMs = Math.round(Date.now() - fetchStart);
			logger.info({ durationMs }, "git fetch completed");
			clone = false;
		} catch (err) 		/* v8 ignore next -- TODO: add test #40625 */ {
			if (err.message === "empty") throw err;
			logger.info({ err }, "git fetch error, falling back to git clone");
		}
	});
	if (clone) await instrument("clone", async () => {
		const cloneStart = Date.now();
		try {
			const opts = [];
			if (config.defaultBranch) opts.push("-b", config.defaultBranch);
			if (config.fullClone) logger.debug("Performing full clone");
			else {
				logger.debug("Performing blobless clone");
				opts.push("--filter=blob:none");
			}
			if (config.extraCloneOpts) Object.entries(config.extraCloneOpts).forEach((e) => opts.push(e[0], `${e[1]}`));
			const emptyDirAndClone = async () => {
				await instrument(`fs.emptyDir(${localDir})`, () => fs.emptyDir(localDir));
				await git.clone(config.url, ".", opts);
			};
			await gitRetry(() => instrument("emptyDirAndClone", emptyDirAndClone));
		} catch (err) 		/* v8 ignore next -- TODO: add test #40625 */ {
			logger.debug({ err }, "git clone error");
			if (err.message?.includes("No space left on device")) throw new Error(SYSTEM_INSUFFICIENT_DISK_SPACE);
			if (err.message === "empty") throw err;
			throw new ExternalHostError(err, "git");
		}
		const durationMs = Math.round(Date.now() - cloneStart);
		logger.debug({ durationMs }, "git clone completed");
	});
	try {
		config.currentBranchSha = (await git.raw(["rev-parse", "HEAD"])).trim();
	} catch (err) 	/* v8 ignore next -- TODO: add test #40625 */ {
		if (err.message?.includes("fatal: not a git repository")) throw new Error(REPOSITORY_CHANGED);
		throw err;
	}
	await instrument("cloneSubmodules", () => cloneSubmodules(!!config.cloneSubmodules, config.cloneSubmodulesFilter));
	try {
		const latestCommit = (await git.log({ n: 1 })).latest;
		logger.debug({ latestCommit }, "latest repository commit");
	} catch (err) 	/* v8 ignore next -- TODO: add test #40625 */ {
		const errChecked = checkForPlatformFailure(err);
		if (errChecked) throw errChecked;
		if (err.message.includes("does not have any commits yet")) throw new Error(REPOSITORY_EMPTY);
		logger.warn({ err }, "Cannot retrieve latest commit");
	}
	config.currentBranch = config.currentBranch ?? config.defaultBranch ?? await getDefaultBranch(git);
	/* v8 ignore next -- TODO: add test #40625 */
	delete getCache()?.semanticCommits;
	if (config.upstreamUrl) {
		const { upstreamUrl } = config;
		await instrument("sync with upstreamUrl", async () => {
			logger.debug(`Bringing default branch up-to-date with ${RENOVATE_FORK_UPSTREAM}, to get latest config`);
			// v8 ignore else -- TODO: add test #40625
			if (!(await git.getRemotes(true)).some((remote) => remote.name === "renovate-fork-upstream")) {
				logger.debug(`Adding remote ${RENOVATE_FORK_UPSTREAM}`);
				await git.addRemote(RENOVATE_FORK_UPSTREAM, upstreamUrl);
			}
			await syncForkWithUpstream(config.currentBranch);
			await fetchBranchCommits(false);
		});
	}
	config.currentBranchSha = (await git.revparse("HEAD")).trim();
	logger.debug(`Current branch SHA: ${config.currentBranchSha}`);
});
async function getRepoStatus(path) {
	if (isString(path)) {
		const localDir = GlobalConfig.get("localDir");
		const localPath = upath.resolve(localDir, path);
		if (!localPath.startsWith(upath.resolve(localDir))) {
			logger.warn({
				localPath,
				localDir
			}, "Preventing access to file outside the local directory");
			throw new Error(INVALID_PATH);
		}
	}
	await syncGit();
	return git.status(path ? [path] : []);
}
function branchExists(branchName) {
	return !!config.branchCommits[branchName];
}
function getBranchCommit(branchName) {
	return config.branchCommits?.[branchName] || null;
}
async function getBranchUpdateDate(branchName) {
	const branchSha = config.branchCommits[branchName];
	if (!branchSha) return null;
	try {
		return await getCommitDate(branchSha);
	} catch (err) {
		logger.debug({
			err,
			branchName
		}, "Error getting branch update date");
		return null;
	}
}
async function getCommitMessages() {
	logger.debug("getCommitMessages");
	// v8 ignore else -- TODO: add test #40625
	if (GlobalConfig.get("platform") !== "local") await syncGit();
	try {
		return (await git.log({
			n: 20,
			format: { message: "%s" },
			"--no-merges": null
		})).all.map((commit) => commit.message);
	} catch 	/* v8 ignore next -- TODO: add test #40625 */ {
		return [];
	}
}
async function checkoutBranch(branchName) {
	logger.debug(`Setting current branch to ${branchName}`);
	await syncGit();
	try {
		await gitRetry(() => git.checkout(submodulesInitizialized ? [
			"-f",
			"--recurse-submodules",
			branchName,
			"--"
		] : [
			"-f",
			branchName,
			"--"
		]));
		config.currentBranch = branchName;
		config.currentBranchSha = (await git.raw(["rev-parse", "HEAD"])).trim();
		const latestCommitDate = await getCommitDate(config.currentBranchSha);
		// v8 ignore else -- TODO: add test #40625
		if (latestCommitDate) logger.debug({
			branchName,
			latestCommitDate,
			sha: config.currentBranchSha
		}, "latest commit");
		await git.reset(ResetMode.HARD);
		return config.currentBranchSha;
	} catch (err) 	/* v8 ignore next -- TODO: add test #40625 */ {
		const errChecked = checkForPlatformFailure(err);
		if (errChecked) throw errChecked;
		if (err.message?.includes("fatal: ambiguous argument")) {
			logger.warn({ err }, "Failed to checkout branch");
			throw new Error(TEMPORARY_ERROR);
		}
		throw err;
	}
}
async function checkoutBranchFromRemote(branchName, remoteName) {
	logger.debug(`Checking out branch ${branchName} from remote ${remoteName}`);
	await syncGit();
	try {
		await gitRetry(() => git.checkoutBranch(branchName, `${remoteName}/${branchName}`));
		config.currentBranch = branchName;
		config.currentBranchSha = (await git.revparse("HEAD")).trim();
		logger.debug(`Checked out branch ${branchName} from remote ${remoteName}`);
		config.branchCommits[branchName] = config.currentBranchSha;
		return config.currentBranchSha;
	} catch (err) {
		const errChecked = checkForPlatformFailure(err);
		/* v8 ignore if -- TODO: add test #40625 */
		if (errChecked) throw errChecked;
		if (err.message?.includes("fatal: ambiguous argument")) {
			logger.warn({ err }, "Failed to checkout branch");
			throw new Error(TEMPORARY_ERROR);
		}
		throw err;
	}
}
async function resetHardFromRemote(remoteAndBranch) {
	try {
		const resetLog = await git.reset(["--hard", remoteAndBranch]);
		logger.debug({ resetLog }, "git reset log");
	} catch (err) {
		logger.error({ err }, "Error during git reset --hard");
		throw err;
	}
}
async function forcePushToRemote(branchName, remote) {
	try {
		const pushLog = await git.push([
			remote,
			branchName,
			"--force"
		]);
		logger.debug({ pushLog }, "git push log");
	} catch (err) {
		logger.error({ err }, "Error during git push --force");
		throw err;
	}
}
async function getFileList() {
	await syncGit();
	const branch = config.currentBranch;
	let files;
	try {
		files = await git.raw([
			"ls-tree",
			"-r",
			`refs/heads/${branch}`
		]);
	} catch (err) 	/* v8 ignore next -- TODO: add test #40625 */ {
		if (err.message?.includes("fatal: Not a valid object name")) {
			logger.debug({ err }, "Branch not found when checking branch list - aborting");
			throw new Error(REPOSITORY_CHANGED);
		}
		throw err;
	}
	/* v8 ignore if -- TODO: add test #40625 */
	if (!files) return [];
	return files.split(newlineRegex).filter(isString).filter((line) => line.startsWith("100")).map((line) => line.split(regEx(/\t/)).pop());
}
function getBranchList() {
	return Object.keys(config.branchCommits ?? {});
}
async function isBranchBehindBase(branchName, baseBranch) {
	const baseBranchSha = getBranchCommit(baseBranch);
	const branchSha = getBranchCommit(branchName);
	let isBehind = getCachedBehindBaseResult(branchName, branchSha, baseBranch, baseBranchSha);
	if (isBehind !== null) {
		logger.debug(`branch.isBehindBase(): using cached result "${isBehind}"`);
		return isBehind;
	}
	logger.debug(`branch.isBehindBase(): using git to calculate against baseBranch "${baseBranch}"`);
	await syncGit();
	try {
		isBehind = (await git.raw([
			"rev-list",
			"--count",
			`${branchSha}..${baseBranchSha}`
		])).trim() !== "0";
		logger.debug({
			baseBranch,
			branchName
		}, `branch.isBehindBase(): ${isBehind}`);
		setCachedBehindBaseResult(branchName, isBehind);
		return isBehind;
	} catch (err) 	/* v8 ignore next -- TODO: add test #40625 */ {
		const errChecked = checkForPlatformFailure(err);
		if (errChecked) throw errChecked;
		throw err;
	}
}
async function isBranchModified(branchName, baseBranch) {
	if (!branchExists(branchName)) {
		logger.debug("branch.isModified(): no cache");
		return false;
	}
	if (config.branchIsModified[branchName] !== void 0) return config.branchIsModified[branchName];
	const isModified = getCachedModifiedResult(branchName, getBranchCommit(branchName));
	if (isModified !== null) {
		logger.debug(`branch.isModified(): using cached result "${isModified}"`);
		config.branchIsModified[branchName] = isModified;
		return isModified;
	}
	logger.debug(`branch.isModified(): using git to calculate against baseBranch "${baseBranch}"`);
	await syncGit();
	const committedAuthors = /* @__PURE__ */ new Set();
	try {
		const commits = await git.log([`origin/${baseBranch}..origin/${branchName}`]);
		for (const commit of commits.all) committedAuthors.add(commit.author_email);
	} catch (err) 	/* v8 ignore next -- TODO: add test #40625 */ {
		if (err.message?.includes("fatal: bad revision")) {
			logger.debug({ err }, "Remote branch not found when checking last commit author - aborting run");
			throw new Error(REPOSITORY_CHANGED);
		}
		logger.warn({ err }, "Error checking last author for isBranchModified");
	}
	const { gitAuthorEmail, ignoredAuthors } = config;
	const includedAuthors = new Set(committedAuthors);
	// v8 ignore else -- TODO: add test #40625
	if (gitAuthorEmail) includedAuthors.delete(gitAuthorEmail);
	for (const ignoredAuthor of ignoredAuthors) includedAuthors.delete(ignoredAuthor);
	if (includedAuthors.size === 0) {
		logger.trace({
			branchName,
			baseBranch,
			committedAuthors: [...committedAuthors],
			includedAuthors: [...includedAuthors],
			gitAuthorEmail,
			ignoredAuthors
		}, "branch.isModified() = false");
		logger.debug("branch.isModified() = false");
		config.branchIsModified[branchName] = false;
		setCachedModifiedResult(branchName, false);
		return false;
	}
	logger.trace({
		branchName,
		baseBranch,
		committedAuthors: [...committedAuthors],
		includedAuthors: [...includedAuthors],
		gitAuthorEmail,
		ignoredAuthors
	}, "branch.isModified() = true");
	logger.debug({
		baseBranch,
		branchName,
		unrecognizedAuthors: [...includedAuthors]
	}, "branch.isModified() = true");
	config.branchIsModified[branchName] = true;
	setCachedModifiedResult(branchName, true);
	return true;
}
async function isBranchConflicted(baseBranch, branch) {
	logger.debug(`isBranchConflicted(${baseBranch}, ${branch})`);
	const baseBranchSha = getBranchCommit(baseBranch);
	const branchSha = getBranchCommit(branch);
	if (!baseBranchSha || !branchSha) {
		logger.warn({
			baseBranch,
			branch
		}, "isBranchConflicted: branch does not exist");
		return true;
	}
	const isConflicted = getCachedConflictResult(branch, branchSha, baseBranch, baseBranchSha);
	if (isBoolean(isConflicted)) {
		logger.debug(`branch.isConflicted(): using cached result "${isConflicted}"`);
		return isConflicted;
	}
	logger.debug(`branch.isConflicted(): using git to calculate against baseBranch ${baseBranch}`);
	let result = false;
	await syncGit();
	await writeGitAuthor();
	const origBranch = config.currentBranch;
	try {
		await git.reset(ResetMode.HARD);
		if (origBranch !== baseBranch) await git.checkout(baseBranch);
		await git.merge([
			"--no-commit",
			"--no-ff",
			`origin/${branch}`
		]);
	} catch (err) {
		result = true;
		/* v8 ignore if -- TODO: add test #40625 */
		if (!err?.git?.conflicts?.length) logger.debug({
			baseBranch,
			branch,
			err
		}, "isBranchConflicted: unknown error");
	} finally {
		try {
			await git.merge(["--abort"]);
			if (origBranch !== baseBranch) await git.checkout(origBranch);
		} catch (err) 		/* v8 ignore next -- TODO: add test #40625 */ {
			logger.debug({
				baseBranch,
				branch,
				err
			}, "isBranchConflicted: cleanup error");
		}
	}
	setCachedConflictResult(branch, result);
	logger.debug(`branch.isConflicted(): ${result}`);
	return result;
}
async function deleteBranch(branchName) {
	await syncGit();
	try {
		const deleteCommand = [
			"push",
			"--delete",
			"origin",
			branchName
		];
		if (getNoVerify().includes("push")) deleteCommand.push("--no-verify");
		await gitRetry(() => git.raw(deleteCommand));
		logger.debug(`Deleted remote branch: ${branchName}`);
	} catch (err) {
		const errChecked = checkForPlatformFailure(err);
		/* v8 ignore if -- TODO: add test #40625 */
		if (errChecked) throw errChecked;
		logger.debug(`No remote branch to delete with name: ${branchName}`);
	}
	try {
		await deleteLocalBranch(branchName);
		/* v8 ignore next -- TODO: add test #40625 (always throws) */
		logger.debug(`Deleted local branch: ${branchName}`);
	} catch (err) {
		const errChecked = checkForPlatformFailure(err);
		/* v8 ignore if -- TODO: add test #40625 */
		if (errChecked) throw errChecked;
		logger.debug(`No local branch to delete with name: ${branchName}`);
	}
	delete config.branchCommits[branchName];
}
async function mergeToLocal(refSpecToMerge) {
	let status;
	try {
		await syncGit();
		await writeGitAuthor();
		await git.reset(ResetMode.HARD);
		await gitRetry(() => git.checkout([
			"-B",
			config.currentBranch,
			"origin/" + config.currentBranch
		]));
		status = await git.status();
		await fetchRevSpec(refSpecToMerge);
		await gitRetry(() => git.merge(["FETCH_HEAD"]));
	} catch (err) {
		logger.debug({
			baseBranch: config.currentBranch,
			baseSha: config.currentBranchSha,
			refSpecToMerge,
			status,
			err
		}, "mergeLocally error");
		throw err;
	}
}
async function mergeBranch(branchName) {
	let status;
	try {
		await syncGit();
		await writeGitAuthor();
		await git.reset(ResetMode.HARD);
		await gitRetry(() => git.checkout([
			"-B",
			branchName,
			"origin/" + branchName
		]));
		await gitRetry(() => git.checkout([
			"-B",
			config.currentBranch,
			"origin/" + config.currentBranch
		]));
		status = await git.status();
		await gitRetry(() => git.merge(["--ff-only", branchName]));
		await gitRetry(() => git.push("origin", config.currentBranch));
		incLimitedValue("Commits");
	} catch (err) {
		logger.debug({
			baseBranch: config.currentBranch,
			baseSha: config.currentBranchSha,
			branchName,
			branchSha: getBranchCommit(branchName),
			status,
			err
		}, "mergeBranch error");
		throw err;
	}
}
async function getCommitDate(ref) {
	const output = await git.show([
		"-s",
		"--format=%cI",
		ref
	]);
	return DateTime.fromISO(output.trim()).toUTC();
}
async function getBranchLastCommitTime(branchName) {
	await syncGit();
	try {
		return (await getCommitDate("origin/" + branchName)).toJSDate();
	} catch (err) {
		const errChecked = checkForPlatformFailure(err);
		/* v8 ignore next 3 -- TODO: add test */
		if (errChecked) throw errChecked;
		return /* @__PURE__ */ new Date();
	}
}
function getBranchFiles(branchName) {
	return getBranchFilesFromRef(`origin/${branchName}`);
}
function getBranchFilesFromCommit(referenceCommit) {
	return getBranchFilesFromRef(referenceCommit);
}
async function getBranchFilesFromRef(refName) {
	await syncGit();
	try {
		return (await gitRetry(() => git.diffSummary([refName, `${refName}^`]))).files.map((file) => file.file);
	} catch (err) 	/* v8 ignore next -- TODO: add test #40625 */ {
		logger.warn({ err }, "getBranchFilesFromRef error");
		const errChecked = checkForPlatformFailure(err);
		if (errChecked) throw errChecked;
		return null;
	}
}
async function getFile(filePath, branchName) {
	await syncGit();
	try {
		const content = await git.show(["origin/" + (branchName ?? config.currentBranch) + ":" + filePath]);
		logWarningIfUnicodeHiddenCharactersInPackageFile(filePath, content);
		return content;
	} catch (err) {
		const errChecked = checkForPlatformFailure(err);
		/* v8 ignore if -- TODO: add test #40625 */
		if (errChecked) throw errChecked;
		return null;
	}
}
async function getFiles(fileNames) {
	const fileContentMap = {};
	for (const fileName of fileNames) fileContentMap[fileName] = await getFile(fileName);
	return fileContentMap;
}
async function hasDiff(sourceRef, targetRef) {
	await syncGit();
	try {
		return await gitRetry(() => git.diff([
			sourceRef,
			targetRef,
			"--"
		])) !== "";
	} catch {
		return true;
	}
}
async function handleCommitAuth(localDir) {
	if (!privateKeySet) {
		await writePrivateKey();
		privateKeySet = true;
	}
	await configSigningKey(localDir);
	await writeGitAuthor();
}
/**
*
* Prepare local branch with commit
*
* 0. Hard reset
* 1. Creates local branch with `origin/` prefix
* 2. Perform `git add` (respecting mode) and `git remove` for each file
* 3. Perform commit
* 4. Check whether resulting commit is empty or not (due to .gitignore)
* 5. If not empty, return commit info for further processing
*
*/
async function prepareCommit({ branchName, files, message, force = false }) {
	const localDir = GlobalConfig.get("localDir");
	await syncGit();
	logger.debug(`Preparing files for committing to branch ${branchName}`);
	await handleCommitAuth(localDir);
	try {
		await git.reset(ResetMode.HARD);
		await git.raw(["clean", "-fd"]);
		const parentCommitSha = config.currentBranchSha;
		await gitRetry(() => git.checkout([
			"-B",
			branchName,
			"origin/" + config.currentBranch
		]));
		const deletedFiles = [];
		const addedModifiedFiles = [];
		const ignoredFiles = [];
		for (const file of files) {
			const fileName = file.path;
			if (file.type === "deletion") try {
				await git.rm([fileName]);
				deletedFiles.push(fileName);
			} catch (err) 			/* v8 ignore next -- TODO: add test #40625 */ {
				const errChecked = checkForPlatformFailure(err);
				if (errChecked) throw errChecked;
				logger.trace({
					err,
					fileName
				}, "Cannot delete file");
				ignoredFiles.push(fileName);
			}
			else {
				if (await isDirectory(upath.join(localDir, fileName))) logger.trace({ fileName }, "Adding directory commit");
				else if (file.contents === null) continue;
				else {
					let contents;
					/* v8 ignore else -- TODO: add test #40625 */
					if (typeof file.contents === "string") contents = Buffer.from(file.contents);
					else contents = file.contents;
					if (file.isSymlink) await fs.symlink(file.contents, upath.join(localDir, fileName));
					else await fs.outputFile(upath.join(localDir, fileName), contents, { mode: file.isExecutable ? 511 : 438 });
				}
				try {
					/* v8 ignore next -- TODO: add test #40625 */
					const addParams = fileName === getConfigFileNames()[0] ? ["-f", fileName] : fileName;
					await git.add(addParams);
					if (file.isExecutable) await git.raw([
						"update-index",
						"--chmod=+x",
						fileName
					]);
					addedModifiedFiles.push(fileName);
				} catch (err) 				/* v8 ignore next -- TODO: add test #40625 */ {
					if (!err.message.includes("The following paths are ignored by one of your .gitignore files")) throw err;
					logger.debug(`Cannot commit ignored file: ${fileName}`);
					ignoredFiles.push(file.path);
				}
			}
		}
		const commitOptions = {};
		if (getNoVerify().includes("commit")) commitOptions["--no-verify"] = null;
		const commitRes = await git.commit(message, [], commitOptions);
		if (isNonEmptyObject(commitRes.summary) && commitRes.summary.changes === 0 && commitRes.summary.insertions === 0 && commitRes.summary.deletions === 0) {
			logger.warn({ commitRes }, "Detected empty commit - aborting git push");
			return null;
		}
		logger.debug({
			deletedFiles,
			ignoredFiles,
			result: commitRes
		}, `git commit`);
		if (!force && !await hasDiff("HEAD", `origin/${branchName}`)) {
			logger.debug({
				branchName,
				deletedFiles,
				addedModifiedFiles,
				ignoredFiles
			}, "No file changes detected. Skipping commit");
			return null;
		}
		return {
			parentCommitSha,
			commitSha: (await git.revparse([branchName])).trim(),
			files: files.filter((fileChange) => {
				if (fileChange.type === "deletion") return deletedFiles.includes(fileChange.path);
				return addedModifiedFiles.includes(fileChange.path);
			})
		};
	} catch (err) 	/* v8 ignore next -- TODO: add test #40625 */ {
		return handleCommitError(err, branchName, files);
	}
}
async function pushCommit({ sourceRef, targetRef, files, pushOptions }) {
	await syncGit();
	logger.debug(`Pushing refSpec ${sourceRef}:${targetRef ?? sourceRef}`);
	let result = false;
	try {
		const gitOptions = {
			"--force-with-lease": null,
			"-u": null
		};
		if (getNoVerify().includes("push")) gitOptions["--no-verify"] = null;
		if (pushOptions) gitOptions["--push-option"] = pushOptions;
		const pushRes = await gitRetry(() => git.push("origin", `${sourceRef}:${targetRef ?? sourceRef}`, gitOptions));
		delete pushRes.repo;
		logger.debug({ result: pushRes }, "git push");
		incLimitedValue("Commits");
		incCountValue("HourlyCommits");
		result = true;
	} catch (err) 	/* v8 ignore next -- TODO: add test #40625 */ {
		handleCommitError(err, sourceRef, files);
	}
	return result;
}
async function fetchBranch(branchName) {
	await syncGit();
	logger.debug(`Fetching branch ${branchName}`);
	try {
		const ref = `refs/heads/${branchName}:refs/remotes/origin/${branchName}`;
		await gitRetry(() => git.pull([
			"origin",
			ref,
			"--force"
		]));
		const commit = (await git.revparse([branchName])).trim();
		config.branchCommits[branchName] = commit;
		config.branchIsModified[branchName] = false;
		return commit;
	} catch (err) 	/* v8 ignore next -- TODO: add test #40625 */ {
		return handleCommitError(err, branchName);
	}
}
async function commitFiles(commitConfig) {
	try {
		const commitResult = await prepareCommit(commitConfig);
		if (commitResult) {
			// v8 ignore else -- TODO: add test #40625
			if (await pushCommit({
				sourceRef: commitConfig.branchName,
				files: commitConfig.files
			})) {
				const { branchName } = commitConfig;
				const { commitSha } = commitResult;
				config.branchCommits[branchName] = commitSha;
				config.branchIsModified[branchName] = false;
				return commitSha;
			}
		}
		return null;
	} catch (err) 	/* v8 ignore next -- TODO: add test #40625 */ {
		if (err.message.includes("[rejected] (stale info)")) throw new Error(REPOSITORY_CHANGED);
		throw err;
	}
}
function getUrl({ protocol, auth, hostname, host, repository }) {
	if (protocol === "ssh") return `git@${hostname}:${repository}.git`;
	return URL.format({
		protocol: protocol ?? "https",
		auth,
		hostname,
		host,
		pathname: repository + ".git"
	});
}
let remoteRefsExist = false;
/**
*
* Non-branch refs allow us to store git objects without triggering CI pipelines.
* It's useful for API-based branch rebasing.
*
* @see https://stackoverflow.com/questions/63866947/pushing-git-non-branch-references-to-a-remote/63868286
*
*/
async function pushCommitToRenovateRef(commitSha, refName) {
	const fullRefName = `refs/renovate/branches/${refName}`;
	await git.raw([
		"update-ref",
		fullRefName,
		commitSha
	]);
	await git.push([
		"--force",
		"origin",
		fullRefName
	]);
	remoteRefsExist = true;
}
/**
*
* Removes all remote "refs/renovate/branches/*" refs in two steps:
*
* Step 1: list refs
*
*   $ git ls-remote origin "refs/renovate/branches/*"
*
*   > cca38e9ea6d10946bdb2d0ca5a52c205783897aa        refs/renovate/branches/foo
*   > 29ac154936c880068994e17eb7f12da7fdca70e5        refs/renovate/branches/bar
*   > 3fafaddc339894b6d4f97595940fd91af71d0355        refs/renovate/branches/baz
*   > ...
*
* Step 2:
*
*   $ git push --delete origin refs/renovate/branches/foo refs/renovate/branches/bar refs/renovate/branches/baz
*
* If Step 2 fails because the repo doesn't allow bulk changes, we'll remove them one by one instead:
*
*   $ git push --delete origin refs/renovate/branches/foo
*   $ git push --delete origin refs/renovate/branches/bar
*   $ git push --delete origin refs/renovate/branches/baz
*/
async function clearRenovateRefs() {
	if (!gitInitialized || !remoteRefsExist) return;
	logger.debug(`Cleaning up Renovate refs: refs/renovate/branches/*`);
	const renovateRefs = [];
	try {
		const refs = (await git.listRemote([config.url, "refs/renovate/branches/*"])).split(newlineRegex).map((line) => line.replace(regEx(/[0-9a-f]+\s+/i), "").trim()).filter((line) => line.startsWith("refs/renovate/branches/"));
		renovateRefs.push(...refs);
	} catch (err) 	/* v8 ignore next -- TODO: add test #40625 */ {
		logger.warn({ err }, `Renovate refs cleanup error`);
	}
	// v8 ignore else -- TODO: add test #40625
	if (renovateRefs.length) try {
		const pushOpts = [
			"--delete",
			"origin",
			...renovateRefs
		];
		await git.push(pushOpts);
	} catch (err) {
		/* v8 ignore else -- TODO: add test #40625 */
		if (bulkChangesDisallowed(err)) for (const ref of renovateRefs) try {
			const pushOpts = [
				"--delete",
				"origin",
				ref
			];
			await git.push(pushOpts);
		} catch (err) 		/* v8 ignore next -- TODO: add test #40625 */ {
			logger.debug({ err }, "Error deleting \"refs/renovate/branches/*\"");
			break;
		}
		else logger.warn({ err }, "Error deleting \"refs/renovate/branches/*\"");
	}
	remoteRefsExist = false;
}
const treeItemRegex = regEx(/^(?<mode>\d{6})\s+(?<type>blob|tree|commit)\s+(?<sha>[0-9a-f]{40})\s+(?<path>.*)$/);
const treeShaRegex = regEx(/tree\s+(?<treeSha>[0-9a-f]{40})\s*/);
/**
*
* Obtain top-level items of commit tree.
* We don't need subtree items, so here are 2 steps only.
*
* Step 1: commit SHA -> tree SHA
*
*   $ git cat-file -p <commit-sha>
*
*   > tree <tree-sha>
*   > parent 59b8b0e79319b7dc38f7a29d618628f3b44c2fd7
*   > ...
*
* Step 2: tree SHA -> tree items (top-level)
*
*   $ git cat-file -p <tree-sha>
*
*   > 040000 tree 389400684d1f004960addc752be13097fe85d776    src
*   > ...
*   > 100644 blob 7d2edde437ad4e7bceb70dbfe70e93350d99c98b    package.json
*
*/
async function listCommitTree(commitSha) {
	const commitOutput = await git.catFile(["-p", commitSha]);
	/* v8 ignore next -- will never happen */
	const { treeSha } = treeShaRegex.exec(commitOutput)?.groups ?? {};
	const lines = (await git.catFile(["-p", treeSha])).split(newlineRegex);
	const result = [];
	for (const line of lines) {
		const matchGroups = treeItemRegex.exec(line)?.groups;
		if (matchGroups) {
			const { path, mode, type, sha } = matchGroups;
			result.push({
				path,
				mode,
				type,
				sha
			});
		}
	}
	return result;
}
async function localBranchExists(branchName) {
	await syncGit();
	return (await git.branchLocal()).all.includes(branchName);
}
/**
* Synchronize a forked branch with its upstream counterpart.
*
* syncForkWithUpstream updates the fork's branch, to match the corresponding branch in the upstream repository.
* The steps are:
* 1. Check if the branch exists locally.
* 2. If the branch exists locally: checkout the local branch.
* 3. If the branch does _not_ exist locally: checkout the upstream branch.
* 4. Reset the local branch to match the upstream branch.
* 5. Force push the (updated) local branch to the origin repository.
*
* @param {string} branchName - The name of the branch to synchronize.
* @returns A promise that resolves to True if the synchronization is successful, or `false` if an error occurs.
*/
async function syncForkWithUpstream(branchName) {
	if (!config.upstreamUrl) return;
	logger.debug(`Synchronizing fork with "${RENOVATE_FORK_UPSTREAM}" remote for branch ${branchName}`);
	/* v8 ignore if -- this should not be possible if upstreamUrl exists */
	if (!(await getRemotes()).some((r) => r === "renovate-fork-upstream")) throw new Error("No upstream remote exists, cannot sync fork");
	try {
		await git.fetch([RENOVATE_FORK_UPSTREAM]);
		if (await localBranchExists(branchName)) await checkoutBranch(branchName);
		else await checkoutBranchFromRemote(branchName, RENOVATE_FORK_UPSTREAM);
		await resetHardFromRemote(`${RENOVATE_FORK_UPSTREAM}/${branchName}`);
		await forcePushToRemote(branchName, "origin");
	} catch (err) 	/* v8 ignore next -- shouldn't happen */ {
		logger.error({ err }, "Error synchronizing fork");
		throw new Error(UNKNOWN_ERROR);
	}
}
async function getRemotes() {
	logger.debug("git.getRemotes()");
	try {
		await syncGit();
		const remotes = await git.getRemotes();
		logger.debug(`Found remotes: ${remotes.map((r) => r.name).join(", ")}`);
		return remotes.map((remote) => remote.name);
	} catch (err) 	/* v8 ignore next */ {
		logger.error({ err }, "Error getting remotes");
		throw err;
	}
}
//#endregion
export { GIT_MINIMUM_VERSION, RENOVATE_FORK_UPSTREAM, branchExists, checkoutBranch, checkoutBranchFromRemote, clearRenovateRefs, cloneSubmodules, commitFiles, deleteBranch, fetchBranch, fetchRevSpec, forcePushToRemote, getBranchCommit, getBranchFiles, getBranchFilesFromCommit, getBranchLastCommitTime, getBranchList, getBranchUpdateDate, getCommitMessages, getFile, getFileList, getFiles, getRemotes, getRepoStatus, getSubmodules, getUrl, gitRetry, hasDiff, initRepo, isBranchBehindBase, isBranchConflicted, isBranchModified, isCloned, listCommitTree, mergeBranch, mergeToLocal, prepareCommit, pushCommit, pushCommitToRenovateRef, resetHardFromRemote, resetToCommit, setGitAuthor, setNoVerify, setPrivateKey, setUserRepoConfig, syncForkWithUpstream, syncGit, validateGitVersion, writeGitAuthor };

//# sourceMappingURL=index.js.map