markdown · 5999 bytes Raw Blame History

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's default_branch.
  • path — show only commits touching this path (passes through to git log -- <path>).
  • authorgit 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). Since git log doesn't expose a cheap total count, the Link: header only emits next / prev rels, never last.

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.