Read-only mirror — the public repositories of the Forgejo instance, refreshed every 10 minutes. The live instance (fg.dieschoenewolke.de) is reachable only over the VPN; clone here with https://git.dieschoenewolke.de/git/<owner>/<repo>.git.
summaryrefslogtreecommitdiff
path: root/src/git-source-provider.ts
blob: fc41fd282ca9c058de4c524346f219530672c9e8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
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
    ))
  ) {
    // Delete the contents of the directory. Don't delete the directory itself
    // since it may be the current working directory.
    core.info(`Deleting the contents of '${settings.repositoryPath}'`)
    for (const file of await fs.promises.readdir(settings.repositoryPath)) {
      await io.rmRF(path.join(settings.repositoryPath, file))
    }
  }

  // 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)
  }
}