Skip to content

Commit 67bd696

Browse files
committed
Fix checkout init for SHA-256 repositories
1 parent 900f221 commit 67bd696

8 files changed

Lines changed: 634 additions & 7 deletions

__test__/git-auth-helper.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,6 +1103,7 @@ async function setup(testName: string): Promise<void> {
11031103
),
11041104
tryDisableAutomaticGarbageCollection: jest.fn(),
11051105
tryGetFetchUrl: jest.fn(),
1106+
tryGetObjectFormat: jest.fn(async () => ({format: '', succeeded: true})),
11061107
tryGetConfigValues: jest.fn(
11071108
async (
11081109
key: string,

__test__/git-command-manager.test.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,169 @@ describe('Test fetchDepth and fetchTags options', () => {
378378
})
379379
})
380380

381+
describe('repository object format', () => {
382+
beforeEach(async () => {
383+
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
384+
jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
385+
})
386+
387+
afterEach(() => {
388+
jest.restoreAllMocks()
389+
})
390+
391+
it('detects SHA-256 from a 64-character HEAD oid', async () => {
392+
mockExec.mockImplementation((path, args, options) => {
393+
if (args.includes('version')) {
394+
options.listeners.stdout(Buffer.from('git version 2.50.1'))
395+
}
396+
397+
if (args.includes('ls-remote')) {
398+
options.listeners.stdout(
399+
Buffer.from(
400+
'ref: refs/heads/main\tHEAD\n' +
401+
'9422233ca7ee1b17f1e905d0e141faf0c401556c41cdc6acd71c6bd685da2e92\tHEAD\n'
402+
)
403+
)
404+
}
405+
406+
return 0
407+
})
408+
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
409+
410+
git = await commandManager.createCommandManager('test', false, false)
411+
412+
const objectFormat = await git.tryGetObjectFormat(
413+
'https://github.com/example/repo'
414+
)
415+
416+
expect(objectFormat).toEqual({format: 'sha256', succeeded: true})
417+
expect(mockExec).toHaveBeenCalledWith(
418+
expect.any(String),
419+
[
420+
'-c',
421+
'protocol.version=2',
422+
'ls-remote',
423+
'--quiet',
424+
'--exit-code',
425+
'--symref',
426+
'https://github.com/example/repo',
427+
'HEAD'
428+
],
429+
expect.objectContaining({
430+
ignoreReturnCode: true,
431+
silent: true
432+
})
433+
)
434+
})
435+
436+
it('detects SHA-1 from a 40-character HEAD oid', async () => {
437+
mockExec.mockImplementation((path, args, options) => {
438+
if (args.includes('version')) {
439+
options.listeners.stdout(Buffer.from('git version 2.50.1'))
440+
}
441+
442+
if (args.includes('ls-remote')) {
443+
options.listeners.stdout(
444+
Buffer.from(
445+
'ref: refs/heads/main\tHEAD\n' +
446+
'c988866043f035e6a46509872215f91d879044c9\tHEAD\n'
447+
)
448+
)
449+
}
450+
451+
return 0
452+
})
453+
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
454+
455+
git = await commandManager.createCommandManager('test', false, false)
456+
457+
await expect(
458+
git.tryGetObjectFormat('https://github.com/example/repo')
459+
).resolves.toEqual({format: 'sha1', succeeded: true})
460+
})
461+
462+
it('returns unsuccessful when HEAD does not resolve to a recognized object id', async () => {
463+
mockExec.mockImplementation((path, args, options) => {
464+
if (args.includes('version')) {
465+
options.listeners.stdout(Buffer.from('git version 2.50.1'))
466+
}
467+
468+
if (args.includes('ls-remote')) {
469+
options.listeners.stdout(Buffer.from('ref: refs/heads/main\tHEAD\n'))
470+
}
471+
472+
return 0
473+
})
474+
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
475+
476+
git = await commandManager.createCommandManager('test', false, false)
477+
478+
await expect(
479+
git.tryGetObjectFormat('https://github.com/example/repo')
480+
).resolves.toEqual({format: '', succeeded: false})
481+
})
482+
483+
it('returns unsuccessful when object format detection cannot reach the remote', async () => {
484+
mockExec.mockImplementation((path, args, options) => {
485+
if (args.includes('version')) {
486+
options.listeners.stdout(Buffer.from('git version 2.50.1'))
487+
return 0
488+
}
489+
490+
return 128
491+
})
492+
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
493+
494+
git = await commandManager.createCommandManager('test', false, false)
495+
496+
await expect(
497+
git.tryGetObjectFormat('https://github.com/example/repo')
498+
).resolves.toEqual({format: '', succeeded: false})
499+
})
500+
501+
it('initializes SHA-256 repositories with the matching object format', async () => {
502+
mockExec.mockImplementation((path, args, options) => {
503+
if (args.includes('version')) {
504+
options.listeners.stdout(Buffer.from('git version 2.50.1'))
505+
}
506+
507+
return 0
508+
})
509+
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
510+
511+
git = await commandManager.createCommandManager('test', false, false)
512+
513+
await git.init('sha256')
514+
515+
expect(mockExec).toHaveBeenCalledWith(
516+
expect.any(String),
517+
['init', '--object-format=sha256', 'test'],
518+
expect.any(Object)
519+
)
520+
})
521+
522+
it('initializes SHA-1 repositories with existing default arguments', async () => {
523+
mockExec.mockImplementation((path, args, options) => {
524+
if (args.includes('version')) {
525+
options.listeners.stdout(Buffer.from('git version 2.50.1'))
526+
}
527+
528+
return 0
529+
})
530+
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
531+
532+
git = await commandManager.createCommandManager('test', false, false)
533+
534+
await git.init('sha1')
535+
536+
expect(mockExec).toHaveBeenCalledWith(
537+
expect.any(String),
538+
['init', 'test'],
539+
expect.any(Object)
540+
)
541+
})
542+
})
543+
381544
describe('git user-agent with orchestration ID', () => {
382545
beforeEach(async () => {
383546
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())

__test__/git-directory-helper.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,7 @@ async function setup(testName: string): Promise<void> {
501501
await fs.promises.stat(path.join(repositoryPath, '.git'))
502502
return repositoryUrl
503503
}),
504+
tryGetObjectFormat: jest.fn(async () => ({format: '', succeeded: true})),
504505
tryGetConfigValues: jest.fn(),
505506
tryGetConfigKeys: jest.fn(),
506507
tryReset: jest.fn(async () => {

__test__/github-api-helper.test.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import * as core from '@actions/core'
2+
import * as github from '@actions/github'
3+
import * as githubApiHelper from '../lib/github-api-helper'
4+
5+
describe('github-api-helper object format', () => {
6+
let getOctokitSpy: jest.SpyInstance
7+
let debugSpy: jest.SpyInstance
8+
let repoGet: jest.Mock
9+
let branchGet: jest.Mock
10+
11+
function mockObjectFormatApi(defaultBranch: string, commitSha: string): void {
12+
repoGet = jest.fn(async () => ({
13+
data: {
14+
default_branch: defaultBranch
15+
}
16+
}))
17+
branchGet = jest.fn(async () => ({
18+
data: {
19+
commit: {
20+
sha: commitSha
21+
}
22+
}
23+
}))
24+
getOctokitSpy = jest.spyOn(github, 'getOctokit').mockReturnValue({
25+
rest: {
26+
repos: {
27+
get: repoGet,
28+
getBranch: branchGet
29+
}
30+
}
31+
} as any)
32+
}
33+
34+
beforeEach(() => {
35+
debugSpy = jest.spyOn(core, 'debug').mockImplementation(jest.fn())
36+
})
37+
38+
afterEach(() => {
39+
jest.restoreAllMocks()
40+
})
41+
42+
it('detects SHA-256 from the default branch commit SHA', async () => {
43+
const commitSha =
44+
'9422233ca7ee1b17f1e905d0e141faf0c401556c41cdc6acd71c6bd685da2e92'
45+
mockObjectFormatApi('main', commitSha)
46+
47+
await expect(
48+
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
49+
).resolves.toEqual({
50+
defaultBranch: 'main',
51+
format: 'sha256',
52+
succeeded: true
53+
})
54+
55+
expect(getOctokitSpy).toHaveBeenCalledWith(
56+
'token',
57+
expect.objectContaining({baseUrl: 'https://api.github.com'})
58+
)
59+
expect(repoGet).toHaveBeenCalledWith({owner: 'owner', repo: 'repo'})
60+
expect(branchGet).toHaveBeenCalledWith({
61+
owner: 'owner',
62+
repo: 'repo',
63+
branch: 'main'
64+
})
65+
})
66+
67+
it('detects SHA-1 from the default branch commit SHA', async () => {
68+
mockObjectFormatApi('main', 'c988866043f035e6a46509872215f91d879044c9')
69+
70+
await expect(
71+
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
72+
).resolves.toEqual({defaultBranch: 'main', format: 'sha1', succeeded: true})
73+
})
74+
75+
it('detects object format from an existing commit without API calls', async () => {
76+
const commitSha =
77+
'9422233ca7ee1b17f1e905d0e141faf0c401556c41cdc6acd71c6bd685da2e92'
78+
getOctokitSpy = jest.spyOn(github, 'getOctokit')
79+
80+
await expect(
81+
githubApiHelper.tryGetRepositoryObjectFormat(
82+
'token',
83+
'owner',
84+
'repo',
85+
undefined,
86+
undefined,
87+
commitSha
88+
)
89+
).resolves.toEqual({format: 'sha256', succeeded: true})
90+
91+
expect(getOctokitSpy).not.toHaveBeenCalled()
92+
})
93+
94+
it('uses a branch ref directly without looking up the default branch', async () => {
95+
const commitSha = 'c988866043f035e6a46509872215f91d879044c9'
96+
repoGet = jest.fn()
97+
branchGet = jest.fn(async () => ({
98+
data: {
99+
commit: {
100+
sha: commitSha
101+
}
102+
}
103+
}))
104+
getOctokitSpy = jest.spyOn(github, 'getOctokit').mockReturnValue({
105+
rest: {
106+
repos: {
107+
get: repoGet,
108+
getBranch: branchGet
109+
}
110+
}
111+
} as any)
112+
113+
await expect(
114+
githubApiHelper.tryGetRepositoryObjectFormat(
115+
'token',
116+
'owner',
117+
'repo',
118+
undefined,
119+
'refs/heads/feature'
120+
)
121+
).resolves.toEqual({format: 'sha1', succeeded: true})
122+
123+
expect(repoGet).not.toHaveBeenCalled()
124+
expect(branchGet).toHaveBeenCalledWith({
125+
owner: 'owner',
126+
repo: 'repo',
127+
branch: 'feature'
128+
})
129+
})
130+
131+
it('returns unsuccessful when the default branch commit SHA is not recognized', async () => {
132+
mockObjectFormatApi('main', 'not-a-sha')
133+
134+
await expect(
135+
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
136+
).resolves.toEqual({format: '', succeeded: false})
137+
expect(debugSpy).toHaveBeenCalledWith(
138+
'Unable to determine repository object format from commit SHA'
139+
)
140+
})
141+
142+
it('returns unsuccessful when the repository API lookup fails', async () => {
143+
repoGet = jest.fn(async () => {
144+
throw new Error('not found')
145+
})
146+
branchGet = jest.fn()
147+
jest.spyOn(github, 'getOctokit').mockReturnValue({
148+
rest: {
149+
repos: {
150+
get: repoGet,
151+
getBranch: branchGet
152+
}
153+
}
154+
} as any)
155+
156+
await expect(
157+
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
158+
).resolves.toEqual({format: '', succeeded: false})
159+
expect(branchGet).not.toHaveBeenCalled()
160+
expect(debugSpy).toHaveBeenCalledWith(
161+
'Unable to determine repository object format: not found'
162+
)
163+
})
164+
})

0 commit comments

Comments
 (0)