diff --git a/__test__/git-source-provider-reference-cache.test.ts b/__test__/git-source-provider-reference-cache.test.ts new file mode 100644 index 0000000..4ce09c6 --- /dev/null +++ b/__test__/git-source-provider-reference-cache.test.ts @@ -0,0 +1,182 @@ +import * as path from 'path' + +const mockStartGroup = jest.fn() +const mockEndGroup = jest.fn() +const mockInfo = jest.fn() +const mockWarning = jest.fn() +const mockSetOutput = jest.fn() +const mockSetSecret = jest.fn() + +const mockCreateCommandManager = jest.fn() +const mockCreateAuthHelper = jest.fn() +const mockPrepareExistingDirectory = jest.fn() +const mockGetFetchUrl = jest.fn() +const mockGetRefSpec = jest.fn() +const mockTestRef = jest.fn() +const mockGetCheckoutInfo = jest.fn() +const mockCheckCommitInfo = jest.fn() +const mockSetRepositoryPath = jest.fn() +const mockSetupCache = jest.fn() +const mockDirectoryExistsSync = jest.fn() +const mockFileExistsSync = jest.fn() + +jest.mock('@actions/core', () => ({ + startGroup: mockStartGroup, + endGroup: mockEndGroup, + info: mockInfo, + warning: mockWarning, + setOutput: mockSetOutput, + setSecret: mockSetSecret +})) + +jest.mock('@actions/io', () => ({ + rmRF: jest.fn(), + mkdirP: jest.fn() +})) + +jest.mock('../src/fs-helper', () => ({ + directoryExistsSync: mockDirectoryExistsSync, + fileExistsSync: mockFileExistsSync +})) + +jest.mock('../src/git-command-manager', () => ({ + MinimumGitSparseCheckoutVersion: {}, + createCommandManager: mockCreateCommandManager +})) + +jest.mock('../src/git-auth-helper', () => ({ + createAuthHelper: mockCreateAuthHelper +})) + +jest.mock('../src/git-directory-helper', () => ({ + prepareExistingDirectory: mockPrepareExistingDirectory +})) + +jest.mock('../src/github-api-helper', () => ({ + downloadRepository: jest.fn(), + getDefaultBranch: jest.fn() +})) + +jest.mock('../src/ref-helper', () => ({ + getRefSpec: mockGetRefSpec, + getCheckoutInfo: mockGetCheckoutInfo, + testRef: mockTestRef, + checkCommitInfo: mockCheckCommitInfo +})) + +jest.mock('../src/state-helper', () => ({ + setRepositoryPath: mockSetRepositoryPath +})) + +jest.mock('../src/url-helper', () => ({ + getFetchUrl: mockGetFetchUrl +})) + +jest.mock('../src/git-cache-helper', () => ({ + GitCacheHelper: jest.fn().mockImplementation(() => ({ + setupCache: mockSetupCache + })) +})) + +import {getSource} from '../src/git-source-provider' + +describe('getSource reference cache regression', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('updates the reference cache and reconfigures alternates for existing repositories', async () => { + const repositoryPath = '/tmp/work/repo' + const repositoryUrl = 'https://github.com/actions/checkout' + const cachePath = '/tmp/reference-cache/actions-checkout.git' + + const mockGit = { + init: jest.fn(), + remoteAdd: jest.fn(), + referenceAdd: jest.fn().mockResolvedValue(undefined), + tryDisableAutomaticGarbageCollection: jest.fn().mockResolvedValue(true), + fetch: jest.fn().mockResolvedValue(undefined), + version: jest.fn().mockResolvedValue({ + checkMinimum: jest.fn().mockReturnValue(true) + }), + disableSparseCheckout: jest.fn().mockResolvedValue(undefined), + checkout: jest.fn().mockResolvedValue(undefined), + log1: jest + .fn() + .mockResolvedValueOnce('commit info') + .mockResolvedValueOnce('0123456789abcdef'), + lfsInstall: jest.fn(), + submoduleSync: jest.fn(), + submoduleUpdate: jest.fn(), + submoduleForeach: jest.fn(), + config: jest.fn() + } + + const mockAuthHelper = { + configureAuth: jest.fn().mockResolvedValue(undefined), + configureGlobalAuth: jest.fn().mockResolvedValue(undefined), + configureSubmoduleAuth: jest.fn().mockResolvedValue(undefined), + configureTempGlobalConfig: jest.fn().mockResolvedValue('/tmp/gitconfig'), + removeAuth: jest.fn().mockResolvedValue(undefined), + removeGlobalAuth: jest.fn().mockResolvedValue(undefined), + removeGlobalConfig: jest.fn().mockResolvedValue(undefined) + } + + mockCreateCommandManager.mockResolvedValue(mockGit) + mockCreateAuthHelper.mockReturnValue(mockAuthHelper) + mockPrepareExistingDirectory.mockResolvedValue(undefined) + mockGetFetchUrl.mockReturnValue(repositoryUrl) + mockGetRefSpec.mockReturnValue(['+refs/heads/main:refs/remotes/origin/main']) + mockTestRef.mockResolvedValue(true) + mockGetCheckoutInfo.mockResolvedValue({ + ref: 'refs/heads/main', + startPoint: 'refs/remotes/origin/main' + }) + mockCheckCommitInfo.mockResolvedValue(undefined) + mockSetupCache.mockResolvedValue(cachePath) + mockFileExistsSync.mockReturnValue(false) + mockDirectoryExistsSync.mockImplementation((targetPath: string) => { + return ( + targetPath === repositoryPath || + targetPath === path.join(repositoryPath, '.git') || + targetPath === path.join(cachePath, 'objects') + ) + }) + + await getSource({ + repositoryPath, + repositoryOwner: 'actions', + repositoryName: 'checkout', + ref: 'refs/heads/main', + commit: '0123456789abcdef', + clean: false, + filter: undefined, + sparseCheckout: undefined as any, + sparseCheckoutConeMode: false, + fetchDepth: 1, + fetchDepthExplicit: true, + fetchTags: false, + showProgress: false, + referenceCache: '/tmp/reference-cache', + lfs: false, + submodules: false, + nestedSubmodules: false, + authToken: 'token', + sshKey: '', + sshKnownHosts: '', + sshStrict: true, + sshUser: 'git', + persistCredentials: false, + workflowOrganizationId: undefined, + githubServerUrl: 'https://github.com', + setSafeDirectory: false + } as any) + + expect(mockGit.init).not.toHaveBeenCalled() + expect(mockGit.remoteAdd).not.toHaveBeenCalled() + expect(mockSetupCache).toHaveBeenCalledWith(mockGit, repositoryUrl) + expect(mockGit.referenceAdd).toHaveBeenCalledWith( + path.join(cachePath, 'objects') + ) + }) +}) diff --git a/__test__/git-source-provider.test.ts b/__test__/git-source-provider.test.ts index dd2d6f2..1fc5941 100644 --- a/__test__/git-source-provider.test.ts +++ b/__test__/git-source-provider.test.ts @@ -1,5 +1,10 @@ import * as core from '@actions/core' -import {adjustFetchDepthForCache} from '../src/git-source-provider' +import * as fsHelper from '../src/fs-helper' +import {GitCacheHelper} from '../src/git-cache-helper' +import { + adjustFetchDepthForCache, + setupReferenceCache +} from '../src/git-source-provider' // Mock @actions/core jest.mock('@actions/core') @@ -86,3 +91,73 @@ describe('adjustFetchDepthForCache', () => { ) }) }) + +describe('setupReferenceCache', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('does nothing when referenceCache is not set', async () => { + const git = { + referenceAdd: jest.fn() + } as any + + await setupReferenceCache(git, '', 'https://github.com/actions/checkout.git') + + expect(git.referenceAdd).not.toHaveBeenCalled() + expect(core.startGroup).not.toHaveBeenCalled() + }) + + it('updates the cache and configures alternates when cache objects exist', async () => { + const git = { + referenceAdd: jest.fn().mockResolvedValue(undefined) + } as any + const setupCacheSpy = jest + .spyOn(GitCacheHelper.prototype, 'setupCache') + .mockResolvedValue('/tmp/reference-cache/repo.git') + jest + .spyOn(fsHelper, 'directoryExistsSync') + .mockReturnValue(true) + + await setupReferenceCache( + git, + '/tmp/reference-cache', + 'https://github.com/actions/checkout.git' + ) + + expect(setupCacheSpy).toHaveBeenCalledWith( + git, + 'https://github.com/actions/checkout.git' + ) + expect(git.referenceAdd).toHaveBeenCalledWith( + '/tmp/reference-cache/repo.git/objects' + ) + }) + + it('warns when the cache objects directory is missing', async () => { + const git = { + referenceAdd: jest.fn().mockResolvedValue(undefined) + } as any + jest + .spyOn(GitCacheHelper.prototype, 'setupCache') + .mockResolvedValue('/tmp/reference-cache/repo.git') + jest + .spyOn(fsHelper, 'directoryExistsSync') + .mockReturnValue(false) + + await setupReferenceCache( + git, + '/tmp/reference-cache', + 'https://github.com/actions/checkout.git' + ) + + expect(git.referenceAdd).not.toHaveBeenCalled() + expect(core.warning).toHaveBeenCalledWith( + 'Reference repository cache objects directory /tmp/reference-cache/repo.git/objects does not exist' + ) + }) +}) diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts index ffe17e0..17134a5 100644 --- a/src/git-source-provider.ts +++ b/src/git-source-provider.ts @@ -23,6 +23,32 @@ interface SubmoduleInfo { url: string } +export async function setupReferenceCache( + git: IGitCommandManager, + referenceCache: string, + repositoryUrl: string +): Promise { + if (!referenceCache) { + return + } + + core.startGroup('Setting up reference repository cache') + try { + const cacheHelper = new GitCacheHelper(referenceCache) + const cachePath = await cacheHelper.setupCache(git, repositoryUrl) + const cacheObjects = path.join(cachePath, 'objects') + if (fsHelper.directoryExistsSync(cacheObjects, false)) { + await git.referenceAdd(cacheObjects) + } else { + core.warning( + `Reference repository cache objects directory ${cacheObjects} does not exist` + ) + } + } finally { + core.endGroup() + } +} + async function iterativeSubmoduleUpdate( git: IGitCommandManager, cacheHelper: GitCacheHelper, @@ -276,22 +302,10 @@ export async function getSource(settings: IGitSourceSettings): Promise { await git.init() await git.remoteAdd('origin', repositoryUrl) core.endGroup() - - // Setup reference cache if requested - if (settings.referenceCache) { - core.startGroup('Setting up reference repository cache') - const cacheHelper = new GitCacheHelper(settings.referenceCache) - const cachePath = await cacheHelper.setupCache(git, repositoryUrl) - const cacheObjects = path.join(cachePath, 'objects') - if (fsHelper.directoryExistsSync(cacheObjects, false)) { - await git.referenceAdd(cacheObjects) - } else { - core.warning(`Reference repository cache objects directory ${cacheObjects} does not exist`) - } - core.endGroup() - } } + await setupReferenceCache(git, settings.referenceCache, repositoryUrl) + // Remove global auth if it was set for reference cache, // to avoid duplicate AUTHORIZATION headers during fetch if (settings.referenceCache) {