From 1304bc930e63640037f6df946d207e6bae0caddd Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Fri, 21 May 2021 21:51:03 +0200
Subject: [PATCH] feat: validate git version (#9979)

---
 docs/development/local-development.md |  2 +-
 lib/util/git/index.spec.ts            |  6 +++++
 lib/util/git/index.ts                 | 36 +++++++++++++++++++++++++++
 lib/workers/global/initialize.spec.ts | 20 +++++++++++++++
 lib/workers/global/initialize.ts      |  9 +++++++
 5 files changed, 72 insertions(+), 1 deletion(-)
 create mode 100644 lib/workers/global/initialize.spec.ts

diff --git a/docs/development/local-development.md b/docs/development/local-development.md
index 75c10b72ea..a35a7f5fa6 100644
--- a/docs/development/local-development.md
+++ b/docs/development/local-development.md
@@ -10,7 +10,7 @@ For example, if you think anything is unclear, or you think something needs to b
 
 You need the following dependencies for local development:
 
-- Git
+- Git `>= 1.22.0`
 - Node.js `>=14.15.4`
 - Yarn `^1.22.5`
 - C++ compiler
diff --git a/lib/util/git/index.spec.ts b/lib/util/git/index.spec.ts
index 4c7a231b94..d12876c280 100644
--- a/lib/util/git/index.spec.ts
+++ b/lib/util/git/index.spec.ts
@@ -95,6 +95,12 @@ describe(getName(), () => {
     await base.cleanup();
   });
 
+  describe('validateGitVersion()', () => {
+    it('has a git version greater or equal to the minimum required', async () => {
+      const res = await git.validateGitVersion();
+      expect(res).toBeTrue();
+    });
+  });
   describe('checkoutBranch(branchName)', () => {
     it('sets the base branch as master', async () => {
       await expect(git.checkoutBranch(defaultBranch)).resolves.not.toThrow();
diff --git a/lib/util/git/index.ts b/lib/util/git/index.ts
index 0acf7e4fe7..e1b0acb506 100644
--- a/lib/util/git/index.ts
+++ b/lib/util/git/index.ts
@@ -23,6 +23,7 @@ import {
 import { logger } from '../../logger';
 import { ExternalHostError } from '../../types/errors/external-host-error';
 import { GitOptions, GitProtocol } from '../../types/git';
+import { api as semver } from '../../versioning/semver';
 import { Limit, incLimitedValue } from '../../workers/global/limits';
 import { GitNoVerifyOption, getNoVerify } from './config';
 import { configSigningKey, writePrivateKey } from './private-key';
@@ -145,6 +146,41 @@ let gitInitialized: boolean;
 
 let privateKeySet = false;
 
+export const GIT_MINIMUM_VERSION = '2.22.0'; // git show-current
+
+export async function validateGitVersion(): Promise<boolean> {
+  let version: string;
+  const globalGit = Git();
+  try {
+    const raw = await globalGit.raw(['--version']);
+    for (const section of raw.split(/\s+/)) {
+      if (semver.isVersion(section)) {
+        version = section;
+        break;
+      }
+    }
+  } catch (err) /* istanbul ignore next */ {
+    logger.error({ err }, 'Error fetching git version');
+    return false;
+  }
+  // istanbul ignore if
+  if (
+    !(
+      version &&
+      (version === GIT_MINIMUM_VERSION ||
+        semver.isGreaterThan(version, GIT_MINIMUM_VERSION))
+    )
+  ) {
+    logger.error(
+      { detectedVersion: version, minimumVersion: GIT_MINIMUM_VERSION },
+      'Git version needs upgrading'
+    );
+    return false;
+  }
+  logger.debug(`Found valid git version: ${version}`);
+  return true;
+}
+
 async function fetchBranchCommits(): Promise<void> {
   config.branchCommits = {};
   const opts = ['ls-remote', '--heads', config.url];
diff --git a/lib/workers/global/initialize.spec.ts b/lib/workers/global/initialize.spec.ts
new file mode 100644
index 0000000000..bfbcc3ee7c
--- /dev/null
+++ b/lib/workers/global/initialize.spec.ts
@@ -0,0 +1,20 @@
+import { getName, mocked } from '../../../test/util';
+import * as _git from '../../util/git';
+import { checkVersions } from './initialize';
+
+jest.mock('../../util/git');
+
+const git = mocked(_git);
+
+describe(getName(), () => {
+  describe('checkVersions()', () => {
+    it('throws if invalid version', async () => {
+      git.validateGitVersion.mockResolvedValueOnce(false);
+      await expect(checkVersions()).rejects.toThrow();
+    });
+    it('returns if valid git version', async () => {
+      git.validateGitVersion.mockResolvedValueOnce(true);
+      await expect(checkVersions()).toResolve();
+    });
+  });
+});
diff --git a/lib/workers/global/initialize.ts b/lib/workers/global/initialize.ts
index 4f1e3286ce..c80dc0ea7f 100644
--- a/lib/workers/global/initialize.ts
+++ b/lib/workers/global/initialize.ts
@@ -6,6 +6,7 @@ import { logger } from '../../logger';
 import { initPlatform } from '../../platform';
 import * as packageCache from '../../util/cache/package';
 import { setEmojiConfig } from '../../util/emoji';
+import { validateGitVersion } from '../../util/git';
 import { Limit, setMaxLimit } from './limits';
 
 async function setDirectories(input: GlobalConfig): Promise<GlobalConfig> {
@@ -34,10 +35,18 @@ function limitCommitsPerRun(config: RenovateConfig): void {
   setMaxLimit(Limit.Commits, limit);
 }
 
+export async function checkVersions(): Promise<void> {
+  const validGitVersion = await validateGitVersion();
+  if (!validGitVersion) {
+    throw new Error('Init: git version needs upgrading');
+  }
+}
+
 export async function globalInitialize(
   config_: RenovateConfig
 ): Promise<RenovateConfig> {
   let config = config_;
+  await checkVersions();
   config = await initPlatform(config);
   config = await setDirectories(config);
   packageCache.init(config);
-- 
GitLab