Files
werkator/docs/prs/2026-09-27-PR#81-pull-request-links.md
c07a22d5b6
werkator build succeeded after 03:13
werkator/deploy build succeeded after 00:07
feat(ui): show the open pull request of a branch as PR#<number> (#81)
Wherever a branch name is shown, its open Gitea pull request stands
beside it, linked to the pull request. The watcher reads the open pull
requests once per poll cycle into PullRequestIndex; pages and API only
read from there. Needs the Gitea connection and token; forks, private
repositories for visitors, and gone branches show none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #81
2026-09-27 13:57:57 +02:00

111 lines
6.5 KiB
Markdown

> **WARNING:** This document describes only the change applied in this PR.
> It may already be outdated once the next PR is merged.
> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.
## The Problem
Werkator works on branches; the pull requests in Gitea do not show anywhere in its pages.
A reviewer who sees a red build on a branch has to look up the pull request of that branch in Gitea by hand.
The branch name alone often says little; `PR#12` is the name the work is discussed under.
## Non-Goals
- No builds of pull requests or of merge results; Werkator still builds branches.
- No pull requests from forks: their head branch belongs to another repository.
- No closed or merged pull requests; the link shows while a pull request is open.
- No other forge than Gitea, and nothing without a token.
- No change to the pull-request gate (`requirePullRequest`); it keeps reading `refs/pull/*/head` without a token.
## The Scenarios
### Feature: the open pull request beside every branch name
#### Background
- A branch *heads* a pull request when the pull request's head is that branch of the repository itself.
- The Gitea connection is `gitea.baseUrl`, `gitea.owner`, `gitea.repo` and `git.token` — the one the statuses are published with.
#### Scenario#81.01: Werkator knows the open pull requests of each repository
So that the pages can show them without asking Gitea on every request.
- **Given** a repository with a Gitea connection and a token
- **When** the watcher's poll cycle reaches origin
- **Then** it reads the open pull requests from Gitea, keyed by head branch
- **and** a pull request from a fork is left out
- **and** of several pull requests from one branch, the newest wins
- **and** a list longer than one page is read page by page
- **And** nothing is read before the first poll cycle, so CLI runs and tests never ask Gitea
##### Verified by
- [GiteaClientTest](../../src/test/kotlin/de/hoennig/werkator/gitea/GiteaClientTest.kt) — `readOpenPullRequests`: own branches only, the newest wins, paging, token header
- [PullRequestIndexTest](../../src/test/kotlin/de/hoennig/werkator/gitea/PullRequestIndexTest.kt) — nothing known before the first refresh, a refresh replaces one repository's list
- [WatcherTest](../../src/test/kotlin/de/hoennig/werkator/watcher/WatcherTest.kt) — every poll cycle that reached origin refreshes the pull requests
#### Scenario#81.02: Every branch name carries its pull request
So that a reader gets from a build to the discussion of its work in one click.
- **Given** a branch that heads an open pull request
- **When** the Latest, Branches, History or Current Builds view, the build page, the Repositories page or the home page shows the branch
- **Then** `PR#<number>` stands beside the branch name, linked to the pull request in Gitea
- **and** on a home card it is plain text, because the card is a link already
- **And** the JSON API carries it as `pullRequest` with `number` and `url`, so every poll renders it too
##### Verified by
- [BuildsApiControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/BuildsApiControllerTest.kt) — `latest`, `history` and `current` carry the pull request
- [BranchListingTest](../../src/test/kotlin/de/hoennig/werkator/server/BranchListingTest.kt) — a branch row and its named pools carry it
- [RepoOverviewsTest](../../src/test/kotlin/de/hoennig/werkator/server/RepoOverviewsTest.kt) — the newest build of an overview carries it
- [UiControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt) — the pages render the `PR#12` link, the Repositories page too
- [UiViewsTest](../../src/test/kotlin/de/hoennig/werkator/server/UiViewsTest.kt) — a link on the Repositories page, plain text on a home card
#### Scenario#81.03: A pull request is content
So that a private repository still gives out no content (RFC 0003, D8).
- **Given** a private repository and a visitor without the control token
- **When** a page or the API shows its builds
- **Then** no pull request is shown, like the branch name it belongs to
- **And** a build whose branch is gone from origin shows no pull request either
##### Verified by
- [BuildsApiControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/BuildsApiControllerTest.kt) — a private repository and a gone branch carry no pull request
- [RepoOverviewsTest](../../src/test/kotlin/de/hoennig/werkator/server/RepoOverviewsTest.kt) — a hidden repository's newest build carries none
#### Scenario#81.04: Without Gitea nothing changes, and a Gitea outage does not blink
So that the feature costs nothing where it cannot work.
- **Given** no Gitea connection or no token, **then** no request is made and no pull request is shown
- **Given** Gitea fails or answers garbage, **then** the last known pull requests stay
- **and** the failure is logged once, not once per poll cycle, and so is the recovery
##### Verified by
- [GiteaClientTest](../../src/test/kotlin/de/hoennig/werkator/gitea/GiteaClientTest.kt) — `Disabled` without a request, `Error` instead of an exception
- [PullRequestIndexTest](../../src/test/kotlin/de/hoennig/werkator/gitea/PullRequestIndexTest.kt) — a failed read keeps the list, a disabled client drops it
## The Solution
`GiteaClient.readOpenPullRequests` lists `GET /api/v1/repos/{owner}/{repo}/pulls?state=open`, 50 per page, at most ten pages.
It keeps a pull request only when `head.repo_id` equals `base.repo_id`, and it takes the link from Gitea's own `html_url`.
`PullRequestIndex` holds the result per `RepoContext`.
The watcher refreshes it at the end of each repository's poll cycle, after a successful fetch: an unreachable origin most likely means an unreachable forge, and the last list stays.
The pages and the API only read from the index.
A page is polled every few seconds by every open browser; a slow forge must never slow a page down.
The DTOs `BuildResultDto`, `BranchDto` and `CurrentBuildDto` carry `pullRequest` (`number`, `url`).
`BuildResultDto.from` drops it for a gone branch, and `Redaction` drops it with the branch.
The views take it from the DTO, so the first paint and every poll say the same.
`werkator.js` renders the same link with `pullRequestLink`, and `UiFormats.pullRequest` writes the home card's text like the script does.
## Additional Changes
- `docs/configuration.md` explains the links under the `gitea` notes; `docs/llms.md` and the architecture skill name them.
- Version 1.23.0 with its release note.
1.22.0 is taken by the open branch of RFC 0006.