Read-only mirror — the public repositories of the Forgejo instance, refreshed every 10 minutes. The live instance (fg.dieschoenewolke.de) is reachable only over the VPN; clone here with https://git.dieschoenewolke.de/git/<owner>/<repo>.git.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoreric sciple <ericsciple@users.noreply.github.com>2025-11-13 15:22:10 -0600
committerGitHub <noreply@github.com>2025-11-13 15:22:10 -0600
commitf97ee346754f209980bd9c4d3ff3f84e84381a9d (patch)
tree954cb4725509e2106b9f5ecf86e0ec7633262d7c
parent937aec85f6b7cd94614c531b7f0c4a52553f1266 (diff)
Cleanup actions/checkout@v6 auth style (#2301)v5.0.1
-rw-r--r--__test__/git-auth-helper.test.ts289
-rw-r--r--__test__/git-directory-helper.test.ts12
-rw-r--r--dist/index.js151
-rw-r--r--src/git-auth-helper.ts107
-rw-r--r--src/git-command-manager.ts100
5 files changed, 657 insertions, 2 deletions
diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts
index 7633704..9acba54 100644
--- a/__test__/git-auth-helper.test.ts
+++ b/__test__/git-auth-helper.test.ts
@@ -675,6 +675,283 @@ describe('git-auth-helper tests', () => {
expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
})
+ const removeAuth_removesV6StyleCredentials =
+ 'removeAuth removes v6 style credentials'
+ it(removeAuth_removesV6StyleCredentials, async () => {
+ // Arrange
+ await setup(removeAuth_removesV6StyleCredentials)
+ const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+ await authHelper.configureAuth()
+
+ // Manually create v6-style credentials that would be left by v6
+ const credentialsFileName =
+ 'git-credentials-12345678-1234-1234-1234-123456789abc.config'
+ const credentialsFilePath = path.join(runnerTemp, credentialsFileName)
+ const basicCredential = Buffer.from(
+ `x-access-token:${settings.authToken}`,
+ 'utf8'
+ ).toString('base64')
+ const credentialsContent = `[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic ${basicCredential}\n`
+ await fs.promises.writeFile(credentialsFilePath, credentialsContent)
+
+ // Add includeIf entries to local git config (simulating v6 configuration)
+ const hostGitDir = path.join(workspace, '.git').replace(/\\/g, '/')
+ await fs.promises.appendFile(
+ localGitConfigPath,
+ `[includeIf "gitdir:${hostGitDir}/"]\n\tpath = ${credentialsFilePath}\n`
+ )
+ await fs.promises.appendFile(
+ localGitConfigPath,
+ `[includeIf "gitdir:/github/workspace/.git/"]\n\tpath = /github/runner_temp/${credentialsFileName}\n`
+ )
+
+ // Verify v6 style config exists
+ let gitConfigContent = (
+ await fs.promises.readFile(localGitConfigPath)
+ ).toString()
+ expect(gitConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(0)
+ expect(
+ gitConfigContent.indexOf(credentialsFilePath)
+ ).toBeGreaterThanOrEqual(0)
+ await fs.promises.stat(credentialsFilePath) // Verify file exists
+
+ // Mock the git methods to handle v6 cleanup
+ const mockTryGetConfigKeys = git.tryGetConfigKeys as jest.Mock<any, any>
+ mockTryGetConfigKeys.mockResolvedValue([
+ `includeIf.gitdir:${hostGitDir}/.path`,
+ 'includeIf.gitdir:/github/workspace/.git/.path'
+ ])
+
+ const mockTryGetConfigValues = git.tryGetConfigValues as jest.Mock<any, any>
+ mockTryGetConfigValues.mockImplementation(async (key: string) => {
+ if (key === `includeIf.gitdir:${hostGitDir}/.path`) {
+ return [credentialsFilePath]
+ }
+ if (key === 'includeIf.gitdir:/github/workspace/.git/.path') {
+ return [`/github/runner_temp/${credentialsFileName}`]
+ }
+ return []
+ })
+
+ const mockTryConfigUnsetValue = git.tryConfigUnsetValue as jest.Mock<
+ any,
+ any
+ >
+ mockTryConfigUnsetValue.mockImplementation(
+ async (
+ key: string,
+ value: string,
+ globalConfig?: boolean,
+ configPath?: string
+ ) => {
+ const targetPath = configPath || localGitConfigPath
+ let content = await fs.promises.readFile(targetPath, 'utf8')
+ // Remove the includeIf section
+ const lines = content
+ .split('\n')
+ .filter(line => !line.includes('includeIf') && !line.includes(value))
+ await fs.promises.writeFile(targetPath, lines.join('\n'))
+ return true
+ }
+ )
+
+ // Act
+ await authHelper.removeAuth()
+
+ // Assert includeIf entries removed from local git config
+ gitConfigContent = (
+ await fs.promises.readFile(localGitConfigPath)
+ ).toString()
+ expect(gitConfigContent.indexOf('includeIf')).toBeLessThan(0)
+ expect(gitConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
+
+ // Assert credentials config file deleted
+ try {
+ await fs.promises.stat(credentialsFilePath)
+ throw new Error('Credentials file should have been deleted')
+ } catch (err) {
+ if ((err as any)?.code !== 'ENOENT') {
+ throw err
+ }
+ }
+ })
+
+ const removeAuth_removesV6StyleCredentialsFromSubmodules =
+ 'removeAuth removes v6 style credentials from submodules'
+ it(removeAuth_removesV6StyleCredentialsFromSubmodules, async () => {
+ // Arrange
+ await setup(removeAuth_removesV6StyleCredentialsFromSubmodules)
+
+ // Create fake submodule config paths
+ const submodule1Dir = path.join(workspace, '.git', 'modules', 'submodule-1')
+ const submodule1ConfigPath = path.join(submodule1Dir, 'config')
+ await fs.promises.mkdir(submodule1Dir, {recursive: true})
+ await fs.promises.writeFile(submodule1ConfigPath, '')
+
+ const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+ await authHelper.configureAuth()
+
+ // Create v6-style credentials file
+ const credentialsFileName =
+ 'git-credentials-abcdef12-3456-7890-abcd-ef1234567890.config'
+ const credentialsFilePath = path.join(runnerTemp, credentialsFileName)
+ const basicCredential = Buffer.from(
+ `x-access-token:${settings.authToken}`,
+ 'utf8'
+ ).toString('base64')
+ const credentialsContent = `[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic ${basicCredential}\n`
+ await fs.promises.writeFile(credentialsFilePath, credentialsContent)
+
+ // Add includeIf entries to submodule config
+ const submodule1GitDir = submodule1Dir.replace(/\\/g, '/')
+ await fs.promises.appendFile(
+ submodule1ConfigPath,
+ `[includeIf "gitdir:${submodule1GitDir}/"]\n\tpath = ${credentialsFilePath}\n`
+ )
+
+ // Verify submodule config has includeIf entry
+ let submoduleConfigContent = (
+ await fs.promises.readFile(submodule1ConfigPath)
+ ).toString()
+ expect(submoduleConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(
+ 0
+ )
+ expect(
+ submoduleConfigContent.indexOf(credentialsFilePath)
+ ).toBeGreaterThanOrEqual(0)
+
+ // Mock getSubmoduleConfigPaths
+ const mockGetSubmoduleConfigPaths =
+ git.getSubmoduleConfigPaths as jest.Mock<any, any>
+ mockGetSubmoduleConfigPaths.mockResolvedValue([submodule1ConfigPath])
+
+ // Mock tryGetConfigKeys for submodule
+ const mockTryGetConfigKeys = git.tryGetConfigKeys as jest.Mock<any, any>
+ mockTryGetConfigKeys.mockImplementation(
+ async (pattern: string, globalConfig?: boolean, configPath?: string) => {
+ if (configPath === submodule1ConfigPath) {
+ return [`includeIf.gitdir:${submodule1GitDir}/.path`]
+ }
+ return []
+ }
+ )
+
+ // Mock tryGetConfigValues for submodule
+ const mockTryGetConfigValues = git.tryGetConfigValues as jest.Mock<any, any>
+ mockTryGetConfigValues.mockImplementation(
+ async (key: string, globalConfig?: boolean, configPath?: string) => {
+ if (
+ configPath === submodule1ConfigPath &&
+ key === `includeIf.gitdir:${submodule1GitDir}/.path`
+ ) {
+ return [credentialsFilePath]
+ }
+ return []
+ }
+ )
+
+ // Mock tryConfigUnsetValue for submodule
+ const mockTryConfigUnsetValue = git.tryConfigUnsetValue as jest.Mock<
+ any,
+ any
+ >
+ mockTryConfigUnsetValue.mockImplementation(
+ async (
+ key: string,
+ value: string,
+ globalConfig?: boolean,
+ configPath?: string
+ ) => {
+ const targetPath = configPath || localGitConfigPath
+ let content = await fs.promises.readFile(targetPath, 'utf8')
+ const lines = content
+ .split('\n')
+ .filter(line => !line.includes('includeIf') && !line.includes(value))
+ await fs.promises.writeFile(targetPath, lines.join('\n'))
+ return true
+ }
+ )
+
+ // Act
+ await authHelper.removeAuth()
+
+ // Assert submodule includeIf entries removed
+ submoduleConfigContent = (
+ await fs.promises.readFile(submodule1ConfigPath)
+ ).toString()
+ expect(submoduleConfigContent.indexOf('includeIf')).toBeLessThan(0)
+ expect(submoduleConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
+
+ // Assert credentials file deleted
+ try {
+ await fs.promises.stat(credentialsFilePath)
+ throw new Error('Credentials file should have been deleted')
+ } catch (err) {
+ if ((err as any)?.code !== 'ENOENT') {
+ throw err
+ }
+ }
+ })
+
+ const removeAuth_skipsV6CleanupWhenEnvVarSet =
+ 'removeAuth skips v6 cleanup when ACTIONS_CHECKOUT_SKIP_V6_CLEANUP is set'
+ it(removeAuth_skipsV6CleanupWhenEnvVarSet, async () => {
+ // Arrange
+ await setup(removeAuth_skipsV6CleanupWhenEnvVarSet)
+
+ // Set the skip environment variable
+ process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'] = '1'
+
+ const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+ await authHelper.configureAuth()
+
+ // Create v6-style credentials file in RUNNER_TEMP
+ const credentialsFileName = 'git-credentials-test-uuid-1234-5678.config'
+ const credentialsFilePath = path.join(runnerTemp, credentialsFileName)
+ const credentialsContent =
+ '[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic token\n'
+ await fs.promises.writeFile(credentialsFilePath, credentialsContent)
+
+ // Add includeIf section to local git config (separate from http.* config)
+ const includeIfSection = `\n[includeIf "gitdir:/some/path/.git/"]\n\tpath = ${credentialsFilePath}\n`
+ await fs.promises.appendFile(localGitConfigPath, includeIfSection)
+
+ // Verify v6 style config exists
+ let gitConfigContent = (
+ await fs.promises.readFile(localGitConfigPath)
+ ).toString()
+ expect(gitConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(0)
+ await fs.promises.stat(credentialsFilePath) // Verify file exists
+
+ // Act
+ await authHelper.removeAuth()
+
+ // Assert v5 cleanup still happened (http.* removed)
+ gitConfigContent = (
+ await fs.promises.readFile(localGitConfigPath)
+ ).toString()
+ expect(
+ gitConfigContent.indexOf('http.https://github.com/.extraheader')
+ ).toBeLessThan(0)
+
+ // Assert v6 cleanup was skipped - includeIf should still be present
+ expect(gitConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(0)
+ expect(
+ gitConfigContent.indexOf(credentialsFilePath)
+ ).toBeGreaterThanOrEqual(0)
+
+ // Assert credentials file still exists (wasn't deleted)
+ await fs.promises.stat(credentialsFilePath) // File should still exist
+
+ // Assert debug message was logged
+ expect(core.debug).toHaveBeenCalledWith(
+ 'Skipping v6 style cleanup due to ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'
+ )
+
+ // Cleanup
+ delete process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP']
+ })
+
const removeGlobalConfig_removesOverride =
'removeGlobalConfig removes override'
it(removeGlobalConfig_removesOverride, async () => {
@@ -796,6 +1073,18 @@ async function setup(testName: string): Promise<void> {
),
tryDisableAutomaticGarbageCollection: jest.fn(),
tryGetFetchUrl: jest.fn(),
+ getSubmoduleConfigPaths: jest.fn(async () => {
+ return []
+ }),
+ tryConfigUnsetValue: jest.fn(async () => {
+ return true
+ }),
+ tryGetConfigValues: jest.fn(async () => {
+ return []
+ }),
+ tryGetConfigKeys: jest.fn(async () => {
+ return []
+ }),
tryReset: jest.fn(),
version: jest.fn()
}
diff --git a/__test__/git-directory-helper.test.ts b/__test__/git-directory-helper.test.ts
index 22e9ae6..1627b84 100644
--- a/__test__/git-directory-helper.test.ts
+++ b/__test__/git-directory-helper.test.ts
@@ -499,6 +499,18 @@ async function setup(testName: string): Promise<void> {
await fs.promises.stat(path.join(repositoryPath, '.git'))
return repositoryUrl
}),
+ getSubmoduleConfigPaths: jest.fn(async () => {
+ return []
+ }),
+ tryConfigUnsetValue: jest.fn(async () => {
+ return true
+ }),
+ tryGetConfigValues: jest.fn(async () => {
+ return []
+ }),
+ tryGetConfigKeys: jest.fn(async () => {
+ return []
+ }),
tryReset: jest.fn(async () => {
return true
}),
diff --git a/dist/index.js b/dist/index.js
index f3ae6f3..b0add8a 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -411,8 +411,50 @@ class GitAuthHelper {
}
removeToken() {
return __awaiter(this, void 0, void 0, function* () {
- // HTTP extra header
+ // Remove HTTP extra header from local git config and submodule configs
yield this.removeGitConfig(this.tokenConfigKey);
+ //
+ // Cleanup actions/checkout@v6 style credentials
+ //
+ const skipV6Cleanup = process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'];
+ if (skipV6Cleanup === '1' || (skipV6Cleanup === null || skipV6Cleanup === void 0 ? void 0 : skipV6Cleanup.toLowerCase()) === 'true') {
+ core.debug('Skipping v6 style cleanup due to ACTIONS_CHECKOUT_SKIP_V6_CLEANUP');
+ return;
+ }
+ try {
+ // Collect credentials config paths that need to be removed
+ const credentialsPaths = new Set();
+ // Remove includeIf entries that point to git-credentials-*.config files
+ const mainCredentialsPaths = yield this.removeIncludeIfCredentials();
+ mainCredentialsPaths.forEach(path => credentialsPaths.add(path));
+ // Remove submodule includeIf entries that point to git-credentials-*.config files
+ try {
+ const submoduleConfigPaths = yield this.git.getSubmoduleConfigPaths(true);
+ for (const configPath of submoduleConfigPaths) {
+ const submoduleCredentialsPaths = yield this.removeIncludeIfCredentials(configPath);
+ submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path));
+ }
+ }
+ catch (err) {
+ core.debug(`Unable to get submodule config paths: ${err}`);
+ }
+ // Remove credentials config files
+ for (const credentialsPath of credentialsPaths) {
+ // Only remove credentials config files if they are under RUNNER_TEMP
+ const runnerTemp = process.env['RUNNER_TEMP'];
+ if (runnerTemp && credentialsPath.startsWith(runnerTemp)) {
+ try {
+ yield io.rmRF(credentialsPath);
+ }
+ catch (err) {
+ core.debug(`Failed to remove credentials config '${credentialsPath}': ${err}`);
+ }
+ }
+ }
+ }
+ catch (err) {
+ core.debug(`Failed to cleanup v6 style credentials: ${err}`);
+ }
});
}
removeGitConfig(configKey_1) {
@@ -430,6 +472,49 @@ class GitAuthHelper {
`sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`, true);
});
}
+ /**
+ * Removes includeIf entries that point to git-credentials-*.config files.
+ * This handles cleanup of credentials configured by newer versions of the action.
+ * @param configPath Optional path to a specific git config file to operate on
+ * @returns Array of unique credentials config file paths that were found and removed
+ */
+ removeIncludeIfCredentials(configPath) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const credentialsPaths = new Set();
+ try {
+ // Get all includeIf.gitdir keys
+ const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:', false, // globalConfig?
+ configPath);
+ for (const key of keys) {
+ // Get all values for this key
+ const values = yield this.git.tryGetConfigValues(key, false, // globalConfig?
+ configPath);
+ if (values.length > 0) {
+ // Remove only values that match git-credentials-<uuid>.config pattern
+ for (const value of values) {
+ if (this.testCredentialsConfigPath(value)) {
+ credentialsPaths.add(value);
+ yield this.git.tryConfigUnsetValue(key, value, false, configPath);
+ }
+ }
+ }
+ }
+ }
+ catch (err) {
+ // Ignore errors - this is cleanup code
+ core.debug(`Error during includeIf cleanup${configPath ? ` for ${configPath}` : ''}: ${err}`);
+ }
+ return Array.from(credentialsPaths);
+ });
+ }
+ /**
+ * Tests if a path matches the git-credentials-*.config pattern used by newer versions.
+ * @param path The path to test
+ * @returns True if the path matches the credentials config pattern
+ */
+ testCredentialsConfigPath(path) {
+ return /git-credentials-[0-9a-f-]+\.config$/i.test(path);
+ }
}
@@ -706,6 +791,16 @@ class GitCommandManager {
throw new Error('Unexpected output when retrieving default branch');
});
}
+ getSubmoduleConfigPaths(recursive) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Get submodule config file paths.
+ // Use `--show-origin` to get the config file path for each submodule.
+ const output = yield this.submoduleForeach(`git config --local --show-origin --name-only --get-regexp remote.origin.url`, recursive);
+ // Extract config file paths from the output (lines starting with "file:").
+ const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
+ return configPaths;
+ });
+ }
getWorkingDirectory() {
return this.workingDirectory;
}
@@ -836,6 +931,20 @@ class GitCommandManager {
return output.exitCode === 0;
});
}
+ tryConfigUnsetValue(configKey, configValue, globalConfig, configFile) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const args = ['config'];
+ if (configFile) {
+ args.push('--file', configFile);
+ }
+ else {
+ args.push(globalConfig ? '--global' : '--local');
+ }
+ args.push('--unset', configKey, configValue);
+ const output = yield this.execGit(args, true);
+ return output.exitCode === 0;
+ });
+ }
tryDisableAutomaticGarbageCollection() {
return __awaiter(this, void 0, void 0, function* () {
const output = yield this.execGit(['config', '--local', 'gc.auto', '0'], true);
@@ -855,6 +964,46 @@ class GitCommandManager {
return stdout;
});
}
+ tryGetConfigValues(configKey, globalConfig, configFile) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const args = ['config'];
+ if (configFile) {
+ args.push('--file', configFile);
+ }
+ else {
+ args.push(globalConfig ? '--global' : '--local');
+ }
+ args.push('--get-all', configKey);
+ const output = yield this.execGit(args, true);
+ if (output.exitCode !== 0) {
+ return [];
+ }
+ return output.stdout
+ .trim()
+ .split('\n')
+ .filter(value => value.trim());
+ });
+ }
+ tryGetConfigKeys(pattern, globalConfig, configFile) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const args = ['config'];
+ if (configFile) {
+ args.push('--file', configFile);
+ }
+ else {
+ args.push(globalConfig ? '--global' : '--local');
+ }
+ args.push('--name-only', '--get-regexp', pattern);
+ const output = yield this.execGit(args, true);
+ if (output.exitCode !== 0) {
+ return [];
+ }
+ return output.stdout
+ .trim()
+ .split('\n')
+ .filter(key => key.trim());
+ });
+ }
tryReset() {
return __awaiter(this, void 0, void 0, function* () {
const output = yield this.execGit(['reset', '--hard', 'HEAD'], true);
diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts
index 126e8e5..0c82ddd 100644
--- a/src/git-auth-helper.ts
+++ b/src/git-auth-helper.ts
@@ -346,8 +346,58 @@ class GitAuthHelper {
}
private async removeToken(): Promise<void> {
- // HTTP extra header
+ // Remove HTTP extra header from local git config and submodule configs
await this.removeGitConfig(this.tokenConfigKey)
+
+ //
+ // Cleanup actions/checkout@v6 style credentials
+ //
+ const skipV6Cleanup = process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP']
+ if (skipV6Cleanup === '1' || skipV6Cleanup?.toLowerCase() === 'true') {
+ core.debug(
+ 'Skipping v6 style cleanup due to ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'
+ )
+ return
+ }
+
+ try {
+ // Collect credentials config paths that need to be removed
+ const credentialsPaths = new Set<string>()
+
+ // Remove includeIf entries that point to git-credentials-*.config files
+ const mainCredentialsPaths = await this.removeIncludeIfCredentials()
+ mainCredentialsPaths.forEach(path => credentialsPaths.add(path))
+
+ // Remove submodule includeIf entries that point to git-credentials-*.config files
+ try {
+ const submoduleConfigPaths =
+ await this.git.getSubmoduleConfigPaths(true)
+ for (const configPath of submoduleConfigPaths) {
+ const submoduleCredentialsPaths =
+ await this.removeIncludeIfCredentials(configPath)
+ submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path))
+ }
+ } catch (err) {
+ core.debug(`Unable to get submodule config paths: ${err}`)
+ }
+
+ // Remove credentials config files
+ for (const credentialsPath of credentialsPaths) {
+ // Only remove credentials config files if they are under RUNNER_TEMP
+ const runnerTemp = process.env['RUNNER_TEMP']
+ if (runnerTemp && credentialsPath.startsWith(runnerTemp)) {
+ try {
+ await io.rmRF(credentialsPath)
+ } catch (err) {
+ core.debug(
+ `Failed to remove credentials config '${credentialsPath}': ${err}`
+ )
+ }
+ }
+ }
+ } catch (err) {
+ core.debug(`Failed to cleanup v6 style credentials: ${err}`)
+ }
}
private async removeGitConfig(
@@ -371,4 +421,59 @@ class GitAuthHelper {
true
)
}
+
+ /**
+ * Removes includeIf entries that point to git-credentials-*.config files.
+ * This handles cleanup of credentials configured by newer versions of the action.
+ * @param configPath Optional path to a specific git config file to operate on
+ * @returns Array of unique credentials config file paths that were found and removed
+ */
+ private async removeIncludeIfCredentials(
+ configPath?: string
+ ): Promise<string[]> {
+ const credentialsPaths = new Set<string>()
+
+ try {
+ // Get all includeIf.gitdir keys
+ const keys = await this.git.tryGetConfigKeys(
+ '^includeIf\\.gitdir:',
+ false, // globalConfig?
+ configPath
+ )
+
+ for (const key of keys) {
+ // Get all values for this key
+ const values = await this.git.tryGetConfigValues(
+ key,
+ false, // globalConfig?
+ configPath
+ )
+ if (values.length > 0) {
+ // Remove only values that match git-credentials-<uuid>.config pattern
+ for (const value of values) {
+ if (this.testCredentialsConfigPath(value)) {
+ credentialsPaths.add(value)
+ await this.git.tryConfigUnsetValue(key, value, false, configPath)
+ }
+ }
+ }
+ }
+ } catch (err) {
+ // Ignore errors - this is cleanup code
+ core.debug(
+ `Error during includeIf cleanup${configPath ? ` for ${configPath}` : ''}: ${err}`
+ )
+ }
+
+ return Array.from(credentialsPaths)
+ }
+
+ /**
+ * Tests if a path matches the git-credentials-*.config pattern used by newer versions.
+ * @param path The path to test
+ * @returns True if the path matches the credentials config pattern
+ */
+ private testCredentialsConfigPath(path: string): boolean {
+ return /git-credentials-[0-9a-f-]+\.config$/i.test(path)
+ }
}
diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts
index 8e42a38..9c789ac 100644
--- a/src/git-command-manager.ts
+++ b/src/git-command-manager.ts
@@ -41,6 +41,7 @@ export interface IGitCommandManager {
}
): Promise<void>
getDefaultBranch(repositoryUrl: string): Promise<string>
+ getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
getWorkingDirectory(): string
init(): Promise<void>
isDetached(): Promise<boolean>
@@ -59,8 +60,24 @@ export interface IGitCommandManager {
tagExists(pattern: string): Promise<boolean>
tryClean(): Promise<boolean>
tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
+ tryConfigUnsetValue(
+ configKey: string,
+ configValue: string,
+ globalConfig?: boolean,
+ configFile?: string
+ ): Promise<boolean>
tryDisableAutomaticGarbageCollection(): Promise<boolean>
tryGetFetchUrl(): Promise<string>
+ tryGetConfigValues(
+ configKey: string,
+ globalConfig?: boolean,
+ configFile?: string
+ ): Promise<string[]>
+ tryGetConfigKeys(
+ pattern: string,
+ globalConfig?: boolean,
+ configFile?: string
+ ): Promise<string[]>
tryReset(): Promise<boolean>
version(): Promise<GitVersion>
}
@@ -323,6 +340,21 @@ class GitCommandManager {
throw new Error('Unexpected output when retrieving default branch')
}
+ async getSubmoduleConfigPaths(recursive: boolean): Promise<string[]> {
+ // Get submodule config file paths.
+ // Use `--show-origin` to get the config file path for each submodule.
+ const output = await this.submoduleForeach(
+ `git config --local --show-origin --name-only --get-regexp remote.origin.url`,
+ recursive
+ )
+
+ // Extract config file paths from the output (lines starting with "file:").
+ const configPaths =
+ output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
+
+ return configPaths
+ }
+
getWorkingDirectory(): string {
return this.workingDirectory
}
@@ -455,6 +487,24 @@ class GitCommandManager {
return output.exitCode === 0
}
+ async tryConfigUnsetValue(
+ configKey: string,
+ configValue: string,
+ globalConfig?: boolean,
+ configFile?: string
+ ): Promise<boolean> {
+ const args = ['config']
+ if (configFile) {
+ args.push('--file', configFile)
+ } else {
+ args.push(globalConfig ? '--global' : '--local')
+ }
+ args.push('--unset', configKey, configValue)
+
+ const output = await this.execGit(args, true)
+ return output.exitCode === 0
+ }
+
async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
const output = await this.execGit(
['config', '--local', 'gc.auto', '0'],
@@ -481,6 +531,56 @@ class GitCommandManager {
return stdout
}
+ async tryGetConfigValues(
+ configKey: string,
+ globalConfig?: boolean,
+ configFile?: string
+ ): Promise<string[]> {
+ const args = ['config']
+ if (configFile) {
+ args.push('--file', configFile)
+ } else {
+ args.push(globalConfig ? '--global' : '--local')
+ }
+ args.push('--get-all', configKey)
+
+ const output = await this.execGit(args, true)
+
+ if (output.exitCode !== 0) {
+ return []
+ }
+
+ return output.stdout
+ .trim()
+ .split('\n')
+ .filter(value => value.trim())
+ }
+
+ async tryGetConfigKeys(
+ pattern: string,
+ globalConfig?: boolean,
+ configFile?: string
+ ): Promise<string[]> {
+ const args = ['config']
+ if (configFile) {
+ args.push('--file', configFile)
+ } else {
+ args.push(globalConfig ? '--global' : '--local')
+ }
+ args.push('--name-only', '--get-regexp', pattern)
+
+ const output = await this.execGit(args, true)
+
+ if (output.exitCode !== 0) {
+ return []
+ }
+
+ return output.stdout
+ .trim()
+ .split('\n')
+ .filter(key => key.trim())
+ }
+
async tryReset(): Promise<boolean> {
const output = await this.execGit(['reset', '--hard', 'HEAD'], true)
return output.exitCode === 0