From ed2311a594464e30b7189963bdc844e2ab800112 Mon Sep 17 00:00:00 2001
From: Gregory Bonaert <greg@latticeflow.ai>
Date: Thu, 10 Apr 2025 10:13:16 +0200
Subject: [PATCH] Add `exclude_from_clean` option

---
 README.md                   |  6 ++++++
 action.yml                  |  2 ++
 dist/index.js               | 19 ++++++++++++++-----
 package-lock.json           |  4 ++--
 package.json                |  2 +-
 src/git-command-manager.ts  | 11 ++++++++---
 src/git-directory-helper.ts |  3 ++-
 src/git-source-provider.ts  |  1 +
 src/git-source-settings.ts  |  5 +++++
 src/input-helper.ts         |  4 ++++
 10 files changed, 45 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md
index 64dc025..4f46f93 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,8 @@
 [![Build and Test](https://github.com/actions/checkout/actions/workflows/test.yml/badge.svg)](https://github.com/actions/checkout/actions/workflows/test.yml)
 
+Changes compared to https://github.com/actions/checkout:
+- Can exclude path when cleaning repository (see `exclude_from_clean` argument)
+
 # Checkout V4
 
 This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it.
@@ -78,6 +81,9 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
     # Default: true
     clean: ''
 
+    # The path to exclude from cleaning
+    exclude_from_clean: ''
+
     # Partially clone against a given filter. Overrides sparse-checkout if set.
     # Default: null
     filter: ''
diff --git a/action.yml b/action.yml
index 6842eb8..5ec9e61 100644
--- a/action.yml
+++ b/action.yml
@@ -57,6 +57,8 @@ inputs:
   clean:
     description: 'Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching'
     default: true
+  exclude_from_clean:
+    description: 'The path to exclude from cleaning'
   filter:
     description: >
       Partially clone against a given filter.
diff --git a/dist/index.js b/dist/index.js
index b0db713..c1ce62e 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -819,9 +819,15 @@ class GitCommandManager {
             return !!output.stdout.trim();
         });
     }
-    tryClean() {
+    tryClean(exclude_from_clean) {
         return __awaiter(this, void 0, void 0, function* () {
-            const output = yield this.execGit(['clean', '-ffdx'], true);
+            let output;
+            if (exclude_from_clean) {
+                output = yield this.execGit(['clean', '-ffdx', '-e', exclude_from_clean], true);
+            }
+            else {
+                output = yield this.execGit(['clean', '-ffdx'], true);
+            }
             return output.exitCode === 0;
         });
     }
@@ -1025,7 +1031,7 @@ const fs = __importStar(__nccwpck_require__(7147));
 const fsHelper = __importStar(__nccwpck_require__(7219));
 const io = __importStar(__nccwpck_require__(7436));
 const path = __importStar(__nccwpck_require__(1017));
-function prepareExistingDirectory(git, repositoryPath, repositoryUrl, clean, ref) {
+function prepareExistingDirectory(git, repositoryPath, repositoryUrl, clean, exclude_from_clean, ref) {
     return __awaiter(this, void 0, void 0, function* () {
         var _a;
         assert.ok(repositoryPath, 'Expected repositoryPath to be defined');
@@ -1094,7 +1100,7 @@ function prepareExistingDirectory(git, repositoryPath, repositoryUrl, clean, ref
                 // Clean
                 if (clean) {
                     core.startGroup('Cleaning the repository');
-                    if (!(yield git.tryClean())) {
+                    if (!(yield git.tryClean(exclude_from_clean))) {
                         core.debug(`The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For further investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.`);
                         remove = true;
                     }
@@ -1216,7 +1222,7 @@ function getSource(settings) {
             }
             // Prepare existing directory, otherwise recreate
             if (isExisting) {
-                yield gitDirectoryHelper.prepareExistingDirectory(git, settings.repositoryPath, repositoryUrl, settings.clean, settings.ref);
+                yield gitDirectoryHelper.prepareExistingDirectory(git, settings.repositoryPath, repositoryUrl, settings.clean, settings.exclude_from_clean, settings.ref);
             }
             if (!git) {
                 // Downloading using REST API
@@ -1766,6 +1772,9 @@ function getInputs() {
         // Clean
         result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE';
         core.debug(`clean = ${result.clean}`);
+        // Exclude from clean
+        result.exclude_from_clean = core.getInput('exclude_from_clean');
+        core.debug(`exclude_from_clean = ${result.exclude_from_clean}`);
         // Filter
         const filter = core.getInput('filter');
         if (filter) {
diff --git a/package-lock.json b/package-lock.json
index 25753a2..084b010 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "checkout",
-  "version": "4.2.2",
+  "version": "4.2.3",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "checkout",
-      "version": "4.2.2",
+      "version": "4.2.3",
       "license": "MIT",
       "dependencies": {
         "@actions/core": "^1.10.1",
diff --git a/package.json b/package.json
index 5661d70..90743a6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "checkout",
-  "version": "4.2.2",
+  "version": "4.2.3",
   "description": "checkout action",
   "main": "lib/main.js",
   "scripts": {
diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts
index 8e42a38..3881735 100644
--- a/src/git-command-manager.ts
+++ b/src/git-command-manager.ts
@@ -57,7 +57,7 @@ export interface IGitCommandManager {
   submoduleUpdate(fetchDepth: number, recursive: boolean): Promise<void>
   submoduleStatus(): Promise<boolean>
   tagExists(pattern: string): Promise<boolean>
-  tryClean(): Promise<boolean>
+  tryClean(exclude_from_clean: string): Promise<boolean>
   tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
   tryDisableAutomaticGarbageCollection(): Promise<boolean>
   tryGetFetchUrl(): Promise<string>
@@ -434,8 +434,13 @@ class GitCommandManager {
     return !!output.stdout.trim()
   }
 
-  async tryClean(): Promise<boolean> {
-    const output = await this.execGit(['clean', '-ffdx'], true)
+  async tryClean(exclude_from_clean: string): Promise<boolean> {
+    let output
+    if (exclude_from_clean) {
+      output = await this.execGit(['clean', '-ffdx', '-e', exclude_from_clean], true)
+    } else {
+      output = await this.execGit(['clean', '-ffdx'], true)
+    }
     return output.exitCode === 0
   }
 
diff --git a/src/git-directory-helper.ts b/src/git-directory-helper.ts
index 9a0085f..908efc2 100644
--- a/src/git-directory-helper.ts
+++ b/src/git-directory-helper.ts
@@ -11,6 +11,7 @@ export async function prepareExistingDirectory(
   repositoryPath: string,
   repositoryUrl: string,
   clean: boolean,
+  exclude_from_clean: string,
   ref: string
 ): Promise<void> {
   assert.ok(repositoryPath, 'Expected repositoryPath to be defined')
@@ -90,7 +91,7 @@ export async function prepareExistingDirectory(
       // Clean
       if (clean) {
         core.startGroup('Cleaning the repository')
-        if (!(await git.tryClean())) {
+        if (!(await git.tryClean(exclude_from_clean))) {
           core.debug(
             `The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For further investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.`
           )
diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts
index 2d35138..4c8f338 100644
--- a/src/git-source-provider.ts
+++ b/src/git-source-provider.ts
@@ -70,6 +70,7 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
         settings.repositoryPath,
         repositoryUrl,
         settings.clean,
+        settings.exclude_from_clean,
         settings.ref
       )
     }
diff --git a/src/git-source-settings.ts b/src/git-source-settings.ts
index 4e41ac3..fc78956 100644
--- a/src/git-source-settings.ts
+++ b/src/git-source-settings.ts
@@ -29,6 +29,11 @@ export interface IGitSourceSettings {
    */
   clean: boolean
 
+  /**
+   * Indicates path to exclude when cleaning
+   */
+  exclude_from_clean: string
+
   /**
    * The filter determining which objects to include
    */
diff --git a/src/input-helper.ts b/src/input-helper.ts
index 059232f..ea057e3 100644
--- a/src/input-helper.ts
+++ b/src/input-helper.ts
@@ -82,6 +82,10 @@ export async function getInputs(): Promise<IGitSourceSettings> {
   result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'
   core.debug(`clean = ${result.clean}`)
 
+  // Exclude from clean
+  result.exclude_from_clean = core.getInput('exclude_from_clean')
+  core.debug(`exclude_from_clean = ${result.exclude_from_clean}`)
+
   // Filter
   const filter = core.getInput('filter')
   if (filter) {