git-source-provider.ts
1 import * as core from '@actions/core' 2 import * as fsHelper from './fs-helper' 3 import * as gitAuthHelper from './git-auth-helper' 4 import * as gitCommandManager from './git-command-manager' 5 import * as gitDirectoryHelper from './git-directory-helper' 6 import * as githubApiHelper from './github-api-helper' 7 import * as io from '@actions/io' 8 import * as path from 'path' 9 import * as refHelper from './ref-helper' 10 import * as stateHelper from './state-helper' 11 import * as urlHelper from './url-helper' 12 import {IGitCommandManager} from './git-command-manager' 13 import {IGitSourceSettings} from './git-source-settings' 14 15 export async function getSource(settings: IGitSourceSettings): Promise<void> { 16 // Repository URL 17 core.info( 18 `Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}` 19 ) 20 const repositoryUrl = urlHelper.getFetchUrl(settings) 21 22 // Remove conflicting file path 23 if (fsHelper.fileExistsSync(settings.repositoryPath)) { 24 await io.rmRF(settings.repositoryPath) 25 } 26 27 // Create directory 28 let isExisting = true 29 if (!fsHelper.directoryExistsSync(settings.repositoryPath)) { 30 isExisting = false 31 await io.mkdirP(settings.repositoryPath) 32 } 33 34 // Git command manager 35 core.startGroup('Getting Git version info') 36 const git = await getGitCommandManager(settings) 37 core.endGroup() 38 39 // Prepare existing directory, otherwise recreate 40 if (isExisting) { 41 await gitDirectoryHelper.prepareExistingDirectory( 42 git, 43 settings.repositoryPath, 44 repositoryUrl, 45 settings.clean, 46 settings.ref 47 ) 48 } 49 50 if (!git) { 51 // Downloading using REST API 52 core.info(`The repository will be downloaded using the GitHub REST API`) 53 core.info( 54 `To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH` 55 ) 56 if (settings.submodules) { 57 throw new Error( 58 `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.` 59 ) 60 } else if (settings.sshKey) { 61 throw new Error( 62 `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.` 63 ) 64 } 65 66 await githubApiHelper.downloadRepository( 67 settings.authToken, 68 settings.repositoryOwner, 69 settings.repositoryName, 70 settings.ref, 71 settings.commit, 72 settings.repositoryPath 73 ) 74 return 75 } 76 77 // Save state for POST action 78 stateHelper.setRepositoryPath(settings.repositoryPath) 79 80 // Initialize the repository 81 if ( 82 !fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git')) 83 ) { 84 core.startGroup('Initializing the repository') 85 await git.init() 86 await git.remoteAdd('origin', repositoryUrl) 87 core.endGroup() 88 } 89 90 // Disable automatic garbage collection 91 core.startGroup('Disabling automatic garbage collection') 92 if (!(await git.tryDisableAutomaticGarbageCollection())) { 93 core.warning( 94 `Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.` 95 ) 96 } 97 core.endGroup() 98 99 const authHelper = gitAuthHelper.createAuthHelper(git, settings) 100 try { 101 // Configure auth 102 core.startGroup('Setting up auth') 103 await authHelper.configureAuth() 104 core.endGroup() 105 106 // Determine the default branch 107 if (!settings.ref && !settings.commit) { 108 core.startGroup('Determining the default branch') 109 if (settings.sshKey) { 110 settings.ref = await git.getDefaultBranch(repositoryUrl) 111 } else { 112 settings.ref = await githubApiHelper.getDefaultBranch( 113 settings.authToken, 114 settings.repositoryOwner, 115 settings.repositoryName 116 ) 117 } 118 core.endGroup() 119 } 120 121 // LFS install 122 if (settings.lfs) { 123 await git.lfsInstall() 124 } 125 126 // Fetch 127 core.startGroup('Fetching the repository') 128 if (settings.fetchDepth <= 0) { 129 // Fetch all branches and tags 130 let refSpec = refHelper.getRefSpecForAllHistory( 131 settings.ref, 132 settings.commit 133 ) 134 await git.fetch(refSpec) 135 136 // When all history is fetched, the ref we're interested in may have moved to a different 137 // commit (push or force push). If so, fetch again with a targeted refspec. 138 if (!(await refHelper.testRef(git, settings.ref, settings.commit))) { 139 refSpec = refHelper.getRefSpec(settings.ref, settings.commit) 140 await git.fetch(refSpec) 141 } 142 } else { 143 const refSpec = refHelper.getRefSpec(settings.ref, settings.commit) 144 await git.fetch(refSpec, settings.fetchDepth) 145 } 146 core.endGroup() 147 148 // Checkout info 149 core.startGroup('Determining the checkout info') 150 const checkoutInfo = await refHelper.getCheckoutInfo( 151 git, 152 settings.ref, 153 settings.commit 154 ) 155 core.endGroup() 156 157 // LFS fetch 158 // Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time). 159 // Explicit lfs fetch will fetch lfs objects in parallel. 160 if (settings.lfs) { 161 core.startGroup('Fetching LFS objects') 162 await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref) 163 core.endGroup() 164 } 165 166 // Checkout 167 core.startGroup('Checking out the ref') 168 await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint) 169 core.endGroup() 170 171 // Submodules 172 if (settings.submodules) { 173 try { 174 // Temporarily override global config 175 core.startGroup('Setting up auth for fetching submodules') 176 await authHelper.configureGlobalAuth() 177 core.endGroup() 178 179 // Checkout submodules 180 core.startGroup('Fetching submodules') 181 await git.submoduleSync(settings.nestedSubmodules) 182 await git.submoduleUpdate( 183 settings.fetchDepth, 184 settings.nestedSubmodules 185 ) 186 await git.submoduleForeach( 187 'git config --local gc.auto 0', 188 settings.nestedSubmodules 189 ) 190 core.endGroup() 191 192 // Persist credentials 193 if (settings.persistCredentials) { 194 core.startGroup('Persisting credentials for submodules') 195 await authHelper.configureSubmoduleAuth() 196 core.endGroup() 197 } 198 } finally { 199 // Remove temporary global config override 200 await authHelper.removeGlobalAuth() 201 } 202 } 203 204 // Get commit information 205 const commitInfo = await git.log1() 206 207 // Log commit sha 208 await git.log1("--format='%H'") 209 210 // Check for incorrect pull request merge commit 211 await refHelper.checkCommitInfo( 212 settings.authToken, 213 commitInfo, 214 settings.repositoryOwner, 215 settings.repositoryName, 216 settings.ref, 217 settings.commit 218 ) 219 } finally { 220 // Remove auth 221 if (!settings.persistCredentials) { 222 core.startGroup('Removing auth') 223 await authHelper.removeAuth() 224 core.endGroup() 225 } 226 } 227 } 228 229 export async function cleanup(repositoryPath: string): Promise<void> { 230 // Repo exists? 231 if ( 232 !repositoryPath || 233 !fsHelper.fileExistsSync(path.join(repositoryPath, '.git', 'config')) 234 ) { 235 return 236 } 237 238 let git: IGitCommandManager 239 try { 240 git = await gitCommandManager.createCommandManager(repositoryPath, false) 241 } catch { 242 return 243 } 244 245 // Remove auth 246 const authHelper = gitAuthHelper.createAuthHelper(git) 247 await authHelper.removeAuth() 248 } 249 250 async function getGitCommandManager( 251 settings: IGitSourceSettings 252 ): Promise<IGitCommandManager | undefined> { 253 core.info(`Working directory is '${settings.repositoryPath}'`) 254 try { 255 return await gitCommandManager.createCommandManager( 256 settings.repositoryPath, 257 settings.lfs 258 ) 259 } catch (err) { 260 // Git is required for LFS 261 if (settings.lfs) { 262 throw err 263 } 264 265 // Otherwise fallback to REST API 266 return undefined 267 } 268 }