mirror of
				https://kkgithub.com/actions/checkout
				synced 2025-10-31 14:21:50 +08:00 
			
		
		
		
	Persist creds to a separate file
This commit is contained in:
		| @ -43,6 +43,7 @@ class GitAuthHelper { | ||||
|   private sshKeyPath = '' | ||||
|   private sshKnownHostsPath = '' | ||||
|   private temporaryHomePath = '' | ||||
|   private credentialsConfigPath = '' // Path to separate credentials config file in RUNNER_TEMP | ||||
|  | ||||
|   constructor( | ||||
|     gitCommandManager: IGitCommandManager, | ||||
| @ -126,16 +127,21 @@ class GitAuthHelper { | ||||
|  | ||||
|   async configureGlobalAuth(): Promise<void> { | ||||
|     // 'configureTempGlobalConfig' noops if already set, just returns the path | ||||
|     const newGitConfigPath = await this.configureTempGlobalConfig() | ||||
|     await this.configureTempGlobalConfig() | ||||
|     try { | ||||
|       // Configure the token | ||||
|       await this.configureToken(newGitConfigPath, true) | ||||
|       await this.configureToken(true) | ||||
|  | ||||
|       // Configure HTTPS instead of SSH | ||||
|       await this.git.tryConfigUnset(this.insteadOfKey, true) | ||||
|       if (!this.settings.sshKey) { | ||||
|         for (const insteadOfValue of this.insteadOfValues) { | ||||
|           await this.git.config(this.insteadOfKey, insteadOfValue, true, true) | ||||
|           await this.git.config( | ||||
|             this.insteadOfKey, | ||||
|             insteadOfValue, | ||||
|             true, // globalConfig? | ||||
|             true // add? | ||||
|           ) | ||||
|         } | ||||
|       } | ||||
|     } catch (err) { | ||||
| @ -150,24 +156,60 @@ class GitAuthHelper { | ||||
|  | ||||
|   async configureSubmoduleAuth(): Promise<void> { | ||||
|     // Remove possible previous HTTPS instead of SSH | ||||
|     await this.removeGitConfig(this.insteadOfKey, true) | ||||
|     await this.removeSubmoduleGitConfig(this.insteadOfKey) | ||||
|  | ||||
|     if (this.settings.persistCredentials) { | ||||
|       // Configure a placeholder value. This approach avoids the credential being captured | ||||
|       // by process creation audit events, which are commonly logged. For more information, | ||||
|       // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing | ||||
|       const output = await this.git.submoduleForeach( | ||||
|         // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline | ||||
|         `sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`, | ||||
|       // Get the credentials config file path in RUNNER_TEMP | ||||
|       const credentialsConfigPath = this.getCredentialsConfigPath() | ||||
|  | ||||
|       // Container credentials config path | ||||
|       const containerCredentialsPath = path.posix.join( | ||||
|         '/github/runner_temp', | ||||
|         path.basename(credentialsConfigPath) | ||||
|       ) | ||||
|  | ||||
|       // Get submodule config file paths. | ||||
|       const configPaths = await this.git.getSubmoduleConfigPaths( | ||||
|         this.settings.nestedSubmodules | ||||
|       ) | ||||
|  | ||||
|       // Replace the placeholder | ||||
|       const configPaths: string[] = | ||||
|         output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [] | ||||
|       // For each submodule, configure includeIf entries pointing to the shared credentials file. | ||||
|       // Configure both host and container paths to support Docker container actions. | ||||
|       for (const configPath of configPaths) { | ||||
|         core.debug(`Replacing token placeholder in '${configPath}'`) | ||||
|         await this.replaceTokenPlaceholder(configPath) | ||||
|         // Submodule Git directory | ||||
|         let submoduleGitDir = path.dirname(configPath) // The config file is at .git/modules/submodule-name/config | ||||
|         submoduleGitDir = submoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows | ||||
|  | ||||
|         // Configure host includeIf | ||||
|         await this.git.config( | ||||
|           `includeIf.gitdir:${submoduleGitDir}.path`, | ||||
|           credentialsConfigPath, | ||||
|           false, // globalConfig? | ||||
|           false, // add? | ||||
|           configPath | ||||
|         ) | ||||
|  | ||||
|         // Container submodule git directory | ||||
|         const githubWorkspace = process.env['GITHUB_WORKSPACE'] | ||||
|         assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined') | ||||
|         let relativeSubmoduleGitDir = path.relative( | ||||
|           githubWorkspace, | ||||
|           submoduleGitDir | ||||
|         ) | ||||
|         relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows | ||||
|         const containerSubmoduleGitDir = path.posix.join( | ||||
|           '/github/workspace', | ||||
|           relativeSubmoduleGitDir | ||||
|         ) | ||||
|  | ||||
|         // Configure container includeIf | ||||
|         await this.git.config( | ||||
|           `includeIf.gitdir:${containerSubmoduleGitDir}.path`, | ||||
|           containerCredentialsPath, | ||||
|           false, // globalConfig? | ||||
|           false, // add? | ||||
|           configPath | ||||
|         ) | ||||
|       } | ||||
|  | ||||
|       if (this.settings.sshKey) { | ||||
| @ -201,6 +243,10 @@ class GitAuthHelper { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Configures SSH authentication by writing the SSH key and known hosts, | ||||
|    * and setting up the GIT_SSH_COMMAND environment variable. | ||||
|    */ | ||||
|   private async configureSsh(): Promise<void> { | ||||
|     if (!this.settings.sshKey) { | ||||
|       return | ||||
| @ -272,57 +318,116 @@ class GitAuthHelper { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async configureToken( | ||||
|     configPath?: string, | ||||
|     globalConfig?: boolean | ||||
|   ): Promise<void> { | ||||
|     // Validate args | ||||
|     assert.ok( | ||||
|       (configPath && globalConfig) || (!configPath && !globalConfig), | ||||
|       'Unexpected configureToken parameter combinations' | ||||
|     ) | ||||
|   /** | ||||
|    * Configures token-based authentication by creating a credentials config file | ||||
|    * and setting up includeIf entries to reference it. | ||||
|    * @param globalConfig Whether to configure global config instead of local | ||||
|    */ | ||||
|   private async configureToken(globalConfig?: boolean): Promise<void> { | ||||
|     // Get the credentials config file path in RUNNER_TEMP | ||||
|     const credentialsConfigPath = this.getCredentialsConfigPath() | ||||
|  | ||||
|     // Default config path | ||||
|     if (!configPath && !globalConfig) { | ||||
|       configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config') | ||||
|     } | ||||
|  | ||||
|     // Configure a placeholder value. This approach avoids the credential being captured | ||||
|     // by process creation audit events, which are commonly logged. For more information, | ||||
|     // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing | ||||
|     // Write placeholder to the separate credentials config file using git config. | ||||
|     // This approach avoids the credential being captured by process creation audit events, | ||||
|     // which are commonly logged. For more information, refer to | ||||
|     // https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing | ||||
|     await this.git.config( | ||||
|       this.tokenConfigKey, | ||||
|       this.tokenPlaceholderConfigValue, | ||||
|       globalConfig | ||||
|       false, // globalConfig? | ||||
|       false, // add? | ||||
|       credentialsConfigPath | ||||
|     ) | ||||
|  | ||||
|     // Replace the placeholder | ||||
|     await this.replaceTokenPlaceholder(configPath || '') | ||||
|   } | ||||
|  | ||||
|   private async replaceTokenPlaceholder(configPath: string): Promise<void> { | ||||
|     assert.ok(configPath, 'configPath is not defined') | ||||
|     let content = (await fs.promises.readFile(configPath)).toString() | ||||
|     // Replace the placeholder in the credentials config file | ||||
|     let content = (await fs.promises.readFile(credentialsConfigPath)).toString() | ||||
|     const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue) | ||||
|     if ( | ||||
|       placeholderIndex < 0 || | ||||
|       placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue) | ||||
|     ) { | ||||
|       throw new Error(`Unable to replace auth placeholder in ${configPath}`) | ||||
|       throw new Error( | ||||
|         `Unable to replace auth placeholder in ${credentialsConfigPath}` | ||||
|       ) | ||||
|     } | ||||
|     assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined') | ||||
|     content = content.replace( | ||||
|       this.tokenPlaceholderConfigValue, | ||||
|       this.tokenConfigValue | ||||
|     ) | ||||
|     await fs.promises.writeFile(configPath, content) | ||||
|     await fs.promises.writeFile(credentialsConfigPath, content) | ||||
|  | ||||
|     // Add include or includeIf to reference the credentials config | ||||
|     if (globalConfig) { | ||||
|       // Global config file is temporary | ||||
|       await this.git.config( | ||||
|         'include.path', | ||||
|         credentialsConfigPath, | ||||
|         true // globalConfig? | ||||
|       ) | ||||
|     } else { | ||||
|       // Host git directory | ||||
|       let gitDir = path.join(this.git.getWorkingDirectory(), '.git') | ||||
|       gitDir = gitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows | ||||
|  | ||||
|       // Configure host includeIf | ||||
|       const hostIncludeKey = `includeIf.gitdir:${gitDir}.path` | ||||
|       await this.git.config(hostIncludeKey, credentialsConfigPath) | ||||
|  | ||||
|       // Container git directory | ||||
|       const workingDirectory = this.git.getWorkingDirectory() | ||||
|       const githubWorkspace = process.env['GITHUB_WORKSPACE'] | ||||
|       assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined') | ||||
|       let relativePath = path.relative(githubWorkspace, workingDirectory) | ||||
|       relativePath = relativePath.replace(/\\/g, '/') // Use forward slashes, even on Windows | ||||
|       const containerGitDir = path.posix.join( | ||||
|         '/github/workspace', | ||||
|         relativePath, | ||||
|         '.git' | ||||
|       ) | ||||
|  | ||||
|       // Container credentials config path | ||||
|       const containerCredentialsPath = path.posix.join( | ||||
|         '/github/runner_temp', | ||||
|         path.basename(credentialsConfigPath) | ||||
|       ) | ||||
|  | ||||
|       // Configure container includeIf | ||||
|       const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path` | ||||
|       await this.git.config(containerIncludeKey, containerCredentialsPath) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Gets or creates the path to the credentials config file in RUNNER_TEMP. | ||||
|    * @returns The absolute path to the credentials config file | ||||
|    */ | ||||
|   private getCredentialsConfigPath(): string { | ||||
|     if (this.credentialsConfigPath) { | ||||
|       return this.credentialsConfigPath | ||||
|     } | ||||
|  | ||||
|     const runnerTemp = process.env['RUNNER_TEMP'] || '' | ||||
|     assert.ok(runnerTemp, 'RUNNER_TEMP is not defined') | ||||
|  | ||||
|     // Create a unique filename for this checkout instance | ||||
|     const configFileName = `git-credentials-${uuid()}.config` | ||||
|     this.credentialsConfigPath = path.join(runnerTemp, configFileName) | ||||
|  | ||||
|     core.debug(`Credentials config path: ${this.credentialsConfigPath}`) | ||||
|     return this.credentialsConfigPath | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Removes SSH authentication configuration by cleaning up SSH keys, | ||||
|    * known hosts files, and SSH command configurations. | ||||
|    */ | ||||
|   private async removeSsh(): Promise<void> { | ||||
|     // SSH key | ||||
|     const keyPath = this.sshKeyPath || stateHelper.SshKeyPath | ||||
|     if (keyPath) { | ||||
|       try { | ||||
|         core.info(`Removing SSH key '${keyPath}'`) | ||||
|         await io.rmRF(keyPath) | ||||
|       } catch (err) { | ||||
|         core.debug(`${(err as any)?.message ?? err}`) | ||||
| @ -335,40 +440,149 @@ class GitAuthHelper { | ||||
|       this.sshKnownHostsPath || stateHelper.SshKnownHostsPath | ||||
|     if (knownHostsPath) { | ||||
|       try { | ||||
|         core.info(`Removing SSH known hosts '${knownHostsPath}'`) | ||||
|         await io.rmRF(knownHostsPath) | ||||
|       } catch { | ||||
|         // Intentionally empty | ||||
|       } catch (err) { | ||||
|         core.debug(`${(err as any)?.message ?? err}`) | ||||
|         core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`) | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // SSH command | ||||
|     core.info('Removing SSH command configuration') | ||||
|     await this.removeGitConfig(SSH_COMMAND_KEY) | ||||
|     await this.removeSubmoduleGitConfig(SSH_COMMAND_KEY) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Removes token-based authentication by cleaning up HTTP headers, | ||||
|    * includeIf entries, and credentials config files. | ||||
|    */ | ||||
|   private async removeToken(): Promise<void> { | ||||
|     // HTTP extra header | ||||
|     // Remove HTTP extra header | ||||
|     core.info('Removing HTTP extra header') | ||||
|     await this.removeGitConfig(this.tokenConfigKey) | ||||
|   } | ||||
|     await this.removeSubmoduleGitConfig(this.tokenConfigKey) | ||||
|  | ||||
|   private async removeGitConfig( | ||||
|     configKey: string, | ||||
|     submoduleOnly: boolean = false | ||||
|   ): Promise<void> { | ||||
|     if (!submoduleOnly) { | ||||
|       if ( | ||||
|         (await this.git.configExists(configKey)) && | ||||
|         !(await this.git.tryConfigUnset(configKey)) | ||||
|       ) { | ||||
|         // Load the config contents | ||||
|         core.warning(`Failed to remove '${configKey}' from the git config`) | ||||
|       } | ||||
|     // Collect credentials config paths that need to be removed | ||||
|     const credentialsPaths = new Set<string>() | ||||
|  | ||||
|     // Remove includeIf entries that point to git-credentials-*.config files | ||||
|     core.info('Removing includeIf entries pointing to 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 | ||||
|     const submoduleConfigPaths = await this.git.getSubmoduleConfigPaths(true) | ||||
|     for (const configPath of submoduleConfigPaths) { | ||||
|       const submoduleCredentialsPaths = | ||||
|         await this.removeIncludeIfCredentials(configPath) | ||||
|       submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path)) | ||||
|     } | ||||
|  | ||||
|     // 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'] | ||||
|       assert.ok(runnerTemp, 'RUNNER_TEMP is not defined') | ||||
|       if (credentialsPath.startsWith(runnerTemp)) { | ||||
|         try { | ||||
|           core.info(`Removing credentials config '${credentialsPath}'`) | ||||
|           await io.rmRF(credentialsPath) | ||||
|         } catch (err) { | ||||
|           core.debug(`${(err as any)?.message ?? err}`) | ||||
|           core.warning( | ||||
|             `Failed to remove credentials config '${credentialsPath}'` | ||||
|           ) | ||||
|         } | ||||
|       } else { | ||||
|         core.debug( | ||||
|           `Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP` | ||||
|         ) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Removes a git config key from the local repository config. | ||||
|    * @param configKey The git config key to remove | ||||
|    */ | ||||
|   private async removeGitConfig(configKey: string): Promise<void> { | ||||
|     if ( | ||||
|       (await this.git.configExists(configKey)) && | ||||
|       !(await this.git.tryConfigUnset(configKey)) | ||||
|     ) { | ||||
|       // Load the config contents | ||||
|       core.warning(`Failed to remove '${configKey}' from the git config`) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Removes a git config key from all submodule configs. | ||||
|    * @param configKey The git config key to remove | ||||
|    */ | ||||
|   private async removeSubmoduleGitConfig(configKey: string): Promise<void> { | ||||
|     const pattern = regexpHelper.escape(configKey) | ||||
|     await this.git.submoduleForeach( | ||||
|       // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline | ||||
|       // Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline. | ||||
|       `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. | ||||
|    * @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 | ||||
|       if (configPath) { | ||||
|         core.debug(`Error during includeIf cleanup for ${configPath}: ${err}`) | ||||
|       } else { | ||||
|         core.debug(`Error during includeIf cleanup: ${err}`) | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return Array.from(credentialsPaths) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Tests if a path matches the git-credentials-*.config pattern. | ||||
|    * @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) | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -28,7 +28,8 @@ export interface IGitCommandManager { | ||||
|     configKey: string, | ||||
|     configValue: string, | ||||
|     globalConfig?: boolean, | ||||
|     add?: boolean | ||||
|     add?: boolean, | ||||
|     configFile?: string | ||||
|   ): Promise<void> | ||||
|   configExists(configKey: string, globalConfig?: boolean): Promise<boolean> | ||||
|   fetch( | ||||
| @ -41,6 +42,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 +61,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> | ||||
| } | ||||
| @ -223,9 +241,15 @@ class GitCommandManager { | ||||
|     configKey: string, | ||||
|     configValue: string, | ||||
|     globalConfig?: boolean, | ||||
|     add?: boolean | ||||
|     add?: boolean, | ||||
|     configFile?: string | ||||
|   ): Promise<void> { | ||||
|     const args: string[] = ['config', globalConfig ? '--global' : '--local'] | ||||
|     const args: string[] = ['config'] | ||||
|     if (configFile) { | ||||
|       args.push('--file', configFile) | ||||
|     } else { | ||||
|       args.push(globalConfig ? '--global' : '--local') | ||||
|     } | ||||
|     if (add) { | ||||
|       args.push('--add') | ||||
|     } | ||||
| @ -323,6 +347,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 +494,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 +538,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 | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 eric sciple
					eric sciple