diff --git a/__test__/input-helper.test.ts b/__test__/input-helper.test.ts index 9514cb4..ec449ec 100644 --- a/__test__/input-helper.test.ts +++ b/__test__/input-helper.test.ts @@ -22,6 +22,11 @@ describe('input-helper tests', () => { return inputs[name] }) + // Mock getMultilineInput + jest.spyOn(core, 'getMultilineInput').mockImplementation((name: string) => { + return inputs[name] ? inputs[name].split('\n') : [] + }) + // Mock error/warning/info/debug jest.spyOn(core, 'error').mockImplementation(jest.fn()) jest.spyOn(core, 'warning').mockImplementation(jest.fn()) @@ -144,4 +149,59 @@ describe('input-helper tests', () => { const settings: IGitSourceSettings = await inputHelper.getInputs() expect(settings.workflowOrganizationId).toBe(123456) }) + + it('sets submodules to false by default', async () => { + const settings: IGitSourceSettings = await inputHelper.getInputs() + expect(settings.submodules).toBe(false) + expect(settings.nestedSubmodules).toBe(false) + expect(settings.specificSubmodules).toEqual([]) + }) + + it('sets submodules to true when input is true', async () => { + inputs.submodules = 'true' + const settings: IGitSourceSettings = await inputHelper.getInputs() + expect(settings.submodules).toBe(true) + expect(settings.nestedSubmodules).toBe(false) + expect(settings.specificSubmodules).toEqual([]) + }) + + it('sets submodules to recursive when input is recursive', async () => { + inputs.submodules = 'recursive' + const settings: IGitSourceSettings = await inputHelper.getInputs() + expect(settings.submodules).toBe(true) + expect(settings.nestedSubmodules).toBe(true) + expect(settings.specificSubmodules).toEqual([]) + }) + + it('parses comma-separated specific submodules', async () => { + inputs.submodules = 'submodule1,submodule2,submodule3' + const settings: IGitSourceSettings = await inputHelper.getInputs() + expect(settings.submodules).toBe(true) + expect(settings.nestedSubmodules).toBe(false) + expect(settings.specificSubmodules).toEqual(['submodule1', 'submodule2', 'submodule3']) + }) + + it('handles whitespace in specific submodules list', async () => { + inputs.submodules = ' submodule1 , submodule2 , submodule3 ' + const settings: IGitSourceSettings = await inputHelper.getInputs() + expect(settings.submodules).toBe(true) + expect(settings.nestedSubmodules).toBe(false) + expect(settings.specificSubmodules).toEqual(['submodule1', 'submodule2', 'submodule3']) + }) + + it('filters empty submodule names', async () => { + inputs.submodules = 'submodule1,,submodule2,' + const settings: IGitSourceSettings = await inputHelper.getInputs() + expect(settings.submodules).toBe(true) + expect(settings.nestedSubmodules).toBe(false) + expect(settings.specificSubmodules).toEqual(['submodule1', 'submodule2']) + }) + + it('handles single specific submodule', async () => { + inputs.submodules = 'single-submodule' + const settings: IGitSourceSettings = await inputHelper.getInputs() + expect(settings.submodules).toBe(true) + expect(settings.nestedSubmodules).toBe(false) + expect(settings.specificSubmodules).toEqual(['single-submodule']) + }) }) diff --git a/action.yml b/action.yml index 5aa90a7..3bfda7d 100644 --- a/action.yml +++ b/action.yml @@ -81,8 +81,9 @@ inputs: default: false submodules: description: > - Whether to checkout submodules: `true` to checkout submodules or `recursive` to - recursively checkout submodules. + Whether to checkout submodules: `true` to checkout submodules, `recursive` to + recursively checkout submodules, or a comma-separated list of specific submodule + names to checkout only those submodules (e.g., 'submodule1,submodule2'). When the `ssh-key` input is not provided, SSH URLs beginning with `git@github.com:` are diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts index cf639a1..5524a4a 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -52,7 +52,9 @@ export interface IGitCommandManager { shaExists(sha: string): Promise submoduleForeach(command: string, recursive: boolean): Promise submoduleSync(recursive: boolean): Promise + submoduleSyncSpecific(submodules: string[]): Promise submoduleUpdate(fetchDepth: number, recursive: boolean): Promise + submoduleUpdateSpecific(fetchDepth: number, submodules: string[]): Promise submoduleStatus(): Promise tagExists(pattern: string): Promise tryClean(): Promise @@ -394,6 +396,36 @@ class GitCommandManager { return output.stdout } + async submoduleExec( + submodule: string, + command: string, + args?: string[] + ): Promise { + const result = new GitOutput() + const defaultListener = { + stdout: (data: Buffer) => { + stdout.push(data.toString()) + } + } + const stdout: string[] = [] + const submodulePath = await this.execGit([ + 'config', + '--file', + '.gitmodules', + '--get', + `submodule.${submodule}.path` + ]).stdout.trim() + + result.exitCode = await exec.exec("bash", ["-c", command], + { + cwd: path.join(this.workingDirectory, submodulePath), + listeners: defaultListener + } + ) + result.stdout = stdout.join('\n') + return result + } + async submoduleSync(recursive: boolean): Promise { const args = ['submodule', 'sync'] if (recursive) { @@ -425,6 +457,32 @@ class GitCommandManager { await this.execGit(args) } + async submoduleSyncSpecific(submodules: string[]): Promise { + for (const submodule of submodules) { + const args = ['submodule', 'sync', '--', submodule] + await this.execGit(args) + } + } + + async submoduleUpdateSpecific(fetchDepth: number, submodules: string[]): Promise { + // Sometimes the submodule can get in a state where there is no commit, + // which causes the update to fail. + // If so, create an empty commit first for specific submodules. + for (const submodule of submodules) { + await this.submoduleExec(submodule, 'git rev-parse HEAD 2>/dev/null || git -c user.name="dummy" -c user.email="dummy@example.com" commit -m "empty commit" --allow-empty') + } + + for (const submodule of submodules) { + const args = ['-c', 'protocol.version=2'] + args.push('submodule', 'update', '--init', '--force') + if (fetchDepth > 0) { + args.push(`--depth=${fetchDepth}`) + } + args.push('--', submodule) + await this.execGit(args) + } + } + async submoduleStatus(): Promise { const output = await this.execGit(['submodule', 'status'], true) core.debug(output.stdout) diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts index eb43373..471aca9 100644 --- a/src/git-source-provider.ts +++ b/src/git-source-provider.ts @@ -236,14 +236,28 @@ export async function getSource(settings: IGitSourceSettings): Promise { 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() + if (settings.specificSubmodules.length > 0) { + core.startGroup(`Fetching specific submodules: ${settings.specificSubmodules.join(', ')}`) + await git.submoduleSyncSpecific(settings.specificSubmodules) + await git.submoduleUpdateSpecific(settings.fetchDepth, settings.specificSubmodules) + // Configure gc.auto for specific submodules + for (const submodule of settings.specificSubmodules) { + await git.submoduleForeach( + 'git config --local gc.auto 0', + false // Don't recurse for specific submodules + ) + } + core.endGroup() + } else { + 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) { diff --git a/src/git-source-settings.ts b/src/git-source-settings.ts index 629350b..96a31fa 100644 --- a/src/git-source-settings.ts +++ b/src/git-source-settings.ts @@ -74,6 +74,11 @@ export interface IGitSourceSettings { */ nestedSubmodules: boolean + /** + * List of specific submodules to checkout (when not checking out all submodules) + */ + specificSubmodules: string[] + /** * The auth token to use when fetching the repository */ diff --git a/src/input-helper.ts b/src/input-helper.ts index e546c19..0aa9593 100644 --- a/src/input-helper.ts +++ b/src/input-helper.ts @@ -125,15 +125,29 @@ export async function getInputs(): Promise { // Submodules result.submodules = false result.nestedSubmodules = false - const submodulesString = (core.getInput('submodules') || '').toUpperCase() - if (submodulesString == 'RECURSIVE') { + result.specificSubmodules = [] + const submodulesString = core.getInput('submodules') || '' + + if (submodulesString.toUpperCase() == 'RECURSIVE') { result.submodules = true result.nestedSubmodules = true - } else if (submodulesString == 'TRUE') { + } else if (submodulesString.toUpperCase() == 'TRUE') { result.submodules = true + } else if (submodulesString && submodulesString.toUpperCase() !== 'FALSE') { + // Parse comma-separated list of specific submodules + result.specificSubmodules = submodulesString + .split(',') + .map(s => s.trim()) + .filter(s => s.length > 0) + + if (result.specificSubmodules.length > 0) { + result.submodules = true + } } + core.debug(`submodules = ${result.submodules}`) core.debug(`recursive submodules = ${result.nestedSubmodules}`) + core.debug(`specific submodules = ${result.specificSubmodules.join(', ')}`) // Auth token result.authToken = core.getInput('token', {required: true})