/ src / git-source-provider.ts
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  }