Compare commits

..

2 Commits

Author SHA1 Message Date
Yashwanth Anantharaju
daa5732ee1 Fix checkout init for SHA-256 repositories 2026-05-21 14:58:31 -04:00
Yashwanth Anantharaju
900f2210b1 fix: expand merge commit SHA regex and add SHA-256 test cases (#2414)
* fix: expand merge commit SHA regex and add SHA-256 test cases

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: add checkCommitInfo SHA coverage

Add checkCommitInfo tests for SHA-1 and SHA-256 merge messages and reject invalid 50-character hex merge heads.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* style: fix Prettier formatting in test and source files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-04 13:30:55 -04:00
11 changed files with 609 additions and 31 deletions

View File

@@ -318,6 +318,39 @@ describe('git-auth-helper tests', () => {
)
})
const configureSshCommand_doesNotPersistSshCommand =
'configureSshCommand does not persist SSH command'
it(configureSshCommand_doesNotPersistSshCommand, async () => {
if (!sshPath) {
process.stdout.write(
`Skipped test "${configureSshCommand_doesNotPersistSshCommand}". Executable 'ssh' not found in the PATH.\n`
)
return
}
// Arrange
await setup(configureSshCommand_doesNotPersistSshCommand)
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
// Act
await authHelper.configureSshCommand()
// Assert git env var
expect(git.setEnvironmentVariable).toHaveBeenCalledWith(
'GIT_SSH_COMMAND',
expect.any(String)
)
// Assert the local SSH command was not persisted for the pre-init probe
expect(git.config).not.toHaveBeenCalledWith(
'core.sshCommand',
expect.any(String)
)
await authHelper.removeSshCommand()
expect(git.env['GIT_SSH_COMMAND']).toBeUndefined()
})
const configureAuth_writesExplicitKnownHosts = 'writes explicit known hosts'
it(configureAuth_writesExplicitKnownHosts, async () => {
if (!sshPath) {
@@ -1103,6 +1136,7 @@ async function setup(testName: string): Promise<void> {
),
tryDisableAutomaticGarbageCollection: jest.fn(),
tryGetFetchUrl: jest.fn(),
tryGetObjectFormat: jest.fn(async () => ({format: '', succeeded: true})),
tryGetConfigValues: jest.fn(
async (
key: string,

View File

@@ -378,6 +378,152 @@ 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 ls-remote protocol output', async () => {
const calls: any[] = []
mockExec.mockImplementation((path, args, options) => {
calls.push({args, env: {...options.env}})
if (args.includes('version')) {
options.listeners.stdout(Buffer.from('git version 2.50.1'))
}
if (args.includes('ls-remote')) {
options.listeners.stderr(
Buffer.from(
'packet: git< version 2\npacket: git< object-format=sha256\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'
)
await git.init()
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
})
)
expect(
calls.find(call => call.args.includes('ls-remote')).env.GIT_TRACE_PACKET
).toBe('1')
expect(
calls.find(call => call.args.includes('init')).env.GIT_TRACE_PACKET
).toBeUndefined()
})
it('returns an empty object format when the remote does not advertise one', 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.stderr(Buffer.from('packet: git< version 2\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: true})
})
it('reports failure 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())

View File

@@ -501,6 +501,7 @@ async function setup(testName: string): Promise<void> {
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 () => {

View File

@@ -133,6 +133,16 @@ describe('input-helper tests', () => {
expect(settings.commit).toBe('1111111111222222222233333333334444444444')
})
it('sets ref to empty when explicit sha-256', async () => {
inputs.ref =
'1111111111222222222233333333334444444444555555555566666666667777'
const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings.ref).toBeFalsy()
expect(settings.commit).toBe(
'1111111111222222222233333333334444444444555555555566666666667777'
)
})
it('sets sha to empty when explicit ref', async () => {
inputs.ref = 'refs/heads/some-other-ref'
const settings: IGitSourceSettings = await inputHelper.getInputs()

View File

@@ -1,8 +1,12 @@
import * as assert from 'assert'
import * as core from '@actions/core'
import * as github from '@actions/github'
import * as refHelper from '../lib/ref-helper'
import {IGitCommandManager} from '../lib/git-command-manager'
const commit = '1234567890123456789012345678901234567890'
const sha256Commit =
'1234567890123456789012345678901234567890123456789012345678901234'
let git: IGitCommandManager
describe('ref-helper tests', () => {
@@ -37,6 +41,12 @@ describe('ref-helper tests', () => {
expect(checkoutInfo.startPoint).toBeFalsy()
})
it('getCheckoutInfo sha-256 only', async () => {
const checkoutInfo = await refHelper.getCheckoutInfo(git, '', sha256Commit)
expect(checkoutInfo.ref).toBe(sha256Commit)
expect(checkoutInfo.startPoint).toBeFalsy()
})
it('getCheckoutInfo refs/heads/', async () => {
const checkoutInfo = await refHelper.getCheckoutInfo(
git,
@@ -227,4 +237,142 @@ describe('ref-helper tests', () => {
'+refs/heads/my/branch:refs/remotes/origin/my/branch'
)
})
describe('checkCommitInfo', () => {
const repositoryOwner = 'some-owner'
const repositoryName = 'some-repo'
const ref = 'refs/pull/123/merge'
const sha1Head = '1111111111222222222233333333334444444444'
const sha1Base = 'aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd'
const sha256Head =
'1111111111222222222233333333334444444444555555555566666666667777'
const sha256Base =
'aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffff0000'
let debugSpy: jest.SpyInstance
let getOctokitSpy: jest.SpyInstance
let repoGetSpy: jest.Mock
let originalEventName: string
let originalPayload: unknown
let originalRef: string
let originalSha: string
function setPullRequestContext(
expectedHeadSha: string,
expectedBaseSha: string,
mergeCommit: string
): void {
;(github.context as any).eventName = 'pull_request'
github.context.ref = ref
github.context.sha = mergeCommit
;(github.context as any).payload = {
action: 'synchronize',
after: expectedHeadSha,
number: 123,
pull_request: {
base: {
sha: expectedBaseSha
}
},
repository: {
private: false
}
}
}
beforeEach(() => {
originalEventName = github.context.eventName
originalPayload = github.context.payload
originalRef = github.context.ref
originalSha = github.context.sha
jest.spyOn(github.context, 'repo', 'get').mockReturnValue({
owner: repositoryOwner,
repo: repositoryName
})
debugSpy = jest.spyOn(core, 'debug').mockImplementation(jest.fn())
repoGetSpy = jest.fn(async () => ({}))
getOctokitSpy = jest.spyOn(github, 'getOctokit').mockReturnValue({
rest: {
repos: {
get: repoGetSpy
}
}
} as any)
})
afterEach(() => {
;(github.context as any).eventName = originalEventName
;(github.context as any).payload = originalPayload
github.context.ref = originalRef
github.context.sha = originalSha
jest.restoreAllMocks()
})
it('returns early for SHA-1 merge commit', async () => {
setPullRequestContext(sha1Head, sha1Base, commit)
await refHelper.checkCommitInfo(
'token',
`Merge ${sha1Head} into ${sha1Base}`,
repositoryOwner,
repositoryName,
ref,
commit
)
expect(getOctokitSpy).not.toHaveBeenCalled()
expect(repoGetSpy).not.toHaveBeenCalled()
})
it('matches SHA-256 merge commit info', async () => {
const actualHeadSha =
'9999999999888888888877777777776666666666555555555544444444443333'
setPullRequestContext(sha256Head, sha256Base, sha256Commit)
await refHelper.checkCommitInfo(
'token',
`Merge ${actualHeadSha} into ${sha256Base}`,
repositoryOwner,
repositoryName,
ref,
sha256Commit
)
expect(getOctokitSpy).toHaveBeenCalledWith(
'token',
expect.objectContaining({
userAgent: expect.stringContaining(
`expected_head_sha=${sha256Head};actual_head_sha=${actualHeadSha}`
)
})
)
expect(repoGetSpy).toHaveBeenCalledWith({
owner: repositoryOwner,
repo: repositoryName
})
expect(debugSpy).toHaveBeenCalledWith(
`Expected head sha ${sha256Head}; actual head sha ${actualHeadSha}`
)
expect(debugSpy).not.toHaveBeenCalledWith('Unexpected message format')
})
it('does not match 50-char hex as a valid merge', async () => {
const invalidHeadSha =
'99999999998888888888777777777766666666665555555555'
setPullRequestContext(sha1Head, sha1Base, commit)
await refHelper.checkCommitInfo(
'token',
`Merge ${invalidHeadSha} into ${sha1Base}`,
repositoryOwner,
repositoryName,
ref,
commit
)
expect(getOctokitSpy).not.toHaveBeenCalled()
expect(repoGetSpy).not.toHaveBeenCalled()
expect(debugSpy).toHaveBeenCalledWith('Unexpected message format')
})
})
})

137
dist/index.js vendored
View File

@@ -252,6 +252,11 @@ class GitAuthHelper {
}
});
}
configureSshCommand() {
return __awaiter(this, void 0, void 0, function* () {
yield this.configureSsh(false);
});
}
configureSubmoduleAuth() {
return __awaiter(this, void 0, void 0, function* () {
// Remove possible previous HTTPS instead of SSH
@@ -313,12 +318,18 @@ class GitAuthHelper {
}
});
}
removeSshCommand() {
return __awaiter(this, void 0, void 0, function* () {
yield this.removeSsh(false);
this.git.removeEnvironmentVariable('GIT_SSH_COMMAND');
});
}
/**
* Configures SSH authentication by writing the SSH key and known hosts,
* and setting up the GIT_SSH_COMMAND environment variable.
*/
configureSsh() {
return __awaiter(this, void 0, void 0, function* () {
return __awaiter(this, arguments, void 0, function* (persistCredentials = this.settings.persistCredentials) {
if (!this.settings.sshKey) {
return;
}
@@ -368,7 +379,7 @@ class GitAuthHelper {
core.info(`Temporarily overriding GIT_SSH_COMMAND=${this.sshCommand}`);
this.git.setEnvironmentVariable('GIT_SSH_COMMAND', this.sshCommand);
// Configure core.sshCommand
if (this.settings.persistCredentials) {
if (persistCredentials) {
yield this.git.config(SSH_COMMAND_KEY, this.sshCommand);
}
});
@@ -454,7 +465,7 @@ class GitAuthHelper {
* known hosts files, and SSH command configurations.
*/
removeSsh() {
return __awaiter(this, void 0, void 0, function* () {
return __awaiter(this, arguments, void 0, function* (removeGitConfig = true) {
var _a, _b;
// SSH key
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath;
@@ -480,10 +491,12 @@ class GitAuthHelper {
core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`);
}
}
// SSH command
core.info('Removing SSH command configuration');
yield this.removeGitConfig(SSH_COMMAND_KEY);
yield this.removeSubmoduleGitConfig(SSH_COMMAND_KEY);
if (removeGitConfig) {
// SSH command
core.info('Removing SSH command configuration');
yield this.removeGitConfig(SSH_COMMAND_KEY);
yield this.removeSubmoduleGitConfig(SSH_COMMAND_KEY);
}
});
}
/**
@@ -896,9 +909,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 +1074,52 @@ class GitCommandManager {
return stdout;
});
}
tryGetObjectFormat(repositoryUrl) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
let stderr = '';
const listeners = {
stderr: (data) => {
stderr += data.toString();
},
errline: (data) => {
stderr += data.toString();
}
};
const existingTracePacket = this.gitEnv['GIT_TRACE_PACKET'];
this.gitEnv['GIT_TRACE_PACKET'] = '1';
try {
const output = yield this.execGit([
'-c',
'protocol.version=2',
'ls-remote',
'--quiet',
'--exit-code',
'--symref',
repositoryUrl,
'HEAD'
], true, true, listeners);
if (output.exitCode !== 0) {
core.debug(`Unable to determine repository object format: git ls-remote exited with ${output.exitCode}`);
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 };
}
finally {
if (existingTracePacket === undefined) {
delete this.gitEnv['GIT_TRACE_PACKET'];
}
else {
this.gitEnv['GIT_TRACE_PACKET'] = existingTracePacket;
}
}
const match = stderr.match(/object-format=(sha1|sha256)(?=\s|$)/);
return { format: match ? match[1] : '', succeeded: true };
});
}
tryGetConfigValues(configKey, globalConfig, configFile) {
return __awaiter(this, void 0, void 0, function* () {
const args = ['config'];
@@ -1449,6 +1513,10 @@ function getSource(settings) {
const git = yield getGitCommandManager(settings);
core.endGroup();
let authHelper = null;
let didStartConfigureAuth = false;
let didConfigureAuth = false;
let didConfigureSshCommand = false;
let didConfigureGlobalAuth = false;
try {
if (git) {
authHelper = gitAuthHelper.createAuthHelper(git, settings);
@@ -1465,6 +1533,20 @@ function getSource(settings) {
stateHelper.setSafeDirectory();
}
}
const configureGlobalAuth = () => __awaiter(this, void 0, void 0, function* () {
if (!authHelper || didConfigureGlobalAuth) {
return;
}
yield authHelper.configureGlobalAuth();
didConfigureGlobalAuth = true;
});
const configureSshCommand = () => __awaiter(this, void 0, void 0, function* () {
if (!authHelper || didConfigureSshCommand) {
return;
}
yield authHelper.configureSshCommand();
didConfigureSshCommand = true;
});
// Prepare existing directory, otherwise recreate
if (isExisting) {
yield gitDirectoryHelper.prepareExistingDirectory(git, settings.repositoryPath, repositoryUrl, settings.clean, settings.ref);
@@ -1486,8 +1568,27 @@ function getSource(settings) {
stateHelper.setRepositoryPath(settings.repositoryPath);
// Initialize the repository
if (!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))) {
core.startGroup('Determining repository object format');
let objectFormatResult = yield git.tryGetObjectFormat(repositoryUrl);
if (!objectFormatResult.succeeded) {
if (settings.sshKey) {
yield configureSshCommand();
}
else {
yield configureGlobalAuth();
}
objectFormatResult = yield git.tryGetObjectFormat(repositoryUrl);
}
if (!objectFormatResult.succeeded) {
throw new Error('Unable to determine repository object format');
}
const objectFormat = 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();
}
@@ -1503,7 +1604,9 @@ function getSource(settings) {
}
// Configure auth
core.startGroup('Setting up auth');
didStartConfigureAuth = true;
yield authHelper.configureAuth();
didConfigureAuth = true;
core.endGroup();
// Determine the default branch
if (!settings.ref && !settings.commit) {
@@ -1599,7 +1702,7 @@ function getSource(settings) {
if (settings.submodules) {
// Temporarily override global config
core.startGroup('Setting up auth for fetching submodules');
yield authHelper.configureGlobalAuth();
yield configureGlobalAuth();
core.endGroup();
// Checkout submodules
core.startGroup('Fetching submodules');
@@ -1625,12 +1728,16 @@ function getSource(settings) {
finally {
// Remove auth
if (authHelper) {
if (!settings.persistCredentials) {
if (!settings.persistCredentials ||
(didStartConfigureAuth && !didConfigureAuth)) {
core.startGroup('Removing auth');
yield authHelper.removeAuth();
core.endGroup();
}
authHelper.removeGlobalConfig();
else if (didConfigureSshCommand && !didConfigureAuth) {
yield authHelper.removeSshCommand();
}
yield authHelper.removeGlobalConfig();
}
}
});
@@ -2021,7 +2128,7 @@ function getInputs() {
}
}
// SHA?
else if (result.ref.match(/^[0-9a-fA-F]{40}$/)) {
else if (result.ref.match(/^(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})$/)) {
result.commit = result.ref;
result.ref = '';
}
@@ -2444,7 +2551,7 @@ function checkCommitInfo(token, commitInfo, repositoryOwner, repositoryName, ref
return;
}
// Extract details from message
const match = commitInfo.match(/Merge ([0-9a-f]{40}) into ([0-9a-f]{40})/);
const match = commitInfo.match(/Merge ([0-9a-f]{40}|[0-9a-f]{64}) into ([0-9a-f]{40}|[0-9a-f]{64})/);
if (!match) {
core.debug('Unexpected message format');
return;

View File

@@ -18,10 +18,12 @@ const SSH_COMMAND_KEY = 'core.sshCommand'
export interface IGitAuthHelper {
configureAuth(): Promise<void>
configureGlobalAuth(): Promise<void>
configureSshCommand(): Promise<void>
configureSubmoduleAuth(): Promise<void>
configureTempGlobalConfig(): Promise<string>
removeAuth(): Promise<void>
removeGlobalConfig(): Promise<void>
removeSshCommand(): Promise<void>
}
export function createAuthHelper(
@@ -154,6 +156,10 @@ class GitAuthHelper {
}
}
async configureSshCommand(): Promise<void> {
await this.configureSsh(false)
}
async configureSubmoduleAuth(): Promise<void> {
// Remove possible previous HTTPS instead of SSH
await this.removeSubmoduleGitConfig(this.insteadOfKey)
@@ -243,11 +249,18 @@ class GitAuthHelper {
}
}
async removeSshCommand(): Promise<void> {
await this.removeSsh(false)
this.git.removeEnvironmentVariable('GIT_SSH_COMMAND')
}
/**
* Configures SSH authentication by writing the SSH key and known hosts,
* and setting up the GIT_SSH_COMMAND environment variable.
*/
private async configureSsh(): Promise<void> {
private async configureSsh(
persistCredentials = this.settings.persistCredentials
): Promise<void> {
if (!this.settings.sshKey) {
return
}
@@ -313,7 +326,7 @@ class GitAuthHelper {
this.git.setEnvironmentVariable('GIT_SSH_COMMAND', this.sshCommand)
// Configure core.sshCommand
if (this.settings.persistCredentials) {
if (persistCredentials) {
await this.git.config(SSH_COMMAND_KEY, this.sshCommand)
}
}
@@ -433,7 +446,7 @@ class GitAuthHelper {
* Removes SSH authentication configuration by cleaning up SSH keys,
* known hosts files, and SSH command configurations.
*/
private async removeSsh(): Promise<void> {
private async removeSsh(removeGitConfig = true): Promise<void> {
// SSH key
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath
if (keyPath) {
@@ -459,10 +472,12 @@ class GitAuthHelper {
}
}
// SSH command
core.info('Removing SSH command configuration')
await this.removeGitConfig(SSH_COMMAND_KEY)
await this.removeSubmoduleGitConfig(SSH_COMMAND_KEY)
if (removeGitConfig) {
// SSH command
core.info('Removing SSH command configuration')
await this.removeGitConfig(SSH_COMMAND_KEY)
await this.removeSubmoduleGitConfig(SSH_COMMAND_KEY)
}
}
/**

View File

@@ -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<void>
branchExists(remote: boolean, pattern: string): Promise<boolean>
@@ -43,7 +48,7 @@ export interface IGitCommandManager {
getDefaultBranch(repositoryUrl: string): Promise<string>
getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
getWorkingDirectory(): string
init(): Promise<void>
init(objectFormat?: string): Promise<void>
isDetached(): Promise<boolean>
lfsFetch(ref: string): Promise<void>
lfsInstall(): Promise<void>
@@ -68,6 +73,7 @@ export interface IGitCommandManager {
): Promise<boolean>
tryDisableAutomaticGarbageCollection(): Promise<boolean>
tryGetFetchUrl(): Promise<string>
tryGetObjectFormat(repositoryUrl: string): Promise<GitObjectFormatResult>
tryGetConfigValues(
configKey: string,
globalConfig?: boolean,
@@ -364,8 +370,14 @@ class GitCommandManager {
return this.workingDirectory
}
async init(): Promise<void> {
await this.execGit(['init', this.workingDirectory])
async init(objectFormat?: string): Promise<void> {
const args = ['init']
if (objectFormat === 'sha256') {
args.push('--object-format=sha256')
}
args.push(this.workingDirectory)
await this.execGit(args)
}
async isDetached(): Promise<boolean> {
@@ -536,6 +548,61 @@ class GitCommandManager {
return stdout
}
async tryGetObjectFormat(
repositoryUrl: string
): Promise<GitObjectFormatResult> {
let stderr = ''
const listeners = {
stderr: (data: Buffer) => {
stderr += data.toString()
},
errline: (data: Buffer) => {
stderr += data.toString()
}
}
const existingTracePacket = this.gitEnv['GIT_TRACE_PACKET']
this.gitEnv['GIT_TRACE_PACKET'] = '1'
try {
const output = await this.execGit(
[
'-c',
'protocol.version=2',
'ls-remote',
'--quiet',
'--exit-code',
'--symref',
repositoryUrl,
'HEAD'
],
true,
true,
listeners
)
if (output.exitCode !== 0) {
core.debug(
`Unable to determine repository object format: git ls-remote exited with ${output.exitCode}`
)
return {format: '', succeeded: false}
}
} catch (err) {
core.debug(
`Unable to determine repository object format: ${(err as any)?.message ?? err}`
)
return {format: '', succeeded: false}
} finally {
if (existingTracePacket === undefined) {
delete this.gitEnv['GIT_TRACE_PACKET']
} else {
this.gitEnv['GIT_TRACE_PACKET'] = existingTracePacket
}
}
const match = stderr.match(/object-format=(sha1|sha256)(?=\s|$)/)
return {format: match ? match[1] : '', succeeded: true}
}
async tryGetConfigValues(
configKey: string,
globalConfig?: boolean,

View File

@@ -40,6 +40,10 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
core.endGroup()
let authHelper: gitAuthHelper.IGitAuthHelper | null = null
let didStartConfigureAuth = false
let didConfigureAuth = false
let didConfigureSshCommand = false
let didConfigureGlobalAuth = false
try {
if (git) {
authHelper = gitAuthHelper.createAuthHelper(git, settings)
@@ -63,6 +67,24 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
}
}
const configureGlobalAuth = async () => {
if (!authHelper || didConfigureGlobalAuth) {
return
}
await authHelper.configureGlobalAuth()
didConfigureGlobalAuth = true
}
const configureSshCommand = async () => {
if (!authHelper || didConfigureSshCommand) {
return
}
await authHelper.configureSshCommand()
didConfigureSshCommand = true
}
// Prepare existing directory, otherwise recreate
if (isExisting) {
await gitDirectoryHelper.prepareExistingDirectory(
@@ -109,8 +131,27 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
if (
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
) {
core.startGroup('Determining repository object format')
let objectFormatResult = await git.tryGetObjectFormat(repositoryUrl)
if (!objectFormatResult.succeeded) {
if (settings.sshKey) {
await configureSshCommand()
} else {
await configureGlobalAuth()
}
objectFormatResult = await git.tryGetObjectFormat(repositoryUrl)
}
if (!objectFormatResult.succeeded) {
throw new Error('Unable to determine repository object format')
}
const objectFormat = 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()
}
@@ -130,7 +171,9 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
}
// Configure auth
core.startGroup('Setting up auth')
didStartConfigureAuth = true
await authHelper.configureAuth()
didConfigureAuth = true
core.endGroup()
// Determine the default branch
@@ -258,7 +301,7 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
if (settings.submodules) {
// Temporarily override global config
core.startGroup('Setting up auth for fetching submodules')
await authHelper.configureGlobalAuth()
await configureGlobalAuth()
core.endGroup()
// Checkout submodules
@@ -299,12 +342,17 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
} finally {
// Remove auth
if (authHelper) {
if (!settings.persistCredentials) {
if (
!settings.persistCredentials ||
(didStartConfigureAuth && !didConfigureAuth)
) {
core.startGroup('Removing auth')
await authHelper.removeAuth()
core.endGroup()
} else if (didConfigureSshCommand && !didConfigureAuth) {
await authHelper.removeSshCommand()
}
authHelper.removeGlobalConfig()
await authHelper.removeGlobalConfig()
}
}
}

View File

@@ -71,7 +71,7 @@ export async function getInputs(): Promise<IGitSourceSettings> {
}
}
// SHA?
else if (result.ref.match(/^[0-9a-fA-F]{40}$/)) {
else if (result.ref.match(/^(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})$/)) {
result.commit = result.ref
result.ref = ''
}

View File

@@ -258,7 +258,9 @@ export async function checkCommitInfo(
}
// Extract details from message
const match = commitInfo.match(/Merge ([0-9a-f]{40}) into ([0-9a-f]{40})/)
const match = commitInfo.match(
/Merge ([0-9a-f]{40}|[0-9a-f]{64}) into ([0-9a-f]{40}|[0-9a-f]{64})/
)
if (!match) {
core.debug('Unexpected message format')
return