2019-12-03 18:28:59 +03:00
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'
2019-12-12 21:16:16 +03:00
import * as retryHelper from './retry-helper'
2019-12-03 18:28:59 +03:00
import { GitVersion } from './git-version'
2019-12-12 21:16:16 +03:00
// Auth header not supported before 2.9
// Wire protocol v2 not supported before 2.18
export const MinimumGitVersion = new GitVersion ( '2.18' )
2019-12-03 18:28:59 +03:00
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 >
2020-03-02 19:33:30 +03:00
setEnvironmentVariable ( name : string , value : string ) : void
2019-12-03 18:28:59 +03:00
tagExists ( pattern : string ) : Promise < boolean >
tryClean ( ) : Promise < boolean >
tryConfigUnset ( configKey : string ) : Promise < boolean >
tryDisableAutomaticGarbageCollection ( ) : Promise < boolean >
tryGetFetchUrl ( ) : Promise < string >
tryReset ( ) : Promise < boolean >
}
2020-03-02 19:33:30 +03:00
export async function createCommandManager (
2019-12-03 18:28:59 +03:00
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 [ ] = [ ]
2020-01-03 18:13:01 +03:00
// Note, this implementation uses "rev-parse --symbolic-full-name" because the output from
2019-12-03 18:28:59 +03:00
// "branch --list" is more difficult when in a detached HEAD state.
2020-01-03 18:13:01 +03:00
// Note, 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.
2019-12-03 18:28:59 +03:00
2020-01-03 18:13:01 +03:00
const args = [ 'rev-parse' , '--symbolic-full-name' ]
2019-12-03 18:28:59 +03:00
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 ) {
2020-01-03 18:13:01 +03:00
if ( branch . startsWith ( 'refs/heads/' ) ) {
branch = branch . substr ( 'refs/heads/' . length )
} else if ( branch . startsWith ( 'refs/remotes/' ) ) {
branch = branch . substr ( 'refs/remotes/' . length )
}
2019-12-03 18:28:59 +03:00
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 > {
2019-12-12 21:49:26 +03:00
await this . execGit ( [ 'config' , '--local' , configKey , configValue ] )
2019-12-03 18:28:59 +03:00
}
async configExists ( configKey : string ) : Promise < boolean > {
const pattern = configKey . replace ( /[^a-zA-Z0-9_]/g , x = > {
return ` \\ ${ x } `
} )
const output = await this . execGit (
2019-12-12 21:49:26 +03:00
[ 'config' , '--local' , '--name-only' , '--get-regexp' , pattern ] ,
2019-12-03 18:28:59 +03:00
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 )
}
2019-12-12 21:16:16 +03:00
const that = this
await retryHelper . execute ( async ( ) = > {
await that . execGit ( args )
} )
2019-12-03 18:28:59 +03:00
}
getWorkingDirectory ( ) : string {
return this . workingDirectory
}
async init ( ) : Promise < void > {
await this . execGit ( [ 'init' , this . workingDirectory ] )
}
async isDetached ( ) : Promise < boolean > {
2020-01-03 18:13:01 +03:00
// 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/' )
2019-12-03 18:28:59 +03:00
}
async lfsFetch ( ref : string ) : Promise < void > {
const args = [ 'lfs' , 'fetch' , 'origin' , ref ]
2019-12-12 21:16:16 +03:00
const that = this
await retryHelper . execute ( async ( ) = > {
await that . execGit ( args )
} )
2019-12-03 18:28:59 +03:00
}
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 ] )
}
2020-03-02 19:33:30 +03:00
setEnvironmentVariable ( name : string , value : string ) : void {
this . gitEnv [ name ] = value
}
2019-12-03 18:28:59 +03:00
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 (
2019-12-12 21:49:26 +03:00
[ 'config' , '--local' , '--unset-all' , configKey ] ,
2019-12-03 18:28:59 +03:00
true
)
return output . exitCode === 0
}
async tryDisableAutomaticGarbageCollection ( ) : Promise < boolean > {
2019-12-12 21:49:26 +03:00
const output = await this . execGit (
[ 'config' , '--local' , 'gc.auto' , '0' ] ,
true
)
2019-12-03 18:28:59 +03:00
return output . exitCode === 0
}
async tryGetFetchUrl ( ) : Promise < string > {
const output = await this . execGit (
2019-12-12 21:49:26 +03:00
[ 'config' , '--local' , '--get' , 'remote.origin.url' ] ,
2019-12-03 18:28:59 +03:00
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
2019-12-12 21:16:16 +03:00
if ( ! gitVersion . checkMinimum ( MinimumGitVersion ) ) {
2019-12-03 18:28:59 +03:00
throw new Error (
2019-12-12 21:16:16 +03:00
` Minimum required git version is ${ MinimumGitVersion } . Your git (' ${ this . gitPath } ') is ${ gitVersion } `
2019-12-03 18:28:59 +03:00
)
}
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
}
}
class GitOutput {
stdout = ''
exitCode = 0
}