add support for submodules (#173)

This commit is contained in:
eric sciple 2020-03-05 14:21:59 -05:00 committed by GitHub
parent 204620207c
commit 422dc45671
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 915 additions and 220 deletions

@ -84,6 +84,35 @@ jobs:
shell: bash shell: bash
run: __test__/verify-lfs.sh run: __test__/verify-lfs.sh
# Submodules false
- name: Submodules false checkout
uses: ./
with:
ref: test-data/v2/submodule
path: submodules-false
- name: Verify submodules false
run: __test__/verify-submodules-false.sh
# Submodules one level
- name: Submodules true checkout
uses: ./
with:
ref: test-data/v2/submodule
path: submodules-true
submodules: true
- name: Verify submodules true
run: __test__/verify-submodules-true.sh
# Submodules recursive
- name: Submodules recursive checkout
uses: ./
with:
ref: test-data/v2/submodule
path: submodules-recursive
submodules: recursive
- name: Verify submodules recursive
run: __test__/verify-submodules-recursive.sh
# Basic checkout using REST API # Basic checkout using REST API
- name: Remove basic - name: Remove basic
if: runner.os != 'windows' if: runner.os != 'windows'

@ -70,6 +70,11 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
# Whether to download Git-LFS files # Whether to download Git-LFS files
# Default: false # Default: false
lfs: '' lfs: ''
# Whether to checkout submodules: `true` to checkout submodules or `recursive` to
# recursively checkout submodules.
# Default: false
submodules: ''
``` ```
<!-- end usage --> <!-- end usage -->

@ -8,10 +8,13 @@ import {IGitSourceSettings} from '../lib/git-source-settings'
const testWorkspace = path.join(__dirname, '_temp', 'git-auth-helper') const testWorkspace = path.join(__dirname, '_temp', 'git-auth-helper')
const originalRunnerTemp = process.env['RUNNER_TEMP'] const originalRunnerTemp = process.env['RUNNER_TEMP']
const originalHome = process.env['HOME']
let workspace: string let workspace: string
let gitConfigPath: string let localGitConfigPath: string
let globalGitConfigPath: string
let runnerTemp: string let runnerTemp: string
let git: IGitCommandManager let tempHomedir: string
let git: IGitCommandManager & {env: {[key: string]: string}}
let settings: IGitSourceSettings let settings: IGitSourceSettings
describe('git-auth-helper tests', () => { describe('git-auth-helper tests', () => {
@ -23,11 +26,24 @@ describe('git-auth-helper tests', () => {
beforeEach(() => { beforeEach(() => {
// Mock setSecret // Mock setSecret
jest.spyOn(core, 'setSecret').mockImplementation((secret: string) => {}) jest.spyOn(core, 'setSecret').mockImplementation((secret: string) => {})
// Mock error/warning/info/debug
jest.spyOn(core, 'error').mockImplementation(jest.fn())
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
jest.spyOn(core, 'info').mockImplementation(jest.fn())
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
}) })
afterEach(() => { afterEach(() => {
// Unregister mocks // Unregister mocks
jest.restoreAllMocks() jest.restoreAllMocks()
// Restore HOME
if (originalHome) {
process.env['HOME'] = originalHome
} else {
delete process.env['HOME']
}
}) })
afterAll(() => { afterAll(() => {
@ -38,10 +54,11 @@ describe('git-auth-helper tests', () => {
} }
}) })
const configuresAuthHeader = 'configures auth header' const configureAuth_configuresAuthHeader =
it(configuresAuthHeader, async () => { 'configureAuth configures auth header'
it(configureAuth_configuresAuthHeader, async () => {
// Arrange // Arrange
await setup(configuresAuthHeader) await setup(configureAuth_configuresAuthHeader)
expect(settings.authToken).toBeTruthy() // sanity check expect(settings.authToken).toBeTruthy() // sanity check
const authHelper = gitAuthHelper.createAuthHelper(git, settings) const authHelper = gitAuthHelper.createAuthHelper(git, settings)
@ -49,7 +66,9 @@ describe('git-auth-helper tests', () => {
await authHelper.configureAuth() await authHelper.configureAuth()
// Assert config // Assert config
const configContent = (await fs.promises.readFile(gitConfigPath)).toString() const configContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
const basicCredential = Buffer.from( const basicCredential = Buffer.from(
`x-access-token:${settings.authToken}`, `x-access-token:${settings.authToken}`,
'utf8' 'utf8'
@ -61,11 +80,15 @@ describe('git-auth-helper tests', () => {
).toBeGreaterThanOrEqual(0) ).toBeGreaterThanOrEqual(0)
}) })
const configuresAuthHeaderEvenWhenPersistCredentialsFalse = const configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse =
'configures auth header even when persist credentials false' 'configureAuth configures auth header even when persist credentials false'
it(configuresAuthHeaderEvenWhenPersistCredentialsFalse, async () => { it(
configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse,
async () => {
// Arrange // Arrange
await setup(configuresAuthHeaderEvenWhenPersistCredentialsFalse) await setup(
configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse
)
expect(settings.authToken).toBeTruthy() // sanity check expect(settings.authToken).toBeTruthy() // sanity check
settings.persistCredentials = false settings.persistCredentials = false
const authHelper = gitAuthHelper.createAuthHelper(git, settings) const authHelper = gitAuthHelper.createAuthHelper(git, settings)
@ -74,19 +97,22 @@ describe('git-auth-helper tests', () => {
await authHelper.configureAuth() await authHelper.configureAuth()
// Assert config // Assert config
const configContent = (await fs.promises.readFile(gitConfigPath)).toString() const configContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
expect( expect(
configContent.indexOf( configContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION` `http.https://github.com/.extraheader AUTHORIZATION`
) )
).toBeGreaterThanOrEqual(0) ).toBeGreaterThanOrEqual(0)
}) }
)
const registersBasicCredentialAsSecret = const configureAuth_registersBasicCredentialAsSecret =
'registers basic credential as secret' 'configureAuth registers basic credential as secret'
it(registersBasicCredentialAsSecret, async () => { it(configureAuth_registersBasicCredentialAsSecret, async () => {
// Arrange // Arrange
await setup(registersBasicCredentialAsSecret) await setup(configureAuth_registersBasicCredentialAsSecret)
expect(settings.authToken).toBeTruthy() // sanity check expect(settings.authToken).toBeTruthy() // sanity check
const authHelper = gitAuthHelper.createAuthHelper(git, settings) const authHelper = gitAuthHelper.createAuthHelper(git, settings)
@ -103,14 +129,139 @@ describe('git-auth-helper tests', () => {
expect(setSecretSpy).toHaveBeenCalledWith(expectedSecret) expect(setSecretSpy).toHaveBeenCalledWith(expectedSecret)
}) })
const removesToken = 'removes token' const configureGlobalAuth_copiesGlobalGitConfig =
it(removesToken, async () => { 'configureGlobalAuth copies global git config'
it(configureGlobalAuth_copiesGlobalGitConfig, async () => {
// Arrange // Arrange
await setup(removesToken) await setup(configureGlobalAuth_copiesGlobalGitConfig)
await fs.promises.writeFile(globalGitConfigPath, 'value-from-global-config')
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
// Act
await authHelper.configureAuth()
await authHelper.configureGlobalAuth()
// Assert original global config not altered
let configContent = (
await fs.promises.readFile(globalGitConfigPath)
).toString()
expect(configContent).toBe('value-from-global-config')
// Assert temporary global config
expect(git.env['HOME']).toBeTruthy()
const basicCredential = Buffer.from(
`x-access-token:${settings.authToken}`,
'utf8'
).toString('base64')
configContent = (
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
).toString()
expect(
configContent.indexOf('value-from-global-config')
).toBeGreaterThanOrEqual(0)
expect(
configContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
)
).toBeGreaterThanOrEqual(0)
})
const configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist =
'configureGlobalAuth creates new git config when global does not exist'
it(
configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist,
async () => {
// Arrange
await setup(
configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist
)
await io.rmRF(globalGitConfigPath)
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
// Act
await authHelper.configureAuth()
await authHelper.configureGlobalAuth()
// Assert original global config not recreated
try {
await fs.promises.stat(globalGitConfigPath)
throw new Error(
`Did not expect file to exist: '${globalGitConfigPath}'`
)
} catch (err) {
if (err.code !== 'ENOENT') {
throw err
}
}
// Assert temporary global config
expect(git.env['HOME']).toBeTruthy()
const basicCredential = Buffer.from(
`x-access-token:${settings.authToken}`,
'utf8'
).toString('base64')
const configContent = (
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
).toString()
expect(
configContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
)
).toBeGreaterThanOrEqual(0)
}
)
const configureSubmoduleAuth_doesNotConfigureTokenWhenPersistCredentialsFalse =
'configureSubmoduleAuth does not configure token when persist credentials false'
it(
configureSubmoduleAuth_doesNotConfigureTokenWhenPersistCredentialsFalse,
async () => {
// Arrange
await setup(
configureSubmoduleAuth_doesNotConfigureTokenWhenPersistCredentialsFalse
)
settings.persistCredentials = false
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
;(git.submoduleForeach as jest.Mock<any, any>).mockClear() // reset calls
// Act
await authHelper.configureSubmoduleAuth()
// Assert
expect(git.submoduleForeach).not.toHaveBeenCalled()
}
)
const configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrue =
'configureSubmoduleAuth configures token when persist credentials true'
it(
configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrue,
async () => {
// Arrange
await setup(
configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrue
)
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
;(git.submoduleForeach as jest.Mock<any, any>).mockClear() // reset calls
// Act
await authHelper.configureSubmoduleAuth()
// Assert
expect(git.submoduleForeach).toHaveBeenCalledTimes(1)
}
)
const removeAuth_removesToken = 'removeAuth removes token'
it(removeAuth_removesToken, async () => {
// Arrange
await setup(removeAuth_removesToken)
const authHelper = gitAuthHelper.createAuthHelper(git, settings) const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth() await authHelper.configureAuth()
let gitConfigContent = ( let gitConfigContent = (
await fs.promises.readFile(gitConfigPath) await fs.promises.readFile(localGitConfigPath)
).toString() ).toString()
expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check
@ -118,9 +269,37 @@ describe('git-auth-helper tests', () => {
await authHelper.removeAuth() await authHelper.removeAuth()
// Assert git config // Assert git config
gitConfigContent = (await fs.promises.readFile(gitConfigPath)).toString() gitConfigContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
expect(gitConfigContent.indexOf('http.')).toBeLessThan(0) expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
}) })
const removeGlobalAuth_removesOverride = 'removeGlobalAuth removes override'
it(removeGlobalAuth_removesOverride, async () => {
// Arrange
await setup(removeGlobalAuth_removesOverride)
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
await authHelper.configureGlobalAuth()
const homeOverride = git.env['HOME'] // Sanity check
expect(homeOverride).toBeTruthy()
await fs.promises.stat(path.join(git.env['HOME'], '.gitconfig'))
// Act
await authHelper.removeGlobalAuth()
// Assert
expect(git.env['HOME']).toBeUndefined()
try {
await fs.promises.stat(homeOverride)
throw new Error(`Should have been deleted '${homeOverride}'`)
} catch (err) {
if (err.code !== 'ENOENT') {
throw err
}
}
})
}) })
async function setup(testName: string): Promise<void> { async function setup(testName: string): Promise<void> {
@ -129,14 +308,19 @@ async function setup(testName: string): Promise<void> {
// Directories // Directories
workspace = path.join(testWorkspace, testName, 'workspace') workspace = path.join(testWorkspace, testName, 'workspace')
runnerTemp = path.join(testWorkspace, testName, 'runner-temp') runnerTemp = path.join(testWorkspace, testName, 'runner-temp')
tempHomedir = path.join(testWorkspace, testName, 'home-dir')
await fs.promises.mkdir(workspace, {recursive: true}) await fs.promises.mkdir(workspace, {recursive: true})
await fs.promises.mkdir(runnerTemp, {recursive: true}) await fs.promises.mkdir(runnerTemp, {recursive: true})
await fs.promises.mkdir(tempHomedir, {recursive: true})
process.env['RUNNER_TEMP'] = runnerTemp process.env['RUNNER_TEMP'] = runnerTemp
process.env['HOME'] = tempHomedir
// Create git config // Create git config
gitConfigPath = path.join(workspace, '.git', 'config') globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
await fs.promises.mkdir(path.join(workspace, '.git'), {recursive: true}) await fs.promises.writeFile(globalGitConfigPath, '')
await fs.promises.writeFile(path.join(workspace, '.git', 'config'), '') localGitConfigPath = path.join(workspace, '.git', 'config')
await fs.promises.mkdir(path.dirname(localGitConfigPath), {recursive: true})
await fs.promises.writeFile(localGitConfigPath, '')
git = { git = {
branchDelete: jest.fn(), branchDelete: jest.fn(),
@ -144,12 +328,20 @@ async function setup(testName: string): Promise<void> {
branchList: jest.fn(), branchList: jest.fn(),
checkout: jest.fn(), checkout: jest.fn(),
checkoutDetach: jest.fn(), checkoutDetach: jest.fn(),
config: jest.fn(async (key: string, value: string) => { config: jest.fn(
await fs.promises.appendFile(gitConfigPath, `\n${key} ${value}`) async (key: string, value: string, globalConfig?: boolean) => {
}), const configPath = globalConfig
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
: localGitConfigPath
await fs.promises.appendFile(configPath, `\n${key} ${value}`)
}
),
configExists: jest.fn( configExists: jest.fn(
async (key: string): Promise<boolean> => { async (key: string, globalConfig?: boolean): Promise<boolean> => {
const content = await fs.promises.readFile(gitConfigPath) const configPath = globalConfig
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
: localGitConfigPath
const content = await fs.promises.readFile(configPath)
const lines = content const lines = content
.toString() .toString()
.split('\n') .split('\n')
@ -157,6 +349,7 @@ async function setup(testName: string): Promise<void> {
return lines.some(x => x.startsWith(key)) return lines.some(x => x.startsWith(key))
} }
), ),
env: {},
fetch: jest.fn(), fetch: jest.fn(),
getWorkingDirectory: jest.fn(() => workspace), getWorkingDirectory: jest.fn(() => workspace),
init: jest.fn(), init: jest.fn(),
@ -165,18 +358,29 @@ async function setup(testName: string): Promise<void> {
lfsInstall: jest.fn(), lfsInstall: jest.fn(),
log1: jest.fn(), log1: jest.fn(),
remoteAdd: jest.fn(), remoteAdd: jest.fn(),
setEnvironmentVariable: jest.fn(), removeEnvironmentVariable: jest.fn((name: string) => delete git.env[name]),
setEnvironmentVariable: jest.fn((name: string, value: string) => {
git.env[name] = value
}),
submoduleForeach: jest.fn(async () => {
return ''
}),
submoduleSync: jest.fn(),
submoduleUpdate: jest.fn(),
tagExists: jest.fn(), tagExists: jest.fn(),
tryClean: jest.fn(), tryClean: jest.fn(),
tryConfigUnset: jest.fn( tryConfigUnset: jest.fn(
async (key: string): Promise<boolean> => { async (key: string, globalConfig?: boolean): Promise<boolean> => {
let content = await fs.promises.readFile(gitConfigPath) const configPath = globalConfig
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
: localGitConfigPath
let content = await fs.promises.readFile(configPath)
let lines = content let lines = content
.toString() .toString()
.split('\n') .split('\n')
.filter(x => x) .filter(x => x)
.filter(x => !x.startsWith(key)) .filter(x => !x.startsWith(key))
await fs.promises.writeFile(gitConfigPath, lines.join('\n')) await fs.promises.writeFile(configPath, lines.join('\n'))
return true return true
} }
), ),
@ -191,6 +395,8 @@ async function setup(testName: string): Promise<void> {
commit: '', commit: '',
fetchDepth: 1, fetchDepth: 1,
lfs: false, lfs: false,
submodules: false,
nestedSubmodules: false,
persistCredentials: true, persistCredentials: true,
ref: 'refs/heads/master', ref: 'refs/heads/master',
repositoryName: 'my-repo', repositoryName: 'my-repo',

@ -363,7 +363,11 @@ async function setup(testName: string): Promise<void> {
lfsInstall: jest.fn(), lfsInstall: jest.fn(),
log1: jest.fn(), log1: jest.fn(),
remoteAdd: jest.fn(), remoteAdd: jest.fn(),
removeEnvironmentVariable: jest.fn(),
setEnvironmentVariable: jest.fn(), setEnvironmentVariable: jest.fn(),
submoduleForeach: jest.fn(),
submoduleSync: jest.fn(),
submoduleUpdate: jest.fn(),
tagExists: jest.fn(), tagExists: jest.fn(),
tryClean: jest.fn(async () => { tryClean: jest.fn(async () => {
return true return true

@ -130,11 +130,4 @@ describe('input-helper tests', () => {
expect(settings.ref).toBe('refs/heads/some-other-ref') expect(settings.ref).toBe('refs/heads/some-other-ref')
expect(settings.commit).toBeFalsy() expect(settings.commit).toBeFalsy()
}) })
it('gives good error message for submodules input', () => {
inputs.submodules = 'true'
assert.throws(() => {
inputHelper.getInputs()
}, /The input 'submodules' is not supported/)
})
}) })

@ -0,0 +1,11 @@
#!/bin/bash
if [ ! -f "./submodules-false/regular-file.txt" ]; then
echo "Expected regular file does not exist"
exit 1
fi
if [ -f "./submodules-false/submodule-level-1/submodule-file.txt" ]; then
echo "Unexpected submodule file exists"
exit 1
fi

@ -1,11 +0,0 @@
#!/bin/bash
if [ ! -f "./submodules-not-checked-out/regular-file.txt" ]; then
echo "Expected regular file does not exist"
exit 1
fi
if [ -f "./submodules-not-checked-out/submodule-level-1/submodule-file.txt" ]; then
echo "Unexpected submodule file exists"
exit 1
fi

@ -0,0 +1,26 @@
#!/bin/bash
if [ ! -f "./submodules-recursive/regular-file.txt" ]; then
echo "Expected regular file does not exist"
exit 1
fi
if [ ! -f "./submodules-recursive/submodule-level-1/submodule-file.txt" ]; then
echo "Expected submodule file does not exist"
exit 1
fi
if [ ! -f "./submodules-recursive/submodule-level-1/submodule-level-2/nested-submodule-file.txt" ]; then
echo "Expected nested submodule file does not exists"
exit 1
fi
echo "Testing persisted credential"
pushd ./submodules-recursive/submodule-level-1/submodule-level-2
git config --local --name-only --get-regexp http.+extraheader && git fetch
if [ "$?" != "0" ]; then
echo "Failed to validate persisted credential"
popd
exit 1
fi
popd

@ -0,0 +1,26 @@
#!/bin/bash
if [ ! -f "./submodules-true/regular-file.txt" ]; then
echo "Expected regular file does not exist"
exit 1
fi
if [ ! -f "./submodules-true/submodule-level-1/submodule-file.txt" ]; then
echo "Expected submodule file does not exist"
exit 1
fi
if [ -f "./submodules-true/submodule-level-1/submodule-level-2/nested-submodule-file.txt" ]; then
echo "Unexpected nested submodule file exists"
exit 1
fi
echo "Testing persisted credential"
pushd ./submodules-true/submodule-level-1
git config --local --name-only --get-regexp http.+extraheader && git fetch
if [ "$?" != "0" ]; then
echo "Failed to validate persisted credential"
popd
exit 1
fi
popd

@ -30,6 +30,11 @@ inputs:
lfs: lfs:
description: 'Whether to download Git-LFS files' description: 'Whether to download Git-LFS files'
default: false default: false
submodules:
description: >
Whether to checkout submodules: `true` to checkout submodules or `recursive` to
recursively checkout submodules.
default: false
runs: runs:
using: node12 using: node12
main: dist/index.js main: dist/index.js

245
dist/index.js vendored

@ -5074,21 +5074,35 @@ var __importStar = (this && this.__importStar) || function (mod) {
result["default"] = mod; result["default"] = mod;
return result; return result;
}; };
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
const assert = __importStar(__webpack_require__(357));
const core = __importStar(__webpack_require__(470)); const core = __importStar(__webpack_require__(470));
const fs = __importStar(__webpack_require__(747)); const fs = __importStar(__webpack_require__(747));
const io = __importStar(__webpack_require__(1));
const os = __importStar(__webpack_require__(87));
const path = __importStar(__webpack_require__(622)); const path = __importStar(__webpack_require__(622));
const regexpHelper = __importStar(__webpack_require__(528));
const v4_1 = __importDefault(__webpack_require__(826));
const IS_WINDOWS = process.platform === 'win32'; const IS_WINDOWS = process.platform === 'win32';
const HOSTNAME = 'github.com'; const HOSTNAME = 'github.com';
const EXTRA_HEADER_KEY = `http.https://${HOSTNAME}/.extraheader`;
function createAuthHelper(git, settings) { function createAuthHelper(git, settings) {
return new GitAuthHelper(git, settings); return new GitAuthHelper(git, settings);
} }
exports.createAuthHelper = createAuthHelper; exports.createAuthHelper = createAuthHelper;
class GitAuthHelper { class GitAuthHelper {
constructor(gitCommandManager, gitSourceSettings) { constructor(gitCommandManager, gitSourceSettings) {
this.tokenConfigKey = `http.https://${HOSTNAME}/.extraheader`;
this.temporaryHomePath = '';
this.git = gitCommandManager; this.git = gitCommandManager;
this.settings = gitSourceSettings || {}; this.settings = gitSourceSettings || {};
// Token auth header
const basicCredential = Buffer.from(`x-access-token:${this.settings.authToken}`, 'utf8').toString('base64');
core.setSecret(basicCredential);
this.tokenPlaceholderConfigValue = `AUTHORIZATION: basic ***`;
this.tokenConfigValue = `AUTHORIZATION: basic ${basicCredential}`;
} }
configureAuth() { configureAuth() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
@ -5098,37 +5112,110 @@ class GitAuthHelper {
yield this.configureToken(); yield this.configureToken();
}); });
} }
configureGlobalAuth() {
return __awaiter(this, void 0, void 0, function* () {
// Create a temp home directory
const runnerTemp = process.env['RUNNER_TEMP'] || '';
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined');
const uniqueId = v4_1.default();
this.temporaryHomePath = path.join(runnerTemp, uniqueId);
yield fs.promises.mkdir(this.temporaryHomePath, { recursive: true });
// Copy the global git config
const gitConfigPath = path.join(process.env['HOME'] || os.homedir(), '.gitconfig');
const newGitConfigPath = path.join(this.temporaryHomePath, '.gitconfig');
let configExists = false;
try {
yield fs.promises.stat(gitConfigPath);
configExists = true;
}
catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
}
if (configExists) {
core.info(`Copying '${gitConfigPath}' to '${newGitConfigPath}'`);
yield io.cp(gitConfigPath, newGitConfigPath);
}
else {
yield fs.promises.writeFile(newGitConfigPath, '');
}
// Configure the token
try {
core.info(`Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`);
this.git.setEnvironmentVariable('HOME', this.temporaryHomePath);
yield this.configureToken(newGitConfigPath, true);
}
catch (err) {
// Unset in case somehow written to the real global config
core.info('Encountered an error when attempting to configure token. Attempting unconfigure.');
yield this.git.tryConfigUnset(this.tokenConfigKey, true);
throw err;
}
});
}
configureSubmoduleAuth() {
return __awaiter(this, void 0, void 0, function* () {
if (this.settings.persistCredentials) {
// Configure a placeholder value. This approach avoids the credential being captured
// by process creation audit events, which are commonly logged. For more information,
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
const output = yield this.git.submoduleForeach(`git config "${this.tokenConfigKey}" "${this.tokenPlaceholderConfigValue}" && git config --local --show-origin --name-only --get-regexp remote.origin.url`, this.settings.nestedSubmodules);
// Replace the placeholder
const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
for (const configPath of configPaths) {
core.debug(`Replacing token placeholder in '${configPath}'`);
this.replaceTokenPlaceholder(configPath);
}
}
});
}
removeAuth() { removeAuth() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
yield this.removeToken(); yield this.removeToken();
}); });
} }
configureToken() { removeGlobalAuth() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
core.info(`Unsetting HOME override`);
this.git.removeEnvironmentVariable('HOME');
yield io.rmRF(this.temporaryHomePath);
});
}
configureToken(configPath, globalConfig) {
return __awaiter(this, void 0, void 0, function* () {
// Validate args
assert.ok((configPath && globalConfig) || (!configPath && !globalConfig), 'Unexpected configureToken parameter combinations');
// Default config path
if (!configPath && !globalConfig) {
configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config');
}
// Configure a placeholder value. This approach avoids the credential being captured // Configure a placeholder value. This approach avoids the credential being captured
// by process creation audit events, which are commonly logged. For more information, // by process creation audit events, which are commonly logged. For more information,
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
const placeholder = `AUTHORIZATION: basic ***`; yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, globalConfig);
yield this.git.config(EXTRA_HEADER_KEY, placeholder); // Replace the placeholder
// Determine the basic credential value yield this.replaceTokenPlaceholder(configPath || '');
const basicCredential = Buffer.from(`x-access-token:${this.settings.authToken}`, 'utf8').toString('base64'); });
core.setSecret(basicCredential);
// Replace the value in the config file
const configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config');
let content = (yield fs.promises.readFile(configPath)).toString();
const placeholderIndex = content.indexOf(placeholder);
if (placeholderIndex < 0 ||
placeholderIndex != content.lastIndexOf(placeholder)) {
throw new Error('Unable to replace auth placeholder in .git/config');
} }
content = content.replace(placeholder, `AUTHORIZATION: basic ${basicCredential}`); replaceTokenPlaceholder(configPath) {
return __awaiter(this, void 0, void 0, function* () {
assert.ok(configPath, 'configPath is not defined');
let content = (yield fs.promises.readFile(configPath)).toString();
const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue);
if (placeholderIndex < 0 ||
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)) {
throw new Error(`Unable to replace auth placeholder in ${configPath}`);
}
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined');
content = content.replace(this.tokenPlaceholderConfigValue, this.tokenConfigValue);
yield fs.promises.writeFile(configPath, content); yield fs.promises.writeFile(configPath, content);
}); });
} }
removeToken() { removeToken() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
// HTTP extra header // HTTP extra header
yield this.removeGitConfig(EXTRA_HEADER_KEY); yield this.removeGitConfig(this.tokenConfigKey);
}); });
} }
removeGitConfig(configKey) { removeGitConfig(configKey) {
@ -5138,6 +5225,8 @@ class GitAuthHelper {
// Load the config contents // Load the config contents
core.warning(`Failed to remove '${configKey}' from the git config`); core.warning(`Failed to remove '${configKey}' from the git config`);
} }
const pattern = regexpHelper.escape(configKey);
yield this.git.submoduleForeach(`git config --local --name-only --get-regexp ${pattern} && git config --local --unset-all ${configKey} || :`, true);
}); });
} }
} }
@ -5172,6 +5261,7 @@ const exec = __importStar(__webpack_require__(986));
const fshelper = __importStar(__webpack_require__(618)); const fshelper = __importStar(__webpack_require__(618));
const io = __importStar(__webpack_require__(1)); const io = __importStar(__webpack_require__(1));
const path = __importStar(__webpack_require__(622)); const path = __importStar(__webpack_require__(622));
const regexpHelper = __importStar(__webpack_require__(528));
const retryHelper = __importStar(__webpack_require__(587)); const retryHelper = __importStar(__webpack_require__(587));
const git_version_1 = __webpack_require__(559); const git_version_1 = __webpack_require__(559);
// Auth header not supported before 2.9 // Auth header not supported before 2.9
@ -5263,17 +5353,26 @@ class GitCommandManager {
yield this.execGit(args); yield this.execGit(args);
}); });
} }
config(configKey, configValue) { config(configKey, configValue, globalConfig) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
yield this.execGit(['config', '--local', configKey, configValue]); yield this.execGit([
'config',
globalConfig ? '--global' : '--local',
configKey,
configValue
]);
}); });
} }
configExists(configKey) { configExists(configKey, globalConfig) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const pattern = configKey.replace(/[^a-zA-Z0-9_]/g, x => { const pattern = regexpHelper.escape(configKey);
return `\\${x}`; const output = yield this.execGit([
}); 'config',
const output = yield this.execGit(['config', '--local', '--name-only', '--get-regexp', pattern], true); globalConfig ? '--global' : '--local',
'--name-only',
'--get-regexp',
pattern
], true);
return output.exitCode === 0; return output.exitCode === 0;
}); });
} }
@ -5343,9 +5442,45 @@ class GitCommandManager {
yield this.execGit(['remote', 'add', remoteName, remoteUrl]); yield this.execGit(['remote', 'add', remoteName, remoteUrl]);
}); });
} }
removeEnvironmentVariable(name) {
delete this.gitEnv[name];
}
setEnvironmentVariable(name, value) { setEnvironmentVariable(name, value) {
this.gitEnv[name] = value; this.gitEnv[name] = value;
} }
submoduleForeach(command, recursive) {
return __awaiter(this, void 0, void 0, function* () {
const args = ['submodule', 'foreach'];
if (recursive) {
args.push('--recursive');
}
args.push(command);
const output = yield this.execGit(args);
return output.stdout;
});
}
submoduleSync(recursive) {
return __awaiter(this, void 0, void 0, function* () {
const args = ['submodule', 'sync'];
if (recursive) {
args.push('--recursive');
}
yield this.execGit(args);
});
}
submoduleUpdate(fetchDepth, recursive) {
return __awaiter(this, void 0, void 0, function* () {
const args = ['-c', 'protocol.version=2'];
args.push('submodule', 'update', '--init', '--force');
if (fetchDepth > 0) {
args.push(`--depth=${fetchDepth}`);
}
if (recursive) {
args.push('--recursive');
}
yield this.execGit(args);
});
}
tagExists(pattern) { tagExists(pattern) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const output = yield this.execGit(['tag', '--list', pattern]); const output = yield this.execGit(['tag', '--list', pattern]);
@ -5358,9 +5493,14 @@ class GitCommandManager {
return output.exitCode === 0; return output.exitCode === 0;
}); });
} }
tryConfigUnset(configKey) { tryConfigUnset(configKey, globalConfig) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const output = yield this.execGit(['config', '--local', '--unset-all', configKey], true); const output = yield this.execGit([
'config',
globalConfig ? '--global' : '--local',
'--unset-all',
configKey
], true);
return output.exitCode === 0; return output.exitCode === 0;
}); });
} }
@ -5551,8 +5691,8 @@ function getSource(settings) {
core.info(`The repository will be downloaded using the GitHub REST API`); core.info(`The repository will be downloaded using the GitHub REST API`);
core.info(`To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`); core.info(`To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`);
yield githubApiHelper.downloadRepository(settings.authToken, settings.repositoryOwner, settings.repositoryName, settings.ref, settings.commit, settings.repositoryPath); yield githubApiHelper.downloadRepository(settings.authToken, settings.repositoryOwner, settings.repositoryName, settings.ref, settings.commit, settings.repositoryPath);
return;
} }
else {
// Save state for POST action // Save state for POST action
stateHelper.setRepositoryPath(settings.repositoryPath); stateHelper.setRepositoryPath(settings.repositoryPath);
// Initialize the repository // Initialize the repository
@ -5585,6 +5725,25 @@ function getSource(settings) {
} }
// Checkout // Checkout
yield git.checkout(checkoutInfo.ref, checkoutInfo.startPoint); yield git.checkout(checkoutInfo.ref, checkoutInfo.startPoint);
// Submodules
if (settings.submodules) {
try {
// Temporarily override global config
yield authHelper.configureGlobalAuth();
// Checkout submodules
yield git.submoduleSync(settings.nestedSubmodules);
yield git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules);
yield git.submoduleForeach('git config --local gc.auto 0', settings.nestedSubmodules);
// Persist credentials
if (settings.persistCredentials) {
yield authHelper.configureSubmoduleAuth();
}
}
finally {
// Remove temporary global config override
yield authHelper.removeGlobalAuth();
}
}
// Dump some info about the checked out commit // Dump some info about the checked out commit
yield git.log1(); yield git.log1();
} }
@ -5594,7 +5753,6 @@ function getSource(settings) {
yield authHelper.removeAuth(); yield authHelper.removeAuth();
} }
} }
}
}); });
} }
exports.getSource = getSource; exports.getSource = getSource;
@ -9428,6 +9586,22 @@ module.exports.Singular = Hook.Singular
module.exports.Collection = Hook.Collection module.exports.Collection = Hook.Collection
/***/ }),
/***/ 528:
/***/ (function(__unusedmodule, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function escape(value) {
return value.replace(/[^a-zA-Z0-9_]/g, x => {
return `\\${x}`;
});
}
exports.escape = escape;
/***/ }), /***/ }),
/***/ 529: /***/ 529:
@ -13731,10 +13905,6 @@ function getInputs() {
// Clean // Clean
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'; result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE';
core.debug(`clean = ${result.clean}`); core.debug(`clean = ${result.clean}`);
// Submodules
if (core.getInput('submodules')) {
throw new Error("The input 'submodules' is not supported in actions/checkout@v2");
}
// Fetch depth // Fetch depth
result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1')); result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1'));
if (isNaN(result.fetchDepth) || result.fetchDepth < 0) { if (isNaN(result.fetchDepth) || result.fetchDepth < 0) {
@ -13744,6 +13914,19 @@ function getInputs() {
// LFS // LFS
result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE'; result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE';
core.debug(`lfs = ${result.lfs}`); core.debug(`lfs = ${result.lfs}`);
// Submodules
result.submodules = false;
result.nestedSubmodules = false;
const submodulesString = (core.getInput('submodules') || '').toUpperCase();
if (submodulesString == 'RECURSIVE') {
result.submodules = true;
result.nestedSubmodules = true;
}
else if (submodulesString == 'TRUE') {
result.submodules = true;
}
core.debug(`submodules = ${result.submodules}`);
core.debug(`recursive submodules = ${result.nestedSubmodules}`);
// Auth token // Auth token
result.authToken = core.getInput('token'); result.authToken = core.getInput('token');
// Persist credentials // Persist credentials

@ -5,6 +5,7 @@ import * as fs from 'fs'
import * as io from '@actions/io' import * as io from '@actions/io'
import * as os from 'os' import * as os from 'os'
import * as path from 'path' import * as path from 'path'
import * as regexpHelper from './regexp-helper'
import * as stateHelper from './state-helper' import * as stateHelper from './state-helper'
import {default as uuid} from 'uuid/v4' import {default as uuid} from 'uuid/v4'
import {IGitCommandManager} from './git-command-manager' import {IGitCommandManager} from './git-command-manager'
@ -12,11 +13,13 @@ import {IGitSourceSettings} from './git-source-settings'
const IS_WINDOWS = process.platform === 'win32' const IS_WINDOWS = process.platform === 'win32'
const HOSTNAME = 'github.com' const HOSTNAME = 'github.com'
const EXTRA_HEADER_KEY = `http.https://${HOSTNAME}/.extraheader`
export interface IGitAuthHelper { export interface IGitAuthHelper {
configureAuth(): Promise<void> configureAuth(): Promise<void>
configureGlobalAuth(): Promise<void>
configureSubmoduleAuth(): Promise<void>
removeAuth(): Promise<void> removeAuth(): Promise<void>
removeGlobalAuth(): Promise<void>
} }
export function createAuthHelper( export function createAuthHelper(
@ -27,8 +30,12 @@ export function createAuthHelper(
} }
class GitAuthHelper { class GitAuthHelper {
private git: IGitCommandManager private readonly git: IGitCommandManager
private settings: IGitSourceSettings private readonly settings: IGitSourceSettings
private readonly tokenConfigKey: string = `http.https://${HOSTNAME}/.extraheader`
private readonly tokenPlaceholderConfigValue: string
private temporaryHomePath = ''
private tokenConfigValue: string
constructor( constructor(
gitCommandManager: IGitCommandManager, gitCommandManager: IGitCommandManager,
@ -36,6 +43,15 @@ class GitAuthHelper {
) { ) {
this.git = gitCommandManager this.git = gitCommandManager
this.settings = gitSourceSettings || (({} as unknown) as IGitSourceSettings) this.settings = gitSourceSettings || (({} as unknown) as IGitSourceSettings)
// Token auth header
const basicCredential = Buffer.from(
`x-access-token:${this.settings.authToken}`,
'utf8'
).toString('base64')
core.setSecret(basicCredential)
this.tokenPlaceholderConfigValue = `AUTHORIZATION: basic ***`
this.tokenConfigValue = `AUTHORIZATION: basic ${basicCredential}`
} }
async configureAuth(): Promise<void> { async configureAuth(): Promise<void> {
@ -46,48 +62,132 @@ class GitAuthHelper {
await this.configureToken() await this.configureToken()
} }
async configureGlobalAuth(): Promise<void> {
// Create a temp home directory
const runnerTemp = process.env['RUNNER_TEMP'] || ''
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
const uniqueId = uuid()
this.temporaryHomePath = path.join(runnerTemp, uniqueId)
await fs.promises.mkdir(this.temporaryHomePath, {recursive: true})
// Copy the global git config
const gitConfigPath = path.join(
process.env['HOME'] || os.homedir(),
'.gitconfig'
)
const newGitConfigPath = path.join(this.temporaryHomePath, '.gitconfig')
let configExists = false
try {
await fs.promises.stat(gitConfigPath)
configExists = true
} catch (err) {
if (err.code !== 'ENOENT') {
throw err
}
}
if (configExists) {
core.info(`Copying '${gitConfigPath}' to '${newGitConfigPath}'`)
await io.cp(gitConfigPath, newGitConfigPath)
} else {
await fs.promises.writeFile(newGitConfigPath, '')
}
// Configure the token
try {
core.info(
`Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`
)
this.git.setEnvironmentVariable('HOME', this.temporaryHomePath)
await this.configureToken(newGitConfigPath, true)
} catch (err) {
// Unset in case somehow written to the real global config
core.info(
'Encountered an error when attempting to configure token. Attempting unconfigure.'
)
await this.git.tryConfigUnset(this.tokenConfigKey, true)
throw err
}
}
async configureSubmoduleAuth(): Promise<void> {
if (this.settings.persistCredentials) {
// Configure a placeholder value. This approach avoids the credential being captured
// by process creation audit events, which are commonly logged. For more information,
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
const output = await this.git.submoduleForeach(
`git config "${this.tokenConfigKey}" "${this.tokenPlaceholderConfigValue}" && git config --local --show-origin --name-only --get-regexp remote.origin.url`,
this.settings.nestedSubmodules
)
// Replace the placeholder
const configPaths: string[] =
output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
for (const configPath of configPaths) {
core.debug(`Replacing token placeholder in '${configPath}'`)
this.replaceTokenPlaceholder(configPath)
}
}
}
async removeAuth(): Promise<void> { async removeAuth(): Promise<void> {
await this.removeToken() await this.removeToken()
} }
private async configureToken(): Promise<void> { async removeGlobalAuth(): Promise<void> {
core.info(`Unsetting HOME override`)
this.git.removeEnvironmentVariable('HOME')
await io.rmRF(this.temporaryHomePath)
}
private async configureToken(
configPath?: string,
globalConfig?: boolean
): Promise<void> {
// Validate args
assert.ok(
(configPath && globalConfig) || (!configPath && !globalConfig),
'Unexpected configureToken parameter combinations'
)
// Default config path
if (!configPath && !globalConfig) {
configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config')
}
// Configure a placeholder value. This approach avoids the credential being captured // Configure a placeholder value. This approach avoids the credential being captured
// by process creation audit events, which are commonly logged. For more information, // by process creation audit events, which are commonly logged. For more information,
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
const placeholder = `AUTHORIZATION: basic ***` await this.git.config(
await this.git.config(EXTRA_HEADER_KEY, placeholder) this.tokenConfigKey,
this.tokenPlaceholderConfigValue,
// Determine the basic credential value globalConfig
const basicCredential = Buffer.from(
`x-access-token:${this.settings.authToken}`,
'utf8'
).toString('base64')
core.setSecret(basicCredential)
// Replace the value in the config file
const configPath = path.join(
this.git.getWorkingDirectory(),
'.git',
'config'
) )
// Replace the placeholder
await this.replaceTokenPlaceholder(configPath || '')
}
private async replaceTokenPlaceholder(configPath: string): Promise<void> {
assert.ok(configPath, 'configPath is not defined')
let content = (await fs.promises.readFile(configPath)).toString() let content = (await fs.promises.readFile(configPath)).toString()
const placeholderIndex = content.indexOf(placeholder) const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue)
if ( if (
placeholderIndex < 0 || placeholderIndex < 0 ||
placeholderIndex != content.lastIndexOf(placeholder) placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)
) { ) {
throw new Error('Unable to replace auth placeholder in .git/config') throw new Error(`Unable to replace auth placeholder in ${configPath}`)
} }
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
content = content.replace( content = content.replace(
placeholder, this.tokenPlaceholderConfigValue,
`AUTHORIZATION: basic ${basicCredential}` this.tokenConfigValue
) )
await fs.promises.writeFile(configPath, content) await fs.promises.writeFile(configPath, content)
} }
private async removeToken(): Promise<void> { private async removeToken(): Promise<void> {
// HTTP extra header // HTTP extra header
await this.removeGitConfig(EXTRA_HEADER_KEY) await this.removeGitConfig(this.tokenConfigKey)
} }
private async removeGitConfig(configKey: string): Promise<void> { private async removeGitConfig(configKey: string): Promise<void> {
@ -98,5 +198,11 @@ class GitAuthHelper {
// Load the config contents // Load the config contents
core.warning(`Failed to remove '${configKey}' from the git config`) core.warning(`Failed to remove '${configKey}' from the git config`)
} }
const pattern = regexpHelper.escape(configKey)
await this.git.submoduleForeach(
`git config --local --name-only --get-regexp ${pattern} && git config --local --unset-all ${configKey} || :`,
true
)
} }
} }

@ -3,6 +3,7 @@ import * as exec from '@actions/exec'
import * as fshelper from './fs-helper' import * as fshelper from './fs-helper'
import * as io from '@actions/io' import * as io from '@actions/io'
import * as path from 'path' import * as path from 'path'
import * as regexpHelper from './regexp-helper'
import * as retryHelper from './retry-helper' import * as retryHelper from './retry-helper'
import {GitVersion} from './git-version' import {GitVersion} from './git-version'
@ -16,8 +17,12 @@ export interface IGitCommandManager {
branchList(remote: boolean): Promise<string[]> branchList(remote: boolean): Promise<string[]>
checkout(ref: string, startPoint: string): Promise<void> checkout(ref: string, startPoint: string): Promise<void>
checkoutDetach(): Promise<void> checkoutDetach(): Promise<void>
config(configKey: string, configValue: string): Promise<void> config(
configExists(configKey: string): Promise<boolean> configKey: string,
configValue: string,
globalConfig?: boolean
): Promise<void>
configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
fetch(fetchDepth: number, refSpec: string[]): Promise<void> fetch(fetchDepth: number, refSpec: string[]): Promise<void>
getWorkingDirectory(): string getWorkingDirectory(): string
init(): Promise<void> init(): Promise<void>
@ -26,10 +31,14 @@ export interface IGitCommandManager {
lfsInstall(): Promise<void> lfsInstall(): Promise<void>
log1(): Promise<void> log1(): Promise<void>
remoteAdd(remoteName: string, remoteUrl: string): Promise<void> remoteAdd(remoteName: string, remoteUrl: string): Promise<void>
removeEnvironmentVariable(name: string): void
setEnvironmentVariable(name: string, value: string): void setEnvironmentVariable(name: string, value: string): void
submoduleForeach(command: string, recursive: boolean): Promise<string>
submoduleSync(recursive: boolean): Promise<void>
submoduleUpdate(fetchDepth: number, recursive: boolean): Promise<void>
tagExists(pattern: string): Promise<boolean> tagExists(pattern: string): Promise<boolean>
tryClean(): Promise<boolean> tryClean(): Promise<boolean>
tryConfigUnset(configKey: string): Promise<boolean> tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
tryDisableAutomaticGarbageCollection(): Promise<boolean> tryDisableAutomaticGarbageCollection(): Promise<boolean>
tryGetFetchUrl(): Promise<string> tryGetFetchUrl(): Promise<string>
tryReset(): Promise<boolean> tryReset(): Promise<boolean>
@ -124,16 +133,32 @@ class GitCommandManager {
await this.execGit(args) await this.execGit(args)
} }
async config(configKey: string, configValue: string): Promise<void> { async config(
await this.execGit(['config', '--local', configKey, configValue]) configKey: string,
configValue: string,
globalConfig?: boolean
): Promise<void> {
await this.execGit([
'config',
globalConfig ? '--global' : '--local',
configKey,
configValue
])
} }
async configExists(configKey: string): Promise<boolean> { async configExists(
const pattern = configKey.replace(/[^a-zA-Z0-9_]/g, x => { configKey: string,
return `\\${x}` globalConfig?: boolean
}) ): Promise<boolean> {
const pattern = regexpHelper.escape(configKey)
const output = await this.execGit( const output = await this.execGit(
['config', '--local', '--name-only', '--get-regexp', pattern], [
'config',
globalConfig ? '--global' : '--local',
'--name-only',
'--get-regexp',
pattern
],
true true
) )
return output.exitCode === 0 return output.exitCode === 0
@ -208,10 +233,48 @@ class GitCommandManager {
await this.execGit(['remote', 'add', remoteName, remoteUrl]) await this.execGit(['remote', 'add', remoteName, remoteUrl])
} }
removeEnvironmentVariable(name: string): void {
delete this.gitEnv[name]
}
setEnvironmentVariable(name: string, value: string): void { setEnvironmentVariable(name: string, value: string): void {
this.gitEnv[name] = value this.gitEnv[name] = value
} }
async submoduleForeach(command: string, recursive: boolean): Promise<string> {
const args = ['submodule', 'foreach']
if (recursive) {
args.push('--recursive')
}
args.push(command)
const output = await this.execGit(args)
return output.stdout
}
async submoduleSync(recursive: boolean): Promise<void> {
const args = ['submodule', 'sync']
if (recursive) {
args.push('--recursive')
}
await this.execGit(args)
}
async submoduleUpdate(fetchDepth: number, recursive: boolean): Promise<void> {
const args = ['-c', 'protocol.version=2']
args.push('submodule', 'update', '--init', '--force')
if (fetchDepth > 0) {
args.push(`--depth=${fetchDepth}`)
}
if (recursive) {
args.push('--recursive')
}
await this.execGit(args)
}
async tagExists(pattern: string): Promise<boolean> { async tagExists(pattern: string): Promise<boolean> {
const output = await this.execGit(['tag', '--list', pattern]) const output = await this.execGit(['tag', '--list', pattern])
return !!output.stdout.trim() return !!output.stdout.trim()
@ -222,9 +285,17 @@ class GitCommandManager {
return output.exitCode === 0 return output.exitCode === 0
} }
async tryConfigUnset(configKey: string): Promise<boolean> { async tryConfigUnset(
configKey: string,
globalConfig?: boolean
): Promise<boolean> {
const output = await this.execGit( const output = await this.execGit(
['config', '--local', '--unset-all', configKey], [
'config',
globalConfig ? '--global' : '--local',
'--unset-all',
configKey
],
true true
) )
return output.exitCode === 0 return output.exitCode === 0

@ -61,7 +61,9 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
settings.commit, settings.commit,
settings.repositoryPath settings.repositoryPath
) )
} else { return
}
// Save state for POST action // Save state for POST action
stateHelper.setRepositoryPath(settings.repositoryPath) stateHelper.setRepositoryPath(settings.repositoryPath)
@ -111,6 +113,33 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
// Checkout // Checkout
await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint) await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
// Submodules
if (settings.submodules) {
try {
// Temporarily override global config
await authHelper.configureGlobalAuth()
// Checkout submodules
await git.submoduleSync(settings.nestedSubmodules)
await git.submoduleUpdate(
settings.fetchDepth,
settings.nestedSubmodules
)
await git.submoduleForeach(
'git config --local gc.auto 0',
settings.nestedSubmodules
)
// Persist credentials
if (settings.persistCredentials) {
await authHelper.configureSubmoduleAuth()
}
} finally {
// Remove temporary global config override
await authHelper.removeGlobalAuth()
}
}
// Dump some info about the checked out commit // Dump some info about the checked out commit
await git.log1() await git.log1()
} finally { } finally {
@ -120,7 +149,6 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
} }
} }
} }
}
export async function cleanup(repositoryPath: string): Promise<void> { export async function cleanup(repositoryPath: string): Promise<void> {
// Repo exists? // Repo exists?

@ -7,6 +7,8 @@ export interface IGitSourceSettings {
clean: boolean clean: boolean
fetchDepth: number fetchDepth: number
lfs: boolean lfs: boolean
submodules: boolean
nestedSubmodules: boolean
authToken: string authToken: string
persistCredentials: boolean persistCredentials: boolean
} }

@ -85,13 +85,6 @@ export function getInputs(): IGitSourceSettings {
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE' result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'
core.debug(`clean = ${result.clean}`) core.debug(`clean = ${result.clean}`)
// Submodules
if (core.getInput('submodules')) {
throw new Error(
"The input 'submodules' is not supported in actions/checkout@v2"
)
}
// Fetch depth // Fetch depth
result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1')) result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1'))
if (isNaN(result.fetchDepth) || result.fetchDepth < 0) { if (isNaN(result.fetchDepth) || result.fetchDepth < 0) {
@ -103,6 +96,19 @@ export function getInputs(): IGitSourceSettings {
result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE' result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE'
core.debug(`lfs = ${result.lfs}`) core.debug(`lfs = ${result.lfs}`)
// Submodules
result.submodules = false
result.nestedSubmodules = false
const submodulesString = (core.getInput('submodules') || '').toUpperCase()
if (submodulesString == 'RECURSIVE') {
result.submodules = true
result.nestedSubmodules = true
} else if (submodulesString == 'TRUE') {
result.submodules = true
}
core.debug(`submodules = ${result.submodules}`)
core.debug(`recursive submodules = ${result.nestedSubmodules}`)
// Auth token // Auth token
result.authToken = core.getInput('token') result.authToken = core.getInput('token')

5
src/regexp-helper.ts Normal file

@ -0,0 +1,5 @@
export function escape(value: string): string {
return value.replace(/[^a-zA-Z0-9_]/g, x => {
return `\\${x}`
})
}