Compare commits

..

1 Commits

Author SHA1 Message Date
Yashwanth Anantharaju
daa5732ee1 Fix checkout init for SHA-256 repositories 2026-05-21 14:58:31 -04:00
7 changed files with 459 additions and 93 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) {
@@ -974,46 +1007,6 @@ describe('git-auth-helper tests', () => {
).toBe(false)
expect((authHelper as any).testCredentialsConfigPath('')).toBe(false)
})
const includeIfCleanupRegex_matchesBothVariants =
'includeIf cleanup regex matches both gitdir: and gitdir/i: keys'
it(includeIfCleanupRegex_matchesBothVariants, async () => {
// The cleanup regex must match both variants so credential
// removal works regardless of which was written
const regex = /^includeIf\.gitdir(\/i)?:/
expect(regex.test('includeIf.gitdir:D:/workspaces/repo/.git.path')).toBe(
true
)
expect(regex.test('includeIf.gitdir/i:D:/Workspaces/repo/.git.path')).toBe(
true
)
expect(regex.test('includeIf.gitdir/i:/github/workspace/.git.path')).toBe(
true
)
expect(regex.test('includeIf.gitdir:~/projects/foo/.git.path')).toBe(true)
expect(regex.test('includeIf.onbranch:main.path')).toBe(false)
expect(regex.test('include.path')).toBe(false)
})
const includeIfDirective_usesCorrectVariantForPlatform =
'includeIf directive uses gitdir/i on Windows and gitdir on other platforms'
it(includeIfDirective_usesCorrectVariantForPlatform, async () => {
await setup(includeIfDirective_usesCorrectVariantForPlatform)
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
const localConfigContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
if (isWindows) {
expect(localConfigContent).toContain('includeIf.gitdir/i:')
expect(localConfigContent).not.toContain('includeIf.gitdir:')
} else {
expect(localConfigContent).toContain('includeIf.gitdir:')
expect(localConfigContent).not.toContain('includeIf.gitdir/i:')
}
})
})
async function setup(testName: string): Promise<void> {
@@ -1143,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 () => {

153
dist/index.js vendored
View File

@@ -151,12 +151,6 @@ const stateHelper = __importStar(__nccwpck_require__(4866));
const urlHelper = __importStar(__nccwpck_require__(9437));
const uuid_1 = __nccwpck_require__(5840);
const IS_WINDOWS = process.platform === 'win32';
// Use case-insensitive gitdir matching on Windows to handle path casing mismatches
// between the runner's GITHUB_WORKSPACE and the actual filesystem casing.
// See: https://github.com/actions/checkout/issues/2345
const INCLUDE_IF_GITDIR = IS_WINDOWS
? 'includeIf.gitdir/i:'
: 'includeIf.gitdir:';
const SSH_COMMAND_KEY = 'core.sshCommand';
function createAuthHelper(git, settings) {
return new GitAuthHelper(git, settings);
@@ -258,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
@@ -276,7 +275,7 @@ class GitAuthHelper {
let submoduleGitDir = path.dirname(configPath); // The config file is at .git/modules/submodule-name/config
submoduleGitDir = submoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
// Configure host includeIf
yield this.git.config(`${INCLUDE_IF_GITDIR}${submoduleGitDir}.path`, credentialsConfigPath, false, // globalConfig?
yield this.git.config(`includeIf.gitdir:${submoduleGitDir}.path`, credentialsConfigPath, false, // globalConfig?
false, // add?
configPath);
// Container submodule git directory
@@ -286,7 +285,7 @@ class GitAuthHelper {
relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
const containerSubmoduleGitDir = path.posix.join('/github/workspace', relativeSubmoduleGitDir);
// Configure container includeIf
yield this.git.config(`${INCLUDE_IF_GITDIR}${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, // globalConfig?
yield this.git.config(`includeIf.gitdir:${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, // globalConfig?
false, // add?
configPath);
}
@@ -319,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;
}
@@ -374,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);
}
});
@@ -416,10 +421,10 @@ class GitAuthHelper {
let gitDir = path.join(this.git.getWorkingDirectory(), '.git');
gitDir = gitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
// Configure host includeIf
const hostIncludeKey = `${INCLUDE_IF_GITDIR}${gitDir}.path`;
const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`;
yield this.git.config(hostIncludeKey, credentialsConfigPath);
// Configure host includeIf for worktrees
const hostWorktreeIncludeKey = `${INCLUDE_IF_GITDIR}${gitDir}/worktrees/*.path`;
const hostWorktreeIncludeKey = `includeIf.gitdir:${gitDir}/worktrees/*.path`;
yield this.git.config(hostWorktreeIncludeKey, credentialsConfigPath);
// Container git directory
const workingDirectory = this.git.getWorkingDirectory();
@@ -431,10 +436,10 @@ class GitAuthHelper {
// Container credentials config path
const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath));
// Configure container includeIf
const containerIncludeKey = `${INCLUDE_IF_GITDIR}${containerGitDir}.path`;
const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`;
yield this.git.config(containerIncludeKey, containerCredentialsPath);
// Configure container includeIf for worktrees
const containerWorktreeIncludeKey = `${INCLUDE_IF_GITDIR}${containerGitDir}/worktrees/*.path`;
const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path`;
yield this.git.config(containerWorktreeIncludeKey, containerCredentialsPath);
}
});
@@ -460,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;
@@ -486,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);
}
});
}
/**
@@ -571,7 +578,7 @@ class GitAuthHelper {
const credentialsPaths = new Set();
try {
// Get all includeIf.gitdir keys
const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir(/i)?:', false, // globalConfig?
const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:', false, // globalConfig?
configPath);
for (const key of keys) {
// Get all values for this key
@@ -902,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() {
@@ -1062,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'];
@@ -1455,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);
@@ -1471,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);
@@ -1492,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();
}
@@ -1509,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) {
@@ -1605,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');
@@ -1631,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();
}
}
});

View File

@@ -13,21 +13,17 @@ import {IGitCommandManager} from './git-command-manager'
import {IGitSourceSettings} from './git-source-settings'
const IS_WINDOWS = process.platform === 'win32'
// Use case-insensitive gitdir matching on Windows to handle path casing mismatches
// between the runner's GITHUB_WORKSPACE and the actual filesystem casing.
// See: https://github.com/actions/checkout/issues/2345
const INCLUDE_IF_GITDIR = IS_WINDOWS
? 'includeIf.gitdir/i:'
: 'includeIf.gitdir:'
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(
@@ -160,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)
@@ -188,7 +188,7 @@ class GitAuthHelper {
// Configure host includeIf
await this.git.config(
`${INCLUDE_IF_GITDIR}${submoduleGitDir}.path`,
`includeIf.gitdir:${submoduleGitDir}.path`,
credentialsConfigPath,
false, // globalConfig?
false, // add?
@@ -210,7 +210,7 @@ class GitAuthHelper {
// Configure container includeIf
await this.git.config(
`${INCLUDE_IF_GITDIR}${containerSubmoduleGitDir}.path`,
`includeIf.gitdir:${containerSubmoduleGitDir}.path`,
containerCredentialsPath,
false, // globalConfig?
false, // add?
@@ -249,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
}
@@ -319,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)
}
}
@@ -377,11 +384,11 @@ class GitAuthHelper {
gitDir = gitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
// Configure host includeIf
const hostIncludeKey = `${INCLUDE_IF_GITDIR}${gitDir}.path`
const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`
await this.git.config(hostIncludeKey, credentialsConfigPath)
// Configure host includeIf for worktrees
const hostWorktreeIncludeKey = `${INCLUDE_IF_GITDIR}${gitDir}/worktrees/*.path`
const hostWorktreeIncludeKey = `includeIf.gitdir:${gitDir}/worktrees/*.path`
await this.git.config(hostWorktreeIncludeKey, credentialsConfigPath)
// Container git directory
@@ -403,11 +410,11 @@ class GitAuthHelper {
)
// Configure container includeIf
const containerIncludeKey = `${INCLUDE_IF_GITDIR}${containerGitDir}.path`
const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`
await this.git.config(containerIncludeKey, containerCredentialsPath)
// Configure container includeIf for worktrees
const containerWorktreeIncludeKey = `${INCLUDE_IF_GITDIR}${containerGitDir}/worktrees/*.path`
const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path`
await this.git.config(
containerWorktreeIncludeKey,
containerCredentialsPath
@@ -439,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) {
@@ -465,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)
}
}
/**
@@ -560,7 +569,7 @@ class GitAuthHelper {
try {
// Get all includeIf.gitdir keys
const keys = await this.git.tryGetConfigKeys(
'^includeIf\\.gitdir(/i)?:',
'^includeIf\\.gitdir:',
false, // globalConfig?
configPath
)

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()
}
}
}