Skip to content

feat: Forgejo and Gitea pull request hosts - #683

Merged
Tryanks merged 9 commits into
mainfrom
feat/forgejo-host
Oct 9, 2026
Merged

Tryanks merged 9 commits into
mainfrom
feat/forgejo-host

Conversation

@Tryanks

@Tryanks Tryanks commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Summary

Step 2 of #643 (A7 of #535): Forgejo and Gitea behind the host boundary, the kind-per-host settings, and Settings → Source Control for any kind.

settings.json   github.hosts{host:{enabled,account}}            (legacy input, still loads)
             →  source_control.hosts{authority:{kind,enabled,account}}   kind ∈ github | forgejo | gitea

services::forge::connect → Hosts (dispatcher): key/host → HostKind::of (core: settings, else name:
                           codeberg.org, gitea.com, a "forgejo"/"gitea"/"github" DNS label, else GitHub
                           as before); an absent capability is refused as Rejection::Unsupported
   ├─ github::GitHub        unchanged behaviour; capabilities = ALL
   └─ forgejo::Forgejo      one impl for Forgejo and Gitea, direct REST /api/v1 per authority
        api.rs        token: saved → GITEA_TOKEN (only for GITEA_INSTANCE_URL's server) → `tea login helper get`;
                      anonymous reads of public repos; /api/v1/version decides the API differences
        reads.rs      summary (+checks state), conversation, files (whole .diff, else /files pages; cursor page:size),
                      file text, labels/reviewers candidates, action state, watch detail, activity
        actions.rs    comment, reply*, resolve*, react, edit, labels by id, reviewers, close/reopen,
                      update branch (merge|rebase), merge (merge|squash|rebase), review in one POST;
                      no auto-merge or merge message (nothing reads either back)
        viewed.rs     viewed marks kept by Tcode on the machine, staled by the file's blob in the diff
        repository.rs authorities with port and mount path; https and ssh remotes
        (* Gitea ≥ 1.26 resolves, ≥ 1.27 replies; Forgejo and older Gitea have neither)

core::pull_request  HostKind{of, detect, authority, rule, public host}, FORGEJO/GITEA terms (+ conflicts/checks
                    pages where the host has them), HOSTS=[GITHUB,FORGEJO,GITEA]; linking text names GitHub plus
                    the configured kinds; a thread's MCP tools keep the names they were registered with
runtime             invalid_host validation by kind; SetHostToken/RefreshHostCredentials; per-host status
ui                  source_control_settings.rs: row per host (mark, authority, switch, source line, one notice,
                    write-only token, Remove host for added hosts), Add host dialog (kind menu + kind's rule);
                    every pull_requests.* string a Forgejo/Gitea page reaches names its host (%{host_name});
                    "Open on <host>" names the server; Files says "Viewed marks kept by Tcode"

Evidence

Before: Settings → Source Control listed GitHub hosts only, with a free host field and no validation; a Forgejo or Gitea URL could not be linked.
After:

Desktop: Source Control Desktop: Add host
Desktop: Forgejo/Gitea PR, conversation Desktop: Forgejo/Gitea PR, files Phone: Source Control

Synthetic hosts (git.acme.test:3000/forge, gitea.acme.test) on a fresh profile; the PR page is the scratch repository below, read with GITEA_TOKEN for its server. The conversation shows the thread without Reply/Resolve (Gitea 1.25 has neither); Files shows the Tcode-kept viewed mark (toggling it wrote viewed-marks.json and showed "1 of 1 viewed").

Live reads through Tcode's host (forge::connect driven by a throwaway probe, not committed; counts are forgejo host=… path=… request log lines):

  • codeberg.org (Forgejo 16.0.0-dev+gitea-1.22.0), forgejo/forgejo#14770, anonymous: summary 2 (merged, head branch from the label since the ref is refs/pull/N/head, checks Failing), conversation 8 (1 comment + 2 approvals, 2 labels, 2 reviewers, complete), files 2 (2 files with hunks, whole diff), file text 1 (go.mod, 255 lines), watch detail 2 (17 statuses, 1 failing, head sha), activity 3 (3 remarks), action state 4 (behind 2, methods merge/squash/rebase); 22 lines in total.
  • gitea.com (Gitea 1.27.0+dev), gitea/tea#1122, anonymous: summary 2 (open, checks Passing), conversation 8 (1 thread of 2 line comments; capabilities reply/resolve true for this version), files 2 (29 files), file text 1, watch detail 2 (2 statuses, both success), activity 4, action state 3 (methods: squash only, as the repo allows); 22 lines.
  • Bugs found and fixed by these reads: the servers ignore paging on issue comments (read once now) and ignore sort on the statuses list (the combined status is read now).

Live writes through Tcode's actions on https://gitea.tryanks.com (Gitea 1.25.5), scratch repo Tryanks/tcode-a7-forgejo-check, credential from tea login helper get (status: source=Cli{tea}, added: false): PR #1 opened from a branch with tea, then through Tcode: comment → Applied; review (COMMENT verdict with a line comment on CHECK.md:3; the author may only comment) → Applied; set label (id 71) → Applied; react ♥ → Applied; close → Applied (summary Closed); reopen → Applied (summary Open); update branch → UpToDate; merge squash at the head → Applied (summary Merged, using 1.25's Do field). Ready-for-review → refused before any write request because the capability is absent (the live run answered Rejected(Invalid); since the review that refusal is Rejected(Unsupported)). The scratch repository was deleted afterwards (tea repos delete).

Not delivered because the API cannot do it (no substitute was built):

  • Draft/ready toggle: EditPullRequestOption has no draft field on codeberg.org, gitea.com or gitea.tryanks.com (/swagger.v1.json); draft is a WIP title prefix. Capability draft is false, so no control shows; the "mark draft/ready" live step is unverified for that reason.
  • Remove agent credits on merge: the API has no read of the default merge message to clean; capability merge_message is false, so the merge sheet hides the option and the server writes its own message.
  • Auto-merge: no endpoint says whether a merge is scheduled, so a scheduled one could never be shown or cancelled; the capability is off and the wiring was removed.
  • Media: only uploads and avatars on the pull request's own server are read (with its token); the rest are drawn by URL.

What tea provides: a token per server through its git credential helper (tea login helper get, never printing tokens otherwise) and the list of login URLs (tea logins list --output json, no tokens). It cannot choose among several logins for one server (its helper picks), so no account picker is shown for tea.

Tests

  • New: the_model_reads_the_hosts_settings_configure (GitHub-only linking text is the literal origin/main text; with Forgejo/Gitea configured it names tea once and "GitHub or Forgejo or Gitea"), each_kind_names_its_hosts_by_its_own_rule, github_hosts_from_before_kinds_load_as_github_source_control_hosts (literal legacy github.hosts input), a_host_is_refused_by_its_kinds_rule (invalid_host with the kind's text), a_server_version_says_which_api_it_speaks, urls_and_remotes_find_the_server_by_port_and_mount, statuses_and_mergeability_read_as_the_watch_needs (Source control: other hosts, Forgejo/Gitea, GitLab, Bitbucket, Azure DevOps (A7) #643: warning fails, a non-draft that cannot merge conflicts, a draft stays unknown), line_comments_on_one_line_share_a_thread_named_by_the_first.
  • New in the review pass: a_url_naming_another_server_after_userinfo_is_refused (https://gitea.x.com@127.0.0.1:8080/… and https://codeberg.org:443@evil.com/… name no server; fails without the authority check), a_conflict_is_a_false_that_holds_at_one_head (a mergeable:false is a conflict only once it has held 30 s at one head, so two back-to-back reads never confirm one; the verdicts are dropped after 24 h and capped at 512), each_host_goes_to_its_kinds_implementation (configured kind beats the name; codeberg.org/gitea.com by name; unknown → GitHub), a_token_goes_only_to_the_server_it_is_for (GITEA_TOKEN only for GITEA_INSTANCE_URL's server; a fake tea is asked per host and its token never reaches another server), a_redirect_off_the_servers_origin_drops_the_token (a media redirect keeps the token only on the server's origin: scheme, host and port; another port of the same host gets none), a_time_ahead_reads_in_the_future (a rate limit's resume time reads "in 5m", rounded up; it once read "resumes 5m ago"), a_threads_tools_keep_the_hosts_they_were_registered_with (this test found that hosts_in compared &'static consts by address, which fails across crates; it now filters HostKind::ALL).
  • Touched, setup or renamed shapes only: services tests/github.rs and tests/pull_request_reads.rs (set_token(HostKind::Github, …), HostSettings{kind}; the env source now carries its variable, Env{name:"GITHUB_TOKEN"}, which is what the fixture sets); runtime tests.rs (SourceControlHost patch, source_control snapshot), pull_requests_tests.rs (settings setup, linking_instructions(&hosts_in([]))), and dispatch_next_pending(None) where false was passed. No assertion was loosened.

Checks at 2fdf22c (after the second review; origin/main merged at 8ccddf8):

  • cargo fmt --all --check: clean
  • cargo clippy --workspace --all-targets --locked -- -D warnings: clean
  • cargo nextest run --workspace --locked --no-fail-fast: 1111 tests run: 1111 passed, 13 skipped
  • cargo machete: no unused dependencies
  • An earlier full run (before the merge) failed tcode-runtime pipe::p4b_tests::attachments_mux_uses_host_session_directory_and_returns_identical_bytes at 16.6 s: it waits for the first host event with the 5-second wall-clock deadline in pipe::tests::next_event. It passed alone (1 s), in the runtime crate's run (197/197) and in the final full run; nothing here touches that path. The driver is tracked as Runtime test driver: attachments_mux test busy-polls against a 5 s wall-clock deadline #687 and not changed here.
  • Visual: desktop light and phone light captured. Re-taken after the second review: both Settings shots (credits description, the command chip's single spaces) and the conversation, on a fresh scratch PR on gitea.tryanks.com (deleted again afterwards), where the line comment shows no reaction button and its ⋯ menu has no Edit; a quick dark-theme look at Settings → Source Control showed marks, notices and the command in theme colours with nothing unreadable; narrow desktop was not inspected. The files shot is from the first scratch PR. iOS, Android and Web run in CI only.

Follow-ups (not in this PR)

  • meta.reviewers_truncated and stack.* still say GitHub (not reachable on Forgejo/Gitea pages).
  • own_line_comment reads reviews unpaged.
  • Tcode-kept viewed marks cannot mark pure renames or mode changes (no blob change in the diff) and are never pruned.
  • Settings: scope hints and the create-token link; origins "Found in a project / linked pull request"; Rejected and RateLimited notices.
  • Kind glyphs on badges and rows; Add host from the link dialog.
  • Narrow desktop not inspected.

Merge Danger

Door: two-way
Blast Radius: source-control
Wire changes, noted above PROTOCOL_VERSION: Settings.github → Settings.source_control with kinded hosts and per-host status, SettingsPatch::SourceControlHost/RemoveSourceControlHost, SetHostToken/RefreshHostCredentials, PullRequestCapabilities.host_viewed_marks and merge_message, PullRequestRejection::Unsupported, HostStatus.added (no origin enum). settings.json written by this build no longer carries github; an older build reading it loses the GitHub host switches (they default on), and this build reads the older file. Saved GitHub tokens stay under @github; Forgejo/Gitea tokens go under @forgejo. Unknown hosts still route to GitHub. The model-visible text is unchanged for GitHub-only settings and grows only when a Forgejo/Gitea host is configured. GitHub-only side effects, accepted: is_pull_request_url (a menu hint) now also accepts /pulls/N on any host, and merges_or_closes also matches tea pr merge|close. The Forgejo mark (assets/icons/forgejo.svg) is Forgejo's logo (CC BY-SA 4.0) drawn monochrome.

Part of #643
Closes #674

Settings move from github.hosts to kind-per-host source_control.hosts
(legacy input still loads); configure and credential status take neutral
per-host settings and status; forge::connect routes each key and host to
its kind's implementation. services::forgejo serves Forgejo and Gitea over
direct REST with a per-host token (saved, GITEA_TOKEN for
GITEA_INSTANCE_URL's server, tea's stored login), branching on
/api/v1/version where the two differ. Host terms add FORGEJO and GITEA,
and the model-visible text names only the kinds settings configure.
Command validation becomes kind-aware (invalid_host).
… on pull request surfaces

One row per host with its kind's mark, switch, credential source, one
notice, a write-only token editor and Remove host for added hosts; an Add
host dialog with a kind picker and the kind's host rule. Open on <host>
names the server for Forgejo and Gitea, and Files says when viewed marks
are kept by Tcode.
Host-named strings on every surface Forgejo and Gitea reach, with the
conflicts and checks pages only where the host has them and an
Unsupported rejection for an absent capability. Forgejo and Gitea offer no
auto-merge or merge message; line comments offer no reaction or edit; a
mergeable:false is a conflict only when read twice at one head. URL hosts
must be authorities, the media credential is the pull request's server's,
and the files cursor pages by the first page's size. One kind lookup and
host rule in core; tools keep the host names they were registered with.
Settings join missing tools with or, say Not connected once, and keep a
command's URL whole on a phone.
A rate limit's resume time reads ahead (in 5m), and soon once passed. A
media redirect keeps the token only on the server's origin. Forgejo's
mergeable:false is a conflict once it has held 30 seconds at one head, and
the verdicts are bounded. no_credential_title names its host; the credits
setting says Forgejo and Gitea use the server's message; the command chip
spaces its words as text. The wire note names added, not origin.
@Tryanks
Tryanks marked this pull request as ready for review October 9, 2026 12:32
@Tryanks
Tryanks enabled auto-merge (squash) October 9, 2026 12:32
@Tryanks
Tryanks merged commit b72ea80 into main Oct 9, 2026
7 checks passed
@Tryanks
Tryanks deleted the feat/forgejo-host branch October 9, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PR page rate-limit notice never shows the resume time

1 participant