123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635 |
- import * as core from '@actions/core'
- import * as exec from '@actions/exec'
- import * as fs from 'fs'
- import * as fshelper from './fs-helper'
- import * as io from '@actions/io'
- import * as path from 'path'
- import * as refHelper from './ref-helper'
- import * as regexpHelper from './regexp-helper'
- import * as retryHelper from './retry-helper'
- import {GitVersion} from './git-version'
- // Auth header not supported before 2.9
- // Wire protocol v2 not supported before 2.18
- // sparse-checkout not [well-]supported before 2.28 (see https://github.com/actions/checkout/issues/1386)
- export const MinimumGitVersion = new GitVersion('2.18')
- export const MinimumGitSparseCheckoutVersion = new GitVersion('2.28')
- export interface IGitCommandManager {
- branchDelete(remote: boolean, branch: string): Promise<void>
- branchExists(remote: boolean, pattern: string): Promise<boolean>
- branchList(remote: boolean): Promise<string[]>
- disableSparseCheckout(): Promise<void>
- sparseCheckout(sparseCheckout: string[]): Promise<void>
- sparseCheckoutNonConeMode(sparseCheckout: string[]): Promise<void>
- checkout(ref: string, startPoint: string): Promise<void>
- checkoutDetach(): Promise<void>
- config(
- configKey: string,
- configValue: string,
- globalConfig?: boolean,
- add?: boolean
- ): Promise<void>
- configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
- fetch(
- refSpec: string[],
- options: {
- filter?: string
- fetchDepth?: number
- fetchTags?: boolean
- showProgress?: boolean
- }
- ): Promise<void>
- getDefaultBranch(repositoryUrl: string): Promise<string>
- getWorkingDirectory(): string
- init(): Promise<void>
- isDetached(): Promise<boolean>
- lfsFetch(ref: string): Promise<void>
- lfsInstall(): Promise<void>
- log1(format?: string): Promise<string>
- remoteAdd(remoteName: string, remoteUrl: string): Promise<void>
- removeEnvironmentVariable(name: string): void
- revParse(ref: string): Promise<string>
- setEnvironmentVariable(name: string, value: string): void
- shaExists(sha: string): Promise<boolean>
- submoduleForeach(command: string, recursive: boolean): Promise<string>
- submoduleSync(recursive: boolean): Promise<void>
- submoduleUpdate(fetchDepth: number, recursive: boolean): Promise<void>
- submoduleStatus(): Promise<boolean>
- tagExists(pattern: string): Promise<boolean>
- tryClean(): Promise<boolean>
- tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
- tryDisableAutomaticGarbageCollection(): Promise<boolean>
- tryGetFetchUrl(): Promise<string>
- tryReset(): Promise<boolean>
- version(): Promise<GitVersion>
- }
- export async function createCommandManager(
- workingDirectory: string,
- lfs: boolean,
- doSparseCheckout: boolean
- ): Promise<IGitCommandManager> {
- return await GitCommandManager.createCommandManager(
- workingDirectory,
- lfs,
- doSparseCheckout
- )
- }
- 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 doSparseCheckout = false
- private workingDirectory = ''
- private gitVersion: GitVersion = new GitVersion()
- // 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-full-name" because the output from
- // "branch --list" is more difficult when in a detached HEAD state.
- // TODO(https://github.com/actions/checkout/issues/786): this implementation uses
- // "rev-parse --symbolic-full-name" because there is a bug
- // in Git 2.18 that causes "rev-parse --symbolic" to output symbolic full names. When
- // 2.18 is no longer supported, we can switch back to --symbolic.
- const args = ['rev-parse', '--symbolic-full-name']
- if (remote) {
- args.push('--remotes=origin')
- } else {
- args.push('--branches')
- }
- const stderr: string[] = []
- const errline: string[] = []
- const stdout: string[] = []
- const stdline: string[] = []
- const listeners = {
- stderr: (data: Buffer) => {
- stderr.push(data.toString())
- },
- errline: (data: Buffer) => {
- errline.push(data.toString())
- },
- stdout: (data: Buffer) => {
- stdout.push(data.toString())
- },
- stdline: (data: Buffer) => {
- stdline.push(data.toString())
- }
- }
- // Suppress the output in order to avoid flooding annotations with innocuous errors.
- await this.execGit(args, false, true, listeners)
- core.debug(`stderr callback is: ${stderr}`)
- core.debug(`errline callback is: ${errline}`)
- core.debug(`stdout callback is: ${stdout}`)
- core.debug(`stdline callback is: ${stdline}`)
- for (let branch of stdline) {
- branch = branch.trim()
- if (!branch) {
- continue
- }
- if (branch.startsWith('refs/heads/')) {
- branch = branch.substring('refs/heads/'.length)
- } else if (branch.startsWith('refs/remotes/')) {
- branch = branch.substring('refs/remotes/'.length)
- }
- result.push(branch)
- }
- return result
- }
- async disableSparseCheckout(): Promise<void> {
- await this.execGit(['sparse-checkout', 'disable'])
- // Disabling 'sparse-checkout` leaves behind an undesirable side-effect in config (even in a pristine environment).
- await this.tryConfigUnset('extensions.worktreeConfig', false)
- }
- async sparseCheckout(sparseCheckout: string[]): Promise<void> {
- await this.execGit(['sparse-checkout', 'set', ...sparseCheckout])
- }
- async sparseCheckoutNonConeMode(sparseCheckout: string[]): Promise<void> {
- await this.execGit(['config', 'core.sparseCheckout', 'true'])
- const output = await this.execGit([
- 'rev-parse',
- '--git-path',
- 'info/sparse-checkout'
- ])
- const sparseCheckoutPath = path.join(
- this.workingDirectory,
- output.stdout.trimRight()
- )
- await fs.promises.appendFile(
- sparseCheckoutPath,
- `\n${sparseCheckout.join('\n')}\n`
- )
- }
- 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,
- globalConfig?: boolean,
- add?: boolean
- ): Promise<void> {
- const args: string[] = ['config', globalConfig ? '--global' : '--local']
- if (add) {
- args.push('--add')
- }
- args.push(...[configKey, configValue])
- await this.execGit(args)
- }
- async configExists(
- configKey: string,
- globalConfig?: boolean
- ): Promise<boolean> {
- const pattern = regexpHelper.escape(configKey)
- const output = await this.execGit(
- [
- 'config',
- globalConfig ? '--global' : '--local',
- '--name-only',
- '--get-regexp',
- pattern
- ],
- true
- )
- return output.exitCode === 0
- }
- async fetch(
- refSpec: string[],
- options: {
- filter?: string
- fetchDepth?: number
- fetchTags?: boolean
- showProgress?: boolean
- }
- ): Promise<void> {
- const args = ['-c', 'protocol.version=2', 'fetch']
- if (!refSpec.some(x => x === refHelper.tagsRefSpec) && !options.fetchTags) {
- args.push('--no-tags')
- }
- args.push('--prune', '--no-recurse-submodules')
- if (options.showProgress) {
- args.push('--progress')
- }
- if (options.filter) {
- args.push(`--filter=${options.filter}`)
- }
- if (options.fetchDepth && options.fetchDepth > 0) {
- args.push(`--depth=${options.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)
- }
- const that = this
- await retryHelper.execute(async () => {
- await that.execGit(args)
- })
- }
- async getDefaultBranch(repositoryUrl: string): Promise<string> {
- let output: GitOutput | undefined
- await retryHelper.execute(async () => {
- output = await this.execGit([
- 'ls-remote',
- '--quiet',
- '--exit-code',
- '--symref',
- repositoryUrl,
- 'HEAD'
- ])
- })
- if (output) {
- // Satisfy compiler, will always be set
- for (let line of output.stdout.trim().split('\n')) {
- line = line.trim()
- if (line.startsWith('ref:') || line.endsWith('HEAD')) {
- return line
- .substr('ref:'.length, line.length - 'ref:'.length - 'HEAD'.length)
- .trim()
- }
- }
- }
- throw new Error('Unexpected output when retrieving default branch')
- }
- getWorkingDirectory(): string {
- return this.workingDirectory
- }
- async init(): Promise<void> {
- await this.execGit(['init', this.workingDirectory])
- }
- async isDetached(): Promise<boolean> {
- // Note, "branch --show-current" would be simpler but isn't available until Git 2.22
- const output = await this.execGit(
- ['rev-parse', '--symbolic-full-name', '--verify', '--quiet', 'HEAD'],
- true
- )
- return !output.stdout.trim().startsWith('refs/heads/')
- }
- async lfsFetch(ref: string): Promise<void> {
- const args = ['lfs', 'fetch', 'origin', ref]
- const that = this
- await retryHelper.execute(async () => {
- await that.execGit(args)
- })
- }
- async lfsInstall(): Promise<void> {
- await this.execGit(['lfs', 'install', '--local'])
- }
- async log1(format?: string): Promise<string> {
- const args = format ? ['log', '-1', format] : ['log', '-1']
- const silent = format ? false : true
- const output = await this.execGit(args, false, silent)
- return output.stdout
- }
- async remoteAdd(remoteName: string, remoteUrl: string): Promise<void> {
- await this.execGit(['remote', 'add', remoteName, remoteUrl])
- }
- removeEnvironmentVariable(name: string): void {
- delete this.gitEnv[name]
- }
- /**
- * Resolves a ref to a SHA. For a branch or lightweight tag, the commit SHA is returned.
- * For an annotated tag, the tag SHA is returned.
- * @param {string} ref For example: 'refs/heads/main' or '/refs/tags/v1'
- * @returns {Promise<string>}
- */
- async revParse(ref: string): Promise<string> {
- const output = await this.execGit(['rev-parse', ref])
- return output.stdout.trim()
- }
- setEnvironmentVariable(name: string, value: string): void {
- this.gitEnv[name] = value
- }
- async shaExists(sha: string): Promise<boolean> {
- const args = ['rev-parse', '--verify', '--quiet', `${sha}^{object}`]
- const output = await this.execGit(args, true)
- return output.exitCode === 0
- }
- 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 submoduleStatus(): Promise<boolean> {
- const output = await this.execGit(['submodule', 'status'], true)
- core.debug(output.stdout)
- return output.exitCode === 0
- }
- 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,
- globalConfig?: boolean
- ): Promise<boolean> {
- const output = await this.execGit(
- [
- 'config',
- globalConfig ? '--global' : '--local',
- '--unset-all',
- configKey
- ],
- true
- )
- return output.exitCode === 0
- }
- async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
- const output = await this.execGit(
- ['config', '--local', 'gc.auto', '0'],
- true
- )
- return output.exitCode === 0
- }
- async tryGetFetchUrl(): Promise<string> {
- const output = await this.execGit(
- ['config', '--local', '--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
- }
- async version(): Promise<GitVersion> {
- return this.gitVersion
- }
- static async createCommandManager(
- workingDirectory: string,
- lfs: boolean,
- doSparseCheckout: boolean
- ): Promise<GitCommandManager> {
- const result = new GitCommandManager()
- await result.initializeCommandManager(
- workingDirectory,
- lfs,
- doSparseCheckout
- )
- return result
- }
- private async execGit(
- args: string[],
- allowAllExitCodes = false,
- silent = false,
- customListeners = {}
- ): 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 defaultListener = {
- stdout: (data: Buffer) => {
- stdout.push(data.toString())
- }
- }
- const mergedListeners = {...defaultListener, ...customListeners}
- const stdout: string[] = []
- const options = {
- cwd: this.workingDirectory,
- env,
- silent,
- ignoreReturnCode: allowAllExitCodes,
- listeners: mergedListeners
- }
- result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options)
- result.stdout = stdout.join('')
- core.debug(result.exitCode.toString())
- core.debug(result.stdout)
- return result
- }
- private async initializeCommandManager(
- workingDirectory: string,
- lfs: boolean,
- doSparseCheckout: 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')
- this.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) {
- this.gitVersion = new GitVersion(match[0])
- }
- }
- if (!this.gitVersion.isValid()) {
- throw new Error('Unable to determine git version')
- }
- // Minimum git version
- if (!this.gitVersion.checkMinimum(MinimumGitVersion)) {
- throw new Error(
- `Minimum required git version is ${MinimumGitVersion}. Your git ('${this.gitPath}') is ${this.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}`
- )
- }
- }
- this.doSparseCheckout = doSparseCheckout
- if (this.doSparseCheckout) {
- if (!this.gitVersion.checkMinimum(MinimumGitSparseCheckoutVersion)) {
- throw new Error(
- `Minimum Git version required for sparse checkout is ${MinimumGitSparseCheckoutVersion}. Your git ('${this.gitPath}') is ${this.gitVersion}`
- )
- }
- }
- // Set the user agent
- const gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`
- core.debug(`Set git useragent to: ${gitHttpUserAgent}`)
- this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent
- }
- }
- class GitOutput {
- stdout = ''
- exitCode = 0
- }
|