From f600e92b42b83984d92e9662c9e513beefb51ad4 Mon Sep 17 00:00:00 2001 From: M Hickford Date: Fri, 23 Feb 2024 22:29:47 +0000 Subject: [PATCH 01/31] Omit GitLab client secret Secret is superfluous for GitLab PKCE https://docs.gitlab.com/ee/api/oauth2.html#authorization-code-with-proof-key-for-code-exchange-pkce --- src/shared/GitLab/GitLabConstants.cs | 1 - src/shared/GitLab/GitLabOAuth2Client.cs | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/GitLab/GitLabConstants.cs b/src/shared/GitLab/GitLabConstants.cs index a686ece7a..69f1f9b9e 100644 --- a/src/shared/GitLab/GitLabConstants.cs +++ b/src/shared/GitLab/GitLabConstants.cs @@ -10,7 +10,6 @@ public static class GitLabConstants // Owned by https://gitlab.com/gitcredentialmanager public const string OAuthClientId = "172b9f227872b5dde33f4d9b1db06a6a5515ae79508e7a00c973c85ce490671e"; - public const string OAuthClientSecret = "7da92770d1447508601e4ba026bc5eb655c8268e818cd609889cc9bae2023f39"; public static readonly Uri OAuthRedirectUri = new Uri("http://127.0.0.1/"); // https://docs.gitlab.com/ee/api/oauth2.html#authorization-code-flow diff --git a/src/shared/GitLab/GitLabOAuth2Client.cs b/src/shared/GitLab/GitLabOAuth2Client.cs index 3b146aaeb..ba72f5b41 100644 --- a/src/shared/GitLab/GitLabOAuth2Client.cs +++ b/src/shared/GitLab/GitLabOAuth2Client.cs @@ -59,7 +59,8 @@ private static string GetClientSecret(ISettings settings) return clientSecret; } - return GitLabConstants.OAuthClientSecret; + // no secret necessary + return null; } } } From 5d98ecef1ddac612219f2e267f9f15ceaa121054 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 20:53:57 +0000 Subject: [PATCH 02/31] build(deps): bump DavidAnson/markdownlint-cli2-action Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 16.0.0 to 17.0.0. - [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases) - [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/b4c9feab76d8025d1e83c653fa3990936df0e6c8...db43aef879112c3119a410d69f66701e0d530809) --- updated-dependencies: - dependency-name: DavidAnson/markdownlint-cli2-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/lint-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-docs.yml b/.github/workflows/lint-docs.yml index 4cf4c4b70..17835c897 100644 --- a/.github/workflows/lint-docs.yml +++ b/.github/workflows/lint-docs.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: DavidAnson/markdownlint-cli2-action@b4c9feab76d8025d1e83c653fa3990936df0e6c8 + - uses: DavidAnson/markdownlint-cli2-action@db43aef879112c3119a410d69f66701e0d530809 with: globs: | "**/*.md" From bc4dfa90f4d7298ddcb1d394b7d0ceb3a4cb3892 Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Mon, 30 Sep 2024 19:06:55 +0100 Subject: [PATCH 03/31] docs: update required dotnet-sdk version in install.md Update the version notation because since the release of git-credential-manager version 2.5.0, the sdk version required for installation is .NET 8. --- docs/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.md b/docs/install.md index 4268858bb..5ae7b44d5 100644 --- a/docs/install.md +++ b/docs/install.md @@ -210,7 +210,7 @@ the preferred install method for Linux because you can use it to install on any distribution][dotnet-supported-distributions]. You can also use this method on macOS if you so choose. -**Note:** Make sure you have installed [version 7.0 of the .NET +**Note:** Make sure you have installed [version 8.0 of the .NET SDK][dotnet-install] before attempting to run the following `dotnet tool` commands. After installing, you will also need to follow the output instructions to add the tools directory to your `PATH`. From 32d205b8ea623dfdb8d663a1ff8b2d5da3a8903b Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 7 Oct 2024 12:47:13 +0100 Subject: [PATCH 04/31] settings: add allow unsafe remotes option Add a new setting that allows users to express an explicit consent to using unsafe remote URLs (such as those using HTTP rather than HTTPS). --- docs/configuration.md | 19 ++++++++++++ docs/environment.md | 29 ++++++++++++++++++- docs/netconfig.md | 18 ++++++++++++ src/shared/Core/Constants.cs | 3 ++ src/shared/Core/Settings.cs | 11 +++++++ .../Objects/TestSettings.cs | 4 +++ 6 files changed, 83 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index a4fecf395..6a38098e3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -255,6 +255,24 @@ Defaults to false (use hardware acceleration where available). --- +### credential.allowUnsafeRemotes + +Allow transmitting credentials to unsafe remote URLs such as unencrypted HTTP +URLs. This setting is not recommended for general use and should only be used +when necessary. + +Defaults false (disallow unsafe remote URLs). + +#### Example + +```shell +git config --global credential.allowUnsafeRemotes true +``` + +**Also see: [GCM_ALLOW_UNSAFE_REMOTES][gcm-allow-unsafe-remotes]** + +--- + ### credential.autoDetectTimeout Set the maximum length of time, in milliseconds, that GCM should wait for a @@ -1024,6 +1042,7 @@ Defaults to disabled. [envars]: environment.md [freedesktop-ss]: https://specifications.freedesktop.org/secret-service/ [gcm-allow-windowsauth]: environment.md#GCM_ALLOW_WINDOWSAUTH +[gcm-allow-unsafe-remotes]: environment.md#GCM_ALLOW_UNSAFE_REMOTES [gcm-authority]: environment.md#GCM_AUTHORITY-deprecated [gcm-autodetect-timeout]: environment.md#GCM_AUTODETECT_TIMEOUT [gcm-azrepos-credentialtype]: environment.md#GCM_AZREPOS_CREDENTIALTYPE diff --git a/docs/environment.md b/docs/environment.md index edda0d714..293a86ae0 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -302,6 +302,32 @@ Defaults to false (use hardware acceleration where available). --- +### GCM_ALLOW_UNSAFE_REMOTES + +Allow transmitting credentials to unsafe remote URLs such as unencrypted HTTP +URLs. This setting is not recommended for general use and should only be used +when necessary. + +Defaults false (disallow unsafe remote URLs). + +#### Example + +##### Windows + +```batch +SET GCM_ALLOW_UNSAFE_REMOTES=true +``` + +##### macOS/Linux + +```bash +export GCM_ALLOW_UNSAFE_REMOTES=true +``` + +**Also see: [credential.allowUnsafeRemotes][credential-allowunsaferemotes]** + +--- + ### GCM_AUTODETECT_TIMEOUT Set the maximum length of time, in milliseconds, that GCM should wait for a @@ -1153,7 +1179,8 @@ Defaults to disabled. [autodetect]: autodetect.md [azure-access-tokens]: azrepos-users-and-tokens.md [configuration]: configuration.md -[credential-allowwindowsauth]: environment.md#credentialallowWindowsAuth +[credential-allowwindowsauth]: configuration.md#credentialallowwindowsauth +[credential-allowunsaferemotes]: configuration.md#credentialallowunsaferemotes [credential-authority]: configuration.md#credentialauthority-deprecated [credential-autodetecttimeout]: configuration.md#credentialautodetecttimeout [credential-azrepos-credential-type]: configuration.md#credentialazreposcredentialtype diff --git a/docs/netconfig.md b/docs/netconfig.md index cf312336f..920344f15 100644 --- a/docs/netconfig.md +++ b/docs/netconfig.md @@ -191,6 +191,22 @@ network traffic inspection tool such as [Telerik Fiddler][telerik-fiddler]. If you are using such tools please consult their documentation for trusting the proxy root certificates. +--- + +## Unsafe Remote URLs + +If you are using a remote URL that is not considered safe, such as unencrypted +HTTP (remote URLs that start with `http://`), host providers may prevent you +from authenticating with your credentials. + +In this case, you should consider using a HTTPS (starting with `https://`) +remote URL to ensure your credentials are transmitted securely. + +If you accept the risks associated with using an unsafe remote URL, you can +configure GCM to allow the use of unsafe remote URLS by setting the environment +variable [`GCM_ALLOW_UNSAFE_REMOTES`][unsafe-envar], or by using the Git +configuration option [`credential.allowUnsafeRemotes`][unsafe-config] to `true`. + [environment]: environment.md [configuration]: configuration.md [git-http-proxy]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy @@ -212,3 +228,5 @@ proxy root certificates. [git-ssl-no-verify]: https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_networking [git-http-ssl-verify]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslVerify [telerik-fiddler]: https://www.telerik.com/fiddler +[unsafe-envar]: environment.md#gcm_allow_unsafe_remotes +[unsafe-config]: configuration.md#credentialallowunsaferemotes diff --git a/src/shared/Core/Constants.cs b/src/shared/Core/Constants.cs index 210c991bc..41ccb990c 100644 --- a/src/shared/Core/Constants.cs +++ b/src/shared/Core/Constants.cs @@ -119,6 +119,7 @@ public static class EnvironmentVariables public const string OAuthDefaultUserName = "GCM_OAUTH_DEFAULT_USERNAME"; public const string GcmDevUseLegacyUiHelpers = "GCM_DEV_USELEGACYUIHELPERS"; public const string GcmGuiSoftwareRendering = "GCM_GUI_SOFTWARE_RENDERING"; + public const string GcmAllowUnsafeRemotes = "GCM_ALLOW_UNSAFE_REMOTES"; } public static class Http @@ -163,6 +164,7 @@ public static class Credential public const string MsAuthUseDefaultAccount = "msauthUseDefaultAccount"; public const string GuiSoftwareRendering = "guiSoftwareRendering"; public const string GpgPassStorePath = "gpgPassStorePath"; + public const string AllowUnsafeRemotes = "allowUnsafeRemotes"; public const string OAuthAuthenticationModes = "oauthAuthModes"; public const string OAuthClientId = "oauthClientId"; @@ -226,6 +228,7 @@ public static class HelpUrls public const string GcmAutoDetect = "https://aka.ms/gcm/autodetect"; public const string GcmDefaultAccount = "https://aka.ms/gcm/defaultaccount"; public const string GcmMultipleUsers = "https://aka.ms/gcm/multipleusers"; + public const string GcmUnsafeRemotes = "https://aka.ms/gcm/unsaferemotes"; } private static Version _gcmVersion; diff --git a/src/shared/Core/Settings.cs b/src/shared/Core/Settings.cs index 2aa71edf4..0e24ce9a3 100644 --- a/src/shared/Core/Settings.cs +++ b/src/shared/Core/Settings.cs @@ -189,6 +189,11 @@ public interface ISettings : IDisposable /// bool UseSoftwareRendering { get; } + /// + /// Permit the use of unsafe remotes URLs such as regular HTTP. + /// + bool AllowUnsafeRemotes { get; } + /// /// Get TRACE2 settings. /// @@ -580,6 +585,12 @@ public bool UseSoftwareRendering } } + public bool AllowUnsafeRemotes => + TryGetSetting(KnownEnvars.GcmAllowUnsafeRemotes, + KnownGitCfg.Credential.SectionName, + KnownGitCfg.Credential.AllowUnsafeRemotes, + out string str) && str.ToBooleanyOrDefault(false); + public Trace2Settings GetTrace2Settings() { var settings = new Trace2Settings(); diff --git a/src/shared/TestInfrastructure/Objects/TestSettings.cs b/src/shared/TestInfrastructure/Objects/TestSettings.cs index f14bf6cc9..3e67e39b0 100644 --- a/src/shared/TestInfrastructure/Objects/TestSettings.cs +++ b/src/shared/TestInfrastructure/Objects/TestSettings.cs @@ -53,6 +53,8 @@ public class TestSettings : ISettings public bool UseMsAuthDefaultAccount { get; set; } + public bool AllowUnsafeRemotes { get; set; } = false; + public Trace2Settings GetTrace2Settings() { return new Trace2Settings() @@ -189,6 +191,8 @@ ProxyConfiguration ISettings.GetProxyConfiguration() bool ISettings.UseSoftwareRendering => false; + bool ISettings.AllowUnsafeRemotes => AllowUnsafeRemotes; + #endregion #region IDisposable From 2fbe3d615d6fb8013e14e55a837a9c8e95742b16 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 7 Oct 2024 12:48:29 +0100 Subject: [PATCH 05/31] bitbucket: support GCM_ALLOW_UNSAFE_REMOTES option --- .../Atlassian.Bitbucket/BitbucketHostProvider.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs b/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs index 35472682c..286398de9 100644 --- a/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs +++ b/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs @@ -55,8 +55,8 @@ public bool IsSupported(InputArguments input) return false; } - // We do not support unencrypted HTTP communications to Bitbucket, - // but we report `true` here for HTTP so that we can show a helpful + // We do not recommend unencrypted HTTP communications to Bitbucket, but it is possible. + // Therefore, we report `true` here for HTTP so that we can show a helpful // error message for the user in `GetCredentialAsync`. return (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http") || StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "https")) && @@ -81,11 +81,14 @@ public bool IsSupported(HttpResponseMessage response) public async Task GetCredentialAsync(InputArguments input) { // We should not allow unencrypted communication and should inform the user - if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http") - && BitbucketHelper.IsBitbucketOrg(input)) + if (!_context.Settings.AllowUnsafeRemotes && + StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http") && + BitbucketHelper.IsBitbucketOrg(input)) { throw new Trace2Exception(_context.Trace2, - "Unencrypted HTTP is not supported for Bitbucket.org. Ensure the repository remote URL is using HTTPS."); + "Unencrypted HTTP is not recommended for Bitbucket.org. " + + "Ensure the repository remote URL is using HTTPS " + + $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes."); } var authModes = await GetSupportedAuthenticationModesAsync(input); From 6b87cc7f21b11da541e1b2c194ec06975d5d1265 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 7 Oct 2024 12:48:45 +0100 Subject: [PATCH 06/31] github: support GCM_ALLOW_UNSAFE_REMOTES option --- src/shared/GitHub/GitHubHostProvider.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/shared/GitHub/GitHubHostProvider.cs b/src/shared/GitHub/GitHubHostProvider.cs index 918e859a0..21d29f651 100644 --- a/src/shared/GitHub/GitHubHostProvider.cs +++ b/src/shared/GitHub/GitHubHostProvider.cs @@ -285,10 +285,13 @@ public virtual Task EraseCredentialAsync(InputArguments input) ThrowIfDisposed(); // We should not allow unencrypted communication and should inform the user - if (StringComparer.OrdinalIgnoreCase.Equals(remoteUri.Scheme, "http")) + if (!_context.Settings.AllowUnsafeRemotes && + StringComparer.OrdinalIgnoreCase.Equals(remoteUri.Scheme, "http")) { throw new Trace2Exception(_context.Trace2, - "Unencrypted HTTP is not supported for GitHub. Ensure the repository remote URL is using HTTPS."); + "Unencrypted HTTP is not recommended for GitHub. " + + "Ensure the repository remote URL is using HTTPS " + + $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes."); } string service = GetServiceName(remoteUri); From f2652f3bad072ce8bcdcdf3615068a9cf815aeaa Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 7 Oct 2024 12:49:00 +0100 Subject: [PATCH 07/31] gitlab: support GCM_ALLOW_UNSAFE_REMOTES option --- src/shared/GitLab/GitLabHostProvider.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/shared/GitLab/GitLabHostProvider.cs b/src/shared/GitLab/GitLabHostProvider.cs index eda6e2f0f..6cda3c0e1 100644 --- a/src/shared/GitLab/GitLabHostProvider.cs +++ b/src/shared/GitLab/GitLabHostProvider.cs @@ -95,10 +95,13 @@ public override async Task GenerateCredentialAsync(InputArguments i ThrowIfDisposed(); // We should not allow unencrypted communication and should inform the user - if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")) + if (!Context.Settings.AllowUnsafeRemotes && + StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")) { throw new Trace2Exception(Context.Trace2, - "Unencrypted HTTP is not supported for GitHub. Ensure the repository remote URL is using HTTPS."); + "Unencrypted HTTP is not recommended for GitLab. " + + "Ensure the repository remote URL is using HTTPS " + + $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes."); } Uri remoteUri = input.GetRemoteUri(); From 7a613f3c2b8064d973b8733411ee1262993cda39 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 7 Oct 2024 12:49:16 +0100 Subject: [PATCH 08/31] azrepos: support GCM_ALLOW_UNSAFE_REMOTES option --- .../AzureReposHostProvider.cs | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs index 1d5c649d0..525704886 100644 --- a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs +++ b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs @@ -59,7 +59,7 @@ public bool IsSupported(InputArguments input) return false; } - // We do not support unencrypted HTTP communications to Azure Repos, + // We do not recommend unencrypted HTTP communications to Azure Repos, // but we report `true` here for HTTP so that we can show a helpful // error message for the user in `CreateCredentialAsync`. return input.TryGetHostAndPort(out string hostName, out _) @@ -208,16 +208,22 @@ protected override void ReleaseManagedResources() base.ReleaseManagedResources(); } - private async Task GeneratePersonalAccessTokenAsync(InputArguments input) + private void ThrowIfUnsafeRemote(InputArguments input) { - ThrowIfDisposed(); - - // We should not allow unencrypted communication and should inform the user - if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")) + if (!_context.Settings.AllowUnsafeRemotes && + StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")) { throw new Trace2Exception(_context.Trace2, - "Unencrypted HTTP is not supported for Azure Repos. Ensure the repository remote URL is using HTTPS."); + "Unencrypted HTTP is not recommended for Azure Repos. " + + "Ensure the repository remote URL is using HTTPS " + + $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes."); } + } + + private async Task GeneratePersonalAccessTokenAsync(InputArguments input) + { + ThrowIfDisposed(); + ThrowIfUnsafeRemote(input); Uri remoteUserUri = input.GetRemoteUri(includeUser: true); Uri orgUri = UriHelpers.CreateOrganizationUri(remoteUserUri, out _); @@ -257,16 +263,11 @@ private async Task GeneratePersonalAccessTokenAsync(InputArguments private async Task GetAzureAccessTokenAsync(InputArguments input) { + ThrowIfUnsafeRemote(input); + Uri remoteWithUserUri = input.GetRemoteUri(includeUser: true); string userName = input.UserName; - // We should not allow unencrypted communication and should inform the user - if (StringComparer.OrdinalIgnoreCase.Equals(remoteWithUserUri.Scheme, "http")) - { - throw new Trace2Exception(_context.Trace2, - "Unencrypted HTTP is not supported for Azure Repos. Ensure the repository remote URL is using HTTPS."); - } - Uri orgUri = UriHelpers.CreateOrganizationUri(remoteWithUserUri, out string orgName); _context.Trace.WriteLine($"Determining Microsoft Authentication authority for Azure DevOps organization '{orgName}'..."); From fc067e8c00d95d1ac520d78744bb962829567552 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 7 Oct 2024 12:49:30 +0100 Subject: [PATCH 09/31] generic: support GCM_ALLOW_UNSAFE_REMOTES option Note that we only emit a warning for the generic host provider rather than failing-fast like the other providers do. This is because we never blocked HTTP remotes previously in the generic provider (which is often used for localhost, custom hosts, etc) and don't want to break any existing scenarios or scripts. The new option can be used to dismiss this warning message. --- src/shared/Core/GenericHostProvider.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/shared/Core/GenericHostProvider.cs b/src/shared/Core/GenericHostProvider.cs index 447e465d5..9f087ca5b 100644 --- a/src/shared/Core/GenericHostProvider.cs +++ b/src/shared/Core/GenericHostProvider.cs @@ -54,6 +54,17 @@ public override async Task GenerateCredentialAsync(InputArguments i { ThrowIfDisposed(); + // We only want to *warn* about HTTP remotes for the generic provider because it supports all protocols + // and, historically, we never blocked HTTP remotes in this provider. + // The user can always set the 'GCM_ALLOW_UNSAFE' setting to silence the warning. + if (!Context.Settings.AllowUnsafeRemotes && + StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")) + { + Context.Streams.Error.WriteLine( + "warning: use of unencrypted HTTP remote URLs is not recommended; " + + $"see {Constants.HelpUrls.GcmUnsafeRemotes} for more information."); + } + Uri uri = input.GetRemoteUri(); // Determine the if the host supports Windows Integration Authentication (WIA) or OAuth From 004b19e6046080b0191e74fd79ab6dfc9984f453 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 7 Oct 2024 14:51:51 +0200 Subject: [PATCH 10/31] docs: update Secret Service links The links to FreeDesktop's Secret Service specifications has changed, it would seem. Signed-off-by: Johannes Schindelin --- docs/configuration.md | 2 +- docs/credstores.md | 2 +- docs/environment.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index a4fecf395..27939e701 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1022,7 +1022,7 @@ Defaults to disabled. [devbox]: https://azure.microsoft.com/en-us/products/dev-box [enterprise-config]: enterprise-config.md [envars]: environment.md -[freedesktop-ss]: https://specifications.freedesktop.org/secret-service/ +[freedesktop-ss]: https://specifications.freedesktop.org/secret-service-spec/ [gcm-allow-windowsauth]: environment.md#GCM_ALLOW_WINDOWSAUTH [gcm-authority]: environment.md#GCM_AUTHORITY-deprecated [gcm-autodetect-timeout]: environment.md#GCM_AUTODETECT_TIMEOUT diff --git a/docs/credstores.md b/docs/credstores.md index 157eaf930..2964f5645 100644 --- a/docs/credstores.md +++ b/docs/credstores.md @@ -257,7 +257,7 @@ that you take with you and use full-disk encryption. [cmdkey]: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/cmdkey [credential-store]: configuration.md#credentialcredentialstore [credential-cache]: https://git-scm.com/docs/git-credential-cache -[freedesktop-secret-service]: https://specifications.freedesktop.org/secret-service/ +[freedesktop-secret-service]: https://specifications.freedesktop.org/secret-service-spec/ [gcm-credential-store]: environment.md#GCM_CREDENTIAL_STORE [git-credential-store]: https://git-scm.com/docs/git-credential-store [mac-keychain-management]: https://support.apple.com/en-gb/guide/mac-help/mchlf375f392/mac diff --git a/docs/environment.md b/docs/environment.md index edda0d714..05c0ea8fd 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -1182,7 +1182,7 @@ Defaults to disabled. [credential-trace-msauth]: configuration.md#credentialtracemsauth [default-values]: enterprise-config.md [devbox]: https://azure.microsoft.com/en-us/products/dev-box -[freedesktop-ss]: https://specifications.freedesktop.org/secret-service/ +[freedesktop-ss]: https://specifications.freedesktop.org/secret-service-spec/ [gcm]: usage.md [gcm-interactive]: #gcm_interactive [gcm-credential-store]: #gcm_credential_store From 180a9e46f71792efea9898181c897fc151460f2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 20:52:26 +0000 Subject: [PATCH 11/31] build(deps): bump lycheeverse/lychee-action from 1.9.3 to 2.0.0 Bumps [lycheeverse/lychee-action](https://github.com/lycheeverse/lychee-action) from 1.9.3 to 2.0.0. - [Release notes](https://github.com/lycheeverse/lychee-action/releases) - [Commits](https://github.com/lycheeverse/lychee-action/compare/c053181aa0c3d17606addfe97a9075a32723548a...7da8ec1fc4e01b5a12062ac6c589c10a4ce70d67) --- updated-dependencies: - dependency-name: lycheeverse/lychee-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/lint-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-docs.yml b/.github/workflows/lint-docs.yml index 4cf4c4b70..84bc2725b 100644 --- a/.github/workflows/lint-docs.yml +++ b/.github/workflows/lint-docs.yml @@ -35,7 +35,7 @@ jobs: - name: Run link checker # For any troubleshooting, see: # https://github.com/lycheeverse/lychee/blob/master/docs/TROUBLESHOOTING.md - uses: lycheeverse/lychee-action@c053181aa0c3d17606addfe97a9075a32723548a + uses: lycheeverse/lychee-action@7da8ec1fc4e01b5a12062ac6c589c10a4ce70d67 with: # user-agent: if a user agent is not specified, some websites (e.g. From 535ed76ec2eababbb92c8db6636dc28579150803 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 20:07:55 +0000 Subject: [PATCH 12/31] build(deps): bump lycheeverse/lychee-action from 2.0.0 to 2.0.2 Bumps [lycheeverse/lychee-action](https://github.com/lycheeverse/lychee-action) from 2.0.0 to 2.0.2. - [Release notes](https://github.com/lycheeverse/lychee-action/releases) - [Commits](https://github.com/lycheeverse/lychee-action/compare/7da8ec1fc4e01b5a12062ac6c589c10a4ce70d67...7cd0af4c74a61395d455af97419279d86aafaede) --- updated-dependencies: - dependency-name: lycheeverse/lychee-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/lint-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-docs.yml b/.github/workflows/lint-docs.yml index 84bc2725b..3bbee52e8 100644 --- a/.github/workflows/lint-docs.yml +++ b/.github/workflows/lint-docs.yml @@ -35,7 +35,7 @@ jobs: - name: Run link checker # For any troubleshooting, see: # https://github.com/lycheeverse/lychee/blob/master/docs/TROUBLESHOOTING.md - uses: lycheeverse/lychee-action@7da8ec1fc4e01b5a12062ac6c589c10a4ce70d67 + uses: lycheeverse/lychee-action@7cd0af4c74a61395d455af97419279d86aafaede with: # user-agent: if a user agent is not specified, some websites (e.g. From a96afbb254675544a1f60cf7a6865b540785deac Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Tue, 15 Oct 2024 12:10:42 +0100 Subject: [PATCH 13/31] credstore: add no-op credential storage option Add a null/no-op credential store option that, as the name suggests, does nothing. This can be useful if the user wants to use another credential helper, configured in-front of GCM via Git, to store credentials. Example config: ```ini [credential] credentialStore = none helper = /bin/my-awesome-helper helper = /usr/local/bin/git-credential-manager ``` In this example, the `my-awesome-helper` will be consulted first to retrieve existing credentials before GCM, and will be asked to store any credentials generated by GCM. --- docs/configuration.md | 1 + docs/credstores.md | 26 ++++++++++++++++++++++++++ docs/environment.md | 1 + src/shared/Core/Constants.cs | 1 + src/shared/Core/CredentialStore.cs | 7 +++++++ src/shared/Core/NullCredentialStore.cs | 19 +++++++++++++++++++ 6 files changed, 55 insertions(+) create mode 100644 src/shared/Core/NullCredentialStore.cs diff --git a/docs/configuration.md b/docs/configuration.md index f843b839a..ba978ef30 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -585,6 +585,7 @@ _(unset)_|Windows: `wincredman`, macOS: `keychain`, Linux: _(none)_|- `gpg`|Use GPG to store encrypted files that are compatible with the [pass][pass] (requires GPG and `pass` to initialize the store).|macOS, Linux `cache`|Git's built-in [credential cache][credential-cache].|macOS, Linux `plaintext`|Store credentials in plaintext files (**UNSECURE**). Customize the plaintext store location with [`credential.plaintextStorePath`][credential-plaintextstorepath].|Windows, macOS, Linux +`none`|Do not store credentials via GCM.|Windows, macOS, Linux #### Example diff --git a/docs/credstores.md b/docs/credstores.md index 2964f5645..43811dc30 100644 --- a/docs/credstores.md +++ b/docs/credstores.md @@ -9,6 +9,7 @@ There are several options for storing credentials that GCM supports: - GPG/[`pass`][passwordstore] compatible files - Git's built-in [credential cache][credential-cache] - Plaintext files +- Passthrough/no-op (no credential store) The default credential stores on macOS and Windows are the macOS Keychain and the Windows Credential Manager, respectively. @@ -251,6 +252,31 @@ permissions on this directory such that no other users or applications can access files within. If possible, use a path that exists on an external volume that you take with you and use full-disk encryption. +## Passthrough/no-op (no credential store) + +**Available on:** _Windows, macOS, Linux_ + +**:warning: .** + +```batch +SET GCM_CREDENTIAL_STORE="none" +``` + +or + +```shell +git config --global credential.credentialStore none +``` + +This option disables the internal credential store. All operations to store or +retrieve credentials will do nothing, and will return success. This is useful if +you want to use a different credential store, chained in sequence via Git +configuration, and don't want GCM to store credentials. + +Note that you'll want to ensure that another credential helper is placed before +GCM in the `credential.helper` Git configuration or else you will be prompted to +enter your credentials every time you interact with a remote repository. + [access-windows-credential-manager]: https://support.microsoft.com/en-us/windows/accessing-credential-manager-1b5c916a-6a16-889f-8581-fc16e8165ac0 [aws-cloudshell]: https://aws.amazon.com/cloudshell/ [azure-cloudshell]: https://docs.microsoft.com/azure/cloud-shell/overview diff --git a/docs/environment.md b/docs/environment.md index 1973132ea..f321caa6c 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -716,6 +716,7 @@ _(unset)_|Windows: `wincredman`, macOS: `keychain`, Linux: _(none)_|- `gpg`|Use GPG to store encrypted files that are compatible with the [`pass` utility][passwordstore] (requires GPG and `pass` to initialize the store).|macOS, Linux `cache`|Git's built-in [credential cache][git-credential-cache].|Windows, macOS, Linux `plaintext`|Store credentials in plaintext files (**UNSECURE**). Customize the plaintext store location with [`GCM_PLAINTEXT_STORE_PATH`][gcm-plaintext-store-path].|Windows, macOS, Linux +`none`|Do not store credentials via GCM.|Windows, macOS, Linux #### Windows diff --git a/src/shared/Core/Constants.cs b/src/shared/Core/Constants.cs index 41ccb990c..191fcc83d 100644 --- a/src/shared/Core/Constants.cs +++ b/src/shared/Core/Constants.cs @@ -38,6 +38,7 @@ public static class CredentialStoreNames public const string SecretService = "secretservice"; public const string Plaintext = "plaintext"; public const string Cache = "cache"; + public const string None = "none"; } public static class RegexPatterns diff --git a/src/shared/Core/CredentialStore.cs b/src/shared/Core/CredentialStore.cs index 83f915d1e..11dc83818 100644 --- a/src/shared/Core/CredentialStore.cs +++ b/src/shared/Core/CredentialStore.cs @@ -100,6 +100,10 @@ private void EnsureBackingStore() _backingStore = new PlaintextCredentialStore(_context.FileSystem, plainStoreRoot, ns); break; + case StoreNames.None: + _backingStore = new NullCredentialStore(); + break; + default: var sb = new StringBuilder(); sb.AppendLine(string.IsNullOrWhiteSpace(credStoreName) @@ -168,6 +172,9 @@ private static void AppendAvailableStoreList(StringBuilder sb) sb.AppendFormat(" {1,-13} : store credentials in plain-text files (UNSECURE){0}", Environment.NewLine, StoreNames.Plaintext); + + sb.AppendFormat(" {1, -13} : disable internal credential storage{0}", + Environment.NewLine, StoreNames.None); } private void ValidateWindowsCredentialManager() diff --git a/src/shared/Core/NullCredentialStore.cs b/src/shared/Core/NullCredentialStore.cs new file mode 100644 index 000000000..fac92f47c --- /dev/null +++ b/src/shared/Core/NullCredentialStore.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace GitCredentialManager; + +/// +/// Credential store that does nothing. This is useful when you want to disable internal credential storage +/// and only use another helper configured in Git to store credentials. +/// +public class NullCredentialStore : ICredentialStore +{ + public IList GetAccounts(string service) => Array.Empty(); + + public ICredential Get(string service, string account) => null; + + public void AddOrUpdate(string service, string account, string secret) { } + + public bool Remove(string service, string account) => false; +} From 178a7d0864733da357075f3a1419564b61a76cfd Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 21 Oct 2024 10:18:37 +0200 Subject: [PATCH 14/31] ci: move to "more official" tgagor/centos There used to be two separate images, `tgagor/centos` and `tgagor/centos-stream`, relating to the CentOS and the CentOS Stream distribution, respectively. However, CentOS ceased to exist, and CentOS Stream is the only remaining actively-maintained project of the two. As per https://hub.docker.com/r/tgagor/centos-stream: Moved to new repo I created new repo for both stream and non stream, variants. I push some images here, but it's better to switch to: https://hub.docker.com/r/tgagor/centos Essentially, the CentOS Stream images are now available as `tgagor/centos`. So let's drop the `tgagor/centos-stream` one. Signed-off-by: Johannes Schindelin --- .github/workflows/validate-install-from-source.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/validate-install-from-source.yml b/.github/workflows/validate-install-from-source.yml index ca57d2daf..4b67b00ae 100644 --- a/.github/workflows/validate-install-from-source.yml +++ b/.github/workflows/validate-install-from-source.yml @@ -21,7 +21,6 @@ jobs: # tgagor is a contributor who pushes updated images weekly, which should # be sufficient for our validation needs. - image: tgagor/centos - - image: tgagor/centos-stream - image: redhat/ubi8 - image: alpine - image: alpine:3.14.10 From 2dece79f0bf3c4d16da7b30833bd21fd271bee03 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 21 Oct 2024 10:48:02 +0200 Subject: [PATCH 15/31] install-from-source: avoid using `which` before it is installed The `which` executable must often be installed because it is missing from many a Docker image. Therefore, it won't _really_ work if one checks `which which` to figure out whether `which` is installed. Let's avoid this by using `type`, which is a shell builtin for most shells. The `type` utility is specified in the POSIX standard, as per https://pubs.opengroup.org/onlinepubs/9699919799/utilities/type.html, yet neither command-line options nor output is standardized. The only thing we _can_ rely on is the exit status. Note: _Technically_, this poses a change of behavior, as `which` resolves only to executables that are on the `PATH` while `type` will also happily report shell builtins. However, this is a net improvement: If running the script in, say, BusyBox, where many of the common utilities (including `which`!) are shell builtins, we would like to avoid forcefully installing the packages without need. Signed-off-by: Johannes Schindelin --- src/linux/Packaging.Linux/install-from-source.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linux/Packaging.Linux/install-from-source.sh b/src/linux/Packaging.Linux/install-from-source.sh index be6ea1579..40259eded 100755 --- a/src/linux/Packaging.Linux/install-from-source.sh +++ b/src/linux/Packaging.Linux/install-from-source.sh @@ -63,7 +63,7 @@ install_packages() { for package in $packages; do # Ensure we don't stomp on existing installations. - if [ ! -z $(which $package) ]; then + if type $package >/dev/null 2>&1; then continue fi From 89adecefada837d99c9c10a32be2a9c84a7fb9fc Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 21 Oct 2024 10:56:17 +0200 Subject: [PATCH 16/31] install-from-source(mariner): awk is required to make dotnet-install.sh work The dotnet-install.sh script expects `awk` to be present, which is not installed by default in Mariner Linux. Signed-off-by: Johannes Schindelin --- src/linux/Packaging.Linux/install-from-source.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linux/Packaging.Linux/install-from-source.sh b/src/linux/Packaging.Linux/install-from-source.sh index 40259eded..0126d1ddf 100755 --- a/src/linux/Packaging.Linux/install-from-source.sh +++ b/src/linux/Packaging.Linux/install-from-source.sh @@ -228,7 +228,7 @@ case "$distribution" in $sudo_cmd tdnf update -y # Install dotnet/GCM dependencies. - install_packages tdnf install "curl git krb5-libs libicu openssl-libs zlib findutils which bash" + install_packages tdnf install "curl git krb5-libs libicu openssl-libs zlib findutils which bash awk" ensure_dotnet_installed ;; From 7b721ea32fab8dadcebe0c988c74f9dc0a67742b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 21 Oct 2024 14:50:48 +0200 Subject: [PATCH 17/31] install-from-source(mariner): ensure that CA certificates are installed This seems to be necessary to avoid problems with the `curl` calls when `dotnet-install.sh` tries to download the `dotnet-sdk` TAR archive: dotnet-install: Attempting to download using aka.ms link https://dotnetcli.azureedge.net/dotnet/Sdk/8.0.403/dotnet-sdk-8.0.403-linux-x64.tar.gz curl: (60) SSL certificate problem: unable to get local issuer certificate More details here: https://curl.se/docs/sslcerts.html curl failed to verify the legitimacy of the server and therefore could not establish a secure connection to it. To learn more about this situation and how to fix it, please visit the web page mentioned above. Signed-off-by: Johannes Schindelin --- src/linux/Packaging.Linux/install-from-source.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linux/Packaging.Linux/install-from-source.sh b/src/linux/Packaging.Linux/install-from-source.sh index 0126d1ddf..8cf60251c 100755 --- a/src/linux/Packaging.Linux/install-from-source.sh +++ b/src/linux/Packaging.Linux/install-from-source.sh @@ -228,7 +228,7 @@ case "$distribution" in $sudo_cmd tdnf update -y # Install dotnet/GCM dependencies. - install_packages tdnf install "curl git krb5-libs libicu openssl-libs zlib findutils which bash awk" + install_packages tdnf install "curl ca-certificates git krb5-libs libicu openssl-libs zlib findutils which bash awk" ensure_dotnet_installed ;; From 41a26cf6dc4f5459835df6cdad0b6d6a43e81502 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 21 Oct 2024 10:25:14 +0200 Subject: [PATCH 18/31] ci: also verify that installation works on Mariner and Arch Linux These currently work, too, and we probably want to keep it that way. Signed-off-by: Johannes Schindelin --- .github/workflows/validate-install-from-source.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/validate-install-from-source.yml b/.github/workflows/validate-install-from-source.yml index 4b67b00ae..2b1fd7696 100644 --- a/.github/workflows/validate-install-from-source.yml +++ b/.github/workflows/validate-install-from-source.yml @@ -27,6 +27,8 @@ jobs: - image: opensuse/leap - image: opensuse/tumbleweed - image: registry.suse.com/suse/sle15:15.4.27.11.31 + - image: archlinux + - image: mcr.microsoft.com/cbl-mariner/base/core:2.0 container: ${{matrix.vector.image}} steps: - run: | @@ -34,6 +36,9 @@ jobs: zypper -n install tar gzip elif [[ ${{matrix.vector.image}} == *"centos"* ]]; then dnf install which -y + elif [[ ${{matrix.vector.image}} == *"mariner"* ]]; then + GNUPGHOME=/root/.gnupg tdnf update -y && + GNUPGHOME=/root/.gnupg tdnf install tar -y # needed for `actions/checkout` fi - uses: actions/checkout@v4 From 4431516e718e9a4448ec1d14002e2d45030f95e3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 20:38:55 +0000 Subject: [PATCH 19/31] build(deps): bump azure/trusted-signing-action from 0.4.0 to 0.5.0 Bumps [azure/trusted-signing-action](https://github.com/azure/trusted-signing-action) from 0.4.0 to 0.5.0. - [Release notes](https://github.com/azure/trusted-signing-action/releases) - [Commits](https://github.com/azure/trusted-signing-action/compare/v0.4.0...v0.5.0) --- updated-dependencies: - dependency-name: azure/trusted-signing-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9f578e109..50c08c793 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -177,7 +177,7 @@ jobs: subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Sign payload files with Azure Code Signing - uses: azure/trusted-signing-action@v0.4.0 + uses: azure/trusted-signing-action@v0.5.0 with: endpoint: https://wus2.codesigning.azure.net/ trusted-signing-account-name: git-fundamentals-signing @@ -204,7 +204,7 @@ jobs: -Destination $env:GITHUB_WORKSPACE\installers - name: Sign installers with Azure Code Signing - uses: azure/trusted-signing-action@v0.4.0 + uses: azure/trusted-signing-action@v0.5.0 with: endpoint: https://wus2.codesigning.azure.net/ trusted-signing-account-name: git-fundamentals-signing From ca7a0d6839ef5f247a293d9fbdca06d831e2c654 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 20:08:32 +0000 Subject: [PATCH 20/31] build(deps): bump actions/setup-dotnet from 4.0.1 to 4.1.0 Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 4.0.1 to 4.1.0. - [Release notes](https://github.com/actions/setup-dotnet/releases) - [Commits](https://github.com/actions/setup-dotnet/compare/v4.0.1...v4.1.0) --- updated-dependencies: - dependency-name: actions/setup-dotnet dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/continuous-integration.yml | 6 +++--- .github/workflows/release.yml | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 745027d8b..8ce40ad8b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -25,7 +25,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v4.0.1 + uses: actions/setup-dotnet@v4.1.0 with: dotnet-version: 8.0.x diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 27834c10e..610574f80 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v4.0.1 + uses: actions/setup-dotnet@v4.1.0 with: dotnet-version: 8.0.x @@ -59,7 +59,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v4.0.1 + uses: actions/setup-dotnet@v4.1.0 with: dotnet-version: 8.0.x @@ -100,7 +100,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v4.0.1 + uses: actions/setup-dotnet@v4.1.0 with: dotnet-version: 8.0.x diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9f578e109..58db506ca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up .NET - uses: actions/setup-dotnet@v4.0.1 + uses: actions/setup-dotnet@v4.1.0 with: dotnet-version: 8.0.x @@ -150,7 +150,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up .NET - uses: actions/setup-dotnet@v4.0.1 + uses: actions/setup-dotnet@v4.1.0 with: dotnet-version: 8.0.x @@ -190,7 +190,7 @@ jobs: # The Azure Code Signing action overrides the .NET version, so we reset it. - name: Set up .NET - uses: actions/setup-dotnet@v4.0.1 + uses: actions/setup-dotnet@v4.1.0 with: dotnet-version: 8.0.x @@ -236,7 +236,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up .NET - uses: actions/setup-dotnet@v4.0.1 + uses: actions/setup-dotnet@v4.1.0 with: dotnet-version: 8.0.x @@ -314,7 +314,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up .NET - uses: actions/setup-dotnet@v4.0.1 + uses: actions/setup-dotnet@v4.1.0 with: dotnet-version: 8.0.x @@ -387,7 +387,7 @@ jobs: path: signed - name: Set up .NET - uses: actions/setup-dotnet@v4.0.1 + uses: actions/setup-dotnet@v4.1.0 with: dotnet-version: 8.0.x @@ -491,7 +491,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up .NET - uses: actions/setup-dotnet@v4.0.1 + uses: actions/setup-dotnet@v4.1.0 with: dotnet-version: 8.0.x @@ -561,7 +561,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up .NET - uses: actions/setup-dotnet@v4.0.1 + uses: actions/setup-dotnet@v4.1.0 with: dotnet-version: 8.0.x From 557937a919dd358bb3db01ab361b2cf92f26bbc5 Mon Sep 17 00:00:00 2001 From: JaoSchmidt Date: Sun, 27 Oct 2024 22:41:57 -0300 Subject: [PATCH 21/31] fix wrong bash if-else syntax --- src/linux/Packaging.Linux/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linux/Packaging.Linux/build.sh b/src/linux/Packaging.Linux/build.sh index 6672857d2..9db7ad5e3 100755 --- a/src/linux/Packaging.Linux/build.sh +++ b/src/linux/Packaging.Linux/build.sh @@ -41,7 +41,7 @@ esac done # Ensure install prefix exists -if [! -d "$INSTALL_PREFIX" ]; then +if [ ! -d "$INSTALL_PREFIX" ]; then mkdir -p "$INSTALL_PREFIX" fi From 61e4fa4c328c232445618dd4ae7a0c5f31776fc5 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Tue, 29 Oct 2024 11:31:13 +0000 Subject: [PATCH 22/31] streams: only consider LF and CRLF as newlines Git only considers LF (`\n`) and CRLF (`\r\n`) as valid line endings for the Git credential protocol. Lone carriage-returns should be treated as part of the value, and not a delimiter in the credential protocol. Override the behaviour of the standard `StreamReader`'s `ReadLineAsync` method to only break on LF or CRLF, in alignment with Git. Note that we also override the non-async `ReadLine` method too as this is also implemented separatley in the base class from the async version. We must also make allowances for .NET Framework where the override of `ReadLineAsync` that takes a `CancellationToken` does not exist. --- src/shared/Core.Tests/GitStreamReaderTests.cs | 193 ++++++++++++++++++ src/shared/Core/GitStreamReader.cs | 70 +++++++ src/shared/Core/StandardStreams.cs | 2 +- 3 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 src/shared/Core.Tests/GitStreamReaderTests.cs create mode 100644 src/shared/Core/GitStreamReader.cs diff --git a/src/shared/Core.Tests/GitStreamReaderTests.cs b/src/shared/Core.Tests/GitStreamReaderTests.cs new file mode 100644 index 000000000..bf656d102 --- /dev/null +++ b/src/shared/Core.Tests/GitStreamReaderTests.cs @@ -0,0 +1,193 @@ +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace GitCredentialManager.Tests; + +public class GitStreamReaderTests +{ + #region ReadLineAsync + + [Fact] + public async Task GitStreamReader_ReadLineAsync_LF() + { + // hello\n + // world\n + + byte[] buffer = Encoding.UTF8.GetBytes("hello\nworld\n"); + using var stream = new MemoryStream(buffer); + var reader = new GitStreamReader(stream, Encoding.UTF8); + + string actual1 = await reader.ReadLineAsync(); + string actual2 = await reader.ReadLineAsync(); + string actual3 = await reader.ReadLineAsync(); + + Assert.Equal("hello", actual1); + Assert.Equal("world", actual2); + Assert.Null(actual3); + } + + [Fact] + public async Task GitStreamReader_ReadLineAsync_CR() + { + // hello\rworld\r + + byte[] buffer = Encoding.UTF8.GetBytes("hello\rworld\r"); + using var stream = new MemoryStream(buffer); + var reader = new GitStreamReader(stream, Encoding.UTF8); + + string actual1 = await reader.ReadLineAsync(); + string actual2 = await reader.ReadLineAsync(); + + Assert.Equal("hello\rworld\r", actual1); + Assert.Null(actual2); + } + + [Fact] + public async Task GitStreamReader_ReadLineAsync_CRLF() + { + // hello\r\n + // world\r\n + + byte[] buffer = Encoding.UTF8.GetBytes("hello\r\nworld\r\n"); + using var stream = new MemoryStream(buffer); + var reader = new GitStreamReader(stream, Encoding.UTF8); + + string actual1 = await reader.ReadLineAsync(); + string actual2 = await reader.ReadLineAsync(); + string actual3 = await reader.ReadLineAsync(); + + Assert.Equal("hello", actual1); + Assert.Equal("world", actual2); + Assert.Null(actual3); + } + + [Fact] + public async Task GitStreamReader_ReadLineAsync_Mixed() + { + // hello\r\n + // world\rthis\n + // is\n + // a\n + // \rmixed\rnewline\r\n + // \n + // string\n + + byte[] buffer = Encoding.UTF8.GetBytes("hello\r\nworld\rthis\nis\na\n\rmixed\rnewline\r\n\nstring\n"); + using var stream = new MemoryStream(buffer); + var reader = new GitStreamReader(stream, Encoding.UTF8); + + string actual1 = await reader.ReadLineAsync(); + string actual2 = await reader.ReadLineAsync(); + string actual3 = await reader.ReadLineAsync(); + string actual4 = await reader.ReadLineAsync(); + string actual5 = await reader.ReadLineAsync(); + string actual6 = await reader.ReadLineAsync(); + string actual7 = await reader.ReadLineAsync(); + string actual8 = await reader.ReadLineAsync(); + + Assert.Equal("hello", actual1); + Assert.Equal("world\rthis", actual2); + Assert.Equal("is", actual3); + Assert.Equal("a", actual4); + Assert.Equal("\rmixed\rnewline", actual5); + Assert.Equal("", actual6); + Assert.Equal("string", actual7); + Assert.Null(actual8); + } + + #endregion + + #region ReadLine + + [Fact] + public void GitStreamReader_ReadLine_LF() + { + // hello\n + // world\n + + byte[] buffer = Encoding.UTF8.GetBytes("hello\nworld\n"); + using var stream = new MemoryStream(buffer); + var reader = new GitStreamReader(stream, Encoding.UTF8); + + string actual1 = reader.ReadLine(); + string actual2 = reader.ReadLine(); + string actual3 = reader.ReadLine(); + + Assert.Equal("hello", actual1); + Assert.Equal("world", actual2); + Assert.Null(actual3); + } + + [Fact] + public void GitStreamReader_ReadLine_CR() + { + // hello\rworld\r + + byte[] buffer = Encoding.UTF8.GetBytes("hello\rworld\r"); + using var stream = new MemoryStream(buffer); + var reader = new GitStreamReader(stream, Encoding.UTF8); + + string actual1 = reader.ReadLine(); + string actual2 = reader.ReadLine(); + + Assert.Equal("hello\rworld\r", actual1); + Assert.Null(actual2); + } + + [Fact] + public void GitStreamReader_ReadLine_CRLF() + { + // hello\r\n + // world\r\n + + byte[] buffer = Encoding.UTF8.GetBytes("hello\r\nworld\r\n"); + using var stream = new MemoryStream(buffer); + var reader = new GitStreamReader(stream, Encoding.UTF8); + + string actual1 = reader.ReadLine(); + string actual2 = reader.ReadLine(); + string actual3 = reader.ReadLine(); + + Assert.Equal("hello", actual1); + Assert.Equal("world", actual2); + Assert.Null(actual3); + } + + [Fact] + public void GitStreamReader_ReadLine_Mixed() + { + // hello\r\n + // world\rthis\n + // is\n + // a\n + // \rmixed\rnewline\r\n + // \n + // string\n + + byte[] buffer = Encoding.UTF8.GetBytes("hello\r\nworld\rthis\nis\na\n\rmixed\rnewline\r\n\nstring\n"); + using var stream = new MemoryStream(buffer); + var reader = new GitStreamReader(stream, Encoding.UTF8); + + string actual1 = reader.ReadLine(); + string actual2 = reader.ReadLine(); + string actual3 = reader.ReadLine(); + string actual4 = reader.ReadLine(); + string actual5 = reader.ReadLine(); + string actual6 = reader.ReadLine(); + string actual7 = reader.ReadLine(); + string actual8 = reader.ReadLine(); + + Assert.Equal("hello", actual1); + Assert.Equal("world\rthis", actual2); + Assert.Equal("is", actual3); + Assert.Equal("a", actual4); + Assert.Equal("\rmixed\rnewline", actual5); + Assert.Equal("", actual6); + Assert.Equal("string", actual7); + Assert.Null(actual8); + } + + #endregion +} diff --git a/src/shared/Core/GitStreamReader.cs b/src/shared/Core/GitStreamReader.cs new file mode 100644 index 000000000..6512b2efc --- /dev/null +++ b/src/shared/Core/GitStreamReader.cs @@ -0,0 +1,70 @@ +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace GitCredentialManager; + +/// +/// StreamReader that does NOT consider a lone carriage-return as a new-line character, +/// only a line-feed or carriage-return immediately followed by a line-feed. +/// +/// The only major operating system that uses a lone carriage-return as a new-line character +/// is the classic Macintosh OS (before OS X), which is not supported by Git. +/// +public class GitStreamReader : StreamReader +{ + public GitStreamReader(Stream stream, Encoding encoding) : base(stream, encoding) { } + + public override string ReadLine() + { +#if NETFRAMEWORK + return ReadLineAsync().ConfigureAwait(false).GetAwaiter().GetResult(); +#else + return ReadLineAsync(CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); +#endif + } + +#if NETFRAMEWORK + public override async Task ReadLineAsync() +#else + public override async ValueTask ReadLineAsync(CancellationToken cancellationToken) +#endif + { + int nr; + var sb = new StringBuilder(); + var buffer = new char[1]; + bool lastWasCR = false; + + while ((nr = await base.ReadAsync(buffer, 0, 1).ConfigureAwait(false)) > 0) + { + char c = buffer[0]; + + // Only treat a line-feed as a new-line character. + // Carriage-returns alone are NOT considered new-line characters. + if (c == '\n') + { + if (lastWasCR) + { + // If the last character was a carriage-return we should remove it from the string builder + // since together with this line-feed it is considered a new-line character. + sb.Length--; + } + + // We have a new-line character, so we should stop reading. + break; + } + + lastWasCR = c == '\r'; + + sb.Append(c); + } + + if (sb.Length == 0 && nr == 0) + { + return null; + } + + return sb.ToString(); + } +} diff --git a/src/shared/Core/StandardStreams.cs b/src/shared/Core/StandardStreams.cs index d0b3042b0..45f9f6cc7 100644 --- a/src/shared/Core/StandardStreams.cs +++ b/src/shared/Core/StandardStreams.cs @@ -39,7 +39,7 @@ public TextReader In { if (_stdIn == null) { - _stdIn = new StreamReader(Console.OpenStandardInput(), EncodingEx.UTF8NoBom); + _stdIn = new GitStreamReader(Console.OpenStandardInput(), EncodingEx.UTF8NoBom); } return _stdIn; From 99e2f7f60e7364fe807e7925f361a81f3c47bd1b Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 30 Oct 2024 09:50:25 +0000 Subject: [PATCH 23/31] release.yml: use gatewatcher mac app certificate Use Gatewatcher to provision the Application certificate and password secrets in our release workflow. --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3a2e708ca..59e7c8bc1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,8 +57,8 @@ jobs: - name: Set up signing/notarization infrastructure env: - A1: ${{ secrets.APPLICATION_CERTIFICATE_BASE64 }} - A2: ${{ secrets.APPLICATION_CERTIFICATE_PASSWORD }} + A1: ${{ secrets.GATEWATCHER_DEVELOPER_ID_CERT }} + A2: ${{ secrets.GATEWATCHER_DEVELOPER_ID_PASSWORD }} I1: ${{ secrets.INSTALLER_CERTIFICATE_BASE64 }} I2: ${{ secrets.INSTALLER_CERTIFICATE_PASSWORD }} N1: ${{ secrets.APPLE_TEAM_ID }} From 786ab03440ddc82e807a97c0e540f5247e44cec6 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 30 Oct 2024 09:52:19 +0000 Subject: [PATCH 24/31] VERSION: bump to 2.6.1 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 82f00d533..cfad4122e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.6.0.0 +2.6.1.0 From 628acd0e500eee6506590ce1dc7f7b8f681cb54c Mon Sep 17 00:00:00 2001 From: theofficialgman <28281419+theofficialgman@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:38:31 -0400 Subject: [PATCH 25/31] add support for Linux arm64 and Linux arm Co-Authored-By: Matthew John Cheetham --- .github/workflows/continuous-integration.yml | 10 ++++- .github/workflows/release.yml | 38 ++++++++++++------- README.md | 4 +- docs/development.md | 6 +++ .../Packaging.Linux/Packaging.Linux.csproj | 4 +- src/linux/Packaging.Linux/build.sh | 32 ++++++++++++++-- src/linux/Packaging.Linux/layout.sh | 9 ++++- src/linux/Packaging.Linux/pack.sh | 33 ++++++++++++---- .../Git-Credential-Manager.csproj | 2 +- 9 files changed, 105 insertions(+), 33 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 1b83a990d..7f7c28a9a 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -54,6 +54,9 @@ jobs: linux: name: Linux runs-on: ubuntu-latest + strategy: + matrix: + runtime: [ linux-x64, linux-arm64, linux-arm ] steps: - uses: actions/checkout@v4 @@ -67,7 +70,10 @@ jobs: run: dotnet restore - name: Build - run: dotnet build --configuration LinuxRelease + run: | + dotnet build src/linux/Packaging.Linux/*.csproj \ + --configuration=Release --no-self-contained \ + --runtime=${{ matrix.runtime }} - name: Test run: | @@ -82,7 +88,7 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: linux-x64 + name: ${{ matrix.runtime }} path: | artifacts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 28858d7c1..b8a2560af 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -232,6 +232,9 @@ jobs: runs-on: ubuntu-latest environment: release needs: prereqs + strategy: + matrix: + runtime: [ linux-x64, linux-arm64, linux-arm ] steps: - uses: actions/checkout@v4 @@ -241,7 +244,10 @@ jobs: dotnet-version: 8.0.x - name: Build - run: dotnet build --configuration=LinuxRelease + run: | + dotnet build src/linux/Packaging.Linux/*.csproj \ + --configuration=LinuxRelease --no-self-contained \ + --runtime=${{ matrix.runtime }} - name: Run Linux unit tests run: | @@ -286,18 +292,18 @@ jobs: run: | # Sign Debian package version=${{ needs.prereqs.outputs.version }} - mv out/linux/Packaging.Linux/Release/deb/gcm-linux_amd64.$version.deb . - debsigs --sign=origin --verify --check gcm-linux_amd64.$version.deb + mv out/linux/Packaging.Linux/Release/deb/gcm-${{ matrix.runtime }}.$version.deb . + debsigs --sign=origin --verify --check gcm-${{ matrix.runtime }}.$version.deb # Generate tarball signature file mv -v out/linux/Packaging.Linux/Release/tar/* . - gpg --batch --yes --armor --output gcm-linux_amd64.$version.tar.gz.asc \ - --detach-sig gcm-linux_amd64.$version.tar.gz + gpg --batch --yes --armor --output gcm-${{ matrix.runtime }}.$version.tar.gz.asc \ + --detach-sig gcm-${{ matrix.runtime }}.$version.tar.gz - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: linux-artifacts + name: ${{ matrix.runtime }}-artifacts path: | ./*.deb ./*.asc @@ -486,9 +492,9 @@ jobs: matrix: component: - os: ubuntu-latest - artifact: linux-artifacts + artifact: linux-x64-artifacts command: git-credential-manager - description: linux + description: linux-x64 - os: macos-latest artifact: macos-osx-x64-artifacts command: git-credential-manager @@ -530,15 +536,15 @@ jobs: Start-Process -Wait -FilePath "$exePath" -ArgumentList "/SILENT /VERYSILENT /NORESTART" } - - name: Install Linux (Debian package) - if: contains(matrix.component.description, 'linux') + - name: Install Linux x64 (Debian package) + if: contains(matrix.component.description, 'linux-x64') run: | debpath=$(find ./*.deb) sudo apt install $debpath "${{ matrix.component.command }}" configure - - name: Install Linux (tarball) - if: contains(matrix.component.description, 'linux') + - name: Install Linux x64 (tarball) + if: contains(matrix.component.description, 'linux-x64') run: | # Ensure we find only the source tarball, not the symbols tarpath=$(find . -name '*[[:digit:]].tar.gz') @@ -618,7 +624,9 @@ jobs: az keyvault secret show --name "$GPG_PUBLIC_KEY_SECRET_NAME" \ --vault-name "$AZURE_VAULT" --query "value" \ | sed -e 's/^"//' -e 's/"$//' | base64 -d >gcm-public.asc - mv gcm-public.asc linux-artifacts + cp gcm-public.asc linux-x64-artifacts/ + cp gcm-public.asc linux-arm64-artifacts/ + mv gcm-public.asc linux-arm-artifacts - uses: actions/github-script@v7 with: @@ -675,7 +683,9 @@ jobs: uploadDirectoryToRelease('osx-payload-and-symbols'), // Upload Linux artifacts - uploadDirectoryToRelease('linux-artifacts'), + uploadDirectoryToRelease('linux-x64-artifacts'), + uploadDirectoryToRelease('linux-arm64-artifacts'), + uploadDirectoryToRelease('linux-arm-artifacts'), // Upload .NET tool package uploadDirectoryToRelease('dotnet-tool-sign'), diff --git a/README.md b/README.md index 18c9b1309..6c6aa1535 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,8 @@ Basic HTTP authentication support|✓|✓|✓ Proxy support|✓|✓|✓ `amd64` support|✓|✓|✓ `x86` support|✓|_N/A_|✗ -`arm64` support|best effort|✓|best effort, no packages -`armhf` support|_N/A_|_N/A_|best effort, no packages +`arm64` support|best effort|✓|✓ +`armhf` support|_N/A_|_N/A_|✓ (\*) GCM guarantees support only for [the Linux distributions that are officially supported by dotnet][dotnet-distributions]. diff --git a/docs/development.md b/docs/development.md index 7729556f9..0242d68b8 100644 --- a/docs/development.md +++ b/docs/development.md @@ -54,6 +54,12 @@ To build from the command line, run: dotnet build -c LinuxDebug ``` +If you want to build for a specific architecture, you can provide `linux-x64` or `linux-arm64` or `linux-arm` as the runtime: + +```shell +dotnet build -c LinuxDebug -r linux-arm64 +``` + 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`. diff --git a/src/linux/Packaging.Linux/Packaging.Linux.csproj b/src/linux/Packaging.Linux/Packaging.Linux.csproj index 8b9755c78..ddfb31500 100644 --- a/src/linux/Packaging.Linux/Packaging.Linux.csproj +++ b/src/linux/Packaging.Linux/Packaging.Linux.csproj @@ -24,8 +24,8 @@ - - + + diff --git a/src/linux/Packaging.Linux/build.sh b/src/linux/Packaging.Linux/build.sh index 6672857d2..88f1b0359 100755 --- a/src/linux/Packaging.Linux/build.sh +++ b/src/linux/Packaging.Linux/build.sh @@ -30,6 +30,10 @@ case "$i" in INSTALL_FROM_SOURCE="${i#*=}" shift # past argument=value ;; + --runtime=*) + RUNTIME="${i#*=}" + shift # past argument=value + ;; --install-prefix=*) INSTALL_PREFIX="${i#*=}" shift # past argument=value @@ -41,10 +45,32 @@ esac done # Ensure install prefix exists -if [! -d "$INSTALL_PREFIX" ]; then +if [ ! -d "$INSTALL_PREFIX" ]; then mkdir -p "$INSTALL_PREFIX" fi +# Fall back to host architecture if no explicit runtime is given. +if test -z "$RUNTIME"; then + HOST_ARCH="`dpkg-architecture -q DEB_HOST_ARCH`" + + case $HOST_ARCH in + amd64) + RUNTIME="linux-x64" + ;; + arm64) + RUNTIME="linux-arm64" + ;; + armhf) + RUNTIME="linux-arm" + ;; + *) + die "Could not determine host architecture!" + ;; + esac +fi + +echo "Building for runtime ${RUNTIME}" + # Perform pre-execution checks CONFIGURATION="${CONFIGURATION:=Debug}" if [ -z "$VERSION" ]; then @@ -56,7 +82,7 @@ PAYLOAD="$OUTDIR/payload" SYMBOLS="$OUTDIR/payload.sym" # Lay out payload -"$INSTALLER_SRC/layout.sh" --configuration="$CONFIGURATION" || exit 1 +"$INSTALLER_SRC/layout.sh" --configuration="$CONFIGURATION" --runtime="$RUNTIME" || exit 1 if [ $INSTALL_FROM_SOURCE = true ]; then echo "Installing to $INSTALL_PREFIX" @@ -79,7 +105,7 @@ if [ $INSTALL_FROM_SOURCE = true ]; then echo "Install complete." else # Pack - "$INSTALLER_SRC/pack.sh" --configuration="$CONFIGURATION" --payload="$PAYLOAD" --symbols="$SYMBOLS" --version="$VERSION" || exit 1 + "$INSTALLER_SRC/pack.sh" --configuration="$CONFIGURATION" --runtime="$RUNTIME" --payload="$PAYLOAD" --symbols="$SYMBOLS" --version="$VERSION" || exit 1 fi echo "Build of Packaging.Linux complete." diff --git a/src/linux/Packaging.Linux/layout.sh b/src/linux/Packaging.Linux/layout.sh index 6679c39ca..9355eaa02 100755 --- a/src/linux/Packaging.Linux/layout.sh +++ b/src/linux/Packaging.Linux/layout.sh @@ -23,6 +23,10 @@ case "$i" in CONFIGURATION="${i#*=}" shift # past argument=value ;; + --runtime=*) + RUNTIME="${i#*=}" + shift # past argument=value + ;; *) # unknown option ;; @@ -39,7 +43,10 @@ PROJ_OUT="$OUT/linux/Packaging.Linux" # Build parameters FRAMEWORK=net8.0 -RUNTIME=linux-x64 + +if [ -z "$RUNTIME" ]; then + die "--runtime was not set" +fi # Perform pre-execution checks CONFIGURATION="${CONFIGURATION:=Debug}" diff --git a/src/linux/Packaging.Linux/pack.sh b/src/linux/Packaging.Linux/pack.sh index 14d26aee5..817704f76 100755 --- a/src/linux/Packaging.Linux/pack.sh +++ b/src/linux/Packaging.Linux/pack.sh @@ -28,6 +28,10 @@ case "$i" in SYMBOLS="${i#*=}" shift # past argument=value ;; + --runtime=*) + RUNTIME="${i#*=}" + shift # past argument=value + ;; --configuration=*) CONFIGURATION="${i#*=}" shift # past argument=value @@ -51,20 +55,17 @@ fi if [ -z "$SYMBOLS" ]; then die "--symbols was not set" fi - -ARCH="`dpkg-architecture -q DEB_HOST_ARCH`" - -if test -z "$ARCH"; then - die "Could not determine host architecture!" +if [ -z "$RUNTIME" ]; then + die "--runtime was not set" fi TAROUT="$PROJ_OUT/$CONFIGURATION/tar/" -TARBALL="$TAROUT/gcm-linux_$ARCH.$VERSION.tar.gz" -SYMTARBALL="$TAROUT/gcm-linux_$ARCH.$VERSION-symbols.tar.gz" +TARBALL="$TAROUT/gcm-$RUNTIME.$VERSION.tar.gz" +SYMTARBALL="$TAROUT/gcm-$RUNTIME.$VERSION-symbols.tar.gz" DEBOUT="$PROJ_OUT/$CONFIGURATION/deb" DEBROOT="$DEBOUT/root" -DEBPKG="$DEBOUT/gcm-linux_$ARCH.$VERSION.deb" +DEBPKG="$DEBOUT/gcm-$RUNTIME.$VERSION.deb" mkdir -p "$DEBROOT" # Set full read, write, execute permissions for owner and just read and execute permissions for group and other @@ -99,6 +100,22 @@ INSTALL_TO="$DEBROOT/usr/local/share/gcm-core/" LINK_TO="$DEBROOT/usr/local/bin/" mkdir -p "$DEBROOT/DEBIAN" "$INSTALL_TO" "$LINK_TO" || exit 1 +# Determine architecture for debian control file from the runtime architecture +case $RUNTIME in + linux-x64) + ARCH="amd64" + ;; + linux-arm64) + ARCH="arm64" + ;; + linux-arm) + ARCH="armhf" + ;; + *) + die "Incompatible runtime architecture given for pack.sh" + ;; +esac + # make the debian control file # this is purposefully not indented, see # https://stackoverflow.com/questions/9349616/bash-eof-in-if-statement diff --git a/src/shared/Git-Credential-Manager/Git-Credential-Manager.csproj b/src/shared/Git-Credential-Manager/Git-Credential-Manager.csproj index 2b594e3eb..456adf547 100644 --- a/src/shared/Git-Credential-Manager/Git-Credential-Manager.csproj +++ b/src/shared/Git-Credential-Manager/Git-Credential-Manager.csproj @@ -4,7 +4,7 @@ Exe net8.0 net472;net8.0 - win-x86;osx-x64;linux-x64;osx-arm64 + win-x86;osx-x64;linux-x64;osx-arm64;linux-arm64;linux-arm x86 git-credential-manager GitCredentialManager From 5cd01b6e7aeccc05afdc17eae2104e8cf7a2c67b Mon Sep 17 00:00:00 2001 From: theofficialgman <28281419+theofficialgman@users.noreply.github.com> Date: Wed, 6 Nov 2024 10:17:18 -0500 Subject: [PATCH 26/31] don't require runtime to be set to install from source --- src/linux/Packaging.Linux/build.sh | 22 ++-------------------- src/linux/Packaging.Linux/layout.sh | 27 ++++++++++++++++----------- src/linux/Packaging.Linux/pack.sh | 20 ++++++++++++++++++++ 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/src/linux/Packaging.Linux/build.sh b/src/linux/Packaging.Linux/build.sh index 88f1b0359..62352a7e8 100755 --- a/src/linux/Packaging.Linux/build.sh +++ b/src/linux/Packaging.Linux/build.sh @@ -49,28 +49,10 @@ if [ ! -d "$INSTALL_PREFIX" ]; then mkdir -p "$INSTALL_PREFIX" fi -# Fall back to host architecture if no explicit runtime is given. -if test -z "$RUNTIME"; then - HOST_ARCH="`dpkg-architecture -q DEB_HOST_ARCH`" - - case $HOST_ARCH in - amd64) - RUNTIME="linux-x64" - ;; - arm64) - RUNTIME="linux-arm64" - ;; - armhf) - RUNTIME="linux-arm" - ;; - *) - die "Could not determine host architecture!" - ;; - esac +if [ ! -z "$RUNTIME" ]; then + echo "Building for runtime ${RUNTIME}" fi -echo "Building for runtime ${RUNTIME}" - # Perform pre-execution checks CONFIGURATION="${CONFIGURATION:=Debug}" if [ -z "$VERSION" ]; then diff --git a/src/linux/Packaging.Linux/layout.sh b/src/linux/Packaging.Linux/layout.sh index 9355eaa02..ccf031156 100755 --- a/src/linux/Packaging.Linux/layout.sh +++ b/src/linux/Packaging.Linux/layout.sh @@ -44,10 +44,6 @@ PROJ_OUT="$OUT/linux/Packaging.Linux" # Build parameters FRAMEWORK=net8.0 -if [ -z "$RUNTIME" ]; then - die "--runtime was not set" -fi - # Perform pre-execution checks CONFIGURATION="${CONFIGURATION:=Debug}" @@ -76,13 +72,22 @@ fi # Publish core application executables echo "Publishing core application..." -$DOTNET_ROOT/dotnet publish "$GCM_SRC" \ - --configuration="$CONFIGURATION" \ - --framework="$FRAMEWORK" \ - --runtime="$RUNTIME" \ - --self-contained \ - -p:PublishSingleFile=true \ - --output="$(make_absolute "$PAYLOAD")" || exit 1 +if [ -z "$RUNTIME" ]; then + $DOTNET_ROOT/dotnet publish "$GCM_SRC" \ + --configuration="$CONFIGURATION" \ + --framework="$FRAMEWORK" \ + --self-contained \ + -p:PublishSingleFile=true \ + --output="$(make_absolute "$PAYLOAD")" || exit 1 +else + $DOTNET_ROOT/dotnet publish "$GCM_SRC" \ + --configuration="$CONFIGURATION" \ + --framework="$FRAMEWORK" \ + --runtime="$RUNTIME" \ + --self-contained \ + -p:PublishSingleFile=true \ + --output="$(make_absolute "$PAYLOAD")" || exit 1 +fi # Collect symbols echo "Collecting managed symbols..." diff --git a/src/linux/Packaging.Linux/pack.sh b/src/linux/Packaging.Linux/pack.sh index 817704f76..4cf5aaea7 100755 --- a/src/linux/Packaging.Linux/pack.sh +++ b/src/linux/Packaging.Linux/pack.sh @@ -100,6 +100,26 @@ INSTALL_TO="$DEBROOT/usr/local/share/gcm-core/" LINK_TO="$DEBROOT/usr/local/bin/" mkdir -p "$DEBROOT/DEBIAN" "$INSTALL_TO" "$LINK_TO" || exit 1 +# Fall back to host architecture if no explicit runtime is given. +if test -z "$RUNTIME"; then + HOST_ARCH="`dpkg-architecture -q DEB_HOST_ARCH`" + + case $HOST_ARCH in + amd64) + RUNTIME="linux-x64" + ;; + arm64) + RUNTIME="linux-arm64" + ;; + armhf) + RUNTIME="linux-arm" + ;; + *) + die "Could not determine host architecture!" + ;; + esac +fi + # Determine architecture for debian control file from the runtime architecture case $RUNTIME in linux-x64) From 47b731ed05ce3e927a17b11a19aa199bd8e28bef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Nov 2024 20:10:35 +0000 Subject: [PATCH 27/31] build(deps): bump lycheeverse/lychee-action from 2.0.2 to 2.1.0 Bumps [lycheeverse/lychee-action](https://github.com/lycheeverse/lychee-action) from 2.0.2 to 2.1.0. - [Release notes](https://github.com/lycheeverse/lychee-action/releases) - [Commits](https://github.com/lycheeverse/lychee-action/compare/7cd0af4c74a61395d455af97419279d86aafaede...f81112d0d2814ded911bd23e3beaa9dda9093915) --- updated-dependencies: - dependency-name: lycheeverse/lychee-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/lint-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-docs.yml b/.github/workflows/lint-docs.yml index 7146eb679..dce42cd58 100644 --- a/.github/workflows/lint-docs.yml +++ b/.github/workflows/lint-docs.yml @@ -35,7 +35,7 @@ jobs: - name: Run link checker # For any troubleshooting, see: # https://github.com/lycheeverse/lychee/blob/master/docs/TROUBLESHOOTING.md - uses: lycheeverse/lychee-action@7cd0af4c74a61395d455af97419279d86aafaede + uses: lycheeverse/lychee-action@f81112d0d2814ded911bd23e3beaa9dda9093915 with: # user-agent: if a user agent is not specified, some websites (e.g. From b378f2a63637ae647e60d04035c34e8b3dd544e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 20:06:34 +0000 Subject: [PATCH 28/31] build(deps): bump DavidAnson/markdownlint-cli2-action Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 17.0.0 to 18.0.0. - [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases) - [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/db43aef879112c3119a410d69f66701e0d530809...eb5ca3ab411449c66620fe7f1b3c9e10547144b0) --- updated-dependencies: - dependency-name: DavidAnson/markdownlint-cli2-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/lint-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-docs.yml b/.github/workflows/lint-docs.yml index 7146eb679..afb1688d2 100644 --- a/.github/workflows/lint-docs.yml +++ b/.github/workflows/lint-docs.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: DavidAnson/markdownlint-cli2-action@db43aef879112c3119a410d69f66701e0d530809 + - uses: DavidAnson/markdownlint-cli2-action@eb5ca3ab411449c66620fe7f1b3c9e10547144b0 with: globs: | "**/*.md" From 235d63649e820a26cb1c8d8f028b790a847d51fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Dec 2024 20:14:20 +0000 Subject: [PATCH 29/31] build(deps): bump actions/setup-dotnet from 4.1.0 to 4.2.0 Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 4.1.0 to 4.2.0. - [Release notes](https://github.com/actions/setup-dotnet/releases) - [Commits](https://github.com/actions/setup-dotnet/compare/v4.1.0...v4.2.0) --- updated-dependencies: - dependency-name: actions/setup-dotnet dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/continuous-integration.yml | 6 +++--- .github/workflows/release.yml | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8ce40ad8b..9f8170d53 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -25,7 +25,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v4.2.0 with: dotnet-version: 8.0.x diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index b0ffeb2d8..b8da48e7b 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v4.2.0 with: dotnet-version: 8.0.x @@ -62,7 +62,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v4.2.0 with: dotnet-version: 8.0.x @@ -106,7 +106,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v4.2.0 with: dotnet-version: 8.0.x diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e020c425..47469eb43 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up .NET - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v4.2.0 with: dotnet-version: 8.0.x @@ -150,7 +150,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up .NET - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v4.2.0 with: dotnet-version: 8.0.x @@ -190,7 +190,7 @@ jobs: # The Azure Code Signing action overrides the .NET version, so we reset it. - name: Set up .NET - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v4.2.0 with: dotnet-version: 8.0.x @@ -239,7 +239,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up .NET - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v4.2.0 with: dotnet-version: 8.0.x @@ -320,7 +320,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up .NET - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v4.2.0 with: dotnet-version: 8.0.x @@ -393,7 +393,7 @@ jobs: path: signed - name: Set up .NET - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v4.2.0 with: dotnet-version: 8.0.x @@ -497,7 +497,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up .NET - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v4.2.0 with: dotnet-version: 8.0.x @@ -567,7 +567,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up .NET - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v4.2.0 with: dotnet-version: 8.0.x From 5f6d32ae4e43694b0b7e329d5c87fcf4d7a1348a Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Fri, 24 Jan 2025 11:56:59 +0000 Subject: [PATCH 30/31] macospreferences: add class to read macOS app preferences --- .../Interop/MacOS/MacOSPreferencesTests.cs | 66 ++++++++++ .../Core/Interop/MacOS/MacOSKeychain.cs | 33 ++--- .../Core/Interop/MacOS/MacOSPreferences.cs | 96 ++++++++++++++ .../Interop/MacOS/Native/CoreFoundation.cs | 119 ++++++++++++++++++ src/shared/TestInfrastructure/TestUtils.cs | 38 ++++++ 5 files changed, 327 insertions(+), 25 deletions(-) create mode 100644 src/shared/Core.Tests/Interop/MacOS/MacOSPreferencesTests.cs create mode 100644 src/shared/Core/Interop/MacOS/MacOSPreferences.cs diff --git a/src/shared/Core.Tests/Interop/MacOS/MacOSPreferencesTests.cs b/src/shared/Core.Tests/Interop/MacOS/MacOSPreferencesTests.cs new file mode 100644 index 000000000..0efb14471 --- /dev/null +++ b/src/shared/Core.Tests/Interop/MacOS/MacOSPreferencesTests.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; +using GitCredentialManager.Interop.MacOS; +using static GitCredentialManager.Tests.TestUtils; + +namespace GitCredentialManager.Tests.Interop.MacOS; + +public class MacOSPreferencesTests +{ + private const string TestAppId = "com.example.gcm-test"; + private const string DefaultsPath = "/usr/bin/defaults"; + + [MacOSFact] + public async Task MacOSPreferences_ReadPreferences() + { + try + { + await SetupTestPreferencesAsync(); + + var pref = new MacOSPreferences(TestAppId); + + // Exists + string stringValue = pref.GetString("myString"); + int? intValue = pref.GetInteger("myInt"); + IDictionary dictValue = pref.GetDictionary("myDict"); + + Assert.NotNull(stringValue); + Assert.Equal("this is a string", stringValue); + Assert.NotNull(intValue); + Assert.Equal(42, intValue); + Assert.NotNull(dictValue); + Assert.Equal(2, dictValue.Count); + Assert.Equal("value1", dictValue["dict-k1"]); + Assert.Equal("value2", dictValue["dict-k2"]); + + // Does not exist + string missingString = pref.GetString("missingString"); + int? missingInt = pref.GetInteger("missingInt"); + IDictionary missingDict = pref.GetDictionary("missingDict"); + + Assert.Null(missingString); + Assert.Null(missingInt); + Assert.Null(missingDict); + } + finally + { + await CleanupTestPreferencesAsync(); + } + } + + private static async Task SetupTestPreferencesAsync() + { + // Using the defaults command set up preferences for the test app + await RunCommandAsync(DefaultsPath, $"write {TestAppId} myString \"this is a string\""); + await RunCommandAsync(DefaultsPath, $"write {TestAppId} myInt -int 42"); + await RunCommandAsync(DefaultsPath, $"write {TestAppId} myDict -dict dict-k1 value1 dict-k2 value2"); + } + + private static async Task CleanupTestPreferencesAsync() + { + // Delete the test app preferences + // defaults delete com.example.gcm-test + await RunCommandAsync(DefaultsPath, $"delete {TestAppId}"); + } +} diff --git a/src/shared/Core/Interop/MacOS/MacOSKeychain.cs b/src/shared/Core/Interop/MacOS/MacOSKeychain.cs index b024be129..9335e136d 100644 --- a/src/shared/Core/Interop/MacOS/MacOSKeychain.cs +++ b/src/shared/Core/Interop/MacOS/MacOSKeychain.cs @@ -302,35 +302,18 @@ private static string GetStringAttribute(IntPtr dict, IntPtr key) return null; } - IntPtr buffer = IntPtr.Zero; - try + if (CFDictionaryGetValueIfPresent(dict, key, out IntPtr value) && value != IntPtr.Zero) { - if (CFDictionaryGetValueIfPresent(dict, key, out IntPtr value) && value != IntPtr.Zero) + if (CFGetTypeID(value) == CFStringGetTypeID()) { - if (CFGetTypeID(value) == CFStringGetTypeID()) - { - int stringLength = (int)CFStringGetLength(value); - int bufferSize = stringLength + 1; - buffer = Marshal.AllocHGlobal(bufferSize); - if (CFStringGetCString(value, buffer, bufferSize, CFStringEncoding.kCFStringEncodingUTF8)) - { - return Marshal.PtrToStringAuto(buffer, stringLength); - } - } - - if (CFGetTypeID(value) == CFDataGetTypeID()) - { - int length = CFDataGetLength(value); - IntPtr ptr = CFDataGetBytePtr(value); - return Marshal.PtrToStringAuto(ptr, length); - } + return CFStringToString(value); } - } - finally - { - if (buffer != IntPtr.Zero) + + if (CFGetTypeID(value) == CFDataGetTypeID()) { - Marshal.FreeHGlobal(buffer); + int length = CFDataGetLength(value); + IntPtr ptr = CFDataGetBytePtr(value); + return Marshal.PtrToStringAuto(ptr, length); } } diff --git a/src/shared/Core/Interop/MacOS/MacOSPreferences.cs b/src/shared/Core/Interop/MacOS/MacOSPreferences.cs new file mode 100644 index 000000000..f866b30a8 --- /dev/null +++ b/src/shared/Core/Interop/MacOS/MacOSPreferences.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using GitCredentialManager.Interop.MacOS.Native; +using static GitCredentialManager.Interop.MacOS.Native.CoreFoundation; + +namespace GitCredentialManager.Interop.MacOS; + +public class MacOSPreferences +{ + private readonly string _appId; + + public MacOSPreferences(string appId) + { + EnsureArgument.NotNull(appId, nameof(appId)); + + _appId = appId; + } + + /// + /// Return a typed value from the app preferences. + /// + /// Preference name. + /// Thrown if the preference is not a string. + /// + /// or null if the preference with the given key does not exist. + /// + public string GetString(string key) + { + return TryGet(key, CFStringToString, out string value) + ? value + : null; + } + + /// + /// Return a typed value from the app preferences. + /// + /// Preference name. + /// Thrown if the preference is not an integer. + /// + /// or null if the preference with the given key does not exist. + /// + public int? GetInteger(string key) + { + return TryGet(key, CFNumberToInt32, out int value) + ? value + : null; + } + + /// + /// Return a typed value from the app preferences. + /// + /// Preference name. + /// Thrown if the preference is not a dictionary. + /// + /// or null if the preference with the given key does not exist. + /// + public IDictionary GetDictionary(string key) + { + return TryGet(key, CFDictionaryToDictionary, out IDictionary value) + ? value + : null; + } + + private bool TryGet(string key, Func converter, out T value) + { + IntPtr cfValue = IntPtr.Zero; + IntPtr keyPtr = IntPtr.Zero; + IntPtr appIdPtr = CreateAppIdPtr(); + + try + { + keyPtr = CFStringCreateWithCString(IntPtr.Zero, key, CFStringEncoding.kCFStringEncodingUTF8); + cfValue = CFPreferencesCopyAppValue(keyPtr, appIdPtr); + + if (cfValue == IntPtr.Zero) + { + value = default; + return false; + } + + value = converter(cfValue); + return true; + } + finally + { + if (cfValue != IntPtr.Zero) CFRelease(cfValue); + if (keyPtr != IntPtr.Zero) CFRelease(keyPtr); + if (appIdPtr != IntPtr.Zero) CFRelease(appIdPtr); + } + } + + private IntPtr CreateAppIdPtr() + { + return CFStringCreateWithCString(IntPtr.Zero, _appId, CFStringEncoding.kCFStringEncodingUTF8); + } +} diff --git a/src/shared/Core/Interop/MacOS/Native/CoreFoundation.cs b/src/shared/Core/Interop/MacOS/Native/CoreFoundation.cs index 0f32a383b..9cab2ca8f 100644 --- a/src/shared/Core/Interop/MacOS/Native/CoreFoundation.cs +++ b/src/shared/Core/Interop/MacOS/Native/CoreFoundation.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using static GitCredentialManager.Interop.MacOS.Native.LibSystem; @@ -55,6 +56,9 @@ public static extern void CFDictionaryAddValue( public static extern IntPtr CFStringCreateWithBytes(IntPtr alloc, byte[] bytes, long numBytes, CFStringEncoding encoding, bool isExternalRepresentation); + [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr CFStringCreateWithCString(IntPtr alloc, string cStr, CFStringEncoding encoding); + [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] public static extern long CFStringGetLength(IntPtr theString); @@ -82,15 +86,130 @@ public static extern IntPtr CFStringCreateWithBytes(IntPtr alloc, byte[] bytes, [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] public static extern int CFArrayGetTypeID(); + [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern int CFNumberGetTypeID(); + [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr CFDataGetBytePtr(IntPtr theData); [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] public static extern int CFDataGetLength(IntPtr theData); + + [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr CFPreferencesCopyAppValue(IntPtr key, IntPtr appID); + + [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern bool CFNumberGetValue(IntPtr number, CFNumberType theType, out IntPtr valuePtr); + + [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr CFDictionaryGetKeysAndValues(IntPtr theDict, IntPtr[] keys, IntPtr[] values); + + [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern long CFDictionaryGetCount(IntPtr theDict); + + public static string CFStringToString(IntPtr cfString) + { + if (cfString == IntPtr.Zero) + { + throw new ArgumentNullException(nameof(cfString)); + } + + if (CFGetTypeID(cfString) != CFStringGetTypeID()) + { + throw new InvalidOperationException("Object is not a CFString."); + } + + long length = CFStringGetLength(cfString); + IntPtr buffer = Marshal.AllocHGlobal((int)length + 1); + + try + { + if (!CFStringGetCString(cfString, buffer, length + 1, CFStringEncoding.kCFStringEncodingUTF8)) + { + throw new InvalidOperationException("Failed to convert CFString to C string."); + } + + return Marshal.PtrToStringAnsi(buffer); + } + finally + { + Marshal.FreeHGlobal(buffer); + } + } + + public static int CFNumberToInt32(IntPtr cfNumber) + { + if (cfNumber == IntPtr.Zero) + { + throw new ArgumentNullException(nameof(cfNumber)); + } + + if (CFGetTypeID(cfNumber) != CFNumberGetTypeID()) + { + throw new InvalidOperationException("Object is not a CFNumber."); + } + + if (!CFNumberGetValue(cfNumber, CFNumberType.kCFNumberIntType, out IntPtr valuePtr)) + { + throw new InvalidOperationException("Failed to convert CFNumber to Int32."); + } + + return valuePtr.ToInt32(); + } + + public static IDictionary CFDictionaryToDictionary(IntPtr cfDict) + { + if (cfDict == IntPtr.Zero) + { + throw new ArgumentNullException(nameof(cfDict)); + } + + if (CFGetTypeID(cfDict) != CFDictionaryGetTypeID()) + { + throw new InvalidOperationException("Object is not a CFDictionary."); + } + + int count = (int)CFDictionaryGetCount(cfDict); + var keys = new IntPtr[count]; + var values = new IntPtr[count]; + + CFDictionaryGetKeysAndValues(cfDict, keys, values); + + var dict = new Dictionary(capacity: count); + for (int i = 0; i < count; i++) + { + string keyStr = CFStringToString(keys[i])!; + string valueStr = CFStringToString(values[i]); + + dict[keyStr] = valueStr; + } + + return dict; + } } public enum CFStringEncoding { kCFStringEncodingUTF8 = 0x08000100, } + + public enum CFNumberType + { + kCFNumberSInt8Type = 1, + kCFNumberSInt16Type = 2, + kCFNumberSInt32Type = 3, + kCFNumberSInt64Type = 4, + kCFNumberFloat32Type = 5, + kCFNumberFloat64Type = 6, + kCFNumberCharType = 7, + kCFNumberShortType = 8, + kCFNumberIntType = 9, + kCFNumberLongType = 10, + kCFNumberLongLongType = 11, + kCFNumberFloatType = 12, + kCFNumberDoubleType = 13, + kCFNumberCFIndexType = 14, + kCFNumberNSIntegerType = 15, + kCFNumberCGFloatType = 16 + } } diff --git a/src/shared/TestInfrastructure/TestUtils.cs b/src/shared/TestInfrastructure/TestUtils.cs index c547856d7..000b8e75e 100644 --- a/src/shared/TestInfrastructure/TestUtils.cs +++ b/src/shared/TestInfrastructure/TestUtils.cs @@ -1,5 +1,7 @@ using System; +using System.Diagnostics; using System.IO; +using System.Threading.Tasks; namespace GitCredentialManager.Tests { @@ -87,5 +89,41 @@ public static string GetUuid(int length = -1) return uuid.Substring(0, length); } + + public static async Task RunCommandAsync(string filePath, string arguments, string workingDirectory = null) + { + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = filePath, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory + } + }; + + process.Start(); + + string output = await process.StandardOutput.ReadToEndAsync(); + string error = await process.StandardError.ReadToEndAsync(); + + await process.WaitForExitAsync(); + + if (process.ExitCode != 0) + { + throw new InvalidOperationException( + $"Command `{filePath} {arguments}` failed with exit code {process.ExitCode}." + + Environment.NewLine + + $"Output: {output}" + + Environment.NewLine + + $"Error: {error}"); + } + + return output; + } } } From b05317f74562e087b56ce8e7fdfc44e33c400589 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Fri, 24 Jan 2025 12:07:18 +0000 Subject: [PATCH 31/31] macossettings: implement default settings for macOS --- docs/enterprise-config.md | 33 ++++++++- src/shared/Core/CommandContext.cs | 2 +- src/shared/Core/Constants.cs | 1 + .../Core/Interop/MacOS/MacOSSettings.cs | 67 +++++++++++++++++++ 4 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 src/shared/Core/Interop/MacOS/MacOSSettings.cs diff --git a/docs/enterprise-config.md b/docs/enterprise-config.md index bfdc7e302..97544a33f 100644 --- a/docs/enterprise-config.md +++ b/docs/enterprise-config.md @@ -55,7 +55,38 @@ those of the [Git configuration][config] settings. The type of each registry key can be either `REG_SZ` (string) or `REG_DWORD` (integer). -## macOS/Linux +## macOS + +Default settings values come from macOS's preferences system. Configuration +profiles can be deployed to devices using a compatible Mobile Device Management +(MDM) solution. + +Configuration for Git Credential Manager must take the form of a dictionary, set +for the domain `git-credential-manager` under the key `configuration`. For +example: + +```shell +defaults write git-credential-manager configuration -dict-add +``` + +..where `` is the name of the settings from the [Git configuration][config] +reference, and `` is the desired value. + +All values in the `configuration` dictionary must be strings. For boolean values +use `true` or `false`, and for integer values use the number in string form. + +To read the current configuration: + +```console +$ defaults read git-credential-manager configuration +{ + = ; + ... + = ; +} +``` + +## Linux Default configuration setting stores has not been implemented. diff --git a/src/shared/Core/CommandContext.cs b/src/shared/Core/CommandContext.cs index 712db32e1..d3ef1dbf6 100644 --- a/src/shared/Core/CommandContext.cs +++ b/src/shared/Core/CommandContext.cs @@ -131,7 +131,7 @@ public CommandContext() gitPath, FileSystem.GetCurrentDirectory() ); - Settings = new Settings(Environment, Git); + Settings = new MacOSSettings(Environment, Git, Trace); } else if (PlatformUtils.IsLinux()) { diff --git a/src/shared/Core/Constants.cs b/src/shared/Core/Constants.cs index 191fcc83d..4777b0cf8 100644 --- a/src/shared/Core/Constants.cs +++ b/src/shared/Core/Constants.cs @@ -16,6 +16,7 @@ public static class Constants public const string GcmDataDirectoryName = ".gcm"; + public const string MacOSBundleId = "git-credential-manager"; public static readonly Guid DevBoxPartnerId = new("e3171dd9-9a5f-e5be-b36c-cc7c4f3f3bcf"); /// diff --git a/src/shared/Core/Interop/MacOS/MacOSSettings.cs b/src/shared/Core/Interop/MacOS/MacOSSettings.cs new file mode 100644 index 000000000..3ef2c8247 --- /dev/null +++ b/src/shared/Core/Interop/MacOS/MacOSSettings.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; + +namespace GitCredentialManager.Interop.MacOS +{ + /// + /// Reads settings from Git configuration, environment variables, and defaults from the system. + /// + public class MacOSSettings : Settings + { + private readonly ITrace _trace; + + public MacOSSettings(IEnvironment environment, IGit git, ITrace trace) + : base(environment, git) + { + EnsureArgument.NotNull(trace, nameof(trace)); + _trace = trace; + + PlatformUtils.EnsureMacOS(); + } + + protected override bool TryGetExternalDefault(string section, string scope, string property, out string value) + { + value = null; + + try + { + // Check for app default preferences for our bundle ID. + // Defaults can be deployed system administrators via device management profiles. + var prefs = new MacOSPreferences(Constants.MacOSBundleId); + IDictionary dict = prefs.GetDictionary("configuration"); + + if (dict is null) + { + // No configuration key exists + return false; + } + + // Wrap the raw dictionary in one configured with the Git configuration key comparer. + // This means we can use the same key comparison rules as Git in our configuration plist dict, + // That is, sections and names are insensitive to case, but the scope is case-sensitive. + var config = new Dictionary(dict, GitConfigurationKeyComparer.Instance); + + string name = string.IsNullOrWhiteSpace(scope) + ? $"{section}.{property}" + : $"{section}.{scope}.{property}"; + + if (!config.TryGetValue(name, out value)) + { + // No property exists + return false; + } + + _trace.WriteLine($"Default setting found in app preferences: {name}={value}"); + return true; + } + catch (Exception ex) + { + // Reading defaults is not critical to the operation of the application + // so we can ignore any errors and just log the failure. + _trace.WriteLine("Failed to read default setting from app preferences."); + _trace.WriteException(ex); + return false; + } + } + } +}