renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
1,294 lines • 50.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GIT_MINIMUM_VERSION = exports.RENOVATE_FORK_UPSTREAM = exports.setPrivateKey = exports.setNoVerify = void 0;
exports.gitRetry = gitRetry;
exports.validateGitVersion = validateGitVersion;
exports.fetchRevSpec = fetchRevSpec;
exports.initRepo = initRepo;
exports.resetToCommit = resetToCommit;
exports.setGitAuthor = setGitAuthor;
exports.writeGitAuthor = writeGitAuthor;
exports.setUserRepoConfig = setUserRepoConfig;
exports.getSubmodules = getSubmodules;
exports.cloneSubmodules = cloneSubmodules;
exports.isCloned = isCloned;
exports.syncGit = syncGit;
exports.getRepoStatus = getRepoStatus;
exports.branchExists = branchExists;
exports.getBranchCommit = getBranchCommit;
exports.getCommitMessages = getCommitMessages;
exports.checkoutBranch = checkoutBranch;
exports.checkoutBranchFromRemote = checkoutBranchFromRemote;
exports.resetHardFromRemote = resetHardFromRemote;
exports.forcePushToRemote = forcePushToRemote;
exports.getFileList = getFileList;
exports.getBranchList = getBranchList;
exports.isBranchBehindBase = isBranchBehindBase;
exports.isBranchModified = isBranchModified;
exports.isBranchConflicted = isBranchConflicted;
exports.deleteBranch = deleteBranch;
exports.mergeToLocal = mergeToLocal;
exports.mergeBranch = mergeBranch;
exports.getBranchLastCommitTime = getBranchLastCommitTime;
exports.getBranchFiles = getBranchFiles;
exports.getFile = getFile;
exports.getFiles = getFiles;
exports.hasDiff = hasDiff;
exports.prepareCommit = prepareCommit;
exports.pushCommit = pushCommit;
exports.fetchBranch = fetchBranch;
exports.commitFiles = commitFiles;
exports.getUrl = getUrl;
exports.pushCommitToRenovateRef = pushCommitToRenovateRef;
exports.clearRenovateRefs = clearRenovateRefs;
exports.listCommitTree = listCommitTree;
exports.syncForkWithUpstream = syncForkWithUpstream;
exports.getRemotes = getRemotes;
const tslib_1 = require("tslib");
const node_url_1 = tslib_1.__importDefault(require("node:url"));
const promises_1 = require("timers/promises");
const is_1 = tslib_1.__importDefault(require("@sindresorhus/is"));
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
const semver_1 = tslib_1.__importDefault(require("semver"));
const simple_git_1 = require("simple-git");
const upath_1 = tslib_1.__importDefault(require("upath"));
const app_strings_1 = require("../../config/app-strings");
const global_1 = require("../../config/global");
const error_messages_1 = require("../../constants/error-messages");
const logger_1 = require("../../logger");
const external_host_error_1 = require("../../types/errors/external-host-error");
const limits_1 = require("../../workers/global/limits");
const repository_1 = require("../cache/repository");
const env_1 = require("../env");
const regex_1 = require("../regex");
const string_match_1 = require("../string-match");
const author_1 = require("./author");
const behind_base_branch_cache_1 = require("./behind-base-branch-cache");
const config_1 = require("./config");
const conflicts_cache_1 = require("./conflicts-cache");
const error_1 = require("./error");
const modified_cache_1 = require("./modified-cache");
const private_key_1 = require("./private-key");
var config_2 = require("./config");
Object.defineProperty(exports, "setNoVerify", { enumerable: true, get: function () { return config_2.setNoVerify; } });
var private_key_2 = require("./private-key");
Object.defineProperty(exports, "setPrivateKey", { enumerable: true, get: function () { return private_key_2.setPrivateKey; } });
// Retry parameters
const retryCount = 5;
const delaySeconds = 3;
const delayFactor = 2;
exports.RENOVATE_FORK_UPSTREAM = 'renovate-fork-upstream';
// A generic wrapper for simpleGit.* calls to make them more fault-tolerant
async function gitRetry(gitFunc) {
let round = 0;
let lastError;
while (round <= retryCount) {
if (round > 0) {
logger_1.logger.debug(`gitRetry round ${round}`);
}
try {
const res = await gitFunc();
if (round > 1) {
logger_1.logger.debug('Successful retry of git function');
}
return res;
}
catch (err) {
lastError = err;
logger_1.logger.debug({ err }, `Git function thrown`);
// Try to transform the Error to ExternalHostError
const errChecked = (0, error_1.checkForPlatformFailure)(err);
if (errChecked instanceof external_host_error_1.ExternalHostError) {
logger_1.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_1.logger.trace({ nextDelay }, `Delay next round`);
await (0, promises_1.setTimeout)(1000 * nextDelay);
round++;
}
// Can't be `undefined` here.
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw lastError;
}
async function isDirectory(dir) {
try {
return (await fs_extra_1.default.stat(dir)).isDirectory();
}
catch {
return false;
}
}
async function getDefaultBranch(git) {
logger_1.logger.debug('getDefaultBranch()');
// see https://stackoverflow.com/a/62352647/3005034
try {
let res = await git.raw(['rev-parse', '--abbrev-ref', 'origin/HEAD']);
/* v8 ignore start -- TODO: add test */
if (!res) {
logger_1.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, '');
}
/* v8 ignore stop */
return res.replace('origin/', '').trim();
/* v8 ignore start -- TODO: add test */
}
catch (err) {
logger_1.logger.debug({ err }, 'Error getting default branch');
const errChecked = (0, error_1.checkForPlatformFailure)(err);
if (errChecked) {
throw errChecked;
}
if (err.message.startsWith('fatal: ref refs/remotes/origin/HEAD is not a symbolic ref')) {
throw new Error(error_messages_1.REPOSITORY_EMPTY);
}
if (err.message.includes("fatal: ambiguous argument 'origin/HEAD'")) {
logger_1.logger.warn('Error getting default branch');
throw new Error(error_messages_1.TEMPORARY_ERROR);
}
throw err;
}
/* v8 ignore stop */
}
let config = {};
// TODO: can be undefined
let git;
let gitInitialized;
let submodulesInitizialized;
let privateKeySet = false;
exports.GIT_MINIMUM_VERSION = '2.33.0'; // git show-current
async function validateGitVersion() {
let version;
const globalGit = (0, simple_git_1.simpleGit)();
try {
const { major, minor, patch, installed } = await globalGit.version();
/* v8 ignore next 4 -- TODO: add test */
if (!installed) {
logger_1.logger.error('Git not installed');
return false;
}
version = `${major}.${minor}.${patch}`;
/* v8 ignore next 4 */
}
catch (err) {
logger_1.logger.error({ err }, 'Error fetching git version');
return false;
}
/* v8 ignore next 7 -- TODO: add test */
if (!(version && semver_1.default.gte(version, exports.GIT_MINIMUM_VERSION))) {
logger_1.logger.error({ detectedVersion: version, minimumVersion: exports.GIT_MINIMUM_VERSION }, 'Git version needs upgrading');
return false;
}
logger_1.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_1.logger.debug(`fetchBranchCommits(): url=${url}`);
const opts = ['ls-remote', '--heads', url];
if (config.extraCloneOpts) {
Object.entries(config.extraCloneOpts).forEach((e) =>
// TODO: types (#22198)
opts.unshift(e[0], `${e[1]}`));
}
try {
const lsRemoteRes = await gitRetry(() => git.raw(opts));
logger_1.logger.trace({ lsRemoteRes }, 'git ls-remote result');
lsRemoteRes
.split(regex_1.newlineRegex)
.filter(Boolean)
.map((line) => line.trim().split((0, regex_1.regEx)(/\s+/)))
.forEach(([sha, ref]) => {
config.branchCommits[ref.replace('refs/heads/', '')] =
sha;
});
logger_1.logger.trace({ branchCommits: config.branchCommits }, 'branch commits');
/* v8 ignore next 11 -- TODO: add test */
}
catch (err) {
const errChecked = (0, error_1.checkForPlatformFailure)(err);
if (errChecked) {
throw errChecked;
}
logger_1.logger.debug({ err }, 'git error');
if (err.message?.includes('Please ask the owner to check their account')) {
throw new Error(error_messages_1.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 = {};
// TODO: safe to pass all env variables? use `getChildEnv` instead?
git = (0, simple_git_1.simpleGit)(global_1.GlobalConfig.get('localDir'), (0, config_1.simpleGitConfig)()).env({
...(0, env_1.getEnv)(),
LANG: 'C.UTF-8',
LC_ALL: 'C.UTF-8',
});
gitInitialized = false;
submodulesInitizialized = false;
await fetchBranchCommits();
}
async function resetToBranch(branchName) {
logger_1.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 4 -- TODO: add test */
async function resetToCommit(commit) {
logger_1.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(regex_1.newlineRegex)
.map((branch) => branch.trim())
.filter((branch) => branch.length > 0 && !branch.startsWith('* '));
logger_1.logger.debug({ existingBranches });
for (const branchName of existingBranches) {
await deleteLocalBranch(branchName);
}
}
function setGitAuthor(gitAuthor) {
const gitAuthorParsed = (0, author_1.parseGitAuthor)(gitAuthor ?? 'Renovate Bot <renovate@whitesourcesoftware.com>');
if (!gitAuthorParsed) {
const error = new Error(error_messages_1.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 next 3 -- TODO: add test */
if (writeGitDone) {
return;
}
config.writeGitDone = true;
try {
if (gitAuthorName) {
logger_1.logger.debug(`Setting git author name: ${gitAuthorName}`);
await git.addConfig('user.name', gitAuthorName);
}
if (gitAuthorEmail) {
logger_1.logger.debug(`Setting git author email: ${gitAuthorEmail}`);
await git.addConfig('user.email', gitAuthorEmail);
}
/* v8 ignore next 11 -- TODO: add test */
}
catch (err) {
const errChecked = (0, error_1.checkForPlatformFailure)(err);
if (errChecked) {
throw errChecked;
}
logger_1.logger.debug({ err, gitAuthorName, gitAuthorEmail }, 'Error setting git author config');
throw new Error(error_messages_1.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((0, regex_1.regEx)(/[\n\s]/))
.filter((_e, i) => i % 2);
/* v8 ignore next 4 -- TODO: add test */
}
catch (err) {
logger_1.logger.warn({ err }, 'Error getting submodules');
return [];
}
}
async function cloneSubmodules(shouldClone, cloneSubmodulesFilter) {
if (!shouldClone || submodulesInitizialized) {
return;
}
submodulesInitizialized = true;
await syncGit();
const submodules = await getSubmodules();
for (const submodule of submodules) {
if (!(0, string_match_1.matchRegexOrGlobList)(submodule, cloneSubmodulesFilter ?? ['*'])) {
logger_1.logger.debug({ cloneSubmodulesFilter }, `Skipping submodule ${submodule}`);
continue;
}
try {
logger_1.logger.debug(`Cloning git submodule at ${submodule}`);
await gitRetry(() => git.submoduleUpdate(['--init', '--recursive', submodule]));
}
catch (err) {
logger_1.logger.warn({ err, submodule }, `Unable to initialise git submodule`);
}
}
}
function isCloned() {
return gitInitialized;
}
async function syncGit() {
if (gitInitialized) {
/* v8 ignore next 3 -- TODO: add test */
if ((0, env_1.getEnv)().RENOVATE_X_CLEAR_HOOKS) {
await git.raw(['config', 'core.hooksPath', '/dev/null']);
}
return;
}
/* v8 ignore next 3 -- failsafe TODO: add test */
if (global_1.GlobalConfig.get('platform') === 'local') {
throw new Error('Cannot sync git when platform=local');
}
gitInitialized = true;
const localDir = global_1.GlobalConfig.get('localDir');
logger_1.logger.debug(`syncGit(): Initializing git repository into ${localDir}`);
const gitHead = upath_1.default.join(localDir, '.git/HEAD');
let clone = true;
if (await fs_extra_1.default.pathExists(gitHead)) {
logger_1.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_1.logger.info({ durationMs }, 'git fetch completed');
clone = false;
/* v8 ignore next 6 -- TODO: add test */
}
catch (err) {
if (err.message === error_messages_1.REPOSITORY_EMPTY) {
throw err;
}
logger_1.logger.info({ err }, 'git fetch error, falling back to git clone');
}
}
if (clone) {
const cloneStart = Date.now();
try {
const opts = [];
if (config.defaultBranch) {
opts.push('-b', config.defaultBranch);
}
if (config.fullClone) {
logger_1.logger.debug('Performing full clone');
}
else {
logger_1.logger.debug('Performing blobless clone');
opts.push('--filter=blob:none');
}
if (config.extraCloneOpts) {
Object.entries(config.extraCloneOpts).forEach((e) =>
// TODO: types (#22198)
opts.push(e[0], `${e[1]}`));
}
const emptyDirAndClone = async () => {
await fs_extra_1.default.emptyDir(localDir);
await git.clone(config.url, '.', opts);
};
await gitRetry(() => emptyDirAndClone());
/* v8 ignore next 10 -- TODO: add test */
}
catch (err) {
logger_1.logger.debug({ err }, 'git clone error');
if (err.message?.includes('No space left on device')) {
throw new Error(error_messages_1.SYSTEM_INSUFFICIENT_DISK_SPACE);
}
if (err.message === error_messages_1.REPOSITORY_EMPTY) {
throw err;
}
throw new external_host_error_1.ExternalHostError(err, 'git');
}
const durationMs = Math.round(Date.now() - cloneStart);
logger_1.logger.debug({ durationMs }, 'git clone completed');
}
try {
config.currentBranchSha = (await git.raw(['rev-parse', 'HEAD'])).trim();
/* v8 ignore next 6 -- TODO: add test */
}
catch (err) {
if (err.message?.includes('fatal: not a git repository')) {
throw new Error(error_messages_1.REPOSITORY_CHANGED);
}
throw err;
}
// This will only happen now if set in global config
await cloneSubmodules(!!config.cloneSubmodules, config.cloneSubmodulesFilter);
try {
const latestCommit = (await git.log({ n: 1 })).latest;
logger_1.logger.debug({ latestCommit }, 'latest repository commit');
/* v8 ignore next 10 -- TODO: add test */
}
catch (err) {
const errChecked = (0, error_1.checkForPlatformFailure)(err);
if (errChecked) {
throw errChecked;
}
if (err.message.includes('does not have any commits yet')) {
throw new Error(error_messages_1.REPOSITORY_EMPTY);
}
logger_1.logger.warn({ err }, 'Cannot retrieve latest commit');
}
config.currentBranch =
config.currentBranch ??
config.defaultBranch ??
(await getDefaultBranch(git));
/* v8 ignore next -- TODO: add test */
delete (0, repository_1.getCache)()?.semanticCommits;
// If upstreamUrl is set then the bot is running in fork mode
// The "upstream" remote is the original repository which was forked from
if (config.upstreamUrl) {
logger_1.logger.debug(`Bringing default branch up-to-date with ${exports.RENOVATE_FORK_UPSTREAM}, to get latest config`);
// Add remote if it does not exist
const remotes = await git.getRemotes(true);
if (!remotes.some((remote) => remote.name === exports.RENOVATE_FORK_UPSTREAM)) {
logger_1.logger.debug(`Adding remote ${exports.RENOVATE_FORK_UPSTREAM}`);
await git.addRemote(exports.RENOVATE_FORK_UPSTREAM, config.upstreamUrl);
}
await syncForkWithUpstream(config.currentBranch);
await fetchBranchCommits(false);
}
config.currentBranchSha = (await git.revparse('HEAD')).trim();
logger_1.logger.debug(`Current branch SHA: ${config.currentBranchSha}`);
}
async function getRepoStatus(path) {
if (is_1.default.string(path)) {
const localDir = global_1.GlobalConfig.get('localDir');
const localPath = upath_1.default.resolve(localDir, path);
if (!localPath.startsWith(upath_1.default.resolve(localDir))) {
logger_1.logger.warn({ localPath, localDir }, 'Preventing access to file outside the local directory');
throw new Error(error_messages_1.INVALID_PATH);
}
}
await syncGit();
return git.status(path ? [path] : []);
}
function branchExists(branchName) {
return !!config.branchCommits[branchName];
}
// Return the commit SHA for a branch
function getBranchCommit(branchName) {
return config.branchCommits[branchName] || null;
}
async function getCommitMessages() {
logger_1.logger.debug('getCommitMessages');
if (global_1.GlobalConfig.get('platform') !== 'local') {
await syncGit();
}
try {
const res = await git.log({
n: 20,
format: { message: '%s' },
});
return res.all.map((commit) => commit.message);
/* v8 ignore next 3 -- TODO: add test */
}
catch {
return [];
}
}
async function checkoutBranch(branchName) {
logger_1.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 git.log({ n: 1 }))?.latest?.date;
if (latestCommitDate) {
logger_1.logger.debug({ branchName, latestCommitDate, sha: config.currentBranchSha }, 'latest commit');
}
await git.reset(simple_git_1.ResetMode.HARD);
return config.currentBranchSha;
/* v8 ignore next 11 -- TODO: add test */
}
catch (err) {
const errChecked = (0, error_1.checkForPlatformFailure)(err);
if (errChecked) {
throw errChecked;
}
if (err.message?.includes('fatal: ambiguous argument')) {
logger_1.logger.warn({ err }, 'Failed to checkout branch');
throw new Error(error_messages_1.TEMPORARY_ERROR);
}
throw err;
}
}
async function checkoutBranchFromRemote(branchName, remoteName) {
logger_1.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_1.logger.debug(`Checked out branch ${branchName} from remote ${remoteName}`);
config.branchCommits[branchName] = config.currentBranchSha;
return config.currentBranchSha;
}
catch (err) {
const errChecked = (0, error_1.checkForPlatformFailure)(err);
/* v8 ignore next 3 -- hard to test */
if (errChecked) {
throw errChecked;
}
if (err.message?.includes('fatal: ambiguous argument')) {
logger_1.logger.warn({ err }, 'Failed to checkout branch');
throw new Error(error_messages_1.TEMPORARY_ERROR);
}
throw err;
}
}
async function resetHardFromRemote(remoteAndBranch) {
try {
const resetLog = await git.reset(['--hard', remoteAndBranch]);
logger_1.logger.debug({ resetLog }, 'git reset log');
}
catch (err) {
logger_1.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_1.logger.debug({ pushLog }, 'git push log');
}
catch (err) {
logger_1.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}`]);
/* v8 ignore next 10 -- TODO: add test */
}
catch (err) {
if (err.message?.includes('fatal: Not a valid object name')) {
logger_1.logger.debug({ err }, 'Branch not found when checking branch list - aborting');
throw new Error(error_messages_1.REPOSITORY_CHANGED);
}
throw err;
}
/* v8 ignore next 3 -- TODO: add test */
if (!files) {
return [];
}
// submodules are starting with `160000 commit`
return files
.split(regex_1.newlineRegex)
.filter(is_1.default.string)
.filter((line) => line.startsWith('100'))
.map((line) => line.split((0, regex_1.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 = (0, behind_base_branch_cache_1.getCachedBehindBaseResult)(branchName, branchSha, baseBranch, baseBranchSha);
if (isBehind !== null) {
logger_1.logger.debug(`branch.isBehindBase(): using cached result "${isBehind}"`);
return isBehind;
}
logger_1.logger.debug('branch.isBehindBase(): using git to calculate');
await syncGit();
try {
const behindCount = (await git.raw(['rev-list', '--count', `${branchSha}..${baseBranchSha}`])).trim();
isBehind = behindCount !== '0';
logger_1.logger.debug({ baseBranch, branchName }, `branch.isBehindBase(): ${isBehind}`);
(0, behind_base_branch_cache_1.setCachedBehindBaseResult)(branchName, isBehind);
return isBehind;
/* v8 ignore next 7 -- TODO: add test */
}
catch (err) {
const errChecked = (0, error_1.checkForPlatformFailure)(err);
if (errChecked) {
throw errChecked;
}
throw err;
}
}
async function isBranchModified(branchName, baseBranch) {
if (!branchExists(branchName)) {
logger_1.logger.debug('branch.isModified(): no cache');
return false;
}
// First check local config
if (config.branchIsModified[branchName] !== undefined) {
return config.branchIsModified[branchName];
}
// Second check repository cache
const isModified = (0, modified_cache_1.getCachedModifiedResult)(branchName, getBranchCommit(branchName));
if (isModified !== null) {
logger_1.logger.debug(`branch.isModified(): using cached result "${isModified}"`);
config.branchIsModified[branchName] = isModified;
return isModified;
}
logger_1.logger.debug('branch.isModified(): using git to calculate');
await syncGit();
const committedAuthors = new Set();
try {
const commits = await git.log([
`origin/${baseBranch}..origin/${branchName}`,
]);
for (const commit of commits.all) {
committedAuthors.add(commit.author_email);
}
/* v8 ignore next 10 -- TODO: add test */
}
catch (err) {
if (err.message?.includes('fatal: bad revision')) {
logger_1.logger.debug({ err }, 'Remote branch not found when checking last commit author - aborting run');
throw new Error(error_messages_1.REPOSITORY_CHANGED);
}
logger_1.logger.warn({ err }, 'Error checking last author for isBranchModified');
}
const { gitAuthorEmail, ignoredAuthors } = config;
const includedAuthors = new Set(committedAuthors);
if (gitAuthorEmail) {
includedAuthors.delete(gitAuthorEmail);
}
for (const ignoredAuthor of ignoredAuthors) {
includedAuthors.delete(ignoredAuthor);
}
if (includedAuthors.size === 0) {
// authors all match - branch has not been modified
logger_1.logger.trace({
branchName,
baseBranch,
committedAuthors: [...committedAuthors],
includedAuthors: [...includedAuthors],
gitAuthorEmail,
ignoredAuthors,
}, 'branch.isModified() = false');
logger_1.logger.debug('branch.isModified() = false');
config.branchIsModified[branchName] = false;
(0, modified_cache_1.setCachedModifiedResult)(branchName, false);
return false;
}
logger_1.logger.trace({
branchName,
baseBranch,
committedAuthors: [...committedAuthors],
includedAuthors: [...includedAuthors],
gitAuthorEmail,
ignoredAuthors,
}, 'branch.isModified() = true');
logger_1.logger.debug({ branchName, unrecognizedAuthors: [...includedAuthors] }, 'branch.isModified() = true');
config.branchIsModified[branchName] = true;
(0, modified_cache_1.setCachedModifiedResult)(branchName, true);
return true;
}
async function isBranchConflicted(baseBranch, branch) {
logger_1.logger.debug(`isBranchConflicted(${baseBranch}, ${branch})`);
const baseBranchSha = getBranchCommit(baseBranch);
const branchSha = getBranchCommit(branch);
if (!baseBranchSha || !branchSha) {
logger_1.logger.warn({ baseBranch, branch }, 'isBranchConflicted: branch does not exist');
return true;
}
const isConflicted = (0, conflicts_cache_1.getCachedConflictResult)(branch, branchSha, baseBranch, baseBranchSha);
if (is_1.default.boolean(isConflicted)) {
logger_1.logger.debug(`branch.isConflicted(): using cached result "${isConflicted}"`);
return isConflicted;
}
logger_1.logger.debug('branch.isConflicted(): using git to calculate');
let result = false;
await syncGit();
await writeGitAuthor();
const origBranch = config.currentBranch;
try {
await git.reset(simple_git_1.ResetMode.HARD);
//TODO: see #18600
if (origBranch !== baseBranch) {
await git.checkout(baseBranch);
}
await git.merge(['--no-commit', '--no-ff', `origin/${branch}`]);
}
catch (err) {
result = true;
/* v8 ignore next 6 -- TODO: add test */
if (!err?.git?.conflicts?.length) {
logger_1.logger.debug({ baseBranch, branch, err }, 'isBranchConflicted: unknown error');
}
}
finally {
try {
await git.merge(['--abort']);
if (origBranch !== baseBranch) {
await git.checkout(origBranch);
}
/* v8 ignore next 6 -- TODO: add test */
}
catch (err) {
logger_1.logger.debug({ baseBranch, branch, err }, 'isBranchConflicted: cleanup error');
}
}
(0, conflicts_cache_1.setCachedConflictResult)(branch, result);
logger_1.logger.debug(`branch.isConflicted(): ${result}`);
return result;
}
async function deleteBranch(branchName) {
await syncGit();
try {
const deleteCommand = ['push', '--delete', 'origin', branchName];
if ((0, config_1.getNoVerify)().includes('push')) {
deleteCommand.push('--no-verify');
}
await gitRetry(() => git.raw(deleteCommand));
logger_1.logger.debug(`Deleted remote branch: ${branchName}`);
}
catch (err) {
const errChecked = (0, error_1.checkForPlatformFailure)(err);
/* v8 ignore next 3 -- TODO: add test */
if (errChecked) {
throw errChecked;
}
logger_1.logger.debug(`No remote branch to delete with name: ${branchName}`);
}
try {
/* v8 ignore next 2 -- TODO: add test (always throws) */
await deleteLocalBranch(branchName);
logger_1.logger.debug(`Deleted local branch: ${branchName}`);
}
catch (err) {
const errChecked = (0, error_1.checkForPlatformFailure)(err);
/* v8 ignore next 3 -- TODO: add test */
if (errChecked) {
throw errChecked;
}
logger_1.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(simple_git_1.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_1.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(simple_git_1.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));
(0, limits_1.incLimitedValue)('Commits');
}
catch (err) {
logger_1.logger.debug({
baseBranch: config.currentBranch,
baseSha: config.currentBranchSha,
branchName,
branchSha: getBranchCommit(branchName),
status,
err,
}, 'mergeBranch error');
throw err;
}
}
async function getBranchLastCommitTime(branchName) {
await syncGit();
try {
const time = await git.show(['-s', '--format=%ai', 'origin/' + branchName]);
return new Date(Date.parse(time));
}
catch (err) {
const errChecked = (0, error_1.checkForPlatformFailure)(err);
/* v8 ignore next 3 -- TODO: add test */
if (errChecked) {
throw errChecked;
}
return new Date();
}
}
async function getBranchFiles(branchName) {
await syncGit();
try {
const diff = await gitRetry(() => git.diffSummary([`origin/${branchName}`, `origin/${branchName}^`]));
return diff.files.map((file) => file.file);
/* v8 ignore next 8 -- TODO: add test */
}
catch (err) {
logger_1.logger.warn({ err }, 'getBranchFiles error');
const errChecked = (0, error_1.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,
]);
return content;
}
catch (err) {
const errChecked = (0, error_1.checkForPlatformFailure)(err);
/* v8 ignore next 3 -- TODO: add test */
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 (0, private_key_1.writePrivateKey)();
privateKeySet = true;
}
await (0, private_key_1.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 = global_1.GlobalConfig.get('localDir');
await syncGit();
logger_1.logger.debug(`Preparing files for committing to branch ${branchName}`);
await handleCommitAuth(localDir);
try {
await git.reset(simple_git_1.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);
/* v8 ignore next 8 -- TODO: add test */
}
catch (err) {
const errChecked = (0, error_1.checkForPlatformFailure)(err);
if (errChecked) {
throw errChecked;
}
logger_1.logger.trace({ err, fileName }, 'Cannot delete file');
ignoredFiles.push(fileName);
}
}
else {
if (await isDirectory(upath_1.default.join(localDir, fileName))) {
// This is usually a git submodule update
logger_1.logger.trace({ fileName }, 'Adding directory commit');
}
else if (file.contents === null) {
continue;
}
else {
let contents;
if (typeof file.contents === 'string') {
contents = Buffer.from(file.contents);
/* v8 ignore next 3 -- TODO: add test */
}
else {
contents = file.contents;
}
// some file systems including Windows don't support the mode
// so the index should be manually updated after adding the file
if (file.isSymlink) {
await fs_extra_1.default.symlink(file.contents, upath_1.default.join(localDir, fileName));
}
else {
await fs_extra_1.default.outputFile(upath_1.default.join(localDir, fileName), contents, {
mode: file.isExecutable ? 0o777 : 0o666,
});
}
}
try {
/* v8 ignore next 2 -- TODO: add test */
const addParams = fileName === app_strings_1.configFileNames[0] ? ['-f', fileName] : fileName;
await git.add(addParams);
if (file.isExecutable) {
await git.raw(['update-index', '--chmod=+x', fileName]);
}
addedModifiedFiles.push(fileName);
/* v8 ignore next 11 -- TODO: add test */
}
catch (err) {
if (!err.message.includes('The following paths are ignored by one of your .gitignore files')) {
throw err;
}
logger_1.logger.debug(`Cannot commit ignored file: ${fileName}`);
ignoredFiles.push(file.path);
}
}
}
const commitOptions = {};
if ((0, config_1.getNoVerify)().includes('commit')) {
commitOptions['--no-verify'] = null;
}
const commitRes = await git.commit(message, [], commitOptions);
if (commitRes.summary &&
commitRes.summary.changes === 0 &&
commitRes.summary.insertions === 0 &&
commitRes.summary.deletions === 0) {
logger_1.logger.warn({ commitRes }, 'Detected empty commit - aborting git push');
return null;
}
logger_1.logger.debug({ deletedFiles, ignoredFiles, result: commitRes }, `git commit`);
if (!force && !(await hasDiff('HEAD', `origin/${branchName}`))) {
logger_1.logger.debug({ branchName, deletedFiles, addedModifiedFiles, ignoredFiles }, 'No file changes detected. Skipping commit');
return null;
}
const commitSha = (await git.revparse([branchName])).trim();
const result = {
parentCommitSha,
commitSha,
files: files.filter((fileChange) => {
if (fileChange.type === 'deletion') {
return deletedFiles.includes(fileChange.path);
}
return addedModifiedFiles.includes(fileChange.path);
}),
};
return result;
/* v8 ignore next 3 -- TODO: add test */
}
catch (err) {
return (0, error_1.handleCommitError)(err, branchName, files);
}
}
async function pushCommit({ sourceRef, targetRef, files, }) {
await syncGit();
logger_1.logger.debug(`Pushing refSpec ${sourceRef}:${targetRef ?? sourceRef}`);
let result = false;
try {
const pushOptions = {
'--force-with-lease': null,
'-u': null,
};
if ((0, config_1.getNoVerify)().includes('push')) {
pushOptions['--no-verify'] = null;
}
const pushRes = await gitRetry(() => git.push('origin', `${sourceRef}:${targetRef ?? sourceRef}`, pushOptions));
delete pushRes.repo;
logger_1.logger.debug({ result: pushRes }, 'git push');
(0, limits_1.incLimitedValue)('Commits');
result = true;
/* v8 ignore next 3 -- TODO: add test */
}
catch (err) {
(0, error_1.handleCommitError)(err, sourceRef, files);
}
return result;
}
async function fetchBranch(branchName) {
await syncGit();
logger_1.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;
/* v8 ignore next 3 -- TODO: add test */
}
catch (err) {
return (0, error_1.handleCommitError)(err, branchName);
}
}
async function commitFiles(commitConfig) {
try {
const commitResult = await prepareCommit(commitConfig);
if (commitResult) {
const pushResult = await pushCommit({
sourceRef: commitConfig.branchName,
files: commitConfig.files,
});
if (pushResult) {
const { branchName } = commitConfig;
const { commitSha } = commitResult;
config.branchCommits[branchName] = commitSha;
config.branchIsModified[branchName] = false;
return commitSha;
}
}
return null;
/* v8 ignore next 6 -- TODO: add test */
}
catch (err) {
if (err.message.includes('[rejected] (stale info)')) {
throw new Error(error_messages_1.REPOSITORY_CHANGED);
}
throw err;
}
}
function getUrl({ protocol, auth, hostname, host, repository, }) {
if (protocol === 'ssh') {
// TODO: types (#22198)
return `git@${hostname}:${repository}.git`;
}
return node_url_1.default.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_1.logger.debug(`Cleaning up Renovate refs: refs/renovate/branches/*`);
const renovateRefs = [];
try {
const rawOutput = await git.listRemote([
config.url,
'refs/renovate/branches/*',
]);
const refs = rawOutput
.split(regex_1.newlineRegex)
.map((line) => line.replace((0, regex_1.regEx)(/[0-9a-f]+\s+/i), '').trim())
.filter((line) => line.startsWith('refs/renovate/branches/'));
renovateRefs.push(...refs);
/* v8 ignore next 3 -- TODO: add test */
}
catch (err) {
logger_1.logger.warn({ err }, `Renovate refs cleanup error`);
}
if (renovateRefs.length) {
try {
const pushOpts = ['--delete', 'origin', ...renovateRefs];
await git.push(pushOpts);
}
catch (err) {
if ((0, error_1.bulkChangesDisallowed)(err)) {
for (const ref of renovateRefs) {
try {
const pushOpts = ['--delete', 'origin', ref];
await git.push(pushOpts);
/* v8 ignore next 4 -- TODO: add test */
}
catch (err) {
logger_1.logger.debug({ err }, 'Error deleting "refs/renovate/branches/*"');
break;
}
}
/* v8 ignore next 3 -- TODO: add test */
}
else {
logger_1.logger.warn({ err }, 'Error deleting "refs/renovate/branches/*"');
}
}
}
remoteRefsExist = false;
}
const treeItemRegex = (0, regex_1.regEx)(/^(?<mode>\d{6})\s+(?<type>blob|tree|commit)\s+(?<sha>[0-9a-f]{40})\s+(?<path>.*)$/);
const treeShaRegex = (0, regex_1.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]);
const { treeSha } =
/* v8 ignore next -- will never happen ? */
treeShaRegex.exec(commitOutput)?.groups ?? {};
const contents = await git.catFile(['-p', treeSha]);
const lines = contents.split(regex_1.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: sha });
}
}
return result;
}
async function localBranchExists(branchName) {
await syncGit();
const localBranches = await git.branchLocal();
return localBranches.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_1.logger.debug(`Synchronizing fork with "${exports.RENOVATE_FORK_UPSTREAM}" remote for branch ${branchName}`);
const remotes = await getRemotes();
/* v8 ignore next 3 -- this should not be possible if upstreamUrl exists */
if (!remotes.some((r) => r === exports.RENOVATE_FORK_UPSTREAM)) {
throw new Error('No upstream remote exists, cannot sync fork');
}
try {
await git.fetch([exports.RENOVATE_FORK_UPSTREAM]);
if (await localBranchExists(branchName)) {
await checkoutBranch(branchName);
}
else {
await checkoutBranchFromRemote(branchName, exports.RENOVATE_FORK_UPSTREA