From f25a3a9f25bd5f4c5d77189cab02ff357b5aedeb Mon Sep 17 00:00:00 2001
From: Thomas Boop <52323235+thboop@users.noreply.github.com>
Date: Thu, 14 Apr 2022 12:12:00 -0400
Subject: [PATCH] Safe Directory v2 update (#764)

* set safe directory when running checkout
---
 CHANGELOG.md                     |   3 +
 __test__/git-auth-helper.test.ts |   9 +-
 dist/index.js                    | 169 +++++++++++++++++------------
 src/git-auth-helper.ts           |  52 ++++++---
 src/git-source-provider.ts       | 179 ++++++++++++++++---------------
 5 files changed, 243 insertions(+), 169 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6f40def..5302870 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
 # Changelog
 
+## v2.4.1
+- [Set the safe directory option on git to prevent git commands failing when running in containers](https://github.com/actions/checkout/pull/762)
+
 ## v2.3.1
 
 - [Fix default branch resolution for .wiki and when using SSH](https://github.com/actions/checkout/pull/284)
diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts
index e14e948..80ccbcb 100644
--- a/__test__/git-auth-helper.test.ts
+++ b/__test__/git-auth-helper.test.ts
@@ -643,10 +643,11 @@ describe('git-auth-helper tests', () => {
     expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
   })
 
-  const removeGlobalAuth_removesOverride = 'removeGlobalAuth removes override'
-  it(removeGlobalAuth_removesOverride, async () => {
+  const removeGlobalConfig_removesOverride =
+    'removeGlobalConfig removes override'
+  it(removeGlobalConfig_removesOverride, async () => {
     // Arrange
-    await setup(removeGlobalAuth_removesOverride)
+    await setup(removeGlobalConfig_removesOverride)
     const authHelper = gitAuthHelper.createAuthHelper(git, settings)
     await authHelper.configureAuth()
     await authHelper.configureGlobalAuth()
@@ -655,7 +656,7 @@ describe('git-auth-helper tests', () => {
     await fs.promises.stat(path.join(git.env['HOME'], '.gitconfig'))
 
     // Act
-    await authHelper.removeGlobalAuth()
+    await authHelper.removeGlobalConfig()
 
     // Assert
     expect(git.env['HOME']).toBeUndefined()
diff --git a/dist/index.js b/dist/index.js
index 8542f7d..7bb2e23 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -6572,9 +6572,13 @@ class GitAuthHelper {
             yield this.configureToken();
         });
     }
-    configureGlobalAuth() {
-        var _a;
+    configureTempGlobalConfig(repositoryPath) {
+        var _a, _b;
         return __awaiter(this, void 0, void 0, function* () {
+            // Already setup global config
+            if (((_a = this.temporaryHomePath) === null || _a === void 0 ? void 0 : _a.length) > 0) {
+                return path.join(this.temporaryHomePath, '.gitconfig');
+            }
             // Create a temp home directory
             const runnerTemp = process.env['RUNNER_TEMP'] || '';
             assert.ok(runnerTemp, 'RUNNER_TEMP is not defined');
@@ -6590,7 +6594,7 @@ class GitAuthHelper {
                 configExists = true;
             }
             catch (err) {
-                if (((_a = err) === null || _a === void 0 ? void 0 : _a.code) !== 'ENOENT') {
+                if (((_b = err) === null || _b === void 0 ? void 0 : _b.code) !== 'ENOENT') {
                     throw err;
                 }
             }
@@ -6601,10 +6605,25 @@ class GitAuthHelper {
             else {
                 yield fs.promises.writeFile(newGitConfigPath, '');
             }
+            // Override HOME
+            core.info(`Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`);
+            this.git.setEnvironmentVariable('HOME', this.temporaryHomePath);
+            // Setup the workspace as a safe directory, so if we pass this into a container job with a different user it doesn't fail
+            // Otherwise all git commands we run in a container fail
+            core.info(`Adding working directory to the temporary git global config as a safe directory`);
+            yield this.git
+                .config('safe.directory', repositoryPath !== null && repositoryPath !== void 0 ? repositoryPath : this.settings.repositoryPath, true, true)
+                .catch(error => {
+                core.info(`Failed to initialize safe directory with error: ${error}`);
+            });
+            return newGitConfigPath;
+        });
+    }
+    configureGlobalAuth() {
+        return __awaiter(this, void 0, void 0, function* () {
+            // 'configureTempGlobalConfig' noops if already set, just returns the path
+            const newGitConfigPath = yield this.configureTempGlobalConfig();
             try {
-                // Override HOME
-                core.info(`Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`);
-                this.git.setEnvironmentVariable('HOME', this.temporaryHomePath);
                 // Configure the token
                 yield this.configureToken(newGitConfigPath, true);
                 // Configure HTTPS instead of SSH
@@ -6657,11 +6676,14 @@ class GitAuthHelper {
             yield this.removeToken();
         });
     }
-    removeGlobalAuth() {
+    removeGlobalConfig() {
+        var _a;
         return __awaiter(this, void 0, void 0, function* () {
-            core.debug(`Unsetting HOME override`);
-            this.git.removeEnvironmentVariable('HOME');
-            yield io.rmRF(this.temporaryHomePath);
+            if (((_a = this.temporaryHomePath) === null || _a === void 0 ? void 0 : _a.length) > 0) {
+                core.debug(`Unsetting HOME override`);
+                this.git.removeEnvironmentVariable('HOME');
+                yield io.rmRF(this.temporaryHomePath);
+            }
         });
     }
     configureSsh() {
@@ -7326,40 +7348,48 @@ function getSource(settings) {
         core.startGroup('Getting Git version info');
         const git = yield getGitCommandManager(settings);
         core.endGroup();
-        // Prepare existing directory, otherwise recreate
-        if (isExisting) {
-            yield gitDirectoryHelper.prepareExistingDirectory(git, settings.repositoryPath, repositoryUrl, settings.clean, settings.ref);
-        }
-        if (!git) {
-            // Downloading using REST API
-            core.info(`The repository will be downloaded using the GitHub REST API`);
-            core.info(`To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`);
-            if (settings.submodules) {
-                throw new Error(`Input 'submodules' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`);
-            }
-            else if (settings.sshKey) {
-                throw new Error(`Input 'ssh-key' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`);
-            }
-            yield githubApiHelper.downloadRepository(settings.authToken, settings.repositoryOwner, settings.repositoryName, settings.ref, settings.commit, settings.repositoryPath);
-            return;
-        }
-        // Save state for POST action
-        stateHelper.setRepositoryPath(settings.repositoryPath);
-        // Initialize the repository
-        if (!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))) {
-            core.startGroup('Initializing the repository');
-            yield git.init();
-            yield git.remoteAdd('origin', repositoryUrl);
-            core.endGroup();
-        }
-        // Disable automatic garbage collection
-        core.startGroup('Disabling automatic garbage collection');
-        if (!(yield git.tryDisableAutomaticGarbageCollection())) {
-            core.warning(`Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`);
-        }
-        core.endGroup();
-        const authHelper = gitAuthHelper.createAuthHelper(git, settings);
+        let authHelper = null;
         try {
+            if (git) {
+                authHelper = gitAuthHelper.createAuthHelper(git, settings);
+                yield authHelper.configureTempGlobalConfig();
+            }
+            // Prepare existing directory, otherwise recreate
+            if (isExisting) {
+                yield gitDirectoryHelper.prepareExistingDirectory(git, settings.repositoryPath, repositoryUrl, settings.clean, settings.ref);
+            }
+            if (!git) {
+                // Downloading using REST API
+                core.info(`The repository will be downloaded using the GitHub REST API`);
+                core.info(`To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`);
+                if (settings.submodules) {
+                    throw new Error(`Input 'submodules' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`);
+                }
+                else if (settings.sshKey) {
+                    throw new Error(`Input 'ssh-key' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`);
+                }
+                yield githubApiHelper.downloadRepository(settings.authToken, settings.repositoryOwner, settings.repositoryName, settings.ref, settings.commit, settings.repositoryPath);
+                return;
+            }
+            // Save state for POST action
+            stateHelper.setRepositoryPath(settings.repositoryPath);
+            // Initialize the repository
+            if (!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))) {
+                core.startGroup('Initializing the repository');
+                yield git.init();
+                yield git.remoteAdd('origin', repositoryUrl);
+                core.endGroup();
+            }
+            // Disable automatic garbage collection
+            core.startGroup('Disabling automatic garbage collection');
+            if (!(yield git.tryDisableAutomaticGarbageCollection())) {
+                core.warning(`Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`);
+            }
+            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');
             yield authHelper.configureAuth();
@@ -7415,27 +7445,21 @@ function getSource(settings) {
             core.endGroup();
             // Submodules
             if (settings.submodules) {
-                try {
-                    // Temporarily override global config
-                    core.startGroup('Setting up auth for fetching submodules');
-                    yield authHelper.configureGlobalAuth();
+                // Temporarily override global config
+                core.startGroup('Setting up auth for fetching submodules');
+                yield authHelper.configureGlobalAuth();
+                core.endGroup();
+                // Checkout submodules
+                core.startGroup('Fetching submodules');
+                yield git.submoduleSync(settings.nestedSubmodules);
+                yield git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules);
+                yield git.submoduleForeach('git config --local gc.auto 0', settings.nestedSubmodules);
+                core.endGroup();
+                // Persist credentials
+                if (settings.persistCredentials) {
+                    core.startGroup('Persisting credentials for submodules');
+                    yield authHelper.configureSubmoduleAuth();
                     core.endGroup();
-                    // Checkout submodules
-                    core.startGroup('Fetching submodules');
-                    yield git.submoduleSync(settings.nestedSubmodules);
-                    yield git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules);
-                    yield git.submoduleForeach('git config --local gc.auto 0', settings.nestedSubmodules);
-                    core.endGroup();
-                    // Persist credentials
-                    if (settings.persistCredentials) {
-                        core.startGroup('Persisting credentials for submodules');
-                        yield authHelper.configureSubmoduleAuth();
-                        core.endGroup();
-                    }
-                }
-                finally {
-                    // Remove temporary global config override
-                    yield authHelper.removeGlobalAuth();
                 }
             }
             // Get commit information
@@ -7447,10 +7471,13 @@ function getSource(settings) {
         }
         finally {
             // Remove auth
-            if (!settings.persistCredentials) {
-                core.startGroup('Removing auth');
-                yield authHelper.removeAuth();
-                core.endGroup();
+            if (authHelper) {
+                if (!settings.persistCredentials) {
+                    core.startGroup('Removing auth');
+                    yield authHelper.removeAuth();
+                    core.endGroup();
+                }
+                authHelper.removeGlobalConfig();
             }
         }
     });
@@ -7472,7 +7499,13 @@ function cleanup(repositoryPath) {
         }
         // Remove auth
         const authHelper = gitAuthHelper.createAuthHelper(git);
-        yield authHelper.removeAuth();
+        try {
+            yield authHelper.configureTempGlobalConfig(repositoryPath);
+            yield authHelper.removeAuth();
+        }
+        finally {
+            yield authHelper.removeGlobalConfig();
+        }
     });
 }
 exports.cleanup = cleanup;
diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts
index 233b3e6..385142a 100644
--- a/src/git-auth-helper.ts
+++ b/src/git-auth-helper.ts
@@ -19,8 +19,9 @@ export interface IGitAuthHelper {
   configureAuth(): Promise<void>
   configureGlobalAuth(): Promise<void>
   configureSubmoduleAuth(): Promise<void>
+  configureTempGlobalConfig(repositoryPath?: string): Promise<string>
   removeAuth(): Promise<void>
-  removeGlobalAuth(): Promise<void>
+  removeGlobalConfig(): Promise<void>
 }
 
 export function createAuthHelper(
@@ -80,7 +81,11 @@ class GitAuthHelper {
     await this.configureToken()
   }
 
-  async configureGlobalAuth(): Promise<void> {
+  async configureTempGlobalConfig(repositoryPath?: string): Promise<string> {
+    // Already setup global config
+    if (this.temporaryHomePath?.length > 0) {
+      return path.join(this.temporaryHomePath, '.gitconfig')
+    }
     // Create a temp home directory
     const runnerTemp = process.env['RUNNER_TEMP'] || ''
     assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
@@ -110,13 +115,34 @@ class GitAuthHelper {
       await fs.promises.writeFile(newGitConfigPath, '')
     }
 
-    try {
-      // Override HOME
-      core.info(
-        `Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`
-      )
-      this.git.setEnvironmentVariable('HOME', this.temporaryHomePath)
+    // Override HOME
+    core.info(
+      `Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`
+    )
+    this.git.setEnvironmentVariable('HOME', this.temporaryHomePath)
 
+    // Setup the workspace as a safe directory, so if we pass this into a container job with a different user it doesn't fail
+    // Otherwise all git commands we run in a container fail
+    core.info(
+      `Adding working directory to the temporary git global config as a safe directory`
+    )
+    await this.git
+      .config(
+        'safe.directory',
+        repositoryPath ?? this.settings.repositoryPath,
+        true,
+        true
+      )
+      .catch(error => {
+        core.info(`Failed to initialize safe directory with error: ${error}`)
+      })
+    return newGitConfigPath
+  }
+
+  async configureGlobalAuth(): Promise<void> {
+    // 'configureTempGlobalConfig' noops if already set, just returns the path
+    const newGitConfigPath = await this.configureTempGlobalConfig()
+    try {
       // Configure the token
       await this.configureToken(newGitConfigPath, true)
 
@@ -181,10 +207,12 @@ class GitAuthHelper {
     await this.removeToken()
   }
 
-  async removeGlobalAuth(): Promise<void> {
-    core.debug(`Unsetting HOME override`)
-    this.git.removeEnvironmentVariable('HOME')
-    await io.rmRF(this.temporaryHomePath)
+  async removeGlobalConfig(): Promise<void> {
+    if (this.temporaryHomePath?.length > 0) {
+      core.debug(`Unsetting HOME override`)
+      this.git.removeEnvironmentVariable('HOME')
+      await io.rmRF(this.temporaryHomePath)
+    }
   }
 
   private async configureSsh(): Promise<void> {
diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts
index 42a12e0..0913229 100644
--- a/src/git-source-provider.ts
+++ b/src/git-source-provider.ts
@@ -36,68 +36,77 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
   const git = await getGitCommandManager(settings)
   core.endGroup()
 
-  // Prepare existing directory, otherwise recreate
-  if (isExisting) {
-    await gitDirectoryHelper.prepareExistingDirectory(
-      git,
-      settings.repositoryPath,
-      repositoryUrl,
-      settings.clean,
-      settings.ref
-    )
-  }
+  let authHelper: gitAuthHelper.IGitAuthHelper | null = null
+  try {
+    if (git) {
+      authHelper = gitAuthHelper.createAuthHelper(git, settings)
+      await authHelper.configureTempGlobalConfig()
+    }
 
-  if (!git) {
-    // Downloading using REST API
-    core.info(`The repository will be downloaded using the GitHub REST API`)
-    core.info(
-      `To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`
-    )
-    if (settings.submodules) {
-      throw new Error(
-        `Input 'submodules' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
-      )
-    } else if (settings.sshKey) {
-      throw new Error(
-        `Input 'ssh-key' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
+    // Prepare existing directory, otherwise recreate
+    if (isExisting) {
+      await gitDirectoryHelper.prepareExistingDirectory(
+        git,
+        settings.repositoryPath,
+        repositoryUrl,
+        settings.clean,
+        settings.ref
       )
     }
 
-    await githubApiHelper.downloadRepository(
-      settings.authToken,
-      settings.repositoryOwner,
-      settings.repositoryName,
-      settings.ref,
-      settings.commit,
-      settings.repositoryPath
-    )
-    return
-  }
+    if (!git) {
+      // Downloading using REST API
+      core.info(`The repository will be downloaded using the GitHub REST API`)
+      core.info(
+        `To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`
+      )
+      if (settings.submodules) {
+        throw new Error(
+          `Input 'submodules' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
+        )
+      } else if (settings.sshKey) {
+        throw new Error(
+          `Input 'ssh-key' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
+        )
+      }
 
-  // Save state for POST action
-  stateHelper.setRepositoryPath(settings.repositoryPath)
+      await githubApiHelper.downloadRepository(
+        settings.authToken,
+        settings.repositoryOwner,
+        settings.repositoryName,
+        settings.ref,
+        settings.commit,
+        settings.repositoryPath
+      )
+      return
+    }
 
-  // Initialize the repository
-  if (
-    !fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
-  ) {
-    core.startGroup('Initializing the repository')
-    await git.init()
-    await git.remoteAdd('origin', repositoryUrl)
+    // Save state for POST action
+    stateHelper.setRepositoryPath(settings.repositoryPath)
+
+    // Initialize the repository
+    if (
+      !fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
+    ) {
+      core.startGroup('Initializing the repository')
+      await git.init()
+      await git.remoteAdd('origin', repositoryUrl)
+      core.endGroup()
+    }
+
+    // Disable automatic garbage collection
+    core.startGroup('Disabling automatic garbage collection')
+    if (!(await git.tryDisableAutomaticGarbageCollection())) {
+      core.warning(
+        `Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`
+      )
+    }
     core.endGroup()
-  }
 
-  // Disable automatic garbage collection
-  core.startGroup('Disabling automatic garbage collection')
-  if (!(await git.tryDisableAutomaticGarbageCollection())) {
-    core.warning(
-      `Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`
-    )
-  }
-  core.endGroup()
-
-  const authHelper = gitAuthHelper.createAuthHelper(git, settings)
-  try {
+    // 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()
@@ -170,34 +179,26 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
 
     // Submodules
     if (settings.submodules) {
-      try {
-        // Temporarily override global config
-        core.startGroup('Setting up auth for fetching submodules')
-        await authHelper.configureGlobalAuth()
-        core.endGroup()
+      // Temporarily override global config
+      core.startGroup('Setting up auth for fetching submodules')
+      await authHelper.configureGlobalAuth()
+      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()
+      // 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()
 
-        // Persist credentials
-        if (settings.persistCredentials) {
-          core.startGroup('Persisting credentials for submodules')
-          await authHelper.configureSubmoduleAuth()
-          core.endGroup()
-        }
-      } finally {
-        // Remove temporary global config override
-        await authHelper.removeGlobalAuth()
+      // Persist credentials
+      if (settings.persistCredentials) {
+        core.startGroup('Persisting credentials for submodules')
+        await authHelper.configureSubmoduleAuth()
+        core.endGroup()
       }
     }
 
@@ -218,10 +219,13 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
     )
   } finally {
     // Remove auth
-    if (!settings.persistCredentials) {
-      core.startGroup('Removing auth')
-      await authHelper.removeAuth()
-      core.endGroup()
+    if (authHelper) {
+      if (!settings.persistCredentials) {
+        core.startGroup('Removing auth')
+        await authHelper.removeAuth()
+        core.endGroup()
+      }
+      authHelper.removeGlobalConfig()
     }
   }
 }
@@ -244,7 +248,12 @@ export async function cleanup(repositoryPath: string): Promise<void> {
 
   // Remove auth
   const authHelper = gitAuthHelper.createAuthHelper(git)
-  await authHelper.removeAuth()
+  try {
+    await authHelper.configureTempGlobalConfig(repositoryPath)
+    await authHelper.removeAuth()
+  } finally {
+    await authHelper.removeGlobalConfig()
+  }
 }
 
 async function getGitCommandManager(