Skip to content
Snippets Groups Projects
Select Git revision
  • 181c52f1d7ce6d73ac4094579d3d3361d390cb7d
  • main default protected
  • renovate/main-renovatebot-github-action-43.x
  • next
  • renovate/main-containerbase-internal-tools-3.x
  • renovate/main-lock-file-maintenance
  • renovate/main-docs-renovate-renovate-41.x
  • feat/gnupg
  • fix/36615b-branch-reuse-no-cache
  • renovate/main-redis-5.x
  • chore/punycode
  • refactor/pin-new-value
  • feat/36219--git-x509-signing
  • feat/structured-logger
  • hotfix/39.264.1
  • feat/skip-dangling
  • gh-readonly-queue/next/pr-36034-7a061c4ca1024a19e2c295d773d9642625d1c2be
  • hotfix/39.238.3
  • refactor/gitlab-auto-approve
  • feat/template-strings
  • gh-readonly-queue/next/pr-35654-137d934242c784e0c45d4b957362214f0eade1d7
  • 41.43.5
  • 41.43.4
  • 41.43.3
  • 41.43.2
  • 41.43.1
  • 41.43.0
  • 41.42.12
  • 41.42.11
  • 41.42.10
  • 41.42.9
  • 41.42.8
  • 41.42.7
  • 41.42.6
  • 41.42.5
  • 41.42.4
  • 41.42.3
  • 41.42.2
  • 41.42.1
  • 41.42.0
  • 41.41.0
41 results

index.spec.ts

Blame
  • user avatar
    Michael Kriese authored and GitHub committed
    7d2a3018
    History
    index.spec.ts 33.67 KiB
    import fs from 'fs-extra';
    import Git from 'simple-git';
    import tmp from 'tmp-promise';
    import { logger, mocked } from '../../../test/util';
    import { GlobalConfig } from '../../config/global';
    import {
      CONFIG_VALIDATION,
      INVALID_PATH,
    } from '../../constants/error-messages';
    import { newlineRegex, regEx } from '../regex';
    import * as _behindBaseCache from './behind-base-branch-cache';
    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('./behind-base-branch-cache');
    jest.mock('./modified-cache');
    jest.mock('timers/promises');
    jest.mock('../cache/repository');
    const behindBaseCache = mocked(_behindBaseCache);
    const conflictsCache = mocked(_conflictsCache);
    const modifiedCache = mocked(_modifiedCache);
    // Class is no longer exported
    const SimpleGit = Git().constructor as { prototype: ReturnType<typeof Git> };
    
    describe('util/git/index', () => {
      jest.setTimeout(60000);
    
      const masterCommitDate = new Date();
      masterCommitDate.setMilliseconds(0);
      let base: tmp.DirectoryResult;
      let origin: tmp.DirectoryResult;
      let defaultBranch: string;
    
      beforeAll(async () => {
        base = await tmp.dir({ unsafeCleanup: true });
        const repo = Git(base.path);
        await repo.init();
        defaultBranch = (await repo.raw('branch', '--show-current')).trim();
        await repo.addConfig('user.email', 'Jest@example.com');
        await repo.addConfig('user.name', 'Jest');
        await fs.writeFile(base.path + '/past_file', 'past');
        await repo.addConfig('commit.gpgsign', 'false');
        await repo.add(['past_file']);
        await repo.commit('past message');
    
        await repo.checkout(['-b', 'renovate/past_branch', defaultBranch]);
        await repo.checkout(['-b', 'develop', defaultBranch]);
    
        await repo.checkout(defaultBranch);
        await fs.writeFile(base.path + '/master_file', defaultBranch);
        await fs.writeFile(base.path + '/file_to_delete', 'bye');
        await repo.add(['master_file', 'file_to_delete']);
        await repo.commit('master message', [
          '--date=' + masterCommitDate.toISOString(),
        ]);
    
        await repo.checkout(['-b', 'renovate/future_branch', defaultBranch]);
        await fs.writeFile(base.path + '/future_file', 'future');
        await repo.add(['future_file']);
        await repo.commit('future message');
    
        await repo.checkoutBranch('renovate/modified_branch', defaultBranch);
        await fs.writeFile(base.path + '/base_file', 'base');
        await repo.add(['base_file']);
        await repo.commit('base message');
        await fs.writeFile(base.path + '/modified_file', 'modified');
        await repo.add(['modified_file']);
        await repo.commit('modification');
    
        await repo.checkoutBranch('renovate/custom_author', defaultBranch);
        await fs.writeFile(base.path + '/custom_file', 'custom');
        await repo.add(['custom_file']);
        await repo.addConfig('user.email', 'custom@example.com');
        await repo.commit('custom message');
    
        await repo.checkoutBranch('renovate/nested_files', defaultBranch);
        await fs.mkdirp(base.path + '/bin/');
        await fs.writeFile(base.path + '/bin/nested', 'nested');
        await fs.writeFile(base.path + '/root', 'root');
        await repo.add(['root', 'bin/nested']);
        await repo.addConfig('user.email', 'custom@example.com');
        await repo.commit('nested message');
    
        await repo.checkoutBranch('renovate/equal_branch', defaultBranch);
    
        await repo.checkout(defaultBranch);
      });
    
      let tmpDir: tmp.DirectoryResult;
    
      const OLD_ENV = process.env;
    
      beforeEach(async () => {
        process.env = { ...OLD_ENV };
        origin = await tmp.dir({ unsafeCleanup: true });
        const repo = Git(origin.path);
        await repo.clone(base.path, '.', ['--bare']);
        await repo.addConfig('commit.gpgsign', 'false');
        tmpDir = await tmp.dir({ unsafeCleanup: true });
        GlobalConfig.set({ localDir: tmpDir.path });
        await git.initRepo({
          url: origin.path,
        });
        git.setUserRepoConfig({ branchPrefix: 'renovate/' });
        git.setGitAuthor('Jest <Jest@example.com>');
        setNoVerify([]);
        await git.syncGit();
        // override some local git settings for better testing
        const local = Git(tmpDir.path);
        await local.addConfig('commit.gpgsign', 'false');
        behindBaseCache.getCachedBehindBaseResult.mockReturnValue(null);
      });
    
      afterEach(async () => {
        await tmpDir?.cleanup();
        await origin?.cleanup();
        jest.restoreAllMocks();
      });
    
      afterAll(async () => {
        process.env = OLD_ENV;
        await base?.cleanup();
      });
    
      describe('gitRetry', () => {
        it('returns result if git returns successfully', async () => {
          const gitFunc = jest.fn().mockImplementation((args) => {
            if (args === undefined) {
              return 'some result';
            } else {
              return 'different result';
            }
          });
          expect(await git.gitRetry(() => gitFunc())).toBe('some result');
          expect(await git.gitRetry(() => gitFunc('arg'))).toBe('different result');
          expect(gitFunc).toHaveBeenCalledTimes(2);
        });
    
        it('retries the func call if ExternalHostError thrown', async () => {
          process.env.NODE_ENV = '';
          const gitFunc = jest
            .fn()
            .mockImplementationOnce(() => {
              throw new Error('The remote end hung up unexpectedly');
            })
            .mockImplementationOnce(() => {
              throw new Error('The remote end hung up unexpectedly');
            })
            .mockImplementationOnce(() => 'some result');
          expect(await git.gitRetry(() => gitFunc())).toBe('some result');
          expect(gitFunc).toHaveBeenCalledTimes(3);
        });
    
        it('retries the func call up to retry count if ExternalHostError thrown', async () => {
          process.env.NODE_ENV = '';
          const gitFunc = jest.fn().mockImplementation(() => {
            throw new Error('The remote end hung up unexpectedly');
          });
          await expect(git.gitRetry(() => gitFunc())).rejects.toThrow(
            'The remote end hung up unexpectedly',
          );
          expect(gitFunc).toHaveBeenCalledTimes(6);
        });
    
        it("doesn't retry and throws an Error if non-ExternalHostError thrown by git", async () => {
          const gitFunc = jest.fn().mockImplementationOnce(() => {
            throw new Error('some error');
          });
          await expect(git.gitRetry(() => gitFunc())).rejects.toThrow('some error');
          expect(gitFunc).toHaveBeenCalledTimes(1);
        });
      });
    
      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();
        });
    
        it('sets non-master base branch', async () => {
          await expect(git.checkoutBranch('develop')).resolves.not.toThrow();
        });
      });
    
      describe('getFileList()', () => {
        it('should return the correct files', async () => {
          expect(await git.getFileList()).toEqual([
            'file_to_delete',
            'master_file',
            'past_file',
          ]);
        });
    
        it('should exclude submodules', async () => {
          const repo = Git(base.path);
          await repo.submoduleAdd(base.path, 'submodule');
          await repo.submoduleAdd(base.path, 'file');
          await repo.commit('Add submodules');
          await git.initRepo({
            cloneSubmodules: true,
            url: base.path,
          });
          await git.syncGit();
          expect(await fs.pathExists(tmpDir.path + '/.gitmodules')).toBeTruthy();
          expect(await git.getFileList()).toEqual([
            '.gitmodules',
            'file_to_delete',
            'master_file',
            'past_file',
          ]);
          await repo.reset(['--hard', 'HEAD^']);
        });
      });
    
      describe('branchExists(branchName)', () => {
        it('should return true if found', () => {
          expect(git.branchExists('renovate/future_branch')).toBeTrue();
        });
    
        it('should return false if not found', () => {
          expect(git.branchExists('not_found')).toBeFalse();
        });
      });
    
      describe('getBranchList()', () => {
        it('should return all branches', () => {
          const res = git.getBranchList();
          expect(res).toContain('renovate/past_branch');
          expect(res).toContain('renovate/future_branch');
          expect(res).toContain(defaultBranch);
        });
      });
    
      describe('isBranchBehindBase()', () => {
        it('should return false if same SHA as master', async () => {
          expect(
            await git.isBranchBehindBase('renovate/future_branch', defaultBranch),
          ).toBeFalse();
        });
    
        it('should return true if SHA different from master', async () => {
          expect(
            await git.isBranchBehindBase('renovate/past_branch', defaultBranch),
          ).toBeTrue();
        });
    
        it('should return result even if non-default and not under branchPrefix', async () => {
          expect(await git.isBranchBehindBase('develop', defaultBranch)).toBeTrue();
        });
    
        it('returns cached value', async () => {
          behindBaseCache.getCachedBehindBaseResult.mockReturnValue(true);
          expect(await git.isBranchBehindBase('develop', defaultBranch)).toBeTrue();
          expect(logger.logger.debug).toHaveBeenCalledWith(
            'branch.isBehindBase(): using cached result "true"',
          );
        });
      });
    
      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();
        });
    
        it('should return false when author matches', async () => {
          expect(await git.isBranchModified('renovate/future_branch')).toBeFalse();
          expect(await git.isBranchModified('renovate/future_branch')).toBeFalse();
        });
    
        it('should return false when author is ignored', async () => {
          git.setUserRepoConfig({
            gitIgnoredAuthors: ['custom@example.com'],
          });
          expect(await git.isBranchModified('renovate/custom_author')).toBeFalse();
        });
    
        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)', () => {
        it('should return same value for equal refs', () => {
          const hex = git.getBranchCommit('renovate/equal_branch');
          expect(hex).toBe(git.getBranchCommit(defaultBranch));
          expect(hex).toHaveLength(40);
        });
    
        it('should return null', () => {
          expect(git.getBranchCommit('not_found')).toBeNull();
        });
      });
    
      describe('getBranchFiles(branchName)', () => {
        it('detects changed files compared to current base branch', async () => {
          const file: FileChange = {
            type: 'addition',
            path: 'some-new-file',
            contents: 'some new-contents',
          };
          await git.commitFiles({
            branchName: 'renovate/branch_with_changes',
            files: [
              file,
              { type: 'addition', path: 'dummy', contents: null as never },
            ],
            message: 'Create something',
          });
          const branchFiles = await git.getBranchFiles(
            'renovate/branch_with_changes',
          );
          expect(branchFiles).toEqual(['some-new-file']);
        });
      });
    
      describe('mergeBranch(branchName)', () => {
        it('should perform a branch merge', async () => {
          await git.mergeBranch('renovate/future_branch');
          const merged = await Git(origin.path).branch([
            '--verbose',
            '--merged',
            defaultBranch,
          ]);
          expect(merged.all).toContain('renovate/future_branch');
        });
    
        it('should throw if branch merge throws', async () => {
          await expect(git.mergeBranch('not_found')).rejects.toThrow();
        });
      });
    
      describe('mergeToLocal(branchName)', () => {
        it('should perform a branch merge without push', async () => {
          expect(fs.existsSync(`${tmpDir.path}/future_file`)).toBeFalse();
          const pushSpy = jest.spyOn(SimpleGit.prototype, 'push');
    
          await git.mergeToLocal('renovate/future_branch');
    
          expect(fs.existsSync(`${tmpDir.path}/future_file`)).toBeTrue();
          expect(pushSpy).toHaveBeenCalledTimes(0);
        });
    
        it('should throw', async () => {
          await expect(git.mergeToLocal('not_found')).rejects.toThrow();
        });
      });
    
      describe('deleteBranch(branchName)', () => {
        it('should send delete', async () => {
          await git.deleteBranch('renovate/past_branch');
          const branches = await Git(origin.path).branch({});
          expect(branches.all).not.toContain('renovate/past_branch');
        });
      });
    
      describe('getBranchLastCommitTime', () => {
        it('should return a Date', async () => {
          const time = await git.getBranchLastCommitTime(defaultBranch);
          expect(time).toEqual(masterCommitDate);
        });
    
        it('handles error', async () => {
          const res = await git.getBranchLastCommitTime('some-branch');
          expect(res).toBeDefined();
        });
      });
    
      describe('getFile(filePath, branchName)', () => {
        it('gets the file', async () => {
          const res = await git.getFile('master_file');
          expect(res).toBe(defaultBranch);
        });
    
        it('short cuts 404', async () => {
          const res = await git.getFile('some-missing-path');
          expect(res).toBeNull();
        });
    
        it('returns null for 404', async () => {
          expect(await git.getFile('some-path', 'some-branch')).toBeNull();
        });
      });
    
      describe('getFiles(filePath)', () => {
        it('gets the file', async () => {
          const res = await git.getFiles(['master_file', 'some_missing_path']);
          expect(res).toEqual({
            master_file: defaultBranch,
            some_missing_path: null,
          });
        });
      });
    
      describe('hasDiff(sourceRef, targetRef)', () => {
        it('compare without changes', () => {
          return expect(git.hasDiff('HEAD', 'HEAD')).resolves.toBeFalse();
        });
    
        it('compare with changes', () => {
          return expect(
            git.hasDiff('origin/master', 'origin/renovate/future_branch'),
          ).resolves.toBeTrue();
        });
      });
    
      describe('commitFiles({branchName, files, message})', () => {
        it('creates file', async () => {
          const file: FileChange = {
            type: 'addition',
            path: 'some-new-file',
            contents: 'some new-contents',
          };
          const commit = await git.commitFiles({
            branchName: 'renovate/past_branch',
            files: [file],
            message: 'Create something',
          });
          expect(commit).not.toBeNull();
        });
    
        it('link file', async () => {
          const file: FileChange = {
            type: 'addition',
            path: 'future_link',
            contents: 'past_file',
            isSymlink: true,
          };
          const commit = await git.commitFiles({
            branchName: 'renovate/future_branch',
            files: [file],
            message: 'Create a link',
          });
          expect(commit).toBeString();
          const tmpGit = Git(tmpDir.path);
          const lsTree = await tmpGit.raw(['ls-tree', commit!]);
          const files = lsTree
            .trim()
            .split(newlineRegex)
            .map((x) => x.split(/\s/))
            .map(([mode, type, _hash, name]) => [mode, type, name]);
          expect(files).toContainEqual(['100644', 'blob', 'past_file']);
          expect(files).toContainEqual(['120000', 'blob', 'future_link']);
        });
    
        it('deletes file', async () => {
          const file: FileChange = {
            type: 'deletion',
            path: 'file_to_delete',
          };
          const commit = await git.commitFiles({
            branchName: 'renovate/something',
            files: [file],
            message: 'Delete something',
          });
          expect(commit).not.toBeNull();
        });
    
        it('updates multiple files', async () => {
          const files: FileChange[] = [
            {
              type: 'addition',
              path: 'some-existing-file',
              contents: 'updated content',
            },
            {
              type: 'addition',
              path: 'some-other-existing-file',
              contents: 'other updated content',
            },
          ];
          const commit = await git.commitFiles({
            branchName: 'renovate/something',
            files,
            message: 'Update something',
          });
          expect(commit).not.toBeNull();
        });
    
        it('uses right commit SHA', async () => {
          const files: FileChange[] = [
            {
              type: 'addition',
              path: 'some-existing-file',
              contents: 'updated content',
            },
            {
              type: 'addition',
              path: 'some-other-existing-file',
              contents: 'other updated content',
            },
          ];
          const commitConfig = {
            baseBranch: 'renovate/something',
            branchName: 'renovate/something',
            files,
            message: 'Update something',
          };
          const commitSha = await git.commitFiles(commitConfig);
          const remoteSha = await git.fetchBranch(commitConfig.branchName);
          expect(commitSha).toEqual(remoteSha);
        });
    
        it('updates git submodules', async () => {
          const files: FileChange[] = [
            {
              type: 'addition',
              path: '.',
              contents: 'some content',
            },
          ];
          const commit = await git.commitFiles({
            branchName: 'renovate/something',
            files,
            message: 'Update something',
          });
          expect(commit).toBeNull();
        });
    
        it('does not push when no diff', async () => {
          const files: FileChange[] = [
            {
              type: 'addition',
              path: 'future_file',
              contents: 'future',
            },
          ];
          const commit = await git.commitFiles({
            branchName: 'renovate/future_branch',
            files,
            message: 'No change update',
          });
          expect(commit).toBeNull();
        });
    
        it('does not pass --no-verify', async () => {
          const commitSpy = jest.spyOn(SimpleGit.prototype, 'commit');
          const pushSpy = jest.spyOn(SimpleGit.prototype, 'push');
    
          const files: FileChange[] = [
            {
              type: 'addition',
              path: 'some-new-file',
              contents: 'some new-contents',
            },
          ];
    
          await git.commitFiles({
            branchName: 'renovate/something',
            files,
            message: 'Pass no-verify',
          });
    
          expect(commitSpy).toHaveBeenCalledWith(
            expect.anything(),
            expect.anything(),
            expect.not.objectContaining({ '--no-verify': null }),
          );
          expect(pushSpy).toHaveBeenCalledWith(
            expect.anything(),
            expect.anything(),
            expect.not.objectContaining({ '--no-verify': null }),
          );
        });
    
        it('passes --no-verify to commit', async () => {
          const commitSpy = jest.spyOn(SimpleGit.prototype, 'commit');
          const pushSpy = jest.spyOn(SimpleGit.prototype, 'push');
    
          const files: FileChange[] = [
            {
              type: 'addition',
              path: 'some-new-file',
              contents: 'some new-contents',
            },
          ];
          setNoVerify(['commit']);
    
          await git.commitFiles({
            branchName: 'renovate/something',
            files,
            message: 'Pass no-verify',
          });
    
          expect(commitSpy).toHaveBeenCalledWith(
            expect.anything(),
            expect.anything(),
            expect.objectContaining({ '--no-verify': null }),
          );
          expect(pushSpy).toHaveBeenCalledWith(
            expect.anything(),
            expect.anything(),
            expect.not.objectContaining({ '--no-verify': null }),
          );
        });
    
        it('passes --no-verify to push', async () => {
          const commitSpy = jest.spyOn(SimpleGit.prototype, 'commit');
          const pushSpy = jest.spyOn(SimpleGit.prototype, 'push');
    
          const files: FileChange[] = [
            {
              type: 'addition',
              path: 'some-new-file',
              contents: 'some new-contents',
            },
          ];
          setNoVerify(['push']);
    
          await git.commitFiles({
            branchName: 'renovate/something',
            files,
            message: 'Pass no-verify',
          });
    
          expect(commitSpy).toHaveBeenCalledWith(
            expect.anything(),
            expect.anything(),
            expect.not.objectContaining({ '--no-verify': null }),
          );
          expect(pushSpy).toHaveBeenCalledWith(
            expect.anything(),
            expect.anything(),
            expect.objectContaining({ '--no-verify': null }),
          );
        });
    
        it('creates file with the executable bit', async () => {
          const file: FileChange = {
            type: 'addition',
            path: 'some-executable',
            contents: 'some new-contents',
            isExecutable: true,
          };
          const commit = await git.commitFiles({
            branchName: 'renovate/past_branch',
            files: [file],
            message: 'Create something',
          });
          expect(commit).not.toBeNull();
    
          const repo = Git(tmpDir.path);
          const result = await repo.raw(['ls-tree', 'HEAD', 'some-executable']);
          expect(result).toStartWith('100755');
        });
      });
    
      describe('getCommitMessages()', () => {
        it('returns commit messages', async () => {
          expect(await git.getCommitMessages()).toEqual([
            'master message',
            'past message',
          ]);
        });
      });
    
      describe('Storage.getUrl()', () => {
        const getUrl = git.getUrl;
    
        it('returns https url', () => {
          expect(
            getUrl({
              protocol: 'https',
              auth: 'user:pass',
              hostname: 'host',
              repository: 'some/repo',
            }),
          ).toBe('https://user:pass@host/some/repo.git');
          expect(
            getUrl({
              auth: 'user:pass',
              hostname: 'host',
              repository: 'some/repo',
            }),
          ).toBe('https://user:pass@host/some/repo.git');
        });
    
        it('returns ssh url', () => {
          expect(
            getUrl({
              protocol: 'ssh',
              auth: 'user:pass',
              hostname: 'host',
              repository: 'some/repo',
            }),
          ).toBe('git@host:some/repo.git');
        });
      });
    
      describe('initRepo())', () => {
        it('should fetch latest', async () => {
          const repo = Git(base.path);
          await repo.checkout(['-b', 'test', defaultBranch]);
          await fs.writeFile(base.path + '/test', 'lorem ipsum');
          await repo.add(['test']);
          await repo.commit('past message2');
          await repo.checkout(defaultBranch);
    
          expect(git.branchExists('test')).toBeFalsy();
    
          expect(await git.getCommitMessages()).toEqual([
            'master message',
            'past message',
          ]);
    
          await git.checkoutBranch('develop');
    
          await git.initRepo({
            url: base.path,
          });
    
          expect(git.branchExists('test')).toBeTruthy();
    
          await git.checkoutBranch('test');
    
          const msg = await git.getCommitMessages();
          expect(msg).toEqual(['past message2', 'master message', 'past message']);
          expect(msg).toContain('past message2');
        });
    
        it('should set branch prefix', async () => {
          const repo = Git(base.path);
          await repo.checkout(['-b', 'renovate/test', defaultBranch]);
          await fs.writeFile(base.path + '/test', 'lorem ipsum');
          await repo.add(['test']);
          await repo.commit('past message2');
          await repo.checkout(defaultBranch);
    
          await git.initRepo({
            url: base.path,
          });
    
          git.setUserRepoConfig({ branchPrefix: 'renovate/' });
          expect(git.branchExists('renovate/test')).toBeTrue();
    
          await git.initRepo({
            url: base.path,
          });
    
          await repo.checkout('renovate/test');
          await repo.commit('past message3', ['--amend']);
    
          git.setUserRepoConfig({ branchPrefix: 'renovate/' });
          expect(git.branchExists('renovate/test')).toBeTrue();
        });
    
        it('should fail clone ssh submodule', async () => {
          const repo = Git(base.path);
          await fs.writeFile(
            base.path + '/.gitmodules',
            '[submodule "test"]\npath=test\nurl=ssh://0.0.0.0',
          );
          await repo.add('.gitmodules');
          await repo.raw([
            'update-index',
            '--add',
            '--cacheinfo',
            '160000',
            '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
            'test',
          ]);
          await repo.commit('Add submodule');
          await git.initRepo({
            cloneSubmodules: true,
            url: base.path,
          });
          await git.syncGit();
          expect(await fs.pathExists(tmpDir.path + '/.gitmodules')).toBeTruthy();
          await repo.reset(['--hard', 'HEAD^']);
        });
    
        it('should use extra clone configuration', async () => {
          await fs.emptyDir(tmpDir.path);
          await git.initRepo({
            url: origin.path,
            extraCloneOpts: {
              '-c': 'extra.clone.config=test-extra-config-value',
            },
            fullClone: true,
          });
          git.getBranchCommit(defaultBranch);
          await git.syncGit();
          const repo = Git(tmpDir.path);
          const res = (await repo.raw(['config', 'extra.clone.config'])).trim();
          expect(res).toBe('test-extra-config-value');
        });
      });
    
      describe('setGitAuthor()', () => {
        it('throws for invalid', () => {
          expect(() => git.setGitAuthor('invalid')).toThrow(CONFIG_VALIDATION);
        });
      });
    
      describe('isBranchConflicted', () => {
        beforeAll(async () => {
          const repo = Git(base.path);
          await repo.init();
    
          await repo.checkout(['-b', 'renovate/conflicted_branch', defaultBranch]);
          await repo.checkout([
            '-b',
            'renovate/non_conflicted_branch',
            defaultBranch,
          ]);
    
          await repo.checkout(defaultBranch);
          await fs.writeFile(base.path + '/one_file', 'past (updated)');
          await repo.add(['one_file']);
          await repo.commit('past (updated) message');
    
          await repo.checkout('renovate/conflicted_branch');
          await fs.writeFile(base.path + '/one_file', 'past (updated branch)');
          await repo.add(['one_file']);
          await repo.commit('past (updated branch) message');
    
          await repo.checkout('renovate/non_conflicted_branch');
          await fs.writeFile(base.path + '/another_file', 'other');
          await repo.add(['another_file']);
          await repo.commit('other (updated branch) message');
    
          await repo.checkout(defaultBranch);
    
          conflictsCache.getCachedConflictResult.mockReturnValue(null);
        });
    
        it('returns true for non-existing source branch', async () => {
          const res = await git.isBranchConflicted(
            defaultBranch,
            'renovate/non_existing_branch',
          );
          expect(res).toBeTrue();
        });
    
        it('returns true for non-existing target branch', async () => {
          const res = await git.isBranchConflicted(
            'renovate/non_existing_branch',
            'renovate/non_conflicted_branch',
          );
          expect(res).toBeTrue();
        });
    
        it('detects conflicted branch', async () => {
          const branchBefore = 'renovate/non_conflicted_branch';
          await git.checkoutBranch(branchBefore);
    
          const res = await git.isBranchConflicted(
            defaultBranch,
            'renovate/conflicted_branch',
          );
    
          expect(res).toBeTrue();
    
          const status = await git.getRepoStatus();
          expect(status.current).toEqual(branchBefore);
          expect(status.isClean()).toBeTrue();
        });
    
        it('detects non-conflicted branch', async () => {
          const branchBefore = 'renovate/conflicted_branch';
          await git.checkoutBranch(branchBefore);
    
          const res = await git.isBranchConflicted(
            defaultBranch,
            'renovate/non_conflicted_branch',
          );
    
          expect(res).toBeFalse();
    
          const status = await git.getRepoStatus();
          expect(status.current).toEqual(branchBefore);
          expect(status.isClean()).toBeTrue();
        });
    
        describe('cachedConflictResult', () => {
          it('returns cached values', async () => {
            conflictsCache.getCachedConflictResult.mockReturnValue(true);
    
            const res = await git.isBranchConflicted(
              defaultBranch,
              'renovate/conflicted_branch',
            );
    
            expect(res).toBeTrue();
            expect(conflictsCache.getCachedConflictResult.mock.calls).toEqual([
              [
                'renovate/conflicted_branch',
                git.getBranchCommit('renovate/conflicted_branch'),
                defaultBranch,
                git.getBranchCommit(defaultBranch),
              ],
            ]);
            expect(conflictsCache.setCachedConflictResult).not.toHaveBeenCalled();
          });
    
          it('caches truthy return value', async () => {
            conflictsCache.getCachedConflictResult.mockReturnValue(null);
    
            const res = await git.isBranchConflicted(
              defaultBranch,
              'renovate/conflicted_branch',
            );
    
            expect(res).toBeTrue();
            expect(conflictsCache.setCachedConflictResult.mock.calls).toEqual([
              ['renovate/conflicted_branch', true],
            ]);
          });
    
          it('caches falsy return value', async () => {
            conflictsCache.getCachedConflictResult.mockReturnValue(null);
    
            const res = await git.isBranchConflicted(
              defaultBranch,
              'renovate/non_conflicted_branch',
            );
    
            expect(res).toBeFalse();
            expect(conflictsCache.setCachedConflictResult.mock.calls).toEqual([
              ['renovate/non_conflicted_branch', false],
            ]);
          });
        });
      });
    
      describe('Renovate non-branch refs', () => {
        const lsRenovateRefs = async (): Promise<string[]> =>
          (await Git(tmpDir.path).raw(['ls-remote', 'origin', 'refs/renovate/*']))
            .split(newlineRegex)
            .map((line) => line.replace(regEx(/[0-9a-f]+\s+/i), ''))
            .filter(Boolean);
    
        it('creates renovate ref in default section', async () => {
          const commit = git.getBranchCommit('develop')!;
    
          await git.pushCommitToRenovateRef(commit, 'foo/bar');
    
          const renovateRefs = await lsRenovateRefs();
          expect(renovateRefs).toContain('refs/renovate/branches/foo/bar');
        });
    
        it('creates custom section for renovate ref', async () => {
          const commit = git.getBranchCommit('develop')!;
    
          await git.pushCommitToRenovateRef(commit, 'bar/baz', 'foo');
    
          const renovateRefs = await lsRenovateRefs();
          expect(renovateRefs).toContain('refs/renovate/foo/bar/baz');
        });
    
        it('clears pushed Renovate refs', async () => {
          const commit = git.getBranchCommit('develop')!;
          await git.pushCommitToRenovateRef(commit, 'foo');
          await git.pushCommitToRenovateRef(commit, 'bar');
          await git.pushCommitToRenovateRef(commit, 'baz');
    
          expect(await lsRenovateRefs()).not.toBeEmpty();
          await git.clearRenovateRefs();
          expect(await lsRenovateRefs()).toBeEmpty();
        });
    
        it('clears remote Renovate refs', async () => {
          const commit = git.getBranchCommit('develop')!;
          const tmpGit = Git(tmpDir.path);
          await tmpGit.raw(['update-ref', 'refs/renovate/aaa', commit]);
          await tmpGit.raw(['push', '--force', 'origin', 'refs/renovate/aaa']);
    
          await git.pushCommitToRenovateRef(commit, 'bbb');
          await git.pushCommitToRenovateRef(commit, 'ccc', 'branches');
    
          const pushSpy = jest.spyOn(SimpleGit.prototype, 'push');
    
          expect(await lsRenovateRefs()).not.toBeEmpty();
          await git.clearRenovateRefs();
          expect(await lsRenovateRefs()).toBeEmpty();
          expect(pushSpy).toHaveBeenCalledOnce();
        });
    
        it('preserves unknown sections by default', async () => {
          const commit = git.getBranchCommit('develop')!;
          const tmpGit = Git(tmpDir.path);
          await tmpGit.raw(['update-ref', 'refs/renovate/foo/bar', commit]);
          await tmpGit.raw(['push', '--force', 'origin', 'refs/renovate/foo/bar']);
          await git.clearRenovateRefs();
          expect(await lsRenovateRefs()).toEqual(['refs/renovate/foo/bar']);
        });
    
        it('falls back to sequential ref deletion if bulk changes are disallowed', async () => {
          const commit = git.getBranchCommit('develop')!;
          await git.pushCommitToRenovateRef(commit, 'foo');
          await git.pushCommitToRenovateRef(commit, 'bar');
          await git.pushCommitToRenovateRef(commit, 'baz');
    
          const pushSpy = jest.spyOn(SimpleGit.prototype, 'push');
          pushSpy.mockImplementationOnce(() => {
            throw new Error(
              'remote: Repository policies do not allow pushes that update more than 2 branches or tags.',
            );
          });
    
          expect(await lsRenovateRefs()).not.toBeEmpty();
          await git.clearRenovateRefs();
          expect(await lsRenovateRefs()).toBeEmpty();
          expect(pushSpy).toHaveBeenCalledTimes(4);
        });
      });
    
      describe('listCommitTree', () => {
        it('creates non-branch ref', async () => {
          const commit = git.getBranchCommit('develop')!;
          const res = await git.listCommitTree(commit);
          expect(res).toEqual([
            {
              mode: '100644',
              path: 'past_file',
              sha: '913705ab2ca79368053a476efa48aa6912d052c5',
              type: 'blob',
            },
          ]);
        });
      });
    
      describe('getRepoStatus', () => {
        it('should pass options into git status', async () => {
          await git.checkoutBranch('renovate/nested_files');
    
          await fs.writeFile(tmpDir.path + '/bin/nested', 'new nested');
          await fs.writeFile(tmpDir.path + '/root', 'new root');
          const resp = await git.getRepoStatus('bin');
    
          expect(resp.modified).toStrictEqual(['bin/nested']);
        });
    
        it('should reject when trying to access directory out of localDir', async () => {
          GlobalConfig.set({ localDir: tmpDir.path });
          await git.checkoutBranch('renovate/nested_files');
    
          await fs.writeFile(tmpDir.path + '/bin/nested', 'new nested');
          await fs.writeFile(tmpDir.path + '/root', 'new root');
    
          await expect(git.getRepoStatus('../../bin')).rejects.toThrow(
            INVALID_PATH,
          );
        });
      });
    
      describe('getSubmodules', () => {
        it('should return empty array', async () => {
          expect(await git.getSubmodules()).toHaveLength(0);
        });
      });
    
      describe('fetchRevSpec()', () => {
        it('fetchRevSpec()', async () => {
          await git.fetchRevSpec(
            `refs/heads/${defaultBranch}:refs/heads/other/${defaultBranch}`,
          );
          //checkout this duplicate
          const sha = await git.checkoutBranch(`other/${defaultBranch}`);
          expect(sha).toBe(git.getBranchCommit(defaultBranch));
        });
      });
    });