Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
# running from unless specified. Example URLs are https://github.com or
# https://my-ghes-server.example.com
github-server-url: ''

# Path to a local directory used as a reference cache for Git clones. Over time,
# this directory will contain bare clones of the checked-out repositories (and
# their submodules). Using this significantly reduces network bandwidth and speeds
# up clones.
reference-cache: ''
```
<!-- end usage -->

Expand Down
6 changes: 5 additions & 1 deletion __test__/git-auth-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1047,8 +1047,10 @@ async function setup(testName: string): Promise<void> {
lfsFetch: jest.fn(),
lfsInstall: jest.fn(),
log1: jest.fn(),
referenceAdd: jest.fn(),
remoteAdd: jest.fn(),
removeEnvironmentVariable: jest.fn((name: string) => delete git.env[name]),
execGit: jest.fn(),
revParse: jest.fn(),
setEnvironmentVariable: jest.fn((name: string, value: string) => {
git.env[name] = value
Expand Down Expand Up @@ -1157,6 +1159,7 @@ async function setup(testName: string): Promise<void> {
sparseCheckout: [],
sparseCheckoutConeMode: true,
fetchDepth: 1,
fetchDepthExplicit: false,
fetchTags: false,
showProgress: true,
lfs: false,
Expand All @@ -1173,7 +1176,8 @@ async function setup(testName: string): Promise<void> {
sshUser: '',
workflowOrganizationId: 123456,
setSafeDirectory: true,
githubServerUrl: githubServerUrl
githubServerUrl: githubServerUrl,
referenceCache: ''
}
}

Expand Down
109 changes: 109 additions & 0 deletions __test__/git-cache-helper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import * as path from 'path'
import * as fs from 'fs'
import * as io from '@actions/io'
import { GitCacheHelper } from '../src/git-cache-helper'
import { IGitCommandManager } from '../src/git-command-manager'

describe('GitCacheHelper', () => {
let cacheHelper: GitCacheHelper
let mockGit: jest.Mocked<IGitCommandManager>

const cacheDir = path.join(__dirname, 'test-cache')

beforeEach(async () => {
cacheHelper = new GitCacheHelper(cacheDir)
mockGit = {
execGit: jest.fn().mockImplementation(async (args) => {
// If git clone is called, simulate creating the destination dir
if (args && args.includes('clone')) {
const dest = args.find((a: string) => a.includes('.tmp.'));
if (dest) {
await io.mkdirP(dest);
} else {
console.log('No .tmp. found in args:', args);
}
}
return { exitCode: 0, stdout: '', stderr: '' };
}),
gitEnv: {}
} as any

await io.mkdirP(cacheDir)
})

afterEach(async () => {
await io.rmRF(cacheDir)
})

it('generates a consistent, short, and safe cache directory name', () => {
const url1 = 'https://github.com/mwyraz/forgejo-actions-checkout.git'
const name1 = (cacheHelper as any).generateCacheDirName(url1)

// Check structure: safe string + hash
expect(name1).toMatch(/^https___github_com_mwyraz_forgejo_actions_checkout_git_[0-9a-f]{8}\.git$/)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some errors can you check again this


// Same URL should produce the same directory name
const url1_duplicate = 'https://github.com/mwyraz/forgejo-actions-checkout.git'
expect((cacheHelper as any).generateCacheDirName(url1_duplicate)).toBe(name1)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you try this from ChatGPT so it's so easy work and short coding so now checking


// Different URL should produce a different directory name
const url2 = 'https://github.com/mwyraz/forgejo-actions-checkout-other.git'
expect((cacheHelper as any).generateCacheDirName(url2)).not.toBe(name1)

// SSH URL
const url3 = 'git@github.com:auth/repo.git'
const name3 = (cacheHelper as any).generateCacheDirName(url3)
expect(name3).toMatch(/^git_github_com_auth_repo_git_[0-9a-f]{8}\.git$/)

// Unclean URLs
const url4 = 'https://github.com/foo/bar.git?v=1'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here also mistak errors 404 recheck

const name4 = (cacheHelper as any).generateCacheDirName(url4)
expect(name4).toMatch(/^https___github_com_foo_bar_git_v_1_[0-9a-f]{8}\.git$/)
})

it('sets up a cache directory if it does not exist', async () => {
const repositoryUrl = 'https://github.com/mwyraz/test-repo.git'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error error complete check again then show me this i approve this than

const resultPath = await cacheHelper.setupCache(mockGit, repositoryUrl)

const expectedName = (cacheHelper as any).generateCacheDirName(repositoryUrl)
expect(resultPath).toBe(path.join(cacheDir, expectedName))

// It should have executed git clone --bare
expect(mockGit.execGit).toHaveBeenCalledWith(
expect.arrayContaining([
'-C',
cacheDir,
'clone',
'--bare',
repositoryUrl,
expect.stringContaining(`${expectedName}.tmp`) // should use tmp dir
])
)
})

it('fetches updates if the cache directory already exists', async () => {
const repositoryUrl = 'https://github.com/mwyraz/existing-repo.git'
const expectedName = (cacheHelper as any).generateCacheDirName(repositoryUrl)
const fixedPath = path.join(cacheDir, expectedName)

// Fake existing directory
await io.mkdirP(path.join(fixedPath, 'objects'))

const resultPath = await cacheHelper.setupCache(mockGit, repositoryUrl)
expect(resultPath).toBe(fixedPath)

// It should have executed git fetch
expect(mockGit.execGit).toHaveBeenCalledWith(
expect.arrayContaining([
'-C',
fixedPath,
'fetch',
'--force',
'--prune',
'--tags',
'origin',
'+refs/heads/*:refs/heads/*'
])
)
})
})
2 changes: 2 additions & 0 deletions __test__/git-directory-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,8 +478,10 @@ async function setup(testName: string): Promise<void> {
lfsFetch: jest.fn(),
lfsInstall: jest.fn(),
log1: jest.fn(),
referenceAdd: jest.fn(),
remoteAdd: jest.fn(),
removeEnvironmentVariable: jest.fn(),
execGit: jest.fn(),
revParse: jest.fn(),
setEnvironmentVariable: jest.fn(),
shaExists: jest.fn(),
Expand Down
182 changes: 182 additions & 0 deletions __test__/git-source-provider-reference-cache.test.ts
Original file line number Diff line number Diff line change
@@ -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')
)
})
})
Loading