githoot

Portals

GitHoot watches portals: the code forges that hold your pull requests. There are two, GitHub and GitLab, and a GitHub-only install looks and behaves exactly as it did before this seam existed. GitLab arrived as one new directory plus a config section, which was the test the seam was built to pass; what it taught the seam is under What GitLab changed.

The shape

Everything that talks to a forge lives under src/portal/. Everything else — the state machine, the hoot ledger, the tray wording, the PR page, the poll loop — speaks one vocabulary and one trait:

What the seam was shaped by

The trait was cut to fit the portals that are not here yet, because a seam cut to fit one side is not a seam:

Several portals at once

Several portals live in one tray. Configure two and each gets its own state, its own group on the page, and its own sign-in.

Configuration

Every existing config.txt describes one GitHub portal without naming it, and keeps working unchanged. On Settings ▸ Portals, Sign in is install and Sign out is uninstall: Sign in on GitLab writes the section below, builds the portal and starts its sign-in; Sign out deletes the credential, switches the section off and stops the portal (except the last portal, which only signs out). A hand edit to the file needs a restart. By hand, naming portals is the same thing:

portal.<name>.type=github|gitlab
portal.<name>.url=https://...        # GitLab self-managed only
portal.<name>.clientId=...           # GitLab: required
portal.<name>.enabled=off            # leave it out without deleting it

The moment any portal. key is present, the implicit GitHub portal disappears, so an old file and a new file never describe two different models at once. To watch GitHub and GitLab, name both. Portals appear in name order. A section that cannot be built (no type, an unknown type, a url that is not http(s)) is left out with a line in the log.

gitlab.com signs in with a shipped application, the way github.com does with the shipped GitHub App: a non-confidential OAuth application with the read_api scope, whose id is public by design. Unlike the GitHub App it needs no installing on a group: a GitLab token acts as the user, and read_api reads everything they can read, repository contents included, since no narrower scope still lists merge requests. A self-managed instance cannot use it, because the application is registered on gitlab.com: register one there (User settings, Applications, non-confidential, read_api) and set clientId. Without one that portal shows off with a reason, never a sign-in button that can only fail. GITLAB_APP_CLIENT_ID overrides a missing key, for testing. Each GitLab portal keeps its credential in ~/.githoot/<name>_token.txt.

GitHub Enterprise Server is not shipped yet, on purpose. The adapter derives every endpoint from a base URL and is tested against a GHES-shaped one, but a GHES instance also needs its own GitHub App registered on it. A url on a GitHub section is ignored, with a line in the log, until url and clientId can ship together for it.

copilotReviews and statusComponents stay as they are: GitHub-specific settings, read into the GitHub adapter’s options. The portal. keys are edited in config.txt only; the settings pages show the portals but cannot write their sections yet.

How GitLab is judged

One document answers all three axes: currentUser.reviewRequestedMergeRequests and authoredMergeRequests, both state:opened, draft:false, with @include dropping a list no axis in play needs. Measured against gitlab.com on 2026-09-28 at complexity 157 of the 250 allowed. A bar’s draft rule switched off drops draft:false from its list; green and amber share the authored one, so whichever of them still skips drafts drops them in code.

Not verified against a signed-in session: the device flow and the secret-less refresh for a non-confidential application. Both need a registered application to exercise.

What GitLab changed

Adding the second adapter was also a test of the seam. What it found:

What the next adapter has to bring

What was written here for GitLab before it existed, kept for comparison with what shipped above:

For GitLab (gitlab.com and self-hosted, same API):

For Bitbucket Cloud:

Everything above about GitLab and Bitbucket was checked against their current documentation when the seam was designed, and is a starting point for the adapter, not a substitute for reading the docs again when it is written.