Forges¶
An adapter speaks to one forge's API for sync (M4). This page says what GitHub and GitLab need, as checked live for the git-bug fork that came before the core. Sync gives the rules they serve, and Where a value lives the model they serve it in: one truth, many mirrors, the three decisions that place a value, and the agreement that makes a mirror reversible.
An adapter implements the core's forge.Forge and passes its conformance
suite, forge/forgetest, over a fake forge served as that forge's
API. The adapter, not the engine, applies its listing margin, paces its
writes and retries a rate limit. It maps the forge's answers to the core's
errors: refused for good, not found, confidential, and no token.
Accounts and tokens¶
Sync acts on a forge through accounts: the posting account, from git-board's settings, and the people who have a token of their own there (see Posting for people without a token).
-
A token comes from an environment variable, when it is set and the forge's host is the one the variable is for, as in CI. Otherwise it comes from git's credential helper for the forge's host, with the account as
username:A helper with nothing for that username is asked once more without it, as a keychain keeps a host's token without one, unless the first call timed out. A person's token counts only when the forge says it is theirs.
-
The credential helper never prompts. Each call to it,
fillandapprovealike, runs withGIT_TERMINAL_PROMPT=0, an emptyGIT_ASKPASSandGCM_INTERACTIVE=never(Git Credential Manager's sign-in window), and is given up after 10 seconds: a helper that hangs is no token, and the run goes on, its lock released at its end. -
An environment token goes to its own host only, because the settings live in the committed
.git-board.toml, and a pull request that edits them must not send a token elsewhere:GITHUB_TOKENis forgithub.comand its API,api.github.com.GITHUB_ENTERPRISE_TOKENis for the host named byGITHUB_ENTERPRISE_HOST. If that is unset, it is never used.GITLAB_TOKENis for the host named byGITLAB_HOST, which isgitlab.comwhen unset.- Any other host gets its token from git's credential helper only.
GH_TOKENis never read.- A base URL must be
https://.http://is accepted only forlocalhost,127.0.0.1and::1; any other makes the adapter refuse to start.
-
The core keeps no token, in git or anywhere else. git-board's Settings stores a new one with
git credential approve. - git-board's own calls to a forge (its images, who can be assigned,
pull requests) take their token by the same rule, through each
adapter's
Token.
GitHub¶
The adapter is forge/github: a project owner/repo on github.com
or a GitHub Enterprise host. It speaks GitHub's REST API,
https://api.github.com or https://<host>/api/v3, plus one GraphQL
query for the times REST doesn't give.
- The token follows Accounts and tokens:
GITHUB_TOKENorGITHUB_ENTERPRISE_TOKENfor their own host, else git's credential helper for the host, with the account's login asusername, and never from a prompt. A person's token counts only when GitHub says it is theirs:/usermust give their account's numeric id. - A forge issue's id is GitHub's numeric
id. REST names an issue by its number, which sync gives with the id (forge.Ref), from the link. A read whose issue has another id is not found. - The listing is every issue updated since the last run, oldest first,
a hundred a page. It starts a minute early: the last run's start is the
local clock's, and
updated_atis GitHub's. An issue or a comment that two pages both list counts once. It reads the issues and their times, a request per hundred, and no comments: an issue with comments is partial, andCompletereads them, a request per hundred comments, when the pull reaches it. - Pull requests are never synced. GitHub's listing of issues holds them; the adapter leaves them out. GitHub has no confidential issues, so reading a pull request alone is the core's confidential error: sync never reads it.
- Status and labels come from the issue, not from its timeline. Some
issues GitHub closes leave no closed event in the timeline
(git-bug#609). And a timeline read page by page can miss items
(git-bug#1638). The status is
state, whatever itsstate_reason: an issue closed as not planned is closed. - When a field last changed, for the later change,
comes from one GraphQL query per page of issues: the body's
lastEditedAt, the last rename and the last reopening. A closed issue's status changed at itsclosed_at. A field GitHub shows no change of changed at the issue's creation. A comment's edit is itsupdated_at, when that is after its creation. An issue whose comments or times fail to load is an error, never half an issue. \r\nis read as\n. A web edit leaves\r\n, which would break the blank lines around sync's header and marker.- 80 writes per minute and 500 per hour. The adapter leaves at least a
second between two writes, as GitHub asks, which keeps under 80 a minute,
and makes at most 500 in any hour. When GitHub still turns a request away for a
rate limit, a 403 or a 429, the adapter waits and tries again, three
times at most. It waits for
Retry-Afterwhen given, else untilx-ratelimit-resetwhen none is left, else a minute: the secondary rate limit gives no reset time. When a limit stops a run, the next one resumes from there, as sync records every write as it makes it. - A read is tried again after a 5xx or a broken connection, a write never
is. GitHub may have made it (see Echoes). A read waits
1, 2, 4 seconds and so on, up to 30, until the waits add up to 5
minutes (
forge.Backoff): long enough for a Wi-Fi drop or a laptop asleep. Each try has the client's own minute before it fails, so a run of hung tries takes longer than the waits. Each wait, and each rate limit's, is told to the run (forge.Waiting), and the progress says it: "GitHub doesn't answer: trying again in 30 s". - Errors. A 404, a 410 and a redirect, as for an issue moved to another repository, are not found. A 403 and a 422 are refused, rate limits aside. A token GitHub rejects, a 401, stops the run when it is the posting account's; a person's is no token.
- Labels are made as needed, in GitHub's grey, before they are added. One write adds the labels; each removal is a write of its own. An added label GitHub leaves out, as it does without push access, is refused alone: the other adds and the removals still go. Removing a label the issue no longer has is no error.
- Assignees go as logins, the whole set in one write. The adapter reads back what GitHub kept: GitHub leaves out a user without access to the repository.
- Visibility is the repository's:
internal, on an Enterprise, when itsvisibilitysays so, else private by itsprivateflag, else public. - A login can be renamed, so people are matched by the forge's numeric user id, never by login.
GitLab¶
forge/gitlab is the adapter, over GitLab's REST API v4.
- A project is named by its numeric id or its path, on a base URL like
https://gitlab.epfl.ch. Its key is the host and the project as named:gitlab.epfl.ch/42400. A numeric id outlives a rename or a move. A path doesn't, and a new key is a new sync entity. - Tokens follow Accounts and tokens. The
posting account's is
GITLAB_TOKENfor its own host, else the credential helper's for the host, with the posting account's login asusername. Another account's comes from the credential helper only, and counts once GitLab's/usersays it is theirs. A helper's token for someone else, or one GitLab turns away, is no token. Asking the helper never prompts. - An issue's id is GitLab's
id, its number theiid. The API reads an issue by its iid only, which sync gives with the id (forge.Ref), from the link. Every write reads the issue first, and one whose issue has another id is not found. - Text edits leave no event. A title or description edited on GitLab
shows in the issue itself, and in a system note:
changed title from …,changed the description. Sync finds the edit by comparing it with last seen. For the later change, a title or a description takes its time from the latest such note, and the status from the latest state event. A field without one takes the issue's last update. updated_atmoves late, so sync lists the issues updated from 2 minutes before the last sync. Measured on gitlab.epfl.ch: after a label or assignee change,updated_atupdates about once a minute at most. It hadn't moved 26 s after a change, and had 71 s after. Closing an issue always moves it. Without the margin, an issue edited right after a sync could carry a time earlier than that sync, and no later sync would pick it up. The margin costs a handful of issues re-read per sync.- Listings are read page by page, 100 at a time, oldest first, up to
the last
X-Next-Page. An issue or a note listed twice, as a page read after a change can repeat one, counts once. The listing reads the issues alone: each is partial, andCompletereads its notes and state events when the pull reaches it. - Comments are the notes people wrote. System notes are GitLab's own, and give only the change times above. Internal notes are for the project's members, so sync never reads them.
- Line endings. A web edit can send
\r\n. Texts come back with\n, so that the blank lines around a header and a marker stay what sync wrote. - The free edition has one assignee per issue. The others stay in
git-board, with one warning. GitLab takes assignees only as the whole set,
assignee_ids. So sync reads them, writes the set only when it changes, and reads back what GitLab kept. GitLab records an assignment only as a system note, so sync reads the assignees off the issue. - Labels go by name, in
add_labelsandremove_labels. GitLab splits them at commas, and its labels hold none, so a label with a comma is refused, alone: the others are sent. So is an added label GitLab's answer leaves out. - Base URLs compare without their trailing slash.
https://gitlab.epfl.chandhttps://gitlab.epfl.ch/are the same server, for projects and for tokens. - Visibility is the project's
public,internalorprivate; another value is an error. An internal project is seen by every user of the instance, not by anyone (see Nothing private published). - Confidential issues are skipped. GitLab lists them to the project's
members, flagged
confidential, and the adapter leaves them out. Reading one on its own is an error (see Nothing private published). GitLab takes writes to one, so each write reads its issue first: an issue that turned confidential gets no more. - Writes go 250 ms apart, under GitLab's default limit of 300 issues
or notes created a minute. A 429 is tried again after its
Retry-After, else at the limit'sRateLimit-Reset, three times at most. A read that gets a 5xx, or whose connection drops, is tried again as GitHub's is, for 5 minutes. A write never is, as GitLab may have made it (see Echoes). Each wait is told, as on GitHub. OnceRateLimit-Remainingis 0, the next request waits forRateLimit-Reset. - Errors. A 404, a 410 and a redirect are not found: a redirect is
never followed, as Go would send
PRIVATE-TOKENalong to any host. A 400, 403, 409 or 422 is refused for good.