Commits
Read-only commits surface. Mirrors GitHub's
/repos/{owner}/{repo}/commits family — backed by git log
on the bare repository so the response is always in lock-step
with the on-disk history.
All endpoints require Authorization: Bearer <pat> with
repo:read and gate on ActionRepoRead. The
common API conventions apply.
List commits
GET /api/v1/repos/{owner}/{repo}/commits[?sha=&path=&author=&since=&until=&page=&per_page=]
Query parameters:
sha— branch / tag / commit SHA to start from. Default: the repo'sdefault_branch.path— show only commits touching this path (passes through togit log -- <path>).author—git log --author=<...>filter (substring match on author name or email).since/until— RFC3339 timestamps bracketing the window.page/per_page— standard pagination (per_page ≤ 100, default 30). Sincegit logdoesn't expose a cheap total count, theLink:header only emitsnext/prevrels, neverlast.
Response shape (one entry):
{
"sha": "5f3a…",
"short_sha": "5f3aabc",
"subject": "fix race in fanout worker",
"body": "longer commit body wraps here",
"author": {
"name": "Alice",
"email": "alice@example.com",
"date": "2026-05-12T00:00:00Z"
},
"verification": {
"verified": false,
"reason": "unsigned",
"signature": null,
"payload": null,
"verified_at": null
}
}
The verification object mirrors GitHub's documented shape and
is always present. See Signature verification
below for the field semantics and the reason enum.
Empty / uninitialised repos return [] rather than 404.
Get a commit
GET /api/v1/repos/{owner}/{repo}/commits/{sha}
{sha} accepts a full 40-char SHA or any unambiguous prefix
that git rev-parse resolves. Unknown SHAs 404.
{
"sha": "5f3a…",
"short_sha": "5f3aabc",
"subject": "fix race in fanout worker",
"body": "…",
"author": { "name": "Alice", "email": "alice@example.com", "date": "2026-05-12T00:00:00Z" },
"committer": { "name": "Alice", "email": "alice@example.com", "date": "2026-05-12T00:00:00Z" },
"parents": ["9c1b…"],
"tree_sha": "abcd…",
"files": [
{
"path": "internal/webhook/fanout.go",
"status": "M",
"additions": 7,
"deletions": 3,
"binary": false
}
],
"stats": { "additions": 7, "deletions": 3, "total": 10 }
}
files[].status is git's letter code (A added, M modified,
D deleted, R renamed, C copied, T type-changed). Renames
and copies surface the original path on old_path.
Signature verification
Every commit response carries a verification object. shithub
runs the signature check server-side against the bytes git
stored in the commit object; the result is cached per
(repo, commit_oid) and surfaced here.
{
"verified": true,
"reason": "valid",
"signature": "-----BEGIN PGP SIGNATURE-----\n…",
"payload": "tree abc…\nparent def…\n…",
"verified_at": "2026-05-12T04:00:00Z"
}
| Field | Type | Notes |
|---|---|---|
verified |
bool | true only when reason == "valid". Always false otherwise. |
reason |
string | One of the enum values below. |
signature |
string | null | The armored signature block as stored on the commit object. |
payload |
string | null | The bytes the signature was computed over (commit object minus gpgsig). |
verified_at |
string | null | RFC3339 timestamp of the cache row; null for unsigned / not-yet-stamped. |
reason enum
The values mirror gh's documented enum exactly:
| Value | Meaning |
|---|---|
valid |
Signature parsed, cryptographically valid, signing email matches a verified email on the key's account. |
unsigned |
Commit object carried no signature header. Default for cache misses; clients render no badge. |
unknown_key |
Signature parsed but no uploaded GPG key matches the signing subkey's fingerprint. |
unverified_email |
Signature is valid for an uploaded key, but the signing email isn't verified on that key's account. |
bad_email |
Signature is valid for an uploaded key, but the signing email isn't on the key at all. |
expired_key |
Signature parsed but the key was expired at signing time. |
not_signing_key |
The key referenced isn't a signing key (capability bits missing). |
malformed_signature |
The gpgsig header didn't parse as an OpenPGP signature block. |
invalid |
Signature parsed but the cryptographic check failed. |
Cache freshness
Verification rows are populated by an asynchronous backfill that
runs whenever a user uploads a GPG key (and once-off at deploy
time via shithubd gpg-backfill-all). Between key upload and
backfill completion, affected commits report unsigned (the
conservative default); the badge appears once the row is
stamped. Revoking a key invalidates affected cache rows;
clients see unsigned until another matching key is uploaded.
View source
| 1 | # Commits |
| 2 | |
| 3 | Read-only commits surface. Mirrors GitHub's |
| 4 | `/repos/{owner}/{repo}/commits` family — backed by `git log` |
| 5 | on the bare repository so the response is always in lock-step |
| 6 | with the on-disk history. |
| 7 | |
| 8 | All endpoints require `Authorization: Bearer <pat>` with |
| 9 | `repo:read` and gate on `ActionRepoRead`. The |
| 10 | [common API conventions](overview.md) apply. |
| 11 | |
| 12 | ## List commits |
| 13 | |
| 14 | ``` |
| 15 | GET /api/v1/repos/{owner}/{repo}/commits[?sha=&path=&author=&since=&until=&page=&per_page=] |
| 16 | ``` |
| 17 | |
| 18 | Query parameters: |
| 19 | |
| 20 | - `sha` — branch / tag / commit SHA to start from. Default: |
| 21 | the repo's `default_branch`. |
| 22 | - `path` — show only commits touching this path (passes |
| 23 | through to `git log -- <path>`). |
| 24 | - `author` — `git log --author=<...>` filter (substring match |
| 25 | on author name or email). |
| 26 | - `since` / `until` — RFC3339 timestamps bracketing the window. |
| 27 | - `page` / `per_page` — standard pagination (per_page ≤ 100, |
| 28 | default 30). Since `git log` doesn't expose a cheap total |
| 29 | count, the `Link:` header only emits `next` / `prev` rels, |
| 30 | never `last`. |
| 31 | |
| 32 | Response shape (one entry): |
| 33 | |
| 34 | ```json |
| 35 | { |
| 36 | "sha": "5f3a…", |
| 37 | "short_sha": "5f3aabc", |
| 38 | "subject": "fix race in fanout worker", |
| 39 | "body": "longer commit body wraps here", |
| 40 | "author": { |
| 41 | "name": "Alice", |
| 42 | "email": "alice@example.com", |
| 43 | "date": "2026-05-12T00:00:00Z" |
| 44 | }, |
| 45 | "verification": { |
| 46 | "verified": false, |
| 47 | "reason": "unsigned", |
| 48 | "signature": null, |
| 49 | "payload": null, |
| 50 | "verified_at": null |
| 51 | } |
| 52 | } |
| 53 | ``` |
| 54 | |
| 55 | The `verification` object mirrors GitHub's documented shape and |
| 56 | is always present. See [Signature verification](#signature-verification) |
| 57 | below for the field semantics and the `reason` enum. |
| 58 | |
| 59 | Empty / uninitialised repos return `[]` rather than `404`. |
| 60 | |
| 61 | ## Get a commit |
| 62 | |
| 63 | ``` |
| 64 | GET /api/v1/repos/{owner}/{repo}/commits/{sha} |
| 65 | ``` |
| 66 | |
| 67 | `{sha}` accepts a full 40-char SHA or any unambiguous prefix |
| 68 | that `git rev-parse` resolves. Unknown SHAs `404`. |
| 69 | |
| 70 | ```json |
| 71 | { |
| 72 | "sha": "5f3a…", |
| 73 | "short_sha": "5f3aabc", |
| 74 | "subject": "fix race in fanout worker", |
| 75 | "body": "…", |
| 76 | "author": { "name": "Alice", "email": "alice@example.com", "date": "2026-05-12T00:00:00Z" }, |
| 77 | "committer": { "name": "Alice", "email": "alice@example.com", "date": "2026-05-12T00:00:00Z" }, |
| 78 | "parents": ["9c1b…"], |
| 79 | "tree_sha": "abcd…", |
| 80 | "files": [ |
| 81 | { |
| 82 | "path": "internal/webhook/fanout.go", |
| 83 | "status": "M", |
| 84 | "additions": 7, |
| 85 | "deletions": 3, |
| 86 | "binary": false |
| 87 | } |
| 88 | ], |
| 89 | "stats": { "additions": 7, "deletions": 3, "total": 10 } |
| 90 | } |
| 91 | ``` |
| 92 | |
| 93 | `files[].status` is git's letter code (`A` added, `M` modified, |
| 94 | `D` deleted, `R` renamed, `C` copied, `T` type-changed). Renames |
| 95 | and copies surface the original path on `old_path`. |
| 96 | |
| 97 | ## Signature verification |
| 98 | |
| 99 | Every commit response carries a `verification` object. shithub |
| 100 | runs the signature check server-side against the bytes git |
| 101 | stored in the commit object; the result is cached per |
| 102 | `(repo, commit_oid)` and surfaced here. |
| 103 | |
| 104 | ```json |
| 105 | { |
| 106 | "verified": true, |
| 107 | "reason": "valid", |
| 108 | "signature": "-----BEGIN PGP SIGNATURE-----\n…", |
| 109 | "payload": "tree abc…\nparent def…\n…", |
| 110 | "verified_at": "2026-05-12T04:00:00Z" |
| 111 | } |
| 112 | ``` |
| 113 | |
| 114 | | Field | Type | Notes | |
| 115 | |---------------|------------------|--------------------------------------------------------------------------------| |
| 116 | | `verified` | bool | `true` only when `reason == "valid"`. Always `false` otherwise. | |
| 117 | | `reason` | string | One of the enum values below. | |
| 118 | | `signature` | string \| null | The armored signature block as stored on the commit object. | |
| 119 | | `payload` | string \| null | The bytes the signature was computed over (commit object minus `gpgsig`). | |
| 120 | | `verified_at` | string \| null | RFC3339 timestamp of the cache row; `null` for unsigned / not-yet-stamped. | |
| 121 | |
| 122 | ### `reason` enum |
| 123 | |
| 124 | The values mirror gh's documented enum exactly: |
| 125 | |
| 126 | | Value | Meaning | |
| 127 | |-----------------------|----------------------------------------------------------------------------------------------------------| |
| 128 | | `valid` | Signature parsed, cryptographically valid, signing email matches a verified email on the key's account. | |
| 129 | | `unsigned` | Commit object carried no signature header. Default for cache misses; clients render no badge. | |
| 130 | | `unknown_key` | Signature parsed but no uploaded GPG key matches the signing subkey's fingerprint. | |
| 131 | | `unverified_email` | Signature is valid for an uploaded key, but the signing email isn't verified on that key's account. | |
| 132 | | `bad_email` | Signature is valid for an uploaded key, but the signing email isn't on the key at all. | |
| 133 | | `expired_key` | Signature parsed but the key was expired at signing time. | |
| 134 | | `not_signing_key` | The key referenced isn't a signing key (capability bits missing). | |
| 135 | | `malformed_signature` | The `gpgsig` header didn't parse as an OpenPGP signature block. | |
| 136 | | `invalid` | Signature parsed but the cryptographic check failed. | |
| 137 | |
| 138 | ### Cache freshness |
| 139 | |
| 140 | Verification rows are populated by an asynchronous backfill that |
| 141 | runs whenever a user uploads a GPG key (and once-off at deploy |
| 142 | time via `shithubd gpg-backfill-all`). Between key upload and |
| 143 | backfill completion, affected commits report `unsigned` (the |
| 144 | conservative default); the badge appears once the row is |
| 145 | stamped. Revoking a key invalidates affected cache rows; |
| 146 | clients see `unsigned` until another matching key is uploaded. |