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'
2020-05-27 16:54:28 +03:00
import * as refHelper from './ref-helper'
2020-03-05 22:21:59 +03:00
import * as regexpHelper from './regexp-helper'
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 >
2020-03-05 22:21:59 +03:00
config (
configKey : string ,
configValue : string ,
globalConfig? : boolean
) : Promise < void >
configExists ( configKey : string , globalConfig? : boolean ) : Promise < boolean >
2020-05-27 16:54:28 +03:00
fetch ( refSpec : string [ ] , fetchDepth? : number ) : Promise < void >
2020-06-18 17:20:33 +03:00
getDefaultBranch ( repositoryUrl : string ) : Promise < string >
2019-12-03 18:28:59 +03:00
getWorkingDirectory ( ) : string
init ( ) : Promise < void >
isDetached ( ) : Promise < boolean >
lfsFetch ( ref : string ) : Promise < void >
lfsInstall ( ) : Promise < void >
2020-09-23 16:41:47 +03:00
log1 ( format? : string ) : Promise < string >
2019-12-03 18:28:59 +03:00
remoteAdd ( remoteName : string , remoteUrl : string ) : Promise < void >
2020-03-05 22:21:59 +03:00
removeEnvironmentVariable ( name : string ) : void
2020-05-27 16:54:28 +03:00
revParse ( ref : string ) : Promise < string >
2020-03-02 19:33:30 +03:00
setEnvironmentVariable ( name : string , value : string ) : void
2020-05-27 16:54:28 +03:00
shaExists ( sha : string ) : Promise < boolean >
2020-03-05 22:21:59 +03:00
submoduleForeach ( command : string , recursive : boolean ) : Promise < string >
submoduleSync ( recursive : boolean ) : Promise < void >
submoduleUpdate ( fetchDepth : number , recursive : boolean ) : Promise < void >
2019-12-03 18:28:59 +03:00
tagExists ( pattern : string ) : Promise < boolean >
tryClean ( ) : Promise < boolean >
2020-03-05 22:21:59 +03:00
tryConfigUnset ( configKey : string , globalConfig? : boolean ) : Promise < boolean >
2019-12-03 18:28:59 +03:00
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 )
}
2020-03-05 22:21:59 +03:00
async config (
configKey : string ,
configValue : string ,
globalConfig? : boolean
) : Promise < void > {
await this . execGit ( [
'config' ,
globalConfig ? '--global' : '--local' ,
configKey ,
configValue
] )
2019-12-03 18:28:59 +03:00
}
2020-03-05 22:21:59 +03:00
async configExists (
configKey : string ,
globalConfig? : boolean
) : Promise < boolean > {
const pattern = regexpHelper . escape ( configKey )
2019-12-03 18:28:59 +03:00
const output = await this . execGit (
2020-03-05 22:21:59 +03:00
[
'config' ,
globalConfig ? '--global' : '--local' ,
'--name-only' ,
'--get-regexp' ,
pattern
] ,
2019-12-03 18:28:59 +03:00
true
)
return output . exitCode === 0
}
2020-05-27 16:54:28 +03:00
async fetch ( refSpec : string [ ] , fetchDepth? : number ) : Promise < void > {
const args = [ '-c' , 'protocol.version=2' , 'fetch' ]
if ( ! refSpec . some ( x = > x === refHelper . tagsRefSpec ) ) {
args . push ( '--no-tags' )
}
args . push ( '--prune' , '--progress' , '--no-recurse-submodules' )
if ( fetchDepth && fetchDepth > 0 ) {
2019-12-03 18:28:59 +03:00
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
}
2020-06-18 17:20:33 +03:00
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' )
}
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' ] )
}
2020-09-23 16:41:47 +03:00
async log1 ( format? : string ) : Promise < string > {
var args = format ? [ 'log' , '-1' , format ] : [ 'log' , '-1' ]
var silent = format ? false : true
const output = await this . execGit ( args , false , silent )
2020-05-21 18:09:16 +03:00
return output . stdout
2019-12-03 18:28:59 +03:00
}
async remoteAdd ( remoteName : string , remoteUrl : string ) : Promise < void > {
await this . execGit ( [ 'remote' , 'add' , remoteName , remoteUrl ] )
}
2020-03-05 22:21:59 +03:00
removeEnvironmentVariable ( name : string ) : void {
delete this . gitEnv [ name ]
}
2020-05-27 16:54:28 +03:00
/ * *
* 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 .
2020-07-14 16:23:30 +03:00
* @param { string } ref For example : 'refs/heads/main' or '/refs/tags/v1'
2020-05-27 16:54:28 +03:00
* @returns { Promise < string > }
* /
async revParse ( ref : string ) : Promise < string > {
const output = await this . execGit ( [ 'rev-parse' , ref ] )
return output . stdout . trim ( )
}
2020-03-02 19:33:30 +03:00
setEnvironmentVariable ( name : string , value : string ) : void {
this . gitEnv [ name ] = value
}
2020-05-27 16:54:28 +03:00
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
}
2020-03-05 22:21:59 +03:00
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 )
}
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
}
2020-03-05 22:21:59 +03:00
async tryConfigUnset (
configKey : string ,
globalConfig? : boolean
) : Promise < boolean > {
2019-12-03 18:28:59 +03:00
const output = await this . execGit (
2020-03-05 22:21:59 +03:00
[
'config' ,
globalConfig ? '--global' : '--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 [ ] ,
2020-09-23 16:41:47 +03:00
allowAllExitCodes = false ,
silent = false
2019-12-03 18:28:59 +03:00
) : 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 ,
2020-09-23 16:41:47 +03:00
silent ,
2019-12-03 18:28:59 +03:00
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
}