From fe86d6d213ceb5ba67d5588ccdced556fbceb964 Mon Sep 17 00:00:00 2001 From: Yashwanth Anantharaju Date: Thu, 21 May 2026 14:31:18 -0400 Subject: [PATCH] Fix checkout init for SHA-256 repositories --- __test__/git-auth-helper.test.ts | 1 + __test__/git-command-manager.test.ts | 163 ++++++++++++++++++++++++++ __test__/git-directory-helper.test.ts | 1 + dist/index.js | 59 +++++++++- src/git-command-manager.ts | 67 ++++++++++- src/git-source-provider.ts | 12 +- 6 files changed, 296 insertions(+), 7 deletions(-) diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts index ad3566a..590bd24 100644 --- a/__test__/git-auth-helper.test.ts +++ b/__test__/git-auth-helper.test.ts @@ -1103,6 +1103,7 @@ async function setup(testName: string): Promise { ), tryDisableAutomaticGarbageCollection: jest.fn(), tryGetFetchUrl: jest.fn(), + tryGetObjectFormat: jest.fn(async () => ({format: '', succeeded: true})), tryGetConfigValues: jest.fn( async ( key: string, diff --git a/__test__/git-command-manager.test.ts b/__test__/git-command-manager.test.ts index 8a97d82..8c4f503 100644 --- a/__test__/git-command-manager.test.ts +++ b/__test__/git-command-manager.test.ts @@ -378,6 +378,169 @@ describe('Test fetchDepth and fetchTags options', () => { }) }) +describe('repository object format', () => { + beforeEach(async () => { + jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn()) + jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn()) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('detects SHA-256 from a 64-character HEAD oid', async () => { + mockExec.mockImplementation((path, args, options) => { + if (args.includes('version')) { + options.listeners.stdout(Buffer.from('git version 2.50.1')) + } + + if (args.includes('ls-remote')) { + options.listeners.stdout( + Buffer.from( + 'ref: refs/heads/main\tHEAD\n' + + '9422233ca7ee1b17f1e905d0e141faf0c401556c41cdc6acd71c6bd685da2e92\tHEAD\n' + ) + ) + } + + return 0 + }) + jest.spyOn(exec, 'exec').mockImplementation(mockExec) + + git = await commandManager.createCommandManager('test', false, false) + + const objectFormat = await git.tryGetObjectFormat( + 'https://github.com/example/repo' + ) + + expect(objectFormat).toEqual({format: 'sha256', succeeded: true}) + expect(mockExec).toHaveBeenCalledWith( + expect.any(String), + [ + '-c', + 'protocol.version=2', + 'ls-remote', + '--quiet', + '--exit-code', + '--symref', + 'https://github.com/example/repo', + 'HEAD' + ], + expect.objectContaining({ + ignoreReturnCode: true, + silent: true + }) + ) + }) + + it('detects SHA-1 from a 40-character HEAD oid', async () => { + mockExec.mockImplementation((path, args, options) => { + if (args.includes('version')) { + options.listeners.stdout(Buffer.from('git version 2.50.1')) + } + + if (args.includes('ls-remote')) { + options.listeners.stdout( + Buffer.from( + 'ref: refs/heads/main\tHEAD\n' + + 'c988866043f035e6a46509872215f91d879044c9\tHEAD\n' + ) + ) + } + + return 0 + }) + jest.spyOn(exec, 'exec').mockImplementation(mockExec) + + git = await commandManager.createCommandManager('test', false, false) + + await expect( + git.tryGetObjectFormat('https://github.com/example/repo') + ).resolves.toEqual({format: 'sha1', succeeded: true}) + }) + + it('returns unsuccessful when HEAD does not resolve to a recognized object id', async () => { + mockExec.mockImplementation((path, args, options) => { + if (args.includes('version')) { + options.listeners.stdout(Buffer.from('git version 2.50.1')) + } + + if (args.includes('ls-remote')) { + options.listeners.stdout(Buffer.from('ref: refs/heads/main\tHEAD\n')) + } + + return 0 + }) + jest.spyOn(exec, 'exec').mockImplementation(mockExec) + + git = await commandManager.createCommandManager('test', false, false) + + await expect( + git.tryGetObjectFormat('https://github.com/example/repo') + ).resolves.toEqual({format: '', succeeded: false}) + }) + + it('returns unsuccessful when object format detection cannot reach the remote', async () => { + mockExec.mockImplementation((path, args, options) => { + if (args.includes('version')) { + options.listeners.stdout(Buffer.from('git version 2.50.1')) + return 0 + } + + return 128 + }) + jest.spyOn(exec, 'exec').mockImplementation(mockExec) + + git = await commandManager.createCommandManager('test', false, false) + + await expect( + git.tryGetObjectFormat('https://github.com/example/repo') + ).resolves.toEqual({format: '', succeeded: false}) + }) + + it('initializes SHA-256 repositories with the matching object format', async () => { + mockExec.mockImplementation((path, args, options) => { + if (args.includes('version')) { + options.listeners.stdout(Buffer.from('git version 2.50.1')) + } + + return 0 + }) + jest.spyOn(exec, 'exec').mockImplementation(mockExec) + + git = await commandManager.createCommandManager('test', false, false) + + await git.init('sha256') + + expect(mockExec).toHaveBeenCalledWith( + expect.any(String), + ['init', '--object-format=sha256', 'test'], + expect.any(Object) + ) + }) + + it('initializes SHA-1 repositories with existing default arguments', async () => { + mockExec.mockImplementation((path, args, options) => { + if (args.includes('version')) { + options.listeners.stdout(Buffer.from('git version 2.50.1')) + } + + return 0 + }) + jest.spyOn(exec, 'exec').mockImplementation(mockExec) + + git = await commandManager.createCommandManager('test', false, false) + + await git.init('sha1') + + expect(mockExec).toHaveBeenCalledWith( + expect.any(String), + ['init', 'test'], + expect.any(Object) + ) + }) +}) + describe('git user-agent with orchestration ID', () => { beforeEach(async () => { jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn()) diff --git a/__test__/git-directory-helper.test.ts b/__test__/git-directory-helper.test.ts index de79dc8..5cab554 100644 --- a/__test__/git-directory-helper.test.ts +++ b/__test__/git-directory-helper.test.ts @@ -501,6 +501,7 @@ async function setup(testName: string): Promise { await fs.promises.stat(path.join(repositoryPath, '.git')) return repositoryUrl }), + tryGetObjectFormat: jest.fn(async () => ({format: '', succeeded: true})), tryGetConfigValues: jest.fn(), tryGetConfigKeys: jest.fn(), tryReset: jest.fn(async () => { diff --git a/dist/index.js b/dist/index.js index 57729b2..4155a08 100644 --- a/dist/index.js +++ b/dist/index.js @@ -896,9 +896,14 @@ class GitCommandManager { getWorkingDirectory() { return this.workingDirectory; } - init() { + init(objectFormat) { return __awaiter(this, void 0, void 0, function* () { - yield this.execGit(['init', this.workingDirectory]); + const args = ['init']; + if (objectFormat === 'sha256') { + args.push('--object-format=sha256'); + } + args.push(this.workingDirectory); + yield this.execGit(args); }); } isDetached() { @@ -1056,6 +1061,45 @@ class GitCommandManager { return stdout; }); } + tryGetObjectFormat(repositoryUrl) { + return __awaiter(this, void 0, void 0, function* () { + var _a; + try { + const output = yield this.execGit([ + '-c', + 'protocol.version=2', + 'ls-remote', + '--quiet', + '--exit-code', + '--symref', + repositoryUrl, + 'HEAD' + ], true, true); + if (output.exitCode !== 0) { + core.debug(`Unable to determine repository object format: git ls-remote exited with ${output.exitCode}`); + return { format: '', succeeded: false }; + } + for (const line of output.stdout.trim().split('\n')) { + const [oid, ref] = line.split('\t'); + if (ref !== 'HEAD') { + continue; + } + if (/^[0-9a-fA-F]{64}$/.test(oid)) { + return { format: 'sha256', succeeded: true }; + } + if (/^[0-9a-fA-F]{40}$/.test(oid)) { + return { format: 'sha1', succeeded: true }; + } + } + core.debug('Unable to determine repository object format from HEAD'); + return { format: '', succeeded: false }; + } + catch (err) { + core.debug(`Unable to determine repository object format: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`); + return { format: '', succeeded: false }; + } + }); + } tryGetConfigValues(configKey, globalConfig, configFile) { return __awaiter(this, void 0, void 0, function* () { const args = ['config']; @@ -1486,8 +1530,17 @@ function getSource(settings) { stateHelper.setRepositoryPath(settings.repositoryPath); // Initialize the repository if (!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))) { + core.startGroup('Determining repository object format'); + const objectFormatResult = yield git.tryGetObjectFormat(repositoryUrl); + const objectFormat = objectFormatResult.succeeded + ? objectFormatResult.format + : ''; + if (objectFormat === 'sha256') { + core.info('Detected SHA-256 repository object format'); + } + core.endGroup(); core.startGroup('Initializing the repository'); - yield git.init(); + yield git.init(objectFormat); yield git.remoteAdd('origin', repositoryUrl); core.endGroup(); } diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts index f5ba40e..9fde8b2 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -15,6 +15,11 @@ import {GitVersion} from './git-version' export const MinimumGitVersion = new GitVersion('2.18') export const MinimumGitSparseCheckoutVersion = new GitVersion('2.28') +export interface GitObjectFormatResult { + format: string + succeeded: boolean +} + export interface IGitCommandManager { branchDelete(remote: boolean, branch: string): Promise branchExists(remote: boolean, pattern: string): Promise @@ -43,7 +48,7 @@ export interface IGitCommandManager { getDefaultBranch(repositoryUrl: string): Promise getSubmoduleConfigPaths(recursive: boolean): Promise getWorkingDirectory(): string - init(): Promise + init(objectFormat?: string): Promise isDetached(): Promise lfsFetch(ref: string): Promise lfsInstall(): Promise @@ -68,6 +73,7 @@ export interface IGitCommandManager { ): Promise tryDisableAutomaticGarbageCollection(): Promise tryGetFetchUrl(): Promise + tryGetObjectFormat(repositoryUrl: string): Promise tryGetConfigValues( configKey: string, globalConfig?: boolean, @@ -364,8 +370,14 @@ class GitCommandManager { return this.workingDirectory } - async init(): Promise { - await this.execGit(['init', this.workingDirectory]) + async init(objectFormat?: string): Promise { + const args = ['init'] + if (objectFormat === 'sha256') { + args.push('--object-format=sha256') + } + args.push(this.workingDirectory) + + await this.execGit(args) } async isDetached(): Promise { @@ -536,6 +548,55 @@ class GitCommandManager { return stdout } + async tryGetObjectFormat( + repositoryUrl: string + ): Promise { + try { + const output = await this.execGit( + [ + '-c', + 'protocol.version=2', + 'ls-remote', + '--quiet', + '--exit-code', + '--symref', + repositoryUrl, + 'HEAD' + ], + true, + true + ) + + if (output.exitCode !== 0) { + core.debug( + `Unable to determine repository object format: git ls-remote exited with ${output.exitCode}` + ) + return {format: '', succeeded: false} + } + + for (const line of output.stdout.trim().split('\n')) { + const [oid, ref] = line.split('\t') + if (ref !== 'HEAD') { + continue + } + if (/^[0-9a-fA-F]{64}$/.test(oid)) { + return {format: 'sha256', succeeded: true} + } + if (/^[0-9a-fA-F]{40}$/.test(oid)) { + return {format: 'sha1', succeeded: true} + } + } + + core.debug('Unable to determine repository object format from HEAD') + return {format: '', succeeded: false} + } catch (err) { + core.debug( + `Unable to determine repository object format: ${(err as any)?.message ?? err}` + ) + return {format: '', succeeded: false} + } + } + async tryGetConfigValues( configKey: string, globalConfig?: boolean, diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts index ec87178..3109ec2 100644 --- a/src/git-source-provider.ts +++ b/src/git-source-provider.ts @@ -109,8 +109,18 @@ export async function getSource(settings: IGitSourceSettings): Promise { if ( !fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git')) ) { + core.startGroup('Determining repository object format') + const objectFormatResult = await git.tryGetObjectFormat(repositoryUrl) + const objectFormat = objectFormatResult.succeeded + ? objectFormatResult.format + : '' + if (objectFormat === 'sha256') { + core.info('Detected SHA-256 repository object format') + } + core.endGroup() + core.startGroup('Initializing the repository') - await git.init() + await git.init(objectFormat) await git.remoteAdd('origin', repositoryUrl) core.endGroup() }