From 90b2209a982843756ce2f6cf9b86eda3291717dc Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 30 Sep 2020 14:27:12 +0100 Subject: [PATCH 01/25] docs: update readme and FAQs; drop preview wording Update the readme and FAQ documentation to include Linux preview support, and elevate macOS and Windows from "preview" to GA. --- README.md | 49 +++++++++++++++++++++++++++++++++++---------- docs/development.md | 12 ++++++++++- docs/faq.md | 16 ++++++++++----- docs/usage.md | 16 +++++++++++++-- 4 files changed, 74 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index c23bebec1..15a1fe43d 100644 --- a/README.md +++ b/README.md @@ -6,36 +6,39 @@ master|[![Build Status](https://mseng.visualstudio.com/AzureDevOps/_apis/build/s --- -[Git Credential Manager Core](https://github.com/Microsoft/Git-Credential-Manager-Core) (GCM Core) is a secure Git credential helper built on [.NET Core](https://microsoft.com/dotnet) that runs on Windows and macOS. Linux support is planned, but not yet scheduled. +[Git Credential Manager Core](https://github.com/microsoft/Git-Credential-Manager-Core) (GCM Core) is a secure Git credential helper built on [.NET Core](https://microsoft.com/dotnet) that runs on Windows and macOS. Linux support is planned, but not yet scheduled. Compared to Git's [built-in credential helpers]((https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage)) (Windows: wincred, macOS: osxkeychain, Linux: gnome-keyring) which provides single-factor authentication support working on any HTTP-enabled Git repository, GCM Core provides multi-factor authentication support for [Azure DevOps](https://dev.azure.com/), Azure DevOps Server (formerly Team Foundation Server), GitHub, and Bitbucket. -## Public preview +Git Credential Manager Core (GCM Core) replaces the .NET Framework-based [Git Credential Manager for Windows](https://github.com/microsoft/Git-Credential-Manager-for-Windows) (GCM), and the Java-based [Git Credential Manager for Mac and Linux](https://github.com/microsoft/Git-Credential-Manager-for-Mac-and-Linux) (Java GCM), providing a consistent authentication experience across all platforms. -The long-term goal of Git Credential Manager Core (GCM Core) is to converge the .NET Framework-based [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows) (GCM), and the Java-based [Git Credential Manager for Mac and Linux](https://github.com/Microsoft/Git-Credential-Manager-for-Mac-and-Linux) (Java GCM), providing a consistent authentication experience across all platforms. +## Current status -### Current status +Git Credential Manager Core is currently available for macOS and Windows, with Linux support in preview. If the Linux version of GCM Core is insufficient then SSH still remains an option: -Git Credential Manager Core is currently in preview for macOS and Windows. Linux support is planned, but not yet scheduled. For now, we recommend [SSH for authentication to Azure DevOps](https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops) for Linux users. +- [Azure DevOps SSH](https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops) +- [GitHub SSH](https://help.github.com/en/articles/connecting-to-github-with-ssh) +- [Bitbucket SSH](https://confluence.atlassian.com/bitbucket/ssh-keys-935365775.html) Feature|Windows|macOS|Linux -|:-:|:-:|:-: -Installer/uninstaller|✓|✓| -Secure platform credential storage|✓
Windows Credential Manager|✓
macOS Keychain| +Installer/uninstaller|✓|✓|✓\*\* +Secure platform credential storage|✓
Windows
Credential
Manager|✓
macOS Keychain|✓
1. Secret Service
2. `pass`/GPG
3. Plaintext files Multi-factor authentication support for Azure DevOps|✓|✓|✓\* Two-factor authentication support for GitHub|✓|✓\*|✓\* Two-factor authentication support for Bitbucket|✓|✓\*|✓\* Windows Integrated Authentication (NTLM/Kerberos) support|✓|_N/A_|_N/A_ Basic HTTP authentication support|✓|✓|✓ -Proxy support|✓|✓| +Proxy support|✓|✓|✓ **Notes:** (\*) Currently only supported when using Git from the terminal or command line. A platform-native UI experience is not yet available, but planned. +(\*\*) Debian package offered but not yet available on an official Microsoft feed. + ### Planned features -- [ ] Linux support ([#135](https://github.com/microsoft/Git-Credential-Manager-Core/issues/135)) - [ ] macOS/Linux native UI ([#136](https://github.com/microsoft/Git-Credential-Manager-Core/issues/136)) ## Download and Install @@ -67,7 +70,7 @@ brew cask uninstall git-credential-manager-core ### macOS Package -We also provide a [.pkg installer](https://github.com/Microsoft/Git-Credential-Manager-Core/releases/latest) with each release. To install, double-click the installation package and follow the instructions presented. +We also provide a [.pkg installer](https://github.com/microsoft/Git-Credential-Manager-Core/releases/latest) with each release. To install, double-click the installation package and follow the instructions presented. #### Uninstall @@ -79,9 +82,33 @@ sudo /usr/local/share/gcm-core/uninstall.sh --- +### Linux Debian package (.deb) + +Download the latest [.deb package](https://github.com/microsoft/Git-Credential-Manager-Core/releases/latest), and run the following: + +```shell +sudo dpkg -i +git-credential-manager-core configure +``` + +Note that Linux distributions [require additional configuration](https://aka.ms/gcmcore-linuxcredstores) to use GCM Core. + +--- + +### Linux tarball (.tar.gz) + +Download the latest [tarball](https://github.com/microsoft/Git-Credential-Manager-Core/releases/latest), and run the following: + +```shell +tar -xvf -C /usr/local/bin +git-credential-manager-core configure +``` + +--- + ### Windows -You can download the [latest installer](https://github.com/Microsoft/Git-Credential-Manager-Core/releases/latest) for Windows. To install, double-click the installation package and follow the instructions presented. +You can download the [latest installer](https://github.com/microsoft/Git-Credential-Manager-Core/releases/latest) for Windows. To install, double-click the installation package and follow the instructions presented. #### Git Credential Manager for Windows diff --git a/docs/development.md b/docs/development.md index 7103be288..6db8a219a 100644 --- a/docs/development.md +++ b/docs/development.md @@ -41,7 +41,17 @@ The flat binaries can also be found in `out\windows\Payload.Windows\bin\Debug\ne ### Linux -_No information yet._ +The two available solution configurations are `LinuxDebug` and `LinuxRelease`. + +To build from the command line, run: + +```shell +dotnet build -c LinuxDebug +``` + +You can find a copy of the Debian package (.deb) file in `out/linux/Packaging.Linux/deb/Debug`. + +The flat binaries can also be found in `out/linux/Packaging.Linux/payload/Debug`. ## Debugging diff --git a/docs/faq.md b/docs/faq.md index 5f2cb2a72..4ec8bfa27 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -24,6 +24,10 @@ Please make sure your remote URLs use "https://" rather than "http://". You probably need to configure Git and GCM Core to use a proxy. Please see detailed information [here](https://aka.ms/gcmcore-httpproxy). +### Q: I'm getting errors about picking a credential store on Linux. + +On Linux you must [select and configure a credential store](https://aka.ms/gcmcore-linuxcredstores), as due to the varied nature of distributions and installations, we cannot guarantee a suitable storage solution is available. + ## About the project ### Q: How does this project relate to [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows) and [Git Credential Manager for Mac and Linux](https://github.com/Microsoft/Git-Credential-Manager-for-Mac-and-Linux)? @@ -35,13 +39,15 @@ Git Credential Manager Core (GCM Core; this project) aims to replace both GCM Wi ### Q: Does this mean GCM for Windows (.NET Framework-based) is deprecated? -No. Git Credential Manager for Windows (GCM Windows) will continue to be supported until such a time that GCM Core is a complete replacement. +Yes. Git Credential Manager for Windows (GCM Windows) is no longer receiving updates and fixes. All development effort has now been directed to GCM Core. GCM Core is available as an credential helper option in Git for Windows 2.28, and will be made the default helper in 2.29. ### Q: Does this mean the Java-based GCM for Mac/Linux is deprecated? -Yes. Usage of Git Credential Manager for Mac and Linux (Java GCM) should be replaced with SSH keys. If you wish to take part in the public preview of GCM Core on macOS please feel free to install the latest preview release and give feedback! Otherwise, using SSH would be preferred on macOS and Linux to Java GCM. +Yes. Usage of Git Credential Manager for Mac and Linux (Java GCM) should be replaced with GCM Core or SSH keys. If you wish to install GCM Core on macOS or Linux, please follow the [download and installation instructions](../README.md#download-and-install). + +### Q: I want to use SSH -SSH configuration instructions: +GCM Core is for HTTPS only. To use SSH please follow the below links: - [Azure DevOps](https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops) - [GitHub](https://help.github.com/en/articles/connecting-to-github-with-ssh) @@ -51,9 +57,9 @@ SSH configuration instructions: GCM Windows was not designed with a cross-platform architecture. -### What level of support does GCM Core have during the public preview? +### What level of support does GCM Core have? -Support will be best-effort. We would really appreciate your feedback as we work to make this a great experience across each platform we support. However, for mission critical applications, please use GCM for Windows on Windows or SSH on Mac and Linux. +Support will be best-effort. We would really appreciate your feedback to make this a great experience across each platform we support. ### Q: Why does GCM Core not support operating system/distribution 'X', or Git hosting provider 'Y'? diff --git a/docs/usage.md b/docs/usage.md index d41c93eca..fb0b260f2 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -6,15 +6,27 @@ GCM Core stays invisible as much as possible, so ideally you’ll forget that yo Assuming GCM Core has been installed, use your favorite terminal to execute the following commands to interact directly with GCM. ```shell -git credential-manager [ []] +git credential-manager-core [ []] ``` ## Commands -### version +### help / --help + +Displays a list of available commands. + +### version / --version Displays the current version. ### get / store / erase Commands for interaction with Git. You shouldn't need to run these manually. + +Read the [Git manual](https://git-scm.com/docs/gitcredentials#_custom_helpers) about custom helpers for more information. + +### configure/unconfigure + +Set your user-level Git configuration (`~/.gitconfig`) to use GCM Core. If you pass +`--system` to these commands, they act on the system-level Git configuration +(`/etc/gitconfig`) instead. From ef29f9adf57abac337a00169fe85b57dcad884e7 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 1 Oct 2020 10:55:46 +0100 Subject: [PATCH 02/25] Update readme to say Linux support is in preview --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 15a1fe43d..cd0989bb4 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ master|[![Build Status](https://mseng.visualstudio.com/AzureDevOps/_apis/build/s --- -[Git Credential Manager Core](https://github.com/microsoft/Git-Credential-Manager-Core) (GCM Core) is a secure Git credential helper built on [.NET Core](https://microsoft.com/dotnet) that runs on Windows and macOS. Linux support is planned, but not yet scheduled. +[Git Credential Manager Core](https://github.com/microsoft/Git-Credential-Manager-Core) (GCM Core) is a secure Git credential helper built on [.NET Core](https://microsoft.com/dotnet) that runs on Windows and macOS. Linux support is in an early preview. Compared to Git's [built-in credential helpers]((https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage)) (Windows: wincred, macOS: osxkeychain, Linux: gnome-keyring) which provides single-factor authentication support working on any HTTP-enabled Git repository, GCM Core provides multi-factor authentication support for [Azure DevOps](https://dev.azure.com/), Azure DevOps Server (formerly Team Foundation Server), GitHub, and Bitbucket. From bc52940ff40a042068fcdb58028d03b77ea2e769 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Fri, 2 Oct 2020 15:20:03 +0100 Subject: [PATCH 03/25] winget: update winget pkg on release Automatically create a new PR to update the winget package for GCM Core on release. --- .github/workflows/release-winget.yaml | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/release-winget.yaml diff --git a/.github/workflows/release-winget.yaml b/.github/workflows/release-winget.yaml new file mode 100644 index 000000000..552f62970 --- /dev/null +++ b/.github/workflows/release-winget.yaml @@ -0,0 +1,32 @@ +name: "release-winget" +on: + release: + types: [released] + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Update winget repository + uses: mjcheetham/update-winget@v1.0 + with: + token: ${{ secrets.WINGET_TOKEN }} + repo: microsoft/winget-pkgs + id: Microsoft.GitCredentialManagerCore + releaseAsset: gcmcore-win-x86-(.*)\.exe + manifestText: | + Id: {{id}} + Version: {{version}} + Name: Git Credential Manager Core + Publisher: Microsoft Corporation + AppMoniker: git-credential-manager-core + Homepage: https://aka.ms/gcmcore + Tags: "gcm, gcmcore, git, credential" + License: Copyright (C) Microsoft Corporation + Description: Secure, cross-platform Git credential storage with authentication to GitHub, Azure Repos, and other popular Git hosting services. + Installers: + - Arch: x86 + Url: {{url}} + InstallerType: Inno + Sha256: {{sha256}} + alwaysUsePullRequest: true From 3b4e022d52a894eaa41e8b5f1e7f2882635c4a00 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 19 Oct 2020 13:20:55 +0100 Subject: [PATCH 04/25] docs: add GitHub auth API deprecation docs Add documentation regarding the upcoming removal of user/pass access to the authorizations API on GitHub. Outline how to upgrade to GCM Core from GCM4W to fix. --- README.md | 6 ++ docs/github-apideprecation.md | 121 ++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 docs/github-apideprecation.md diff --git a/README.md b/README.md index cd0989bb4..8708dcb5c 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,12 @@ brew tap microsoft/git brew cask install git-credential-manager-core ``` +After installing you can stay up-to-date with new releases by running: + +```shell +brew upgrade git-credential-manager-core +``` + #### Git Credential Manager for Mac and Linux (Java-based GCM) If you have an existing installation of the 'Java GCM' on macOS and you have installed this using Homebrew, this installation will be unlinked (`brew unlink git-credential-manager`) when GCM Core is installed. diff --git a/docs/github-apideprecation.md b/docs/github-apideprecation.md new file mode 100644 index 000000000..e7786a7b7 --- /dev/null +++ b/docs/github-apideprecation.md @@ -0,0 +1,121 @@ +# GitHub Authentication Deprecation + +## What's going on? + +GitHub now [requires token-based authentication](https://github.blog/2020-07-30-token-authentication-requirements-for-api-and-git-operations/) to +call their APIs, and in the future, use Git itself. + +This means Git credential helpers such as [Git Credential Manager (GCM) for +Windows](https://github.com/microsoft/Git-Credential-Manager-for-Windows), and +old versions of [GCM Core](https://aka.ms/gcmcore) that offer username/password +flows **will not be able to create new access tokens** for accessing Git +repositories. + +If you already have tokens generated by Git credential helpers like GCM for +Windows, they will continue to work until they expire or are revoked/deleted. + +## What should I do now? + +### Windows command-line users + +The best thing to do right now is upgrade to the latest Git for Windows (at +least version 2.29), which includes a version of Git Credential Manger Core that +uses supported OAuth token-based authentication. + +[Download the latest Git for Windows ⬇️](https://git-scm.com/download/win) + +### Visual Studio users + +Please update to the latest supported release of Visual Studio, that includes +GCM Core and support for OAuth token-based authentication. + +- [Visual Studio 2019 ⬇️](https://docs.microsoft.com/en-us/visualstudio/install/update-visual-studio?view=vs-2019) +- [Visual Studio 2017 ⬇️](https://docs.microsoft.com/en-us/visualstudio/install/update-visual-studio?view=vs-2017) + +### SSH, macOS, and Linux users + +If you are using SSH this change does **not** affect you. + +If you are using an older version of Git Credential Manager Core (before +2.0.124-beta) please upgrade to the latest version following [these +instructions](https://github.com/microsoft/Git-Credential-Manager-Core#download-and-install). + +## What if I cannot upgrade Git for Windows? + +If you are unable to upgrade Git for Windows, you can manually install Git +Credential Manager Core as a standalone install. This will override the older, +GCM for Windows bundled with the Git for Windows installation. + +[Download Git Credential Manager Core standalone ⬇️](https://aka.ms/gcmcore-latest) + +## What if I cannot use Git Credential Manager Core? + +If you are unable to use Git Credential Manager Core due to a bug or +compatibility issue we'd [like to know why](https://github.com/microsoft/Git-Credential-Manager-Core/issues/new/choose)! + +## Help! I cannot make any changes to my Windows machine without an Administrator! + +If you do not have permission to change your installation (for example in a +corporate environment) there is a workaround which should work and does not +require administrator permissions. + +0. Tell your system administrator they should start planning to upgrade the + installed version of Git for Windows to at least 2.29! 😁 + +1. Go to https://github.com/settings/tokens/new to [create a new personal access + token (PAT)](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token). + +2. Enter a name ("note") for the token and select the `repo`, `gist`, and + `workflow` scopes: +![image](https://user-images.githubusercontent.com/5658207/95448332-1beb2000-095b-11eb-9a48-9c05b1926a6b.png) +... +![image](https://user-images.githubusercontent.com/5658207/95447304-6f5c6e80-0959-11eb-924b-50b86c2b3d77.png) +... +![image](https://user-images.githubusercontent.com/5658207/95447450-a3d02a80-0959-11eb-82a8-2d2834d5aa16.png) +... +![image](https://user-images.githubusercontent.com/5658207/95447343-7b483080-0959-11eb-8e00-151d53893f3f.png) + +3. Click "Generate Token" + +![image](https://user-images.githubusercontent.com/5658207/95448393-31f8e080-095b-11eb-9568-cfd1c567a65c.png) + +4. **[IMPORTANT]** Keep the resulting page open as this contains your new token + (this will only be displayed once!) + +![image](https://user-images.githubusercontent.com/5658207/95448288-ff4ee800-095a-11eb-9709-8e37bde8b716.png) + +5. Save the generated PAT in the Windows Credential Manager: + + 1. If you prefer to use the command-line, open a command prompt (cmd.exe) and + type the following: + + ```bash + cmdkey /generic:git:https://github.com /user:PersonalAccessToken /pass + ``` + + You will prompted to enter a password – copy the newly generated PAT in + step 4 and paste it here, and press Enter + + ![image](https://user-images.githubusercontent.com/5658207/95448479-4fc64580-095b-11eb-9970-0b6faf7f4ae7.png) + + 1. If you do not wish to use the command-line, [open the Credential Manager + via Control Panel](https://support.microsoft.com/en-us/windows/accessing-credential-manager-1b5c916a-6a16-889f-8581-fc16e8165ac0) + and select the "Windows Credentials" tab. + + ![image](https://user-images.githubusercontent.com/5658207/96468389-f6e09200-1223-11eb-9993-ae7b4096b769.png) + + Click "Add a generic credential", and enter the following details: + + - Internet or network address: `git:https://github.com` + - Username: `PersonalAccessToken` + - Password: _(copy and paste the PAT generated in step 4 here)_ + + ![image](https://user-images.githubusercontent.com/5658207/96468318-ddd7e100-1223-11eb-8cd4-aa118493c538.png) + +## What about GitHub Enterprise Server (GHES)? + +As mentioned in [the blog post](https://github.blog/2020-07-30-token-authentication-requirements-for-api-and-git-operations/), +the new token-based authentication requirements **DO NOT** apply to GHES: + +> We have not announced any changes to GitHub Enterprise Server, which remains +> unaffected at this time. From 034027922e05d9311101c84d31716541431cfcc7 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 26 Oct 2020 14:22:20 +0000 Subject: [PATCH 05/25] Update GitHub auth change document PAT link Update the GitHub auth change document PAT link to auto-select the `repo`, `gist`, and `workflow` scopes. --- docs/github-apideprecation.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/github-apideprecation.md b/docs/github-apideprecation.md index e7786a7b7..0b419f0da 100644 --- a/docs/github-apideprecation.md +++ b/docs/github-apideprecation.md @@ -62,11 +62,10 @@ require administrator permissions. 0. Tell your system administrator they should start planning to upgrade the installed version of Git for Windows to at least 2.29! 😁 -1. Go to https://github.com/settings/tokens/new to [create a new personal access - token (PAT)](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token). +1. [Create a new personal access token](https://github.com/settings/tokens/new?scopes=repo,gist,workflow) (see official [documentation](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token)) -2. Enter a name ("note") for the token and select the `repo`, `gist`, and - `workflow` scopes: +2. Enter a name ("note") for the token and ensure the `repo`, `gist`, and + `workflow` scopes are selected: ![image](https://user-images.githubusercontent.com/5658207/95448332-1beb2000-095b-11eb-9a48-9c05b1926a6b.png) ... ![image](https://user-images.githubusercontent.com/5658207/95447304-6f5c6e80-0959-11eb-924b-50b86c2b3d77.png) From abaaf67af3fc9c24bf3c0e71cb74e317dd333abd Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 26 Oct 2020 16:18:53 +0000 Subject: [PATCH 06/25] wia: fix bug where Allow WIA setting default wrong Fix a bug where the 'Allow Windows Integrated Authentication' setting was evaluating to `false` when unset, rather than `true` as per design and the documentation. --- .../SettingsTests.cs | 109 ++++++++++++++++++ .../Settings.cs | 2 +- 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/src/shared/Microsoft.Git.CredentialManager.Tests/SettingsTests.cs b/src/shared/Microsoft.Git.CredentialManager.Tests/SettingsTests.cs index dd8cef277..0bc16592d 100644 --- a/src/shared/Microsoft.Git.CredentialManager.Tests/SettingsTests.cs +++ b/src/shared/Microsoft.Git.CredentialManager.Tests/SettingsTests.cs @@ -319,6 +319,115 @@ public void Settings_IsSecretTracingEnabled_EnvarFalsey_ReturnsFalse() Assert.False(settings.IsSecretTracingEnabled); } + [Fact] + public void Settings_IsWindowsIntegratedAuthenticationEnabled_EnvarUnset_ReturnsTrue() + { + var envars = new TestEnvironment(); + var git = new TestGit(); + + var settings = new Settings(envars, git); + + Assert.True(settings.IsWindowsIntegratedAuthenticationEnabled); + } + + [Fact] + public void Settings_IsWindowsIntegratedAuthenticationEnabled_EnvarTruthy_ReturnsTrue() + { + var envars = new TestEnvironment + { + Variables = {[Constants.EnvironmentVariables.GcmAllowWia] = "1"} + }; + var git = new TestGit(); + + var settings = new Settings(envars, git); + + Assert.True(settings.IsWindowsIntegratedAuthenticationEnabled); + } + + [Fact] + public void Settings_IsWindowsIntegratedAuthenticationEnabled_EnvarFalsey_ReturnsFalse() + { + var envars = new TestEnvironment + { + Variables = {[Constants.EnvironmentVariables.GcmAllowWia] = "0"}, + }; + var git = new TestGit(); + + var settings = new Settings(envars, git); + + Assert.False(settings.IsWindowsIntegratedAuthenticationEnabled); + } + + [Fact] + public void Settings_IsWindowsIntegratedAuthenticationEnabled_EnvarNonBooleanyValue_ReturnsTrue() + { + var envars = new TestEnvironment + { + Variables = {[Constants.EnvironmentVariables.GcmAllowWia] = Guid.NewGuid().ToString("N")}, + }; + var git = new TestGit(); + + var settings = new Settings(envars, git); + + Assert.True(settings.IsWindowsIntegratedAuthenticationEnabled); + } + + [Fact] + public void Settings_IsWindowsIntegratedAuthenticationEnabled_ConfigUnset_ReturnsTrue() + { + var envars = new TestEnvironment(); + var git = new TestGit(); + + var settings = new Settings(envars, git); + + Assert.True(settings.IsWindowsIntegratedAuthenticationEnabled); + } + + [Fact] + public void Settings_IsWindowsIntegratedAuthenticationEnabled_ConfigTruthy_ReturnsTrue() + { + const string section = Constants.GitConfiguration.Credential.SectionName; + const string property = Constants.GitConfiguration.Credential.AllowWia; + + var envars = new TestEnvironment(); + var git = new TestGit(); + git.GlobalConfiguration[$"{section}.{property}"] = "1"; + + var settings = new Settings(envars, git); + + Assert.True(settings.IsWindowsIntegratedAuthenticationEnabled); + } + + [Fact] + public void Settings_IsWindowsIntegratedAuthenticationEnabled_ConfigFalsey_ReturnsFalse() + { + const string section = Constants.GitConfiguration.Credential.SectionName; + const string property = Constants.GitConfiguration.Credential.AllowWia; + + var envars = new TestEnvironment(); + var git = new TestGit(); + git.GlobalConfiguration[$"{section}.{property}"] = "0"; + + var settings = new Settings(envars, git); + + Assert.False(settings.IsWindowsIntegratedAuthenticationEnabled); + } + + [Fact] + public void Settings_IsWindowsIntegratedAuthenticationEnabled_ConfigNonBooleanyValue_ReturnsTrue() + { + const string section = Constants.GitConfiguration.Credential.SectionName; + const string property = Constants.GitConfiguration.Credential.AllowWia; + + var envars = new TestEnvironment(); + var git = new TestGit(); + git.GlobalConfiguration[$"{section}.{property}"] = Guid.NewGuid().ToString(); + + var settings = new Settings(envars, git); + + Assert.True(settings.IsWindowsIntegratedAuthenticationEnabled); + } + [Fact] public void Settings_ProxyConfiguration_Unset_ReturnsNull() { diff --git a/src/shared/Microsoft.Git.CredentialManager/Settings.cs b/src/shared/Microsoft.Git.CredentialManager/Settings.cs index 211eae153..11fef2937 100644 --- a/src/shared/Microsoft.Git.CredentialManager/Settings.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Settings.cs @@ -311,7 +311,7 @@ public bool IsInteractionAllowed TryGetSetting(KnownEnvars.GcmAuthority, GitCredCfg.SectionName, GitCredCfg.Authority, out string authority) ? authority : null; public bool IsWindowsIntegratedAuthenticationEnabled => - TryGetSetting(KnownEnvars.GcmAllowWia, GitCredCfg.SectionName, GitCredCfg.AllowWia, out string value) && value.ToBooleanyOrDefault(true); + !TryGetSetting(KnownEnvars.GcmAllowWia, GitCredCfg.SectionName, GitCredCfg.AllowWia, out string value) || value.ToBooleanyOrDefault(true); public bool IsCertificateVerificationEnabled { From 4514b0f9188ec928a1183594e623133ff08a8b74 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 28 Oct 2020 16:06:18 +0000 Subject: [PATCH 07/25] osx: update the Mac installer to make product archives Update the macOS installer build to also create a distribution/product archive package, that contains the flat component package we were already building. The product archive allows us to do things like set welcome and conclusion messages, titles, and display a license (like the Windows installers do). In the new welcome and conclusion screens we include helpful links and instructions on how to uninstall GCM Core, and how to configure it for other users on the system. The macOS installer continues to install in a global location, but only configure the user's global Git configuration. --- src/osx/Installer.Mac/build.sh | 10 ++- src/osx/Installer.Mac/dist.sh | 73 ++++++++++++++++++ src/osx/Installer.Mac/distribution.xml | 21 +++++ src/osx/Installer.Mac/pack.sh | 14 ++-- .../Installer.Mac/resources/background.png | Bin 0 -> 16922 bytes .../Installer.Mac/resources/en.lproj/LICENSE | 1 + .../resources/en.lproj/conclusion.html | 41 ++++++++++ .../resources/en.lproj/welcome.html | 34 ++++++++ src/osx/Installer.Mac/scripts/postinstall | 7 +- src/osx/Installer.Mac/uninstall.sh | 12 ++- 10 files changed, 197 insertions(+), 16 deletions(-) create mode 100755 src/osx/Installer.Mac/dist.sh create mode 100644 src/osx/Installer.Mac/distribution.xml create mode 100644 src/osx/Installer.Mac/resources/background.png create mode 120000 src/osx/Installer.Mac/resources/en.lproj/LICENSE create mode 100644 src/osx/Installer.Mac/resources/en.lproj/conclusion.html create mode 100644 src/osx/Installer.Mac/resources/en.lproj/welcome.html diff --git a/src/osx/Installer.Mac/build.sh b/src/osx/Installer.Mac/build.sh index 466eb5f6e..ba5308da1 100755 --- a/src/osx/Installer.Mac/build.sh +++ b/src/osx/Installer.Mac/build.sh @@ -38,11 +38,15 @@ if [ -z "$VERSION" ]; then die "--version was not set" fi -PAYLOAD="$INSTALLER_OUT/pkg/$CONFIGURATION/payload" -PKGOUT="$INSTALLER_OUT/pkg/$CONFIGURATION/gcmcore-osx-$VERSION.pkg" +OUTDIR="$INSTALLER_OUT/pkg/$CONFIGURATION" +PAYLOAD="$OUTDIR/payload" +COMPONENTDIR="$OUTDIR/components" +COMPONENTOUT="$COMPONENTDIR/com.microsoft.gitcredentialmanager.component.pkg" +DISTOUT="$OUTDIR/gcmcore-osx-$VERSION.pkg" # Layout and pack "$INSTALLER_SRC/layout.sh" --configuration="$CONFIGURATION" --output="$PAYLOAD" || exit 1 -"$INSTALLER_SRC/pack.sh" --payload="$PAYLOAD" --version="$VERSION" --output="$PKGOUT" || exit 1 +"$INSTALLER_SRC/pack.sh" --payload="$PAYLOAD" --version="$VERSION" --output="$COMPONENTOUT" || exit 1 +"$INSTALLER_SRC/dist.sh" --package-path="$COMPONENTDIR" --version="$VERSION" --output="$DISTOUT" || exit 1 echo "Build of Installer.Mac complete." diff --git a/src/osx/Installer.Mac/dist.sh b/src/osx/Installer.Mac/dist.sh new file mode 100755 index 000000000..749231583 --- /dev/null +++ b/src/osx/Installer.Mac/dist.sh @@ -0,0 +1,73 @@ +#!/bin/bash +die () { + echo "$*" >&2 + exit 1 +} + +# Directories +THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" +ROOT="$( cd "$THISDIR"/../../.. ; pwd -P )" +SRC="$ROOT/src" +OUT="$ROOT/out" +INSTALLER_SRC="$SRC/osx/Installer.Mac" +RESXPATH="$INSTALLER_SRC/resources" +DISTPATH="$INSTALLER_SRC/distribution.xml" + +# Product information +IDENTIFIER="com.microsoft.gitcredentialmanager.dist" + +# Parse script arguments +for i in "$@" +do +case "$i" in + --version=*) + VERSION="${i#*=}" + shift # past argument=value + ;; + --package-path=*) + PACKAGEPATH="${i#*=}" + shift # past argument=value + ;; + --output=*) + DISTOUT="${i#*=}" + shift # past argument=value + ;; + *) + # unknown option + ;; +esac +done + +# Perform pre-execution checks +if [ -z "$VERSION" ]; then + die "--version was not set" +fi +if [ -z "$PACKAGEPATH" ]; then + die "--package-path was not set" +elif [ ! -d "$PACKAGEPATH" ]; then + die "Could not find '$PACKAGEPATH'. Did you run pack.sh first?" +fi +if [ -z "$DISTOUT" ]; then + die "--output was not set" +fi + +# Cleanup any old package +if [ -e "$DISTOUT" ]; then + echo "Deleteing old product package '$DISTOUT'..." + rm "$DISTOUT" +fi + +# Ensure the parent directory for the package exists +mkdir -p "$(dirname "$DISTOUT")" + +# Build product installer +echo "Building product package..." +/usr/bin/productbuild \ + --package-path "$PACKAGEPATH" \ + --resources "$RESXPATH" \ + --distribution "$DISTPATH" \ + --identifier "$IDENTIFIER" \ + --version "$VERSION" \ + "$DISTOUT" || exit 1 + +echo "Product build complete." diff --git a/src/osx/Installer.Mac/distribution.xml b/src/osx/Installer.Mac/distribution.xml new file mode 100644 index 000000000..4bf9b9fb6 --- /dev/null +++ b/src/osx/Installer.Mac/distribution.xml @@ -0,0 +1,21 @@ + + + Git Credential Manager Core + + + + + + + + + + + + + + + + com.microsoft.gitcredentialmanager.component.pkg + + diff --git a/src/osx/Installer.Mac/pack.sh b/src/osx/Installer.Mac/pack.sh index e795ae313..b58f4ce5a 100755 --- a/src/osx/Installer.Mac/pack.sh +++ b/src/osx/Installer.Mac/pack.sh @@ -12,7 +12,7 @@ OUT="$ROOT/out" INSTALLER_SRC="$SRC/osx/Installer.Mac" # Product information -IDENTIFIER="com.microsoft.GitCredentialManager" +IDENTIFIER="com.microsoft.gitcredentialmanager" INSTALL_LOCATION="/usr/local/share/gcm-core" # Parse script arguments @@ -50,13 +50,13 @@ if [ -z "$PKGOUT" ]; then die "--output was not set" fi -# Cleanup any old package file +# Cleanup any old component if [ -e "$PKGOUT" ]; then - echo "Deleteing old package '$PKGOUT'..." + echo "Deleteing old component '$PKGOUT'..." rm "$PKGOUT" fi -# Ensure the parent directory for the package exists +# Ensure the parent directory for the component exists mkdir -p "$(dirname "$PKGOUT")" # Set full read, write, execute permissions for owner and just read and execute permissions for group and other @@ -67,8 +67,8 @@ echo "Setting file permissions..." echo "Removing extended attributes..." /usr/bin/xattr -rc "$PAYLOAD" || exit 1 -# Build installer package -echo "Building installer package..." +# Build component packages +echo "Building core component package..." /usr/bin/pkgbuild \ --root "$PAYLOAD/" \ --install-location "$INSTALL_LOCATION" \ @@ -77,4 +77,4 @@ echo "Building installer package..." --version "$VERSION" \ "$PKGOUT" || exit 1 -echo "Pack complete." +echo "Component pack complete." diff --git a/src/osx/Installer.Mac/resources/background.png b/src/osx/Installer.Mac/resources/background.png new file mode 100644 index 0000000000000000000000000000000000000000..fb9c1b154fae4748fce7a5f7d0b084afc6811344 GIT binary patch literal 16922 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 literal 0 HcmV?d00001 diff --git a/src/osx/Installer.Mac/resources/en.lproj/LICENSE b/src/osx/Installer.Mac/resources/en.lproj/LICENSE new file mode 120000 index 000000000..2a64f9d0f --- /dev/null +++ b/src/osx/Installer.Mac/resources/en.lproj/LICENSE @@ -0,0 +1 @@ +../../../../../LICENSE \ No newline at end of file diff --git a/src/osx/Installer.Mac/resources/en.lproj/conclusion.html b/src/osx/Installer.Mac/resources/en.lproj/conclusion.html new file mode 100644 index 000000000..53e817c7d --- /dev/null +++ b/src/osx/Installer.Mac/resources/en.lproj/conclusion.html @@ -0,0 +1,41 @@ + + + + + + + +
+

Git Credential Manager Core was installed in /usr/local/share/gcm-core and configured for the current user.

+
+
+

Other users

+

+ GCM Core has already been automatically configured for use by the current user with Git. + If other users wish to use GCM Core, have them run the following command to update their global Git configuration (~/.gitconfig): +

+

$ git-credential-manager-core configure

+

+ To configure GCM Core for all users, run the following command to update the system Git configuration: +

+

$ git-credential-manager-core configure --system

+
+
+

Uninstall

+

If you wish to uninstall GCM Core, run the following script:

+

$ /usr/local/share/gcm-core/uninstall.sh

+
+
+

Resources

+ +
+ + diff --git a/src/osx/Installer.Mac/resources/en.lproj/welcome.html b/src/osx/Installer.Mac/resources/en.lproj/welcome.html new file mode 100644 index 000000000..97c0454af --- /dev/null +++ b/src/osx/Installer.Mac/resources/en.lproj/welcome.html @@ -0,0 +1,34 @@ + + + + + + + +
+

Git Credential Manager Core

+

+ Git Credential Manager Core is a secure, cross-platform Git credential helper with authentication support for GitHub, Azure Repos, and other popular Git hosting services. +

+
+
+

Installation notes

+

+ If you have the old Java-based Git Credential Manager for Mac & Linux installed through Homebrew, it will be unlinked after installation. +

+

+ Git Credential Manager Core will be configured as the Git credential helper for the system or user, depending on if it is installed for all users, or just the current user. +

+
+
+

Learn more

+ +
+ + diff --git a/src/osx/Installer.Mac/scripts/postinstall b/src/osx/Installer.Mac/scripts/postinstall index cfb337622..9fdc006f1 100755 --- a/src/osx/Installer.Mac/scripts/postinstall +++ b/src/osx/Installer.Mac/scripts/postinstall @@ -1,6 +1,9 @@ #!/bin/bash set -e +PACKAGE=$1 +INSTALL_DESTINATION=$2 + function IsBrewPkgInstalled { # Check if Homebrew is installed @@ -24,9 +27,9 @@ then fi # Create symlink to GCM in /usr/local/bin -/bin/ln -Fs /usr/local/share/gcm-core/git-credential-manager-core /usr/local/bin/git-credential-manager-core +/bin/ln -Fs "$INSTALL_DESTINATION/git-credential-manager-core" /usr/local/bin/git-credential-manager-core # Configure GCM for the current user -/usr/local/share/gcm-core/git-credential-manager-core configure +"$INSTALL_DESTINATION/git-credential-manager-core" configure exit 0 diff --git a/src/osx/Installer.Mac/uninstall.sh b/src/osx/Installer.Mac/uninstall.sh index 47624086c..2657046d0 100755 --- a/src/osx/Installer.Mac/uninstall.sh +++ b/src/osx/Installer.Mac/uninstall.sh @@ -1,5 +1,8 @@ #!/bin/bash +THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" +GCMBIN="$THISDIR/git-credential-manager-core" + # Ensure we're running as root if [ $(id -u) != "0" ] then @@ -9,7 +12,7 @@ fi # Unconfigure echo "Unconfiguring credential helper..." -/usr/local/share/gcm-core/git-credential-manager-core unconfigure +"$GCMBIN" unconfigure # Remove symlink if [ -L /usr/local/bin/git-credential-manager-core ] @@ -21,13 +24,14 @@ else fi # Forget package installation/delete receipt -sudo pkgutil --forget com.microsoft.GitCredentialManager +echo "Removing installation receipt..." +pkgutil --forget com.microsoft.gitcredentialmanager # Remove application files -if [ -d /usr/local/share/gcm-core/ ] +if [ -d "$THISDIR" ] then echo "Deleting application files..." - sudo rm -rf /usr/local/share/gcm-core/ + rm -rf "$THISDIR" else echo "No application files found." fi From 2ef55df808f045909048310e82e81d3a1df90b11 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 28 Oct 2020 16:05:09 +0000 Subject: [PATCH 08/25] git: ensure that Git config quotes cmd args Ensure that we are correclty quoting arguments to Git configuration commands. We must ensure double-quotes are escaped correclty, and also any runs of back-slashes ('\') are preserved _UNLESS_ a double-quote ('"') follows. --- .../GitConfigurationTests.cs | 33 ++++++ .../GitConfiguration.cs | 103 +++++++++++++++++- 2 files changed, 130 insertions(+), 6 deletions(-) diff --git a/src/shared/Microsoft.Git.CredentialManager.Tests/GitConfigurationTests.cs b/src/shared/Microsoft.Git.CredentialManager.Tests/GitConfigurationTests.cs index 5c332303c..2bbc4ba5f 100644 --- a/src/shared/Microsoft.Git.CredentialManager.Tests/GitConfigurationTests.cs +++ b/src/shared/Microsoft.Git.CredentialManager.Tests/GitConfigurationTests.cs @@ -11,6 +11,39 @@ namespace Microsoft.Git.CredentialManager.Tests { public class GitConfigurationTests { + [Theory] + [InlineData(null, "\"\"")] + [InlineData("", "\"\"")] + [InlineData("hello", "hello")] + [InlineData("hello world", "\"hello world\"")] + [InlineData("C:\\app.exe", "C:\\app.exe")] + [InlineData("C:\\path with space\\app.exe", "\"C:\\path with space\\app.exe\"")] + [InlineData("''", "\"''\"")] + [InlineData("'hello'", "\"'hello'\"")] + [InlineData("'hello world'", "\"'hello world'\"")] + [InlineData("'C:\\app.exe'", "\"'C:\\app.exe'\"")] + [InlineData("'C:\\path with space\\app.exe'", "\"'C:\\path with space\\app.exe'\"")] + [InlineData("\"\"", "\"\\\"\\\"\"")] + [InlineData("\"hello\"", "\"\\\"hello\\\"\"")] + [InlineData("\"hello world\"", "\"\\\"hello world\\\"\"")] + [InlineData("\"C:\\app.exe\"", "\"\\\"C:\\app.exe\\\"\"")] + [InlineData("\"C:\\path with space\\app.exe\"", "\"\\\"C:\\path with space\\app.exe\\\"\"")] + [InlineData("\\", "\\")] + [InlineData("\\\\", "\\\\")] + [InlineData("\\\\\\", "\\\\\\")] + [InlineData("\"", "\"\\\"\"")] + [InlineData("\\\"", "\"\\\\\\\"\"")] + [InlineData("\\\\\"", "\"\\\\\\\\\\\"\"")] + [InlineData("\"\\", "\"\\\"\\\\\"")] + [InlineData("\"\\\\", "\"\\\"\\\\\\\\\"")] + [InlineData("ab\\", "ab\\")] + [InlineData("a b\\", "\"a b\\\\\"")] + public void GitConfiguration_QuoteCmdArg(string input, string expected) + { + string actual = GitProcessConfiguration.QuoteCmdArg(input); + Assert.Equal(expected, actual); + } + [Fact] public void GitProcess_GetConfiguration_ReturnsConfiguration() { diff --git a/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs b/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs index 86f9c52cb..304732606 100644 --- a/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs +++ b/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Text; namespace Microsoft.Git.CredentialManager { @@ -131,7 +132,7 @@ public void Enumerate(GitConfigurationEnumerationCallback cb) public bool TryGetValue(string name, out string value) { string level = GetLevelFilterArg(); - using (Process git = _git.CreateProcess($"config {level} {name}")) + using (Process git = _git.CreateProcess($"config {level} {QuoteCmdArg(name)}")) { git.Start(); git.WaitForExit(); @@ -171,7 +172,7 @@ public void SetValue(string name, string value) } string level = GetLevelFilterArg(); - using (Process git = _git.CreateProcess($"config {level} {name} \"{value}\"")) + using (Process git = _git.CreateProcess($"config {level} {QuoteCmdArg(name)} {QuoteCmdArg(value)}")) { git.Start(); git.WaitForExit(); @@ -195,7 +196,7 @@ public void Unset(string name) } string level = GetLevelFilterArg(); - using (Process git = _git.CreateProcess($"config {level} --unset {name}")) + using (Process git = _git.CreateProcess($"config {level} --unset {QuoteCmdArg(name)}")) { git.Start(); git.WaitForExit(); @@ -214,7 +215,14 @@ public void Unset(string name) public IEnumerable GetRegex(string nameRegex, string valueRegex) { string level = GetLevelFilterArg(); - using (Process git = _git.CreateProcess($"config --null {level} --get-regex {nameRegex} {valueRegex}")) + + var gitArgs = $"config --null {level} --get-regex {QuoteCmdArg(nameRegex)}"; + if (valueRegex != null) + { + gitArgs += $" {QuoteCmdArg(valueRegex)}"; + } + + using (Process git = _git.CreateProcess(gitArgs)) { git.Start(); // To avoid deadlocks, always read the output stream first and then wait @@ -252,7 +260,13 @@ public void ReplaceAll(string name, string valueRegex, string value) } string level = GetLevelFilterArg(); - using (Process git = _git.CreateProcess($"config {level} --replace-all {name} {value} {valueRegex}")) + var gitArgs = $"config {level} --replace-all {QuoteCmdArg(name)} {QuoteCmdArg(value)}"; + if (valueRegex != null) + { + gitArgs += $" {QuoteCmdArg(valueRegex)}"; + } + + using (Process git = _git.CreateProcess(gitArgs)) { git.Start(); git.WaitForExit(); @@ -276,7 +290,13 @@ public void UnsetAll(string name, string valueRegex) } string level = GetLevelFilterArg(); - using (Process git = _git.CreateProcess($"config {level} --unset-all {name} {valueRegex}")) + var gitArgs = $"config {level} --unset-all {QuoteCmdArg(name)}"; + if (valueRegex != null) + { + gitArgs += $" {QuoteCmdArg(valueRegex)}"; + } + + using (Process git = _git.CreateProcess(gitArgs)) { git.Start(); git.WaitForExit(); @@ -310,6 +330,77 @@ private string GetLevelFilterArg() return null; } } + + public static string QuoteCmdArg(string str) + { + bool needsQuotes = string.IsNullOrEmpty(str); + var result = new StringBuilder(); + + for (int i = 0; i < (str?.Length ?? 0); i++) + { + switch (str![i]) + { + case '"': + result.Append("\\\""); + needsQuotes = true; + break; + + case ' ': + case '{': + case '*': + case '?': + case '\r': + case '\n': + case '\t': + case '\'': + result.Append(str[i]); + needsQuotes = true; + break; + + case '\\': + int end = i; + + // Copy all the '\'s in this run. + while (end < str.Length && str[end] == '\\') + { + result.Append('\\'); + end++; + } + + // If we ended the run of '\'s with a '"' then we need to double up the number of '\'s. + // The '"' will be escaped on the next pass of the loop. + // Also if we have reached the end of the string, and we need to book-end the result + // with double quotes ('"') we should escape all the '\'s to prevent ending on an + // escaped '"' in the result. + if (end < str.Length && str[end] == '"' || + end == str.Length && needsQuotes) + { + result.Append('\\', end - i); + } + + // Back-off one character + if (end > i) + { + end--; + } + + i = end; + break; + + default: + result.Append(str[i]); + break; + } + } + + if (needsQuotes) + { + result.Insert(0, '"'); + result.Append('"'); + } + + return result.ToString(); + } } public static class GitConfigurationExtensions From 241580f0d494ba3d425b0a8517ad24013d23ad4a Mon Sep 17 00:00:00 2001 From: Dmitry Volodin Date: Wed, 28 Oct 2020 21:13:48 +0300 Subject: [PATCH 09/25] Just two tiny typos --- docs/github-apideprecation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/github-apideprecation.md b/docs/github-apideprecation.md index 0b419f0da..4023ebef7 100644 --- a/docs/github-apideprecation.md +++ b/docs/github-apideprecation.md @@ -19,7 +19,7 @@ Windows, they will continue to work until they expire or are revoked/deleted. ### Windows command-line users The best thing to do right now is upgrade to the latest Git for Windows (at -least version 2.29), which includes a version of Git Credential Manger Core that +least version 2.29), which includes a version of Git Credential Manager Core that uses supported OAuth token-based authentication. [Download the latest Git for Windows ⬇️](https://git-scm.com/download/win) @@ -92,7 +92,7 @@ require administrator permissions. cmdkey /generic:git:https://github.com /user:PersonalAccessToken /pass ``` - You will prompted to enter a password – copy the newly generated PAT in + You will be prompted to enter a password – copy the newly generated PAT in step 4 and paste it here, and press Enter ![image](https://user-images.githubusercontent.com/5658207/95448479-4fc64580-095b-11eb-9970-0b6faf7f4ae7.png) From 9b8c6d5f23145709114b51fbdd0576155b1232b3 Mon Sep 17 00:00:00 2001 From: Kyle Rader Date: Wed, 28 Oct 2020 15:17:45 -0400 Subject: [PATCH 10/25] .gitignore: ignore signing outputs Signed-off-by: Derrick Stolee --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 2a2c85213..1b6aff9c9 100644 --- a/.gitignore +++ b/.gitignore @@ -340,3 +340,7 @@ out/ # dotnet local tools .tools/ + +# Signing generated Files +auth.json +input.json \ No newline at end of file From aaaee557baaf37523445ba41dba5a7a83ff42d80 Mon Sep 17 00:00:00 2001 From: Kyle Rader Date: Wed, 28 Oct 2020 15:19:05 -0400 Subject: [PATCH 11/25] Signing: add python script for Linux signing The .github/run_esrp_signing.py script is executed on the signing machines. This mostly accumulates a set of JSON input files before running the ESRP signing tool that was securely downloaded. Signed-off-by: Derrick Stolee --- .github/run_esrp_signing.py | 112 ++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 .github/run_esrp_signing.py diff --git a/.github/run_esrp_signing.py b/.github/run_esrp_signing.py new file mode 100644 index 000000000..223950767 --- /dev/null +++ b/.github/run_esrp_signing.py @@ -0,0 +1,112 @@ +import json +import os +import glob +import pprint +import subprocess +import sys + +esrp_tool = os.path.join("esrp", "tools", "EsrpClient.exe") + +aad_id = os.environ['AZURE_AAD_ID'].strip() +workspace = os.environ['GITHUB_WORKSPACE'].strip() + +source_root_location = os.path.join(workspace, "deb", "Release") +destination_location = os.path.join(workspace) + +files = glob.glob(os.path.join(source_root_location, "*.deb")) + +print("Found files:") +pprint.pp(files) + +if len(files) < 1 or not files[0].endswith(".deb"): + print("Error: cannot find .deb to sign") + exit(1) + +file_to_sign = os.path.basename(files[0]) + +auth_json = { + "Version": "1.0.0", + "AuthenticationType": "AAD_CERT", + "TenantId": "72f988bf-86f1-41af-91ab-2d7cd011db47", + "ClientId": aad_id, + "AuthCert": { + "SubjectName": f"CN={aad_id}.microsoft.com", + "StoreLocation": "LocalMachine", + "StoreName": "My", + }, + "RequestSigningCert": { + "SubjectName": f"CN={aad_id}", + "StoreLocation": "LocalMachine", + "StoreName": "My", + } +} + +input_json = { + "Version": "1.0.0", + "SignBatches": [ + { + "SourceLocationType": "UNC", + "SourceRootDirectory": source_root_location, + "DestinationLocationType": "UNC", + "DestinationRootDirectory": destination_location, + "SignRequestFiles": [ + { + "CustomerCorrelationId": "01A7F55F-6CDD-4123-B255-77E6F212CDAD", + "SourceLocation": file_to_sign, + "DestinationLocation": os.path.join("Signed", file_to_sign), + } + ], + "SigningInfo": { + "Operations": [ + { + "KeyCode": "CP-450779-Pgp", + "OperationCode": "LinuxSign", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0", + } + ] + } + } + ] +} + +policy_json = { + "Version": "1.0.0", + "Intent": "production release", + "ContentType": "Debian package", +} + +configs = [ + ("auth.json", auth_json), + ("input.json", input_json), + ("policy.json", policy_json), +] + +for filename, data in configs: + with open(filename, 'w') as fp: + json.dump(data, fp) + +# Run ESRP Client +esrp_out = "esrp_out.json" +result = subprocess.run( + [esrp_tool, "sign", + "-a", "auth.json", + "-i", "input.json", + "-p", "policy.json", + "-o", esrp_out, + "-l", "Verbose"], + cwd=workspace) + +if result.returncode != 0: + print("Failed to run ESRPClient.exe") + sys.exit(1) + +if os.path.isfile(esrp_out): + print("ESRP output json:") + with open(esrp_out, 'r') as fp: + pprint.pp(json.load(fp)) + +signed_file = os.path.join(destination_location, "Signed", file_to_sign) +if os.path.isfile(signed_file): + print(f"Success!\nSigned {signed_file}") From 2a1ed62ba86a387a7e8f39d5b56224167527ef4b Mon Sep 17 00:00:00 2001 From: Kyle Rader Date: Wed, 28 Oct 2020 15:21:20 -0400 Subject: [PATCH 12/25] Release: build and sign Linux installers This launches a job that downloads the ESRP signing tool client, then loads it with the necessary information for our signing abilities. Finally, it publishes our signed .deb to the build artifacts. Signed-off-by: Derrick Stolee --- .github/workflows/build-signed-deb.yml | 92 ++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 .github/workflows/build-signed-deb.yml diff --git a/.github/workflows/build-signed-deb.yml b/.github/workflows/build-signed-deb.yml new file mode 100644 index 000000000..e0d309b0c --- /dev/null +++ b/.github/workflows/build-signed-deb.yml @@ -0,0 +1,92 @@ +name: "Build Signed Debian Installer" + +on: + release: + types: [released] + +jobs: + build: + name: "Build" + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Indicate full history so Nerdbank.GitVersioning works. + + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.302 + + - name: Install dependencies + run: dotnet restore --force + + - name: Build Linux Payloads + run: dotnet build -c Release src/linux/Packaging.Linux/Packaging.Linux.csproj + + - name: Upload Installers + uses: actions/upload-artifact@v2 + with: + name: LinuxInstallers + path: | + out/linux/Packaging.Linux/deb/Release/*.deb + out/linux/Packaging.Linux/tar/Release/*.tar.gz + + sign: + name: 'Sign' + runs-on: windows-latest + needs: build + steps: + - name: setup python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - uses: actions/checkout@v2 + + - name: 'Download Installer Artifact' + uses: actions/download-artifact@v2 + with: + name: LinuxInstallers + + - uses: Azure/login@v1.1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: 'Install ESRP Client' + shell: pwsh + env: + AZ_SUB: ${{ secrets.AZURE_SUBSCRIPTION }} + run: | + az storage blob download --subscription "$env:AZ_SUB" --account-name gitcitoolstore -c tools -n microsoft.esrpclient.1.2.47.nupkg -f esrp.zip + Expand-Archive -Path esrp.zip -DestinationPath .\esrp + + - name: Install Certs + shell: pwsh + env: + AZ_SUB: ${{ secrets.AZURE_SUBSCRIPTION }} + AZ_VAULT: ${{ secrets.AZURE_VAULT }} + SSL_CERT: ${{ secrets.VAULT_SSL_CERT_NAME }} + ESRP_CERT: ${{ secrets.VAULT_ESRP_CERT_NAME }} + run: | + az keyvault secret download --subscription "$env:AZ_SUB" --vault-name "$env:AZ_VAULT" --name "$env:SSL_CERT" -f out.pfx + certutil -f -importpfx out.pfx + Remove-Item out.pfx + + az keyvault secret download --subscription "$env:AZ_SUB" --vault-name "$env:AZ_VAULT" --name "$env:ESRP_CERT" -f out.pfx + certutil -f -importpfx out.pfx + Remove-Item out.pfx + + - name: Run ESRP Client + shell: pwsh + env: + AZURE_AAD_ID: ${{ secrets.AZURE_AAD_ID }} + run: | + python .github/run_esrp_signing.py + + - name: Upload Installer + uses: actions/upload-artifact@v2 + with: + name: DebianInstallerSigned + path: | + Signed/*.deb \ No newline at end of file From 8c5667a5be9a241a7910b66d365c55f3aa8a22b6 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 28 Oct 2020 16:02:01 +0000 Subject: [PATCH 13/25] configuration: update config cmd to set full path Update the configure and unconfigure actions to now set the full file path to GCM Core, rather than rely on `manager-core` resolving to `git-credential-manager-core` on the PATH by Git. This was a problem because Git for Windows now bundles GCM Core, and even if a standalone install of GCM Core was present on the system with a higher version, since the bundled copy is found on the PATH before anything else, Git was always picking the old one. The change to using full paths helps fix this issue, and also another issue on macOS where sometimes the /usr/local/bin directory is not on the PATH (such as for the root user, or during a postinstall script for a flat-package [.pkg] file). --- .../AzureReposHostProviderTests.cs | 57 ++-- .../AzureReposHostProvider.cs | 20 +- .../ApplicationTests.cs | 292 +++++++++++------- .../ConfigurationServiceTests.cs | 49 +-- .../Application.cs | 192 ++++++++---- .../ConfigurationService.cs | 60 +--- 6 files changed, 361 insertions(+), 309 deletions(-) diff --git a/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs b/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs index fef1ed6b4..1ff37732f 100644 --- a/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs +++ b/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs @@ -179,18 +179,15 @@ public async Task AzureReposProvider_GetCredentialAsync_ReturnsCredential() [Fact] public async Task AzureReposHostProvider_ConfigureAsync_UseHttpPathSetTrue_DoesNothing() { - var provider = new AzureReposHostProvider(new TestCommandContext()); + var context = new TestCommandContext(); + var provider = new AzureReposHostProvider(context); - var environment = new TestEnvironment(); - var git = new TestGit(); - git.GlobalConfiguration.Dictionary[AzDevUseHttpPathKey] = new List {"true"}; + context.Git.GlobalConfiguration.Dictionary[AzDevUseHttpPathKey] = new List {"true"}; - await provider.ConfigureAsync( - environment, EnvironmentVariableTarget.User, - git, GitConfigurationLevel.Global); + await provider.ConfigureAsync(ConfigurationTarget.User); - Assert.Single(git.GlobalConfiguration.Dictionary); - Assert.True(git.GlobalConfiguration.Dictionary.TryGetValue(AzDevUseHttpPathKey, out IList actualValues)); + Assert.Single(context.Git.GlobalConfiguration.Dictionary); + Assert.True(context.Git.GlobalConfiguration.Dictionary.TryGetValue(AzDevUseHttpPathKey, out IList actualValues)); Assert.Single(actualValues); Assert.Equal("true", actualValues[0]); } @@ -198,18 +195,15 @@ await provider.ConfigureAsync( [Fact] public async Task AzureReposHostProvider_ConfigureAsync_UseHttpPathSetFalse_SetsUseHttpPathTrue() { - var provider = new AzureReposHostProvider(new TestCommandContext()); + var context = new TestCommandContext(); + var provider = new AzureReposHostProvider(context); - var environment = new TestEnvironment(); - var git = new TestGit(); - git.GlobalConfiguration.Dictionary[AzDevUseHttpPathKey] = new List {"false"}; + context.Git.GlobalConfiguration.Dictionary[AzDevUseHttpPathKey] = new List {"false"}; - await provider.ConfigureAsync( - environment, EnvironmentVariableTarget.User, - git, GitConfigurationLevel.Global); + await provider.ConfigureAsync(ConfigurationTarget.User); - Assert.Single(git.GlobalConfiguration.Dictionary); - Assert.True(git.GlobalConfiguration.Dictionary.TryGetValue(AzDevUseHttpPathKey, out IList actualValues)); + Assert.Single(context.Git.GlobalConfiguration.Dictionary); + Assert.True(context.Git.GlobalConfiguration.Dictionary.TryGetValue(AzDevUseHttpPathKey, out IList actualValues)); Assert.Single(actualValues); Assert.Equal("true", actualValues[0]); } @@ -217,17 +211,13 @@ await provider.ConfigureAsync( [Fact] public async Task AzureReposHostProvider_ConfigureAsync_UseHttpPathUnset_SetsUseHttpPathTrue() { - var provider = new AzureReposHostProvider(new TestCommandContext()); - - var environment = new TestEnvironment(); - var git = new TestGit(); + var context = new TestCommandContext(); + var provider = new AzureReposHostProvider(context); - await provider.ConfigureAsync( - environment, EnvironmentVariableTarget.User, - git, GitConfigurationLevel.Global); + await provider.ConfigureAsync(ConfigurationTarget.User); - Assert.Single(git.GlobalConfiguration.Dictionary); - Assert.True(git.GlobalConfiguration.Dictionary.TryGetValue(AzDevUseHttpPathKey, out IList actualValues)); + Assert.Single(context.Git.GlobalConfiguration.Dictionary); + Assert.True(context.Git.GlobalConfiguration.Dictionary.TryGetValue(AzDevUseHttpPathKey, out IList actualValues)); Assert.Single(actualValues); Assert.Equal("true", actualValues[0]); } @@ -236,17 +226,14 @@ await provider.ConfigureAsync( [Fact] public async Task AzureReposHostProvider_UnconfigureAsync_UseHttpPathSet_RemovesEntry() { - var provider = new AzureReposHostProvider(new TestCommandContext()); + var context = new TestCommandContext(); + var provider = new AzureReposHostProvider(context); - var environment = new TestEnvironment(); - var git = new TestGit(); - git.GlobalConfiguration.Dictionary[AzDevUseHttpPathKey] = new List {"true"}; + context.Git.GlobalConfiguration.Dictionary[AzDevUseHttpPathKey] = new List {"true"}; - await provider.UnconfigureAsync( - environment, EnvironmentVariableTarget.User, - git, GitConfigurationLevel.Global); + await provider.UnconfigureAsync(ConfigurationTarget.User); - Assert.Empty(git.GlobalConfiguration.Dictionary); + Assert.Empty(context.Git.GlobalConfiguration.Dictionary); } } } diff --git a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs index bfc2ab1c3..57962cb12 100644 --- a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs +++ b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs @@ -250,13 +250,15 @@ private static string GetAccountNameForCredentialQuery(InputArguments input) string IConfigurableComponent.Name => "Azure Repos provider"; - public Task ConfigureAsync( - IEnvironment environment, EnvironmentVariableTarget environmentTarget, - IGit git, GitConfigurationLevel configurationLevel) + public Task ConfigureAsync(ConfigurationTarget target) { string useHttpPathKey = $"{KnownGitCfg.Credential.SectionName}.https://dev.azure.com.{KnownGitCfg.Credential.UseHttpPath}"; - IGitConfiguration targetConfig = git.GetConfiguration(configurationLevel); + GitConfigurationLevel configurationLevel = target == ConfigurationTarget.System + ? GitConfigurationLevel.System + : GitConfigurationLevel.Global; + + IGitConfiguration targetConfig = _context.Git.GetConfiguration(configurationLevel); if (targetConfig.TryGetValue(useHttpPathKey, out string currentValue) && currentValue.IsTruthy()) { @@ -271,15 +273,17 @@ public Task ConfigureAsync( return Task.CompletedTask; } - public Task UnconfigureAsync( - IEnvironment environment, EnvironmentVariableTarget environmentTarget, - IGit git, GitConfigurationLevel configurationLevel) + public Task UnconfigureAsync(ConfigurationTarget target) { string useHttpPathKey = $"{KnownGitCfg.Credential.SectionName}.https://dev.azure.com.{KnownGitCfg.Credential.UseHttpPath}"; _context.Trace.WriteLine("Clearing Git configuration 'credential.useHttpPath' for https://dev.azure.com..."); - IGitConfiguration targetConfig = git.GetConfiguration(configurationLevel); + GitConfigurationLevel configurationLevel = target == ConfigurationTarget.System + ? GitConfigurationLevel.System + : GitConfigurationLevel.Global; + + IGitConfiguration targetConfig = _context.Git.GetConfiguration(configurationLevel); targetConfig.Unset(useHttpPathKey); return Task.CompletedTask; diff --git a/src/shared/Microsoft.Git.CredentialManager.Tests/ApplicationTests.cs b/src/shared/Microsoft.Git.CredentialManager.Tests/ApplicationTests.cs index 5a0b16350..4b5dad792 100644 --- a/src/shared/Microsoft.Git.CredentialManager.Tests/ApplicationTests.cs +++ b/src/shared/Microsoft.Git.CredentialManager.Tests/ApplicationTests.cs @@ -1,217 +1,277 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Git.CredentialManager.Tests.Objects; -using Moq; using Xunit; namespace Microsoft.Git.CredentialManager.Tests { public class ApplicationTests { - #region Common configuration tests + [Fact] + public async Task Application_ConfigureAsync_NoHelpers_AddsEmptyAndGcm() + { + const string emptyHelper = ""; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; + + var context = new TestCommandContext(); + IConfigurableComponent application = new Application(context, executablePath); + await application.ConfigureAsync(ConfigurationTarget.User); + + Assert.Single(context.Git.GlobalConfiguration.Dictionary); + Assert.True(context.Git.GlobalConfiguration.Dictionary.TryGetValue(key, out var actualValues)); + Assert.Equal(2, actualValues.Count); + Assert.Equal(emptyHelper, actualValues[0]); + Assert.Equal(executablePath, actualValues[1]); + } [Fact] - public async Task Application_ConfigureAsync_HelperSet_DoesNothing() + public async Task Application_ConfigureAsync_Gcm_AddsEmptyBeforeGcm() { const string emptyHelper = ""; - const string gcmConfigName = "manager-core"; const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; - IConfigurableComponent application = new Application(new TestCommandContext(), executablePath); + var context = new TestCommandContext(); + IConfigurableComponent application = new Application(context, executablePath); + + context.Git.GlobalConfiguration.Dictionary[key] = new List {executablePath}; + + await application.ConfigureAsync(ConfigurationTarget.User); + + Assert.Single(context.Git.GlobalConfiguration.Dictionary); + Assert.True(context.Git.GlobalConfiguration.Dictionary.TryGetValue(key, out var actualValues)); + Assert.Equal(2, actualValues.Count); + Assert.Equal(emptyHelper, actualValues[0]); + Assert.Equal(executablePath, actualValues[1]); + } + + [Fact] + public async Task Application_ConfigureAsync_EmptyAndGcm_DoesNothing() + { + const string emptyHelper = ""; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; - var environment = new Mock(); + var context = new TestCommandContext(); + IConfigurableComponent application = new Application(context, executablePath); - var git = new TestGit(); - git.GlobalConfiguration.Dictionary[key] = new List + context.Git.GlobalConfiguration.Dictionary[key] = new List { - emptyHelper, gcmConfigName + emptyHelper, executablePath }; - await application.ConfigureAsync( - environment.Object, EnvironmentVariableTarget.User, - git, GitConfigurationLevel.Global); + await application.ConfigureAsync(ConfigurationTarget.User); - Assert.Single(git.GlobalConfiguration.Dictionary); - Assert.True(git.GlobalConfiguration.Dictionary.TryGetValue(key, out var actualValues)); + Assert.Single(context.Git.GlobalConfiguration.Dictionary); + Assert.True(context.Git.GlobalConfiguration.Dictionary.TryGetValue(key, out var actualValues)); Assert.Equal(2, actualValues.Count); Assert.Equal(emptyHelper, actualValues[0]); - Assert.Equal(gcmConfigName, actualValues[1]); + Assert.Equal(executablePath, actualValues[1]); } [Fact] - public async Task Application_ConfigureAsync_HelperSetWithOthersPreceding_DoesNothing() + public async Task Application_ConfigureAsync_EmptyAndGcmWithOthersBefore_DoesNothing() { const string emptyHelper = ""; - const string gcmConfigName = "manager-core"; + const string beforeHelper = "foo"; const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; - IConfigurableComponent application = new Application(new TestCommandContext(), executablePath); - - var environment = new Mock(); + var context = new TestCommandContext(); + IConfigurableComponent application = new Application(context, executablePath); - var git = new TestGit(); - git.GlobalConfiguration.Dictionary[key] = new List + context.Git.GlobalConfiguration.Dictionary[key] = new List { - "foo", "bar", emptyHelper, gcmConfigName + beforeHelper, emptyHelper, executablePath }; - await application.ConfigureAsync( - environment.Object, EnvironmentVariableTarget.User, - git, GitConfigurationLevel.Global); + await application.ConfigureAsync(ConfigurationTarget.User); - Assert.Single(git.GlobalConfiguration.Dictionary); - Assert.True(git.GlobalConfiguration.Dictionary.TryGetValue(key, out var actualValues)); - Assert.Equal(4, actualValues.Count); - Assert.Equal("foo", actualValues[0]); - Assert.Equal("bar", actualValues[1]); - Assert.Equal(emptyHelper, actualValues[2]); - Assert.Equal(gcmConfigName, actualValues[3]); + Assert.Single(context.Git.GlobalConfiguration.Dictionary); + Assert.True(context.Git.GlobalConfiguration.Dictionary.TryGetValue(key, out var actualValues)); + Assert.Equal(3, actualValues.Count); + Assert.Equal(beforeHelper, actualValues[0]); + Assert.Equal(emptyHelper, actualValues[1]); + Assert.Equal(executablePath, actualValues[2]); } [Fact] - public async Task Application_ConfigureAsync_HelperSetWithOthersFollowing_ClearsEntriesSetsHelper() + public async Task Application_ConfigureAsync_EmptyAndGcmWithOthersAfter_DoesNothing() { const string emptyHelper = ""; - const string gcmConfigName = "manager-core"; + const string afterHelper = "foo"; const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; - IConfigurableComponent application = new Application(new TestCommandContext(), executablePath); - - var environment = new Mock(); + var context = new TestCommandContext(); + IConfigurableComponent application = new Application(context, executablePath); - var git = new TestGit(); - git.GlobalConfiguration.Dictionary[key] = new List + context.Git.GlobalConfiguration.Dictionary[key] = new List { - "bar", emptyHelper, executablePath, "foo" + emptyHelper, executablePath, afterHelper }; - await application.ConfigureAsync( - environment.Object, EnvironmentVariableTarget.User, - git, GitConfigurationLevel.Global); + await application.ConfigureAsync(ConfigurationTarget.User); - Assert.Single(git.GlobalConfiguration.Dictionary); - Assert.True(git.GlobalConfiguration.Dictionary.TryGetValue(key, out var actualValues)); - Assert.Equal(2, actualValues.Count); + Assert.Single(context.Git.GlobalConfiguration.Dictionary); + Assert.True(context.Git.GlobalConfiguration.Dictionary.TryGetValue(key, out var actualValues)); + Assert.Equal(3, actualValues.Count); Assert.Equal(emptyHelper, actualValues[0]); - Assert.Equal(gcmConfigName, actualValues[1]); + Assert.Equal(executablePath, actualValues[1]); + Assert.Equal(afterHelper, actualValues[2]); } [Fact] - public async Task Application_ConfigureAsync_HelperNotSet_SetsHelper() + public async Task Application_ConfigureAsync_EmptyAndGcmWithOthersBeforeAndAfter_DoesNothing() { const string emptyHelper = ""; - const string gcmConfigName = "manager-core"; + const string beforeHelper = "foo"; + const string afterHelper = "bar"; const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; - IConfigurableComponent application = new Application(new TestCommandContext(), executablePath); + var context = new TestCommandContext(); + IConfigurableComponent application = new Application(context, executablePath); - var environment = new Mock(); + context.Git.GlobalConfiguration.Dictionary[key] = new List + { + beforeHelper, emptyHelper, executablePath, afterHelper + }; - var git = new TestGit(); + await application.ConfigureAsync(ConfigurationTarget.User); - await application.ConfigureAsync( - environment.Object, EnvironmentVariableTarget.User, - git, GitConfigurationLevel.Global); + Assert.Single(context.Git.GlobalConfiguration.Dictionary); + Assert.True(context.Git.GlobalConfiguration.Dictionary.TryGetValue(key, out var actualValues)); + Assert.Equal(4, actualValues.Count); + Assert.Equal(beforeHelper, actualValues[0]); + Assert.Equal(emptyHelper, actualValues[1]); + Assert.Equal(executablePath, actualValues[2]); + Assert.Equal(afterHelper, actualValues[3]); + } - Assert.Single(git.GlobalConfiguration.Dictionary); - Assert.True(git.GlobalConfiguration.Dictionary.TryGetValue(key, out var actualValues)); - Assert.Equal(2, actualValues.Count); - Assert.Equal(emptyHelper, actualValues[0]); - Assert.Equal(gcmConfigName, actualValues[1]); + [Fact] + public async Task Application_UnconfigureAsync_NoHelpers_DoesNothing() + { + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; + + var context = new TestCommandContext(); + IConfigurableComponent application = new Application(context, executablePath); + await application.UnconfigureAsync(ConfigurationTarget.User); + + Assert.Empty(context.Git.GlobalConfiguration.Dictionary); } [Fact] - public async Task Application_UnconfigureAsync_HelperSet_RemovesEntries() + public async Task Application_UnconfigureAsync_Gcm_RemovesGcm() { - const string emptyHelper = ""; - const string gcmConfigName = "manager-core"; const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; - IConfigurableComponent application = new Application(new TestCommandContext(), executablePath); + var context = new TestCommandContext(); + IConfigurableComponent application = new Application(context, executablePath); - var environment = new Mock(); - var git = new TestGit(); - git.GlobalConfiguration.Dictionary[key] = new List {emptyHelper, gcmConfigName}; + context.Git.GlobalConfiguration.Dictionary[key] = new List {executablePath}; - await application.UnconfigureAsync( - environment.Object, EnvironmentVariableTarget.User, - git, GitConfigurationLevel.Global); + await application.UnconfigureAsync(ConfigurationTarget.User); - Assert.Empty(git.GlobalConfiguration.Dictionary); + Assert.Empty(context.Git.GlobalConfiguration.Dictionary); } - #endregion + [Fact] + public async Task Application_UnconfigureAsync_EmptyAndGcm_RemovesEmptyAndGcm() + { + const string emptyHelper = ""; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; - #region Windows-specific configuration tests + var context = new TestCommandContext(); + IConfigurableComponent application = new Application(context, executablePath); - [PlatformFact(Platforms.Windows)] - public async Task Application_ConfigureAsync_User_PathSet_DoesNothing() - { - const string directoryPath = @"X:\Install Location"; - const string executablePath = @"X:\Install Location\git-credential-manager-core.exe"; + context.Git.GlobalConfiguration.Dictionary[key] = new List {emptyHelper, executablePath}; - IConfigurableComponent application = new Application(new TestCommandContext(), executablePath); + await application.UnconfigureAsync(ConfigurationTarget.User); - var environment = new Mock(); - environment.Setup(x => x.IsDirectoryOnPath(directoryPath)).Returns(true); + Assert.Empty(context.Git.GlobalConfiguration.Dictionary); + } - var git = new TestGit(); + [Fact] + public async Task Application_UnconfigureAsync_EmptyAndGcmWithOthersBefore_RemovesEmptyAndGcm() + { + const string emptyHelper = ""; + const string beforeHelper = "foo"; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; + + var context = new TestCommandContext(); + IConfigurableComponent application = new Application(context, executablePath); + + context.Git.GlobalConfiguration.Dictionary[key] = new List + { + beforeHelper, emptyHelper, executablePath + }; - await application.ConfigureAsync( - environment.Object, EnvironmentVariableTarget.User, - git, GitConfigurationLevel.Global); + await application.UnconfigureAsync(ConfigurationTarget.User); - environment.Verify(x => x.AddDirectoryToPath(It.IsAny(), It.IsAny()), Times.Never); + Assert.Single(context.Git.GlobalConfiguration.Dictionary); + Assert.True(context.Git.GlobalConfiguration.Dictionary.TryGetValue(key, out var actualValues)); + Assert.Equal(1, actualValues.Count); + Assert.Equal(beforeHelper, actualValues[0]); } - [PlatformFact(Platforms.Windows)] - public async Task Application_ConfigureAsync_User_PathNotSet_SetsUserPath() + [Fact] + public async Task Application_UnconfigureAsync_EmptyAndGcmWithOthersAfterBefore_RemovesGcmOnly() { - const string directoryPath = @"X:\Install Location"; - const string executablePath = @"X:\Install Location\git-credential-manager-core.exe"; - - IConfigurableComponent application = new Application(new TestCommandContext(), executablePath); + const string emptyHelper = ""; + const string afterHelper = "bar"; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; - var environment = new Mock(); - environment.Setup(x => x.IsDirectoryOnPath(directoryPath)).Returns(false); + var context = new TestCommandContext(); + IConfigurableComponent application = new Application(context, executablePath); - var git = new TestGit(); + context.Git.GlobalConfiguration.Dictionary[key] = new List + { + emptyHelper, executablePath, afterHelper + }; - await application.ConfigureAsync( - environment.Object, EnvironmentVariableTarget.User, - git, GitConfigurationLevel.Global); + await application.UnconfigureAsync(ConfigurationTarget.User); - environment.Verify(x => x.AddDirectoryToPath(directoryPath, EnvironmentVariableTarget.User), Times.Once); + Assert.Single(context.Git.GlobalConfiguration.Dictionary); + Assert.True(context.Git.GlobalConfiguration.Dictionary.TryGetValue(key, out var actualValues)); + Assert.Equal(2, actualValues.Count); + Assert.Equal(emptyHelper, actualValues[0]); + Assert.Equal(afterHelper, actualValues[1]); } - [PlatformFact(Platforms.Windows)] - public async Task Application_UnconfigureAsync_User_PathSet_RemovesFromUserPath() + [Fact] + public async Task Application_UnconfigureAsync_EmptyAndGcmWithOthersBeforeAndAfter_RemovesGcmOnly() { - const string directoryPath = @"X:\Install Location"; - const string executablePath = @"X:\Install Location\git-credential-manager-core.exe"; - - IConfigurableComponent application = new Application(new TestCommandContext(), executablePath); + const string emptyHelper = ""; + const string beforeHelper = "foo"; + const string afterHelper = "bar"; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; - var environment = new Mock(); - environment.Setup(x => x.IsDirectoryOnPath(directoryPath)).Returns(true); + var context = new TestCommandContext(); + IConfigurableComponent application = new Application(context, executablePath); - var git = new TestGit(); + context.Git.GlobalConfiguration.Dictionary[key] = new List + { + beforeHelper, emptyHelper, executablePath, afterHelper + }; - await application.UnconfigureAsync( - environment.Object, EnvironmentVariableTarget.User, - git, GitConfigurationLevel.Global); + await application.UnconfigureAsync(ConfigurationTarget.User); - environment.Verify(x => x.RemoveDirectoryFromPath(directoryPath, EnvironmentVariableTarget.User), Times.Once); + Assert.Single(context.Git.GlobalConfiguration.Dictionary); + Assert.True(context.Git.GlobalConfiguration.Dictionary.TryGetValue(key, out var actualValues)); + Assert.Equal(3, actualValues.Count); + Assert.Equal(beforeHelper, actualValues[0]); + Assert.Equal(emptyHelper, actualValues[1]); + Assert.Equal(afterHelper, actualValues[2]); } - - #endregion } } diff --git a/src/shared/Microsoft.Git.CredentialManager.Tests/ConfigurationServiceTests.cs b/src/shared/Microsoft.Git.CredentialManager.Tests/ConfigurationServiceTests.cs index ea84a6b08..e2644ad3f 100644 --- a/src/shared/Microsoft.Git.CredentialManager.Tests/ConfigurationServiceTests.cs +++ b/src/shared/Microsoft.Git.CredentialManager.Tests/ConfigurationServiceTests.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using System; using System.Threading.Tasks; using Microsoft.Git.CredentialManager.Tests.Objects; using Moq; @@ -26,17 +25,11 @@ public async Task ConfigurationService_ConfigureAsync_System_ComponentsAreConfig await service.ConfigureAsync(ConfigurationTarget.System); - component1.Verify(x => x.ConfigureAsync( - context.Environment, EnvironmentVariableTarget.Machine, - context.Git, GitConfigurationLevel.System), + component1.Verify(x => x.ConfigureAsync(ConfigurationTarget.System), Times.Once); - component2.Verify(x => x.ConfigureAsync( - context.Environment, EnvironmentVariableTarget.Machine, - context.Git, GitConfigurationLevel.System), + component2.Verify(x => x.ConfigureAsync(ConfigurationTarget.System), Times.Once); - component3.Verify(x => x.ConfigureAsync( - context.Environment, EnvironmentVariableTarget.Machine, - context.Git, GitConfigurationLevel.System), + component3.Verify(x => x.ConfigureAsync(ConfigurationTarget.System), Times.Once); } @@ -56,17 +49,11 @@ public async Task ConfigurationService_ConfigureAsync_User_ComponentsAreConfigur await service.ConfigureAsync(ConfigurationTarget.User); - component1.Verify(x => x.ConfigureAsync( - context.Environment, EnvironmentVariableTarget.User, - context.Git, GitConfigurationLevel.Global), + component1.Verify(x => x.ConfigureAsync(ConfigurationTarget.User), Times.Once); - component2.Verify(x => x.ConfigureAsync( - context.Environment, EnvironmentVariableTarget.User, - context.Git, GitConfigurationLevel.Global), + component2.Verify(x => x.ConfigureAsync(ConfigurationTarget.User), Times.Once); - component3.Verify(x => x.ConfigureAsync( - context.Environment, EnvironmentVariableTarget.User, - context.Git, GitConfigurationLevel.Global), + component3.Verify(x => x.ConfigureAsync(ConfigurationTarget.User), Times.Once); } @@ -86,17 +73,11 @@ public async Task ConfigurationService_UnconfigureAsync_System_ComponentsAreUnco await service.UnconfigureAsync(ConfigurationTarget.System); - component1.Verify(x => x.UnconfigureAsync( - context.Environment, EnvironmentVariableTarget.Machine, - context.Git, GitConfigurationLevel.System), + component1.Verify(x => x.UnconfigureAsync(ConfigurationTarget.System), Times.Once); - component2.Verify(x => x.UnconfigureAsync( - context.Environment, EnvironmentVariableTarget.Machine, - context.Git, GitConfigurationLevel.System), + component2.Verify(x => x.UnconfigureAsync(ConfigurationTarget.System), Times.Once); - component3.Verify(x => x.UnconfigureAsync( - context.Environment, EnvironmentVariableTarget.Machine, - context.Git, GitConfigurationLevel.System), + component3.Verify(x => x.UnconfigureAsync(ConfigurationTarget.System), Times.Once); } @@ -116,17 +97,11 @@ public async Task ConfigurationService_UnconfigureAsync_User_ComponentsAreUnconf await service.UnconfigureAsync(ConfigurationTarget.User); - component1.Verify(x => x.UnconfigureAsync( - context.Environment, EnvironmentVariableTarget.User, - context.Git, GitConfigurationLevel.Global), + component1.Verify(x => x.UnconfigureAsync(ConfigurationTarget.User), Times.Once); - component2.Verify(x => x.UnconfigureAsync( - context.Environment, EnvironmentVariableTarget.User, - context.Git, GitConfigurationLevel.Global), + component2.Verify(x => x.UnconfigureAsync(ConfigurationTarget.User), Times.Once); - component3.Verify(x => x.UnconfigureAsync( - context.Environment, EnvironmentVariableTarget.User, - context.Git, GitConfigurationLevel.Global), + component3.Verify(x => x.UnconfigureAsync(ConfigurationTarget.User), Times.Once); } } diff --git a/src/shared/Microsoft.Git.CredentialManager/Application.cs b/src/shared/Microsoft.Git.CredentialManager/Application.cs index 37c7c4a3f..2148f7332 100644 --- a/src/shared/Microsoft.Git.CredentialManager/Application.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Application.cs @@ -3,6 +3,7 @@ using System; using System.IO; using System.Linq; +using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Git.CredentialManager.Commands; @@ -141,90 +142,128 @@ protected bool WriteException(Exception ex) string IConfigurableComponent.Name => "Git Credential Manager"; - Task IConfigurableComponent.ConfigureAsync( - IEnvironment environment, EnvironmentVariableTarget environmentTarget, - IGit git, GitConfigurationLevel configurationLevel) + Task IConfigurableComponent.ConfigureAsync(ConfigurationTarget target) { - // NOTE: We currently only update the PATH in Windows installations and leave putting the GCM executable - // on the PATH on other platform to their installers. - if (PlatformUtils.IsWindows()) - { - string directoryPath = Path.GetDirectoryName(_appPath); - if (!environment.IsDirectoryOnPath(directoryPath)) - { - Context.Trace.WriteLine("Adding application to PATH..."); - environment.AddDirectoryToPath(directoryPath, environmentTarget); - } - else - { - Context.Trace.WriteLine("Application is already on the PATH."); - } - } - string helperKey = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; - string gitConfigAppName = GetGitConfigAppName(); - - IGitConfiguration targetConfig = git.GetConfiguration(configurationLevel); - - /* - * We are looking for the following to be considered already set: - * - * [credential] - * ... # any number of helper entries - * helper = # an empty value to reset/clear any previous entries - * helper = {gitConfigAppName} # the expected executable value in the last position & directly following the empty value - * - */ - - string[] currentValues = targetConfig.GetRegex(helperKey, Constants.RegexPatterns.Any).ToArray(); - if (currentValues.Length < 2 || - !string.IsNullOrWhiteSpace(currentValues[currentValues.Length - 2]) || // second to last entry is empty - currentValues[currentValues.Length - 1] != gitConfigAppName) // last entry is the expected executable + string appPath = GetGitConfigAppPath(); + + IGitConfiguration config; + switch (target) { - Context.Trace.WriteLine("Updating Git credential helper configuration..."); + case ConfigurationTarget.User: + // For per-user configuration, we are looking for the following to be set in the global config: + // + // [credential] + // ... # any number of helper entries (possibly none) + // helper = # an empty value to reset/clear any previous entries (if applicable) + // helper = {appPath} # the expected executable value & directly following the empty value + // ... # any number of helper entries (possibly none) + // + config = Context.Git.GetConfiguration(GitConfigurationLevel.Global); + string[] currentValues = config.GetRegex(helperKey, Constants.RegexPatterns.Any).ToArray(); + + // Try to locate an existing app entry with a blank reset/clear entry immediately preceding + int appIndex = Array.FindIndex(currentValues, x => Context.FileSystem.IsSamePath(x, appPath)); + if (appIndex > 0 && string.IsNullOrWhiteSpace(currentValues[appIndex - 1])) + { + Context.Trace.WriteLine("Credential helper user configuration is already set correctly."); + } + else + { + Context.Trace.WriteLine("Updating Git credential helper user configuration..."); - // Clear any existing entries in the configuration. - targetConfig.UnsetAll(helperKey, Constants.RegexPatterns.Any); + // Clear any existing app entries in the configuration + config.UnsetAll(helperKey, Regex.Escape(appPath)); - // Add an empty value for `credential.helper`, which has the effect of clearing any helper value - // from any lower-level Git configuration, then add a second value which is the actual executable path. - targetConfig.SetValue(helperKey, string.Empty); - targetConfig.ReplaceAll(helperKey, Constants.RegexPatterns.None, gitConfigAppName); - } - else - { - Context.Trace.WriteLine("Credential helper configuration is already set correctly."); - } + // Add an empty value for `credential.helper`, which has the effect of clearing any helper value + // from any lower-level Git configuration, then add a second value which is the actual executable path. + config.ReplaceAll(helperKey, Constants.RegexPatterns.None, string.Empty); + config.ReplaceAll(helperKey, Constants.RegexPatterns.None, appPath); + } + break; + case ConfigurationTarget.System: + // For machine-wide configuration, we are looking for the following to be set in the system config: + // + // [credential] + // helper = {appPath} + // + config = Context.Git.GetConfiguration(GitConfigurationLevel.System); + string currentValue = config.GetValue(helperKey); + if (Context.FileSystem.IsSamePath(currentValue, appPath)) + { + Context.Trace.WriteLine("Credential helper system configuration is already set correctly."); + } + else + { + Context.Trace.WriteLine("Updating Git credential helper system configuration..."); + config.SetValue(helperKey, appPath); + } + + break; + + default: + throw new ArgumentOutOfRangeException(nameof(target), target, "Unknown configuration target."); + } return Task.CompletedTask; } - Task IConfigurableComponent.UnconfigureAsync( - IEnvironment environment, EnvironmentVariableTarget environmentTarget, - IGit git, GitConfigurationLevel configurationLevel) + Task IConfigurableComponent.UnconfigureAsync(ConfigurationTarget target) { string helperKey = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; - string gitConfigAppName = GetGitConfigAppName(); + string appPath = GetGitConfigAppPath(); - IGitConfiguration targetConfig = git.GetConfiguration(configurationLevel); - - Context.Trace.WriteLine("Removing Git credential helper configuration..."); + IGitConfiguration config; + switch (target) + { + case ConfigurationTarget.User: + // For per-user configuration, we are looking for the following to be set in the global config: + // + // [credential] + // ... # any number of helper entries (possibly none) + // helper = # an empty value to reset/clear any previous entries (if applicable) + // helper = {appPath} # the expected executable value & directly following the empty value + // ... # any number of helper entries (possibly none) + // + // We should remove the {appPath} entry, and any blank entries immediately preceding IFF there are no more entries following. + // + Context.Trace.WriteLine("Removing Git credential helper user configuration..."); + + config = Context.Git.GetConfiguration(GitConfigurationLevel.Global); + string[] currentValues = config.GetRegex(helperKey, Constants.RegexPatterns.Any).ToArray(); + + int appIndex = Array.FindIndex(currentValues, x => Context.FileSystem.IsSamePath(x, appPath)); + if (appIndex > -1) + { + // Check for the presence of a blank entry immediately preceding an app entry in the last position + if (appIndex > 0 && appIndex == currentValues.Length - 1 && + string.IsNullOrWhiteSpace(currentValues[appIndex - 1])) + { + // Clear the blank entry + config.UnsetAll(helperKey, Constants.RegexPatterns.Empty); + } - // Clear any blank 'reset' entries - targetConfig.UnsetAll(helperKey, Constants.RegexPatterns.Empty); + // Clear app entry + config.UnsetAll(helperKey, Regex.Escape(appPath)); + } + break; - // Clear GCM executable entries - targetConfig.UnsetAll(helperKey, Regex.Escape(gitConfigAppName)); + case ConfigurationTarget.System: + // For machine-wide configuration, we are looking for the following to be set in the system config: + // + // [credential] + // helper = {appPath} + // + // We should remove the {appPath} entry if it exists. + // + Context.Trace.WriteLine("Removing Git credential helper system configuration..."); + config = Context.Git.GetConfiguration(GitConfigurationLevel.System); + config.UnsetAll(helperKey, Regex.Escape(appPath)); + break; - // NOTE: We currently only update the PATH in Windows installations and leave removing the GCM executable - // on the PATH on other platform to their installers. - // Remove GCM executable from the PATH - if (PlatformUtils.IsWindows()) - { - Context.Trace.WriteLine("Removing application from the PATH..."); - string directoryPath = Path.GetDirectoryName(_appPath); - environment.RemoveDirectoryFromPath(directoryPath, environmentTarget); + default: + throw new ArgumentOutOfRangeException(nameof(target), target, "Unknown configuration target."); } return Task.CompletedTask; @@ -243,6 +282,23 @@ private string GetGitConfigAppName() return _appPath; } + private string GetGitConfigAppPath() + { + string path = _appPath; + + // On Windows we must use UNIX style path separators + if (PlatformUtils.IsWindows()) + { + path = path.Replace('\\', '/'); + } + + // We must escape escape characters like ' ', '(', and ')' + return path + .Replace(" ", "\\ ") + .Replace("(", "\\(") + .Replace(")", "\\)");; + } + #endregion } } diff --git a/src/shared/Microsoft.Git.CredentialManager/ConfigurationService.cs b/src/shared/Microsoft.Git.CredentialManager/ConfigurationService.cs index 7c9d3cc60..6e084c229 100644 --- a/src/shared/Microsoft.Git.CredentialManager/ConfigurationService.cs +++ b/src/shared/Microsoft.Git.CredentialManager/ConfigurationService.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -35,22 +34,14 @@ public interface IConfigurableComponent /// /// Configure the environment and Git to work with this hosting provider. /// - /// Environment variables. - /// Environment variable target to update. - /// Git object. - /// Git configuration level to update. - Task ConfigureAsync(IEnvironment environment, EnvironmentVariableTarget environmentTarget, - IGit git, GitConfigurationLevel configurationLevel); + /// Configuration target. + Task ConfigureAsync(ConfigurationTarget target); /// /// Remove changes to the environment and Git configuration previously made with . /// - /// Environment variables. - /// Environment variable target to update. - /// Git object. - /// Git configuration level to update. - Task UnconfigureAsync(IEnvironment environment, EnvironmentVariableTarget environmentTarget, - IGit git, GitConfigurationLevel configurationLevel); + /// Configuration target. + Task UnconfigureAsync(ConfigurationTarget target); } public interface IConfigurationService @@ -93,44 +84,23 @@ public void AddComponent(IConfigurableComponent component) _components.Add(component); } - public Task ConfigureAsync(ConfigurationTarget target) => RunAsync(target, true); - - public Task UnconfigureAsync(ConfigurationTarget target) => RunAsync(target, false); - - private async Task RunAsync(ConfigurationTarget target, bool configure) + public async Task ConfigureAsync(ConfigurationTarget target) { - GitConfigurationLevel configLevel; - EnvironmentVariableTarget envTarget; - switch (target) + foreach (IConfigurableComponent component in _components) { - case ConfigurationTarget.User: - configLevel = GitConfigurationLevel.Global; - envTarget = EnvironmentVariableTarget.User; - break; - - case ConfigurationTarget.System: - configLevel = GitConfigurationLevel.System; - envTarget = EnvironmentVariableTarget.Machine; - break; - - default: - throw new ArgumentOutOfRangeException(nameof(target)); + _context.Trace.WriteLine($"Configuring component '{component.Name}'..."); + _context.Streams.Error.WriteLine($"Configuring component '{component.Name}'..."); + await component.ConfigureAsync(target); } + } + public async Task UnconfigureAsync(ConfigurationTarget target) + { foreach (IConfigurableComponent component in _components) { - if (configure) - { - _context.Trace.WriteLine($"Configuring component '{component.Name}'..."); - _context.Streams.Error.WriteLine($"Configuring component '{component.Name}'..."); - await component.ConfigureAsync(_context.Environment, envTarget, _context.Git, configLevel); - } - else - { - _context.Trace.WriteLine($"Unconfiguring component '{component.Name}'..."); - _context.Streams.Error.WriteLine($"Unconfiguring component '{component.Name}'..."); - await component.UnconfigureAsync(_context.Environment, envTarget, _context.Git, configLevel); - } + _context.Trace.WriteLine($"Unconfiguring component '{component.Name}'..."); + _context.Streams.Error.WriteLine($"Unconfiguring component '{component.Name}'..."); + await component.UnconfigureAsync(target); } } From 4f12cc7f9399157893c68c97cb971028d2559b88 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 28 Oct 2020 15:25:39 +0000 Subject: [PATCH 14/25] windows: split Windows installer in to user/system Split the Windows installer into two versions: a single-user installer, and a system-wide installer. The old installer installed in Program Files (x86) and set the system Git configuration, which is now what the system-flavoured installer does. The new single-user installer places files in %LOCALAPPDATA% and updates the ~/.gitconfig (global) user Git configuration instead. The benefit of the single-user installer is that it does not require an administrator to install it, and doesn't affect other users of the system. --- .../Installer.Windows.csproj | 15 ++-- src/windows/Installer.Windows/Setup.iss | 72 ++++++++++++++----- 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/src/windows/Installer.Windows/Installer.Windows.csproj b/src/windows/Installer.Windows/Installer.Windows.csproj index 4e2f1b79b..fe95782d7 100644 --- a/src/windows/Installer.Windows/Installer.Windows.csproj +++ b/src/windows/Installer.Windows/Installer.Windows.csproj @@ -18,7 +18,7 @@ - + all @@ -29,7 +29,7 @@ before we attempt to sign any files or validate they exist. --> - + Microsoft400 false @@ -47,10 +47,13 @@ - $(NuGetPackageRoot)Tools.InnoSetup\5.6.1\tools\ISCC.exe /DPayloadDir=$(PayloadPath) Setup.iss /O$(OutputPath) + $(NuGetPackageRoot)Tools.InnoSetup\6.0.5\tools\ISCC.exe /DPayloadDir=$(PayloadPath) /DInstallTarget=system Setup.iss /O$(OutputPath) + $(NuGetPackageRoot)Tools.InnoSetup\6.0.5\tools\ISCC.exe /DPayloadDir=$(PayloadPath) /DInstallTarget=user Setup.iss /O$(OutputPath) - - + + + + @@ -59,4 +62,4 @@ - \ No newline at end of file + diff --git a/src/windows/Installer.Windows/Setup.iss b/src/windows/Installer.Windows/Setup.iss index 55f6d5b37..00031d93a 100644 --- a/src/windows/Installer.Windows/Setup.iss +++ b/src/windows/Installer.Windows/Setup.iss @@ -1,43 +1,63 @@ -; This script requires Inno Setup Compiler 5.6.1 or later to compile +; This script requires Inno Setup Compiler 6.0.0 or later to compile ; The Inno Setup Compiler (and IDE) can be found at http://www.jrsoftware.org/isinfo.php ; General documentation on how to use InnoSetup scripts: http://www.jrsoftware.org/ishelp/index.php ; Ensure minimum Inno Setup tooling version -#if VER < EncodeVer(5,6,1) - #error Update your Inno Setup version (5.6.1 or newer) +#if VER < EncodeVer(6,0,0) + #error Update your Inno Setup version (6.0.0 or newer) #endif #ifndef PayloadDir #error Payload directory path property 'PayloadDir' must be specified #endif -#ifnexist PayloadDir + "\git-credential-manager-core.exe" - #error Payload files are missing +#ifndef InstallTarget + #error Installer target property 'InstallTarget' must be specifed +#endif + +#if InstallTarget == "user" + #define GcmAppId "{{aa76d31d-432c-42ee-844c-bc0bc801cef3}}" + #define GcmLongName "Git Credential Manager Manager Core (User)" + #define GcmSetupExe "gcmcoreuser" + #define GcmConfigureCmdArgs "--user" +#elif InstallTarget == "system" + #define GcmAppId "{{fdfae50a-1bc1-4ead-9228-1e1c275e8d12}}" + #define GcmLongName "Git Credential Manager Manager Core" + #define GcmSetupExe "gcmcore" + #define GcmConfigureCmdArgs "--system" +#else + #error Installer target property 'InstallTarget' must be 'user' or 'system' #endif ; Define core properties -#define GcmName "Git Credential Manager Core" +#define GcmShortName "Git Credential Manager Core" #define GcmPublisher "Microsoft Corporation" #define GcmPublisherUrl "https://www.microsoft.com" -#define GcmCopyright "Copyright (c) Microsoft 2019" -#define GcmUrl "https://github.com/microsoft/Git-Credential-Manager-Core" +#define GcmCopyright "Copyright (c) Microsoft 2020" +#define GcmUrl "https://aka.ms/gcmcore" #define GcmReadme "https://github.com/microsoft/Git-Credential-Manager-Core/blob/master/README.md" #define GcmRepoRoot "..\..\.." #define GcmAssets GcmRepoRoot + "\assets" +#define GcmExe "git-credential-manager-core.exe" +#define GcmArch "x86" + +#ifnexist PayloadDir + "\" + GcmExe + #error Payload files are missing +#endif ; Generate the GCM version version from the CLI executable #define VerMajor #define VerMinor #define VerBuild #define VerRevision -#expr ParseVersion(PayloadDir + "\git-credential-manager-core.exe", VerMajor, VerMinor, VerBuild, VerRevision) +#expr ParseVersion(PayloadDir + "\" + GcmExe, VerMajor, VerMinor, VerBuild, VerRevision) #define GcmVersion str(VerMajor) + "." + str(VerMinor) + "." + str(VerBuild) + "." + str(VerRevision) [Setup] -AppId={{fdfae50a-1bc1-4ead-9228-1e1c275e8d12}} -AppName={#GcmName} +AppId={#GcmAppId} +AppName={#GcmLongName} AppVersion={#GcmVersion} -AppVerName={#GcmName} {#GcmVersion} +AppVerName={#GcmLongName} {#GcmVersion} AppPublisher={#GcmPublisher} AppPublisherURL={#GcmPublisherUrl} AppSupportURL={#GcmUrl} @@ -47,13 +67,13 @@ AppCopyright={#GcmCopyright} AppReadmeFile={#GcmReadme} VersionInfoVersion={#GcmVersion} LicenseFile={#GcmRepoRoot}\LICENSE -OutputBaseFilename=gcmcore-win-x86-{#GcmVersion} -DefaultDirName={pf}\{#GcmName} +OutputBaseFilename={#GcmSetupExe}-win-{#GcmArch}-{#GcmVersion} +DefaultDirName={autopf}\{#GcmShortName} Compression=lzma2 SolidCompression=yes MinVersion=6.1.7600 DisableDirPage=yes -UninstallDisplayIcon={app}\git-credential-manager-core.exe +UninstallDisplayIcon={app}\{#GcmExe} SetupIconFile={#GcmAssets}\gcmicon.ico WizardImageFile={#GcmAssets}\gcmicon128.bmp WizardSmallImageFile={#GcmAssets}\gcmicon64.bmp @@ -61,6 +81,9 @@ WizardStyle=modern WizardImageStretch=no WindowResizable=no ChangesEnvironment=yes +#if InstallTarget == "user" + PrivilegesRequired=lowest +#endif [Languages] Name: english; MessagesFile: "compiler:Default.isl"; @@ -72,10 +95,10 @@ Name: full; Description: "Full installation"; Flags: iscustom; ; No individual components [Run] -Filename: "{app}\git-credential-manager-core.exe"; Parameters: "configure"; Flags: runhidden +Filename: "{app}\{#GcmExe}"; Parameters: "configure {#GcmConfigureCmdArgs}"; Flags: runhidden [UninstallRun] -Filename: "{app}\git-credential-manager-core.exe"; Parameters: "unconfigure"; Flags: runhidden +Filename: "{app}\{#GcmExe}"; Parameters: "unconfigure {#GcmConfigureCmdArgs}"; Flags: runhidden [Files] Source: "{#PayloadDir}\Atlassian.Bitbucket.dll"; DestDir: "{app}"; Flags: ignoreversion @@ -95,3 +118,18 @@ Source: "{#PayloadDir}\Microsoft.IdentityModel.JsonWebTokens.dll"; DestDir: Source: "{#PayloadDir}\Microsoft.IdentityModel.Logging.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "{#PayloadDir}\Microsoft.IdentityModel.Tokens.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "{#PayloadDir}\Newtonsoft.Json.dll"; DestDir: "{app}"; Flags: ignoreversion + +[Code] +// Don't allow installing conflicting architectures +function InitializeSetup(): Boolean; +begin + Result := True; + + #if InstallTarget == "user" + if not WizardSilent() and IsAdmin() then begin + if MsgBox('This User Installer is not meant to be run as an Administrator. If you would like to install Git Credential Manager Core for all users in this system, download the System Installer instead from https://aka.ms/gcmcore-latest. Are you sure you want to continue?', mbError, MB_OKCANCEL) = IDCANCEL then begin + Result := False; + end; + end; + #endif +end; From 6d3132ecc24f05ce2cb56bd1c704d1c763ece928 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 29 Oct 2020 11:25:41 +0000 Subject: [PATCH 15/25] osx: fix incorrect wording of installer welcome msg Fix the incorrect wording of the macOS installer welcome screen. We do not configure different gitconfig files depending on the install mode - we always update the current user's gitconfig only. --- src/osx/Installer.Mac/resources/en.lproj/welcome.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/osx/Installer.Mac/resources/en.lproj/welcome.html b/src/osx/Installer.Mac/resources/en.lproj/welcome.html index 97c0454af..633453f20 100644 --- a/src/osx/Installer.Mac/resources/en.lproj/welcome.html +++ b/src/osx/Installer.Mac/resources/en.lproj/welcome.html @@ -21,7 +21,7 @@

Installation notes

If you have the old Java-based Git Credential Manager for Mac & Linux installed through Homebrew, it will be unlinked after installation.

- Git Credential Manager Core will be configured as the Git credential helper for the system or user, depending on if it is installed for all users, or just the current user. + Git Credential Manager Core will be configured as the Git credential helper for the current user by updating the global Git configuration file (~/.gitconfig).

From dc5135d66f3a9fe6126ad88ffaf05ce2e13e4b7b Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 29 Oct 2020 15:47:01 +0000 Subject: [PATCH 16/25] windows: fix a typo in the user windows installer --- src/windows/Installer.Windows/Setup.iss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/windows/Installer.Windows/Setup.iss b/src/windows/Installer.Windows/Setup.iss index 00031d93a..c46e6175e 100644 --- a/src/windows/Installer.Windows/Setup.iss +++ b/src/windows/Installer.Windows/Setup.iss @@ -17,12 +17,12 @@ #if InstallTarget == "user" #define GcmAppId "{{aa76d31d-432c-42ee-844c-bc0bc801cef3}}" - #define GcmLongName "Git Credential Manager Manager Core (User)" + #define GcmLongName "Git Credential Manager Core (User)" #define GcmSetupExe "gcmcoreuser" #define GcmConfigureCmdArgs "--user" #elif InstallTarget == "system" #define GcmAppId "{{fdfae50a-1bc1-4ead-9228-1e1c275e8d12}}" - #define GcmLongName "Git Credential Manager Manager Core" + #define GcmLongName "Git Credential Manager Core" #define GcmSetupExe "gcmcore" #define GcmConfigureCmdArgs "--system" #else From 364aeb34d6d17d60def3b6438eed86eb92b60600 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 29 Oct 2020 16:05:17 +0000 Subject: [PATCH 17/25] configure: append GCMCore even in system case Rather than set the helper to be _only_ GCM Core in the system configuration when called with `(un)configure --system`, we do what we are already doing in the user-case. We only append an empty ("") reset entry, and then the GCM full path entry. This means on uninstall of the standalone GCM (be that the system or user install), we restore the previous entry, always. --- .../Application.cs | 162 +++++++----------- 1 file changed, 62 insertions(+), 100 deletions(-) diff --git a/src/shared/Microsoft.Git.CredentialManager/Application.cs b/src/shared/Microsoft.Git.CredentialManager/Application.cs index 2148f7332..251df8c27 100644 --- a/src/shared/Microsoft.Git.CredentialManager/Application.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Application.cs @@ -3,7 +3,6 @@ using System; using System.IO; using System.Linq; -using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Git.CredentialManager.Commands; @@ -147,63 +146,41 @@ Task IConfigurableComponent.ConfigureAsync(ConfigurationTarget target) string helperKey = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; string appPath = GetGitConfigAppPath(); - IGitConfiguration config; - switch (target) - { - case ConfigurationTarget.User: - // For per-user configuration, we are looking for the following to be set in the global config: - // - // [credential] - // ... # any number of helper entries (possibly none) - // helper = # an empty value to reset/clear any previous entries (if applicable) - // helper = {appPath} # the expected executable value & directly following the empty value - // ... # any number of helper entries (possibly none) - // - config = Context.Git.GetConfiguration(GitConfigurationLevel.Global); - string[] currentValues = config.GetRegex(helperKey, Constants.RegexPatterns.Any).ToArray(); - - // Try to locate an existing app entry with a blank reset/clear entry immediately preceding - int appIndex = Array.FindIndex(currentValues, x => Context.FileSystem.IsSamePath(x, appPath)); - if (appIndex > 0 && string.IsNullOrWhiteSpace(currentValues[appIndex - 1])) - { - Context.Trace.WriteLine("Credential helper user configuration is already set correctly."); - } - else - { - Context.Trace.WriteLine("Updating Git credential helper user configuration..."); + GitConfigurationLevel configLevel = target == ConfigurationTarget.System + ? GitConfigurationLevel.System + : GitConfigurationLevel.Global; - // Clear any existing app entries in the configuration - config.UnsetAll(helperKey, Regex.Escape(appPath)); + Context.Trace.WriteLine($"Configuring for config level '{configLevel}'."); - // Add an empty value for `credential.helper`, which has the effect of clearing any helper value - // from any lower-level Git configuration, then add a second value which is the actual executable path. - config.ReplaceAll(helperKey, Constants.RegexPatterns.None, string.Empty); - config.ReplaceAll(helperKey, Constants.RegexPatterns.None, appPath); - } - break; + IGitConfiguration config = Context.Git.GetConfiguration(configLevel); - case ConfigurationTarget.System: - // For machine-wide configuration, we are looking for the following to be set in the system config: - // - // [credential] - // helper = {appPath} - // - config = Context.Git.GetConfiguration(GitConfigurationLevel.System); - string currentValue = config.GetValue(helperKey); - if (Context.FileSystem.IsSamePath(currentValue, appPath)) - { - Context.Trace.WriteLine("Credential helper system configuration is already set correctly."); - } - else - { - Context.Trace.WriteLine("Updating Git credential helper system configuration..."); - config.SetValue(helperKey, appPath); - } + // We are looking for the following to be set in the config: + // + // [credential] + // ... # any number of helper entries (possibly none) + // helper = # an empty value to reset/clear any previous entries (if applicable) + // helper = {appPath} # the expected executable value & directly following the empty value + // ... # any number of helper entries (possibly none) + // + string[] currentValues = config.GetRegex(helperKey, Constants.RegexPatterns.Any).ToArray(); - break; + // Try to locate an existing app entry with a blank reset/clear entry immediately preceding + int appIndex = Array.FindIndex(currentValues, x => Context.FileSystem.IsSamePath(x, appPath)); + if (appIndex > 0 && string.IsNullOrWhiteSpace(currentValues[appIndex - 1])) + { + Context.Trace.WriteLine("Credential helper configuration is already set correctly."); + } + else + { + Context.Trace.WriteLine("Updating Git credential helper configuration..."); - default: - throw new ArgumentOutOfRangeException(nameof(target), target, "Unknown configuration target."); + // Clear any existing app entries in the configuration + config.UnsetAll(helperKey, Regex.Escape(appPath)); + + // Add an empty value for `credential.helper`, which has the effect of clearing any helper value + // from any lower-level Git configuration, then add a second value which is the actual executable path. + config.ReplaceAll(helperKey, Constants.RegexPatterns.None, string.Empty); + config.ReplaceAll(helperKey, Constants.RegexPatterns.None, appPath); } return Task.CompletedTask; @@ -214,56 +191,41 @@ Task IConfigurableComponent.UnconfigureAsync(ConfigurationTarget target) string helperKey = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; string appPath = GetGitConfigAppPath(); - IGitConfiguration config; - switch (target) - { - case ConfigurationTarget.User: - // For per-user configuration, we are looking for the following to be set in the global config: - // - // [credential] - // ... # any number of helper entries (possibly none) - // helper = # an empty value to reset/clear any previous entries (if applicable) - // helper = {appPath} # the expected executable value & directly following the empty value - // ... # any number of helper entries (possibly none) - // - // We should remove the {appPath} entry, and any blank entries immediately preceding IFF there are no more entries following. - // - Context.Trace.WriteLine("Removing Git credential helper user configuration..."); - - config = Context.Git.GetConfiguration(GitConfigurationLevel.Global); - string[] currentValues = config.GetRegex(helperKey, Constants.RegexPatterns.Any).ToArray(); - - int appIndex = Array.FindIndex(currentValues, x => Context.FileSystem.IsSamePath(x, appPath)); - if (appIndex > -1) - { - // Check for the presence of a blank entry immediately preceding an app entry in the last position - if (appIndex > 0 && appIndex == currentValues.Length - 1 && - string.IsNullOrWhiteSpace(currentValues[appIndex - 1])) - { - // Clear the blank entry - config.UnsetAll(helperKey, Constants.RegexPatterns.Empty); - } + GitConfigurationLevel configLevel = target == ConfigurationTarget.System + ? GitConfigurationLevel.System + : GitConfigurationLevel.Global; - // Clear app entry - config.UnsetAll(helperKey, Regex.Escape(appPath)); - } - break; + Context.Trace.WriteLine($"Unconfiguring for config level '{configLevel}'."); - case ConfigurationTarget.System: - // For machine-wide configuration, we are looking for the following to be set in the system config: - // - // [credential] - // helper = {appPath} - // - // We should remove the {appPath} entry if it exists. - // - Context.Trace.WriteLine("Removing Git credential helper system configuration..."); - config = Context.Git.GetConfiguration(GitConfigurationLevel.System); - config.UnsetAll(helperKey, Regex.Escape(appPath)); - break; + IGitConfiguration config = Context.Git.GetConfiguration(configLevel); - default: - throw new ArgumentOutOfRangeException(nameof(target), target, "Unknown configuration target."); + // We are looking for the following to be set in the config: + // + // [credential] + // ... # any number of helper entries (possibly none) + // helper = # an empty value to reset/clear any previous entries (if applicable) + // helper = {appPath} # the expected executable value & directly following the empty value + // ... # any number of helper entries (possibly none) + // + // We should remove the {appPath} entry, and any blank entries immediately preceding IFF there are no more entries following. + // + Context.Trace.WriteLine("Removing Git credential helper configuration..."); + + string[] currentValues = config.GetRegex(helperKey, Constants.RegexPatterns.Any).ToArray(); + + int appIndex = Array.FindIndex(currentValues, x => Context.FileSystem.IsSamePath(x, appPath)); + if (appIndex > -1) + { + // Check for the presence of a blank entry immediately preceding an app entry in the last position + if (appIndex > 0 && appIndex == currentValues.Length - 1 && + string.IsNullOrWhiteSpace(currentValues[appIndex - 1])) + { + // Clear the blank entry + config.UnsetAll(helperKey, Constants.RegexPatterns.Empty); + } + + // Clear app entry + config.UnsetAll(helperKey, Regex.Escape(appPath)); } return Task.CompletedTask; From 1c01f1f2bf50cf2136fff631f40d24cacc15dc93 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 29 Oct 2020 17:04:02 +0000 Subject: [PATCH 18/25] git: add --get-all and --add Git config commands Add the --get-all and --add Git configuration commands, and use them in place of --get-regexp and --replace-all where applicable. --- .../Application.cs | 8 +-- .../GitConfiguration.cs | 70 +++++++++++++++++++ .../Objects/TestGitConfiguration.cs | 28 +++++++- 3 files changed, 100 insertions(+), 6 deletions(-) diff --git a/src/shared/Microsoft.Git.CredentialManager/Application.cs b/src/shared/Microsoft.Git.CredentialManager/Application.cs index 251df8c27..d4906ff0b 100644 --- a/src/shared/Microsoft.Git.CredentialManager/Application.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Application.cs @@ -162,7 +162,7 @@ Task IConfigurableComponent.ConfigureAsync(ConfigurationTarget target) // helper = {appPath} # the expected executable value & directly following the empty value // ... # any number of helper entries (possibly none) // - string[] currentValues = config.GetRegex(helperKey, Constants.RegexPatterns.Any).ToArray(); + string[] currentValues = config.GetAll(helperKey).ToArray(); // Try to locate an existing app entry with a blank reset/clear entry immediately preceding int appIndex = Array.FindIndex(currentValues, x => Context.FileSystem.IsSamePath(x, appPath)); @@ -179,8 +179,8 @@ Task IConfigurableComponent.ConfigureAsync(ConfigurationTarget target) // Add an empty value for `credential.helper`, which has the effect of clearing any helper value // from any lower-level Git configuration, then add a second value which is the actual executable path. - config.ReplaceAll(helperKey, Constants.RegexPatterns.None, string.Empty); - config.ReplaceAll(helperKey, Constants.RegexPatterns.None, appPath); + config.Add(helperKey, string.Empty); + config.Add(helperKey, appPath); } return Task.CompletedTask; @@ -211,7 +211,7 @@ Task IConfigurableComponent.UnconfigureAsync(ConfigurationTarget target) // Context.Trace.WriteLine("Removing Git credential helper configuration..."); - string[] currentValues = config.GetRegex(helperKey, Constants.RegexPatterns.Any).ToArray(); + string[] currentValues = config.GetAll(helperKey).ToArray(); int appIndex = Array.FindIndex(currentValues, x => Context.FileSystem.IsSamePath(x, appPath)); if (appIndex > -1) diff --git a/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs b/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs index 304732606..57d385801 100644 --- a/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs +++ b/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs @@ -49,12 +49,26 @@ public interface IGitConfiguration /// Configuration entry value. void SetValue(string name, string value); + /// + /// Add a new value for a configuration entry. + /// + /// Configuration entry name. + /// Configuration entry value. + void Add(string name, string value); + /// /// Deletes a configuration entry from the highest level. /// /// Configuration entry name. void Unset(string name); + /// + /// Get all value of a multivar configuration entry. + /// + /// Configuration entry name. + /// All values of the multivar configuration entry. + IEnumerable GetAll(string name); + /// /// Get all values of a multivar configuration entry. /// @@ -188,6 +202,30 @@ public void SetValue(string name, string value) } } + public void Add(string name, string value) + { + if (_filterLevel == GitConfigurationLevel.All) + { + throw new InvalidOperationException("Must have a specific configuration level filter to add values."); + } + + string level = GetLevelFilterArg(); + using (Process git = _git.CreateProcess($"config {level} --add {QuoteCmdArg(name)} {QuoteCmdArg(value)}")) + { + git.Start(); + git.WaitForExit(); + + switch (git.ExitCode) + { + case 0: // OK + break; + default: + throw new Exception( + $"Failed to add Git configuration entry '{name}' with value '{value}'. Exit code '{git.ExitCode}' (level={_filterLevel})"); + } + } + } + public void Unset(string name) { if (_filterLevel == GitConfigurationLevel.All) @@ -212,6 +250,38 @@ public void Unset(string name) } } + public IEnumerable GetAll(string name) + { + string level = GetLevelFilterArg(); + + var gitArgs = $"config --null {level} --get-all {QuoteCmdArg(name)}"; + + using (Process git = _git.CreateProcess(gitArgs)) + { + git.Start(); + + // TODO: don't read in all the data at once; stream it + string data = git.StandardOutput.ReadToEnd(); + git.WaitForExit(); + + switch (git.ExitCode) + { + case 0: // OK + case 1: // No results + break; + default: + throw new Exception( + $"Failed to get Git configuration multi-valued entry '{name}'. Exit code '{git.ExitCode}' (level={_filterLevel})"); + } + + string[] entries = data.Split('\0'); + foreach (string entry in entries) + { + yield return entry; + } + } + } + public IEnumerable GetRegex(string nameRegex, string valueRegex) { string level = GetLevelFilterArg(); diff --git a/src/shared/TestInfrastructure/Objects/TestGitConfiguration.cs b/src/shared/TestInfrastructure/Objects/TestGitConfiguration.cs index 47173a58e..71f1313c7 100644 --- a/src/shared/TestInfrastructure/Objects/TestGitConfiguration.cs +++ b/src/shared/TestInfrastructure/Objects/TestGitConfiguration.cs @@ -90,6 +90,17 @@ public void SetValue(string name, string value) } } + public void Add(string name, string value) + { + if (!Dictionary.TryGetValue(name, out IList values)) + { + values = new List(); + Dictionary[name] = values; + } + + values.Add(value); + } + public void Unset(string name) { // TODO: simulate git @@ -101,11 +112,24 @@ public void Unset(string name) Dictionary.Remove(name); } + public IEnumerable GetAll(string name) + { + if (Dictionary.TryGetValue(name, out IList values)) + { + return values; + } + + return Enumerable.Empty(); + } + public IEnumerable GetRegex(string nameRegex, string valueRegex) { - if (Dictionary.TryGetValue(nameRegex, out IList values)) + foreach (string key in Dictionary.Keys) { - return values.Where(x => Regex.IsMatch(x, valueRegex)); + if (Regex.IsMatch(key, nameRegex)) + { + return Dictionary[key].Where(x => Regex.IsMatch(x, valueRegex)); + } } return Enumerable.Empty(); From 1638afef0ce32a73bcdcd3e468cb6de2365e2f95 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 29 Oct 2020 17:05:41 +0000 Subject: [PATCH 19/25] git: drop the redundant 'Value' from Get/Set methods --- .../AzureReposHostProvider.cs | 4 +- .../GitConfigurationTests.cs | 50 +++++++++---------- .../GitConfiguration.cs | 32 ++++++------ .../Settings.cs | 2 +- .../Objects/TestGitConfiguration.cs | 8 +-- .../Objects/TestSettings.cs | 2 +- 6 files changed, 49 insertions(+), 49 deletions(-) diff --git a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs index 57962cb12..0419feeed 100644 --- a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs +++ b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs @@ -260,14 +260,14 @@ public Task ConfigureAsync(ConfigurationTarget target) IGitConfiguration targetConfig = _context.Git.GetConfiguration(configurationLevel); - if (targetConfig.TryGetValue(useHttpPathKey, out string currentValue) && currentValue.IsTruthy()) + if (targetConfig.TryGet(useHttpPathKey, out string currentValue) && currentValue.IsTruthy()) { _context.Trace.WriteLine("Git configuration 'credential.useHttpPath' is already set to 'true' for https://dev.azure.com."); } else { _context.Trace.WriteLine("Setting Git configuration 'credential.useHttpPath' to 'true' for https://dev.azure.com..."); - targetConfig.SetValue(useHttpPathKey, "true"); + targetConfig.Set(useHttpPathKey, "true"); } return Task.CompletedTask; diff --git a/src/shared/Microsoft.Git.CredentialManager.Tests/GitConfigurationTests.cs b/src/shared/Microsoft.Git.CredentialManager.Tests/GitConfigurationTests.cs index 2bbc4ba5f..89aae5d48 100644 --- a/src/shared/Microsoft.Git.CredentialManager.Tests/GitConfigurationTests.cs +++ b/src/shared/Microsoft.Git.CredentialManager.Tests/GitConfigurationTests.cs @@ -130,7 +130,7 @@ bool cb(string name, string value) } [Fact] - public void GitConfiguration_TryGetValue_Name_Exists_ReturnsTrueOutString() + public void GitConfiguration_TryGet_Name_Exists_ReturnsTrueOutString() { string repoPath = CreateRepository(out string workDirPath); Git(repoPath, workDirPath, "config --local user.name john.doe").AssertSuccess(); @@ -140,14 +140,14 @@ public void GitConfiguration_TryGetValue_Name_Exists_ReturnsTrueOutString() var git = new GitProcess(trace, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); - bool result = config.TryGetValue("user.name", out string value); + bool result = config.TryGet("user.name", out string value); Assert.True(result); Assert.NotNull(value); Assert.Equal("john.doe", value); } [Fact] - public void GitConfiguration_TryGetValue_Name_DoesNotExists_ReturnsFalse() + public void GitConfiguration_TryGet_Name_DoesNotExists_ReturnsFalse() { string repoPath = CreateRepository(); @@ -157,13 +157,13 @@ public void GitConfiguration_TryGetValue_Name_DoesNotExists_ReturnsFalse() IGitConfiguration config = git.GetConfiguration(); string randomName = $"{Guid.NewGuid():N}.{Guid.NewGuid():N}"; - bool result = config.TryGetValue(randomName, out string value); + bool result = config.TryGet(randomName, out string value); Assert.False(result); Assert.Null(value); } [Fact] - public void GitConfiguration_TryGetValue_SectionProperty_Exists_ReturnsTrueOutString() + public void GitConfiguration_TryGet_SectionProperty_Exists_ReturnsTrueOutString() { string repoPath = CreateRepository(out string workDirPath); Git(repoPath, workDirPath, "config --local user.name john.doe").AssertSuccess(); @@ -173,14 +173,14 @@ public void GitConfiguration_TryGetValue_SectionProperty_Exists_ReturnsTrueOutSt var git = new GitProcess(trace, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); - bool result = config.TryGetValue("user", "name", out string value); + bool result = config.TryGet("user", "name", out string value); Assert.True(result); Assert.NotNull(value); Assert.Equal("john.doe", value); } [Fact] - public void GitConfiguration_TryGetValue_SectionProperty_DoesNotExists_ReturnsFalse() + public void GitConfiguration_TryGet_SectionProperty_DoesNotExists_ReturnsFalse() { string repoPath = CreateRepository(); @@ -191,13 +191,13 @@ public void GitConfiguration_TryGetValue_SectionProperty_DoesNotExists_ReturnsFa string randomSection = Guid.NewGuid().ToString("N"); string randomProperty = Guid.NewGuid().ToString("N"); - bool result = config.TryGetValue(randomSection, randomProperty, out string value); + bool result = config.TryGet(randomSection, randomProperty, out string value); Assert.False(result); Assert.Null(value); } [Fact] - public void GitConfiguration_TryGetValue_SectionScopeProperty_Exists_ReturnsTrueOutString() + public void GitConfiguration_TryGet_SectionScopeProperty_Exists_ReturnsTrueOutString() { string repoPath = CreateRepository(out string workDirPath); Git(repoPath, workDirPath, "config --local user.example.com.name john.doe").AssertSuccess(); @@ -207,14 +207,14 @@ public void GitConfiguration_TryGetValue_SectionScopeProperty_Exists_ReturnsTrue var git = new GitProcess(trace, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); - bool result = config.TryGetValue("user", "example.com", "name", out string value); + bool result = config.TryGet("user", "example.com", "name", out string value); Assert.True(result); Assert.NotNull(value); Assert.Equal("john.doe", value); } [Fact] - public void GitConfiguration_TryGetValue_SectionScopeProperty_NullScope_ReturnsTrueOutUnscopedString() + public void GitConfiguration_TryGet_SectionScopeProperty_NullScope_ReturnsTrueOutUnscopedString() { string repoPath = CreateRepository(out string workDirPath); Git(repoPath, workDirPath, "config --local user.name john.doe").AssertSuccess(); @@ -224,14 +224,14 @@ public void GitConfiguration_TryGetValue_SectionScopeProperty_NullScope_ReturnsT var git = new GitProcess(trace, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); - bool result = config.TryGetValue("user", null, "name", out string value); + bool result = config.TryGet("user", null, "name", out string value); Assert.True(result); Assert.NotNull(value); Assert.Equal("john.doe", value); } [Fact] - public void GitConfiguration_TryGetValue_SectionScopeProperty_DoesNotExists_ReturnsFalse() + public void GitConfiguration_TryGet_SectionScopeProperty_DoesNotExists_ReturnsFalse() { string repoPath = CreateRepository(); @@ -243,7 +243,7 @@ public void GitConfiguration_TryGetValue_SectionScopeProperty_DoesNotExists_Retu string randomSection = Guid.NewGuid().ToString("N"); string randomScope = Guid.NewGuid().ToString("N"); string randomProperty = Guid.NewGuid().ToString("N"); - bool result = config.TryGetValue(randomSection, randomScope, randomProperty, out string value); + bool result = config.TryGet(randomSection, randomScope, randomProperty, out string value); Assert.False(result); Assert.Null(value); } @@ -259,7 +259,7 @@ public void GitConfiguration_GetString_Name_Exists_ReturnsString() var git = new GitProcess(trace, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); - string value = config.GetValue("user.name"); + string value = config.Get("user.name"); Assert.NotNull(value); Assert.Equal("john.doe", value); } @@ -275,7 +275,7 @@ public void GitConfiguration_GetString_Name_DoesNotExists_ThrowsException() IGitConfiguration config = git.GetConfiguration(); string randomName = $"{Guid.NewGuid():N}.{Guid.NewGuid():N}"; - Assert.Throws(() => config.GetValue(randomName)); + Assert.Throws(() => config.Get(randomName)); } [Fact] @@ -289,7 +289,7 @@ public void GitConfiguration_GetString_SectionProperty_Exists_ReturnsString() var git = new GitProcess(trace, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); - string value = config.GetValue("user", "name"); + string value = config.Get("user", "name"); Assert.NotNull(value); Assert.Equal("john.doe", value); } @@ -306,7 +306,7 @@ public void GitConfiguration_GetString_SectionProperty_DoesNotExists_ThrowsExcep string randomSection = Guid.NewGuid().ToString("N"); string randomProperty = Guid.NewGuid().ToString("N"); - Assert.Throws(() => config.GetValue(randomSection, randomProperty)); + Assert.Throws(() => config.Get(randomSection, randomProperty)); } [Fact] @@ -320,7 +320,7 @@ public void GitConfiguration_GetString_SectionScopeProperty_Exists_ReturnsString var git = new GitProcess(trace, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); - string value = config.GetValue("user", "example.com", "name"); + string value = config.Get("user", "example.com", "name"); Assert.NotNull(value); Assert.Equal("john.doe", value); } @@ -336,7 +336,7 @@ public void GitConfiguration_GetString_SectionScopeProperty_NullScope_ReturnsUns var git = new GitProcess(trace, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); - string value = config.GetValue("user", null, "name"); + string value = config.Get("user", null, "name"); Assert.NotNull(value); Assert.Equal("john.doe", value); } @@ -354,11 +354,11 @@ public void GitConfiguration_GetString_SectionScopeProperty_DoesNotExists_Throws string randomSection = Guid.NewGuid().ToString("N"); string randomScope = Guid.NewGuid().ToString("N"); string randomProperty = Guid.NewGuid().ToString("N"); - Assert.Throws(() => config.GetValue(randomSection, randomScope, randomProperty)); + Assert.Throws(() => config.Get(randomSection, randomScope, randomProperty)); } [Fact] - public void GitConfiguration_SetValue_Local_SetsLocalConfig() + public void GitConfiguration_Set_Local_SetsLocalConfig() { string repoPath = CreateRepository(out string workDirPath); @@ -367,7 +367,7 @@ public void GitConfiguration_SetValue_Local_SetsLocalConfig() var git = new GitProcess(trace, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(GitConfigurationLevel.Local); - config.SetValue("core.foobar", "foo123"); + config.Set("core.foobar", "foo123"); GitResult localResult = Git(repoPath, workDirPath, "config --local core.foobar"); @@ -375,7 +375,7 @@ public void GitConfiguration_SetValue_Local_SetsLocalConfig() } [Fact] - public void GitConfiguration_SetValue_All_ThrowsException() + public void GitConfiguration_Set_All_ThrowsException() { string repoPath = CreateRepository(out _); @@ -384,7 +384,7 @@ public void GitConfiguration_SetValue_All_ThrowsException() var git = new GitProcess(trace, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(GitConfigurationLevel.All); - Assert.Throws(() => config.SetValue("core.foobar", "test123")); + Assert.Throws(() => config.Set("core.foobar", "test123")); } [Fact] diff --git a/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs b/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs index 57d385801..98ea5c34c 100644 --- a/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs +++ b/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs @@ -40,14 +40,14 @@ public interface IGitConfiguration /// Configuration entry name. /// Configuration entry value. /// True if the value was found, false otherwise. - bool TryGetValue(string name, out string value); + bool TryGet(string name, out string value); /// /// Set the value of a configuration entry. /// /// Configuration entry name. /// Configuration entry value. - void SetValue(string name, string value); + void Set(string name, string value); /// /// Add a new value for a configuration entry. @@ -143,7 +143,7 @@ public void Enumerate(GitConfigurationEnumerationCallback cb) } } - public bool TryGetValue(string name, out string value) + public bool TryGet(string name, out string value) { string level = GetLevelFilterArg(); using (Process git = _git.CreateProcess($"config {level} {QuoteCmdArg(name)}")) @@ -178,7 +178,7 @@ public bool TryGetValue(string name, out string value) } } - public void SetValue(string name, string value) + public void Set(string name, string value) { if (_filterLevel == GitConfigurationLevel.All) { @@ -482,9 +482,9 @@ public static class GitConfigurationExtensions /// Configuration object. /// Configuration entry name. /// Configuration entry value. - public static string GetValue(this IGitConfiguration config, string name) + public static string Get(this IGitConfiguration config, string name) { - if (!config.TryGetValue(name, out string value)) + if (!config.TryGet(name, out string value)) { throw new KeyNotFoundException($"Git configuration entry with the name '{name}' was not found."); } @@ -500,9 +500,9 @@ public static string GetValue(this IGitConfiguration config, string name) /// Configuration property name. /// A configuration entry with the specified key was not found. /// Configuration entry value. - public static string GetValue(this IGitConfiguration config, string section, string property) + public static string Get(this IGitConfiguration config, string section, string property) { - return GetValue(config, $"{section}.{property}"); + return Get(config, $"{section}.{property}"); } /// @@ -514,14 +514,14 @@ public static string GetValue(this IGitConfiguration config, string section, str /// Configuration property name. /// A configuration entry with the specified key was not found. /// Configuration entry value. - public static string GetValue(this IGitConfiguration config, string section, string scope, string property) + public static string Get(this IGitConfiguration config, string section, string scope, string property) { if (scope is null) { - return GetValue(config, section, property); + return Get(config, section, property); } - return GetValue(config, $"{section}.{scope}.{property}"); + return Get(config, $"{section}.{scope}.{property}"); } /// @@ -532,9 +532,9 @@ public static string GetValue(this IGitConfiguration config, string section, str /// Configuration property name. /// Configuration entry value. /// True if the value was found, false otherwise. - public static bool TryGetValue(this IGitConfiguration config, string section, string property, out string value) + public static bool TryGet(this IGitConfiguration config, string section, string property, out string value) { - return config.TryGetValue($"{section}.{property}", out value); + return config.TryGet($"{section}.{property}", out value); } /// @@ -546,14 +546,14 @@ public static bool TryGetValue(this IGitConfiguration config, string section, st /// Configuration property name. /// Configuration entry value. /// True if the value was found, false otherwise. - public static bool TryGetValue(this IGitConfiguration config, string section, string scope, string property, out string value) + public static bool TryGet(this IGitConfiguration config, string section, string scope, string property, out string value) { if (scope is null) { - return TryGetValue(config, section, property, out value); + return TryGet(config, section, property, out value); } - return config.TryGetValue($"{section}.{scope}.{property}", out value); + return config.TryGet($"{section}.{scope}.{property}", out value); } } } diff --git a/src/shared/Microsoft.Git.CredentialManager/Settings.cs b/src/shared/Microsoft.Git.CredentialManager/Settings.cs index 11fef2937..dc1ee19e4 100644 --- a/src/shared/Microsoft.Git.CredentialManager/Settings.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Settings.cs @@ -247,7 +247,7 @@ public IEnumerable GetSettingValues(string envarName, string section, st * property = value * */ - if (config.TryGetValue($"{section}.{property}", out value)) + if (config.TryGet($"{section}.{property}", out value)) { yield return value; } diff --git a/src/shared/TestInfrastructure/Objects/TestGitConfiguration.cs b/src/shared/TestInfrastructure/Objects/TestGitConfiguration.cs index 71f1313c7..2925d9021 100644 --- a/src/shared/TestInfrastructure/Objects/TestGitConfiguration.cs +++ b/src/shared/TestInfrastructure/Objects/TestGitConfiguration.cs @@ -25,8 +25,8 @@ public TestGitConfiguration(IDictionary> config = null) /// public string this[string key] { - get => TryGetValue(key, out string value) ? value : null; - set => SetValue(key, value); + get => TryGet(key, out string value) ? value : null; + set => Set(key, value); } #region IGitConfiguration @@ -45,7 +45,7 @@ public void Enumerate(GitConfigurationEnumerationCallback cb) } } - public bool TryGetValue(string name, out string value) + public bool TryGet(string name, out string value) { if (Dictionary.TryGetValue(name, out var values)) { @@ -66,7 +66,7 @@ public bool TryGetValue(string name, out string value) return false; } - public void SetValue(string name, string value) + public void Set(string name, string value) { if (!Dictionary.TryGetValue(name, out IList values)) { diff --git a/src/shared/TestInfrastructure/Objects/TestSettings.cs b/src/shared/TestInfrastructure/Objects/TestSettings.cs index d6e18b021..408bc5cd6 100644 --- a/src/shared/TestInfrastructure/Objects/TestSettings.cs +++ b/src/shared/TestInfrastructure/Objects/TestSettings.cs @@ -52,7 +52,7 @@ public bool TryGetSetting(string envarName, string section, string property, out return true; } - if (GitConfiguration?.TryGetValue(section, property, out value) ?? false) + if (GitConfiguration?.TryGet(section, property, out value) ?? false) { return true; } From 16ee30429bab009c9f1896ef26bbb22760c3ec1b Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 29 Oct 2020 17:27:29 +0000 Subject: [PATCH 20/25] configure: handle subsequent empty entries after GCM If we have empty helper entries after GCM, then we should attempt to reconfigure to put GCM back "at the front", since otherwise it's effectively being disabled by those subsequent empty entries. --- .../ApplicationTests.cs | 28 +++++++++++++++++++ .../Application.cs | 8 ++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/shared/Microsoft.Git.CredentialManager.Tests/ApplicationTests.cs b/src/shared/Microsoft.Git.CredentialManager.Tests/ApplicationTests.cs index 4b5dad792..bdb78ad0e 100644 --- a/src/shared/Microsoft.Git.CredentialManager.Tests/ApplicationTests.cs +++ b/src/shared/Microsoft.Git.CredentialManager.Tests/ApplicationTests.cs @@ -152,6 +152,34 @@ public async Task Application_ConfigureAsync_EmptyAndGcmWithOthersBeforeAndAfter Assert.Equal(afterHelper, actualValues[3]); } + [Fact] + public async Task Application_ConfigureAsync_EmptyAndGcmWithEmptyAfter_RemovesExistingGcmAndAddsEmptyAndGcm() + { + const string emptyHelper = ""; + const string afterHelper = "foo"; + const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core"; + string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; + + var context = new TestCommandContext(); + IConfigurableComponent application = new Application(context, executablePath); + + context.Git.GlobalConfiguration.Dictionary[key] = new List + { + emptyHelper, executablePath, emptyHelper, afterHelper + }; + + await application.ConfigureAsync(ConfigurationTarget.User); + + Assert.Single(context.Git.GlobalConfiguration.Dictionary); + Assert.True(context.Git.GlobalConfiguration.Dictionary.TryGetValue(key, out var actualValues)); + Assert.Equal(5, actualValues.Count); + Assert.Equal(emptyHelper, actualValues[0]); + Assert.Equal(emptyHelper, actualValues[1]); + Assert.Equal(afterHelper, actualValues[2]); + Assert.Equal(emptyHelper, actualValues[3]); + Assert.Equal(executablePath, actualValues[4]); + } + [Fact] public async Task Application_UnconfigureAsync_NoHelpers_DoesNothing() { diff --git a/src/shared/Microsoft.Git.CredentialManager/Application.cs b/src/shared/Microsoft.Git.CredentialManager/Application.cs index d4906ff0b..9724959c1 100644 --- a/src/shared/Microsoft.Git.CredentialManager/Application.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Application.cs @@ -160,13 +160,15 @@ Task IConfigurableComponent.ConfigureAsync(ConfigurationTarget target) // ... # any number of helper entries (possibly none) // helper = # an empty value to reset/clear any previous entries (if applicable) // helper = {appPath} # the expected executable value & directly following the empty value - // ... # any number of helper entries (possibly none) + // ... # any number of helper entries (possibly none, but not the empty value '') // string[] currentValues = config.GetAll(helperKey).ToArray(); - // Try to locate an existing app entry with a blank reset/clear entry immediately preceding + // Try to locate an existing app entry with a blank reset/clear entry immediately preceding, + // and no other blank empty/clear entries following (which effectively disable us). int appIndex = Array.FindIndex(currentValues, x => Context.FileSystem.IsSamePath(x, appPath)); - if (appIndex > 0 && string.IsNullOrWhiteSpace(currentValues[appIndex - 1])) + int lastEmptyIndex = Array.FindLastIndex(currentValues, string.IsNullOrWhiteSpace); + if (appIndex > 0 && string.IsNullOrWhiteSpace(currentValues[appIndex - 1]) && lastEmptyIndex < appIndex) { Context.Trace.WriteLine("Credential helper configuration is already set correctly."); } From 8ce1ed2a34d8ff390262a24a1c58f36c6fcdf2fe Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Fri, 30 Oct 2020 10:31:35 +0000 Subject: [PATCH 21/25] git: better gitcfg error and trace messages --- .../GitConfiguration.cs | 53 ++++++++++++------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs b/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs index 98ea5c34c..ed7e60888 100644 --- a/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs +++ b/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs @@ -126,8 +126,8 @@ public void Enumerate(GitConfigurationEnumerationCallback cb) case 0: // OK break; default: - throw new Exception( - $"Failed to enumerate all Git configuration entries. Exit code '{git.ExitCode}' (level={_filterLevel})"); + _trace.WriteLine($"Failed to enumerate config entries (exit={git.ExitCode}, level={_filterLevel})"); + throw CreateGitException(git, "Failed to enumerate all Git configuration entries"); } IEnumerable entries = data.Split('\0').Where(x => !string.IsNullOrWhiteSpace(x)); @@ -159,8 +159,7 @@ public bool TryGet(string name, out string value) value = null; return false; default: // Error - _trace.WriteLine( - $"Failed to read Git configuration entry '{name}'. Exit code '{git.ExitCode}' (level={_filterLevel})"); + _trace.WriteLine($"Failed to read Git configuration entry '{name}'. (exit={git.ExitCode}, level={_filterLevel})"); value = null; return false; } @@ -196,8 +195,8 @@ public void Set(string name, string value) case 0: // OK break; default: - throw new Exception( - $"Failed to set Git configuration entry '{name}' to '{value}'. Exit code '{git.ExitCode}' (level={_filterLevel})"); + _trace.WriteLine($"Failed to set config entry '{name}' to value '{value}' (exit={git.ExitCode}, level={_filterLevel})"); + throw CreateGitException(git, $"Failed to set Git configuration entry '{name}'"); } } } @@ -220,8 +219,8 @@ public void Add(string name, string value) case 0: // OK break; default: - throw new Exception( - $"Failed to add Git configuration entry '{name}' with value '{value}'. Exit code '{git.ExitCode}' (level={_filterLevel})"); + _trace.WriteLine($"Failed to add config entry '{name}' with value '{value}' (exit={git.ExitCode}, level={_filterLevel})"); + throw CreateGitException(git, $"Failed to add Git configuration entry '{name}'"); } } } @@ -242,10 +241,11 @@ public void Unset(string name) switch (git.ExitCode) { case 0: // OK + case 5: // Trying to unset a value that does not exist break; default: - throw new Exception( - $"Failed to unset Git configuration entry '{name}'. Exit code '{git.ExitCode}' (level={_filterLevel})"); + _trace.WriteLine($"Failed to unset config entry '{name}' (exit={git.ExitCode}, level={_filterLevel})"); + throw CreateGitException(git, $"Failed to unset Git configuration entry '{name}'"); } } } @@ -270,8 +270,8 @@ public IEnumerable GetAll(string name) case 1: // No results break; default: - throw new Exception( - $"Failed to get Git configuration multi-valued entry '{name}'. Exit code '{git.ExitCode}' (level={_filterLevel})"); + _trace.WriteLine($"Failed to get all config entries '{name}' (exit={git.ExitCode}, level={_filterLevel})"); + throw CreateGitException(git, $"Failed to get all Git configuration entries '{name}'"); } string[] entries = data.Split('\0'); @@ -306,8 +306,8 @@ public IEnumerable GetRegex(string nameRegex, string valueRegex) case 1: // No results break; default: - throw new Exception( - $"Failed to get Git configuration multi-valued entry '{nameRegex}' with value regex '{valueRegex}'. Exit code '{git.ExitCode}' (level={_filterLevel})"); + _trace.WriteLine($"Failed to get all multivar regex '{nameRegex}' and value regex '{valueRegex}' (exit={git.ExitCode}, level={_filterLevel})"); + throw CreateGitException(git, $"Failed to get Git configuration multi-valued entries with name regex '{nameRegex}'"); } string[] entries = data.Split('\0'); @@ -346,8 +346,8 @@ public void ReplaceAll(string name, string valueRegex, string value) case 0: // OK break; default: - throw new Exception( - $"Failed to set Git configuration multi-valued entry '{name}' with value regex '{valueRegex}' to value '{value}'. Exit code '{git.ExitCode}' (level={_filterLevel})"); + _trace.WriteLine($"Failed to replace all multivar '{name}' and value regex '{valueRegex}' with new value '{value}' (exit={git.ExitCode}, level={_filterLevel})"); + throw CreateGitException(git, $"Failed to replace all Git configuration multi-valued entries '{name}'"); } } } @@ -377,12 +377,29 @@ public void UnsetAll(string name, string valueRegex) case 5: // Trying to unset a value that does not exist break; default: - throw new Exception( - $"Failed to unset all Git configuration multi-valued entries '{name}' with value regex '{valueRegex}'. Exit code '{git.ExitCode}' (level={_filterLevel})"); + _trace.WriteLine($"Failed to unset all multivar '{name}' with value regex '{valueRegex}' (exit={git.ExitCode}, level={_filterLevel})"); + throw CreateGitException(git, $"Failed to unset all Git configuration multi-valued entries '{name}'"); } } } + private Exception CreateGitException(Process git, string message) + { + var exceptionMessage = new StringBuilder(); + string gitMessage = git.StandardError.ReadToEnd(); + + if (!string.IsNullOrWhiteSpace(gitMessage)) + { + exceptionMessage.AppendLine(gitMessage); + } + + exceptionMessage.AppendLine(message); + exceptionMessage.AppendLine($"Exit code: '{git.ExitCode}'"); + exceptionMessage.AppendLine($"Configuration level: {_filterLevel}"); + + throw new Exception(exceptionMessage.ToString()); + } + private string GetLevelFilterArg() { switch (_filterLevel) From c552709f311c731dfc31708a0920032d2c4e4f27 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Fri, 30 Oct 2020 10:35:15 +0000 Subject: [PATCH 22/25] azrepos-cfg: only clear useHttpPath on Windows if no manager-core Only clear the useHttpPath=true option on calls to unconfigure if the "manager-core" option is not present and we're in the system config on a Windows platform. This would be the case for the bundled GCM Core in Git for Windows, which we would break by removing this option. --- .../AzureReposHostProviderTests.cs | 47 ++++++++++++++++++- .../AzureReposHostProvider.cs | 11 ++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs b/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs index 1ff37732f..a624a9708 100644 --- a/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs +++ b/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Microsoft.Git.CredentialManager; using Microsoft.Git.CredentialManager.Authentication; +using Microsoft.Git.CredentialManager.Tests; using Microsoft.Git.CredentialManager.Tests.Objects; using Moq; using Xunit; @@ -14,6 +15,8 @@ namespace Microsoft.AzureRepos.Tests { public class AzureReposHostProviderTests { + private static readonly string HelperKey = + $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; private static readonly string AzDevUseHttpPathKey = $"{Constants.GitConfiguration.Credential.SectionName}.https://dev.azure.com.{Constants.GitConfiguration.Credential.UseHttpPath}"; @@ -222,7 +225,6 @@ public async Task AzureReposHostProvider_ConfigureAsync_UseHttpPathUnset_SetsUse Assert.Equal("true", actualValues[0]); } - [Fact] public async Task AzureReposHostProvider_UnconfigureAsync_UseHttpPathSet_RemovesEntry() { @@ -235,5 +237,48 @@ public async Task AzureReposHostProvider_UnconfigureAsync_UseHttpPathSet_Removes Assert.Empty(context.Git.GlobalConfiguration.Dictionary); } + + [PlatformFact(Platforms.Windows)] + public async Task AzureReposHostProvider_UnconfigureAsync_System_Windows_UseHttpPathSetAndManagerCoreHelper_DoesNotRemoveEntry() + { + var context = new TestCommandContext(); + var provider = new AzureReposHostProvider(context); + + context.Git.SystemConfiguration.Dictionary[HelperKey] = new List {"manager-core"}; + context.Git.SystemConfiguration.Dictionary[AzDevUseHttpPathKey] = new List {"true"}; + + await provider.UnconfigureAsync(ConfigurationTarget.System); + + Assert.True(context.Git.SystemConfiguration.Dictionary.TryGetValue(AzDevUseHttpPathKey, out IList actualValues)); + Assert.Single(actualValues); + Assert.Equal("true", actualValues[0]); + } + + [PlatformFact(Platforms.Windows)] + public async Task AzureReposHostProvider_UnconfigureAsync_System_Windows_UseHttpPathSetNoManagerCoreHelper_RemovesEntry() + { + var context = new TestCommandContext(); + var provider = new AzureReposHostProvider(context); + + context.Git.SystemConfiguration.Dictionary[AzDevUseHttpPathKey] = new List {"true"}; + + await provider.UnconfigureAsync(ConfigurationTarget.System); + + Assert.Empty(context.Git.SystemConfiguration.Dictionary); + } + + [PlatformFact(Platforms.Windows)] + public async Task AzureReposHostProvider_UnconfigureAsync_User_Windows_UseHttpPathSetAndManagerCoreHelper_RemovesEntry() + { + var context = new TestCommandContext(); + var provider = new AzureReposHostProvider(context); + + context.Git.GlobalConfiguration.Dictionary[HelperKey] = new List {"manager-core"}; + context.Git.GlobalConfiguration.Dictionary[AzDevUseHttpPathKey] = new List {"true"}; + + await provider.UnconfigureAsync(ConfigurationTarget.User); + + Assert.False(context.Git.GlobalConfiguration.Dictionary.TryGetValue(AzDevUseHttpPathKey, out _)); + } } } diff --git a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs index 0419feeed..7ee5ed020 100644 --- a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs +++ b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.Git.CredentialManager; using Microsoft.Git.CredentialManager.Authentication; @@ -275,6 +276,7 @@ public Task ConfigureAsync(ConfigurationTarget target) public Task UnconfigureAsync(ConfigurationTarget target) { + string helperKey = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}"; string useHttpPathKey = $"{KnownGitCfg.Credential.SectionName}.https://dev.azure.com.{KnownGitCfg.Credential.UseHttpPath}"; _context.Trace.WriteLine("Clearing Git configuration 'credential.useHttpPath' for https://dev.azure.com..."); @@ -284,7 +286,14 @@ public Task UnconfigureAsync(ConfigurationTarget target) : GitConfigurationLevel.Global; IGitConfiguration targetConfig = _context.Git.GetConfiguration(configurationLevel); - targetConfig.Unset(useHttpPathKey); + + // On Windows, if there is a "manager-core" entry remaining in the system config then we must not clear + // the useHttpPath option otherwise this would break the bundled version of GCM Core in Git for Windows. + if (!PlatformUtils.IsWindows() || target != ConfigurationTarget.System || + targetConfig.GetAll(helperKey).All(x => !string.Equals(x, "manager-core"))) + { + targetConfig.Unset(useHttpPathKey); + } return Task.CompletedTask; } From ba8114691ae711a6fd33890aec06050f3b706579 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Fri, 30 Oct 2020 10:57:05 +0000 Subject: [PATCH 23/25] git: fix --get-all output parsing bug Since --null means that each config entry terminates with a null character ('\0') we are left with one extra entry in the array after splitting the string. This is NOT a real entry and we shouldn't return it from the method. --- .../GitConfiguration.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs b/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs index ed7e60888..0915b1455 100644 --- a/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs +++ b/src/shared/Microsoft.Git.CredentialManager/GitConfiguration.cs @@ -267,18 +267,23 @@ public IEnumerable GetAll(string name) switch (git.ExitCode) { case 0: // OK + string[] entries = data.Split('\0'); + + // Because each line terminates with the \0 character, splitting leaves us with one + // bogus blank entry at the end of the array which we should ignore + for (var i = 0; i < entries.Length - 1; i++) + { + yield return entries[i]; + } + break; + case 1: // No results break; + default: _trace.WriteLine($"Failed to get all config entries '{name}' (exit={git.ExitCode}, level={_filterLevel})"); throw CreateGitException(git, $"Failed to get all Git configuration entries '{name}'"); } - - string[] entries = data.Split('\0'); - foreach (string entry in entries) - { - yield return entry; - } } } From e483a98ac72d666e40aa28285375e6ad814b7858 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Fri, 30 Oct 2020 10:13:49 -0400 Subject: [PATCH 24/25] Actions: use workflow_dispatch for manually running workflows See [1] for more information. [1] https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/ Signed-off-by: Derrick Stolee --- .github/workflows/build-installers.yml | 1 + .github/workflows/build-signed-deb.yml | 1 + .github/workflows/continuous-integration.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/build-installers.yml b/.github/workflows/build-installers.yml index c202fbf64..71336f45a 100644 --- a/.github/workflows/build-installers.yml +++ b/.github/workflows/build-installers.yml @@ -1,6 +1,7 @@ name: Build-Installers on: + workflow_dispatch: push: branches: [ master, release ] pull_request: diff --git a/.github/workflows/build-signed-deb.yml b/.github/workflows/build-signed-deb.yml index e0d309b0c..3373c072b 100644 --- a/.github/workflows/build-signed-deb.yml +++ b/.github/workflows/build-signed-deb.yml @@ -1,6 +1,7 @@ name: "Build Signed Debian Installer" on: + workflow_dispatch: release: types: [released] diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 7cec30fc9..bffee0884 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -1,6 +1,7 @@ name: GCM-Core on: + workflow_dispatch: push: branches: [ master, linux ] pull_request: From 67e3189635abfce598fad861544dd6fdec6c938f Mon Sep 17 00:00:00 2001 From: mastercoms Date: Sun, 1 Nov 2020 12:55:56 -0500 Subject: [PATCH 25/25] fix missing space in username input this caused the preceding argument to fail due to lack of separation between args --- src/shared/GitHub/GitHubAuthentication.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/GitHub/GitHubAuthentication.cs b/src/shared/GitHub/GitHubAuthentication.cs index 284adc671..fb45b1ec1 100644 --- a/src/shared/GitHub/GitHubAuthentication.cs +++ b/src/shared/GitHub/GitHubAuthentication.cs @@ -72,7 +72,7 @@ public async Task GetAuthenticationAsync(Uri targetU if ((modes & AuthenticationModes.Basic) != 0) promptArgs.Append(" --basic"); if ((modes & AuthenticationModes.OAuth) != 0) promptArgs.Append(" --oauth"); if (!GitHubHostProvider.IsGitHubDotCom(targetUri)) promptArgs.AppendFormat(" --enterprise-url {0}", targetUri); - if (!string.IsNullOrWhiteSpace(userName)) promptArgs.AppendFormat("--username {0}", userName); + if (!string.IsNullOrWhiteSpace(userName)) promptArgs.AppendFormat(" --username {0}", userName); IDictionary resultDict = await InvokeHelperAsync(helperPath, promptArgs.ToString(), null);