Protocol foundation: capability negotiation, GitRequest/GitResponse, state[] + continue#2357
Draft
mjcheetham wants to merge 42 commits into
Draft
Protocol foundation: capability negotiation, GitRequest/GitResponse, state[] + continue#2357mjcheetham wants to merge 42 commits into
GitRequest/GitResponse, state[] + continue#2357mjcheetham wants to merge 42 commits into
Conversation
There was a problem hiding this comment.
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
InputArgumentstoGitRequestand updates the host-provider surface area accordingly. - Introduces
GitResponse(Ok/Continue/Cancel/Yield), capability negotiation plumbing, and the newgit credential capabilityaction. - 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
GetCredentialAsyncreturns anICredential, but the PR changes the contract to return aGitResponse(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.
bfb699d to
47cab99
Compare
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 churningIHostProvideragain.What's in this PR
Eight commits, ordered as a readable story:
hostprovider: rename InputArguments to GitRequestPure rename.
InputArgumentsframed the type as a low-level argument bag;GitRequestmatches its actual role as the anchor for everything a host provider needs to know about a single credential helper invocation. No methods change shape.hostprovider: add capability negotiation and GitResponseIntroduces the
GitCapabilities[Flags]enum and aGitCapabilitiesExtensionshelper that parses incomingcapability[]lines, advertises GCM's own set (empty for now), and renders flags back to wire names. Surfaces a typedCapabilitiesproperty onGitRequest. Replaces the oldGetCredentialResultwith a newGitResponsetype. Wires the handshake throughGetCommandend-to-end — the intersection of what Git and GCM advertised is echoed back on the response (currently empty, so nocapability[]lines appear in practice, but the plumbing is live).hostprovider: add 'git credential capability' actionGit 2.46 also added a
capabilityaction distinct from get/store/erase. Implements it asCapabilityCommand(no stdin, no provider selection — capabilities are a global property of the helper). Today emits justversion 0; advertised capabilities will appear here automatically as flags are added toGitCapabilitiesExtensions.Advertised.commands: hide Git credential helper actions from --helpget/store/erase/capabilityare protocol entry points for Git, not user-facing commands. Mark themIsHidden = truesogit-credential-manager --helpno longer lists them alongside the commands a human actually runs (configure, diagnose, per-provider subcommands).response: add Ok/Cancel factories and wire quit=1Gives providers a non-exceptional way to express "user cancelled the prompt" / "no eligible account".
GitResponse.Cancel()emitsquit=1so 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.response: add Yield() for helpers that have nothing to offerThird 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).
hostprovider: wire the state and continue protocol capabilityThe marquee feature. Adds the
Stateflag to the advertised set, surfacesGitRequest.Stateas a lazyIReadOnlyDictionary<string,string>filtered to the reservedgcm.prefix, and growsGitResponsewith a fourth shape:Continue(credential)returns a credential while signalling that another exchange is expected. State writes go through a single validatingSetState(or the fluentWithState); reads through the read-only view. The shape matrix becomesOk / 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.github: delete stranded SelectAccountCommandImplCleanup that fell off the bottom of a previous refactor (
aab6feffolded.UI.Avaloniainto.UIbut 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[]+continuethe 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
getresponse and persisted on the nextstore— 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[], soGitRequest.CapabilitiesisNone, and the helper-side handshake echoes nothing back. Every existing provider path works exactly as today.Existing providers.
The
InputArguments→GitRequestrename is mechanical and complete across the repo; third-party providers building against GCM source will pick up the rename but need no behavioural changes. TheGetCredentialResult→GitResponseswap preserves the existingAdditionalPropertiesescape hatch (still used byGenericHostProviderfor thentlm=allowsignal).quit=1on Cancel.Previously, a cancelled prompt surfaced as
fatal: ...via the top-level exception handler. After this PR, providers that have migrated toGitResponse.Cancel()emitquit=1instead — same end result for the user (no credential), cleaner contract with Git. Providers that still throw are unchanged.Testing
quit=1/ yield emission paths.capabilityaction is verified end-to-end against the documentedversion 0+capability <name>response format.Notes for reviewers
Capabilities.Noneadvertised 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.Closes part of #2057.