2019-12-03 18:28:59 +03:00
import * as core from '@actions/core'
import * as fs from 'fs'
import * as fsHelper from './fs-helper'
import * as gitCommandManager from './git-command-manager'
2019-12-12 21:16:16 +03:00
import * as githubApiHelper from './github-api-helper'
2019-12-03 18:28:59 +03:00
import * as io from '@actions/io'
import * as path from 'path'
import * as refHelper from './ref-helper'
2019-12-12 21:16:16 +03:00
import * as stateHelper from './state-helper'
2019-12-03 18:28:59 +03: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
2019-12-12 21:49:26 +03:00
authToken : string
persistCredentials : boolean
2019-12-03 18:28:59 +03:00
}
export async function getSource ( settings : ISourceSettings ) : Promise < void > {
2019-12-12 21:16:16 +03:00
// Repository URL
2019-12-03 18:28:59 +03: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-12 21:16:16 +03:00
const git = await getGitCommandManager ( settings )
2019-12-03 18:28:59 +03:00
2019-12-12 21:16:16 +03:00
// Prepare existing directory, otherwise recreate
if ( isExisting ) {
await prepareExistingDirectory (
2019-12-03 18:28:59 +03:00
git ,
settings . repositoryPath ,
repositoryUrl ,
settings . clean
)
}
2019-12-12 21:16:16 +03: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 (
2019-12-12 21:49:26 +03:00
settings . authToken ,
2019-12-12 21:16:16 +03:00
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 18:28:59 +03:00
2019-12-12 21:16:16 +03: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 18:28:59 +03:00
2019-12-12 21:16:16 +03:00
// Remove possible previous extraheader
await removeGitConfig ( git , authConfigKey )
2019-12-12 21:49:26 +03:00
try {
// Config auth token
await configureAuthToken ( git , settings . authToken )
2019-12-12 21:16:16 +03:00
2019-12-12 21:49:26 +03:00
// LFS install
if ( settings . lfs ) {
await git . lfsInstall ( )
}
2019-12-03 18:28:59 +03:00
2019-12-12 21:49:26 +03:00
// Fetch
const refSpec = refHelper . getRefSpec ( settings . ref , settings . commit )
await git . fetch ( settings . fetchDepth , refSpec )
2019-12-03 18:28:59 +03:00
2019-12-12 21:49:26 +03:00
// Checkout info
const checkoutInfo = await refHelper . getCheckoutInfo (
git ,
settings . ref ,
settings . commit
)
2019-12-03 18:28:59 +03:00
2019-12-12 21:49:26 +03: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 18:28:59 +03:00
2019-12-12 21:49:26 +03:00
// Checkout
await git . checkout ( checkoutInfo . ref , checkoutInfo . startPoint )
2019-12-03 18:28:59 +03:00
2019-12-12 21:49:26 +03:00
// Dump some info about the checked out commit
await git . log1 ( )
} finally {
if ( ! settings . persistCredentials ) {
await removeGitConfig ( git , authConfigKey )
}
}
2019-12-12 21:16:16 +03:00
}
2019-12-03 18:28:59 +03: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-12 21:16:16 +03: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 18:28:59 +03:00
git : IGitCommandManager ,
repositoryPath : string ,
repositoryUrl : string ,
clean : boolean
2019-12-12 21:16:16 +03:00
) : Promise < void > {
let remove = false
// Check whether using git or REST API
if ( ! git ) {
remove = true
}
2019-12-03 18:28:59 +03:00
// Fetch URL does not match
2019-12-12 21:16:16 +03:00
else if (
2019-12-03 18:28:59 +03:00
! fsHelper . directoryExistsSync ( path . join ( repositoryPath , '.git' ) ) ||
repositoryUrl !== ( await git . tryGetFetchUrl ( ) )
) {
2019-12-12 21:16:16 +03: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 18:28:59 +03:00
try {
2019-12-12 21:16:16 +03: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 18:28:59 +03:00
} catch ( error ) {
core . warning (
2019-12-12 21:16:16 +03:00
` Unable to prepare the existing repository. The repository will be recreated instead. `
2019-12-03 18:28:59 +03:00
)
2019-12-12 21:16:16 +03:00
remove = true
2019-12-03 18:28:59 +03:00
}
}
2019-12-12 21:16:16 +03: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 18:28:59 +03:00
}
2019-12-12 21:49:26 +03:00
async function configureAuthToken (
git : IGitCommandManager ,
authToken : string
) : Promise < void > {
2019-12-12 22:04:04 +03:00
// Configure a placeholder value. This approach avoids the credential being captured
// by process creation audit events, which are commonly logged. For more information,
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
const placeholder = ` AUTHORIZATION: basic *** `
await git . config ( authConfigKey , placeholder )
// Determine the basic credential value
const basicCredential = Buffer . from (
2019-12-12 21:49:26 +03:00
` x-access-token: ${ authToken } ` ,
'utf8'
) . toString ( 'base64' )
2019-12-12 22:04:04 +03:00
core . setSecret ( basicCredential )
// Replace the value in the config file
const configPath = path . join ( git . getWorkingDirectory ( ) , '.git' , 'config' )
let content = ( await fs . promises . readFile ( configPath ) ) . toString ( )
const placeholderIndex = content . indexOf ( placeholder )
if (
placeholderIndex < 0 ||
placeholderIndex != content . lastIndexOf ( placeholder )
) {
throw new Error ( 'Unable to replace auth placeholder in .git/config' )
}
content = content . replace (
placeholder ,
` AUTHORIZATION: basic ${ basicCredential } `
)
await fs . promises . writeFile ( configPath , content )
2019-12-12 21:49:26 +03:00
}
2019-12-03 18:28:59 +03:00
async function removeGitConfig (
git : IGitCommandManager ,
configKey : string
) : Promise < void > {
if (
( await git . configExists ( configKey ) ) &&
! ( await git . tryConfigUnset ( configKey ) )
) {
// Load the config contents
2019-12-12 21:49:26 +03:00
core . warning ( ` Failed to remove ' ${ configKey } ' from the git config ` )
2019-12-03 18:28:59 +03:00
}
}