Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e9dc8a4
release: fix VERSION parsing for release
mjcheetham Sep 23, 2026
4f9d6fe
build: drop win-x86 (32-bit x86) builds for release and CI
mjcheetham Sep 25, 2026
6fe3c29
build: drop win-x86 (32-bit x86) builds for release and CI (#2456)
mjcheetham Sep 25, 2026
57ed653
README.md: update support matrix for CPU/OS
mjcheetham Sep 25, 2026
64ba07b
entra: do not use default account on DevBox
mjcheetham Sep 28, 2026
bb83cfc
entra: do not use default account on DevBox (#2459)
mjcheetham Sep 28, 2026
000a175
protocol: add and wire 'authtype' capability
becm Sep 26, 2026
36f4624
credential: add flag for ephemeral response state
becm Sep 30, 2026
a091842
docs: update WSL docs to use correct GfW install paths
mjcheetham Oct 1, 2026
dab009e
docs: update WSL docs to use correct GfW install paths (#2460)
mjcheetham Oct 1, 2026
917c824
docs: fix WSL without GfW paths
mjcheetham Oct 1, 2026
5344748
docs: fix WSL without GfW paths (#2461)
mjcheetham Oct 1, 2026
9c3b25d
protocol: add support for 'authtype' capability (#2457)
mjcheetham Oct 1, 2026
5bc1c09
msal: ensure we only parent to visible console windows
mjcheetham Oct 5, 2026
a7a2d47
msal: extract console parent to a method
mjcheetham Oct 5, 2026
d997195
msal: allow testing of MsalParentWindowAdatper
mjcheetham Oct 5, 2026
261bdd3
msal: prefer the console window over progress stub
mjcheetham Oct 5, 2026
6f4fedc
msal: add tests of the parent window adapter
mjcheetham Oct 5, 2026
8413a8f
docs: document the GCM_MODAL_PARENTHWND variable
mjcheetham Oct 5, 2026
2d60006
msal: restore visible console parenting on Windows (#2464)
mjcheetham Oct 5, 2026
601b7ad
auth: share visible console parenting
mjcheetham Oct 5, 2026
a3f1cea
ui: use console fallback for helper parents
mjcheetham Oct 5, 2026
f55797b
ui: extend console parenting beyond MSAL (#2465)
mjcheetham Oct 5, 2026
1ca8630
VERSION: bump to 3.0.1
mjcheetham Oct 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 1 addition & 13 deletions .azure-pipelines/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,6 @@ parameters:
- name: windows_matrix
type: object
default:
- id: windows_x86
jobName: 'Windows (x86)'
runtime: win-x86
pool: GitClientPME-1ESHostedPool-intel-pc
poolArch: amd64
image: win-x86_64-ado1es
os: windows
- id: windows_x64
jobName: 'Windows (x64)'
runtime: win-x64
Expand Down Expand Up @@ -609,7 +602,7 @@ extends:
inputs:
targetType: inline
script: |
echo "##vso[task.setvariable variable=value;isOutput=true;isReadOnly=true]$(cat ./VERSION | sed -E 's/.[0-9]+$//')"
echo "##vso[task.setvariable variable=value;isOutput=true;isReadOnly=true]$(cat ./VERSION)"

- job: github
displayName: 'Publish GitHub release'
Expand All @@ -626,9 +619,6 @@ extends:
isProduction: true
inputs:
# Installers and packages
- input: pipelineArtifact
artifactName: 'win-x86'
targetPath: $(Pipeline.Workspace)/assets/win-x86
- input: pipelineArtifact
artifactName: 'win-x64'
targetPath: $(Pipeline.Workspace)/assets/win-x64
Expand Down Expand Up @@ -664,8 +654,6 @@ extends:
isDraft: true
addChangeLog: false
assets: |
$(Pipeline.Workspace)/assets/win-x86/*.exe
$(Pipeline.Workspace)/assets/win-x86/*.zip
$(Pipeline.Workspace)/assets/win-x64/*.exe
$(Pipeline.Workspace)/assets/win-x64/*.zip
$(Pipeline.Workspace)/assets/win-arm64/*.exe
Expand Down
11 changes: 1 addition & 10 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
strategy:
fail-fast: false
matrix:
runtime: [ win-x86, win-x64, win-arm64 ]
runtime: [ win-x64, win-arm64 ]

steps:
- uses: actions/checkout@v7
Expand All @@ -27,15 +27,6 @@ jobs:
with:
dotnet-version: 10.0.x

# The x86 test host requires an x86 .NET runtime, which isn't pre-installed
# on the runner, nor can the actions/setup-dotnet action install it.
# Install it manually so tests can run.
- name: Setup .NET (x86)
if: matrix.runtime == 'win-x86'
run: |
Invoke-WebRequest 'https://dot.net/v1/dotnet-install.ps1' -OutFile dotnet-install.ps1
./dotnet-install.ps1 -Channel 10.0 -Architecture x86 -InstallDir 'C:\Program Files (x86)\dotnet'

- name: Install dependencies
run: dotnet restore

Expand Down
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ You can still use Git with SSH - see the specific documentation for your host on
how to set up SSH: [Azure DevOps][azure-devops-ssh], [GitHub][github-ssh],
[Bitbucket][bitbucket-ssh]

Feature|Windows|macOS|Linux
Feature|Windows|macOS|Linux¹
-|:-:|:-:|:-:
Installer/uninstaller|✓|✓|✓
Secure platform [credential storage][gcm-credstores]|✓|✓|✓
Expand All @@ -47,10 +47,20 @@ Windows Integrated Authentication (NTLM/Kerberos)|✓|_N/A_|_N/A_
Generic OAuth authentication|✓|✓|✓
Basic HTTP authentication|✓|✓|✓
Network proxies|✓|✓|✓
`amd64` support|✓|✓|✓
`x86` support|✓|_N/A_|✗
`arm64` support|best effort|✓|✓
`armhf` support|_N/A_|_N/A_|✓
x64 _(x86_64 / AMD64)_ support|✓|✓²|✓
x86 _(i686)_ support|✓³|_N/A_|✗
ARM64 _(aarch64)_ support|✓|✓|✓
ARM32 _(armhf)_ support|_N/A_|_N/A_|✓³

1. Linux support is limited to the
[distributions officially supported by .NET][dotnet-os-support].

2. Mac support on Intel-based (x64) devices is best effort.
Apple announced that macOS 28 will [drop support][apple-intel-support] for
Intel-based applications.

3. No official builds produced, but local builds should be possible.
Support is best effort.

## Supported Environments

Expand Down Expand Up @@ -183,3 +193,4 @@ When using GitHub logos, please be sure to follow the
[roadmap]: https://github.com/git-ecosystem/git-credential-manager/milestones?direction=desc&sort=due_date&state=open
[roadmap-announcement]: https://github.com/git-ecosystem/git-credential-manager/discussions/1203
[workflow-status]: https://github.com/git-ecosystem/git-credential-manager/actions/workflows/continuous-integration.yml
[apple-intel-support]: https://support.apple.com/en-us/102527
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.0.0
3.0.1
40 changes: 40 additions & 0 deletions docs/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,46 @@ Defaults to enabled.

---

### GCM_MODAL_PARENTHWND

Specify the parent window handle (`HWND`) for modal dialogs on Windows.

When this variable is unset, GCM tries to parent authentication dialogs to the
console's visible root-owner window. Hidden or invalid windows are ignored.

Explicitly setting a handle using this variable takes precedence and it is not
checked for visibility.

> [!NOTE]
> This is intended for use by application developers who are calling Git and/or
> GCM from their application and wish to correctly parent authentication dialogs
> to an existing window.
>
> Do **not** set this manually.

#### Example

##### Windows

```csharp
public static int Example(IntPtr myWindowHandle)
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo("git.exe", "pull")
{
Environment =
{
["GCM_MODAL_PARENTHWND"] = myWindowHandle.ToString("d")
}
};
process.Start();
process.WaitForExit();
return process.ExitCode;
}
```

---

### GCM_PROVIDER

Define the host provider to use when authenticating.
Expand Down
60 changes: 46 additions & 14 deletions docs/wsl.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,46 @@ Start by installing the [latest Git for Windows ⬇️][latest-git-for-windows]
_Inside your WSL installation_, run the following command to set GCM as the Git
credential helper:

> [!IMPORTANT]
> The exact location of git-credential-manager.exe may be different in your
> installation of Git for Windows depending on the version you have installed.
> Please check and confirm the actual installation path before proceeding.

### Git for Windows (x64 / x86_64 / "64-bit")

The latest versions of Git for Windows for x64 (v2.56.0 and later) use the
`ucrt64` toolchain and installation path:

```shell
git config --global credential.helper "/mnt/c/Program\ Files/Git/ucrt64/bin/git-credential-manager.exe"
```

Older versions of Git for Windows for x64 (v2.55.x and earlier) use the
`mingw64` toolchain and installation path:

```shell
git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager.exe"
```

> **Note:** the location of git-credential-manager.exe may be different in your
installation of Git for Windows.
### Git for Windows (ARM64)

If you intend to use Azure DevOps you must _also_ set the following Git
configuration _inside of your WSL installation_.
All known versions of Git for Windows for ARM64 use the `clangarm64` toolchain
and installation path:

```shell
git config --global credential.https://dev.azure.com.useHttpPath true
git config --global credential.helper "/mnt/c/Program\ Files/Git/clangarm64/bin/git-credential-manager.exe"
```

## Azure DevOps Additional Configuration

> [!IMPORTANT]
> If you intend to use Azure DevOps you must _also_ set the following Git
> configuration _inside of your WSL installation_.
>
> ```shell
> git config --global credential.https://dev.azure.com.useHttpPath true
> ```

## Configuring WSL without Git for Windows

If you wish to use GCM inside of WSL _without installing Git for Windows_
Expand All @@ -47,21 +73,27 @@ _Inside your WSL installation_, run the following command to set GCM as the Git
credential helper:

```shell
# For x64 or ARM64 installations of Git Credential Manager
git config --global credential.helper "/mnt/c/Program\ Files/Git\ Credential\ Manager/git-credential-manager.exe"

# For x86 installations of Git Credential Manager
git config --global credential.helper "/mnt/c/Program\ Files\ \(x86\)/Git\ Credential\ Manager/git-credential-manager.exe"

# For Azure DevOps support only
git config --global credential.https://dev.azure.com.useHttpPath true
```

In **_Windows_** you need to update the `WSLENV` environment variable to include
the value `GIT_EXEC_PATH/wp`. From an _Administrator_ Command Prompt run the
following:

```batch
SETX WSLENV %WSLENV%:GIT_EXEC_PATH/wp
```

After updating the `WSLENV` environment variable, restart your WSL installation.
> [!IMPORTANT]
> In **_Windows_** you need to update the `WSLENV` environment variable to
> include the value `GIT_EXEC_PATH/wp`. From an _Administrator_ Command Prompt
> run the following:
>
> ```batch
> SETX WSLENV %WSLENV%:GIT_EXEC_PATH/wp
> ```
>
> After updating the `WSLENV` environment variable, **restart your WSL
> installation**.

### Using the user-only GCM installer?

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Threading;
using GitCredentialManager.Authentication.Entra;
using Xunit;

namespace GitCredentialManager.Tests.Authentication.Entra;

public class MsalParentWindowAdapterTests
{
[Theory]
[InlineData(42, false)]
[InlineData(42, true)]
public void GetWindow_ExplicitParent_ReturnsParent(int parentValue, bool createIfMissing)
{
object parentWindow = new IntPtr(parentValue);
using var adapter = new MsalParentWindowAdapter(parentWindow, createIfMissing,
_ => throw new InvalidOperationException("A progress window should not be created."));

object result = adapter.GetWindow();
Assert.Same(parentWindow, result);
}

[Fact]
public void GetWindow_NoParent_Required_CreatesProgressWindow()
{
var progressWindow = new IntPtr(42);
var windowToken = CancellationToken.None;
using (var adapter = new MsalParentWindowAdapter(IntPtr.Zero, true,
ct =>
{
windowToken = ct;
return progressWindow;
}))
{
IntPtr result = Assert.IsType<IntPtr>(adapter.GetWindow());

Assert.Equal(progressWindow, result);
Assert.True(windowToken.CanBeCanceled);
Assert.False(windowToken.IsCancellationRequested);
}

Assert.True(windowToken.IsCancellationRequested);
}

[Fact]
public void GetWindow_NoParent_NotRequired_ReturnsNull()
{
using var adapter = new MsalParentWindowAdapter(IntPtr.Zero, false,
_ => throw new InvalidOperationException("A progress window should not be created."));

object result = adapter.GetWindow();

Assert.Null(result);
}
}
2 changes: 1 addition & 1 deletion src/Core.Tests/Commands/CapabilityCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public void CapabilityCommand_Execute_WritesVersionAndAdvertisedCapabilities()
Assert.StartsWith("version 0\n", actualOutput);

// GCM advertises the state capability.
Assert.Equal("version 0\ncapability state\n", actualOutput);
Assert.Equal("version 0\ncapability state\ncapability authtype\n", actualOutput);
}

[Fact]
Expand Down
58 changes: 57 additions & 1 deletion src/Core.Tests/Commands/GetCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public async Task GetCommand_ExecuteAsync_NegotiatedCapability_EchoesIntersectio
string actualOutput = context.Streams.Out.ToString().Replace("\r\n", "\n");

Assert.Contains("capability[]=state\n", actualOutput);
Assert.DoesNotContain("capability[]=authtype", actualOutput);
Assert.Contains("capability[]=authtype\n", actualOutput);
}

[Fact]
Expand Down Expand Up @@ -328,6 +328,62 @@ public async Task GetCommand_ExecuteAsync_OutputOrdering_CapabilitiesFirstThenSc
"state[] must follow continue=1");
}

[Fact]
public async Task GetCommand_ExecuteAsync_CapabilityAuthType_MissingRevertsToPlainPassword()
{
ICredential testCredential = new GitCredential("alice", "hunter2");
var response = GitResponse.Ok(testCredential, isEphemeral: true, authtype: "token");

var stdin = "protocol=https\nhost=example.com\n\n";

var providerMock = new Mock<IHostProvider>();
providerMock.Setup(x => x.GetCredentialAsync(It.IsAny<GitRequest>()))
.ReturnsAsync(response);
var providerRegistry = new TestHostProviderRegistry { Provider = providerMock.Object };
var context = new TestCommandContext { Streams = { In = stdin } };

var command = new GetCommand(context, providerRegistry);

await command.ExecuteAsync();

string[] actualOutput = context.Streams.Out.ToString().Replace("\r\n", "\n").Split('\n');

// Emits regular Credential without 'state[]=authtype' support.
Assert.Contains("username=alice", actualOutput);
Assert.Contains("password=hunter2", actualOutput);
Assert.DoesNotContain("authtype=token", actualOutput);
Assert.DoesNotContain("credential=hunter2", actualOutput);
Assert.DoesNotContain("ephemeral=1", actualOutput);
}

[Fact]
public async Task GetCommand_ExecuteAsync_CapabilityAuthType_UsesAuthTypeFeatures()
{
ICredential testCredential = new GitCredential("alice", "hunter2");
var response = GitResponse.Ok(testCredential, isEphemeral: true, authtype: "token");

var stdin = "protocol=https\nhost=example.com\ncapability[]=authtype\n\n";

var providerMock = new Mock<IHostProvider>();
providerMock.Setup(x => x.GetCredentialAsync(It.IsAny<GitRequest>()))
.ReturnsAsync(response);
var providerRegistry = new TestHostProviderRegistry { Provider = providerMock.Object };
var context = new TestCommandContext { Streams = { In = stdin } };

var command = new GetCommand(context, providerRegistry);

await command.ExecuteAsync();

string[] actualOutput = context.Streams.Out.ToString().Replace("\r\n", "\n").Split('\n');

// Ephemeral credential with 'authtype' and 'credential' value
Assert.DoesNotContain("username=alice", actualOutput);
Assert.DoesNotContain("password=hunter2", actualOutput);
Assert.Contains("authtype=token", actualOutput);
Assert.Contains("credential=hunter2", actualOutput);
Assert.Contains("ephemeral=1", actualOutput);
}

#region Helpers

private static IDictionary<string, string> ParseDictionary(StringBuilder sb) => ParseDictionary(sb.ToString());
Expand Down
Loading
Loading