2023-05-25 18:22:24 +00:00
/*global AsyncIterableIterator*/
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import { createReadStream , promises as fs } from 'fs'
2023-06-16 06:17:13 +00:00
import { readFile } from 'fs/promises'
import { flattenDeep } from 'lodash'
2023-05-25 18:22:24 +00:00
import mm from 'micromatch'
import * as path from 'path'
import { createInterface } from 'readline'
2023-06-16 06:17:13 +00:00
import { parseDocument } from 'yaml'
2023-06-14 18:45:32 +00:00
import { ChangedFiles , ChangeTypeEnum } from './changedFiles'
2023-05-25 18:22:24 +00:00
import { Inputs } from './inputs'
const MINIMUM_GIT_VERSION = '2.18.0'
2023-08-03 19:25:35 +00:00
export const isWindows = ( ) : boolean = > {
return process . platform === 'win32'
}
2023-05-25 18:22:24 +00:00
/ * *
2023-08-03 19:25:35 +00:00
* Normalize file path separators to '/' on Linux / macOS and '\\' on Windows
* @param p - file path
2023-05-25 18:22:24 +00:00
* @returns file path with normalized separators
* /
2023-08-03 19:25:35 +00:00
export const normalizeSeparators = ( p : string ) : string = > {
2023-05-25 18:22:24 +00:00
// Windows
2023-08-03 19:25:35 +00:00
if ( isWindows ( ) ) {
2023-05-25 18:22:24 +00:00
// Convert slashes on Windows
p = p . replace ( /\//g , '\\' )
// Remove redundant slashes
const isUnc = /^\\\\+[^\\]/ . test ( p ) // e.g. \\hello
return ( isUnc ? '\\' : '' ) + p . replace ( /\\\\+/g , '\\' ) // preserve leading \\ for UNC
}
// Remove redundant slashes
return p . replace ( /\/\/+/g , '/' )
}
/ * *
* Trims unnecessary trailing slash from file path
2023-08-03 19:25:35 +00:00
* @param p - file path
2023-05-25 18:22:24 +00:00
* @returns file path without unnecessary trailing slash
* /
const safeTrimTrailingSeparator = ( p : string ) : string = > {
// Empty path
if ( ! p ) {
return ''
}
// Normalize separators
p = normalizeSeparators ( p )
// No trailing slash
if ( ! p . endsWith ( path . sep ) ) {
return p
}
// Check '/' on Linux/macOS and '\' on Windows
if ( p === path . sep ) {
return p
}
// On Windows, avoid trimming the drive root, e.g. C:\ or \\hello
2023-08-03 19:25:35 +00:00
if ( isWindows ( ) && /^[A-Z]:\\$/i . test ( p ) ) {
2023-05-25 18:22:24 +00:00
return p
}
// Trim trailing slash
return p . substring ( 0 , p . length - 1 )
}
2023-08-03 19:25:35 +00:00
/ * *
* Gets the dirname of a path , similar to the Node . js path . dirname ( ) function except that this function
* also works for Windows UNC root paths , e . g . \ \ hello \ world
* @param p - file path
* @returns dirname of path
* /
export const getDirname = ( p : string ) : string = > {
2023-05-25 18:22:24 +00:00
// Normalize slashes and trim unnecessary trailing slash
p = safeTrimTrailingSeparator ( p )
// Windows UNC root, e.g. \\hello or \\hello\world
2023-08-03 19:25:35 +00:00
if ( isWindows ( ) && /^\\\\[^\\]+(\\[^\\]+)?$/ . test ( p ) ) {
2023-05-25 18:22:24 +00:00
return p
}
// Get dirname
let result = path . dirname ( p )
// Trim trailing slash for Windows UNC root, e.g. \\hello\world\
2023-08-03 19:25:35 +00:00
if ( isWindows ( ) && /^\\\\[^\\]+\\[^\\]+\\$/ . test ( result ) ) {
2023-05-25 18:22:24 +00:00
result = safeTrimTrailingSeparator ( result )
}
return result
}
2023-08-03 19:25:35 +00:00
/ * *
* Converts the version string to a number
* @param version - version string
* @returns version number
* /
2023-05-25 18:22:24 +00:00
const versionToNumber = ( version : string ) : number = > {
const [ major , minor , patch ] = version . split ( '.' ) . map ( Number )
return major * 1000000 + minor * 1000 + patch
}
2023-08-03 19:25:35 +00:00
/ * *
* Verifies the minimum required git version
* @returns minimum required git version
* @throws Minimum git version requirement is not met
* @throws Git is not installed
* @throws Git is not found in PATH
* @throws An unexpected error occurred
* /
2023-05-25 18:22:24 +00:00
export const verifyMinimumGitVersion = async ( ) : Promise < void > = > {
const { exitCode , stdout , stderr } = await exec . getExecOutput (
'git' ,
[ '--version' ] ,
2023-06-23 17:20:13 +00:00
{ silent : ! core . isDebug ( ) }
2023-05-25 18:22:24 +00:00
)
if ( exitCode !== 0 ) {
throw new Error ( stderr || 'An unexpected error occurred' )
}
const gitVersion = stdout . trim ( )
if ( versionToNumber ( gitVersion ) < versionToNumber ( MINIMUM_GIT_VERSION ) ) {
throw new Error (
` Minimum required git version is ${ MINIMUM_GIT_VERSION } , your version is ${ gitVersion } `
)
}
}
2023-08-03 19:25:35 +00:00
/ * *
* Checks if a path exists
* @param filePath - path to check
* @returns path exists
* /
2023-05-25 18:22:24 +00:00
const exists = async ( filePath : string ) : Promise < boolean > = > {
try {
await fs . access ( filePath )
return true
} catch {
return false
}
}
2023-08-03 19:25:35 +00:00
/ * *
* Generates lines of a file as an async iterable iterator
* @param filePath - path of file to read
* @param excludedFiles - whether to exclude files
* /
2023-05-25 18:22:24 +00:00
async function * lineOfFileGenerator ( {
filePath ,
excludedFiles
} : {
filePath : string
excludedFiles : boolean
} ) : AsyncIterableIterator < string > {
const fileStream = createReadStream ( filePath )
/* istanbul ignore next */
fileStream . on ( 'error' , error = > {
throw error
} )
const rl = createInterface ( {
input : fileStream ,
crlfDelay : Infinity
} )
for await ( const line of rl ) {
if ( ! line . startsWith ( '#' ) && line !== '' ) {
if ( excludedFiles ) {
if ( line . startsWith ( '!' ) ) {
yield line
} else {
yield ` ! ${ line } `
}
} else {
yield line
}
}
}
}
2023-08-03 19:25:35 +00:00
/ * *
* Gets the file patterns from a source file
* @param filePaths - paths of files to read
* @param excludedFiles - whether to exclude the file patterns
* /
2023-05-25 18:22:24 +00:00
const getFilesFromSourceFile = async ( {
filePaths ,
excludedFiles = false
} : {
filePaths : string [ ]
excludedFiles? : boolean
} ) : Promise < string [ ] > = > {
2023-06-16 06:17:13 +00:00
const lines : string [ ] = [ ]
2023-05-25 18:22:24 +00:00
for ( const filePath of filePaths ) {
for await ( const line of lineOfFileGenerator ( { filePath , excludedFiles } ) ) {
lines . push ( line )
}
}
return lines
}
2023-08-03 19:25:35 +00:00
/ * *
* Sets the global git configs
* @param name - name of config
* @param value - value of config
* @throws Couldn ' t update git global config
* /
2023-05-25 18:22:24 +00:00
export const updateGitGlobalConfig = async ( {
name ,
value
} : {
name : string
value : string
} ) : Promise < void > = > {
const { exitCode , stderr } = await exec . getExecOutput (
'git' ,
[ 'config' , '--global' , name , value ] ,
{
ignoreReturnCode : true ,
2023-06-23 17:20:13 +00:00
silent : ! core . isDebug ( )
2023-05-25 18:22:24 +00:00
}
)
/* istanbul ignore if */
if ( exitCode !== 0 || stderr ) {
core . warning ( stderr || ` Couldn't update git global config ${ name } ` )
}
}
2023-08-03 19:25:35 +00:00
/ * *
* Checks if a git repository is shallow
* @param cwd - working directory
* @returns repository is shallow
* /
2023-05-25 18:22:24 +00:00
export const isRepoShallow = async ( { cwd } : { cwd : string } ) : Promise < boolean > = > {
const { stdout } = await exec . getExecOutput (
'git' ,
[ 'rev-parse' , '--is-shallow-repository' ] ,
{
cwd ,
2023-06-23 17:20:13 +00:00
silent : ! core . isDebug ( )
2023-05-25 18:22:24 +00:00
}
)
return stdout . trim ( ) === 'true'
}
2023-08-03 19:25:35 +00:00
/ * *
* Checks if a submodule exists
* @param cwd - working directory
* @returns submodule exists
* /
2023-05-25 18:22:24 +00:00
export const submoduleExists = async ( {
cwd
} : {
cwd : string
} ) : Promise < boolean > = > {
2023-06-25 17:12:13 +00:00
const { stdout , exitCode , stderr } = await exec . getExecOutput (
2023-05-25 21:26:13 +00:00
'git' ,
[ 'submodule' , 'status' ] ,
{
cwd ,
ignoreReturnCode : true ,
2023-06-23 17:20:13 +00:00
silent : ! core . isDebug ( )
2023-05-25 21:26:13 +00:00
}
)
if ( exitCode !== 0 ) {
2023-06-25 17:12:13 +00:00
core . warning ( stderr || "Couldn't list submodules" )
2023-05-25 21:26:13 +00:00
return false
}
2023-05-25 18:22:24 +00:00
return stdout . trim ( ) !== ''
}
2023-08-03 19:25:35 +00:00
/ * *
* Fetches the git repository
* @param args - arguments for fetch command
* @param cwd - working directory
* /
2023-05-25 18:22:24 +00:00
export const gitFetch = async ( {
args ,
cwd
} : {
args : string [ ]
cwd : string
} ) : Promise < number > = > {
const { exitCode } = await exec . getExecOutput ( 'git' , [ 'fetch' , '-q' , . . . args ] , {
cwd ,
ignoreReturnCode : true ,
2023-06-23 17:20:13 +00:00
silent : ! core . isDebug ( )
2023-05-25 18:22:24 +00:00
} )
return exitCode
}
2023-08-03 19:25:35 +00:00
/ * *
* Fetches the git repository submodules
* @param args - arguments for fetch command
* @param cwd - working directory
* /
2023-05-25 18:22:24 +00:00
export const gitFetchSubmodules = async ( {
args ,
cwd
} : {
args : string [ ]
cwd : string
} ) : Promise < void > = > {
const { exitCode , stderr } = await exec . getExecOutput (
'git' ,
[ 'submodule' , 'foreach' , 'git' , 'fetch' , '-q' , . . . args ] ,
{
cwd ,
ignoreReturnCode : true ,
2023-06-23 17:20:13 +00:00
silent : ! core . isDebug ( )
2023-05-25 18:22:24 +00:00
}
)
/* istanbul ignore if */
if ( exitCode !== 0 ) {
core . warning ( stderr || "Couldn't fetch submodules" )
}
}
2023-08-03 19:25:35 +00:00
/ * *
* Retrieves all the submodule paths
* @param cwd - working directory
* /
2023-05-25 18:22:24 +00:00
export const getSubmodulePath = async ( {
cwd
} : {
cwd : string
} ) : Promise < string [ ] > = > {
const { exitCode , stdout , stderr } = await exec . getExecOutput (
'git' ,
[ 'submodule' , 'status' ] ,
{
cwd ,
ignoreReturnCode : true ,
2023-06-23 17:20:13 +00:00
silent : ! core . isDebug ( )
2023-05-25 18:22:24 +00:00
}
)
if ( exitCode !== 0 ) {
core . warning ( stderr || "Couldn't get submodule names" )
return [ ]
}
return stdout
. trim ( )
. split ( '\n' )
2023-08-22 03:11:59 +00:00
. map ( ( line : string ) = > normalizeSeparators ( line . trim ( ) . split ( ' ' ) [ 1 ] ) )
2023-05-25 18:22:24 +00:00
}
2023-08-03 19:25:35 +00:00
/ * *
* Retrieves commit sha of a submodule from a parent commit
* @param cwd - working directory
* @param parentSha1 - parent commit sha
* @param parentSha2 - parent commit sha
* @param submodulePath - path of submodule
* @param diff - diff type between parent commits ( ` .. ` or ` ... ` )
* /
2023-05-25 18:22:24 +00:00
export const gitSubmoduleDiffSHA = async ( {
cwd ,
parentSha1 ,
parentSha2 ,
submodulePath ,
diff
} : {
cwd : string
parentSha1 : string
parentSha2 : string
submodulePath : string
diff : string
} ) : Promise < { previousSha? : string ; currentSha? : string } > = > {
const { stdout } = await exec . getExecOutput (
'git' ,
2023-08-03 19:25:35 +00:00
[ 'diff' , ` ${ parentSha1 } ${ diff } ${ parentSha2 } ` , '--' , submodulePath ] ,
2023-05-25 18:22:24 +00:00
{
cwd ,
2023-06-23 17:20:13 +00:00
silent : ! core . isDebug ( )
2023-05-25 18:22:24 +00:00
}
)
const subprojectCommitPreRegex =
/^(?<preCommit>-)Subproject commit (?<commitHash>.+)$/m
const subprojectCommitCurRegex =
/^(?<curCommit>\+)Subproject commit (?<commitHash>.+)$/m
const previousSha =
subprojectCommitPreRegex . exec ( stdout ) ? . groups ? . commitHash ||
'4b825dc642cb6eb9a060e54bf8d69288fbee4904'
const currentSha = subprojectCommitCurRegex . exec ( stdout ) ? . groups ? . commitHash
if ( currentSha ) {
return { previousSha , currentSha }
}
core . debug (
` No submodule commit found for ${ submodulePath } between ${ parentSha1 } ${ diff } ${ parentSha2 } `
)
return { }
}
export const gitRenamedFiles = async ( {
cwd ,
sha1 ,
sha2 ,
diff ,
oldNewSeparator ,
isSubmodule = false ,
parentDir = ''
} : {
cwd : string
sha1 : string
sha2 : string
diff : string
oldNewSeparator : string
isSubmodule? : boolean
parentDir? : string
} ) : Promise < string [ ] > = > {
const { exitCode , stderr , stdout } = await exec . getExecOutput (
'git' ,
[
'diff' ,
'--name-status' ,
'--ignore-submodules=all' ,
'--diff-filter=R' ,
` ${ sha1 } ${ diff } ${ sha2 } `
] ,
{
cwd ,
ignoreReturnCode : true ,
2023-06-23 17:20:13 +00:00
silent : ! core . isDebug ( )
2023-05-25 18:22:24 +00:00
}
)
if ( exitCode !== 0 ) {
if ( isSubmodule ) {
core . warning (
stderr ||
` Failed to get renamed files for submodule between: ${ sha1 } ${ diff } ${ sha2 } `
)
core . warning (
'Please ensure that submodules are initialized and up to date. See: https://github.com/actions/checkout#usage'
)
} else {
core . error (
stderr || ` Failed to get renamed files between: ${ sha1 } ${ diff } ${ sha2 } `
)
throw new Error ( 'Unable to get renamed files' )
}
return [ ]
}
return stdout
. trim ( )
. split ( '\n' )
. filter ( Boolean )
2023-06-05 03:46:30 +00:00
. map ( ( line : string ) = > {
2023-05-25 18:22:24 +00:00
core . debug ( ` Renamed file: ${ line } ` )
const [ , oldPath , newPath ] = line . split ( '\t' )
if ( isSubmodule ) {
2023-08-22 03:11:59 +00:00
return ` ${ normalizeSeparators (
2023-05-25 18:22:24 +00:00
path . join ( parentDir , oldPath )
2023-08-22 03:11:59 +00:00
) } $ { oldNewSeparator } $ { normalizeSeparators (
path . join ( parentDir , newPath )
) } `
2023-05-25 18:22:24 +00:00
}
2023-08-22 03:11:59 +00:00
return ` ${ normalizeSeparators (
oldPath
) } $ { oldNewSeparator } $ { normalizeSeparators ( newPath ) } `
2023-05-25 18:22:24 +00:00
} )
}
2023-08-03 19:25:35 +00:00
/ * *
* Retrieves all the changed files between two commits
* @param cwd - working directory
* @param sha1 - commit sha
* @param sha2 - commit sha
* @param diff - diff type between parent commits ( ` .. ` or ` ... ` )
* @param isSubmodule - is the repo a submodule
* @param parentDir - parent directory of the submodule
* @param outputRenamedFilesAsDeletedAndAdded - output renamed files as deleted and added
2023-08-30 20:51:36 +00:00
* @param failOnInitialDiffError - fail if the initial diff fails
* @param failOnSubmoduleDiffError - fail if the submodule diff fails
2023-08-03 19:25:35 +00:00
* /
2023-06-14 18:45:32 +00:00
export const getAllChangedFiles = async ( {
2023-05-25 18:22:24 +00:00
cwd ,
sha1 ,
sha2 ,
diff ,
isSubmodule = false ,
2023-06-14 19:59:31 +00:00
parentDir = '' ,
2023-08-30 20:51:36 +00:00
outputRenamedFilesAsDeletedAndAdded = false ,
failOnInitialDiffError = false ,
failOnSubmoduleDiffError = false
2023-05-25 18:22:24 +00:00
} : {
cwd : string
sha1 : string
sha2 : string
diff : string
isSubmodule? : boolean
parentDir? : string
2023-06-14 19:59:31 +00:00
outputRenamedFilesAsDeletedAndAdded? : boolean
2023-08-30 20:51:36 +00:00
failOnInitialDiffError? : boolean
failOnSubmoduleDiffError? : boolean
2023-06-14 18:45:32 +00:00
} ) : Promise < ChangedFiles > = > {
2023-05-25 18:22:24 +00:00
const { exitCode , stdout , stderr } = await exec . getExecOutput (
'git' ,
[
'diff' ,
2023-06-14 18:45:32 +00:00
'--name-status' ,
2023-05-25 18:22:24 +00:00
'--ignore-submodules=all' ,
2023-06-14 18:45:32 +00:00
` --diff-filter=ACDMRTUX ` ,
2023-05-25 18:22:24 +00:00
` ${ sha1 } ${ diff } ${ sha2 } `
] ,
{
cwd ,
ignoreReturnCode : true ,
2023-06-23 17:20:13 +00:00
silent : ! core . isDebug ( )
2023-05-25 18:22:24 +00:00
}
)
2023-06-14 18:45:32 +00:00
const changedFiles : ChangedFiles = {
[ ChangeTypeEnum . Added ] : [ ] ,
[ ChangeTypeEnum . Copied ] : [ ] ,
[ ChangeTypeEnum . Deleted ] : [ ] ,
[ ChangeTypeEnum . Modified ] : [ ] ,
[ ChangeTypeEnum . Renamed ] : [ ] ,
[ ChangeTypeEnum . TypeChanged ] : [ ] ,
[ ChangeTypeEnum . Unmerged ] : [ ] ,
[ ChangeTypeEnum . Unknown ] : [ ]
}
2023-05-25 18:22:24 +00:00
2023-08-30 20:51:36 +00:00
if ( exitCode !== 0 ) {
if ( failOnInitialDiffError && ! isSubmodule ) {
throw new Error (
` Failed to get changed files between: ${ sha1 } ${ diff } ${ sha2 } : ${ stderr } `
)
} else if ( failOnSubmoduleDiffError && isSubmodule ) {
throw new Error (
` Failed to get changed files for submodule between: ${ sha1 } ${ diff } ${ sha2 } : ${ stderr } `
)
}
}
2023-05-25 18:22:24 +00:00
if ( exitCode !== 0 ) {
if ( isSubmodule ) {
core . warning (
stderr ||
` Failed to get changed files for submodule between: ${ sha1 } ${ diff } ${ sha2 } `
)
core . warning (
'Please ensure that submodules are initialized and up to date. See: https://github.com/actions/checkout#usage'
)
} else {
core . warning (
stderr || ` Failed to get changed files between: ${ sha1 } ${ diff } ${ sha2 } `
)
}
2023-06-14 18:45:32 +00:00
return changedFiles
2023-05-25 18:22:24 +00:00
}
2023-06-14 18:45:32 +00:00
const lines = stdout . split ( '\n' ) . filter ( Boolean )
for ( const line of lines ) {
2023-06-14 19:59:31 +00:00
const [ changeType , filePath , newPath = '' ] = line . split ( '\t' )
2023-06-14 18:45:32 +00:00
const normalizedFilePath = isSubmodule
2023-08-22 03:11:59 +00:00
? normalizeSeparators ( path . join ( parentDir , filePath ) )
: normalizeSeparators ( filePath )
2023-06-14 19:59:31 +00:00
const normalizedNewPath = isSubmodule
2023-08-22 03:11:59 +00:00
? normalizeSeparators ( path . join ( parentDir , newPath ) )
: normalizeSeparators ( newPath )
2023-06-14 18:45:32 +00:00
if ( changeType . startsWith ( 'R' ) ) {
2023-06-14 19:59:31 +00:00
if ( outputRenamedFilesAsDeletedAndAdded ) {
changedFiles [ ChangeTypeEnum . Deleted ] . push ( normalizedFilePath )
changedFiles [ ChangeTypeEnum . Added ] . push ( normalizedNewPath )
} else {
2023-06-19 21:34:12 +00:00
changedFiles [ ChangeTypeEnum . Renamed ] . push ( normalizedNewPath )
2023-06-14 19:59:31 +00:00
}
2023-06-14 18:45:32 +00:00
} else {
changedFiles [ changeType as ChangeTypeEnum ] . push ( normalizedFilePath )
}
}
return changedFiles
}
2023-05-25 18:22:24 +00:00
2023-08-03 19:25:35 +00:00
/ * *
* Filters the changed files by the file patterns
* @param allDiffFiles - all the changed files
* @param filePatterns - file patterns to filter by
* /
2023-06-14 18:45:32 +00:00
export const getFilteredChangedFiles = async ( {
allDiffFiles ,
filePatterns
} : {
allDiffFiles : ChangedFiles
filePatterns : string [ ]
} ) : Promise < ChangedFiles > = > {
const changedFiles : ChangedFiles = {
[ ChangeTypeEnum . Added ] : [ ] ,
[ ChangeTypeEnum . Copied ] : [ ] ,
[ ChangeTypeEnum . Deleted ] : [ ] ,
[ ChangeTypeEnum . Modified ] : [ ] ,
[ ChangeTypeEnum . Renamed ] : [ ] ,
[ ChangeTypeEnum . TypeChanged ] : [ ] ,
[ ChangeTypeEnum . Unmerged ] : [ ] ,
[ ChangeTypeEnum . Unknown ] : [ ]
2023-05-25 18:22:24 +00:00
}
2023-06-15 01:45:42 +00:00
const hasFilePatterns = filePatterns . length > 0
2023-08-22 03:11:59 +00:00
const isWin = isWindows ( )
2023-05-25 18:22:24 +00:00
2023-06-14 18:45:32 +00:00
for ( const changeType of Object . keys ( allDiffFiles ) ) {
const files = allDiffFiles [ changeType as ChangeTypeEnum ]
if ( hasFilePatterns ) {
changedFiles [ changeType as ChangeTypeEnum ] = mm ( files , filePatterns , {
dot : true ,
2023-08-22 03:11:59 +00:00
windows : isWin ,
2023-06-14 18:45:32 +00:00
noext : true
2023-08-23 04:06:23 +00:00
} ) . map ( normalizeSeparators )
2023-06-14 18:45:32 +00:00
} else {
changedFiles [ changeType as ChangeTypeEnum ] = files
}
}
return changedFiles
2023-05-25 18:22:24 +00:00
}
export const gitLog = async ( {
args ,
cwd
} : {
args : string [ ]
cwd : string
} ) : Promise < string > = > {
const { stdout } = await exec . getExecOutput ( 'git' , [ 'log' , . . . args ] , {
cwd ,
2023-06-23 17:20:13 +00:00
silent : ! core . isDebug ( )
2023-05-25 18:22:24 +00:00
} )
return stdout . trim ( )
}
export const getHeadSha = async ( { cwd } : { cwd : string } ) : Promise < string > = > {
const { stdout } = await exec . getExecOutput ( 'git' , [ 'rev-parse' , 'HEAD' ] , {
cwd ,
2023-06-23 17:20:13 +00:00
silent : ! core . isDebug ( )
2023-05-25 18:22:24 +00:00
} )
return stdout . trim ( )
}
2023-06-24 15:27:16 +00:00
export const isInsideWorkTree = async ( {
cwd
} : {
cwd : string
} ) : Promise < boolean > = > {
const { stdout } = await exec . getExecOutput (
'git' ,
[ 'rev-parse' , '--is-inside-work-tree' ] ,
{
cwd ,
ignoreReturnCode : true ,
silent : ! core . isDebug ( )
}
)
return stdout . trim ( ) === 'true'
}
2023-06-01 16:03:09 +00:00
export const getRemoteBranchHeadSha = async ( {
2023-05-25 18:22:24 +00:00
cwd ,
2023-06-01 16:03:09 +00:00
branch
2023-05-25 18:22:24 +00:00
} : {
cwd : string
2023-06-01 16:03:09 +00:00
branch : string
2023-05-25 18:22:24 +00:00
} ) : Promise < string > = > {
const { stdout } = await exec . getExecOutput (
'git' ,
2023-06-01 16:34:16 +00:00
[ 'rev-parse' , ` origin/ ${ branch } ` ] ,
2023-05-25 18:22:24 +00:00
{
cwd ,
2023-06-23 17:20:13 +00:00
silent : ! core . isDebug ( )
2023-05-25 18:22:24 +00:00
}
)
2023-06-01 16:03:09 +00:00
return stdout . trim ( )
2023-05-25 18:22:24 +00:00
}
2023-05-25 23:39:26 +00:00
export const getParentSha = async ( { cwd } : { cwd : string } ) : Promise < string > = > {
2023-05-26 16:48:32 +00:00
const { stdout , exitCode } = await exec . getExecOutput (
2023-05-25 23:39:26 +00:00
'git' ,
[ 'rev-list' , '-n' , '1' , 'HEAD^' ] ,
{
cwd ,
2023-05-26 16:48:32 +00:00
ignoreReturnCode : true ,
2023-06-23 17:20:13 +00:00
silent : ! core . isDebug ( )
2023-05-25 23:39:26 +00:00
}
)
2023-05-25 18:22:24 +00:00
2023-05-26 16:48:32 +00:00
if ( exitCode !== 0 ) {
return ''
}
2023-05-25 18:22:24 +00:00
return stdout . trim ( )
}
export const verifyCommitSha = async ( {
sha ,
cwd ,
showAsErrorMessage = true
} : {
sha : string
cwd : string
showAsErrorMessage? : boolean
} ) : Promise < number > = > {
const { exitCode , stderr } = await exec . getExecOutput (
'git' ,
[ 'rev-parse' , '--verify' , ` ${ sha } ^{commit} ` ] ,
{
cwd ,
ignoreReturnCode : true ,
2023-06-23 17:20:13 +00:00
silent : ! core . isDebug ( )
2023-05-25 18:22:24 +00:00
}
)
if ( exitCode !== 0 ) {
if ( showAsErrorMessage ) {
core . error ( ` Unable to locate the commit sha: ${ sha } ` )
core . error (
"Please verify that the commit sha is correct, and increase the 'fetch_depth' input if needed"
)
core . debug ( stderr )
} else {
core . warning ( ` Unable to locate the commit sha: ${ sha } ` )
core . debug ( stderr )
}
}
return exitCode
}
export const getPreviousGitTag = async ( {
cwd
} : {
cwd : string
} ) : Promise < { tag : string ; sha : string } > = > {
const { stdout } = await exec . getExecOutput (
'git' ,
2023-08-22 19:41:54 +00:00
[ 'tag' , '--sort=-creatordate' ] ,
2023-05-25 18:22:24 +00:00
{
cwd ,
2023-06-23 17:20:13 +00:00
silent : ! core . isDebug ( )
2023-05-25 18:22:24 +00:00
}
)
const tags = stdout . trim ( ) . split ( '\n' )
if ( tags . length < 2 ) {
core . warning ( 'No previous tag found' )
return { tag : '' , sha : '' }
}
const previousTag = tags [ 1 ]
const { stdout : stdout2 } = await exec . getExecOutput (
'git' ,
[ 'rev-parse' , previousTag ] ,
{
cwd ,
2023-06-23 17:20:13 +00:00
silent : ! core . isDebug ( )
2023-05-25 18:22:24 +00:00
}
)
const sha = stdout2 . trim ( )
return { tag : previousTag , sha }
}
export const canDiffCommits = async ( {
cwd ,
sha1 ,
sha2 ,
diff
} : {
cwd : string
sha1 : string
sha2 : string
diff : string
} ) : Promise < boolean > = > {
2023-08-05 06:26:02 +00:00
if ( diff === '...' ) {
const mergeBase = await getMergeBase ( cwd , sha1 , sha2 )
if ( ! mergeBase ) {
core . warning ( ` Unable to find merge base between ${ sha1 } and ${ sha2 } ` )
return false
}
const { exitCode , stderr } = await exec . getExecOutput (
'git' ,
[ 'log' , '--format=%H' , ` ${ mergeBase } .. ${ sha2 } ` ] ,
{
cwd ,
ignoreReturnCode : true ,
silent : ! core . isDebug ( )
}
)
if ( exitCode !== 0 ) {
core . warning ( stderr || ` Error checking commit history ` )
return false
}
return true
} else {
const { exitCode , stderr } = await exec . getExecOutput (
'git' ,
[ 'diff' , '--quiet' , sha1 , sha2 ] ,
{
cwd ,
ignoreReturnCode : true ,
silent : ! core . isDebug ( )
}
)
if ( exitCode !== 0 ) {
core . warning ( stderr || ` Error checking commit history ` )
return false
}
return true
}
}
const getMergeBase = async (
cwd : string ,
sha1 : string ,
sha2 : string
) : Promise < string | null > = > {
const { exitCode , stdout } = await exec . getExecOutput (
2023-05-25 18:22:24 +00:00
'git' ,
2023-08-05 06:26:02 +00:00
[ 'merge-base' , sha1 , sha2 ] ,
2023-05-25 18:22:24 +00:00
{
cwd ,
ignoreReturnCode : true ,
2023-06-23 17:20:13 +00:00
silent : ! core . isDebug ( )
2023-05-25 18:22:24 +00:00
}
)
if ( exitCode !== 0 ) {
2023-08-05 06:26:02 +00:00
return null
2023-05-25 18:22:24 +00:00
}
2023-08-05 06:26:02 +00:00
return stdout . trim ( )
2023-05-25 18:22:24 +00:00
}
export const getDirnameMaxDepth = ( {
2023-08-03 19:25:35 +00:00
relativePath ,
2023-05-25 18:22:24 +00:00
dirNamesMaxDepth ,
2023-06-06 12:00:56 +00:00
excludeCurrentDir
2023-05-25 18:22:24 +00:00
} : {
2023-08-03 19:25:35 +00:00
relativePath : string
2023-05-25 18:22:24 +00:00
dirNamesMaxDepth? : number
2023-06-06 12:00:56 +00:00
excludeCurrentDir? : boolean
2023-05-25 18:22:24 +00:00
} ) : string = > {
2023-08-03 19:25:35 +00:00
const pathArr = getDirname ( relativePath ) . split ( path . sep )
2023-05-25 18:22:24 +00:00
const maxDepth = Math . min ( dirNamesMaxDepth || pathArr . length , pathArr . length )
let output = pathArr [ 0 ]
for ( let i = 1 ; i < maxDepth ; i ++ ) {
output = path . join ( output , pathArr [ i ] )
}
2023-06-06 12:00:56 +00:00
if ( excludeCurrentDir && output === '.' ) {
2023-05-25 18:22:24 +00:00
return ''
}
2023-08-22 03:11:59 +00:00
return normalizeSeparators ( output )
2023-05-25 18:22:24 +00:00
}
export const jsonOutput = ( {
value ,
shouldEscape
} : {
2023-09-04 20:03:32 +00:00
value : string | string [ ] | boolean
2023-05-25 18:22:24 +00:00
shouldEscape : boolean
} ) : string = > {
const result = JSON . stringify ( value )
return shouldEscape ? result . replace ( /"/g , '\\"' ) : result
}
2023-08-22 03:11:59 +00:00
export const getDirNamesIncludeFilesPattern = ( {
inputs
} : {
inputs : Inputs
} ) : string [ ] = > {
return inputs . dirNamesIncludeFiles
. split ( inputs . dirNamesIncludeFilesSeparator )
. filter ( Boolean )
}
2023-05-25 18:22:24 +00:00
export const getFilePatterns = async ( {
2023-05-26 14:20:56 +00:00
inputs ,
workingDirectory
2023-05-25 18:22:24 +00:00
} : {
inputs : Inputs
2023-05-26 14:20:56 +00:00
workingDirectory : string
2023-05-25 18:22:24 +00:00
} ) : Promise < string [ ] > = > {
let filePatterns = inputs . files
. split ( inputs . filesSeparator )
2023-08-22 03:11:59 +00:00
. filter ( Boolean )
2023-05-25 18:22:24 +00:00
. join ( '\n' )
if ( inputs . filesFromSourceFile !== '' ) {
const inputFilesFromSourceFile = inputs . filesFromSourceFile
. split ( inputs . filesFromSourceFileSeparator )
. filter ( p = > p !== '' )
2023-05-26 14:20:56 +00:00
. map ( p = > path . join ( workingDirectory , p ) )
2023-05-25 18:22:24 +00:00
core . debug ( ` files from source file: ${ inputFilesFromSourceFile } ` )
const filesFromSourceFiles = (
await getFilesFromSourceFile ( { filePaths : inputFilesFromSourceFile } )
) . join ( '\n' )
core . debug ( ` files from source files patterns: ${ filesFromSourceFiles } ` )
filePatterns = filePatterns . concat ( '\n' , filesFromSourceFiles )
}
if ( inputs . filesIgnore ) {
const filesIgnorePatterns = inputs . filesIgnore
. split ( inputs . filesIgnoreSeparator )
. filter ( p = > p !== '' )
. map ( p = > {
if ( ! p . startsWith ( '!' ) ) {
p = ` ! ${ p } `
}
return p
} )
. join ( '\n' )
core . debug ( ` files ignore patterns: ${ filesIgnorePatterns } ` )
filePatterns = filePatterns . concat ( '\n' , filesIgnorePatterns )
}
if ( inputs . filesIgnoreFromSourceFile ) {
const inputFilesIgnoreFromSourceFile = inputs . filesIgnoreFromSourceFile
. split ( inputs . filesIgnoreFromSourceFileSeparator )
. filter ( p = > p !== '' )
2023-05-26 14:20:56 +00:00
. map ( p = > path . join ( workingDirectory , p ) )
2023-05-25 18:22:24 +00:00
core . debug (
` files ignore from source file: ${ inputFilesIgnoreFromSourceFile } `
)
const filesIgnoreFromSourceFiles = (
await getFilesFromSourceFile ( {
filePaths : inputFilesIgnoreFromSourceFile ,
excludedFiles : true
} )
) . join ( '\n' )
core . debug (
` files ignore from source files patterns: ${ filesIgnoreFromSourceFiles } `
)
filePatterns = filePatterns . concat ( '\n' , filesIgnoreFromSourceFiles )
}
2023-08-03 19:25:35 +00:00
if ( isWindows ( ) ) {
2023-05-25 18:22:24 +00:00
filePatterns = filePatterns . replace ( /\r\n/g , '\n' )
filePatterns = filePatterns . replace ( /\r/g , '\n' )
}
2023-08-23 04:06:23 +00:00
core . debug ( ` Input file patterns: ${ filePatterns } ` )
2023-05-25 18:22:24 +00:00
2023-05-29 16:12:36 +00:00
return filePatterns
. trim ( )
. split ( '\n' )
. filter ( Boolean )
. map ( pattern = > {
if ( pattern . endsWith ( '/' ) ) {
return ` ${ pattern } ** `
} else {
2023-08-23 04:06:23 +00:00
const pathParts = pattern . split ( '/' )
2023-05-29 16:12:36 +00:00
const lastPart = pathParts [ pathParts . length - 1 ]
2023-09-01 19:49:08 +00:00
if ( ! lastPart . includes ( '.' ) ) {
2023-08-23 04:06:23 +00:00
return ` ${ pattern } /** `
2023-05-29 16:12:36 +00:00
} else {
return pattern
}
}
} )
2023-05-25 18:22:24 +00:00
}
2023-06-16 06:17:13 +00:00
// Example YAML input:
// filesYaml: |
// frontend:
// - frontend/**
// backend:
// - backend/**
// test: test/**
// shared: &shared
// - common/**
// lib:
// - *shared
// - lib/**
// Return an Object:
// {
// frontend: ['frontend/**'],
// backend: ['backend/**'],
// test: ['test/**'],
// shared: ['common/**'],
// lib: ['common/**', 'lib/**']
// }
type YamlObject = {
[ key : string ] : string | string [ ] | [ string [ ] , string ]
}
const getYamlFilePatternsFromContents = async ( {
content = '' ,
filePath = '' ,
excludedFiles = false
} : {
content? : string
filePath? : string
excludedFiles? : boolean
} ) : Promise < Record < string , string [ ] > > = > {
const filePatterns : Record < string , string [ ] > = { }
let source = ''
if ( filePath ) {
if ( ! ( await exists ( filePath ) ) ) {
core . error ( ` File does not exist: ${ filePath } ` )
throw new Error ( ` File does not exist: ${ filePath } ` )
}
source = await readFile ( filePath , 'utf8' )
} else {
source = content
}
const doc = parseDocument ( source , { merge : true , schema : 'failsafe' } )
if ( doc . errors . length > 0 ) {
if ( filePath ) {
2023-07-20 18:05:55 +00:00
throw new Error ( ` YAML errors in ${ filePath } : ${ doc . errors } ` )
2023-06-16 06:17:13 +00:00
} else {
2023-07-20 18:05:55 +00:00
throw new Error ( ` YAML errors: ${ doc . errors } ` )
2023-06-16 06:17:13 +00:00
}
}
if ( doc . warnings . length > 0 ) {
if ( filePath ) {
2023-07-20 18:05:55 +00:00
throw new Error ( ` YAML warnings in ${ filePath } : ${ doc . warnings } ` )
2023-06-16 06:17:13 +00:00
} else {
2023-07-20 18:05:55 +00:00
throw new Error ( ` YAML warnings: ${ doc . warnings } ` )
2023-06-16 06:17:13 +00:00
}
}
const yamlObject = doc . toJS ( ) as YamlObject
for ( const key in yamlObject ) {
let value = yamlObject [ key ]
if ( typeof value === 'string' && value . includes ( '\n' ) ) {
value = value . split ( '\n' )
}
if ( typeof value === 'string' ) {
value = value . trim ( )
if ( value ) {
filePatterns [ key ] = [
excludedFiles && ! value . startsWith ( '!' ) ? ` ! ${ value } ` : value
]
}
} else if ( Array . isArray ( value ) ) {
filePatterns [ key ] = flattenDeep ( value )
. filter ( v = > v . trim ( ) !== '' )
. map ( v = > {
if ( excludedFiles && ! v . startsWith ( '!' ) ) {
v = ` ! ${ v } `
}
return v
} )
}
}
return filePatterns
}
export const getYamlFilePatterns = async ( {
inputs ,
workingDirectory
} : {
inputs : Inputs
workingDirectory : string
} ) : Promise < Record < string , string [ ] > > = > {
let filePatterns : Record < string , string [ ] > = { }
if ( inputs . filesYaml ) {
filePatterns = {
. . . ( await getYamlFilePatternsFromContents ( { content : inputs.filesYaml } ) )
}
}
if ( inputs . filesYamlFromSourceFile ) {
const inputFilesYamlFromSourceFile = inputs . filesYamlFromSourceFile
. split ( inputs . filesYamlFromSourceFileSeparator )
. filter ( p = > p !== '' )
. map ( p = > path . join ( workingDirectory , p ) )
core . debug ( ` files yaml from source file: ${ inputFilesYamlFromSourceFile } ` )
for ( const filePath of inputFilesYamlFromSourceFile ) {
const newFilePatterns = await getYamlFilePatternsFromContents ( { filePath } )
for ( const key in newFilePatterns ) {
if ( key in filePatterns ) {
core . warning (
` files_yaml_from_source_file: Duplicated key ${ key } detected in ${ filePath } , the ${ filePatterns [ key ] } will be overwritten by ${ newFilePatterns [ key ] } . `
)
}
}
filePatterns = {
. . . filePatterns ,
. . . newFilePatterns
}
}
}
if ( inputs . filesIgnoreYaml ) {
const newIgnoreFilePatterns = await getYamlFilePatternsFromContents ( {
content : inputs.filesIgnoreYaml ,
excludedFiles : true
} )
for ( const key in newIgnoreFilePatterns ) {
if ( key in filePatterns ) {
core . warning (
` files_ignore_yaml: Duplicated key ${ key } detected, the ${ filePatterns [ key ] } will be overwritten by ${ newIgnoreFilePatterns [ key ] } . `
)
}
}
}
if ( inputs . filesIgnoreYamlFromSourceFile ) {
const inputFilesIgnoreYamlFromSourceFile =
inputs . filesIgnoreYamlFromSourceFile
. split ( inputs . filesIgnoreYamlFromSourceFileSeparator )
. filter ( p = > p !== '' )
. map ( p = > path . join ( workingDirectory , p ) )
core . debug (
` files ignore yaml from source file: ${ inputFilesIgnoreYamlFromSourceFile } `
)
for ( const filePath of inputFilesIgnoreYamlFromSourceFile ) {
const newIgnoreFilePatterns = await getYamlFilePatternsFromContents ( {
filePath ,
excludedFiles : true
} )
for ( const key in newIgnoreFilePatterns ) {
if ( key in filePatterns ) {
core . warning (
` files_ignore_yaml_from_source_file: Duplicated key ${ key } detected in ${ filePath } , the ${ filePatterns [ key ] } will be overwritten by ${ newIgnoreFilePatterns [ key ] } . `
)
}
}
filePatterns = {
. . . filePatterns ,
. . . newIgnoreFilePatterns
}
}
}
return filePatterns
}
2023-07-19 07:50:59 +00:00
export const getRecoverFilePatterns = ( {
inputs
} : {
inputs : Inputs
} ) : string [ ] = > {
let filePatterns : string [ ] = inputs . recoverFiles . split (
inputs . recoverFilesSeparator
)
if ( inputs . recoverFilesIgnore ) {
const ignoreFilePatterns = inputs . recoverFilesIgnore . split (
inputs . recoverFilesSeparator
)
filePatterns = filePatterns . concat (
ignoreFilePatterns . map ( p = > {
if ( p . startsWith ( '!' ) ) {
return p
} else {
return ` ! ${ p } `
}
} )
)
}
core . debug ( ` recover file patterns: ${ filePatterns } ` )
return filePatterns . filter ( Boolean )
}
2023-05-25 18:22:24 +00:00
export const setOutput = async ( {
key ,
value ,
2023-09-04 20:03:32 +00:00
writeOutputFiles ,
outputDir ,
json = false ,
shouldEscape = false
2023-05-25 18:22:24 +00:00
} : {
key : string
2023-09-04 20:03:32 +00:00
value : string | string [ ] | boolean
writeOutputFiles : boolean
outputDir : string
json? : boolean
shouldEscape? : boolean
2023-05-25 18:22:24 +00:00
} ) : Promise < void > = > {
2023-09-04 20:03:32 +00:00
let cleanedValue
if ( json ) {
cleanedValue = jsonOutput ( { value , shouldEscape } )
} else {
cleanedValue = value . toString ( ) . trim ( )
}
2023-05-25 18:22:24 +00:00
core . setOutput ( key , cleanedValue )
2023-09-04 20:03:32 +00:00
if ( writeOutputFiles ) {
const extension = json ? 'json' : 'txt'
2023-05-25 18:22:24 +00:00
const outputFilePath = path . join ( outputDir , ` ${ key } . ${ extension } ` )
if ( ! ( await exists ( outputDir ) ) ) {
await fs . mkdir ( outputDir , { recursive : true } )
}
await fs . writeFile ( outputFilePath , cleanedValue . replace ( /\\"/g , '"' ) )
}
}
2023-06-17 00:57:12 +00:00
const getDeletedFileContents = async ( {
cwd ,
filePath ,
sha
} : {
cwd : string
filePath : string
sha : string
} ) : Promise < string > = > {
const { stdout , exitCode , stderr } = await exec . getExecOutput (
'git' ,
[ 'show' , ` ${ sha } : ${ filePath } ` ] ,
{
cwd ,
2023-06-23 17:20:13 +00:00
silent : ! core . isDebug ( ) ,
2023-06-17 00:57:12 +00:00
ignoreReturnCode : true
}
)
if ( exitCode !== 0 ) {
throw new Error (
` Error getting file content from git history " ${ filePath } ": ${ stderr } `
)
}
return stdout
}
export const recoverDeletedFiles = async ( {
inputs ,
workingDirectory ,
deletedFiles ,
2023-07-19 07:50:59 +00:00
recoverPatterns ,
2023-06-17 00:57:12 +00:00
sha
} : {
inputs : Inputs
workingDirectory : string
deletedFiles : string [ ]
2023-07-19 07:50:59 +00:00
recoverPatterns : string [ ]
2023-06-17 00:57:12 +00:00
sha : string
} ) : Promise < void > = > {
2023-07-19 07:50:59 +00:00
let recoverableDeletedFiles = deletedFiles
core . debug ( ` recoverable deleted files: ${ recoverableDeletedFiles } ` )
if ( recoverPatterns . length > 0 ) {
recoverableDeletedFiles = mm ( deletedFiles , recoverPatterns , {
dot : true ,
2023-08-03 19:25:35 +00:00
windows : isWindows ( ) ,
2023-07-19 07:50:59 +00:00
noext : true
} )
core . debug ( ` filtered recoverable deleted files: ${ recoverableDeletedFiles } ` )
}
2023-06-17 00:57:12 +00:00
2023-07-19 07:50:59 +00:00
for ( const deletedFile of recoverableDeletedFiles ) {
let target = path . join ( workingDirectory , deletedFile )
2023-06-17 00:57:12 +00:00
2023-07-19 07:50:59 +00:00
if ( inputs . recoverDeletedFilesToDestination ) {
target = path . join (
workingDirectory ,
inputs . recoverDeletedFilesToDestination ,
deletedFile
)
}
const deletedFileContents = await getDeletedFileContents ( {
cwd : workingDirectory ,
filePath : deletedFile ,
sha
} )
if ( ! ( await exists ( path . dirname ( target ) ) ) ) {
await fs . mkdir ( path . dirname ( target ) , { recursive : true } )
2023-06-17 00:57:12 +00:00
}
2023-07-19 07:50:59 +00:00
await fs . writeFile ( target , deletedFileContents )
2023-06-17 00:57:12 +00:00
}
}
2023-06-23 17:20:13 +00:00
export const hasLocalGitDirectory = async ( {
workingDirectory
} : {
workingDirectory : string
} ) : Promise < boolean > = > {
2023-07-20 18:05:55 +00:00
return await isInsideWorkTree ( {
2023-06-24 15:27:16 +00:00
cwd : workingDirectory
} )
2023-06-23 17:20:13 +00:00
}