mirror of
				https://github.com/actions/checkout.git
				synced 2025-10-31 07:30:32 +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
	 eric sciple
					eric sciple