mirror of
https://code.forgejo.org/actions/checkout.git
synced 2026-03-08 05:55:49 +00:00
feat: implement reference-cache for faster checkouts
- Add `reference-cache` input to action.yml - Introduce `GitCacheHelper` for bare clone cache management - Prevent race conditions with `proper-lockfile` and atomic directory renames - Support iterative submodule caching and robust relative URL resolution - Append to `info/alternates` preserving existing alternate references - Add fallback to standard clone on submodule cache failure - Add unit tests for `GitCacheHelper` Signed-off-by: Michael Wyraz <mw@brick4u.de>
This commit is contained in:
parent
0c366fd6a8
commit
9ddd3f4b35
16 changed files with 2996 additions and 32 deletions
|
|
@ -160,6 +160,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||||
# running from unless specified. Example URLs are https://github.com or
|
# running from unless specified. Example URLs are https://github.com or
|
||||||
# https://my-ghes-server.example.com
|
# https://my-ghes-server.example.com
|
||||||
github-server-url: ''
|
github-server-url: ''
|
||||||
|
|
||||||
|
# Path to a local directory used as a reference cache for Git clones. Over time,
|
||||||
|
# this directory will contain bare clones of the checked-out repositories (and
|
||||||
|
# their submodules). Using this significantly reduces network bandwidth and speeds
|
||||||
|
# up clones.
|
||||||
|
reference-cache: ''
|
||||||
```
|
```
|
||||||
<!-- end usage -->
|
<!-- end usage -->
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1047,8 +1047,10 @@ async function setup(testName: string): Promise<void> {
|
||||||
lfsFetch: jest.fn(),
|
lfsFetch: jest.fn(),
|
||||||
lfsInstall: jest.fn(),
|
lfsInstall: jest.fn(),
|
||||||
log1: jest.fn(),
|
log1: jest.fn(),
|
||||||
|
referenceAdd: jest.fn(),
|
||||||
remoteAdd: jest.fn(),
|
remoteAdd: jest.fn(),
|
||||||
removeEnvironmentVariable: jest.fn((name: string) => delete git.env[name]),
|
removeEnvironmentVariable: jest.fn((name: string) => delete git.env[name]),
|
||||||
|
execGit: jest.fn(),
|
||||||
revParse: jest.fn(),
|
revParse: jest.fn(),
|
||||||
setEnvironmentVariable: jest.fn((name: string, value: string) => {
|
setEnvironmentVariable: jest.fn((name: string, value: string) => {
|
||||||
git.env[name] = value
|
git.env[name] = value
|
||||||
|
|
@ -1173,7 +1175,8 @@ async function setup(testName: string): Promise<void> {
|
||||||
sshUser: '',
|
sshUser: '',
|
||||||
workflowOrganizationId: 123456,
|
workflowOrganizationId: 123456,
|
||||||
setSafeDirectory: true,
|
setSafeDirectory: true,
|
||||||
githubServerUrl: githubServerUrl
|
githubServerUrl: githubServerUrl,
|
||||||
|
referenceCache: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
109
__test__/git-cache-helper.test.ts
Normal file
109
__test__/git-cache-helper.test.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as io from '@actions/io'
|
||||||
|
import { GitCacheHelper } from '../src/git-cache-helper'
|
||||||
|
import { IGitCommandManager } from '../src/git-command-manager'
|
||||||
|
|
||||||
|
describe('GitCacheHelper', () => {
|
||||||
|
let cacheHelper: GitCacheHelper
|
||||||
|
let mockGit: jest.Mocked<IGitCommandManager>
|
||||||
|
|
||||||
|
const cacheDir = path.join(__dirname, 'test-cache')
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
cacheHelper = new GitCacheHelper(cacheDir)
|
||||||
|
mockGit = {
|
||||||
|
execGit: jest.fn().mockImplementation(async (args) => {
|
||||||
|
// If git clone is called, simulate creating the destination dir
|
||||||
|
if (args && args.includes('clone')) {
|
||||||
|
const dest = args.find((a: string) => a.includes('.tmp.'));
|
||||||
|
if (dest) {
|
||||||
|
await io.mkdirP(dest);
|
||||||
|
} else {
|
||||||
|
console.log('No .tmp. found in args:', args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { exitCode: 0, stdout: '', stderr: '' };
|
||||||
|
}),
|
||||||
|
gitEnv: {}
|
||||||
|
} as any
|
||||||
|
|
||||||
|
await io.mkdirP(cacheDir)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await io.rmRF(cacheDir)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('generates a consistent, short, and safe cache directory name', () => {
|
||||||
|
const url1 = 'https://github.com/mwyraz/forgejo-actions-checkout.git'
|
||||||
|
const name1 = (cacheHelper as any).generateCacheDirName(url1)
|
||||||
|
|
||||||
|
// Check structure: safe string + hash
|
||||||
|
expect(name1).toMatch(/^https___github_com_mwyraz_forgejo_actions_checkout_git_[0-9a-f]{8}\.git$/)
|
||||||
|
|
||||||
|
// Same URL should produce the same directory name
|
||||||
|
const url1_duplicate = 'https://github.com/mwyraz/forgejo-actions-checkout.git'
|
||||||
|
expect((cacheHelper as any).generateCacheDirName(url1_duplicate)).toBe(name1)
|
||||||
|
|
||||||
|
// Different URL should produce a different directory name
|
||||||
|
const url2 = 'https://github.com/mwyraz/forgejo-actions-checkout-other.git'
|
||||||
|
expect((cacheHelper as any).generateCacheDirName(url2)).not.toBe(name1)
|
||||||
|
|
||||||
|
// SSH URL
|
||||||
|
const url3 = 'git@github.com:auth/repo.git'
|
||||||
|
const name3 = (cacheHelper as any).generateCacheDirName(url3)
|
||||||
|
expect(name3).toMatch(/^git_github_com_auth_repo_git_[0-9a-f]{8}\.git$/)
|
||||||
|
|
||||||
|
// Unclean URLs
|
||||||
|
const url4 = 'https://github.com/foo/bar.git?v=1'
|
||||||
|
const name4 = (cacheHelper as any).generateCacheDirName(url4)
|
||||||
|
expect(name4).toMatch(/^https___github_com_foo_bar_git_v_1_[0-9a-f]{8}\.git$/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets up a cache directory if it does not exist', async () => {
|
||||||
|
const repositoryUrl = 'https://github.com/mwyraz/test-repo.git'
|
||||||
|
const resultPath = await cacheHelper.setupCache(mockGit, repositoryUrl)
|
||||||
|
|
||||||
|
const expectedName = (cacheHelper as any).generateCacheDirName(repositoryUrl)
|
||||||
|
expect(resultPath).toBe(path.join(cacheDir, expectedName))
|
||||||
|
|
||||||
|
// It should have executed git clone --bare
|
||||||
|
expect(mockGit.execGit).toHaveBeenCalledWith(
|
||||||
|
expect.arrayContaining([
|
||||||
|
'-C',
|
||||||
|
cacheDir,
|
||||||
|
'clone',
|
||||||
|
'--bare',
|
||||||
|
repositoryUrl,
|
||||||
|
expect.stringContaining(`${expectedName}.tmp`) // should use tmp dir
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fetches updates if the cache directory already exists', async () => {
|
||||||
|
const repositoryUrl = 'https://github.com/mwyraz/existing-repo.git'
|
||||||
|
const expectedName = (cacheHelper as any).generateCacheDirName(repositoryUrl)
|
||||||
|
const fixedPath = path.join(cacheDir, expectedName)
|
||||||
|
|
||||||
|
// Fake existing directory
|
||||||
|
await io.mkdirP(path.join(fixedPath, 'objects'))
|
||||||
|
|
||||||
|
const resultPath = await cacheHelper.setupCache(mockGit, repositoryUrl)
|
||||||
|
expect(resultPath).toBe(fixedPath)
|
||||||
|
|
||||||
|
// It should have executed git fetch
|
||||||
|
expect(mockGit.execGit).toHaveBeenCalledWith(
|
||||||
|
expect.arrayContaining([
|
||||||
|
'-C',
|
||||||
|
fixedPath,
|
||||||
|
'fetch',
|
||||||
|
'--force',
|
||||||
|
'--prune',
|
||||||
|
'--tags',
|
||||||
|
'origin',
|
||||||
|
'+refs/heads/*:refs/heads/*'
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -478,8 +478,10 @@ async function setup(testName: string): Promise<void> {
|
||||||
lfsFetch: jest.fn(),
|
lfsFetch: jest.fn(),
|
||||||
lfsInstall: jest.fn(),
|
lfsInstall: jest.fn(),
|
||||||
log1: jest.fn(),
|
log1: jest.fn(),
|
||||||
|
referenceAdd: jest.fn(),
|
||||||
remoteAdd: jest.fn(),
|
remoteAdd: jest.fn(),
|
||||||
removeEnvironmentVariable: jest.fn(),
|
removeEnvironmentVariable: jest.fn(),
|
||||||
|
execGit: jest.fn(),
|
||||||
revParse: jest.fn(),
|
revParse: jest.fn(),
|
||||||
setEnvironmentVariable: jest.fn(),
|
setEnvironmentVariable: jest.fn(),
|
||||||
shaExists: jest.fn(),
|
shaExists: jest.fn(),
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ describe('input-helper tests', () => {
|
||||||
expect(settings.repositoryOwner).toBe('some-owner')
|
expect(settings.repositoryOwner).toBe('some-owner')
|
||||||
expect(settings.repositoryPath).toBe(gitHubWorkspace)
|
expect(settings.repositoryPath).toBe(gitHubWorkspace)
|
||||||
expect(settings.setSafeDirectory).toBe(true)
|
expect(settings.setSafeDirectory).toBe(true)
|
||||||
|
expect(settings.referenceCache || '').toBe('')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('qualifies ref', async () => {
|
it('qualifies ref', async () => {
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,12 @@ inputs:
|
||||||
github-server-url:
|
github-server-url:
|
||||||
description: The base URL for the GitHub instance that you are trying to clone from, will use environment defaults to fetch from the same instance that the workflow is running from unless specified. Example URLs are https://github.com or https://my-ghes-server.example.com
|
description: The base URL for the GitHub instance that you are trying to clone from, will use environment defaults to fetch from the same instance that the workflow is running from unless specified. Example URLs are https://github.com or https://my-ghes-server.example.com
|
||||||
required: false
|
required: false
|
||||||
|
reference-cache:
|
||||||
|
description: >
|
||||||
|
Path to a local directory used as a reference cache for Git clones. Over time,
|
||||||
|
this directory will contain bare clones of the checked-out repositories (and their submodules).
|
||||||
|
Using this significantly reduces network bandwidth and speeds up clones.
|
||||||
|
required: false
|
||||||
outputs:
|
outputs:
|
||||||
ref:
|
ref:
|
||||||
description: 'The branch, tag or SHA that was checked out'
|
description: 'The branch, tag or SHA that was checked out'
|
||||||
|
|
|
||||||
37
adrs/2303-reference-cache.md
Normal file
37
adrs/2303-reference-cache.md
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Reference Cache für schnelle Checkouts
|
||||||
|
|
||||||
|
## Zusammenfassung
|
||||||
|
Einführung eines lokal verwalteten Git-Referenz-Caches für Haupt-Repositories und Submodule, um Netzwerk-Traffic und Checkout-Zeiten auf persistenten Runnern (z.B. Self-Hosted) massiv zu reduzieren.
|
||||||
|
|
||||||
|
## Implementierungsplan
|
||||||
|
|
||||||
|
1. **Inputs:**
|
||||||
|
- In `action.yml` einen neuen Input `reference-cache` (Pfad zum Cache-Verzeichnis) hinzufügen. Default ist leer.
|
||||||
|
- In `src/git-source-settings.ts` und `src/input-helper.ts` den Input auslesen und bereitstellen (`settings.referenceCache`).
|
||||||
|
|
||||||
|
2. **Cache Manager (`src/git-cache-helper.ts`):**
|
||||||
|
- Eine neue Klasse/Helper-Logik, die das Erstellen (`git clone --bare`) und Aktualisieren (`git fetch --force`) von Bare Cache-Repos übernimmt.
|
||||||
|
- **Namenskonvention Cache-Verzeichnis:** Damit Admin-Lesbarkeit und Kollisionsfreiheit gewährleistet sind, wird das Cache-Verzeichnis aus der Repository-URL gebildet:
|
||||||
|
- Alle Sonderzeichen in der URL durch `_` ersetzen.
|
||||||
|
- Ein kurzer Hash (z. B. erste 8 Zeichen des SHA256) der echten URL zur Eindeutigkeit anhängen.
|
||||||
|
- Beispiel: `<reference-cache>/https___github_com_actions_checkout_8f9b1c2a.git`
|
||||||
|
|
||||||
|
3. **Haupt-Repo Checkout (`src/git-source-provider.ts`):**
|
||||||
|
- Vor dem Setup des Checkouts prüfen, ob `reference-cache` gesetzt ist.
|
||||||
|
- Wenn ja: den Cache-Ordner für die Haupt-URL aktualisieren/anlegen.
|
||||||
|
- Nach dem initialen `git.init()` den Pfad in `.git/objects/info/alternates` schreiben, der auf das `objects`-Verzeichnis des Cache-Ordners zeigt.
|
||||||
|
|
||||||
|
4. **Submodule Checkouts (Iterativ statt monolithisch):**
|
||||||
|
- Der aktuelle Befehl `git submodule update --recursive` funktioniert nicht out-of-the-box mit `reference`, wenn jedes Submodul seinen individuellen Referenz-Cache benötigt.
|
||||||
|
- Wenn `reference-cache` aktiv ist und Submodule initialisiert werden sollen:
|
||||||
|
- Lese `.gitmodules` aus (alle Sub-URLs ermitteln).
|
||||||
|
- Für jedes Submodul den Cache (genauso wie in Step 2) anlegen oder aktualisieren.
|
||||||
|
- Submodul einzeln auschecken per `git submodule update --init --reference <cache-pfad/.git> <pfad>`.
|
||||||
|
- Bei der Einstellung `recursive`: In jedes Submodul-Verzeichnis wechseln und den Vorgang für `.gitmodules` rekursiv auf Skript-Ebene durchführen (anstatt Git's `--recursive` Flag einfach weiterzugeben).
|
||||||
|
|
||||||
|
## Akzeptanzkriterien
|
||||||
|
1. **Neue Option konfigurierbar**: Der Input `reference-cache` kann übergeben werden, der Code reagiert darauf.
|
||||||
|
2. **Ordnerstruktur korrekt**: Der Cache-Ordner für das Hauptrepo und Submodule erhält Namen nach der "URL_Sonderzeichen_Ersetzt+SHA_Cut"-Logik.
|
||||||
|
3. **Bandbreite gespart / Alternates genutzt**: Beim Hauptcheckout wird eine `.git/objects/info/alternates`-Datei mit Pfad zum lokalen Cache erzeugt. Danach ausgeführte `git fetch`-Befehle sind signifikant schneller bzw. laden deutlich weniger Bytes herunter.
|
||||||
|
4. **Submodule erhalten Caches**: Auch tiefe (rekursive) Submodule profitieren für deren jeweilige Remote-URL vom Cache, da pro Submodul ein passender `--reference` Punkt dynamisch berechnet und übergeben wird.
|
||||||
|
5. **Kein --dissociate**: Aus Performance-Gründen bleibt der Arbeitsordner an den Cache gebunden (`git repack` ist zeitaufwändig). Fällt der Cache weg, muss der Workspace erst einmal neu erzeugt werden (was bei Action Runnern die Norm ist, falls es nicht ohnehin "single-use" Runner sind).
|
||||||
2446
dist/index.js
vendored
2446
dist/index.js
vendored
File diff suppressed because it is too large
Load diff
41
package-lock.json
generated
41
package-lock.json
generated
|
|
@ -19,6 +19,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "^24.1.0",
|
"@types/node": "^24.1.0",
|
||||||
|
"@types/proper-lockfile": "^4.1.4",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.9.0",
|
"@typescript-eslint/eslint-plugin": "^7.9.0",
|
||||||
"@typescript-eslint/parser": "^7.9.0",
|
"@typescript-eslint/parser": "^7.9.0",
|
||||||
|
|
@ -30,6 +31,7 @@
|
||||||
"jest-circus": "^29.7.0",
|
"jest-circus": "^29.7.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
|
"proper-lockfile": "^4.1.2",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
}
|
}
|
||||||
|
|
@ -1523,6 +1525,23 @@
|
||||||
"undici-types": "~7.8.0"
|
"undici-types": "~7.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/proper-lockfile": {
|
||||||
|
"version": "4.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.4.tgz",
|
||||||
|
"integrity": "sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/retry": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/retry": {
|
||||||
|
"version": "0.12.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz",
|
||||||
|
"integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/stack-utils": {
|
"node_modules/@types/stack-utils": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
|
||||||
|
|
@ -5980,6 +5999,18 @@
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proper-lockfile": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.4",
|
||||||
|
"retry": "^0.12.0",
|
||||||
|
"signal-exit": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
|
@ -6135,6 +6166,16 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/retry": {
|
||||||
|
"version": "0.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||||
|
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reusify": {
|
"node_modules/reusify": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "^24.1.0",
|
"@types/node": "^24.1.0",
|
||||||
|
"@types/proper-lockfile": "^4.1.4",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.9.0",
|
"@typescript-eslint/eslint-plugin": "^7.9.0",
|
||||||
"@typescript-eslint/parser": "^7.9.0",
|
"@typescript-eslint/parser": "^7.9.0",
|
||||||
|
|
@ -49,6 +50,7 @@
|
||||||
"jest-circus": "^29.7.0",
|
"jest-circus": "^29.7.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
|
"proper-lockfile": "^4.1.2",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export interface IGitAuthHelper {
|
||||||
configureSubmoduleAuth(): Promise<void>
|
configureSubmoduleAuth(): Promise<void>
|
||||||
configureTempGlobalConfig(): Promise<string>
|
configureTempGlobalConfig(): Promise<string>
|
||||||
removeAuth(): Promise<void>
|
removeAuth(): Promise<void>
|
||||||
|
removeGlobalAuth(): Promise<void>
|
||||||
removeGlobalConfig(): Promise<void>
|
removeGlobalConfig(): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,6 +236,12 @@ class GitAuthHelper {
|
||||||
await this.removeToken()
|
await this.removeToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeGlobalAuth(): Promise<void> {
|
||||||
|
core.debug('Removing global auth entries')
|
||||||
|
await this.git.tryConfigUnset('include.path', true)
|
||||||
|
await this.git.tryConfigUnset(this.insteadOfKey, true)
|
||||||
|
}
|
||||||
|
|
||||||
async removeGlobalConfig(): Promise<void> {
|
async removeGlobalConfig(): Promise<void> {
|
||||||
if (this.temporaryHomePath?.length > 0) {
|
if (this.temporaryHomePath?.length > 0) {
|
||||||
core.debug(`Unsetting HOME override`)
|
core.debug(`Unsetting HOME override`)
|
||||||
|
|
|
||||||
98
src/git-cache-helper.ts
Normal file
98
src/git-cache-helper.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as crypto from 'crypto'
|
||||||
|
import * as lockfile from 'proper-lockfile'
|
||||||
|
import {IGitCommandManager} from './git-command-manager'
|
||||||
|
|
||||||
|
export class GitCacheHelper {
|
||||||
|
constructor(private referenceCache: string) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares the reference cache for a given repository URL.
|
||||||
|
* If the cache does not exist, it performs a bare clone.
|
||||||
|
* If it exists, it performs a fetch to update it.
|
||||||
|
* Returns the absolute path to the bare cache repository.
|
||||||
|
*/
|
||||||
|
async setupCache(git: IGitCommandManager, repositoryUrl: string): Promise<string> {
|
||||||
|
const cacheDirName = this.generateCacheDirName(repositoryUrl)
|
||||||
|
const cachePath = path.join(this.referenceCache, cacheDirName)
|
||||||
|
|
||||||
|
// Ensure the base cache directory exists before we try to lock inside it
|
||||||
|
if (!fs.existsSync(this.referenceCache)) {
|
||||||
|
await fs.promises.mkdir(this.referenceCache, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use a dedicated lock dir specifically for this repository's cache
|
||||||
|
// since we cannot place a lock *inside* a repository that might not exist yet
|
||||||
|
const lockfilePath = `${cachePath}.lock`
|
||||||
|
|
||||||
|
// Ensure the file we are locking exists
|
||||||
|
if (!fs.existsSync(lockfilePath)) {
|
||||||
|
await fs.promises.writeFile(lockfilePath, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
core.debug(`Acquiring lock for ${repositoryUrl} at ${lockfilePath}`)
|
||||||
|
|
||||||
|
let releaseLock: () => Promise<void>
|
||||||
|
try {
|
||||||
|
// proper-lockfile creates a ".lock" directory next to the target file.
|
||||||
|
// We configure it to wait up to 10 minutes (600,000 ms) for another process to finish.
|
||||||
|
// E.g. cloning a very large monorepo might take minutes.
|
||||||
|
releaseLock = await lockfile.lock(lockfilePath, {
|
||||||
|
retries: {
|
||||||
|
retries: 60, // try 60 times
|
||||||
|
factor: 1, // linear backoff
|
||||||
|
minTimeout: 10000, // wait 10 seconds between tries
|
||||||
|
maxTimeout: 10000, // (total max wait time: 600s = 10m)
|
||||||
|
randomize: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
core.debug(`Lock acquired.`)
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Failed to acquire lock for repository cache ${repositoryUrl}: ${err}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(path.join(cachePath, 'objects'))) {
|
||||||
|
core.info(`Reference cache for ${repositoryUrl} exists. Updating...`)
|
||||||
|
const args = ['-C', cachePath, 'fetch', '--force', '--prune', '--tags', 'origin', '+refs/heads/*:refs/heads/*']
|
||||||
|
await git.execGit(args)
|
||||||
|
} else {
|
||||||
|
core.info(`Reference cache for ${repositoryUrl} does not exist. Cloning --bare...`)
|
||||||
|
|
||||||
|
// Use a temporary clone pattern to prevent corrupted repos if process is killed mid-clone
|
||||||
|
const tmpPath = `${cachePath}.tmp.${crypto.randomUUID()}`
|
||||||
|
try {
|
||||||
|
const args = ['-C', this.referenceCache, 'clone', '--bare', repositoryUrl, tmpPath]
|
||||||
|
await git.execGit(args)
|
||||||
|
|
||||||
|
if (fs.existsSync(cachePath)) {
|
||||||
|
// In rare cases where it somehow exists but objects/ didn't, clean it up
|
||||||
|
await fs.promises.rm(cachePath, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
await fs.promises.rename(tmpPath, cachePath)
|
||||||
|
} catch (cloneErr) {
|
||||||
|
// Cleanup partial clone if an error occurred
|
||||||
|
await fs.promises.rm(tmpPath, { recursive: true, force: true }).catch(() => {})
|
||||||
|
throw cloneErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await releaseLock()
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachePath
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a directory name for the cache based on the URL.
|
||||||
|
* Replaces non-alphanumeric characters with underscores
|
||||||
|
* and appends a short SHA256 hash of the original URL.
|
||||||
|
*/
|
||||||
|
generateCacheDirName(url: string): string {
|
||||||
|
const cleanUrl = url.replace(/[^a-zA-Z0-9]/g, '_')
|
||||||
|
const hash = crypto.createHash('sha256').update(url).digest('hex').substring(0, 8)
|
||||||
|
return `${cleanUrl}_${hash}.git`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,11 @@ import {GitVersion} from './git-version'
|
||||||
export const MinimumGitVersion = new GitVersion('2.18')
|
export const MinimumGitVersion = new GitVersion('2.18')
|
||||||
export const MinimumGitSparseCheckoutVersion = new GitVersion('2.28')
|
export const MinimumGitSparseCheckoutVersion = new GitVersion('2.28')
|
||||||
|
|
||||||
|
export class GitOutput {
|
||||||
|
stdout = ''
|
||||||
|
exitCode = 0
|
||||||
|
}
|
||||||
|
|
||||||
export interface IGitCommandManager {
|
export interface IGitCommandManager {
|
||||||
branchDelete(remote: boolean, branch: string): Promise<void>
|
branchDelete(remote: boolean, branch: string): Promise<void>
|
||||||
branchExists(remote: boolean, pattern: string): Promise<boolean>
|
branchExists(remote: boolean, pattern: string): Promise<boolean>
|
||||||
|
|
@ -48,6 +53,7 @@ export interface IGitCommandManager {
|
||||||
lfsFetch(ref: string): Promise<void>
|
lfsFetch(ref: string): Promise<void>
|
||||||
lfsInstall(): Promise<void>
|
lfsInstall(): Promise<void>
|
||||||
log1(format?: string): Promise<string>
|
log1(format?: string): Promise<string>
|
||||||
|
referenceAdd(referenceObjects: string): Promise<void>
|
||||||
remoteAdd(remoteName: string, remoteUrl: string): Promise<void>
|
remoteAdd(remoteName: string, remoteUrl: string): Promise<void>
|
||||||
removeEnvironmentVariable(name: string): void
|
removeEnvironmentVariable(name: string): void
|
||||||
revParse(ref: string): Promise<string>
|
revParse(ref: string): Promise<string>
|
||||||
|
|
@ -80,6 +86,12 @@ export interface IGitCommandManager {
|
||||||
): Promise<string[]>
|
): Promise<string[]>
|
||||||
tryReset(): Promise<boolean>
|
tryReset(): Promise<boolean>
|
||||||
version(): Promise<GitVersion>
|
version(): Promise<GitVersion>
|
||||||
|
execGit(
|
||||||
|
args: string[],
|
||||||
|
allowAllExitCodes?: boolean,
|
||||||
|
silent?: boolean,
|
||||||
|
customListeners?: any
|
||||||
|
): Promise<GitOutput>
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createCommandManager(
|
export async function createCommandManager(
|
||||||
|
|
@ -401,6 +413,32 @@ class GitCommandManager {
|
||||||
await this.execGit(['remote', 'add', remoteName, remoteUrl])
|
await this.execGit(['remote', 'add', remoteName, remoteUrl])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async referenceAdd(referenceObjects: string): Promise<void> {
|
||||||
|
const alternatesPath = path.join(
|
||||||
|
this.workingDirectory,
|
||||||
|
'.git',
|
||||||
|
'objects',
|
||||||
|
'info',
|
||||||
|
'alternates'
|
||||||
|
)
|
||||||
|
core.info(`Configuring git alternate to reference objects at ${referenceObjects}`)
|
||||||
|
const infoDir = path.dirname(alternatesPath)
|
||||||
|
if (!fs.existsSync(infoDir)) {
|
||||||
|
await fs.promises.mkdir(infoDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
let existing = ''
|
||||||
|
if (fs.existsSync(alternatesPath)) {
|
||||||
|
existing = (await fs.promises.readFile(alternatesPath, 'utf8')).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = existing ? existing.split('\n') : []
|
||||||
|
if (!lines.includes(referenceObjects)) {
|
||||||
|
lines.push(referenceObjects)
|
||||||
|
await fs.promises.writeFile(alternatesPath, lines.join('\n') + '\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
removeEnvironmentVariable(name: string): void {
|
removeEnvironmentVariable(name: string): void {
|
||||||
delete this.gitEnv[name]
|
delete this.gitEnv[name]
|
||||||
}
|
}
|
||||||
|
|
@ -609,7 +647,7 @@ class GitCommandManager {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private async execGit(
|
async execGit(
|
||||||
args: string[],
|
args: string[],
|
||||||
allowAllExitCodes = false,
|
allowAllExitCodes = false,
|
||||||
silent = false,
|
silent = false,
|
||||||
|
|
@ -746,7 +784,3 @@ class GitCommandManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GitOutput {
|
|
||||||
stdout = ''
|
|
||||||
exitCode = 0
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,156 @@ import {
|
||||||
IGitCommandManager
|
IGitCommandManager
|
||||||
} from './git-command-manager'
|
} from './git-command-manager'
|
||||||
import {IGitSourceSettings} from './git-source-settings'
|
import {IGitSourceSettings} from './git-source-settings'
|
||||||
|
import {GitCacheHelper} from './git-cache-helper'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
|
||||||
|
interface SubmoduleInfo {
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function iterativeSubmoduleUpdate(
|
||||||
|
git: IGitCommandManager,
|
||||||
|
cacheHelper: GitCacheHelper,
|
||||||
|
repositoryPath: string,
|
||||||
|
fetchDepth: number,
|
||||||
|
nestedSubmodules: boolean
|
||||||
|
): Promise<void> {
|
||||||
|
const gitmodulesPath = path.join(repositoryPath, '.gitmodules')
|
||||||
|
if (!fs.existsSync(gitmodulesPath)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const submodules = new Map<string, SubmoduleInfo>()
|
||||||
|
|
||||||
|
// Get all submodule config keys
|
||||||
|
try {
|
||||||
|
const output = await git.execGit([
|
||||||
|
'-C', repositoryPath,
|
||||||
|
'config', '--file', gitmodulesPath, '--get-regexp', 'submodule\\..*'
|
||||||
|
], true, true)
|
||||||
|
|
||||||
|
const lines = output.stdout.split('\n').filter(l => l.trim().length > 0)
|
||||||
|
for (const line of lines) {
|
||||||
|
const match = line.match(/^submodule\.(.+?)\.(path|url)\s+(.*)$/)
|
||||||
|
if (match) {
|
||||||
|
const [, name, key, value] = match
|
||||||
|
if (!submodules.has(name)) {
|
||||||
|
submodules.set(name, { name, path: '', url: '' })
|
||||||
|
}
|
||||||
|
const info = submodules.get(name)!
|
||||||
|
if (key === 'path') info.path = value
|
||||||
|
if (key === 'url') info.url = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
core.warning(`Failed to read .gitmodules: ${err}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const info of submodules.values()) {
|
||||||
|
if (!info.path || !info.url) continue
|
||||||
|
|
||||||
|
core.info(`Processing submodule ${info.name} at ${info.path}`)
|
||||||
|
|
||||||
|
// Resolve relative URLs or valid URLs
|
||||||
|
let subUrl = info.url
|
||||||
|
if (subUrl.startsWith('../') || subUrl.startsWith('./')) {
|
||||||
|
// In checkout action, relative URLs are handled automatically by git.
|
||||||
|
// But for our bare cache clone, we need an absolute URL.
|
||||||
|
let originUrl = ''
|
||||||
|
try {
|
||||||
|
const originOut = await git.execGit(['-C', repositoryPath, 'remote', 'get-url', 'origin'], true, true)
|
||||||
|
if (originOut.exitCode === 0) {
|
||||||
|
originUrl = originOut.stdout.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originUrl) {
|
||||||
|
try {
|
||||||
|
if (originUrl.match(/^https?:\/\//)) {
|
||||||
|
// Using Node's URL class to resolve relative paths for HTTP(s)
|
||||||
|
const parsedOrigin = new URL(originUrl.replace(/\.git$/, ''))
|
||||||
|
const resolvedUrl = new URL(subUrl, parsedOrigin.href + '/')
|
||||||
|
subUrl = resolvedUrl.href
|
||||||
|
} else {
|
||||||
|
// Fallback for SSH URLs which new URL() cannot parse (e.g. git@github.com:org/repo)
|
||||||
|
let originParts = originUrl.replace(/\.git$/, '').split('/')
|
||||||
|
originParts.pop() // remove current repo
|
||||||
|
|
||||||
|
// Handle multiple ../
|
||||||
|
let subTarget = subUrl
|
||||||
|
while (subTarget.startsWith('../')) {
|
||||||
|
if (originParts.length === 0) break // Can't go higher
|
||||||
|
originParts.pop()
|
||||||
|
subTarget = subTarget.substring(3)
|
||||||
|
}
|
||||||
|
if (subTarget.startsWith('./')) {
|
||||||
|
subTarget = subTarget.substring(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originParts.length > 0) {
|
||||||
|
subUrl = originParts.join('/') + '/' + subTarget
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fallback does not work
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subUrl || subUrl.startsWith('../') || subUrl.startsWith('./')) {
|
||||||
|
core.warning(`Could not resolve absolute URL for submodule ${info.name}. Falling back to standard clone.`)
|
||||||
|
await invokeStandardSubmoduleUpdate(git, repositoryPath, fetchDepth, info.path)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prepare cache
|
||||||
|
const cachePath = await cacheHelper.setupCache(git, subUrl)
|
||||||
|
|
||||||
|
// Submodule update for this specific one
|
||||||
|
const args = ['-C', repositoryPath, '-c', 'protocol.version=2', 'submodule', 'update', '--init', '--force']
|
||||||
|
if (fetchDepth > 0) {
|
||||||
|
args.push(`--depth=${fetchDepth}`)
|
||||||
|
}
|
||||||
|
args.push('--reference', cachePath)
|
||||||
|
args.push(info.path)
|
||||||
|
|
||||||
|
const output = await git.execGit(args, true)
|
||||||
|
if (output.exitCode !== 0) {
|
||||||
|
throw new Error(`Submodule update failed with exit code ${output.exitCode}`)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
core.warning(`Reference cache failed for submodule ${info.name} (${err}). Falling back to standard clone...`)
|
||||||
|
await invokeStandardSubmoduleUpdate(git, repositoryPath, fetchDepth, info.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursive update inside the submodule
|
||||||
|
if (nestedSubmodules) {
|
||||||
|
const subRepoPath = path.join(repositoryPath, info.path)
|
||||||
|
await iterativeSubmoduleUpdate(
|
||||||
|
git,
|
||||||
|
cacheHelper,
|
||||||
|
subRepoPath,
|
||||||
|
fetchDepth,
|
||||||
|
nestedSubmodules
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function invokeStandardSubmoduleUpdate(git: IGitCommandManager, repositoryPath: string, fetchDepth: number, submodulePath: string) {
|
||||||
|
const args = ['-C', repositoryPath, '-c', 'protocol.version=2', 'submodule', 'update', '--init', '--force']
|
||||||
|
if (fetchDepth > 0) {
|
||||||
|
args.push(`--depth=${fetchDepth}`)
|
||||||
|
}
|
||||||
|
args.push(submodulePath)
|
||||||
|
await git.execGit(args)
|
||||||
|
}
|
||||||
|
|
||||||
export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||||
// Repository URL
|
// Repository URL
|
||||||
|
|
@ -105,6 +255,19 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||||
// Save state for POST action
|
// Save state for POST action
|
||||||
stateHelper.setRepositoryPath(settings.repositoryPath)
|
stateHelper.setRepositoryPath(settings.repositoryPath)
|
||||||
|
|
||||||
|
// If we didn't initialize it above, do it now
|
||||||
|
if (!authHelper) {
|
||||||
|
authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we need global auth setup early for reference cache
|
||||||
|
// Global auth does not require a local .git directory
|
||||||
|
if (settings.referenceCache) {
|
||||||
|
core.startGroup('Setting up global auth for reference cache')
|
||||||
|
await authHelper.configureGlobalAuth()
|
||||||
|
core.endGroup()
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the repository
|
// Initialize the repository
|
||||||
if (
|
if (
|
||||||
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
||||||
|
|
@ -113,8 +276,35 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||||
await git.init()
|
await git.init()
|
||||||
await git.remoteAdd('origin', repositoryUrl)
|
await git.remoteAdd('origin', repositoryUrl)
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
|
|
||||||
|
// Setup reference cache if requested
|
||||||
|
if (settings.referenceCache) {
|
||||||
|
core.startGroup('Setting up reference repository cache')
|
||||||
|
const cacheHelper = new GitCacheHelper(settings.referenceCache)
|
||||||
|
const cachePath = await cacheHelper.setupCache(git, repositoryUrl)
|
||||||
|
const cacheObjects = path.join(cachePath, 'objects')
|
||||||
|
if (fsHelper.directoryExistsSync(cacheObjects, false)) {
|
||||||
|
await git.referenceAdd(cacheObjects)
|
||||||
|
} else {
|
||||||
|
core.warning(`Reference repository cache objects directory ${cacheObjects} does not exist`)
|
||||||
|
}
|
||||||
|
core.endGroup()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove global auth if it was set for reference cache,
|
||||||
|
// to avoid duplicate AUTHORIZATION headers during fetch
|
||||||
|
if (settings.referenceCache) {
|
||||||
|
core.startGroup('Removing global auth after reference cache setup')
|
||||||
|
await authHelper.removeGlobalAuth()
|
||||||
|
core.endGroup()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure auth (must happen after git init so .git exists)
|
||||||
|
core.startGroup('Setting up auth')
|
||||||
|
await authHelper.configureAuth()
|
||||||
|
core.endGroup()
|
||||||
|
|
||||||
// Disable automatic garbage collection
|
// Disable automatic garbage collection
|
||||||
core.startGroup('Disabling automatic garbage collection')
|
core.startGroup('Disabling automatic garbage collection')
|
||||||
if (!(await git.tryDisableAutomaticGarbageCollection())) {
|
if (!(await git.tryDisableAutomaticGarbageCollection())) {
|
||||||
|
|
@ -124,15 +314,6 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||||
}
|
}
|
||||||
core.endGroup()
|
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
|
// Determine the default branch
|
||||||
if (!settings.ref && !settings.commit) {
|
if (!settings.ref && !settings.commit) {
|
||||||
core.startGroup('Determining the default branch')
|
core.startGroup('Determining the default branch')
|
||||||
|
|
@ -264,7 +445,21 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||||
// Checkout submodules
|
// Checkout submodules
|
||||||
core.startGroup('Fetching submodules')
|
core.startGroup('Fetching submodules')
|
||||||
await git.submoduleSync(settings.nestedSubmodules)
|
await git.submoduleSync(settings.nestedSubmodules)
|
||||||
await git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules)
|
|
||||||
|
if (settings.referenceCache) {
|
||||||
|
core.info('Iterative submodule update using reference cache')
|
||||||
|
const cacheHelper = new GitCacheHelper(settings.referenceCache)
|
||||||
|
await iterativeSubmoduleUpdate(
|
||||||
|
git,
|
||||||
|
cacheHelper,
|
||||||
|
settings.repositoryPath,
|
||||||
|
settings.fetchDepth,
|
||||||
|
settings.nestedSubmodules
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
await git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules)
|
||||||
|
}
|
||||||
|
|
||||||
await git.submoduleForeach(
|
await git.submoduleForeach(
|
||||||
'git config --local gc.auto 0',
|
'git config --local gc.auto 0',
|
||||||
settings.nestedSubmodules
|
settings.nestedSubmodules
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,11 @@ export interface IGitSourceSettings {
|
||||||
*/
|
*/
|
||||||
showProgress: boolean
|
showProgress: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The path to a local directory used as a reference cache for Git clones
|
||||||
|
*/
|
||||||
|
referenceCache: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates whether to fetch LFS objects
|
* Indicates whether to fetch LFS objects
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -161,5 +161,9 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
||||||
result.githubServerUrl = core.getInput('github-server-url')
|
result.githubServerUrl = core.getInput('github-server-url')
|
||||||
core.debug(`GitHub Host URL = ${result.githubServerUrl}`)
|
core.debug(`GitHub Host URL = ${result.githubServerUrl}`)
|
||||||
|
|
||||||
|
// Reference Cache
|
||||||
|
result.referenceCache = core.getInput('reference-cache')
|
||||||
|
core.debug(`Reference Cache = ${result.referenceCache}`)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue