/*global AsyncIterableIterator*/ import * as core from '@actions/core' import * as exec from '@actions/exec' import {MatchKind} from '@actions/glob/lib/internal-match-kind' import {Pattern} from '@actions/glob/lib/internal-pattern' import * as patternHelper from '@actions/glob/lib/internal-pattern-helper' import * as path from 'path' import {createReadStream, promises as fs} from 'fs' import {createInterface} from 'readline' import {Inputs} from './inputs' const MINIMUM_GIT_VERSION = '2.18.0' const versionToNumber = (version: string): number => { const [major, minor, patch] = version.split('.').map(Number) return major * 1000000 + minor * 1000 + patch } export const verifyMinimumGitVersion = async (): Promise => { const {exitCode, stdout, stderr} = await exec.getExecOutput('git', [ '--version' ]) 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}` ) } } export function escapeString(value: string): string { return value.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') } export async function exists(filePath: string): Promise { try { await fs.access(filePath) return true } catch { return false } } export async function getPatterns(filePatterns: string): Promise { const IS_WINDOWS: boolean = process.platform === 'win32' const patterns = [] if (IS_WINDOWS) { filePatterns = filePatterns.replace(/\r\n/g, '\n') filePatterns = filePatterns.replace(/\r/g, '\n') } const lines = filePatterns.split('\n').map(filePattern => filePattern.trim()) for (let line of lines) { // Empty or comment if (!(!line || line.startsWith('#'))) { line = IS_WINDOWS ? line.replace(/\\/g, '/') : line const pattern = new Pattern(line) // @ts-ignore pattern.minimatch.options.nobrace = false // @ts-ignore pattern.minimatch.make() patterns.push(pattern) if ( pattern.trailingSeparator || pattern.segments[pattern.segments.length - 1] !== '**' ) { patterns.push( new Pattern(pattern.negate, true, pattern.segments.concat('**')) ) } } } return patterns } async function* lineOfFileGenerator({ filePath, excludedFiles }: { filePath: string excludedFiles: boolean }): AsyncIterableIterator { 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 } } } } export async function getFilesFromSourceFile({ filePaths, excludedFiles = false }: { filePaths: string[] excludedFiles?: boolean }): Promise { const lines = [] for (const filePath of filePaths) { for await (const line of lineOfFileGenerator({filePath, excludedFiles})) { lines.push(line) } } return lines } export const updateGitGlobalConfig = async ({ name, value }: { name: string value: string }): Promise => { const {exitCode, stderr} = await exec.getExecOutput( 'git', ['config', '--global', name, value], { ignoreReturnCode: true } ) /* istanbul ignore if */ if (exitCode !== 0 || stderr) { core.warning(stderr || `Couldn't update git global config ${name}`) } } export const isRepoShallow = async ({cwd}: {cwd: string}): Promise => { const {exitCode, stdout, stderr} = await exec.getExecOutput( 'git', ['rev-parse', '--is-shallow-repository'], {cwd} ) if (exitCode !== 0) { throw new Error(stderr || 'An unexpected error occurred') } return stdout.trim() === 'true' } export const submoduleExists = async ({ cwd }: { cwd: string }): Promise => { const {exitCode, stdout, stderr} = await exec.getExecOutput( 'git', ['submodule', 'status'], {cwd} ) if (exitCode !== 0) { throw new Error(stderr || 'An unexpected error occurred') } return stdout.trim() !== '' } export const gitFetch = async ({ args, cwd }: { args: string[] cwd: string }): Promise => { const {exitCode, stderr} = await exec.getExecOutput( 'git', ['fetch', ...args], {cwd} ) /* istanbul ignore if */ if (exitCode !== 0 || stderr) { core.warning(stderr || "Couldn't fetch repository") } return exitCode } export const gitFetchSubmodules = async ({ args, cwd }: { args: string[] cwd: string }): Promise => { const {exitCode, stderr} = await exec.getExecOutput( 'git', ['submodule', 'foreach', 'git', 'fetch', ...args], {cwd} ) /* istanbul ignore if */ if (exitCode !== 0 || stderr) { core.warning(stderr || "Couldn't fetch submodules") } } export const getSubmodulePath = async ({ cwd }: { cwd: string }): Promise => { const {exitCode, stdout, stderr} = await exec.getExecOutput( 'git', ['submodule', 'status'], {cwd} ) if (exitCode !== 0) { core.warning(stderr || "Couldn't get submodule names") return [] } return stdout .split('\n') .filter(Boolean) .map(line => line.split(' ')[1]) } 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 {exitCode, stdout, stderr} = await exec.getExecOutput( 'git', ['diff', parentSha1, parentSha2, '--', submodulePath], {cwd} ) if (exitCode !== 0) { throw new Error(stderr || 'An unexpected error occurred') } const subprojectCommitPreRegex = /^(?-)Subproject commit (?.+)$/m const subprojectCommitCurRegex = /^(?\+)Subproject commit (?.+)$/m const previousSha = subprojectCommitPreRegex.exec(stdout)?.groups?.commitHash || '4b825dc642cb6eb9a060e54bf8d69288fbee4904' const currentSha = subprojectCommitCurRegex.exec(stdout)?.groups?.commitHash if (currentSha) { return {previousSha, currentSha} } core.warning( `No submodule commit found for ${submodulePath} between ${parentSha1}${diff}${parentSha2}` ) return {} } export const gitRenamedFiles = async ({ cwd, sha1, sha2, diff, oldNewSeparator, isSubmodule = false }: { cwd: string sha1: string sha2: string diff: string oldNewSeparator: string isSubmodule?: boolean }): Promise => { const {exitCode, stderr, stdout} = await exec.getExecOutput( 'git', [ 'diff', '--name-status', '--ignore-submodules=all', '--diff-filter=R', `${sha1}${diff}${sha2}` ], {cwd} ) 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') .map(line => { const [, oldPath, newPath] = line.split('\t') return `${escapeString(oldPath)}${oldNewSeparator}${escapeString( newPath )}` }) } export const gitDiff = async ({ cwd, sha1, sha2, diff, diffFilter, filePatterns = [], isSubmodule = false }: { cwd: string sha1: string sha2: string diffFilter: string diff: string filePatterns?: Pattern[] isSubmodule?: boolean }): Promise => { const {exitCode, stdout, stderr} = await exec.getExecOutput( 'git', [ 'diff', '--name-only', '--ignore-submodules=all', `--diff-filter=${diffFilter}`, `${sha1}${diff}${sha2}` ], {cwd} ) 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.error( stderr || `Failed to get changed files between: ${sha1}${diff}${sha2}` ) throw new Error('Unable to get changed files') } return [] } return stdout .split('\n') .filter(filePath => { if (filePatterns.length === 0) { return filePath !== '' } const match = patternHelper.match(filePatterns, filePath) return filePath !== '' && match === MatchKind.All }) .map(p => escapeString(p)) } export const gitLog = async ({ args, cwd }: { args: string[] cwd: string }): Promise => { const {exitCode, stdout, stderr} = await exec.getExecOutput( 'git', ['log', ...args], {cwd} ) if (exitCode !== 0) { throw new Error(stderr || 'An unexpected error occurred') } return stdout.trim() } export const getHeadSha = async ({cwd}: {cwd: string}): Promise => { const {exitCode, stdout, stderr} = await exec.getExecOutput( 'git', ['rev-parse', 'HEAD'], {cwd} ) if (exitCode !== 0) { throw new Error(stderr || 'Unable to get HEAD sha') } return stdout.trim() } export const getParentHeadSha = async ({ cwd }: { cwd: string }): Promise => { const {exitCode, stdout, stderr} = await exec.getExecOutput( 'git', ['rev-parse', 'HEAD^'], {cwd} ) if (exitCode !== 0) { throw new Error(stderr || 'Unable to get HEAD^ sha') } return stdout.trim() } export const getBranchHeadSha = async ({ branch, cwd }: { branch: string cwd: string }): Promise => { const {exitCode, stdout, stderr} = await exec.getExecOutput( 'git', ['rev-parse', branch], {cwd} ) if (exitCode !== 0) { throw new Error(stderr || `Unable to get ${branch} head sha`) } return stdout.trim() } export const verifyCommitSha = async ({ sha, cwd, showAsErrorMessage = true }: { sha: string cwd: string showAsErrorMessage?: boolean }): Promise => { const {exitCode, stderr} = await exec.getExecOutput( 'git', ['rev-parse', '--quiet', '--verify', `${sha}^{commit}`], {cwd} ) 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 {exitCode, stdout, stderr} = await exec.getExecOutput( 'git', ['tag', '--sort=-version:refname'], {cwd} ) if (exitCode !== 0) { throw new Error(stderr || 'Unable to get previous tag') } const tags = stdout.trim().split('\n') if (tags.length < 2) { core.warning('No previous tag found') return {tag: '', sha: ''} } const previousTag = tags[1] const { exitCode: exitCode2, stdout: stdout2, stderr: stderr2 } = await exec.getExecOutput('git', ['rev-parse', previousTag], {cwd}) if (exitCode2 !== 0) { throw new Error(stderr2 || 'Unable to get previous tag') } 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 => { const {exitCode, stderr} = await exec.getExecOutput( 'git', ['diff', '--name-only', '--ignore-submodules=all', `${sha1}${diff}${sha2}`], {cwd} ) if (exitCode !== 0) { core.warning(stderr || `Unable find merge base between ${sha1} and ${sha2}`) return false } return true } export const getDirnameMaxDepth = ({ pathStr, dirNamesMaxDepth, excludeRoot }: { pathStr: string dirNamesMaxDepth?: number excludeRoot?: boolean }): string => { const pathArr = pathStr.split(path.sep) 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]) } if (excludeRoot && output === '.') { return '' } return output } export const jsonOutput = ({ value, rawFormat }: { value: string | string[] rawFormat: boolean }): string => { if (rawFormat) { return JSON.stringify(value) } else { return JSON.stringify(value, null, 2) } } export const getFilePatterns = async ({ inputs }: { inputs: Inputs }): Promise => { let filesPatterns: string = inputs.files .split(inputs.filesSeparator) .filter(p => p !== '') .join('\n') core.debug(`files patterns: ${filesPatterns}`) if (inputs.filesFromSourceFile !== '') { const inputFilesFromSourceFile = inputs.filesFromSourceFile .split(inputs.filesFromSourceFileSeparator) .filter(p => p !== '') const filesFromSourceFiles = ( await getFilesFromSourceFile({filePaths: inputFilesFromSourceFile}) ).join('\n') core.debug(`files from source files patterns: ${filesFromSourceFiles}`) filesPatterns = filesPatterns.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}`) filesPatterns = filesPatterns.concat('\n', filesIgnorePatterns) } if (inputs.filesIgnoreFromSourceFile) { const inputFilesIgnoreFromSourceFile = inputs.filesIgnoreFromSourceFile .split(inputs.filesIgnoreFromSourceFileSeparator) .filter(p => p !== '') const filesIgnoreFromSourceFiles = ( await getFilesFromSourceFile({ filePaths: inputFilesIgnoreFromSourceFile, excludedFiles: true }) ).join('\n') core.debug( `files ignore from source files patterns: ${filesIgnoreFromSourceFiles}` ) filesPatterns = filesPatterns.concat('\n', filesIgnoreFromSourceFiles) } const patterns = await getPatterns(filesPatterns) core.debug(`patterns: ${patterns}`) return patterns } export const setOutput = async ({ key, value, inputs }: { key: string value: string | boolean inputs: Inputs }): Promise => { const cleanedValue = value.toString().trim() if (inputs.writeOutputFiles) { const outputDir = inputs.outputDir || '.github/outputs' const extension = inputs.json ? 'json' : 'txt' const outputFilePath = path.join(outputDir, `${key}.${extension}`) if (!(await exists(outputDir))) { await fs.mkdir(outputDir, {recursive: true}) } await fs.writeFile(outputFilePath, cleanedValue) } else { core.setOutput(key, cleanedValue) } }