|
| 1 | +# Change URIs |
| 2 | + |
| 3 | +A change URI is the system-wide identity of a code change — a Pull Request, a Phabricator Diff, or a git ref/commit. It is minted by the client at submission, validated at the gateway, and flows as an opaque string through the shared `Change` wire contract (`api/base/change`), cross-domain queue payloads, and storage, where it is a primary-key column and correlation key. A change URI must therefore be globally unambiguous on its own: interpretable without knowing which queue carried it or how any backend is wired. |
| 4 | + |
| 5 | +## Shape |
| 6 | + |
| 7 | +Every change URI is an RFC 3986 URI of the form `scheme://{host[:port]}/{path}`, with a uniform division of labor: |
| 8 | + |
| 9 | +- **scheme** — the provider *model*: how to parse the path and which extension family (change provider, merge checker, pusher) can act on it. One scheme per model — deployment flavors of the same model (github.com vs. GitHub Enterprise) do **not** get their own schemes, because the flavor is derivable from the host and two spellings for one instance would break identity. |
| 10 | +- **authority** — the provider *instance*: the `host[:port]` the change lives on. Mandatory. |
| 11 | +- **path** — the change within that instance, pinned to an exact code state (head SHA or diff ID), so staleness is detectable by comparing the pin against the provider's current state. |
| 12 | + |
| 13 | +## Formats |
| 14 | + |
| 15 | +| Provider | Format | Example | |
| 16 | +|---|---|---| |
| 17 | +| GitHub PR | `github://{host[:port]}/{org}/{repo}/pull/{pr}/{head_sha}` | `github://github.uberinternal.com/uber/submitqueue/pull/123/c3a4…89ab` | |
| 18 | +| Phabricator Diff | `phab://{host[:port]}/D{revision}/{diff}` | `phab://phabricator.example.com/D12345/67890` | |
| 19 | +| git ref/commit | `git://{host[:port]}/{repo}/{ref}/{sha}` | `git://git.example.com:9418/uber/mono/refs%2Fheads%2Fmain/c3a4…89ab` | |
| 20 | + |
| 21 | +Path rules per provider: |
| 22 | + |
| 23 | +- **GitHub** — `{org}` may be a nested path (`uber/frontend`); the literal `pull` segment separates it from the PR number, mirroring the real PR URL layout so URIs are built by substitution, not reshaping. `{head_sha}` is the PR's head commit at submission time. |
| 24 | +- **Phabricator** — `D{revision}` is the logical review (stable across updates); `{diff}` is the uploaded patch version that pins the exact code state, analogous to GitHub's head SHA. Both are positive integers without leading zeros. |
| 25 | +- **git** — `{repo}` is the repository path on the remote and may contain slashes; `{ref}` is a fully-qualified git ref (`refs/heads/main`, `refs/tags/v1.0`), percent-encoded so it occupies a single path segment; `{sha}` is a commit that ref has pointed to. |
| 26 | + |
| 27 | +## Canonical form |
| 28 | + |
| 29 | +URIs are compared as opaque strings everywhere (primary keys, claim lookups, staleness checks), so exactly one spelling per change is valid. Parsers **validate the canonical form and reject everything else — they never normalize**, because normalization applied at one entry point and skipped at another lets two spellings of one change into the system. |
| 30 | + |
| 31 | +- **Host** — required, non-empty, lowercase (DNS is case-insensitive, so case variants would alias one instance into many identities). Uppercase is rejected, not folded. |
| 32 | +- **Port** — optional, digits only, verbatim when present. Custom schemes have no registered default port, so there is nothing to strip; omit it unless the backend listens on a non-standard one. |
| 33 | +- **Commit SHAs** — the full 40-character lowercase hex form. Abbreviated or uppercase SHAs are rejected, not expanded or folded. |
| 34 | +- **All other path segments** — verbatim. Org, repo, and ref segments live in namespaces that are case-sensitive (git refs, repository paths on a git remote) or provider-canonical (GitHub resolves org/repo case-insensitively, but each repo has one canonical casing and uppercase is legal — the parser cannot know which). Folding their case would silently point the identity at a different resource; canonical casing here is the provider's to enforce, at the point where the provider is consulted. |
| 35 | +- **Round-trip** — parsing a valid URI and re-serializing the parsed form yields the input byte-for-byte. |
| 36 | + |
| 37 | +Parsing is delegated to `net/url`, which handles `host:port` splitting, bracketed IPv6 hosts, and percent-encoding correctly. |
| 38 | + |
| 39 | +## Rejected alternatives |
| 40 | + |
| 41 | +- **Host out-of-band in queue config.** Conflates identity with routing: the meaning of a stored primary-key value must not depend on deployment wiring, and the shared contract must be interpretable by every domain that imports it. |
| 42 | +- **Per-flavor schemes** (`ghe://`, `ghes://`). Redundant with the authority, and an open-ended enum baked into parsers and routing — a new instance should be configuration, not code. The flavors share one PR model and one API surface; what does differ per instance (API base path, version skew) is wiring config on the client for that host, never identifier grammar. |
| 43 | +- **The provider's web URL as identity** (`https://github.com/uber/repo/pull/123`). Human-facing URLs don't uniformly pin the code state, vary with provider UI cosmetics, and hand our identity grammar to a third party. Custom schemes keep the grammar strict and ours. |
0 commit comments