Read-only mirror — the public repositories of the Forgejo instance, refreshed every 10 minutes. The live instance (fg.dieschoenewolke.de) is reachable only over the VPN; clone here with https://git.dieschoenewolke.de/git/<owner>/<repo>.git.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoreric sciple <ericsciple@users.noreply.github.com>2020-03-05 14:21:59 -0500
committerGitHub <noreply@github.com>2020-03-05 14:21:59 -0500
commit7ca5766bfacc6c307d7e69645aa94ac44318af46 (patch)
tree6cf0884441af3df11112f769de7e2bd6b2db727e
parent05e00fbe0759a9339f189796ba5ceb3c4c78a556 (diff)
add support for submodules (#173)
-rw-r--r--.github/workflows/test.yml29
-rw-r--r--README.md5
-rw-r--r--__test__/git-auth-helper.test.ts296
-rw-r--r--__test__/git-directory-helper.test.ts4
-rw-r--r--__test__/input-helper.test.ts7
-rwxr-xr-x__test__/verify-submodules-false.sh11
-rwxr-xr-x__test__/verify-submodules-not-checked-out.sh11
-rwxr-xr-x__test__/verify-submodules-recursive.sh26
-rwxr-xr-x__test__/verify-submodules-true.sh26
-rw-r--r--action.yml5
-rw-r--r--dist/index.js309
-rw-r--r--src/git-auth-helper.ts154
-rw-r--r--src/git-command-manager.ts95
-rw-r--r--src/git-source-provider.ts128
-rw-r--r--src/git-source-settings.ts2
-rw-r--r--src/input-helper.ts20
-rw-r--r--src/regexp-helper.ts5
17 files changed, 914 insertions, 219 deletions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8c0e759..3121c41 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -84,6 +84,35 @@ jobs:
shell: bash
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
- name: Remove basic
if: runner.os != 'windows'
diff --git a/README.md b/README.md
index 54b3429..9233a25 100644
--- a/README.md
+++ b/README.md
@@ -70,6 +70,11 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
# Whether to download Git-LFS files
# Default: false
lfs: ''
+
+ # Whether to checkout submodules: `true` to checkout submodules or `recursive` to
+ # recursively checkout submodules.
+ # Default: false
+ submodules: ''
```
<!-- end usage -->
diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts
index fab9f37..68926f2 100644
--- a/__test__/git-auth-helper.test.ts
+++ b/__test__/git-auth-helper.test.ts
@@ -8,10 +8,13 @@ import {IGitSourceSettings} from '../lib/git-source-settings'
const testWorkspace = path.join(__dirname, '_temp', 'git-auth-helper')
const originalRunnerTemp = process.env['RUNNER_TEMP']
+const originalHome = process.env['HOME']
let workspace: string
-let gitConfigPath: string
+let localGitConfigPath: string
+let globalGitConfigPath: string
let runnerTemp: string
-let git: IGitCommandManager
+let tempHomedir: string
+let git: IGitCommandManager & {env: {[key: string]: string}}
let settings: IGitSourceSettings
describe('git-auth-helper tests', () => {
@@ -23,11 +26,24 @@ describe('git-auth-helper tests', () => {
beforeEach(() => {
// Mock setSecret
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(() => {
// Unregister mocks
jest.restoreAllMocks()
+
+ // Restore HOME
+ if (originalHome) {
+ process.env['HOME'] = originalHome
+ } else {
+ delete process.env['HOME']
+ }
})
afterAll(() => {
@@ -38,10 +54,11 @@ describe('git-auth-helper tests', () => {
}
})
- const configuresAuthHeader = 'configures auth header'
- it(configuresAuthHeader, async () => {
+ const configureAuth_configuresAuthHeader =
+ 'configureAuth configures auth header'
+ it(configureAuth_configuresAuthHeader, async () => {
// Arrange
- await setup(configuresAuthHeader)
+ await setup(configureAuth_configuresAuthHeader)
expect(settings.authToken).toBeTruthy() // sanity check
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
@@ -49,7 +66,9 @@ describe('git-auth-helper tests', () => {
await authHelper.configureAuth()
// Assert config
- const configContent = (await fs.promises.readFile(gitConfigPath)).toString()
+ const configContent = (
+ await fs.promises.readFile(localGitConfigPath)
+ ).toString()
const basicCredential = Buffer.from(
`x-access-token:${settings.authToken}`,
'utf8'
@@ -61,32 +80,39 @@ describe('git-auth-helper tests', () => {
).toBeGreaterThanOrEqual(0)
})
- const configuresAuthHeaderEvenWhenPersistCredentialsFalse =
- 'configures auth header even when persist credentials false'
- it(configuresAuthHeaderEvenWhenPersistCredentialsFalse, async () => {
- // Arrange
- await setup(configuresAuthHeaderEvenWhenPersistCredentialsFalse)
- expect(settings.authToken).toBeTruthy() // sanity check
- settings.persistCredentials = false
- const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+ const configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse =
+ 'configureAuth configures auth header even when persist credentials false'
+ it(
+ configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse,
+ async () => {
+ // Arrange
+ await setup(
+ configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse
+ )
+ expect(settings.authToken).toBeTruthy() // sanity check
+ settings.persistCredentials = false
+ const authHelper = gitAuthHelper.createAuthHelper(git, settings)
- // Act
- await authHelper.configureAuth()
+ // Act
+ await authHelper.configureAuth()
- // Assert config
- const configContent = (await fs.promises.readFile(gitConfigPath)).toString()
- expect(
- configContent.indexOf(
- `http.https://github.com/.extraheader AUTHORIZATION`
- )
- ).toBeGreaterThanOrEqual(0)
- })
+ // Assert config
+ const configContent = (
+ await fs.promises.readFile(localGitConfigPath)
+ ).toString()
+ expect(
+ configContent.indexOf(
+ `http.https://github.com/.extraheader AUTHORIZATION`
+ )
+ ).toBeGreaterThanOrEqual(0)
+ }
+ )
- const registersBasicCredentialAsSecret =
- 'registers basic credential as secret'
- it(registersBasicCredentialAsSecret, async () => {
+ const configureAuth_registersBasicCredentialAsSecret =
+ 'configureAuth registers basic credential as secret'
+ it(configureAuth_registersBasicCredentialAsSecret, async () => {
// Arrange
- await setup(registersBasicCredentialAsSecret)
+ await setup(configureAuth_registersBasicCredentialAsSecret)
expect(settings.authToken).toBeTruthy() // sanity check
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
@@ -103,14 +129,139 @@ describe('git-auth-helper tests', () => {
expect(setSecretSpy).toHaveBeenCalledWith(expectedSecret)
})
- const removesToken = 'removes token'
- it(removesToken, async () => {
+ const configureGlobalAuth_copiesGlobalGitConfig =
+ 'configureGlobalAuth copies global git config'
+ it(configureGlobalAuth_copiesGlobalGitConfig, async () => {
+ // Arrange
+ 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(removesToken)
+ await setup(removeAuth_removesToken)
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
let gitConfigContent = (
- await fs.promises.readFile(gitConfigPath)
+ await fs.promises.readFile(localGitConfigPath)
).toString()
expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check
@@ -118,9 +269,37 @@ describe('git-auth-helper tests', () => {
await authHelper.removeAuth()
// Assert git config
- gitConfigContent = (await fs.promises.readFile(gitConfigPath)).toString()
+ gitConfigContent = (
+ await fs.promises.readFile(localGitConfigPath)
+ ).toString()
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> {
@@ -129,14 +308,19 @@ async function setup(testName: string): Promise<void> {
// Directories
workspace = path.join(testWorkspace, testName, 'workspace')
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(runnerTemp, {recursive: true})
+ await fs.promises.mkdir(tempHomedir, {recursive: true})
process.env['RUNNER_TEMP'] = runnerTemp
+ process.env['HOME'] = tempHomedir
// Create git config
- gitConfigPath = path.join(workspace, '.git', 'config')
- await fs.promises.mkdir(path.join(workspace, '.git'), {recursive: true})
- await fs.promises.writeFile(path.join(workspace, '.git', 'config'), '')
+ globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
+ await fs.promises.writeFile(globalGitConfigPath, '')
+ localGitConfigPath = path.join(workspace, '.git', 'config')
+ await fs.promises.mkdir(path.dirname(localGitConfigPath), {recursive: true})
+ await fs.promises.writeFile(localGitConfigPath, '')
git = {
branchDelete: jest.fn(),
@@ -144,12 +328,20 @@ async function setup(testName: string): Promise<void> {
branchList: jest.fn(),
checkout: jest.fn(),
checkoutDetach: jest.fn(),
- config: jest.fn(async (key: string, value: string) => {
- await fs.promises.appendFile(gitConfigPath, `\n${key} ${value}`)
- }),
+ config: jest.fn(
+ 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(
- async (key: string): Promise<boolean> => {
- const content = await fs.promises.readFile(gitConfigPath)
+ async (key: string, globalConfig?: boolean): Promise<boolean> => {
+ const configPath = globalConfig
+ ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+ : localGitConfigPath
+ const content = await fs.promises.readFile(configPath)
const lines = content
.toString()
.split('\n')
@@ -157,6 +349,7 @@ async function setup(testName: string): Promise<void> {
return lines.some(x => x.startsWith(key))
}
),
+ env: {},
fetch: jest.fn(),
getWorkingDirectory: jest.fn(() => workspace),
init: jest.fn(),
@@ -165,18 +358,29 @@ async function setup(testName: string): Promise<void> {
lfsInstall: jest.fn(),
log1: 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(),
tryClean: jest.fn(),
tryConfigUnset: jest.fn(
- async (key: string): Promise<boolean> => {
- let content = await fs.promises.readFile(gitConfigPath)
+ async (key: string, globalConfig?: boolean): Promise<boolean> => {
+ const configPath = globalConfig
+ ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+ : localGitConfigPath
+ let content = await fs.promises.readFile(configPath)
let lines = content
.toString()
.split('\n')
.filter(x => x)
.filter(x => !x.startsWith(key))
- await fs.promises.writeFile(gitConfigPath, lines.join('\n'))
+ await fs.promises.writeFile(configPath, lines.join('\n'))
return true
}
),
@@ -191,6 +395,8 @@ async function setup(testName: string): Promise<void> {
commit: '',
fetchDepth: 1,
lfs: false,
+ submodules: false,
+ nestedSubmodules: false,
persistCredentials: true,
ref: 'refs/heads/master',
repositoryName: 'my-repo',
diff --git a/__test__/git-directory-helper.test.ts b/__test__/git-directory-helper.test.ts
index 4383e1d..c39a2a5 100644
--- a/__test__/git-directory-helper.test.ts
+++ b/__test__/git-directory-helper.test.ts
@@ -363,7 +363,11 @@ async function setup(testName: string): Promise<void> {
lfsInstall: jest.fn(),
log1: jest.fn(),
remoteAdd: jest.fn(),
+ removeEnvironmentVariable: jest.fn(),
setEnvironmentVariable: jest.fn(),
+ submoduleForeach: jest.fn(),
+ submoduleSync: jest.fn(),
+ submoduleUpdate: jest.fn(),
tagExists: jest.fn(),
tryClean: jest.fn(async () => {
return true
diff --git a/__test__/input-helper.test.ts b/__test__/input-helper.test.ts
index 53c7000..00732ef 100644
--- a/__test__/input-helper.test.ts
+++ b/__test__/input-helper.test.ts
@@ -130,11 +130,4 @@ describe('input-helper tests', () => {
expect(settings.ref).toBe('refs/heads/some-other-ref')
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/)
- })
})
diff --git a/__test__/verify-submodules-false.sh b/__test__/verify-submodules-false.sh
new file mode 100755
index 0000000..733e247
--- /dev/null
+++ b/__test__/verify-submodules-false.sh
@@ -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 \ No newline at end of file
diff --git a/__test__/verify-submodules-not-checked-out.sh b/__test__/verify-submodules-not-checked-out.sh
deleted file mode 100755
index dcc0487..0000000
--- a/__test__/verify-submodules-not-checked-out.sh
+++ /dev/null
@@ -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
diff --git a/__test__/verify-submodules-recursive.sh b/__test__/verify-submodules-recursive.sh
new file mode 100755
index 0000000..1b68f9b
--- /dev/null
+++ b/__test__/verify-submodules-recursive.sh
@@ -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
diff --git a/__test__/verify-submodules-true.sh b/__test__/verify-submodules-true.sh
new file mode 100755
index 0000000..43769fe
--- /dev/null
+++ b/__test__/verify-submodules-true.sh
@@ -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
diff --git a/action.yml b/action.yml
index 9650409..a411037 100644
--- a/action.yml
+++ b/action.yml
@@ -30,6 +30,11 @@ inputs:
lfs:
description: 'Whether to download Git-LFS files'
default: false
+ submodules:
+ description: >
+ Whether to checkout submodules: `true` to checkout submodules or `recursive` to
+ recursively checkout submodules.
+ default: false
runs:
using: node12
main: dist/index.js
diff --git a/dist/index.js b/dist/index.js
index 4b06982..3a29067 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -5074,21 +5074,35 @@ var __importStar = (this && this.__importStar) || function (mod) {
result["default"] = mod;
return result;
};
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
Object.defineProperty(exports, "__esModule", { value: true });
+const assert = __importStar(__webpack_require__(357));
const core = __importStar(__webpack_require__(470));
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 regexpHelper = __importStar(__webpack_require__(528));
+const v4_1 = __importDefault(__webpack_require__(826));
const IS_WINDOWS = process.platform === 'win32';
const HOSTNAME = 'github.com';
-const EXTRA_HEADER_KEY = `http.https://${HOSTNAME}/.extraheader`;
function createAuthHelper(git, settings) {
return new GitAuthHelper(git, settings);
}
exports.createAuthHelper = createAuthHelper;
class GitAuthHelper {
constructor(gitCommandManager, gitSourceSettings) {
+ this.tokenConfigKey = `http.https://${HOSTNAME}/.extraheader`;
+ this.temporaryHomePath = '';
this.git = gitCommandManager;
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() {
return __awaiter(this, void 0, void 0, function* () {
@@ -5098,37 +5112,110 @@ class GitAuthHelper {
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() {
return __awaiter(this, void 0, void 0, function* () {
yield this.removeToken();
});
}
- configureToken() {
+ removeGlobalAuth() {
+ 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
// 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 placeholder = `AUTHORIZATION: basic ***`;
- yield this.git.config(EXTRA_HEADER_KEY, placeholder);
- // Determine the basic credential value
- 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');
+ yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, globalConfig);
+ // Replace the placeholder
+ yield this.replaceTokenPlaceholder(configPath || '');
+ });
+ }
+ 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(placeholder);
+ const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue);
if (placeholderIndex < 0 ||
- placeholderIndex != content.lastIndexOf(placeholder)) {
- throw new Error('Unable to replace auth placeholder in .git/config');
+ placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)) {
+ throw new Error(`Unable to replace auth placeholder in ${configPath}`);
}
- content = content.replace(placeholder, `AUTHORIZATION: basic ${basicCredential}`);
+ assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined');
+ content = content.replace(this.tokenPlaceholderConfigValue, this.tokenConfigValue);
yield fs.promises.writeFile(configPath, content);
});
}
removeToken() {
return __awaiter(this, void 0, void 0, function* () {
// HTTP extra header
- yield this.removeGitConfig(EXTRA_HEADER_KEY);
+ yield this.removeGitConfig(this.tokenConfigKey);
});
}
removeGitConfig(configKey) {
@@ -5138,6 +5225,8 @@ class GitAuthHelper {
// Load the config contents
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 io = __importStar(__webpack_require__(1));
const path = __importStar(__webpack_require__(622));
+const regexpHelper = __importStar(__webpack_require__(528));
const retryHelper = __importStar(__webpack_require__(587));
const git_version_1 = __webpack_require__(559);
// Auth header not supported before 2.9
@@ -5263,17 +5353,26 @@ class GitCommandManager {
yield this.execGit(args);
});
}
- config(configKey, configValue) {
+ config(configKey, configValue, globalConfig) {
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* () {
- const pattern = configKey.replace(/[^a-zA-Z0-9_]/g, x => {
- return `\\${x}`;
- });
- const output = yield this.execGit(['config', '--local', '--name-only', '--get-regexp', pattern], true);
+ const pattern = regexpHelper.escape(configKey);
+ const output = yield this.execGit([
+ 'config',
+ globalConfig ? '--global' : '--local',
+ '--name-only',
+ '--get-regexp',
+ pattern
+ ], true);
return output.exitCode === 0;
});
}
@@ -5343,9 +5442,45 @@ class GitCommandManager {
yield this.execGit(['remote', 'add', remoteName, remoteUrl]);
});
}
+ removeEnvironmentVariable(name) {
+ delete this.gitEnv[name];
+ }
setEnvironmentVariable(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) {
return __awaiter(this, void 0, void 0, function* () {
const output = yield this.execGit(['tag', '--list', pattern]);
@@ -5358,9 +5493,14 @@ class GitCommandManager {
return output.exitCode === 0;
});
}
- tryConfigUnset(configKey) {
+ tryConfigUnset(configKey, globalConfig) {
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;
});
}
@@ -5551,48 +5691,66 @@ function getSource(settings) {
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`);
yield githubApiHelper.downloadRepository(settings.authToken, settings.repositoryOwner, settings.repositoryName, settings.ref, settings.commit, settings.repositoryPath);
+ return;
}
- else {
- // Save state for POST action
- stateHelper.setRepositoryPath(settings.repositoryPath);
- // Initialize the repository
- if (!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))) {
- yield git.init();
- yield git.remoteAdd('origin', repositoryUrl);
+ // Save state for POST action
+ stateHelper.setRepositoryPath(settings.repositoryPath);
+ // Initialize the repository
+ if (!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))) {
+ yield git.init();
+ yield git.remoteAdd('origin', repositoryUrl);
+ }
+ // Disable automatic garbage collection
+ if (!(yield git.tryDisableAutomaticGarbageCollection())) {
+ core.warning(`Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`);
+ }
+ const authHelper = gitAuthHelper.createAuthHelper(git, settings);
+ try {
+ // Configure auth
+ yield authHelper.configureAuth();
+ // LFS install
+ if (settings.lfs) {
+ yield git.lfsInstall();
}
- // Disable automatic garbage collection
- if (!(yield git.tryDisableAutomaticGarbageCollection())) {
- core.warning(`Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`);
+ // Fetch
+ const refSpec = refHelper.getRefSpec(settings.ref, settings.commit);
+ yield git.fetch(settings.fetchDepth, refSpec);
+ // Checkout info
+ const checkoutInfo = yield refHelper.getCheckoutInfo(git, settings.ref, settings.commit);
+ // LFS fetch
+ // Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time).
+ // Explicit lfs fetch will fetch lfs objects in parallel.
+ if (settings.lfs) {
+ yield git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref);
}
- const authHelper = gitAuthHelper.createAuthHelper(git, settings);
- try {
- // Configure auth
- yield authHelper.configureAuth();
- // LFS install
- if (settings.lfs) {
- yield git.lfsInstall();
+ // Checkout
+ 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();
+ }
}
- // Fetch
- const refSpec = refHelper.getRefSpec(settings.ref, settings.commit);
- yield git.fetch(settings.fetchDepth, refSpec);
- // Checkout info
- const checkoutInfo = yield refHelper.getCheckoutInfo(git, settings.ref, settings.commit);
- // LFS fetch
- // Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time).
- // Explicit lfs fetch will fetch lfs objects in parallel.
- if (settings.lfs) {
- yield git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref);
+ finally {
+ // Remove temporary global config override
+ yield authHelper.removeGlobalAuth();
}
- // Checkout
- yield git.checkout(checkoutInfo.ref, checkoutInfo.startPoint);
- // Dump some info about the checked out commit
- yield git.log1();
}
- finally {
- // Remove auth
- if (!settings.persistCredentials) {
- yield authHelper.removeAuth();
- }
+ // Dump some info about the checked out commit
+ yield git.log1();
+ }
+ finally {
+ // Remove auth
+ if (!settings.persistCredentials) {
+ yield authHelper.removeAuth();
}
}
});
@@ -9430,6 +9588,22 @@ 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:
/***/ (function(module, __unusedexports, __webpack_require__) {
@@ -13731,10 +13905,6 @@ function getInputs() {
// Clean
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE';
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
result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1'));
if (isNaN(result.fetchDepth) || result.fetchDepth < 0) {
@@ -13744,6 +13914,19 @@ function getInputs() {
// LFS
result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE';
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
result.authToken = core.getInput('token');
// Persist credentials
diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts
index 7f7f11f..dd76fe9 100644
--- a/src/git-auth-helper.ts
+++ b/src/git-auth-helper.ts
@@ -5,6 +5,7 @@ import * as fs from 'fs'
import * as io from '@actions/io'
import * as os from 'os'
import * as path from 'path'
+import * as regexpHelper from './regexp-helper'
import * as stateHelper from './state-helper'
import {default as uuid} from 'uuid/v4'
import {IGitCommandManager} from './git-command-manager'
@@ -12,11 +13,13 @@ import {IGitSourceSettings} from './git-source-settings'
const IS_WINDOWS = process.platform === 'win32'
const HOSTNAME = 'github.com'
-const EXTRA_HEADER_KEY = `http.https://${HOSTNAME}/.extraheader`
export interface IGitAuthHelper {
configureAuth(): Promise<void>
+ configureGlobalAuth(): Promise<void>
+ configureSubmoduleAuth(): Promise<void>
removeAuth(): Promise<void>
+ removeGlobalAuth(): Promise<void>
}
export function createAuthHelper(
@@ -27,8 +30,12 @@ export function createAuthHelper(
}
class GitAuthHelper {
- private git: IGitCommandManager
- private settings: IGitSourceSettings
+ private readonly git: IGitCommandManager
+ private readonly settings: IGitSourceSettings
+ private readonly tokenConfigKey: string = `http.https://${HOSTNAME}/.extraheader`
+ private readonly tokenPlaceholderConfigValue: string
+ private temporaryHomePath = ''
+ private tokenConfigValue: string
constructor(
gitCommandManager: IGitCommandManager,
@@ -36,6 +43,15 @@ class GitAuthHelper {
) {
this.git = gitCommandManager
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> {
@@ -46,48 +62,132 @@ class GitAuthHelper {
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> {
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
// 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 placeholder = `AUTHORIZATION: basic ***`
- await this.git.config(EXTRA_HEADER_KEY, placeholder)
+ await this.git.config(
+ this.tokenConfigKey,
+ this.tokenPlaceholderConfigValue,
+ globalConfig
+ )
- // Determine the basic credential value
- const basicCredential = Buffer.from(
- `x-access-token:${this.settings.authToken}`,
- 'utf8'
- ).toString('base64')
- core.setSecret(basicCredential)
+ // Replace the placeholder
+ await this.replaceTokenPlaceholder(configPath || '')
+ }
- // Replace the value in the config file
- const configPath = path.join(
- this.git.getWorkingDirectory(),
- '.git',
- 'config'
- )
+ private async replaceTokenPlaceholder(configPath: string): Promise<void> {
+ assert.ok(configPath, 'configPath is not defined')
let content = (await fs.promises.readFile(configPath)).toString()
- const placeholderIndex = content.indexOf(placeholder)
+ const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue)
if (
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(
- placeholder,
- `AUTHORIZATION: basic ${basicCredential}`
+ this.tokenPlaceholderConfigValue,
+ this.tokenConfigValue
)
await fs.promises.writeFile(configPath, content)
}
private async removeToken(): Promise<void> {
// HTTP extra header
- await this.removeGitConfig(EXTRA_HEADER_KEY)
+ await this.removeGitConfig(this.tokenConfigKey)
}
private async removeGitConfig(configKey: string): Promise<void> {
@@ -98,5 +198,11 @@ class GitAuthHelper {
// Load the config contents
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
+ )
}
}
diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts
index 2b0d054..4cbfe4a 100644
--- a/src/git-command-manager.ts
+++ b/src/git-command-manager.ts
@@ -3,6 +3,7 @@ import * as exec from '@actions/exec'
import * as fshelper from './fs-helper'
import * as io from '@actions/io'
import * as path from 'path'
+import * as regexpHelper from './regexp-helper'
import * as retryHelper from './retry-helper'
import {GitVersion} from './git-version'
@@ -16,8 +17,12 @@ export interface IGitCommandManager {
branchList(remote: boolean): Promise<string[]>
checkout(ref: string, startPoint: string): Promise<void>
checkoutDetach(): Promise<void>
- config(configKey: string, configValue: string): Promise<void>
- configExists(configKey: string): Promise<boolean>
+ config(
+ configKey: string,
+ configValue: string,
+ globalConfig?: boolean
+ ): Promise<void>
+ configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
fetch(fetchDepth: number, refSpec: string[]): Promise<void>
getWorkingDirectory(): string
init(): Promise<void>
@@ -26,10 +31,14 @@ export interface IGitCommandManager {
lfsInstall(): Promise<void>
log1(): Promise<void>
remoteAdd(remoteName: string, remoteUrl: string): Promise<void>
+ removeEnvironmentVariable(name: 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>
tryClean(): Promise<boolean>
- tryConfigUnset(configKey: string): Promise<boolean>
+ tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
tryDisableAutomaticGarbageCollection(): Promise<boolean>
tryGetFetchUrl(): Promise<string>
tryReset(): Promise<boolean>
@@ -124,16 +133,32 @@ class GitCommandManager {
await this.execGit(args)
}
- async config(configKey: string, configValue: string): Promise<void> {
- await this.execGit(['config', '--local', configKey, configValue])
+ async config(
+ configKey: string,
+ configValue: string,
+ globalConfig?: boolean
+ ): Promise<void> {
+ await this.execGit([
+ 'config',
+ globalConfig ? '--global' : '--local',
+ configKey,
+ configValue
+ ])
}
- async configExists(configKey: string): Promise<boolean> {
- const pattern = configKey.replace(/[^a-zA-Z0-9_]/g, x => {
- return `\\${x}`
- })
+ async configExists(
+ configKey: string,
+ globalConfig?: boolean
+ ): Promise<boolean> {
+ const pattern = regexpHelper.escape(configKey)
const output = await this.execGit(
- ['config', '--local', '--name-only', '--get-regexp', pattern],
+ [
+ 'config',
+ globalConfig ? '--global' : '--local',
+ '--name-only',
+ '--get-regexp',
+ pattern
+ ],
true
)
return output.exitCode === 0
@@ -208,10 +233,48 @@ class GitCommandManager {
await this.execGit(['remote', 'add', remoteName, remoteUrl])
}
+ removeEnvironmentVariable(name: string): void {
+ delete this.gitEnv[name]
+ }
+
setEnvironmentVariable(name: string, value: string): void {
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> {
const output = await this.execGit(['tag', '--list', pattern])
return !!output.stdout.trim()
@@ -222,9 +285,17 @@ class GitCommandManager {
return output.exitCode === 0
}
- async tryConfigUnset(configKey: string): Promise<boolean> {
+ async tryConfigUnset(
+ configKey: string,
+ globalConfig?: boolean
+ ): Promise<boolean> {
const output = await this.execGit(
- ['config', '--local', '--unset-all', configKey],
+ [
+ 'config',
+ globalConfig ? '--global' : '--local',
+ '--unset-all',
+ configKey
+ ],
true
)
return output.exitCode === 0
diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts
index 34b822c..90f97c9 100644
--- a/src/git-source-provider.ts
+++ b/src/git-source-provider.ts
@@ -61,64 +61,92 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
settings.commit,
settings.repositoryPath
)
- } else {
- // Save state for POST action
- stateHelper.setRepositoryPath(settings.repositoryPath)
-
- // Initialize the repository
- if (
- !fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
- ) {
- await git.init()
- await git.remoteAdd('origin', repositoryUrl)
- }
+ return
+ }
- // Disable automatic garbage collection
- if (!(await git.tryDisableAutomaticGarbageCollection())) {
- core.warning(
- `Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`
- )
- }
+ // Save state for POST action
+ stateHelper.setRepositoryPath(settings.repositoryPath)
- const authHelper = gitAuthHelper.createAuthHelper(git, settings)
- try {
- // Configure auth
- await authHelper.configureAuth()
+ // Initialize the repository
+ if (
+ !fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
+ ) {
+ await git.init()
+ await git.remoteAdd('origin', repositoryUrl)
+ }
- // LFS install
- if (settings.lfs) {
- await git.lfsInstall()
- }
+ // Disable automatic garbage collection
+ if (!(await git.tryDisableAutomaticGarbageCollection())) {
+ core.warning(
+ `Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`
+ )
+ }
- // Fetch
- const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
- await git.fetch(settings.fetchDepth, refSpec)
-
- // Checkout info
- const checkoutInfo = await refHelper.getCheckoutInfo(
- git,
- settings.ref,
- settings.commit
- )
-
- // LFS fetch
- // Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time).
- // Explicit lfs fetch will fetch lfs objects in parallel.
- if (settings.lfs) {
- await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref)
- }
+ const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+ try {
+ // Configure auth
+ await authHelper.configureAuth()
- // Checkout
- await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
+ // LFS install
+ if (settings.lfs) {
+ await git.lfsInstall()
+ }
+
+ // Fetch
+ const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
+ await git.fetch(settings.fetchDepth, refSpec)
+
+ // Checkout info
+ const checkoutInfo = await refHelper.getCheckoutInfo(
+ git,
+ settings.ref,
+ settings.commit
+ )
+
+ // LFS fetch
+ // Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time).
+ // Explicit lfs fetch will fetch lfs objects in parallel.
+ if (settings.lfs) {
+ await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref)
+ }
- // Dump some info about the checked out commit
- await git.log1()
- } finally {
- // Remove auth
- if (!settings.persistCredentials) {
- await authHelper.removeAuth()
+ // Checkout
+ 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
+ await git.log1()
+ } finally {
+ // Remove auth
+ if (!settings.persistCredentials) {
+ await authHelper.removeAuth()
+ }
}
}
diff --git a/src/git-source-settings.ts b/src/git-source-settings.ts
index 49d8825..e411fad 100644
--- a/src/git-source-settings.ts
+++ b/src/git-source-settings.ts
@@ -7,6 +7,8 @@ export interface IGitSourceSettings {
clean: boolean
fetchDepth: number
lfs: boolean
+ submodules: boolean
+ nestedSubmodules: boolean
authToken: string
persistCredentials: boolean
}
diff --git a/src/input-helper.ts b/src/input-helper.ts
index e50dbc5..3769350 100644
--- a/src/input-helper.ts
+++ b/src/input-helper.ts
@@ -85,13 +85,6 @@ export function getInputs(): IGitSourceSettings {
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'
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
result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1'))
if (isNaN(result.fetchDepth) || result.fetchDepth < 0) {
@@ -103,6 +96,19 @@ export function getInputs(): IGitSourceSettings {
result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE'
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
result.authToken = core.getInput('token')
diff --git a/src/regexp-helper.ts b/src/regexp-helper.ts
new file mode 100644
index 0000000..ec76c3a
--- /dev/null
+++ b/src/regexp-helper.ts
@@ -0,0 +1,5 @@
+export function escape(value: string): string {
+ return value.replace(/[^a-zA-Z0-9_]/g, x => {
+ return `\\${x}`
+ })
+}