mirror of
				https://github.com/actions/checkout.git
				synced 2025-11-04 11:35:09 +08: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