Worktree verbs, single sign-on URL, and OAuth device flow - #118
Merged
Merged
Conversation
Three additions a consuming desktop launcher needs and this library does not have: worktree list/add/remove/prune, repository enumeration that can see an organisation's private repositories, and a way to obtain a GitHub credential rather than only resolve one. All additive, targeting 3.2.0. Also ignores .worktrees/, matching the convention used for isolated feature work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eight tasks over the 2026-09-21 design, test-first throughout. Resolves the spec's one open question: Octokit 14.0.0 does expose InitiateDeviceFlow and CreateAccessTokenForDeviceFlow, and the latter polls internally, so the device flow is a translation layer rather than two hand-rolled posts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ktsu.Sdk's Sdk.targets copies its own packaged gitignore template over SolutionDir/.gitignore on every build, so an entry added here does not survive. The worktree directory is excluded through .git/info/exclude instead, which is local, uncommitted, and not SDK-managed.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…onse Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
No user.email is configured on this machine, so git derives one from the hostname: commits land as matt@Mac.home or matt@MattBookPro.local rather than under an address .mailmap already knows. Both now resolve to the same identity as every other alias, which also cleans up the matt@MattBookPro.local commits already on main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Address review findings on the OAuth device flow: WaitForTokenAsync now tracks an elapsed-time budget from a monotonic clock and throws once the device code's expiry passes, rather than polling forever against a server that never resolves. Every wait between polls is floored to GitHub's documented 5-second minimum, applied at the point the wait is issued so a hand-built GitHubDeviceCode is covered as well as a parsed one. Also stops echoing the token endpoint's response body into a failure message, and adds coverage for the pending/slow-down/expiry/floor/cancellation paths the poll loop previously left untested. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extends the README's Features bullets and adds three usage examples for the worktree verbs, GitHubProvider's OwnerKind routing, and the GitHubDeviceFlow sign-in. Also corrects the design doc's Transport and polling section, which still described the device flow as wrapping Octokit's OauthClient; it posts directly to GitHub's device and token endpoints instead, because Octokit percent-encodes the read:org scope's colon and collapses OAuth error bodies into ApiException. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes the GitHubProvider reference entry, which still claimed GetRepositoriesAsync is public-only after OwnerKind made that conditional. Adds the missing worktree rows across the GitRepository, Verb Builders, and Result Models tables, a GitHubDeviceFlow reference section, and GitHubOAuthClientId to Semantic Types. Also corrects the non-compiling CredentialWithToken.Token assignment carried over into the design doc and GitHubDeviceFlow's own XML remarks. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RequestDeviceCodeAsync now validates verification_uri with Uri.TryCreate(..., UriKind.Absolute, ...) instead of letting a malformed value throw UriFormatException past this type's documented GitHostingRequestException-only failure surface. WaitForTokenAsync's error switch now distinguishes access_denied/expired_token (authentication failure) from incorrect_client_credentials/unsupported_grant_type/ device_flow_disabled and any unrecognised code (request failure, since none of those can be fixed by retrying sign-in), matching the design spec's Failures table, which is amended to document the device_flow_disabled and unrecognised-code additions. Also renames NowTicks to NowMilliseconds to name it by its unit rather than by the member it happens to default to, stops a future edit from reintroducing the token endpoint's response body into its failure message unnoticed, and fixes the org-repositories fixture's owner.type to Organization. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Merges origin/main, which brought in PR #115's GitHub repository enumeration routing. Our own OwnerKind-based routing on this branch solved the same problem and is dropped in favour of upstream's approach, per the owner's decision. Upstream infers the owner's account type from GET /users/{login} rather than taking an explicit kind from the caller, and it handles three cases ours did not: an unauthenticated provider skips the type probe entirely and calls the public route directly; a GitHub App installation token whose GET /user answers 403 falls back to the public route instead of failing; and the routing decision is never inferred from a 404, which an SSO-blocked organisation answers just as an absent one would, so ours risked silently under-reporting an organisation's repositories. Removed the GitHubOwnerKind enum, the OwnerKind property, and their tests and documentation. The worktree verbs, the X-GitHub-SSO authorisation URL in failure messages, and GitHubDeviceFlow are untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three findings from the code quality review. The device flow's `case null when string.IsNullOrEmpty(parsed.AccessToken)` tested a constant: the preceding `case string` arm consumes every non-null error, so the error is provably null by the time control reaches it. The null test is dropped and the token check moves into the default arm, which also lets the null-forgiveness operator go, since the compiler narrows AccessToken through the plain check where it could not through the pattern. The two loops in GitWorktreeParser.Parse and TryGetSingleSignOnUrl each remapped their iteration variable on the first line of the body. Both now map at the enumeration source instead. No behaviour changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.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.



Three additive capabilities a consuming desktop launcher needs and this library did not have. No existing member changes shape, and no existing behaviour changes for a caller that does not opt in.
Worktree verbs
Worktrees(),AddWorktree(path),RemoveWorktree(path)andPruneWorktrees()onGitRepository, with aGitWorktreemodel and a parser forgit worktree list --porcelain. The library previously had no worktree verb at all —GitProbes.IsWorkTreeAsyncasks whether a path is a working tree andGitStatusEntry.WorkTreeStatenames half a status code, but neither creates, lists, or removes anything.GitWorktree.IsMainis positional: git's porcelain has no attribute for it, the main working tree is simply the first record. It exists so a caller can refuse to remove the worktree that owns the repository.CheckingOutandFromwrite the same<commit-ish>operand, so they are one field under two names and the later call wins — the same last-call-wins rule that governs the four mode settings, matchingIGitBranchListBuilder's existingLocalOnly/RemoteOnly.Deliberately out of scope:
worktree move,lockandunlock, and--dry-runon prune.Single sign-on authorisation URL
A
403carryingX-GitHub-SSOnow surfaces the authorisation URL in the exception message. Previously a token one click away from working was indistinguishable from a bad one, and the fix was a link the caller could not see.The header is read case-insensitively by scanning rather than by keyed lookup, matching how
TryGetRetryAfterSecondsalready readsRetry-Afterand for the same reason. The URL is only ever appended — a403without the header produces exactly the exception it produced before, and the arm ordering inTranslateis untouched.This reaches all three of
Translate's call sites, so pull request listing and creation benefit from it as well as enumeration.OAuth device flow
GitHubDeviceFlowobtains a credential rather than only resolving one. Two calls rather than one, because the pause between them is minutes long and the user code must stay on screen throughout; a single call taking a callback would fire it from an HTTP continuation and force every graphical caller to marshal back to a UI thread. It stores nothing — the caller decides where the secret lives.Built on a raw
HttpClientrather than Octokit'sOauthClient. Two problems turned up empirically:InitiateDeviceFlowpercent-encodes the scope colon, andCreateAccessTokenForDeviceFlowthrowsApiExceptionfor an OAutherrorbody instead of returning it — collapsing a refused authorisation and an expired code into the same shape a transport failure gets, which is precisely the distinction this feature exists to make. It mirrorsAzureDevOpsProvider, which already talks rawHttpClientwith a source-generatedJsonSerializerContext. The design document records the reasoning.GitHubDeviceCodecarries bothUserCode(shown to a person) andDeviceCode(sent in the poll). They are not interchangeable.The poll loop enforces a monotonic expiry deadline from
ExpiresInand a five-second interval floor — anintervalof0off the wire would otherwise busy-loop against github.com. Error codes are split deliberately:access_deniedandexpired_tokenare authentication failures, whileincorrect_client_credentials,unsupported_grant_typeanddevice_flow_disabledare request failures, so a deleted client ID surfaces as a configuration fault rather than looping a user through a sign-in that can never succeed.Not yet verified end to end
No GitHub OAuth App exists for this yet, so every device-flow test runs against a fake transport. Registering one — device flow enabled, SSO-approved, scopes
repoandread:org— is what remains before the flow can be confirmed against the real service.One further note: these two endpoints accept a JSON request body although GitHub's documentation shows form encoding. That is relied upon and recorded in the type's remarks.
Testing
668 passing, 0 failing. Build clean on
net10.0andnet9.0, zero warnings, and no[SuppressMessage]added anywhere inGitIntegration/.Device-flow tests inject the delay and clock, so the suite exercises the poll loop's wait, cumulative
slow_downwidening, expiry and cancellation without sleeping.🤖 Generated with Claude Code