Skip to content

Protocol foundation: capability negotiation, GitRequest/GitResponse, state[] + continue#2357

Draft
mjcheetham wants to merge 42 commits into
git-ecosystem:vnextfrom
mjcheetham:new-protocol
Draft

Protocol foundation: capability negotiation, GitRequest/GitResponse, state[] + continue#2357
mjcheetham wants to merge 42 commits into
git-ecosystem:vnextfrom
mjcheetham:new-protocol

Conversation

@mjcheetham

Copy link
Copy Markdown
Contributor

Refs: #2057

Summary

Git 2.46 added a capability negotiation handshake to the credential helper protocol, gating a set of new attributes — state[], continue, authtype, credential, ephemeral, password_expiry_utc, oauth_refresh_token.

This PR lays the foundation for GCM to participate in that protocol and ships the first capability end-to-end (state + continue). It is purely infrastructure: behaviour for end-users is unchanged for any existing flow, but the host-provider surface is now in a shape where individual provider features (smarter account selection, multistage auth, etc.) can land one commit at a time without churning IHostProvider again.

What's in this PR

Eight commits, ordered as a readable story:

  1. hostprovider: rename InputArguments to GitRequest
    Pure rename. InputArguments framed the type as a low-level argument bag; GitRequest matches its actual role as the anchor for everything a host provider needs to know about a single credential helper invocation. No methods change shape.

  2. hostprovider: add capability negotiation and GitResponse
    Introduces the GitCapabilities [Flags] enum and a GitCapabilitiesExtensions helper that parses incoming capability[] lines, advertises GCM's own set (empty for now), and renders flags back to wire names. Surfaces a typed Capabilities property on GitRequest. Replaces the old GetCredentialResult with a new GitResponse type. Wires the handshake through GetCommand end-to-end — the intersection of what Git and GCM advertised is echoed back on the response (currently empty, so no capability[] lines appear in practice, but the plumbing is live).

  3. hostprovider: add 'git credential capability' action
    Git 2.46 also added a capability action distinct from get/store/erase. Implements it as CapabilityCommand (no stdin, no provider selection — capabilities are a global property of the helper). Today emits just version 0; advertised capabilities will appear here automatically as flags are added to GitCapabilitiesExtensions.Advertised.

  4. commands: hide Git credential helper actions from --help
    get/store/erase/capability are protocol entry points for Git, not user-facing commands. Mark them IsHidden = true so git-credential-manager --help no longer lists them alongside the commands a human actually runs (configure, diagnose, per-provider subcommands).

  5. response: add Ok/Cancel factories and wire quit=1
    Gives providers a non-exceptional way to express "user cancelled the prompt" / "no eligible account". GitResponse.Cancel() emits quit=1 so Git aborts the credential pipeline (no terminal re-prompt after the user already cancelled in a GUI dialog). GitResponse.Ok(credential) is the named factory for the success case. Mutually-exclusive shape enforced by the constructor.

  6. response: add Yield() for helpers that have nothing to offer
    Third shape: "I have nothing to contribute for this request, but I'm not stopping you — try other helpers or fall back to your interactive prompt". Translates to the polite empty response (terminating blank line, no credential fields, no quit signal) so Git proceeds to the next helper. Today providers had no clean way to say this — they either threw (wrong: a no-op isn't an error) or returned empty credentials (wrong: that's the WIA signal and gets stored).

  7. hostprovider: wire the state and continue protocol capability
    The marquee feature. Adds the State flag to the advertised set, surfaces GitRequest.State as a lazy IReadOnlyDictionary<string,string> filtered to the reserved gcm. prefix, and grows GitResponse with a fourth shape: Continue(credential) returns a credential while signalling that another exchange is expected. State writes go through a single validating SetState (or the fluent WithState); reads through the read-only view. The shape matrix becomes Ok / Continue / Cancel / Yield, all mutually exclusive. This is what unlocks optimistic account selection, multistage NTLM/Kerberos flows, and any provider scenario that benefits from per-request memory across the get → store/erase cycle.

  8. github: delete stranded SelectAccountCommandImpl
    Cleanup that fell off the bottom of a previous refactor (aab6fef folded .UI.Avalonia into .UI but missed this file). True no-op — no .csproj, no consumer, no behaviour change.

What this enables (deliberately not in this PR)

These belong in follow-up PRs that each ship a concrete user-visible win on top of this foundation:

  • AzRepos smarter account selection.
    With state[] + continue the provider can return a token optimistically (e.g. the only cached account in the request's tenant), record which account it tried in state, and pick a different one on a 401 retry — all within a single Git operation. Sits on top of the binding-manager refactor and account-pool commands that already use this infrastructure on a separate branch.

  • Picker UI prompt-and-remember.
    The picker emits a "remember this choice" checkbox; the result is threaded through state on the get response and persisted on the next store — so a binding is only written when the chosen credential actually worked.

  • GitHub / Bitbucket / GitLab state-driven UX.
    Each is its own PR when there's a concrete win.

  • Other capability-gated attributes (authtype, credential, password_expiry_utc, oauth_refresh_token, ephemeral).
    One capability flag at a time, each in its own PR.

Compatibility

  • Older Git (no capability handshake).
    Git never sends capability[], so GitRequest.Capabilities is None, and the helper-side handshake echoes nothing back. Every existing provider path works exactly as today.

  • Existing providers.
    The InputArgumentsGitRequest rename is mechanical and complete across the repo; third-party providers building against GCM source will pick up the rename but need no behavioural changes. The GetCredentialResultGitResponse swap preserves the existing AdditionalProperties escape hatch (still used by GenericHostProvider for the ntlm=allow signal).

  • quit=1 on Cancel.
    Previously, a cancelled prompt surfaced as fatal: ... via the top-level exception handler. After this PR, providers that have migrated to GitResponse.Cancel() emit quit=1 instead — same end result for the user (no credential), cleaner contract with Git. Providers that still throw are unchanged.

Testing

  • Full unit test suite passes (Core, AzureRepos, GitHub, Bitbucket, GitLab).
  • New tests cover capability parsing/rendering, response shape mutual exclusion, state validation rules, and the quit=1 / yield emission paths.
  • The capability action is verified end-to-end against the documented version 0 + capability <name> response format.

Notes for reviewers

  • The eight commits are organised so each one tells a focused story; reviewing commit-by-commit is recommended over the squashed diff. The renames in commit 1 are noisy but mechanical; the substance starts in commit 2.
  • The Capabilities.None advertised set in commit 2 is deliberate. Commit 7 is the one that flips on the first real capability; landing it as a separate commit makes the end-to-end wiring visible without buried-in-noise.
  • No provider behaviour changes in this PR. Anyone landing a feature on top is expected to add tests for the user-visible win, not for the underlying capability plumbing.

Closes part of #2057.

@mjcheetham mjcheetham marked this pull request as ready for review June 24, 2026 10:57
@mjcheetham mjcheetham requested review from a team as code owners June 24, 2026 10:57
@mjcheetham mjcheetham added the gcm3.0 Targets the next major release of Git Credential Manager - v3.0 label Jun 24, 2026
@mjcheetham mjcheetham requested a review from Copilot June 24, 2026 11:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR modernizes the credential-helper protocol layer to support Git 2.46 capability negotiation and introduces new typed request/response primitives (GitRequest, GitResponse) as the foundation for state[] + continue and future capability-gated attributes.

Changes:

  • Renames InputArguments to GitRequest and updates the host-provider surface area accordingly.
  • Introduces GitResponse (Ok/Continue/Cancel/Yield), capability negotiation plumbing, and the new git credential capability action.
  • Adds state validation and updates providers/tests/docs; removes a stranded GitHub Avalonia command implementation.

Reviewed changes

Copilot reviewed 52 out of 52 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/shared/TestInfrastructure/Objects/TestHostProviderRegistry.cs Updates test registry interface to accept GitRequest.
src/shared/TestInfrastructure/Objects/TestHostProvider.cs Updates test host provider callbacks and overrides to use GitRequest.
src/shared/Microsoft.AzureRepos/UriHelpers.cs Renames parameter/docs from InputArguments to GitRequest.
src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs Updates Azure Repos provider to the new GitRequest/GitResponse API.
src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs Adapts Azure Repos tests to GitRequest/GitResponse.
src/shared/GitLab/GitLabHostProvider.cs Updates GitLab provider to the new request/response types.
src/shared/GitLab.Tests/GitLabHostProviderTests.cs Adapts GitLab tests to GitRequest.
src/shared/GitHub/GitHubHostProvider.cs Updates GitHub provider to the new request/response types.
src/shared/GitHub.UI.Avalonia/Commands/SelectAccountCommandImpl.cs Deletes an unused/stranded Avalonia implementation.
src/shared/GitHub.Tests/GitHubHostProviderTests.cs Adapts GitHub tests to GitRequest and variable renames.
src/shared/Core/HostProviderRegistry.cs Updates registry selection to operate on GitRequest.
src/shared/Core/HostProvider.cs Updates host provider interface to GitRequest + GitResponse and removes GetCredentialResult.
src/shared/Core/GitStateValidation.cs Adds state key/value validation helpers for state[] emission.
src/shared/Core/GitResponse.cs Adds the new response shape model (Ok/Continue/Cancel/Yield) and state support.
src/shared/Core/GitRequest.cs Implements capability parsing and state parsing on the request.
src/shared/Core/GitCapabilities.cs Introduces GitCapabilities flags and name mapping helpers.
src/shared/Core/GenericOAuthConfig.cs Updates generic OAuth config detection to use GitRequest.
src/shared/Core/GenericHostProvider.cs Ports generic provider to the new request/response types and capability plumbing.
src/shared/Core/Constants.cs Adds protocol constants and SupportedCapabilities.
src/shared/Core/Commands/StoreCommand.cs Updates Store command to GitRequest and hides it from --help.
src/shared/Core/Commands/GitCommandBase.cs Renames the base “minimum args” validation to EnsureMinimumRequest and uses GitRequest.
src/shared/Core/Commands/GetCommand.cs Implements negotiation echo + quit=1/yield behavior + state/continue gated emission.
src/shared/Core/Commands/EraseCommand.cs Updates Erase command to GitRequest and hides it from --help.
src/shared/Core/Commands/CapabilityCommand.cs Adds the new capability action implementation.
src/shared/Core/Application.cs Registers the new CapabilityCommand.
src/shared/Core.Tests/InputArgumentsTests.cs Removes the old InputArguments test suite (superseded by GitRequestTests).
src/shared/Core.Tests/HostProviderTests.cs Updates core host provider tests to GitRequest.
src/shared/Core.Tests/HostProviderRegistryTests.cs Updates registry tests to GitRequest and helper rename.
src/shared/Core.Tests/GitStateValidationTests.cs Adds validation unit tests for state key/value rules.
src/shared/Core.Tests/GitResponseTests.cs Adds unit tests for GitResponse shape/state behavior.
src/shared/Core.Tests/GitRequestTests.cs Adds unit tests for GitRequest capabilities/state parsing and existing argument parsing.
src/shared/Core.Tests/GitCapabilitiesTests.cs Adds unit tests for capability parsing/rendering and advertised set.
src/shared/Core.Tests/GenericOAuthConfigTests.cs Updates OAuth config tests to use GitRequest.
src/shared/Core.Tests/GenericHostProviderTests.cs Updates generic provider tests to the new request/response surface.
src/shared/Core.Tests/Commands/StoreCommandTests.cs Updates Store command tests to use GitRequest.
src/shared/Core.Tests/Commands/GitCommandBaseTests.cs Updates GitCommandBase tests to use GitRequest.
src/shared/Core.Tests/Commands/GetCommandTests.cs Expands Get command tests for negotiation, cancel/yield, state/continue gating, and ordering.
src/shared/Core.Tests/Commands/EraseCommandTests.cs Updates Erase command tests to use GitRequest.
src/shared/Core.Tests/Commands/CapabilityCommandTests.cs Adds tests for capability action output and stdin-independence.
src/shared/Atlassian.Bitbucket/OAuth2ClientRegistry.cs Updates OAuth2 client registry selection to use GitRequest.
src/shared/Atlassian.Bitbucket/IRegistry.cs Updates registry interface to accept GitRequest.
src/shared/Atlassian.Bitbucket/BitbucketRestApiRegistry.cs Updates Bitbucket REST API registry selection to use GitRequest.
src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs Updates refresh-token service-name derivation to use GitRequest.
src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs Ports Bitbucket provider to GitRequest/GitResponse.
src/shared/Atlassian.Bitbucket/BitbucketHelper.cs Updates helper API to accept GitRequest.
src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs Updates Bitbucket authentication interface/impl to use GitRequest.
src/shared/Atlassian.Bitbucket.Tests/OAuth2ClientRegistryTest.cs Updates OAuth2 registry tests to use GitRequest.
src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs Updates refresh-token service-name tests to use GitRequest.
src/shared/Atlassian.Bitbucket.Tests/BitbucketRestApiRegistryTest.cs Updates REST API registry tests to use GitRequest.
src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs Updates Bitbucket provider tests to use GitRequest/GitResponse.
docs/hostprovider.md Updates host-provider docs references from InputArguments to GitRequest.
docs/architecture.md Updates architecture docs references from InputArguments to GitRequest.
Comments suppressed due to low confidence (1)

docs/architecture.md:225

  • This documentation still states that GetCredentialAsync returns an ICredential, but the PR changes the contract to return a GitResponse (which may be Ok/Continue/Cancel/Yield). Update the docs so implementers follow the new API correctly.
`GitRequest` contains the request information passed over standard input
from Git/the caller; the same as was passed to `IsSupported`.

The return value for the `get` operation must be an `ICredential` that Git can
use to complete authentication.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/shared/Core/GitCapabilities.cs
Comment thread src/shared/Core/GitResponse.cs
Comment thread src/shared/Core/GitStateValidation.cs Outdated
@mjcheetham mjcheetham force-pushed the new-protocol branch 3 times, most recently from bfb699d to 47cab99 Compare June 29, 2026 09:35
Drop the .NET Framework target (net472) from all projects and build
scripts. Windows builds will now target the, only, TFM of `net10.0`.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Remove all code that was conditional on a .NET Framework target. Now
that we're only building against the CoreCLR, this code was dead.

Note that we now lose the ability to use the embedded web view for
Microsoft authentication, which was a .NET Framework-only feature (and
therefore Windows only). Support for embedded web view flows will be
restored at a later date via Avalonia WebView.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Add the SupportedOSPlatform attributes to types that only run on the
relevant specific OS platform. This will help ensure we're always gating
use of OS-specific behaviour to that specific OS.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Suppress the "This call site is reachable on all platforms" warning
for all test projects - we use custom Xunit attributes to dynamically
skip OS-specific tests unless the test is running on the appropriate OS.
The analyzer for CA1416 does not know how to handle this.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Upgrade all Avalonia packages to the latest version, which is 12.0.5 at
time of writing.

Version 12.x has some breaking changes that we must react to, including
a required and explicit `x:DataType` specified for the now default
compiled bindings in all views. There have also been changes to how the
clipboard is accessed, as well as hotkeys. Finally some properties have
been renamed and reworked, specifically on the customising window
chrome/decorations and control placeholder text.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
In order to support ahead-of-time (AOT) compilation we must avoid using
reflection-based APIs. JSON serialisation is one of those APIs that we
use a lot across the product.

System.Text.Json supports source generation-based serialisation. We opt
into this by creating explict 'JSON contexts' for each area that define
the set of types we should generate serialisers for, including custom
serialisation options (case sensitivity, naming conventions, etc).

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Enable JSON source generation for Trace2 messages. This will further
help unlock AOT compilation options which cannot use reflection-based
APIs.

Replace the custom JSON naming policy with the built-in lower snake case
policy, and for good measure also explictly set the enum string values
as attributes on the members themselves.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Enable AOT compilation on publish for Git Credential Manager by default!

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
@mjcheetham mjcheetham marked this pull request as draft June 29, 2026 13:42
Enabling Native AOT makes the Linux CI jobs link a native binary for
the target runtime. Cross-linking the Arm runtimes from the x64
ubuntu-latest runner has no target toolchain, so the linux-arm job
fails at the clang link step.

Run the Arm matrix entries on GitHub's Arm64 runners so linux-arm64
links natively, and install the armhf GNU toolchain
(gcc-arm-linux-gnueabihf, which pulls in binutils and the sysroot) for
the 32-bit linux-arm target, which still cross-compiles on the Arm64
host.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Native AOT links the published binary with clang. Leaving it on for a
source install would force every distro to install a C toolchain just
to build a local copy, which is why the install-from-source validation
fails across the board with "gcc or clang is missing".

Installing from source does not need a native binary, so turn AOT off
for it alone: set PublishAot=false in the environment for the build
(the executable project honours an externally supplied value, and the
nested publish inherits it through the packaging scripts). With AOT
off, layout.sh's existing self-contained, single-file publish produces
a portable binary that needs only the .NET SDK to build and bundles its
own runtime to run - so it launches even on the distros where the SDK
was installed to a non-standard location. The shipped packages set
nothing and still default to AOT.

This is a deliberate stopgap that leaves the rest of the
install-from-source flow as-is; a later commit reworks
install-from-source handling altogether.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The src/shared directory is a relic of when code was split between
platform-specific src/{windows,osx,linux} trees and a separate shared
tree. Every library and test project is cross-platform now, so the
extra "shared" level only adds noise to project paths and solution
entries.

Lift all libraries and their test projects up one level into src/.
This is a pure move: project contents and their relative references to
one another are unchanged, so the assemblies that get built are
identical.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
With the shared projects flattened into src/, move the
Directory.Build.props that scoped them up to src/ so it keeps applying
to exactly the same set of projects.

Drop the manual PlatformOutPath/BaseOutputPath overrides while here:
output redirection is being centralized via UseArtifactsOutput in the
root props (see later commit), so per-tree output paths are no longer
needed. Only the chained import of the repository-root props remains.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Rename the executable project from Git-Credential-Manager to
git-credential-manager so the project, its directory and the output
binary all match the git-credential-manager command name and the
lowercase naming used across the rest of the build.

This is a pure move; the project file is unchanged apart from its
path and filename casing.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The DotnetTool project existed only to repackage the executable as a
.NET tool, carrying its own csproj, DotnetToolSettings.xml, nuspec and
layout/pack scripts alongside the real project.

Its packaging responsibilities move into the reworked build system
introduced in the following commits, so remove the project. Preserve the
tool icon by moving it next to the executable project.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Collect the custom MSBuild tasks - GetVersion and
GenerateWindowsAppManifest - together with their project and the .tasks
import file under a dedicated build/msbuild/ directory, and rename the
project to MSBuildTasks.

This frees the top level of build/ for the platform packaging that
follows and gives the tasks assembly a clearer home. Point the root
Directory.Build.targets at the new import path and thread a
VersionOverride through to the GetVersion task.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Add lib-cli.sh and its PowerShell counterpart lib-cli.psm1: small
helper libraries that the per-platform build/publish/pack/archive scripts
introduced next will source. They centralize logging, repository and
artifacts path resolution, and runtime/version/configuration
normalisation (plus Inno Setup discovery on Windows).

Keeping this logic in one place per shell keeps the platform scripts
thin and consistent, and gives them a single source of truth for things
like the verbose-output flag and the out/ layout.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Replace the old src/linux/Packaging.Linux project and its
build/layout/pack scripts with a build/linux layout. A
Linux.Distribution.csproj (a NoTargets project) anchors packaging in the
solution, while thin publish/pack/archive scripts built on the shared
lib-cli library do the actual work, replacing the previous monolithic
layout.sh and pack.sh.

The Debian control file moves under debian-package/, and
install-from-source.sh carries over unchanged.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Replace src/osx/Installer.Mac with a build/macos layout mirroring the
Linux one: a Mac.Distribution.csproj (NoTargets) plus
publish/pack/archive/codesign scripts and an import-developer-certificate
helper, all built on the shared lib-cli library. This subsumes the old
build/layout/pack/dist/notarize/codesign scripts.

The installer's distribution XML, resources, postinstall script,
entitlements and uninstall.sh move under build/macos, with the .pkg
pieces grouped under installer/.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Replace src/windows/Installer.Windows with a build/windows layout
matching the other platforms: a Windows.Distribution.csproj (NoTargets)
plus publish/pack/archive PowerShell scripts built on lib-cli.psm1, and a
download-innosetup helper that fetches the Inno Setup compiler on demand.
The Setup.iss installer script moves under installer/.

Bump the Inno Setup package to 6.7.3 and drop its now-unused
GeneratePathProperty: the compiler is resolved by the download helper at
build time rather than from the NuGet package path.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Replace the legacy MSBuild .sln with the newer .slnx solution format.
SLNX is terser, diffs and merges far more cleanly, and lists projects by
path without the GUID bookkeeping of the old format. The new solution
references the flattened src/ projects and the build/ distribution
projects in their new homes.

Rename the ReSharper DotSettings sidecar to match the new lowercase
solution name so it keeps applying.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Turn on UseArtifactsOutput so every project's binaries and
intermediates land under out/ in the standard .NET artifacts layout
(replacing the per-tree output paths removed earlier), and default all
projects to net10.0 - individual projects may still override.

Bump the SDK in global.json to 10.0 and register the
Microsoft.Build.NoTargets MSBuild SDK used by the new distribution
projects. Normalise VERSION to the three-part 3.0.0.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Point the Azure Pipelines release definition and the GitHub Actions
workflows at the new build/ script entry points (publish/pack/archive,
import-developer-certificate) and the flattened project paths, replacing
references to the old src/{linux,osx,windows} packaging projects and
scripts.

Add the ESRP sign.yml template that the release pipeline invokes to
code-sign the Windows and macOS artifacts.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Refresh the development guide to match the reworked build: refer to
git-credential-manager.slnx, document building each platform's
distributables through the build/<os> projects with --configuration and
--runtime, and point at the new out/package and out/publish output
locations.

Add a note explaining why building the macOS distribution with
Homebrew's non-portable .NET SDK fails at the Native AOT link step, and
recommend using the official SDK instead.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The Linux, Mac and Windows *.Distribution.csproj projects each run
their platform build.sh / publish pipeline from a target that fires
AfterTargets="Build". Because they sat in the solution default build
set, a plain `dotnet build` (or `dotnet test`) on
git-credential-manager.slnx kicked off the full publish-and-package
pipeline on the matching OS - slow, and with side effects no inner-loop
build should have.

Mark the three distribution projects with <Build Project="false" />
so a solution build covers just the product and its tests. They stay in
the solution (visible and loadable in IDEs) and are still built directly
via `dotnet build build/<os>`, which is how CI and a deliberate
distribution build invoke them.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Publishing always produced an ahead-of-time (AOT) build, since the
project sets PublishAot=true. AOT needs a native toolchain (clang, zlib)
to link, which is fine for the shipped package but a heavy requirement
for building from source.

Add an --aot / --no-aot toggle to the Linux and macOS publish.sh
scripts (and an equivalent -Aot switch to the Windows publish.ps1),
defaulting to AOT so the shipped package is unchanged. --no-aot publishes
a trimmed, self-contained build that needs only the .NET SDK.

The boolean --x / --no-x parsing lives in a new bool_flag helper in
lib-cli.sh for reuse. Trimming for the non-AOT build is configured in the
project (PublishTrimmed, gated on PublishAot being off) rather than passed
on the command line; the product is already AOT-safe, hence trim-safe.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The Linux install-from-source script built through the deleted
src/linux/Packaging.Linux project (its InstallFromSource MSBuild path)
and detected a checkout using the old src/linux paths and the .sln file,
so it stopped working after the build-system rework.

Promote it to a single cross-platform build/install-from-source.sh:
detect the OS, bootstrap dependencies (Linux distro package managers, or
git plus the .NET SDK on macOS), publish through the per-OS
build/<os>/publish.sh, and stage the result under <prefix>/share/gcm-core
with a launcher symlink in <prefix>/bin - the same layout the .deb uses.

Default to a trimmed, self-contained non-AOT build (needs only the
.NET SDK); --aot opts into a native build (and the clang/zlib toolchain
on Linux). Elevate the install with sudo only when the prefix is not
writable, so user-prefix installs need no root.

Also fix two latent bugs carried over from the old script: the
SDK-detection test broke when multiple SDKs were installed (forcing a
needless dotnet re-download), and the dotnet bootstrap changed the
working directory, which broke repo detection.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The validate-install-from-source workflow invoked the script from its
old src/linux/Packaging.Linux location, which no longer exists after the
rework. Point it at the new build/install-from-source.sh.

Add a macOS job so the cross-platform script is exercised on macOS as
well as the Linux distro matrix. The default non-AOT build needs only the
.NET SDK, so it runs without a native toolchain on the runner.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The install-from-source script moved from src/linux/Packaging.Linux to
a cross-platform build/install-from-source.sh and gained an --aot toggle
that defaults to a trimmed, self-contained non-AOT build.

Fix the now-broken script link in the uninstall guide, document the
--aot option, and note that the script also runs on macOS.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The Install/Update/Uninstall subsections under the "## .NET tool"
heading were h4 (####) while their parent is h2, leaving a gap in the
heading hierarchy. Promote them to h3 (###) to match the sibling
sections.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The standalone DotnetTool project was dropped during the build-system
rework. Restore the ability to produce the git-credential-manager .NET tool
NuGet package, placing it under build/dntool to match the per-platform build
layout.

publish.sh publishes the application as portable, framework-dependent IL
(no AOT, trimming, runtime identifier or apphost); pack.sh then runs dotnet
pack with the no-build option, pointing PublishDir at the published layout so
that assemblies code-signed between the two steps are packaged as-is. The SDK
generates DotnetToolSettings.xml and a symbol package, so no hand-written
nuspec is required.

Dntool.Distribution.csproj is a NoTargets project that carries only the
tool and package metadata; it is driven by the scripts and marked no-build in
the solution, like the other distribution projects.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Add a job to the CI workflow that builds the git-credential-manager .NET
tool package (unsigned) via build/dntool/build.sh and uploads the resulting
.nupkg/.snupkg, so the tool packaging is exercised on every push alongside the
per-platform distributables.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The .NET tool was dropped from the release pipeline during the
build-system rework, leaving a commented stub that referenced the removed
src/shared/DotnetTool scripts.

Add a dotnet_tool build job that publishes the framework-dependent IL via
build/dntool/publish.sh, ESRP-signs the managed assemblies (SigntoolSign),
then packs the signed layout into the .nupkg/.snupkg via build/dntool/pack.sh.
Re-enable the NuGet publish job to push the package.

Signing is performed by the ESRP service rather than the agent, so the
build job runs on the existing Linux pool alongside the other platform
builds.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Add a .NET tool section to the development guide: how to build the package
with build/dntool/build.sh (it is script-built rather than via dotnet build,
being platform-agnostic) and how to install the result into an isolated tool
path to try it.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The Windows release jobs publish the executable with Native AOT, which
links the final binary with the MSVC linker (link.exe). The 1ES hosted
images do not ship the C++ toolchain, so every Windows leg fails with
"Platform linker not found" from Microsoft.NETCore.Native.Windows.targets.

Add a setup script that installs VS 2022 Build Tools with the single
VC.Tools component for the agent's architecture - x86.x64 on the Intel
legs (win-x86, win-x64), ARM64 on the Arm leg (win-arm64) - and run it
before the publish step. Installing just the architecture's component
keeps the download to the minimum that still provides link.exe and the
MSVC libraries; the .NET ILCompiler then discovers the toolchain through
the Visual Studio setup API.

Assisted-by: Claude Opus 4.8
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
The type that wraps the key/value lines Git writes over standard
input is the natural anchor for everything a host provider needs
to know about a single credential helper invocation. Calling it
"InputArguments" frames it as a low-level argument bag and leaves
nowhere obvious to add things like the negotiated Git credential
protocol capability set or the soon-to-be-introduced state[] /
continue payloads.

Rename the type to GitRequest so the type's name matches its
conceptual role, and rename the matching `input` parameters and
local variables to `request` so the calling code reads as
"operate on the request" rather than holding onto the old
InputArguments framing. Tests, docs, and every host provider
implementation move in lockstep. This commit is a pure rename:
no methods or properties change shape, and no behaviour changes.

The follow-up commit will hang the new capability/response
infrastructure off the renamed type.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Assisted-by: Claude Opus 4.7
Git 2.46 added a credential helper protocol capability handshake
(capability[] lines), gating new fields like state[], continue,
authtype, credential, ephemeral, password_expiry_utc, and
oauth_refresh_token. None of those are wired up yet, but the host
provider surface should grow a place for them to land without
churning IHostProvider again.

Introduce a GitCapabilities [Flags] enum and a GitCapabilitiesExtensions
helper that knows how to parse incoming capability names, advertise
GCM's own supported set (currently empty), and render flags to their
on-the-wire protocol names. Surface a typed Capabilities property on
GitRequest that lazily parses the capability[] lines Git wrote.

Replace the old GetCredentialResult bag with a GitResponse type that
keeps the existing AdditionalProperties escape hatch (used today only
by GenericHostProvider for the ntlm=allow signal) and leaves room for
typed capability-gated properties to be added one at a time.

Wire the negotiation handshake through GetCommand end-to-end: the
intersection of what Git advertised and what GCM advertises is echoed
back as capability[]= lines on the response. The intersection is
empty for now, so no capability lines appear in practice, but the
plumbing is in place. Also fix a latent issue in the output path so
empty username/password values (the WIA signal from GenericHostProvider)
round-trip correctly: emit scalar fields via a string dictionary
rather than the multi-value writer that normalises away empty values.

Store and erase deliberately remain Task-returning: per the Git
credential protocol, neither action emits output, so a response type
would be vestigial.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Assisted-by: Claude Opus 4.7
Git 2.46 added a separate `capability` action to the credential
helper protocol, distinct from the get/store/erase key=value flow.
A caller (typically Git itself) invokes a helper with the
`capability` argument and reads a fixed-format response from
standard output to discover which protocol capabilities the helper
understands, without having to make a real credential request.

The response format is:

    version 0
    capability <name>
    ...

A non-zero exit, or a first line that does not begin with the
literal `version ` and a space, is treated by callers as a signal
that the helper supports no capabilities.

Implement this action as a new CapabilityCommand that does not
read stdin and does not pick a host provider (capabilities are a
global property of the helper, not per-host). The advertised set
is taken from GitCapabilitiesExtensions.Advertised, which today
is GitCapabilities.None, so the command prints just `version 0`.
The infrastructure is in place so that adding a new flag to the
advertised set is the one change required to start announcing it
through both this action and the inline negotiation handshake on
`get`.

Extract the capability-flags-to-protocol-names projection from
GetCommand into GitCapabilitiesExtensions.ToProtocolNames so both
the inline handshake and the standalone action share one
implementation.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Assisted-by: Claude Opus 4.7
`get`, `store`, `erase`, and `capability` are entry points used by
Git itself over the credential helper protocol. They are not
user-facing commands: invoking them by hand requires writing the
key=value protocol on standard input, and they have no useful
behaviour outside of being called by Git. Listing them in
`git-credential-manager --help` is just noise that distracts from
the commands a human actually runs (configure, unconfigure,
diagnose, and the per-provider subcommands).

Set IsHidden = true on each of the four commands. They remain
fully invokable (Git's calls are unaffected), they just no longer
appear in the top-level help listing.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Assisted-by: Claude Opus 4.7
When a provider can produce a credential it returns one. When it
deliberately can't (the user closed an auth prompt, no eligible
account was found, etc.) it currently has to throw, which routes
through the generic top-level exception handler and surfaces as
"fatal: ..." on stderr with exit -1. That conflates "the provider
decided not to authenticate" with "something went unexpectedly
wrong", and forces every "no credential" path through an
exception.

Give providers a non-exceptional way to express both outcomes:

* GitResponse.Ok(credential) -- successful response; same shape as
  the existing public constructor, just named for the intent.

* GitResponse.Cancel() -- cancellation response; carries no
  credential and tells the command layer to emit `quit=1` on
  standard output, which is the Git credential helper protocol's
  signal to abort the credential acquisition pipeline. Git
  responds by terminating the operation immediately, rather than
  falling back to an interactive terminal prompt that would just
  re-ask the user who already cancelled in a GUI dialog.

Enforce the invariant in GitResponse that cancelled responses
cannot carry a credential and that non-cancelled responses must.
The existing 1-arg constructor is retained as an alias for Ok so
in-tree providers keep compiling; migrating them to the factories
is the next commit.

Exit-code plumbing is deliberately not touched: `quit=1` is the
in-band protocol signal that the protocol actually defines for
this case, and Git's `die()` on receiving it makes the helper's
exit code irrelevant. Reserving non-zero exit for genuinely
unexpected internal errors (which already throw and route through
Application.OnException) keeps the two channels semantically
distinct.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Assisted-by: Claude Opus 4.7
GitResponse already distinguishes "I produced a credential" (Ok)
from "stop the whole acquisition pipeline" (Cancel, wired as
quit=1). There is a third shape the credential helper protocol
expects: "I have nothing to contribute for this request, but I'm
not stopping you -- please try other helpers or fall back to your
interactive prompt." Today providers have no clean way to say
this; they either throw (wrong: a no-op is not an error) or
construct a response with empty credentials (wrong: that's the
WIA signal and gets stored).

Add a Yield() factory that returns a response whose IsYielded
flag is set. The command layer translates it into an empty
response on standard output: just the terminating blank line, no
credential fields, no quit signal. Git then proceeds to the next
helper in the chain or to its built-in interactive prompt --
which is the exact behaviour Git defaults to when a helper
returns nothing, so this is the most polite "I'm out" a helper
can send.

Keep the response shape constructor enforcing that Ok / Cancel /
Yield are mutually exclusive: a response carrying a credential
cannot be cancelled or yielded, and a response cannot be both
cancelled and yielded at once. AdditionalProperties continue to
be ignored on the non-Ok shapes.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Assisted-by: Claude Opus 4.7
Git 2.46 added the `state` capability to the credential helper
protocol, gating two new attributes:

  state[]   -- opaque per-helper key/value pairs Git stores between
               invocations and replays back when calling the same
               helper, so providers can carry context across the
               get + store/erase command cycle without their own
               sidecar storage.

  continue  -- a boolean signal from helper to Git indicating that
               the credential just returned is a non-final part of
               a multistage authentication flow; Git is expected to
               call the helper again after a follow-up 401.

This is the marquee feature behind issue git-ecosystem#2057. It unlocks
optimistic account selection (try one account, remember the
choice in state, fall through to a different account on 401),
multistage authentication for NTLM/Kerberos-style flows, and any
provider scenario that benefits from per-request memory.

Surface
-------

* GitCapabilities.State flag added and included in the advertised
  set so the negotiation handshake is functional end-to-end.

* GitRequest.State exposes a lazy IReadOnlyDictionary<string,string>
  of incoming state. Only entries whose key begins with the
  reserved `gcm.` prefix are kept (per the protocol's "ignore
  values that don't match its prefix" rule); the prefix is
  stripped from dictionary keys. Malformed entries are silently
  discarded.

* GitResponse grows a fourth shape: Continue(credential) returns
  a response that carries a credential and signals `continue=1`.
  The shape matrix is now Ok / Continue / Cancel / Yield, all
  mutually exclusive, enforced by the constructor.

* GitResponse adds a curated state surface:
    State    -- IReadOnlyDictionary<string,string> view for reads
                and enumeration; standard IDictionary patterns
                (indexer, TryGetValue, ContainsKey, foreach) work.
    SetState -- the single mutation path; validates every entry
                and silently no-ops on Cancel/Yield.
    WithState -- fluent equivalent of SetState that returns the
                 same instance for chaining at the return site.

  There is no IDictionary mutation surface and no GetState/TryGetState
  forwarder: writes always go through the validating method, reads
  go through the dictionary view. Smaller API surface, no
  duplicated semantics.

* SetState always validates key and value against the wire
  protocol rules (no '=' in key, no newline or NUL anywhere, no
  empty key, no leading `gcm.` prefix) and throws ArgumentException
  on violations regardless of response shape: those are
  programming errors that should surface at the call site rather
  than being silently dropped on the wire.

* On Cancel and Yield shapes SetState then silently no-ops:
  state has no meaning when no credential is being returned, so
  providers that build a response speculatively and then switch
  shape don't have to remember to strip state.

* Constants.CredentialProtocol gains StateKey, ContinueKey, and
  GcmStatePrefix so the wire vocabulary lives in one place.

Wire emission
-------------

GetCommand writes its response in protocol order: capability[]
directives first (the spec requires these precede any value
depending on them), then scalar fields (protocol/host/path/
username/password and AdditionalProperties), then continue=1,
then state[]= entries, then the terminating blank line.

state[] and continue are gated on the negotiated `state`
capability. If a provider sets either but the capability was not
negotiated with Git, both are silently dropped with a trace
message. Dropping continue is loud in the trace specifically
because it changes auth semantics: Git will treat the credential
as final and likely fail on the next 401.

Out of scope
------------

No in-tree provider produces state or continue yet. Wiring
specific scenarios (AzureRepos multi-account MSAL, GitHub
account selection, etc.) is left to follow-up commits that can
focus on each scenario's design without needing to also land
infrastructure.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Assisted-by: Claude Opus 4.7
When `aab6fef` ("github: merge .UI.Avn and .UI in to GitHub") folded
the separate `.UI.Avalonia` and `.UI` projects back into the main
GitHub project, it deleted the sibling
{Credentials,DeviceCode,TwoFactor}CommandImpl.cs files that lived
under `src/shared/GitHub.UI.Avalonia/Commands/`. The
`SelectAccountCommandImpl.cs` that had been added two months earlier
in `483d6d3` ("github: add prompt to select accounts") was missed.

That left a lone source file under a directory belonging to a
project that no longer exists: no `.csproj`, no solution entry, no
consumer instantiates it. `GitHubAuthentication.ShowSelectAccountPromptAsync`
calls `AvaloniaUi.ShowViewAsync<SelectAccountView>` directly rather
than going through the helper-command process, so deleting the file
is a true no-op.

Remove the orphan file and its directory, completing what `aab6fef`
started. The abstract `SelectAccountCommand` HelperCommand base in
`GitHub/UI/Commands/` is left in place — it is dead code along with
its three abstract siblings, but that is a pre-existing condition
out of scope for this commit.

Assisted-by: Claude Opus 4.7
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gcm3.0 Targets the next major release of Git Credential Manager - v3.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants