Read-only mirror — the public repositories of the Forgejo instance, refreshed every 10 minutes. The live instance (fg.dieschoenewolke.de) is reachable only over the VPN; clone here with https://git.dieschoenewolke.de/git/<owner>/<repo>.git.
summaryrefslogtreecommitdiff
path: root/src/git-command-manager.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/git-command-manager.ts')
-rw-r--r--src/git-command-manager.ts399
1 files changed, 399 insertions, 0 deletions
diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts
new file mode 100644
index 0000000..6e4f22f
--- /dev/null
+++ b/src/git-command-manager.ts
@@ -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
+}