diff --git a/lib/util/git/index.spec.ts b/lib/util/git/index.spec.ts index e6537feaaf3f0cbd6a58222883f77985f0911e8c..c3d1886cc4fc9e7988a8a32f385fe7fd79872cd3 100644 --- a/lib/util/git/index.spec.ts +++ b/lib/util/git/index.spec.ts @@ -9,13 +9,16 @@ import { } from '../../constants/error-messages'; import { newlineRegex, regEx } from '../regex'; import * as _conflictsCache from './conflicts-cache'; +import * as _modifiedCache from './modified-cache'; import type { FileChange } from './types'; import * as git from '.'; import { setNoVerify } from '.'; jest.mock('./conflicts-cache'); +jest.mock('./modified-cache'); jest.mock('delay'); const conflictsCache = mocked(_conflictsCache); +const modifiedCache = mocked(_modifiedCache); // Class is no longer exported const SimpleGit = Git().constructor as { prototype: ReturnType<typeof Git> }; @@ -250,6 +253,10 @@ describe('util/git/index', () => { }); describe('isBranchModified()', () => { + beforeEach(() => { + modifiedCache.getCachedModifiedResult.mockReturnValue(null); + }); + it('should return false when branch is not found', async () => { expect(await git.isBranchModified('renovate/not_found')).toBeFalse(); }); @@ -269,6 +276,11 @@ describe('util/git/index', () => { it('should return true when custom author is unknown', async () => { expect(await git.isBranchModified('renovate/custom_author')).toBeTrue(); }); + + it('should return value stored in modifiedCacheResult', async () => { + modifiedCache.getCachedModifiedResult.mockReturnValue(true); + expect(await git.isBranchModified('renovate/future_branch')).toBeTrue(); + }); }); describe('getBranchCommit(branchName)', () => { diff --git a/lib/util/git/index.ts b/lib/util/git/index.ts index 3e7a7c2057fcaa956c48d7c69c2cb2be99ce5f7b..e4181f81533c5f0f215594c88c3dc4c7eaf36d2b 100644 --- a/lib/util/git/index.ts +++ b/lib/util/git/index.ts @@ -34,6 +34,10 @@ import { setCachedConflictResult, } from './conflicts-cache'; import { checkForPlatformFailure, handleCommitError } from './error'; +import { + getCachedModifiedResult, + setCachedModifiedResult, +} from './modified-cache'; import { configSigningKey, writePrivateKey } from './private-key'; import type { CommitFilesConfig, @@ -564,11 +568,6 @@ export async function isBranchStale(branchName: string): Promise<boolean> { } export async function isBranchModified(branchName: string): Promise<boolean> { - await syncGit(); - // First check cache - if (config.branchIsModified[branchName] !== undefined) { - return config.branchIsModified[branchName]; - } if (!branchExists(branchName)) { logger.debug( { branchName }, @@ -576,6 +575,20 @@ export async function isBranchModified(branchName: string): Promise<boolean> { ); return false; } + // First check local config + if (config.branchIsModified[branchName] !== undefined) { + return config.branchIsModified[branchName]; + } + // Second check repoCache + const isModified = getCachedModifiedResult( + branchName, + config.branchCommits[branchName] + ); + if (isModified !== null) { + return (config.branchIsModified[branchName] = isModified); + } + + await syncGit(); // Retrieve the author of the most recent commit let lastAuthor: string | undefined; try { @@ -606,6 +619,11 @@ export async function isBranchModified(branchName: string): Promise<boolean> { // author matches - branch has not been modified logger.debug({ branchName }, 'Branch has not been modified'); config.branchIsModified[branchName] = false; + setCachedModifiedResult( + branchName, + config.branchCommits[branchName], + false + ); return false; } logger.debug( @@ -613,6 +631,7 @@ export async function isBranchModified(branchName: string): Promise<boolean> { 'Last commit author does not match git author email - branch has been modified' ); config.branchIsModified[branchName] = true; + setCachedModifiedResult(branchName, config.branchCommits[branchName], true); return true; } diff --git a/lib/util/git/modified-cache.spec.ts b/lib/util/git/modified-cache.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..510e9aaec64292dd420b213e7105f59741f12dc5 --- /dev/null +++ b/lib/util/git/modified-cache.spec.ts @@ -0,0 +1,87 @@ +import { mocked } from '../../../test/util'; +import * as _repositoryCache from '../cache/repository'; +import type { BranchCache, RepoCacheData } from '../cache/repository/types'; +import { + getCachedModifiedResult, + setCachedModifiedResult, +} from './modified-cache'; + +jest.mock('../cache/repository'); +const repositoryCache = mocked(_repositoryCache); + +describe('util/git/modified-cache', () => { + let repoCache: RepoCacheData = {}; + + beforeEach(() => { + repoCache = {}; + repositoryCache.getCache.mockReturnValue(repoCache); + }); + + describe('getCachedModifiedResult', () => { + it('returns null if cache is not populated', () => { + expect(getCachedModifiedResult('foo', '111')).toBeNull(); + }); + + it('returns null if target key not found', () => { + expect(getCachedModifiedResult('foo', '111')).toBeNull(); + }); + + it('returns null if target SHA has changed', () => { + repoCache.branches = [{ branchName: 'foo', sha: 'aaa' } as BranchCache]; + expect(getCachedModifiedResult('foo', '111')).toBeNull(); + }); + + it('returns true', () => { + repoCache.branches = [ + { branchName: 'foo', sha: '111', isModified: true } as BranchCache, + ]; + expect(getCachedModifiedResult('foo', '111')).toBeTrue(); + }); + + it('returns false', () => { + repoCache.branches = [ + { branchName: 'foo', sha: '111', isModified: false } as BranchCache, + ]; + expect(getCachedModifiedResult('foo', '111')).toBeFalse(); + }); + }); + + describe('setCachedModifiedResult', () => { + it('sets value for unpopulated cache', () => { + setCachedModifiedResult('foo', '111', false); + expect(repoCache).toEqual({ + branches: [{ branchName: 'foo', sha: '111', isModified: false }], + }); + }); + + it('replaces value when SHA has changed', () => { + setCachedModifiedResult('foo', '111', false); + setCachedModifiedResult('foo', '121', false); + setCachedModifiedResult('foo', '131', false); + expect(repoCache).toEqual({ + branches: [{ branchName: 'foo', sha: '131', isModified: false }], + }); + }); + + it('replaces value when both value and SHA have changed', () => { + setCachedModifiedResult('foo', '111', false); + setCachedModifiedResult('foo', 'aaa', true); + expect(repoCache).toEqual({ + branches: [{ branchName: 'foo', sha: 'aaa', isModified: true }], + }); + }); + + it('handles multiple branches', () => { + setCachedModifiedResult('foo-1', '111', false); + setCachedModifiedResult('foo-2', 'aaa', true); + setCachedModifiedResult('foo-3', '222', false); + expect(repoCache).toEqual({ + branches: [ + { branchName: 'foo-1', sha: '111', isModified: false }, + { branchName: 'foo-2', sha: 'aaa', isModified: true }, + { branchName: 'foo-3', sha: '222', isModified: false }, + ], + }); + }); + }); +}); diff --git a/lib/util/git/modified-cache.ts b/lib/util/git/modified-cache.ts new file mode 100644 index 0000000000000000000000000000000000000000..1d46d8239612f3f91b8f8c9e648f900946980216 --- /dev/null +++ b/lib/util/git/modified-cache.ts @@ -0,0 +1,41 @@ +import is from '@sindresorhus/is'; +import { getCache } from '../cache/repository'; +import type { BranchCache } from '../cache/repository/types'; + +export function getCachedModifiedResult( + branchName: string, + branchSha: string +): boolean | null { + const { branches } = getCache(); + const branch = branches?.find((branch) => branch.branchName === branchName); + + if (branch?.sha !== branchSha) { + return null; + } + + return branch.isModified; +} + +export function setCachedModifiedResult( + branchName: string, + branchSha: string, + isModified: boolean +): void { + const cache = getCache(); + cache.branches ??= []; + const { branches } = cache; + const branch = + branches?.find((branch) => branch.branchName === branchName) ?? + ({ branchName: branchName } as BranchCache); + + // if branch not present add it to cache + if (is.undefined(branch.sha)) { + branches.push(branch); + } + + if (branch.sha !== branchSha) { + branch.sha = branchSha; + } + + branch.isModified = isModified; +}