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
path: root/src
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 /src
parent05e00fbe0759a9339f189796ba5ceb3c4c78a556 (diff)
add support for submodules (#173)
Diffstat (limited to 'src')
-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
6 files changed, 311 insertions, 93 deletions
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}`
+ })
+}