UNPKG

renovate

Version:

Automated dependency updates. Flexible so you don't need to be.

1,294 lines • 50.9 kB
"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