2019-12-03 23:28:59 +08:00
import * as core from '@actions/core'
import * as coreCommand from '@actions/core/lib/command'
import * as fs from 'fs'
import * as fsHelper from './fs-helper'
import * as gitCommandManager from './git-command-manager'
2019-12-13 02:16:16 +08:00
import * as githubApiHelper from './github-api-helper'
2019-12-03 23:28:59 +08:00
import * as io from '@actions/io'
import * as path from 'path'
import * as refHelper from './ref-helper'
2019-12-13 02:16:16 +08:00
import * as stateHelper from './state-helper'
2019-12-03 23:28:59 +08:00
import { IGitCommandManager } from './git-command-manager'
const authConfigKey = ` http.https://github.com/.extraheader `
export interface ISourceSettings {
repositoryPath : string
repositoryOwner : string
repositoryName : string
ref : string
commit : string
clean : boolean
fetchDepth : number
lfs : boolean
accessToken : string
}
export async function getSource ( settings : ISourceSettings ) : Promise < void > {
2019-12-13 02:16:16 +08:00
// Repository URL
2019-12-03 23:28:59 +08:00
core . info (
` Syncing repository: ${ settings . repositoryOwner } / ${ settings . repositoryName } `
)
const repositoryUrl = ` https://github.com/ ${ encodeURIComponent (
settings . repositoryOwner
) } / $ { encodeURIComponent ( settings . repositoryName ) } `
// Remove conflicting file path
if ( fsHelper . fileExistsSync ( settings . repositoryPath ) ) {
await io . rmRF ( settings . repositoryPath )
}
// Create directory
let isExisting = true
if ( ! fsHelper . directoryExistsSync ( settings . repositoryPath ) ) {
isExisting = false
await io . mkdirP ( settings . repositoryPath )
}
// Git command manager
2019-12-13 02:16:16 +08:00
const git = await getGitCommandManager ( settings )
2019-12-03 23:28:59 +08:00
2019-12-13 02:16:16 +08:00
// Prepare existing directory, otherwise recreate
if ( isExisting ) {
await prepareExistingDirectory (
2019-12-03 23:28:59 +08:00
git ,
settings . repositoryPath ,
repositoryUrl ,
settings . clean
)
}
2019-12-13 02:16:16 +08:00
if ( ! git ) {
// Downloading using REST API
core . info ( ` The repository will be downloaded using the GitHub REST API ` )
core . info (
` To create a local Git repository instead, add Git ${ gitCommandManager . MinimumGitVersion } or higher to the PATH `
)
await githubApiHelper . downloadRepository (
settings . accessToken ,
settings . repositoryOwner ,
settings . repositoryName ,
settings . ref ,
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 )
}
2019-12-03 23:28:59 +08:00
2019-12-13 02:16:16 +08:00
// 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. `
)
}
2019-12-03 23:28:59 +08:00
2019-12-13 02:16:16 +08:00
// Remove possible previous extraheader
await removeGitConfig ( git , authConfigKey )
// Add extraheader (auth)
const base64Credentials = Buffer . from (
` x-access-token: ${ settings . accessToken } ` ,
'utf8'
) . toString ( 'base64' )
core . setSecret ( base64Credentials )
const authConfigValue = ` AUTHORIZATION: basic ${ base64Credentials } `
await git . config ( authConfigKey , authConfigValue )
// LFS install
if ( settings . lfs ) {
await git . lfsInstall ( )
}
2019-12-03 23:28:59 +08:00
2019-12-13 02:16:16 +08:00
// Fetch
const refSpec = refHelper . getRefSpec ( settings . ref , settings . commit )
await git . fetch ( settings . fetchDepth , refSpec )
2019-12-03 23:28:59 +08:00
2019-12-13 02:16:16 +08:00
// Checkout info
const checkoutInfo = await refHelper . getCheckoutInfo (
git ,
settings . ref ,
settings . commit
)
2019-12-03 23:28:59 +08:00
2019-12-13 02:16:16 +08:00
// 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 )
}
2019-12-03 23:28:59 +08:00
2019-12-13 02:16:16 +08:00
// Checkout
await git . checkout ( checkoutInfo . ref , checkoutInfo . startPoint )
2019-12-03 23:28:59 +08:00
2019-12-13 02:16:16 +08:00
// Dump some info about the checked out commit
await git . log1 ( )
}
2019-12-03 23:28:59 +08:00
}
export async function cleanup ( repositoryPath : string ) : Promise < void > {
// Repo exists?
if ( ! fsHelper . fileExistsSync ( path . join ( repositoryPath , '.git' , 'config' ) ) ) {
return
}
fsHelper . directoryExistsSync ( repositoryPath , true )
// Remove the config key
const git = await gitCommandManager . CreateCommandManager (
repositoryPath ,
false
)
await removeGitConfig ( git , authConfigKey )
}
2019-12-13 02:16:16 +08:00
async function getGitCommandManager (
settings : ISourceSettings
) : Promise < IGitCommandManager > {
core . info ( ` Working directory is ' ${ settings . repositoryPath } ' ` )
let git = ( null as unknown ) as IGitCommandManager
try {
return await gitCommandManager . CreateCommandManager (
settings . repositoryPath ,
settings . lfs
)
} catch ( err ) {
// Git is required for LFS
if ( settings . lfs ) {
throw err
}
// Otherwise fallback to REST API
return ( null as unknown ) as IGitCommandManager
}
}
async function prepareExistingDirectory (
2019-12-03 23:28:59 +08:00
git : IGitCommandManager ,
repositoryPath : string ,
repositoryUrl : string ,
clean : boolean
2019-12-13 02:16:16 +08:00
) : Promise < void > {
let remove = false
// Check whether using git or REST API
if ( ! git ) {
remove = true
}
2019-12-03 23:28:59 +08:00
// Fetch URL does not match
2019-12-13 02:16:16 +08:00
else if (
2019-12-03 23:28:59 +08:00
! fsHelper . directoryExistsSync ( path . join ( repositoryPath , '.git' ) ) ||
repositoryUrl !== ( await git . tryGetFetchUrl ( ) )
) {
2019-12-13 02:16:16 +08:00
remove = true
} else {
// Delete any index.lock and shallow.lock left by a previously canceled run or crashed git process
const lockPaths = [
path . join ( repositoryPath , '.git' , 'index.lock' ) ,
path . join ( repositoryPath , '.git' , 'shallow.lock' )
]
for ( const lockPath of lockPaths ) {
try {
await io . rmRF ( lockPath )
} catch ( error ) {
core . debug ( ` Unable to delete ' ${ lockPath } '. ${ error . message } ` )
}
}
2019-12-03 23:28:59 +08:00
try {
2019-12-13 02:16:16 +08:00
// Checkout detached HEAD
if ( ! ( await git . isDetached ( ) ) ) {
await git . checkoutDetach ( )
}
// Remove all refs/heads/*
let branches = await git . branchList ( false )
for ( const branch of branches ) {
await git . branchDelete ( false , branch )
}
// Remove all refs/remotes/origin/* to avoid conflicts
branches = await git . branchList ( true )
for ( const branch of branches ) {
await git . branchDelete ( true , branch )
}
// Clean
if ( clean ) {
if ( ! ( await git . tryClean ( ) ) ) {
core . debug (
` The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For futher investigation, manually run 'git clean -ffdx' on the directory ' ${ repositoryPath } '. `
)
remove = true
} else if ( ! ( await git . tryReset ( ) ) ) {
remove = true
}
if ( remove ) {
core . warning (
` Unable to clean or reset the repository. The repository will be recreated instead. `
)
}
}
2019-12-03 23:28:59 +08:00
} catch ( error ) {
core . warning (
2019-12-13 02:16:16 +08:00
` Unable to prepare the existing repository. The repository will be recreated instead. `
2019-12-03 23:28:59 +08:00
)
2019-12-13 02:16:16 +08:00
remove = true
2019-12-03 23:28:59 +08:00
}
}
2019-12-13 02:16:16 +08:00
if ( remove ) {
// Delete the contents of the directory. Don't delete the directory itself
// since it might be the current working directory.
core . info ( ` Deleting the contents of ' ${ repositoryPath } ' ` )
for ( const file of await fs . promises . readdir ( repositoryPath ) ) {
await io . rmRF ( path . join ( repositoryPath , file ) )
}
}
2019-12-03 23:28:59 +08:00
}
async function removeGitConfig (
git : IGitCommandManager ,
configKey : string
) : Promise < void > {
if (
( await git . configExists ( configKey ) ) &&
! ( await git . tryConfigUnset ( configKey ) )
) {
// Load the config contents
core . warning (
` Failed to remove ' ${ configKey } ' from the git config. Attempting to remove the config value by editing the file directly. `
)
const configPath = path . join ( git . getWorkingDirectory ( ) , '.git' , 'config' )
fsHelper . fileExistsSync ( configPath )
let contents = fs . readFileSync ( configPath ) . toString ( ) || ''
// Filter - only includes lines that do not contain the config key
const upperConfigKey = configKey . toUpperCase ( )
const split = contents
. split ( '\n' )
. filter ( x = > ! x . toUpperCase ( ) . includes ( upperConfigKey ) )
contents = split . join ( '\n' )
// Rewrite the config file
fs . writeFileSync ( configPath , contents )
}
}