Repository navigation
Gitea and Forgejo behind the code tools, with a self-hosted server URL (OP2-84) - #16
Merged
Merged
Conversation
One connector covers both, since Forgejo serves Gitea's /api/v1. It works the way Gmail joined mail: a new ProviderKind.Gitea (stored by name, so no migration), an ICodeProvider per code host, and a CodeServiceRouter that the code tools and the repo resolver get as IGitHubService. The github_* tools keep their names and now serve either host. - GiteaConnectionService takes a server URL (required; there is no public default) in any form a person types, sub-path included, and normalises it to the /api/v1 base. It probes /user with a "token" authorization header and names the fix for 401, 403, 404, or a web page where the API should be. As for GitHub, read is on and write is off. - GiteaService maps Gitea's API onto the same models. "Awaiting my review", "mine", "assigned" and "mentioned" are flags on repos/issues/search. Counts come from X-Total-Count. Per-repository personal searches go by owner and are filtered by name, because the endpoint has no repository filter. Drafts are the draft flag or a "WIP:" title. Contents use "contents" at the root and the raw route for files the API won't inline. - Connections page: an "add Gitea / Forgejo" form, replace-token, and a health check. The repo-names page includes Gitea connections. The "gitea" HTTP client gets the private-CA handler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V7aurnqD63TPgNSBec8xrk
BrewingCoder
added a commit
that referenced
this pull request
Nov 5, 2026
OP2-285: a Google reconnect now forgets the remembered "this account has no readable zone". That answer is cached for an hour so every listing does not make a doomed call, but reconnecting is exactly what grants the Calendar settings scope — the README tells people to do it — so keeping the old answer made the fix appear to do nothing for up to an hour. OP2-84: Normalise cut the server path at the first place the four characters "/api" appeared, not at the /api segment, so a server under a sub-path of its own such as https://example.com/api-git normalised to the wrong root and every call went to https://example.com/api/v1/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V7aurjxxqEYmeNDuAinHyQ
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.
Implements OP2-84: Gitea / Forgejo as a code provider with a self-hosted base URL, covering the whole GitHub tool surface (repos, commits, issues, pull requests, triage, files).
Shape
It plugs in the way Gmail joined mail, so none of the tools have to know which provider they're talking to:
ProviderKind.Giteais one provider for both Gitea and Forgejo, since Forgejo serves the same/api/v1. It's stored by name like the other providers, so no migration.ICodeProvider : IGitHubServiceplusCodeServiceRouter(src/OutPost2.Infrastructure/Code/CodeProviders.cs). These do dispatch only. Two providers registered for one kind is a startup error, and an unsupported kind gets a one-sentenceGitHubExceptioninstead of a call made with the wrong token.GitHubServiceis now oneICodeProvider;IGitHubServiceresolves to the router, soCodeToolsandRepoResolverare unchanged apart from the connection filter.github_*names. Renaming them would break every MCP client already configured. Their descriptions now mention Gitea and Forgejo.GitHubExceptionis still the single exception type the code tools catch, documented as historical.code.readandcode.write. Read starts on and write starts off, as for GitHub.CodeToolsnow acceptsGitHub or Giteaconnections, so the per-provider-default rules apply unchanged.Gitea is not wire-compatible with GitHub: the differences, all in
GiteaServicesearch/issues?q=review-requested:@me …repos/issues/search?type=pulls&review_requested=true(alsocreated,assigned,mentioned).review_requestedneeds Gitea 1.20+; that's in the READMErepo:qualifier in the queryownerand keeps only the matchingrepository.full_name, paging until it has enoughtotal_countin the bodyX-Total-Countheader. Triage counts uselimit=1draftpull_request.drafton newer servers, or aWIP:/[WIP]title. That's Gitea's own convention, and the only signal on older serversper_page=100limit, capped at the server'sMAX_RESPONSE_ITEMS(50 by default). Repository listing stops at the same 500 ceiling as GitHubpushed_atupdated_atmoves on a push, so it's used instead and the list is sorted client-sideAuthorization: BearerAuthorization: token …, which every release acceptsrepos/{repo}/raw/{path}, for files the contents API doesn't inlinecontents/at the rootcontentswith nothing after itUnassigned and all-open questions without a repository are refused, as on GitHub. Repo-scoped all-open and unassigned use
repos/{repo}/issues?type=…. A 409 on commits is an empty repository. 404, 401 and 403 each get their own sentence naming the host.Connecting
GiteaConnectionService.Normalisetakes what a person types:git.example.com, a URL with a trailing slash, the API URL itself, a server under a sub-path (https://example.com/git), orhttp://ip:port. It returns…/api/v1/. A blank address is refused, because there is no public default host.GET /userand gives a fix for each failure:read:userscope, plusread:repositoryandread:issue./versionand logs whether the server is Gitea or Forgejo (Forgejo reports…+gitea-…). Nothing behaves differently based on it.POST /connections/gitea/add, replace-token support, and the health check.giteaHTTP client with the private-CA handler (OP2-45), since these servers are self-hosted by definition.Tests
tests/OutPost2.Tests/Code/GiteaTests.cshas 38 tests, all against mocked HTTP:WIP:.CodeToolschoosing a Gitea connection and answeringgithub_triagefrom it end to end.Full suite (Linux, .NET 10.0.403, local Postgres): 1042 passed, 20 failed, 52 skipped. The 20 failures are the
FormBindingTests/OAuthFlowTestsweb-page tests that fail the same way on unmodifiedmainin this environment (HTTP 500 without Vault/Garage); this branch adds none. Since those page tests can't render here, the new add-connection form hasn't been rendered. It copies the YouTrack and GitHub forms' markup.Not done / worth knowing
LiveGiteaProbein the style ofLiveGitHubProbewould be the next step for someone with a server.user/reposscope: I've relied on it returning the token's owned, collaborator and organisation repositories, but haven't checked that against a live server. If an org's repositories are missing, the fallback is to pageorgs/{org}/reposfor each org fromuser/orgs.code.writeis declared, and off, so a later write tool has a capability to check.mainlocally and they merge without conflicts. The combined suite gives 1128 passed and the same 20 environment failures. Sweep provider text into tool output, fix every gap, and guard it mechanically (OP2-286) #15's hostile-text harness finds tool classes by reflection, so it also exercises the Gitea router path.https://claude.ai/code/session_01V7aurnqD63TPgNSBec8xrk
Generated by Claude Code