mirror of
https://gitea.com/actions/checkout.git
synced 2025-04-08 00:09:40 +00:00
Convert checkout to a regular action (#70)
This commit is contained in:
77
src/fs-helper.ts
Normal file
77
src/fs-helper.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import * as fs from 'fs'
|
||||
|
||||
export function directoryExistsSync(path: string, required?: boolean): boolean {
|
||||
if (!path) {
|
||||
throw new Error("Arg 'path' must not be empty")
|
||||
}
|
||||
|
||||
let stats: fs.Stats
|
||||
try {
|
||||
stats = fs.statSync(path)
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
if (!required) {
|
||||
return false
|
||||
}
|
||||
|
||||
throw new Error(`Directory '${path}' does not exist`)
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
|
||||
)
|
||||
}
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
return true
|
||||
} else if (!required) {
|
||||
return false
|
||||
}
|
||||
|
||||
throw new Error(`Directory '${path}' does not exist`)
|
||||
}
|
||||
|
||||
export function existsSync(path: string): boolean {
|
||||
if (!path) {
|
||||
throw new Error("Arg 'path' must not be empty")
|
||||
}
|
||||
|
||||
try {
|
||||
fs.statSync(path)
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return false
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function fileExistsSync(path: string): boolean {
|
||||
if (!path) {
|
||||
throw new Error("Arg 'path' must not be empty")
|
||||
}
|
||||
|
||||
let stats: fs.Stats
|
||||
try {
|
||||
stats = fs.statSync(path)
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return false
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
|
||||
)
|
||||
}
|
||||
|
||||
if (!stats.isDirectory()) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
399
src/git-command-manager.ts
Normal file
399
src/git-command-manager.ts
Normal file
@ -0,0 +1,399 @@
|
||||
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'
|
||||
import {GitVersion} from './git-version'
|
||||
|
||||
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>
|
||||
config(configKey: string, configValue: string): Promise<void>
|
||||
configExists(configKey: string): Promise<boolean>
|
||||
fetch(fetchDepth: number, refSpec: string[]): Promise<void>
|
||||
getWorkingDirectory(): string
|
||||
init(): Promise<void>
|
||||
isDetached(): Promise<boolean>
|
||||
lfsFetch(ref: string): Promise<void>
|
||||
lfsInstall(): Promise<void>
|
||||
log1(): Promise<void>
|
||||
remoteAdd(remoteName: string, remoteUrl: string): Promise<void>
|
||||
tagExists(pattern: string): Promise<boolean>
|
||||
tryClean(): Promise<boolean>
|
||||
tryConfigUnset(configKey: string): Promise<boolean>
|
||||
tryDisableAutomaticGarbageCollection(): Promise<boolean>
|
||||
tryGetFetchUrl(): Promise<string>
|
||||
tryReset(): Promise<boolean>
|
||||
}
|
||||
|
||||
export async function CreateCommandManager(
|
||||
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[] = []
|
||||
|
||||
// Note, this implementation uses "rev-parse --symbolic" because the output from
|
||||
// "branch --list" is more difficult when in a detached HEAD state.
|
||||
|
||||
const args = ['rev-parse', '--symbolic']
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
async config(configKey: string, configValue: string): Promise<void> {
|
||||
await this.execGit(['config', configKey, configValue])
|
||||
}
|
||||
|
||||
async configExists(configKey: string): Promise<boolean> {
|
||||
const pattern = configKey.replace(/[^a-zA-Z0-9_]/g, x => {
|
||||
return `\\${x}`
|
||||
})
|
||||
const output = await this.execGit(
|
||||
['config', '--name-only', '--get-regexp', pattern],
|
||||
true
|
||||
)
|
||||
return output.exitCode === 0
|
||||
}
|
||||
|
||||
async fetch(fetchDepth: number, refSpec: string[]): Promise<void> {
|
||||
const args = [
|
||||
'-c',
|
||||
'protocol.version=2',
|
||||
'fetch',
|
||||
'--no-tags',
|
||||
'--prune',
|
||||
'--progress',
|
||||
'--no-recurse-submodules'
|
||||
]
|
||||
if (fetchDepth > 0) {
|
||||
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)
|
||||
}
|
||||
|
||||
let attempt = 1
|
||||
const maxAttempts = 3
|
||||
while (attempt <= maxAttempts) {
|
||||
const allowAllExitCodes = attempt < maxAttempts
|
||||
const output = await this.execGit(args, allowAllExitCodes)
|
||||
if (output.exitCode === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
const seconds = this.getRandomIntInclusive(1, 10)
|
||||
core.warning(
|
||||
`Git fetch failed with exit code ${output.exitCode}. Waiting ${seconds} seconds before trying again.`
|
||||
)
|
||||
await this.sleep(seconds * 1000)
|
||||
attempt++
|
||||
}
|
||||
}
|
||||
|
||||
getWorkingDirectory(): string {
|
||||
return this.workingDirectory
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
await this.execGit(['init', this.workingDirectory])
|
||||
}
|
||||
|
||||
async isDetached(): Promise<boolean> {
|
||||
// Note, this implementation uses "branch --show-current" because
|
||||
// "rev-parse --symbolic-full-name HEAD" can fail on a new repo
|
||||
// with nothing checked out.
|
||||
|
||||
const output = await this.execGit(['branch', '--show-current'])
|
||||
return output.stdout.trim() === ''
|
||||
}
|
||||
|
||||
async lfsFetch(ref: string): Promise<void> {
|
||||
const args = ['lfs', 'fetch', 'origin', ref]
|
||||
|
||||
let attempt = 1
|
||||
const maxAttempts = 3
|
||||
while (attempt <= maxAttempts) {
|
||||
const allowAllExitCodes = attempt < maxAttempts
|
||||
const output = await this.execGit(args, allowAllExitCodes)
|
||||
if (output.exitCode === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
const seconds = this.getRandomIntInclusive(1, 10)
|
||||
core.warning(
|
||||
`Git lfs fetch failed with exit code ${output.exitCode}. Waiting ${seconds} seconds before trying again.`
|
||||
)
|
||||
await this.sleep(seconds * 1000)
|
||||
attempt++
|
||||
}
|
||||
}
|
||||
|
||||
async lfsInstall(): Promise<void> {
|
||||
await this.execGit(['lfs', 'install', '--local'])
|
||||
}
|
||||
|
||||
async log1(): Promise<void> {
|
||||
await this.execGit(['log', '-1'])
|
||||
}
|
||||
|
||||
async remoteAdd(remoteName: string, remoteUrl: string): Promise<void> {
|
||||
await this.execGit(['remote', 'add', remoteName, remoteUrl])
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
async tryConfigUnset(configKey: string): Promise<boolean> {
|
||||
const output = await this.execGit(
|
||||
['config', '--unset-all', configKey],
|
||||
true
|
||||
)
|
||||
return output.exitCode === 0
|
||||
}
|
||||
|
||||
async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
|
||||
const output = await this.execGit(['config', 'gc.auto', '0'], true)
|
||||
return output.exitCode === 0
|
||||
}
|
||||
|
||||
async tryGetFetchUrl(): Promise<string> {
|
||||
const output = await this.execGit(
|
||||
['config', '--get', 'remote.origin.url'],
|
||||
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[],
|
||||
allowAllExitCodes = false
|
||||
): 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,
|
||||
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
|
||||
// Note:
|
||||
// - Auth header not supported before 2.9
|
||||
// - Wire protocol v2 not supported before 2.18
|
||||
const minimumGitVersion = new GitVersion('2.18')
|
||||
if (!gitVersion.checkMinimum(minimumGitVersion)) {
|
||||
throw new Error(
|
||||
`Minimum required git version is ${minimumGitVersion}. Your git ('${this.gitPath}') is ${gitVersion}`
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
private getRandomIntInclusive(minimum: number, maximum: number): number {
|
||||
minimum = Math.floor(minimum)
|
||||
maximum = Math.floor(maximum)
|
||||
return Math.floor(Math.random() * (maximum - minimum + 1)) + minimum
|
||||
}
|
||||
|
||||
private async sleep(milliseconds): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, milliseconds))
|
||||
}
|
||||
}
|
||||
|
||||
class GitOutput {
|
||||
stdout = ''
|
||||
exitCode = 0
|
||||
}
|
246
src/git-source-provider.ts
Normal file
246
src/git-source-provider.ts
Normal file
@ -0,0 +1,246 @@
|
||||
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'
|
||||
import * as io from '@actions/io'
|
||||
import * as path from 'path'
|
||||
import * as refHelper from './ref-helper'
|
||||
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> {
|
||||
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
|
||||
core.info(`Working directory is '${settings.repositoryPath}'`)
|
||||
const git = await gitCommandManager.CreateCommandManager(
|
||||
settings.repositoryPath,
|
||||
settings.lfs
|
||||
)
|
||||
|
||||
// Try prepare existing directory, otherwise recreate
|
||||
if (
|
||||
isExisting &&
|
||||
!(await tryPrepareExistingDirectory(
|
||||
git,
|
||||
settings.repositoryPath,
|
||||
repositoryUrl,
|
||||
settings.clean
|
||||
))
|
||||
) {
|
||||
await io.rmRF(settings.repositoryPath)
|
||||
await io.mkdirP(settings.repositoryPath)
|
||||
}
|
||||
|
||||
// Initialize the repository
|
||||
if (
|
||||
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
||||
) {
|
||||
await git.init()
|
||||
await git.remoteAdd('origin', repositoryUrl)
|
||||
}
|
||||
|
||||
// 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.`
|
||||
)
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// Fetch
|
||||
const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
|
||||
await git.fetch(settings.fetchDepth, refSpec)
|
||||
|
||||
// Checkout info
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(
|
||||
git,
|
||||
settings.ref,
|
||||
settings.commit
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Checkout
|
||||
await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
|
||||
|
||||
// Dump some info about the checked out commit
|
||||
await git.log1()
|
||||
|
||||
// Set intra-task state for cleanup
|
||||
coreCommand.issueCommand(
|
||||
'save-state',
|
||||
{name: 'repositoryPath'},
|
||||
settings.repositoryPath
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
async function tryPrepareExistingDirectory(
|
||||
git: IGitCommandManager,
|
||||
repositoryPath: string,
|
||||
repositoryUrl: string,
|
||||
clean: boolean
|
||||
): Promise<boolean> {
|
||||
// Fetch URL does not match
|
||||
if (
|
||||
!fsHelper.directoryExistsSync(path.join(repositoryPath, '.git')) ||
|
||||
repositoryUrl !== (await git.tryGetFetchUrl())
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 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}`)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 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)
|
||||
}
|
||||
} catch (error) {
|
||||
core.warning(
|
||||
`Unable to prepare the existing repository. The repository will be recreated instead.`
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
// Clean
|
||||
if (clean) {
|
||||
let succeeded = true
|
||||
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}'.`
|
||||
)
|
||||
succeeded = false
|
||||
} else if (!(await git.tryReset())) {
|
||||
succeeded = false
|
||||
}
|
||||
|
||||
if (!succeeded) {
|
||||
core.warning(
|
||||
`Unable to clean or reset the repository. The repository will be recreated instead.`
|
||||
)
|
||||
}
|
||||
|
||||
return succeeded
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
77
src/git-version.ts
Normal file
77
src/git-version.ts
Normal file
@ -0,0 +1,77 @@
|
||||
export class GitVersion {
|
||||
private readonly major: number = NaN
|
||||
private readonly minor: number = NaN
|
||||
private readonly patch: number = NaN
|
||||
|
||||
/**
|
||||
* Used for comparing the version of git and git-lfs against the minimum required version
|
||||
* @param version the version string, e.g. 1.2 or 1.2.3
|
||||
*/
|
||||
constructor(version?: string) {
|
||||
if (version) {
|
||||
const match = version.match(/^(\d+)\.(\d+)(\.(\d+))?$/)
|
||||
if (match) {
|
||||
this.major = Number(match[1])
|
||||
this.minor = Number(match[2])
|
||||
if (match[4]) {
|
||||
this.patch = Number(match[4])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the instance against a minimum required version
|
||||
* @param minimum Minimum version
|
||||
*/
|
||||
checkMinimum(minimum: GitVersion): boolean {
|
||||
if (!minimum.isValid()) {
|
||||
throw new Error('Arg minimum is not a valid version')
|
||||
}
|
||||
|
||||
// Major is insufficient
|
||||
if (this.major < minimum.major) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Major is equal
|
||||
if (this.major === minimum.major) {
|
||||
// Minor is insufficient
|
||||
if (this.minor < minimum.minor) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Minor is equal
|
||||
if (this.minor === minimum.minor) {
|
||||
// Patch is insufficient
|
||||
if (this.patch && this.patch < (minimum.patch || 0)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the instance was constructed from a valid version string
|
||||
*/
|
||||
isValid(): boolean {
|
||||
return !isNaN(this.major)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version as a string, e.g. 1.2 or 1.2.3
|
||||
*/
|
||||
toString(): string {
|
||||
let result = ''
|
||||
if (this.isValid()) {
|
||||
result = `${this.major}.${this.minor}`
|
||||
if (!isNaN(this.patch)) {
|
||||
result += `.${this.patch}`
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
104
src/input-helper.ts
Normal file
104
src/input-helper.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as fsHelper from './fs-helper'
|
||||
import * as github from '@actions/github'
|
||||
import * as path from 'path'
|
||||
import {ISourceSettings} from './git-source-provider'
|
||||
|
||||
export function getInputs(): ISourceSettings {
|
||||
const result = ({} as unknown) as ISourceSettings
|
||||
|
||||
// GitHub workspace
|
||||
let githubWorkspacePath = process.env['GITHUB_WORKSPACE']
|
||||
if (!githubWorkspacePath) {
|
||||
throw new Error('GITHUB_WORKSPACE not defined')
|
||||
}
|
||||
githubWorkspacePath = path.resolve(githubWorkspacePath)
|
||||
core.debug(`GITHUB_WORKSPACE = '${githubWorkspacePath}'`)
|
||||
fsHelper.directoryExistsSync(githubWorkspacePath, true)
|
||||
|
||||
// Qualified repository
|
||||
const qualifiedRepository =
|
||||
core.getInput('repository') ||
|
||||
`${github.context.repo.owner}/${github.context.repo.repo}`
|
||||
core.debug(`qualified repository = '${qualifiedRepository}'`)
|
||||
const splitRepository = qualifiedRepository.split('/')
|
||||
if (
|
||||
splitRepository.length !== 2 ||
|
||||
!splitRepository[0] ||
|
||||
!splitRepository[1]
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid repository '${qualifiedRepository}'. Expected format {owner}/{repo}.`
|
||||
)
|
||||
}
|
||||
result.repositoryOwner = splitRepository[0]
|
||||
result.repositoryName = splitRepository[1]
|
||||
|
||||
// Repository path
|
||||
result.repositoryPath = core.getInput('path') || '.'
|
||||
result.repositoryPath = path.resolve(
|
||||
githubWorkspacePath,
|
||||
result.repositoryPath
|
||||
)
|
||||
if (
|
||||
!(result.repositoryPath + path.sep).startsWith(
|
||||
githubWorkspacePath + path.sep
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`Repository path '${result.repositoryPath}' is not under '${githubWorkspacePath}'`
|
||||
)
|
||||
}
|
||||
|
||||
// Workflow repository?
|
||||
const isWorkflowRepository =
|
||||
qualifiedRepository.toUpperCase() ===
|
||||
`${github.context.repo.owner}/${github.context.repo.repo}`.toUpperCase()
|
||||
|
||||
// Source branch, source version
|
||||
result.ref = core.getInput('ref')
|
||||
if (!result.ref) {
|
||||
if (isWorkflowRepository) {
|
||||
result.ref = github.context.ref
|
||||
result.commit = github.context.sha
|
||||
}
|
||||
|
||||
if (!result.ref && !result.commit) {
|
||||
result.ref = 'refs/heads/master'
|
||||
}
|
||||
}
|
||||
// SHA?
|
||||
else if (result.ref.match(/^[0-9a-fA-F]{40}$/)) {
|
||||
result.commit = result.ref
|
||||
result.ref = ''
|
||||
}
|
||||
core.debug(`ref = '${result.ref}'`)
|
||||
core.debug(`commit = '${result.commit}'`)
|
||||
|
||||
// Clean
|
||||
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'
|
||||
core.debug(`clean = ${result.clean}`)
|
||||
|
||||
// Submodules
|
||||
if (core.getInput('submodules')) {
|
||||
throw new Error(
|
||||
"The input 'submodules' is not supported in actions/checkout@v2"
|
||||
)
|
||||
}
|
||||
|
||||
// Fetch depth
|
||||
result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1'))
|
||||
if (isNaN(result.fetchDepth) || result.fetchDepth < 0) {
|
||||
result.fetchDepth = 0
|
||||
}
|
||||
core.debug(`fetch depth = ${result.fetchDepth}`)
|
||||
|
||||
// LFS
|
||||
result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE'
|
||||
core.debug(`lfs = ${result.lfs}`)
|
||||
|
||||
// Access token
|
||||
result.accessToken = core.getInput('token')
|
||||
|
||||
return result
|
||||
}
|
47
src/main.ts
Normal file
47
src/main.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as coreCommand from '@actions/core/lib/command'
|
||||
import * as gitSourceProvider from './git-source-provider'
|
||||
import * as inputHelper from './input-helper'
|
||||
import * as path from 'path'
|
||||
|
||||
const cleanupRepositoryPath = process.env['STATE_repositoryPath'] as string
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
const sourceSettings = inputHelper.getInputs()
|
||||
|
||||
try {
|
||||
// Register problem matcher
|
||||
coreCommand.issueCommand(
|
||||
'add-matcher',
|
||||
{},
|
||||
path.join(__dirname, 'problem-matcher.json')
|
||||
)
|
||||
|
||||
// Get sources
|
||||
await gitSourceProvider.getSource(sourceSettings)
|
||||
} finally {
|
||||
// Unregister problem matcher
|
||||
coreCommand.issueCommand('remove-matcher', {owner: 'checkout-git'}, '')
|
||||
}
|
||||
} catch (error) {
|
||||
core.setFailed(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanup(): Promise<void> {
|
||||
try {
|
||||
await gitSourceProvider.cleanup(cleanupRepositoryPath)
|
||||
} catch (error) {
|
||||
core.warning(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// Main
|
||||
if (!cleanupRepositoryPath) {
|
||||
run()
|
||||
}
|
||||
// Post
|
||||
else {
|
||||
cleanup()
|
||||
}
|
102
src/misc/generate-docs.ts
Normal file
102
src/misc/generate-docs.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
import * as yaml from 'js-yaml'
|
||||
|
||||
//
|
||||
// SUMMARY
|
||||
//
|
||||
// This script rebuilds the usage section in the README.md to be consistent with the action.yml
|
||||
|
||||
function updateUsage(
|
||||
actionReference: string,
|
||||
actionYamlPath: string = 'action.yml',
|
||||
readmePath: string = 'README.md',
|
||||
startToken: string = '<!-- start usage -->',
|
||||
endToken: string = '<!-- end usage -->'
|
||||
): void {
|
||||
if (!actionReference) {
|
||||
throw new Error('Parameter actionReference must not be empty')
|
||||
}
|
||||
|
||||
// Load the action.yml
|
||||
const actionYaml = yaml.safeLoad(fs.readFileSync(actionYamlPath).toString())
|
||||
|
||||
// Load the README
|
||||
const originalReadme = fs.readFileSync(readmePath).toString()
|
||||
|
||||
// Find the start token
|
||||
const startTokenIndex = originalReadme.indexOf(startToken)
|
||||
if (startTokenIndex < 0) {
|
||||
throw new Error(`Start token '${startToken}' not found`)
|
||||
}
|
||||
|
||||
// Find the end token
|
||||
const endTokenIndex = originalReadme.indexOf(endToken)
|
||||
if (endTokenIndex < 0) {
|
||||
throw new Error(`End token '${endToken}' not found`)
|
||||
} else if (endTokenIndex < startTokenIndex) {
|
||||
throw new Error('Start token must appear before end token')
|
||||
}
|
||||
|
||||
// Build the new README
|
||||
const newReadme: string[] = []
|
||||
|
||||
// Append the beginning
|
||||
newReadme.push(originalReadme.substr(0, startTokenIndex + startToken.length))
|
||||
|
||||
// Build the new usage section
|
||||
newReadme.push('```yaml', `- uses: ${actionReference}`, ' with:')
|
||||
const inputs = actionYaml.inputs
|
||||
let firstInput = true
|
||||
for (const key of Object.keys(inputs)) {
|
||||
const input = inputs[key]
|
||||
|
||||
// Line break between inputs
|
||||
if (!firstInput) {
|
||||
newReadme.push('')
|
||||
}
|
||||
|
||||
// Constrain the width of the description
|
||||
const width = 80
|
||||
let description = input.description as string
|
||||
while (description) {
|
||||
// Longer than width? Find a space to break apart
|
||||
let segment: string = description
|
||||
if (description.length > width) {
|
||||
segment = description.substr(0, width + 1)
|
||||
while (!segment.endsWith(' ')) {
|
||||
segment = segment.substr(0, segment.length - 1)
|
||||
}
|
||||
} else {
|
||||
segment = description
|
||||
}
|
||||
|
||||
description = description.substr(segment.length) // Remaining
|
||||
segment = segment.trimRight() // Trim the trailing space
|
||||
newReadme.push(` # ${segment}`)
|
||||
}
|
||||
|
||||
// Input and default
|
||||
if (input.default !== undefined) {
|
||||
newReadme.push(` # Default: ${input.default}`)
|
||||
}
|
||||
newReadme.push(` ${key}: ''`)
|
||||
|
||||
firstInput = false
|
||||
}
|
||||
|
||||
newReadme.push('```')
|
||||
|
||||
// Append the end
|
||||
newReadme.push(originalReadme.substr(endTokenIndex))
|
||||
|
||||
// Write the new README
|
||||
fs.writeFileSync(readmePath, newReadme.join(os.EOL))
|
||||
}
|
||||
|
||||
updateUsage(
|
||||
'actions/checkout@preview',
|
||||
path.join(__dirname, '..', '..', 'action.yml'),
|
||||
path.join(__dirname, '..', '..', 'README.md')
|
||||
)
|
109
src/ref-helper.ts
Normal file
109
src/ref-helper.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import {IGitCommandManager} from './git-command-manager'
|
||||
|
||||
export interface ICheckoutInfo {
|
||||
ref: string
|
||||
startPoint: string
|
||||
}
|
||||
|
||||
export async function getCheckoutInfo(
|
||||
git: IGitCommandManager,
|
||||
ref: string,
|
||||
commit: string
|
||||
): Promise<ICheckoutInfo> {
|
||||
if (!git) {
|
||||
throw new Error('Arg git cannot be empty')
|
||||
}
|
||||
|
||||
if (!ref && !commit) {
|
||||
throw new Error('Args ref and commit cannot both be empty')
|
||||
}
|
||||
|
||||
const result = ({} as unknown) as ICheckoutInfo
|
||||
const upperRef = (ref || '').toUpperCase()
|
||||
|
||||
// SHA only
|
||||
if (!ref) {
|
||||
result.ref = commit
|
||||
}
|
||||
// refs/heads/
|
||||
else if (upperRef.startsWith('REFS/HEADS/')) {
|
||||
const branch = ref.substring('refs/heads/'.length)
|
||||
result.ref = branch
|
||||
result.startPoint = `refs/remotes/origin/${branch}`
|
||||
}
|
||||
// refs/pull/
|
||||
else if (upperRef.startsWith('REFS/PULL/')) {
|
||||
const branch = ref.substring('refs/pull/'.length)
|
||||
result.ref = `refs/remotes/pull/${branch}`
|
||||
}
|
||||
// refs/tags/
|
||||
else if (upperRef.startsWith('REFS/')) {
|
||||
result.ref = ref
|
||||
}
|
||||
// Unqualified ref, check for a matching branch or tag
|
||||
else {
|
||||
if (await git.branchExists(true, `origin/${ref}`)) {
|
||||
result.ref = ref
|
||||
result.startPoint = `refs/remotes/origin/${ref}`
|
||||
} else if (await git.tagExists(`${ref}`)) {
|
||||
result.ref = `refs/tags/${ref}`
|
||||
} else {
|
||||
throw new Error(
|
||||
`A branch or tag with the name '${ref}' could not be found`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function getRefSpec(ref: string, commit: string): string[] {
|
||||
if (!ref && !commit) {
|
||||
throw new Error('Args ref and commit cannot both be empty')
|
||||
}
|
||||
|
||||
const upperRef = (ref || '').toUpperCase()
|
||||
|
||||
// SHA
|
||||
if (commit) {
|
||||
// refs/heads
|
||||
if (upperRef.startsWith('REFS/HEADS/')) {
|
||||
const branch = ref.substring('refs/heads/'.length)
|
||||
return [`+${commit}:refs/remotes/origin/${branch}`]
|
||||
}
|
||||
// refs/pull/
|
||||
else if (upperRef.startsWith('REFS/PULL/')) {
|
||||
const branch = ref.substring('refs/pull/'.length)
|
||||
return [`+${commit}:refs/remotes/pull/${branch}`]
|
||||
}
|
||||
// refs/tags/
|
||||
else if (upperRef.startsWith('REFS/TAGS/')) {
|
||||
return [`+${commit}:${ref}`]
|
||||
}
|
||||
// Otherwise no destination ref
|
||||
else {
|
||||
return [commit]
|
||||
}
|
||||
}
|
||||
// Unqualified ref, check for a matching branch or tag
|
||||
else if (!upperRef.startsWith('REFS/')) {
|
||||
return [
|
||||
`+refs/heads/${ref}*:refs/remotes/origin/${ref}*`,
|
||||
`+refs/tags/${ref}*:refs/tags/${ref}*`
|
||||
]
|
||||
}
|
||||
// refs/heads/
|
||||
else if (upperRef.startsWith('REFS/HEADS/')) {
|
||||
const branch = ref.substring('refs/heads/'.length)
|
||||
return [`+${ref}:refs/remotes/origin/${branch}`]
|
||||
}
|
||||
// refs/pull/
|
||||
else if (upperRef.startsWith('REFS/PULL/')) {
|
||||
const branch = ref.substring('refs/pull/'.length)
|
||||
return [`+${ref}:refs/remotes/pull/${branch}`]
|
||||
}
|
||||
// refs/tags/
|
||||
else {
|
||||
return [`+${ref}:${ref}`]
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user