From 03f9a47bc3e8f799af2cc39f94a4b2be2de9ca4f Mon Sep 17 00:00:00 2001
From: Marcus Tillmanns <maddimax@gmail.com>
Date: Tue, 27 Aug 2024 09:55:59 +0200
Subject: [PATCH] Add string[] option to submodules

Allows checking out only specific submodules instead of all
---
 .github/workflows/test.yml       | 11 ++++++++
 README.md                        |  4 +++
 __test__/git-auth-helper.test.ts |  1 +
 __test__/input-helper.test.ts    | 17 ++++++++++++
 action.yml                       |  4 +++
 dist/index.js                    | 44 +++++++++++++++++++++++-------
 src/git-command-manager.ts       | 46 ++++++++++++++++++++++++--------
 src/git-source-provider.ts       |  6 ++++-
 src/git-source-settings.ts       |  5 ++++
 src/input-helper.ts              | 12 ++++++++-
 10 files changed, 128 insertions(+), 22 deletions(-)

diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1ef3c313e..8a9b336cb 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -154,6 +154,17 @@ jobs:
           submodules: true
       - name: Verify submodules true
         run: __test__/verify-submodules-true.sh
+  
+      # Submodules limited
+      - name: Checkout submodules true
+        uses: ./
+        with:
+          ref: test-data/v2/submodule-ssh-url
+          path: submodules-true
+          submodules: true
+          submodule-directories: submodule-level-1
+      - name: Verify submodules true
+        run: __test__/verify-submodules-true.sh
 
       # Submodules recursive
       - name: Checkout submodules recursive
diff --git a/README.md b/README.md
index 9b6176d94..d376c49a9 100644
--- a/README.md
+++ b/README.md
@@ -116,6 +116,10 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
     # Default: false
     submodules: ''
 
+    # A list of submodules to use when `submodules` is `true`.
+    # Default: null
+    submodule-directories: ''
+
     # Add repository path as safe.directory for Git global config by running `git
     # config --global --add safe.directory <path>`
     # Default: true
diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts
index 7633704cc..ff60baef6 100644
--- a/__test__/git-auth-helper.test.ts
+++ b/__test__/git-auth-helper.test.ts
@@ -813,6 +813,7 @@ async function setup(testName: string): Promise<void> {
     lfs: false,
     submodules: false,
     nestedSubmodules: false,
+    submoduleDirectories: null,
     persistCredentials: true,
     ref: 'refs/heads/main',
     repositoryName: 'my-repo',
diff --git a/__test__/input-helper.test.ts b/__test__/input-helper.test.ts
index 9514cb42d..35c1848dd 100644
--- a/__test__/input-helper.test.ts
+++ b/__test__/input-helper.test.ts
@@ -21,6 +21,13 @@ describe('input-helper tests', () => {
     jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
       return inputs[name]
     })
+    // Mock getMultilineInput
+    jest.spyOn(core, 'getMultilineInput').mockImplementation((name: string) => {
+      const input: string[] = (inputs[name] || '')
+        .split('\n')
+        .filter(x => x !== '')
+      return input.map(inp => inp.trim())
+    })
 
     // Mock error/warning/info/debug
     jest.spyOn(core, 'error').mockImplementation(jest.fn())
@@ -87,6 +94,7 @@ describe('input-helper tests', () => {
     expect(settings.showProgress).toBe(true)
     expect(settings.lfs).toBe(false)
     expect(settings.ref).toBe('refs/heads/some-ref')
+    expect(settings.submoduleDirectories).toBe(null)
     expect(settings.repositoryName).toBe('some-repo')
     expect(settings.repositoryOwner).toBe('some-owner')
     expect(settings.repositoryPath).toBe(gitHubWorkspace)
@@ -144,4 +152,13 @@ describe('input-helper tests', () => {
     const settings: IGitSourceSettings = await inputHelper.getInputs()
     expect(settings.workflowOrganizationId).toBe(123456)
   })
+  it('sets submoduleDirectories', async () => {
+    inputs['submodule-directories'] = 'submodule1\nsubmodule2'
+    const settings: IGitSourceSettings = await inputHelper.getInputs()
+    expect(settings.submoduleDirectories).toStrictEqual([
+      'submodule1',
+      'submodule2'
+    ])
+    expect(settings.submodules).toBe(true)
+  })
 })
diff --git a/action.yml b/action.yml
index 75d5ae2d8..d18a526df 100644
--- a/action.yml
+++ b/action.yml
@@ -92,6 +92,10 @@ inputs:
       When the `ssh-key` input is not provided, SSH URLs beginning with `git@github.com:` are
       converted to HTTPS.
     default: false
+  submodule-directories:
+    description: >
+      A list of submodules to use when `submodules` is `true`.
+    default: null
   set-safe-directory:
     description: Add repository path as safe.directory for Git global config by running `git config --global --add safe.directory <path>`
     default: true
diff --git a/dist/index.js b/dist/index.js
index 9d959a9ee..aba7b8409 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -795,17 +795,32 @@ class GitCommandManager {
             yield this.execGit(args);
         });
     }
-    submoduleUpdate(fetchDepth, recursive) {
+    submoduleUpdate(fetchDepth, recursive, submoduleDirectories) {
         return __awaiter(this, void 0, void 0, function* () {
-            const args = ['-c', 'protocol.version=2'];
-            args.push('submodule', 'update', '--init', '--force');
-            if (fetchDepth > 0) {
-                args.push(`--depth=${fetchDepth}`);
+            if (submoduleDirectories) {
+                for (const submodule of submoduleDirectories) {
+                    const args = ['-c', 'protocol.version=2'];
+                    args.push('submodule', 'update', '--init', '--force', submodule);
+                    if (fetchDepth > 0) {
+                        args.push(`--depth=${fetchDepth}`);
+                    }
+                    if (recursive) {
+                        args.push('--recursive');
+                    }
+                    yield this.execGit(args);
+                }
             }
-            if (recursive) {
-                args.push('--recursive');
+            else {
+                const args = ['-c', 'protocol.version=2'];
+                args.push('submodule', 'update', '--init', '--force');
+                if (fetchDepth > 0) {
+                    args.push(`--depth=${fetchDepth}`);
+                }
+                if (recursive) {
+                    args.push('--recursive');
+                }
+                yield this.execGit(args);
             }
-            yield this.execGit(args);
         });
     }
     submoduleStatus() {
@@ -1342,7 +1357,7 @@ function getSource(settings) {
                 // Checkout submodules
                 core.startGroup('Fetching submodules');
                 yield git.submoduleSync(settings.nestedSubmodules);
-                yield git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules);
+                yield git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules, settings.submoduleDirectories);
                 yield git.submoduleForeach('git config --local gc.auto 0', settings.nestedSubmodules);
                 core.endGroup();
                 // Persist credentials
@@ -1805,6 +1820,7 @@ function getInputs() {
         // Submodules
         result.submodules = false;
         result.nestedSubmodules = false;
+        result.submoduleDirectories = null;
         const submodulesString = (core.getInput('submodules') || '').toUpperCase();
         if (submodulesString == 'RECURSIVE') {
             result.submodules = true;
@@ -1813,8 +1829,18 @@ function getInputs() {
         else if (submodulesString == 'TRUE') {
             result.submodules = true;
         }
+        const submoduleDirectories = core.getMultilineInput('submodule-directories');
+        if (submoduleDirectories.length > 0) {
+            result.submoduleDirectories = submoduleDirectories;
+            if (!result.submodules)
+                result.submodules = true;
+        }
+        else {
+            result.submoduleDirectories = null;
+        }
         core.debug(`submodules = ${result.submodules}`);
         core.debug(`recursive submodules = ${result.nestedSubmodules}`);
+        core.debug(`submodule directories = ${result.submoduleDirectories}`);
         // Auth token
         result.authToken = core.getInput('token', { required: true });
         // SSH
diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts
index 8e42a387f..9c3704348 100644
--- a/src/git-command-manager.ts
+++ b/src/git-command-manager.ts
@@ -54,7 +54,11 @@ export interface IGitCommandManager {
   shaExists(sha: string): Promise<boolean>
   submoduleForeach(command: string, recursive: boolean): Promise<string>
   submoduleSync(recursive: boolean): Promise<void>
-  submoduleUpdate(fetchDepth: number, recursive: boolean): Promise<void>
+  submoduleUpdate(
+    fetchDepth: number,
+    recursive: boolean,
+    submoduleDirectories: string[] | null
+  ): Promise<void>
   submoduleStatus(): Promise<boolean>
   tagExists(pattern: string): Promise<boolean>
   tryClean(): Promise<boolean>
@@ -409,18 +413,38 @@ class GitCommandManager {
     await this.execGit(args)
   }
 
-  async submoduleUpdate(fetchDepth: number, recursive: boolean): Promise<void> {
-    const args = ['-c', 'protocol.version=2']
-    args.push('submodule', 'update', '--init', '--force')
-    if (fetchDepth > 0) {
-      args.push(`--depth=${fetchDepth}`)
-    }
+  async submoduleUpdate(
+    fetchDepth: number,
+    recursive: boolean,
+    submoduleDirectories: string[] | null
+  ): Promise<void> {
+    if (submoduleDirectories) {
+      for (const submodule of submoduleDirectories) {
+        const args = ['-c', 'protocol.version=2']
+        args.push('submodule', 'update', '--init', '--force', submodule)
+        if (fetchDepth > 0) {
+          args.push(`--depth=${fetchDepth}`)
+        }
 
-    if (recursive) {
-      args.push('--recursive')
-    }
+        if (recursive) {
+          args.push('--recursive')
+        }
 
-    await this.execGit(args)
+        await this.execGit(args)
+      }
+    } else {
+      const args = ['-c', 'protocol.version=2']
+      args.push('submodule', 'update', '--init', '--force')
+      if (fetchDepth > 0) {
+        args.push(`--depth=${fetchDepth}`)
+      }
+
+      if (recursive) {
+        args.push('--recursive')
+      }
+
+      await this.execGit(args)
+    }
   }
 
   async submoduleStatus(): Promise<boolean> {
diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts
index f723d94f4..122e1316f 100644
--- a/src/git-source-provider.ts
+++ b/src/git-source-provider.ts
@@ -242,7 +242,11 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
       // Checkout submodules
       core.startGroup('Fetching submodules')
       await git.submoduleSync(settings.nestedSubmodules)
-      await git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules)
+      await git.submoduleUpdate(
+        settings.fetchDepth,
+        settings.nestedSubmodules,
+        settings.submoduleDirectories
+      )
       await git.submoduleForeach(
         'git config --local gc.auto 0',
         settings.nestedSubmodules
diff --git a/src/git-source-settings.ts b/src/git-source-settings.ts
index 4e41ac302..15554896a 100644
--- a/src/git-source-settings.ts
+++ b/src/git-source-settings.ts
@@ -74,6 +74,11 @@ export interface IGitSourceSettings {
    */
   nestedSubmodules: boolean
 
+  /**
+   * Indicates which submodule paths to checkout
+   */
+  submoduleDirectories: string[] | null
+
   /**
    * The auth token to use when fetching the repository
    */
diff --git a/src/input-helper.ts b/src/input-helper.ts
index 059232f5c..f367ce05e 100644
--- a/src/input-helper.ts
+++ b/src/input-helper.ts
@@ -125,6 +125,7 @@ export async function getInputs(): Promise<IGitSourceSettings> {
   // Submodules
   result.submodules = false
   result.nestedSubmodules = false
+  result.submoduleDirectories = null
   const submodulesString = (core.getInput('submodules') || '').toUpperCase()
   if (submodulesString == 'RECURSIVE') {
     result.submodules = true
@@ -132,9 +133,18 @@ export async function getInputs(): Promise<IGitSourceSettings> {
   } else if (submodulesString == 'TRUE') {
     result.submodules = true
   }
+
+  const submoduleDirectories = core.getMultilineInput('submodule-directories')
+  if (submoduleDirectories.length > 0) {
+    result.submoduleDirectories = submoduleDirectories
+    if (!result.submodules) result.submodules = true
+  } else {
+    result.submoduleDirectories = null
+  }
+
   core.debug(`submodules = ${result.submodules}`)
   core.debug(`recursive submodules = ${result.nestedSubmodules}`)
-
+  core.debug(`submodule directories = ${result.submoduleDirectories}`)
   // Auth token
   result.authToken = core.getInput('token', {required: true})