diff options
Diffstat (limited to 'src/git-source-provider.ts')
| -rw-r--r-- | src/git-source-provider.ts | 246 |
1 files changed, 246 insertions, 0 deletions
diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts new file mode 100644 index 0000000..0752100 --- /dev/null +++ b/src/git-source-provider.ts @@ -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) + } +} |
