mirror of
https://github.com/actions/checkout
synced 2024-11-09 23:22:40 +00:00
400 lines
11 KiB
TypeScript
400 lines
11 KiB
TypeScript
|
import * as core from '@actions/core'
|
||
|
import * as exec from '@actions/exec'
|
||
|
import * as fshelper from './fs-helper'
|
||
|
import * as io from '@actions/io'
|
||
|
import * as path from 'path'
|
||
|
import {GitVersion} from './git-version'
|
||
|
|
||
|
export interface IGitCommandManager {
|
||
|
branchDelete(remote: boolean, branch: string): Promise<void>
|
||
|
branchExists(remote: boolean, pattern: string): Promise<boolean>
|
||
|
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>
|
||
|
fetch(fetchDepth: number, refSpec: string[]): Promise<void>
|
||
|
getWorkingDirectory(): string
|
||
|
init(): Promise<void>
|
||
|
isDetached(): Promise<boolean>
|
||
|
lfsFetch(ref: string): Promise<void>
|
||
|
lfsInstall(): Promise<void>
|
||
|
log1(): Promise<void>
|
||
|
remoteAdd(remoteName: string, remoteUrl: string): Promise<void>
|
||
|
tagExists(pattern: string): Promise<boolean>
|
||
|
tryClean(): Promise<boolean>
|
||
|
tryConfigUnset(configKey: string): Promise<boolean>
|
||
|
tryDisableAutomaticGarbageCollection(): Promise<boolean>
|
||
|
tryGetFetchUrl(): Promise<string>
|
||
|
tryReset(): Promise<boolean>
|
||
|
}
|
||
|
|
||
|
export async function CreateCommandManager(
|
||
|
workingDirectory: string,
|
||
|
lfs: boolean
|
||
|
): Promise<IGitCommandManager> {
|
||
|
return await GitCommandManager.createCommandManager(workingDirectory, lfs)
|
||
|
}
|
||
|
|
||
|
class GitCommandManager {
|
||
|
private gitEnv = {
|
||
|
GIT_TERMINAL_PROMPT: '0', // Disable git prompt
|
||
|
GCM_INTERACTIVE: 'Never' // Disable prompting for git credential manager
|
||
|
}
|
||
|
private gitPath = ''
|
||
|
private lfs = false
|
||
|
private workingDirectory = ''
|
||
|
|
||
|
// Private constructor; use createCommandManager()
|
||
|
private constructor() {}
|
||
|
|
||
|
async branchDelete(remote: boolean, branch: string): Promise<void> {
|
||
|
const args = ['branch', '--delete', '--force']
|
||
|
if (remote) {
|
||
|
args.push('--remote')
|
||
|
}
|
||
|
args.push(branch)
|
||
|
|
||
|
await this.execGit(args)
|
||
|
}
|
||
|
|
||
|
async branchExists(remote: boolean, pattern: string): Promise<boolean> {
|
||
|
const args = ['branch', '--list']
|
||
|
if (remote) {
|
||
|
args.push('--remote')
|
||
|
}
|
||
|
args.push(pattern)
|
||
|
|
||
|
const output = await this.execGit(args)
|
||
|
return !!output.stdout.trim()
|
||
|
}
|
||
|
|
||
|
async branchList(remote: boolean): Promise<string[]> {
|
||
|
const result: string[] = []
|
||
|
|
||
|
// Note, this implementation uses "rev-parse --symbolic" because the output from
|
||
|
// "branch --list" is more difficult when in a detached HEAD state.
|
||
|
|
||
|
const args = ['rev-parse', '--symbolic']
|
||
|
if (remote) {
|
||
|
args.push('--remotes=origin')
|
||
|
} else {
|
||
|
args.push('--branches')
|
||
|
}
|
||
|
|
||
|
const output = await this.execGit(args)
|
||
|
|
||
|
for (let branch of output.stdout.trim().split('\n')) {
|
||
|
branch = branch.trim()
|
||
|
if (branch) {
|
||
|
result.push(branch)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
async checkout(ref: string, startPoint: string): Promise<void> {
|
||
|
const args = ['checkout', '--progress', '--force']
|
||
|
if (startPoint) {
|
||
|
args.push('-B', ref, startPoint)
|
||
|
} else {
|
||
|
args.push(ref)
|
||
|
}
|
||
|
|
||
|
await this.execGit(args)
|
||
|
}
|
||
|
|
||
|
async checkoutDetach(): Promise<void> {
|
||
|
const args = ['checkout', '--detach']
|
||
|
await this.execGit(args)
|
||
|
}
|
||
|
|
||
|
async config(configKey: string, configValue: string): Promise<void> {
|
||
|
await this.execGit(['config', configKey, configValue])
|
||
|
}
|
||
|
|
||
|
async configExists(configKey: string): Promise<boolean> {
|
||
|
const pattern = configKey.replace(/[^a-zA-Z0-9_]/g, x => {
|
||
|
return `\\${x}`
|
||
|
})
|
||
|
const output = await this.execGit(
|
||
|
['config', '--name-only', '--get-regexp', pattern],
|
||
|
true
|
||
|
)
|
||
|
return output.exitCode === 0
|
||
|
}
|
||
|
|
||
|
async fetch(fetchDepth: number, refSpec: string[]): Promise<void> {
|
||
|
const args = [
|
||
|
'-c',
|
||
|
'protocol.version=2',
|
||
|
'fetch',
|
||
|
'--no-tags',
|
||
|
'--prune',
|
||
|
'--progress',
|
||
|
'--no-recurse-submodules'
|
||
|
]
|
||
|
if (fetchDepth > 0) {
|
||
|
args.push(`--depth=${fetchDepth}`)
|
||
|
} else if (
|
||
|
fshelper.fileExistsSync(
|
||
|
path.join(this.workingDirectory, '.git', 'shallow')
|
||
|
)
|
||
|
) {
|
||
|
args.push('--unshallow')
|
||
|
}
|
||
|
|
||
|
args.push('origin')
|
||
|
for (const arg of refSpec) {
|
||
|
args.push(arg)
|
||
|
}
|
||
|
|
||
|
let attempt = 1
|
||
|
const maxAttempts = 3
|
||
|
while (attempt <= maxAttempts) {
|
||
|
const allowAllExitCodes = attempt < maxAttempts
|
||
|
const output = await this.execGit(args, allowAllExitCodes)
|
||
|
if (output.exitCode === 0) {
|
||
|
break
|
||
|
}
|
||
|
|
||
|
const seconds = this.getRandomIntInclusive(1, 10)
|
||
|
core.warning(
|
||
|
`Git fetch failed with exit code ${output.exitCode}. Waiting ${seconds} seconds before trying again.`
|
||
|
)
|
||
|
await this.sleep(seconds * 1000)
|
||
|
attempt++
|
||
|
}
|
||
|
}
|
||
|
|
||
|
getWorkingDirectory(): string {
|
||
|
return this.workingDirectory
|
||
|
}
|
||
|
|
||
|
async init(): Promise<void> {
|
||
|
await this.execGit(['init', this.workingDirectory])
|
||
|
}
|
||
|
|
||
|
async isDetached(): Promise<boolean> {
|
||
|
// Note, this implementation uses "branch --show-current" because
|
||
|
// "rev-parse --symbolic-full-name HEAD" can fail on a new repo
|
||
|
// with nothing checked out.
|
||
|
|
||
|
const output = await this.execGit(['branch', '--show-current'])
|
||
|
return output.stdout.trim() === ''
|
||
|
}
|
||
|
|
||
|
async lfsFetch(ref: string): Promise<void> {
|
||
|
const args = ['lfs', 'fetch', 'origin', ref]
|
||
|
|
||
|
let attempt = 1
|
||
|
const maxAttempts = 3
|
||
|
while (attempt <= maxAttempts) {
|
||
|
const allowAllExitCodes = attempt < maxAttempts
|
||
|
const output = await this.execGit(args, allowAllExitCodes)
|
||
|
if (output.exitCode === 0) {
|
||
|
break
|
||
|
}
|
||
|
|
||
|
const seconds = this.getRandomIntInclusive(1, 10)
|
||
|
core.warning(
|
||
|
`Git lfs fetch failed with exit code ${output.exitCode}. Waiting ${seconds} seconds before trying again.`
|
||
|
)
|
||
|
await this.sleep(seconds * 1000)
|
||
|
attempt++
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async lfsInstall(): Promise<void> {
|
||
|
await this.execGit(['lfs', 'install', '--local'])
|
||
|
}
|
||
|
|
||
|
async log1(): Promise<void> {
|
||
|
await this.execGit(['log', '-1'])
|
||
|
}
|
||
|
|
||
|
async remoteAdd(remoteName: string, remoteUrl: string): Promise<void> {
|
||
|
await this.execGit(['remote', 'add', remoteName, remoteUrl])
|
||
|
}
|
||
|
|
||
|
async tagExists(pattern: string): Promise<boolean> {
|
||
|
const output = await this.execGit(['tag', '--list', pattern])
|
||
|
return !!output.stdout.trim()
|
||
|
}
|
||
|
|
||
|
async tryClean(): Promise<boolean> {
|
||
|
const output = await this.execGit(['clean', '-ffdx'], true)
|
||
|
return output.exitCode === 0
|
||
|
}
|
||
|
|
||
|
async tryConfigUnset(configKey: string): Promise<boolean> {
|
||
|
const output = await this.execGit(
|
||
|
['config', '--unset-all', configKey],
|
||
|
true
|
||
|
)
|
||
|
return output.exitCode === 0
|
||
|
}
|
||
|
|
||
|
async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
|
||
|
const output = await this.execGit(['config', 'gc.auto', '0'], true)
|
||
|
return output.exitCode === 0
|
||
|
}
|
||
|
|
||
|
async tryGetFetchUrl(): Promise<string> {
|
||
|
const output = await this.execGit(
|
||
|
['config', '--get', 'remote.origin.url'],
|
||
|
true
|
||
|
)
|
||
|
|
||
|
if (output.exitCode !== 0) {
|
||
|
return ''
|
||
|
}
|
||
|
|
||
|
const stdout = output.stdout.trim()
|
||
|
if (stdout.includes('\n')) {
|
||
|
return ''
|
||
|
}
|
||
|
|
||
|
return stdout
|
||
|
}
|
||
|
|
||
|
async tryReset(): Promise<boolean> {
|
||
|
const output = await this.execGit(['reset', '--hard', 'HEAD'], true)
|
||
|
return output.exitCode === 0
|
||
|
}
|
||
|
|
||
|
static async createCommandManager(
|
||
|
workingDirectory: string,
|
||
|
lfs: boolean
|
||
|
): Promise<GitCommandManager> {
|
||
|
const result = new GitCommandManager()
|
||
|
await result.initializeCommandManager(workingDirectory, lfs)
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
private async execGit(
|
||
|
args: string[],
|
||
|
allowAllExitCodes = false
|
||
|
): Promise<GitOutput> {
|
||
|
fshelper.directoryExistsSync(this.workingDirectory, true)
|
||
|
|
||
|
const result = new GitOutput()
|
||
|
|
||
|
const env = {}
|
||
|
for (const key of Object.keys(process.env)) {
|
||
|
env[key] = process.env[key]
|
||
|
}
|
||
|
for (const key of Object.keys(this.gitEnv)) {
|
||
|
env[key] = this.gitEnv[key]
|
||
|
}
|
||
|
|
||
|
const stdout: string[] = []
|
||
|
|
||
|
const options = {
|
||
|
cwd: this.workingDirectory,
|
||
|
env,
|
||
|
ignoreReturnCode: allowAllExitCodes,
|
||
|
listeners: {
|
||
|
stdout: (data: Buffer) => {
|
||
|
stdout.push(data.toString())
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options)
|
||
|
result.stdout = stdout.join('')
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
private async initializeCommandManager(
|
||
|
workingDirectory: string,
|
||
|
lfs: boolean
|
||
|
): Promise<void> {
|
||
|
this.workingDirectory = workingDirectory
|
||
|
|
||
|
// Git-lfs will try to pull down assets if any of the local/user/system setting exist.
|
||
|
// If the user didn't enable `LFS` in their pipeline definition, disable LFS fetch/checkout.
|
||
|
this.lfs = lfs
|
||
|
if (!this.lfs) {
|
||
|
this.gitEnv['GIT_LFS_SKIP_SMUDGE'] = '1'
|
||
|
}
|
||
|
|
||
|
this.gitPath = await io.which('git', true)
|
||
|
|
||
|
// Git version
|
||
|
core.debug('Getting git version')
|
||
|
let gitVersion = new GitVersion()
|
||
|
let gitOutput = await this.execGit(['version'])
|
||
|
let stdout = gitOutput.stdout.trim()
|
||
|
if (!stdout.includes('\n')) {
|
||
|
const match = stdout.match(/\d+\.\d+(\.\d+)?/)
|
||
|
if (match) {
|
||
|
gitVersion = new GitVersion(match[0])
|
||
|
}
|
||
|
}
|
||
|
if (!gitVersion.isValid()) {
|
||
|
throw new Error('Unable to determine git version')
|
||
|
}
|
||
|
|
||
|
// Minimum git version
|
||
|
// Note:
|
||
|
// - Auth header not supported before 2.9
|
||
|
// - Wire protocol v2 not supported before 2.18
|
||
|
const minimumGitVersion = new GitVersion('2.18')
|
||
|
if (!gitVersion.checkMinimum(minimumGitVersion)) {
|
||
|
throw new Error(
|
||
|
`Minimum required git version is ${minimumGitVersion}. Your git ('${this.gitPath}') is ${gitVersion}`
|
||
|
)
|
||
|
}
|
||
|
|
||
|
if (this.lfs) {
|
||
|
// Git-lfs version
|
||
|
core.debug('Getting git-lfs version')
|
||
|
let gitLfsVersion = new GitVersion()
|
||
|
const gitLfsPath = await io.which('git-lfs', true)
|
||
|
gitOutput = await this.execGit(['lfs', 'version'])
|
||
|
stdout = gitOutput.stdout.trim()
|
||
|
if (!stdout.includes('\n')) {
|
||
|
const match = stdout.match(/\d+\.\d+(\.\d+)?/)
|
||
|
if (match) {
|
||
|
gitLfsVersion = new GitVersion(match[0])
|
||
|
}
|
||
|
}
|
||
|
if (!gitLfsVersion.isValid()) {
|
||
|
throw new Error('Unable to determine git-lfs version')
|
||
|
}
|
||
|
|
||
|
// Minimum git-lfs version
|
||
|
// Note:
|
||
|
// - Auth header not supported before 2.1
|
||
|
const minimumGitLfsVersion = new GitVersion('2.1')
|
||
|
if (!gitLfsVersion.checkMinimum(minimumGitLfsVersion)) {
|
||
|
throw new Error(
|
||
|
`Minimum required git-lfs version is ${minimumGitLfsVersion}. Your git-lfs ('${gitLfsPath}') is ${gitLfsVersion}`
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Set the user agent
|
||
|
const gitHttpUserAgent = `git/${gitVersion} (github-actions-checkout)`
|
||
|
core.debug(`Set git useragent to: ${gitHttpUserAgent}`)
|
||
|
this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent
|
||
|
}
|
||
|
|
||
|
private getRandomIntInclusive(minimum: number, maximum: number): number {
|
||
|
minimum = Math.floor(minimum)
|
||
|
maximum = Math.floor(maximum)
|
||
|
return Math.floor(Math.random() * (maximum - minimum + 1)) + minimum
|
||
|
}
|
||
|
|
||
|
private async sleep(milliseconds): Promise<void> {
|
||
|
return new Promise(resolve => setTimeout(resolve, milliseconds))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class GitOutput {
|
||
|
stdout = ''
|
||
|
exitCode = 0
|
||
|
}
|