From 854d0a86e8fbd83b047f77a0b6e8a79b213e63d4 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Fri, 28 May 2021 12:36:53 +0200
Subject: [PATCH] feat(internal): use cache to delay git cloning (#10209)

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---
 lib/util/cache/repository/types.ts            |  2 ++
 .../init/__snapshots__/merge.spec.ts.snap     |  7 +++++++
 lib/workers/repository/init/merge.spec.ts     |  8 +++++++
 lib/workers/repository/init/merge.ts          | 15 ++++++++++++-
 lib/workers/repository/init/semantic.spec.ts  | 12 ++++++++++-
 lib/workers/repository/init/semantic.ts       | 13 +++++++++---
 .../repository/onboarding/branch/check.ts     | 18 ++++++++++++++++
 .../onboarding/branch/index.spec.ts           | 21 +++++++++++++++++++
 8 files changed, 91 insertions(+), 5 deletions(-)

diff --git a/lib/util/cache/repository/types.ts b/lib/util/cache/repository/types.ts
index 3a7995b559..bca06bd77c 100644
--- a/lib/util/cache/repository/types.ts
+++ b/lib/util/cache/repository/types.ts
@@ -32,6 +32,8 @@ export interface BranchCache {
 }
 
 export interface Cache {
+  configFileName?: string;
+  semanticCommits?: 'enabled' | 'disabled';
   branches?: BranchCache[];
   repository?: string;
   revision?: number;
diff --git a/lib/workers/repository/init/__snapshots__/merge.spec.ts.snap b/lib/workers/repository/init/__snapshots__/merge.spec.ts.snap
index 9f136f5d06..42481bc0f3 100644
--- a/lib/workers/repository/init/__snapshots__/merge.spec.ts.snap
+++ b/lib/workers/repository/init/__snapshots__/merge.spec.ts.snap
@@ -21,6 +21,13 @@ Object {
 }
 `;
 
+exports[`workers/repository/init/merge detectRepoFileConfig() finds .renovaterc.json 2`] = `
+Object {
+  "configFileName": ".renovaterc.json",
+  "configFileParsed": "{\\"something\\":\\"new\\"}",
+}
+`;
+
 exports[`workers/repository/init/merge detectRepoFileConfig() finds and parse renovate.json5 1`] = `
 Object {
   "configFileName": "renovate.json5",
diff --git a/lib/workers/repository/init/merge.spec.ts b/lib/workers/repository/init/merge.spec.ts
index e45fd77aca..b5e620a136 100644
--- a/lib/workers/repository/init/merge.spec.ts
+++ b/lib/workers/repository/init/merge.spec.ts
@@ -5,9 +5,11 @@ import {
   getName,
   git,
   mocked,
+  platform,
 } from '../../../../test/util';
 import * as _migrateAndValidate from '../../../config/migrate-validate';
 import * as _migrate from '../../../config/migration';
+import { initialize } from '../../../util/cache/repository';
 import {
   checkForRepoConfigError,
   detectRepoFileConfig,
@@ -33,6 +35,10 @@ jest.mock('../../../config/migrate-validate');
 
 describe(getName(), () => {
   describe('detectRepoFileConfig()', () => {
+    beforeEach(async () => {
+      await initialize({});
+    });
+
     it('returns config if not found', async () => {
       git.getFileList.mockResolvedValue(['package.json']);
       fs.readLocalFile.mockResolvedValue('{}');
@@ -87,6 +93,8 @@ describe(getName(), () => {
     it('finds .renovaterc.json', async () => {
       git.getFileList.mockResolvedValue(['package.json', '.renovaterc.json']);
       fs.readLocalFile.mockResolvedValue('{}');
+      platform.getJsonFile.mockResolvedValueOnce('{"something":"new"}');
+      expect(await detectRepoFileConfig()).toMatchSnapshot();
       expect(await detectRepoFileConfig()).toMatchSnapshot();
     });
   });
diff --git a/lib/workers/repository/init/merge.ts b/lib/workers/repository/init/merge.ts
index 08117020fe..79d029c8b1 100644
--- a/lib/workers/repository/init/merge.ts
+++ b/lib/workers/repository/init/merge.ts
@@ -15,12 +15,24 @@ import {
 } from '../../../constants/error-messages';
 import * as npmApi from '../../../datasource/npm';
 import { logger } from '../../../logger';
+import { platform } from '../../../platform';
+import { getCache } from '../../../util/cache/repository';
 import { readLocalFile } from '../../../util/fs';
 import { getFileList } from '../../../util/git';
 import * as hostRules from '../../../util/host-rules';
 import type { RepoFileConfig } from './types';
 
 export async function detectRepoFileConfig(): Promise<RepoFileConfig> {
+  const cache = getCache();
+  let { configFileName } = cache;
+  if (configFileName) {
+    const configFileParsed = await platform.getJsonFile(configFileName);
+    if (configFileParsed) {
+      logger.debug('Existing config file confirmed');
+      return { configFileName, configFileParsed };
+    }
+    logger.debug('Existing config file no longer exists');
+  }
   const fileList = await getFileList();
   async function detectConfigFile(): Promise<string | null> {
     for (const fileName of configFileNames) {
@@ -40,11 +52,12 @@ export async function detectRepoFileConfig(): Promise<RepoFileConfig> {
     }
     return null;
   }
-  const configFileName = await detectConfigFile();
+  configFileName = await detectConfigFile();
   if (!configFileName) {
     logger.debug('No renovate config file found');
     return {};
   }
+  cache.configFileName = configFileName;
   logger.debug(`Found ${configFileName} config file`);
   let configFileParsed;
   if (configFileName === 'package.json') {
diff --git a/lib/workers/repository/init/semantic.spec.ts b/lib/workers/repository/init/semantic.spec.ts
index 1ac0f5a339..e3be88d5c7 100644
--- a/lib/workers/repository/init/semantic.spec.ts
+++ b/lib/workers/repository/init/semantic.spec.ts
@@ -1,4 +1,5 @@
 import { RenovateConfig, getConfig, getName, git } from '../../../../test/util';
+import { initialize } from '../../../util/cache/repository';
 import { detectSemanticCommits } from './semantic';
 
 jest.mock('../../../util/git');
@@ -13,11 +14,20 @@ beforeEach(() => {
 
 describe(getName(), () => {
   describe('detectSemanticCommits()', () => {
+    beforeEach(async () => {
+      await initialize({});
+    });
     it('detects false if unknown', async () => {
       config.semanticCommits = null;
-      git.getCommitMessages.mockResolvedValue(['foo', 'bar']);
+      git.getCommitMessages.mockResolvedValueOnce(['foo', 'bar']);
+      git.getCommitMessages.mockResolvedValueOnce([
+        'fix: foo',
+        'refactor: bar',
+      ]);
       const res = await detectSemanticCommits();
       expect(res).toBe('disabled');
+      const res2 = await detectSemanticCommits();
+      expect(res2).toBe('disabled');
     });
     it('detects true if known', async () => {
       config.semanticCommits = null;
diff --git a/lib/workers/repository/init/semantic.ts b/lib/workers/repository/init/semantic.ts
index 62816e63e5..297fa13a65 100644
--- a/lib/workers/repository/init/semantic.ts
+++ b/lib/workers/repository/init/semantic.ts
@@ -1,19 +1,26 @@
 import conventionalCommitsDetector from 'conventional-commits-detector';
 import { logger } from '../../../logger';
+import { getCache } from '../../../util/cache/repository';
 import { getCommitMessages } from '../../../util/git';
 
 type DetectedSemanticCommit = 'enabled' | 'disabled';
 
 export async function detectSemanticCommits(): Promise<DetectedSemanticCommit> {
   logger.debug('detectSemanticCommits()');
+  const cache = getCache();
+  if (cache.semanticCommits) {
+    return cache.semanticCommits;
+  }
   const commitMessages = await getCommitMessages();
   logger.trace(`commitMessages=${JSON.stringify(commitMessages)}`);
   const type = conventionalCommitsDetector(commitMessages);
   logger.debug('Semantic commits detection: ' + type);
   if (type === 'angular') {
     logger.debug('angular semantic commits detected');
-    return 'enabled';
+    cache.semanticCommits = 'enabled';
+  } else {
+    logger.debug('No semantic commits detected');
+    cache.semanticCommits = 'disabled';
   }
-  logger.debug('No semantic commits detected');
-  return 'disabled';
+  return cache.semanticCommits;
 }
diff --git a/lib/workers/repository/onboarding/branch/check.ts b/lib/workers/repository/onboarding/branch/check.ts
index 9a2a436921..475f1e9bc9 100644
--- a/lib/workers/repository/onboarding/branch/check.ts
+++ b/lib/workers/repository/onboarding/branch/check.ts
@@ -7,6 +7,7 @@ import {
 import { logger } from '../../../../logger';
 import { platform } from '../../../../platform';
 import { PrState } from '../../../../types';
+import { getCache } from '../../../../util/cache/repository';
 import { readLocalFile } from '../../../../util/fs';
 import { getFileList } from '../../../../util/git';
 
@@ -57,6 +58,23 @@ export const isOnboarded = async (config: RenovateConfig): Promise<boolean> => {
     // Return early and avoid checking for config files
     return true;
   }
+  const cache = getCache();
+  if (cache.configFileName) {
+    logger.debug('Checking cached config file name');
+    try {
+      const configFileContent = await platform.getJsonFile(
+        cache.configFileName
+      );
+      if (configFileContent) {
+        logger.debug('Existing config file confirmed');
+        return true;
+      }
+    } catch (err) {
+      // probably file doesn't exist
+    }
+    logger.debug('Existing config file no longer exists');
+    delete cache.configFileName;
+  }
   if (await configFileExists()) {
     await platform.ensureIssueClosing(title);
     return true;
diff --git a/lib/workers/repository/onboarding/branch/index.spec.ts b/lib/workers/repository/onboarding/branch/index.spec.ts
index 10074a91a1..b7faee9dd8 100644
--- a/lib/workers/repository/onboarding/branch/index.spec.ts
+++ b/lib/workers/repository/onboarding/branch/index.spec.ts
@@ -5,6 +5,7 @@ import {
   getConfig,
   getName,
   git,
+  mocked,
   platform,
 } from '../../../../../test/util';
 import {
@@ -13,6 +14,7 @@ import {
 } from '../../../../constants/error-messages';
 import { Pr } from '../../../../platform';
 import { PrState } from '../../../../types';
+import * as _cache from '../../../../util/cache/repository';
 import * as _config from './config';
 import * as _rebase from './rebase';
 import { checkOnboardingBranch } from '.';
@@ -21,10 +23,13 @@ const rebase: any = _rebase;
 const configModule: any = _config;
 
 jest.mock('../../../../workers/repository/onboarding/branch/rebase');
+jest.mock('../../../../util/cache/repository');
 jest.mock('../../../../util/fs');
 jest.mock('../../../../util/git');
 jest.mock('./config');
 
+const cache = mocked(_cache);
+
 describe(getName(), () => {
   describe('checkOnboardingBranch', () => {
     let config: RenovateConfig;
@@ -33,6 +38,7 @@ describe(getName(), () => {
       config = getConfig();
       config.repository = 'some/repo';
       git.getFileList.mockResolvedValue([]);
+      cache.getCache.mockReturnValue({});
     });
     it('throws if no package files', async () => {
       await expect(checkOnboardingBranch(config)).rejects.toThrow(
@@ -110,6 +116,21 @@ describe(getName(), () => {
       const res = await checkOnboardingBranch(config);
       expect(res.repoIsOnboarded).toBe(true);
     });
+
+    it('handles removed cached file name', async () => {
+      cache.getCache.mockReturnValue({ configFileName: '.renovaterc' });
+      git.getFileList.mockResolvedValueOnce(['renovate.json']);
+      const res = await checkOnboardingBranch(config);
+      expect(res.repoIsOnboarded).toBe(true);
+    });
+
+    it('handles cached file name', async () => {
+      cache.getCache.mockReturnValue({ configFileName: '.renovaterc' });
+      platform.getJsonFile.mockResolvedValueOnce({});
+      const res = await checkOnboardingBranch(config);
+      expect(res.repoIsOnboarded).toBe(true);
+    });
+
     it('detects repo is onboarded via package.json config', async () => {
       git.getFileList.mockResolvedValueOnce(['package.json']);
       fs.readLocalFile.mockResolvedValueOnce('{"renovate":{}}');
-- 
GitLab