Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions __test__/input-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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'])
})
})
5 changes: 3 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 `[email protected]:` are
Expand Down
58 changes: 58 additions & 0 deletions src/git-command-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ export interface IGitCommandManager {
shaExists(sha: string): Promise<boolean>
submoduleForeach(command: string, recursive: boolean): Promise<string>
submoduleSync(recursive: boolean): Promise<void>
submoduleSyncSpecific(submodules: string[]): Promise<void>
submoduleUpdate(fetchDepth: number, recursive: boolean): Promise<void>
submoduleUpdateSpecific(fetchDepth: number, submodules: string[]): Promise<void>
submoduleStatus(): Promise<boolean>
tagExists(pattern: string): Promise<boolean>
tryClean(): Promise<boolean>
Expand Down Expand Up @@ -394,6 +396,36 @@ class GitCommandManager {
return output.stdout
}

async submoduleExec(
submodule: string,
command: string,
args?: string[]
): Promise<GitOutput> {
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<void> {
const args = ['submodule', 'sync']
if (recursive) {
Expand Down Expand Up @@ -425,6 +457,32 @@ class GitCommandManager {
await this.execGit(args)
}

async submoduleSyncSpecific(submodules: string[]): Promise<void> {
for (const submodule of submodules) {
const args = ['submodule', 'sync', '--', submodule]
await this.execGit(args)
}
}

async submoduleUpdateSpecific(fetchDepth: number, submodules: string[]): Promise<void> {
// 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="[email protected]" 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<boolean> {
const output = await this.execGit(['submodule', 'status'], true)
core.debug(output.stdout)
Expand Down
30 changes: 22 additions & 8 deletions src/git-source-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,14 +236,28 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
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) {
Expand Down
5 changes: 5 additions & 0 deletions src/git-source-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
20 changes: 17 additions & 3 deletions src/input-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,29 @@ export async function getInputs(): Promise<IGitSourceSettings> {
// 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})
Expand Down