1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
//! One repository's storage, as the server sees it.
//!
//! # Why this exists
//!
//! The server used to hold `FsStore` — a concrete type from the packfile
//! engine — and reach through it for objects and refs. That is what made
//! "amputate the engine" a change to six files in `gunnar-cli` and
//! forty-three call sites in the bench harness rather than a change to one
//! manifest: the engine's *name* was in the server's own signatures.
//!
//! So the shape is inverted. The server names this trait, an engine implements
//! it, and which engine is running is a fact about startup rather than about
//! the source.
//!
//! # This is the repo-resolution seam, and it did not have to be invented
//!
//! `gunnar-server`'s `StorageEngine` was *repository*-oriented (`open_repo`,
//! `ingest_push`); [`GitOps`](git_storage_trait::GitOps) is *object*-oriented (the
//! twelve). Deleting the
//! former left one question open — something must still own **"which
//! repository"** — and the answer is this trait, which already existed and had
//! already made the right promise. See the storage-trait disposition.
//!
//! # What it deliberately does not have
//!
//! `FsStore` also offered `slot_table()`, `rescan_probes()`,
//! `stored_hash_kind()`, `with_journal_gap_hook()` and `repack()`. Every one of
//! those is a packfile concept — a `gix-odb` slot table, a re-scan of
//! `objects/pack`, a journal-gap hook for a ref store made of files — and none
//! is on this trait. A storage engine that has no packfiles cannot answer them,
//! and a trait that asked would be the engine's shape wearing the server's name
//! again.
//!
//! `head()` and `set_head()` are not here either — but not for the reason this
//! header used to give. It said *"`HEAD` is a reference, and `GitOps::refs` /
//! `update_ref` already own it"*, and the `GitServe` HEAD ruling settled that
//! they do not: `HEAD` is a pseudo-ref and is deliberately absent from the row
//! stream. It has an accessor pair, [`GitServe::head`] and
//! [`GitServe::set_head`], and they are
//! reachable through [`RepoStore::git`] — which is a method on the store this
//! trait resolves, not a second method on this trait.
//!
//! # `secrets()` is not here either, since 2026-08-10
//!
//! It used to be, and the reason it was is written down in the version of this
//! header that carried it: *"the server keeps them in a repository — the
//! control repository — and an engine that can hold objects and refs for one
//! can hold its secrets too."* The premise was the thing that changed. The
//! server does not keep its secrets in a repository any more; they are rows in
//! `control.redb` beside the catalog and the identity table
//! ([`gunnar_core::secrets`]), which is where a compare-and-swap-free key/value
//! pair always belonged.
//!
//! What that method cost while it was here was **not** an extra trait method.
//! It was that every implementor had to produce a secret store at
//! `open_repo` time, so opening *any* repository created an Arrow secret log
//! beside it — a `create_dir_all`, a lock file and an `secrets.arrow` per
//! repository, for a store nothing ever read. And it was that reading the
//! server's own SSH host key went through a repository, which meant through a
//! storage engine, which is what made `gunnar-server` unable to boot without
//! one. Removing it is why `ServerStores` no longer has a lazily-opened control
//! repository at all.
//!
//! **Verified before removing it** (2026-08-10): the only non-test caller in
//! the tree was `ServerStores::secrets`, on the *control* repository. No
//! served repository's secret store was ever read. There is no per-repository
//! secret.
//!
//! # One `git()`, where there were two
//!
//! `RepoStore::objects` and `RepoStore::refs` used to be separate, over two
//! gunnar-owned traits. Both are now `GitOps`, the single backend-neutral
//! contract, so they collapse into [`RepoStore::git`]. A repository's objects and
//! its references are written by **one** durable transaction (`GitOps::put` —
//! "the pack's bytes durable first, then the refs that point into them"), and two
//! handles onto one transaction was always a seam that could not be honoured.
//!
//! # `Arc`, not a borrow
//!
//! [`RepoStore::git`] hands back an `Arc` because the push path clones the
//! handle into a `PackIngest` that outlives the borrow, and the serve path
//! shares one across workers. Returning `&dyn GitServe` reads more cheaply
//! and does not survive either call site.
//!
//! # It hands out `GitServe`, not `GitOps` — DECIDED 2026-08-10
//!
//! `GitServe: GitOps`, so this is still **one** method and **one** authority:
//! the eleven are reachable through the supertrait and nothing was added beside
//! them. What changed is which end of the family the seam is stated at, and the
//! reason it had to change is that the hand-off is one-way. There is no route
//! from `dyn GitOps` back to the reading half — `dyn` has no downcast — so a
//! server holding the narrow handle simply cannot serve a clone from it.
//!
//! And every consumer of this trait is a *serving* process: `UploadPack` and
//! `ReceivePack` take `&dyn GitServe`, `gunnar-policy`'s branch-protection
//! ancestry walk and its signature gate take `&dyn GitServe`, and `HEAD` is
//! reachable **only** through [`GitServe::head`] / [`GitServe::set_head`],
//! because it is deliberately not a row in `GitOps::refs` (the `GitServe` HEAD ruling). There is
//! no useful `RepoStore` that cannot be served, so the bound belongs on the
//! method.
//!
//! The alternative — a second accessor beside `git()` — was rejected: two
//! methods handing out two views of one store is two authorities on one handle,
//! which is the exact thing collapsing `objects()` + `refs()` removed.
//!
//! # `Arc<dyn GitServe>`, and the `Send + Sync` that is no longer spelled here
//!
//! [`RepoStore`] is `Send + Sync`, because the server holds one per repository
//! and serves many connections from it at once. A `Send + Sync` type cannot hold
//! an `Arc<dyn GitServe>` unless `GitOps` itself is `Send + Sync`, so for a while
//! this signature spelled the bounds out at the use site — the narrow fix, taken
//! because the contract is frozen and widening it is
//! a two-implementation decision.
//!
//! **That decision was taken.** `git-storage-trait` now declares
//! `pub trait GitOps: Send + Sync`, which both implementations already satisfied
//! (each is held behind an `Arc` and served from many connections at once), so
//! the signature is back to a bare `Arc<dyn _>` exactly as frozen. Do **not**
//! re-spell the bounds at a use site: the supertrait exists precisely so that
//! spelling is not viral, and a consumer who writes one form and not the other
//! gets a needless diff rather than an error.
use Path;
use Arc;
use GitServe;
/// One repository, opened.
///
/// Implementations must be cheap to share: the server holds one per repository
/// and serves many connections from it at once.