diff --git a/lib/util/git/behind-base-branch-cache.spec.ts b/lib/util/git/behind-base-branch-cache.spec.ts new file mode 100644 index 00000000000000..e711c11cc25c11 --- /dev/null +++ b/lib/util/git/behind-base-branch-cache.spec.ts @@ -0,0 +1,40 @@ +import { mocked } from '../../../test/util'; +import * as _repositoryCache from '../cache/repository'; +import type { BranchCache, RepoCacheData } from '../cache/repository/types'; +import { getCachedBehindBaseResult } from './behind-base-branch-cache'; + +jest.mock('../cache/repository'); +const repositoryCache = mocked(_repositoryCache); + +describe('util/git/behind-base-branch-cache', () => { + let repoCache: RepoCacheData = {}; + + beforeEach(() => { + repoCache = {}; + repositoryCache.getCache.mockReturnValue(repoCache); + }); + + describe('getCachedBehindBaseResult', () => { + it('returns null if cache is not populated', () => { + expect(getCachedBehindBaseResult('foo', '111')).toBeNull(); + }); + + it('returns null if branch not found', () => { + expect(getCachedBehindBaseResult('foo', '111')).toBeNull(); + }); + + it('returns true if target SHA has changed', () => { + repoCache.branches = [ + { branchName: 'foo', sha: 'aaa', parentSha: '222' } as BranchCache, + ]; + expect(getCachedBehindBaseResult('foo', '111')).toBeTrue(); + }); + + it('returns false if target SHA has not changed', () => { + repoCache.branches = [ + { branchName: 'foo', sha: 'aaa', parentSha: '111' } as BranchCache, + ]; + expect(getCachedBehindBaseResult('foo', '111')).toBeFalse(); + }); + }); +}); diff --git a/lib/util/git/behind-base-branch-cache.ts b/lib/util/git/behind-base-branch-cache.ts new file mode 100644 index 00000000000000..5b9677dbe6793d --- /dev/null +++ b/lib/util/git/behind-base-branch-cache.ts @@ -0,0 +1,20 @@ +import { getCache } from '../cache/repository'; + +// Compare cached parent Sha of a branch to the fetched base-branch sha to determine whether the branch is behind the base +// Since cache is updated after each run, this will be sufficient to determine whether a branch is behind its parent. +export function getCachedBehindBaseResult( + branchName: string, + currentBaseBranchSha: string +): boolean | null { + const cache = getCache(); + const { branches = [] } = cache; + const cachedBranch = branches?.find( + (branch) => branch.branchName === branchName + ); + + if (!cachedBranch) { + return null; + } + + return currentBaseBranchSha !== cachedBranch.parentSha; +} diff --git a/lib/util/git/index.spec.ts b/lib/util/git/index.spec.ts index f6ecee0d1affab..11e5182a856e81 100644 --- a/lib/util/git/index.spec.ts +++ b/lib/util/git/index.spec.ts @@ -7,6 +7,8 @@ import { CONFIG_VALIDATION, INVALID_PATH, } from '../../constants/error-messages'; +import * as _repoCache from '../cache/repository'; +import type { BranchCache } from '../cache/repository/types'; import { newlineRegex, regEx } from '../regex'; import * as _conflictsCache from './conflicts-cache'; import * as _modifiedCache from './modified-cache'; @@ -17,6 +19,8 @@ import { setNoVerify } from '.'; jest.mock('./conflicts-cache'); jest.mock('./modified-cache'); jest.mock('delay'); +jest.mock('../cache/repository'); +const repoCache = mocked(_repoCache); const conflictsCache = mocked(_conflictsCache); const modifiedCache = mocked(_modifiedCache); @@ -239,16 +243,27 @@ describe('util/git/index', () => { describe('isBranchBehindBase()', () => { it('should return false if same SHA as master', async () => { + repoCache.getCache.mockReturnValueOnce({}); expect( await git.isBranchBehindBase('renovate/future_branch') ).toBeFalse(); }); it('should return true if SHA different from master', async () => { + repoCache.getCache.mockReturnValueOnce({}); expect(await git.isBranchBehindBase('renovate/past_branch')).toBeTrue(); }); it('should return result even if non-default and not under branchPrefix', async () => { + const parentSha = await git.getBranchParentSha('develop'); + repoCache.getCache.mockReturnValueOnce({}).mockReturnValueOnce({ + branches: [ + { + branchName: 'develop', + parentSha: parentSha, + } as BranchCache, + ], + }); expect(await git.isBranchBehindBase('develop')).toBeTrue(); expect(await git.isBranchBehindBase('develop')).toBeTrue(); // cache }); diff --git a/lib/util/git/index.ts b/lib/util/git/index.ts index e522315210c130..4e99a4527d35ce 100644 --- a/lib/util/git/index.ts +++ b/lib/util/git/index.ts @@ -28,6 +28,7 @@ import type { GitProtocol } from '../../types/git'; import { Limit, incLimitedValue } from '../../workers/global/limits'; import { newlineRegex, regEx } from '../regex'; import { parseGitAuthor } from './author'; +import { getCachedBehindBaseResult } from './behind-base-branch-cache'; import { getNoVerify, simpleGitConfig } from './config'; import { getCachedConflictResult, @@ -550,6 +551,13 @@ export function getBranchList(): string[] { } export async function isBranchBehindBase(branchName: string): Promise { + const { currentBranchSha } = config; + + let isBehind = getCachedBehindBaseResult(branchName, currentBranchSha); + if (isBehind !== null) { + return isBehind; + } + await syncGit(); try { const { currentBranchSha, currentBranch } = config; @@ -559,7 +567,7 @@ export async function isBranchBehindBase(branchName: string): Promise { '--contains', config.currentBranchSha, ]); - const isBehind = !branches.all.map(localName).includes(branchName); + isBehind = !branches.all.map(localName).includes(branchName); logger.debug( { isBehind, currentBranch, currentBranchSha }, `isBranchBehindBase=${isBehind}`