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
File diff suppressed because it is too large
Load Diff
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