mirror of
https://code.forgejo.org/actions/checkout.git
synced 2024-11-05 10:12:53 +01:00
d106d4669b
* Add support for sparse checkouts * sparse-checkout: optionally turn off cone mode While it _is_ true that cone mode is the default nowadays (mainly for performance reasons: code mode is much faster than non-cone mode), there _are_ legitimate use cases where non-cone mode is really useful. Let's add a flag to optionally disable cone mode. Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> * Verify minimum Git version for sparse checkout The `git sparse-checkout` command is available only since Git version v2.25.0. The `actions/checkout` Action actually supports older Git versions than that; As of time of writing, the minimum version is v2.18.0. Instead of raising this minimum version even for users who do not require a sparse checkout, only check for this minimum version specifically when a sparse checkout was asked for. Suggested-by: Tingluo Huang <tingluohuang@github.com> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> * Support sparse checkout/LFS better Instead of fetching all the LFS objects present in the current revision in a sparse checkout, whether they are needed inside the sparse cone or not, let's instead only pull the ones that are actually needed. To do that, let's avoid running that preemptive `git lfs fetch` call in case of a sparse checkout. An alternative that was considered during the development of this patch (and ultimately rejected) was to use `git lfs pull --include <path>...`, but it turned out to be too inflexible because it requires exact paths, not the patterns that are available via the sparse checkout definition, and that risks running into command-line length limitations. Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> --------- Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> Co-authored-by: Daniel <daniel.fernandez@feverup.com>
331 lines
10 KiB
TypeScript
331 lines
10 KiB
TypeScript
import * as core from '@actions/core'
|
|
import * as fsHelper from './fs-helper'
|
|
import * as gitAuthHelper from './git-auth-helper'
|
|
import * as gitCommandManager from './git-command-manager'
|
|
import * as gitDirectoryHelper from './git-directory-helper'
|
|
import * as githubApiHelper from './github-api-helper'
|
|
import * as io from '@actions/io'
|
|
import * as path from 'path'
|
|
import * as refHelper from './ref-helper'
|
|
import * as stateHelper from './state-helper'
|
|
import * as urlHelper from './url-helper'
|
|
import {IGitCommandManager} from './git-command-manager'
|
|
import {IGitSourceSettings} from './git-source-settings'
|
|
|
|
export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
|
// Repository URL
|
|
core.info(
|
|
`Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}`
|
|
)
|
|
const repositoryUrl = urlHelper.getFetchUrl(settings)
|
|
|
|
// 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.startGroup('Getting Git version info')
|
|
const git = await getGitCommandManager(settings)
|
|
core.endGroup()
|
|
|
|
let authHelper: gitAuthHelper.IGitAuthHelper | null = null
|
|
try {
|
|
if (git) {
|
|
authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
if (settings.setSafeDirectory) {
|
|
// Setup the repository path as a safe directory, so if we pass this into a container job with a different user it doesn't fail
|
|
// Otherwise all git commands we run in a container fail
|
|
await authHelper.configureTempGlobalConfig()
|
|
core.info(
|
|
`Adding repository directory to the temporary git global config as a safe directory`
|
|
)
|
|
|
|
await git
|
|
.config('safe.directory', settings.repositoryPath, true, true)
|
|
.catch(error => {
|
|
core.info(
|
|
`Failed to initialize safe directory with error: ${error}`
|
|
)
|
|
})
|
|
|
|
stateHelper.setSafeDirectory()
|
|
}
|
|
}
|
|
|
|
// Prepare existing directory, otherwise recreate
|
|
if (isExisting) {
|
|
await gitDirectoryHelper.prepareExistingDirectory(
|
|
git,
|
|
settings.repositoryPath,
|
|
repositoryUrl,
|
|
settings.clean,
|
|
settings.ref
|
|
)
|
|
}
|
|
|
|
if (!git) {
|
|
// Downloading using REST API
|
|
core.info(`The repository will be downloaded using the GitHub REST API`)
|
|
core.info(
|
|
`To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`
|
|
)
|
|
if (settings.submodules) {
|
|
throw new Error(
|
|
`Input 'submodules' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
|
|
)
|
|
} else if (settings.sshKey) {
|
|
throw new Error(
|
|
`Input 'ssh-key' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
|
|
)
|
|
}
|
|
|
|
await githubApiHelper.downloadRepository(
|
|
settings.authToken,
|
|
settings.repositoryOwner,
|
|
settings.repositoryName,
|
|
settings.ref,
|
|
settings.commit,
|
|
settings.repositoryPath,
|
|
settings.githubServerUrl
|
|
)
|
|
return
|
|
}
|
|
|
|
// Save state for POST action
|
|
stateHelper.setRepositoryPath(settings.repositoryPath)
|
|
|
|
// Initialize the repository
|
|
if (
|
|
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
|
) {
|
|
core.startGroup('Initializing the repository')
|
|
await git.init()
|
|
await git.remoteAdd('origin', repositoryUrl)
|
|
core.endGroup()
|
|
}
|
|
|
|
// Disable automatic garbage collection
|
|
core.startGroup('Disabling 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.`
|
|
)
|
|
}
|
|
core.endGroup()
|
|
|
|
// If we didn't initialize it above, do it now
|
|
if (!authHelper) {
|
|
authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
}
|
|
// Configure auth
|
|
core.startGroup('Setting up auth')
|
|
await authHelper.configureAuth()
|
|
core.endGroup()
|
|
|
|
// Determine the default branch
|
|
if (!settings.ref && !settings.commit) {
|
|
core.startGroup('Determining the default branch')
|
|
if (settings.sshKey) {
|
|
settings.ref = await git.getDefaultBranch(repositoryUrl)
|
|
} else {
|
|
settings.ref = await githubApiHelper.getDefaultBranch(
|
|
settings.authToken,
|
|
settings.repositoryOwner,
|
|
settings.repositoryName,
|
|
settings.githubServerUrl
|
|
)
|
|
}
|
|
core.endGroup()
|
|
}
|
|
|
|
// LFS install
|
|
if (settings.lfs) {
|
|
await git.lfsInstall()
|
|
}
|
|
|
|
// Fetch
|
|
core.startGroup('Fetching the repository')
|
|
const fetchOptions: {filter?: string; fetchDepth?: number} = {}
|
|
if (settings.sparseCheckout) fetchOptions.filter = 'blob:none'
|
|
if (settings.fetchDepth <= 0) {
|
|
// Fetch all branches and tags
|
|
let refSpec = refHelper.getRefSpecForAllHistory(
|
|
settings.ref,
|
|
settings.commit
|
|
)
|
|
await git.fetch(refSpec, fetchOptions)
|
|
|
|
// When all history is fetched, the ref we're interested in may have moved to a different
|
|
// commit (push or force push). If so, fetch again with a targeted refspec.
|
|
if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
|
|
refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
|
|
await git.fetch(refSpec, fetchOptions)
|
|
}
|
|
} else {
|
|
fetchOptions.fetchDepth = settings.fetchDepth
|
|
const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
|
|
await git.fetch(refSpec, fetchOptions)
|
|
}
|
|
core.endGroup()
|
|
|
|
// Checkout info
|
|
core.startGroup('Determining the checkout info')
|
|
const checkoutInfo = await refHelper.getCheckoutInfo(
|
|
git,
|
|
settings.ref,
|
|
settings.commit
|
|
)
|
|
core.endGroup()
|
|
|
|
// 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.
|
|
// For sparse checkouts, let `checkout` fetch the needed objects lazily.
|
|
if (settings.lfs && !settings.sparseCheckout) {
|
|
core.startGroup('Fetching LFS objects')
|
|
await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref)
|
|
core.endGroup()
|
|
}
|
|
|
|
// Sparse checkout
|
|
if (settings.sparseCheckout) {
|
|
core.startGroup('Setting up sparse checkout')
|
|
if (settings.sparseCheckoutConeMode) {
|
|
await git.sparseCheckout(settings.sparseCheckout)
|
|
} else {
|
|
await git.sparseCheckoutNonConeMode(settings.sparseCheckout)
|
|
}
|
|
core.endGroup()
|
|
}
|
|
|
|
// Checkout
|
|
core.startGroup('Checking out the ref')
|
|
await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
|
|
core.endGroup()
|
|
|
|
// Submodules
|
|
if (settings.submodules) {
|
|
// Temporarily override global config
|
|
core.startGroup('Setting up auth for fetching submodules')
|
|
await authHelper.configureGlobalAuth()
|
|
core.endGroup()
|
|
|
|
// Checkout submodules
|
|
core.startGroup('Fetching submodules')
|
|
await git.submoduleSync(settings.nestedSubmodules)
|
|
await git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules)
|
|
await git.submoduleForeach(
|
|
'git config --local gc.auto 0',
|
|
settings.nestedSubmodules
|
|
)
|
|
core.endGroup()
|
|
|
|
// Persist credentials
|
|
if (settings.persistCredentials) {
|
|
core.startGroup('Persisting credentials for submodules')
|
|
await authHelper.configureSubmoduleAuth()
|
|
core.endGroup()
|
|
}
|
|
}
|
|
|
|
// Get commit information
|
|
const commitInfo = await git.log1()
|
|
|
|
// Log commit sha
|
|
await git.log1("--format='%H'")
|
|
|
|
// Check for incorrect pull request merge commit
|
|
await refHelper.checkCommitInfo(
|
|
settings.authToken,
|
|
commitInfo,
|
|
settings.repositoryOwner,
|
|
settings.repositoryName,
|
|
settings.ref,
|
|
settings.commit,
|
|
settings.githubServerUrl
|
|
)
|
|
} finally {
|
|
// Remove auth
|
|
if (authHelper) {
|
|
if (!settings.persistCredentials) {
|
|
core.startGroup('Removing auth')
|
|
await authHelper.removeAuth()
|
|
core.endGroup()
|
|
}
|
|
authHelper.removeGlobalConfig()
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function cleanup(repositoryPath: string): Promise<void> {
|
|
// Repo exists?
|
|
if (
|
|
!repositoryPath ||
|
|
!fsHelper.fileExistsSync(path.join(repositoryPath, '.git', 'config'))
|
|
) {
|
|
return
|
|
}
|
|
|
|
let git: IGitCommandManager
|
|
try {
|
|
git = await gitCommandManager.createCommandManager(
|
|
repositoryPath,
|
|
false,
|
|
false
|
|
)
|
|
} catch {
|
|
return
|
|
}
|
|
|
|
// Remove auth
|
|
const authHelper = gitAuthHelper.createAuthHelper(git)
|
|
try {
|
|
if (stateHelper.PostSetSafeDirectory) {
|
|
// Setup the repository path as a safe directory, so if we pass this into a container job with a different user it doesn't fail
|
|
// Otherwise all git commands we run in a container fail
|
|
await authHelper.configureTempGlobalConfig()
|
|
core.info(
|
|
`Adding repository directory to the temporary git global config as a safe directory`
|
|
)
|
|
|
|
await git
|
|
.config('safe.directory', repositoryPath, true, true)
|
|
.catch(error => {
|
|
core.info(`Failed to initialize safe directory with error: ${error}`)
|
|
})
|
|
}
|
|
|
|
await authHelper.removeAuth()
|
|
} finally {
|
|
await authHelper.removeGlobalConfig()
|
|
}
|
|
}
|
|
|
|
async function getGitCommandManager(
|
|
settings: IGitSourceSettings
|
|
): Promise<IGitCommandManager | undefined> {
|
|
core.info(`Working directory is '${settings.repositoryPath}'`)
|
|
try {
|
|
return await gitCommandManager.createCommandManager(
|
|
settings.repositoryPath,
|
|
settings.lfs,
|
|
settings.sparseCheckout != null
|
|
)
|
|
} catch (err) {
|
|
// Git is required for LFS
|
|
if (settings.lfs) {
|
|
throw err
|
|
}
|
|
|
|
// Otherwise fallback to REST API
|
|
return undefined
|
|
}
|
|
}
|