git-ents.gitmain
⌘K
foforge
cli.rs445 lines · 16.9 KB · rusthistorycomment on this file
1//! `git ents`'s argument grammar — `figue` derive definitions only.
2//!
3//! Per this project's engineering conventions, this module carries no
4//! logic: every doc comment here becomes `--help` text, and
5//! [`crate::exe`] is the only place a [`Top`] variant is interpreted.
6
7use std::path::PathBuf;
8
9use facet::Facet;
10use figue::{self as args, FigueBuiltins};
11
12pub use ents_forge::comment::CommentAction;
13pub use ents_forge::issue::IssueAction;
14pub use ents_forge::review::ReviewAction;
15pub use ents_kiln::toolchain::ToolchainAction;
16
17/// Local root wiring, subcommand surface, and the single-node hosted
18/// root's git-hook plumbing (`docs/development-plan.adoc`, phase 6).
19#[derive(Facet)]
20pub struct Cli {
21 /// The subcommand to run.
22 #[facet(args::subcommand)]
23 pub command: Top,
24 /// `--help`/`--version`/`--completions` wiring `figue` provides for
25 /// every CLI built on it.
26 #[facet(flatten)]
27 pub builtins: FigueBuiltins,
28}
29
30/// Every top-level `git ents` subcommand.
31// @relation(roots.local, roots.worktree-update, roots.single-node-hosted, lens.serve, scope=file)
32#[derive(Facet)]
33#[repr(u8)]
34pub enum Top {
35 /// Configure this repository for signed local writes: resolve or
36 /// generate a signing key, record it as `user.signingkey` with
37 /// `gpg.format=ssh`, and set `receive.denyCurrentBranch=updateInstead`
38 /// so the integration-test harness can push into this repository's
39 /// checked-out branch (`roots.worktree-update`).
40 ///
41 /// With `--hosted`, configures the single-node hosted root instead
42 /// (`roots.single-node-hosted`): a signing key for the hosted worker,
43 /// and this binary's own `pre-receive`/`post-receive` hooks installed
44 /// into a bare repository's `hooks/` directory. Without these hooks
45 /// installed, a hosted bare repository accepts every push ungated —
46 /// stock git's `receive-pack` has no gate of its own.
47 Setup {
48 /// Key to sign with; defaults to `user.signingkey`, else a new
49 /// `~/.ssh/id_ed25519` is generated.
50 #[facet(args::named)]
51 key: Option<PathBuf>,
52 /// Configure the single-node hosted root instead of the local
53 /// one: install this binary's `hook pre-receive`/`hook
54 /// post-receive` into a bare repository's own hooks, and a
55 /// signing key for the hosted worker.
56 #[facet(args::named, default)]
57 hosted: bool,
58 /// The bare repository to configure with `--hosted`; defaults to
59 /// the current directory. Ignored without `--hosted`.
60 #[facet(args::positional, default)]
61 path: Option<PathBuf>,
62 },
63 /// Bootstrap a fresh hosted root from a clone of it: enroll yourself
64 /// as the self-admitting first member (`gate.bootstrap`), then vouch
65 /// for the server's own key (`roots.web-signing`) so its fail-closed
66 /// web UI can boot, pushing both enrollments to the remote. Run from
67 /// your clone, never on the server — enrolling server-side would
68 /// make the machine the trust root instead of the operator.
69 Bootstrap {
70 /// Your username to enroll (`refs/meta/member/<username>`).
71 #[facet(args::positional)]
72 username: String,
73 /// The server's public key to vouch for; defaults to fetching
74 /// `/.ents/server-key` from the remote's host — the hosted
75 /// root's front proxy publishes the key's public half there
76 /// while the web UI awaits this enrollment. Required when the
77 /// remote is not http(s).
78 #[facet(args::named)]
79 server_pubkey: Option<String>,
80 /// The username the server key is enrolled under; defaults to
81 /// `forge`.
82 #[facet(args::named)]
83 server_name: Option<String>,
84 /// The remote to push both enrollments to; defaults to `origin`.
85 #[facet(args::named)]
86 remote: Option<String>,
87 /// Key to sign both enrollments with; defaults to
88 /// `user.signingkey`.
89 #[facet(args::named)]
90 key: Option<PathBuf>,
91 },
92 /// Manage the repository members at `refs/meta/member/<username>`.
93 Members {
94 /// The member action to run.
95 #[facet(args::subcommand)]
96 action: MembersAction,
97 },
98 /// Manage this repository's account identity at `refs/meta/account`.
99 Account {
100 /// The account action to run.
101 #[facet(args::subcommand)]
102 action: AccountAction,
103 },
104 /// Manage the configured effects at `refs/meta/effects/<name>` and run
105 /// them locally.
106 Effect {
107 /// The effect action to run.
108 #[facet(args::subcommand)]
109 action: EffectAction,
110 },
111 /// Manage the toolchains stored as git trees at
112 /// `refs/meta/toolchains/<name>`.
113 Toolchain {
114 /// The toolchain action to run.
115 #[facet(args::subcommand)]
116 action: ToolchainAction,
117 },
118 /// Comment on code: one comment per ref at `refs/meta/comments/<id>`,
119 /// anchored to a blob (and optionally lines) at a commit.
120 Comment {
121 /// The comment action to run.
122 #[facet(args::subcommand)]
123 action: CommentAction,
124 },
125 /// Manage issues at `refs/meta/issues/<id>`.
126 Issue {
127 /// The issue action to run.
128 #[facet(args::subcommand)]
129 action: IssueAction,
130 },
131 /// Review a commit: a verdict plus a body at
132 /// `refs/meta/reviews/<target>/<member>`, with a retention pin at
133 /// `refs/meta/pins/reviews/<target>/<member>` keeping the reviewed
134 /// commit reachable.
135 Review {
136 /// The review action to run.
137 #[facet(args::subcommand)]
138 action: ReviewAction,
139 },
140 /// Work with entities awaiting adoption at
141 /// `refs/meta/inbox/<member>/<id>`.
142 Inbox {
143 /// The inbox action to run.
144 #[facet(args::subcommand)]
145 action: InboxAction,
146 },
147 /// Manage redactions recorded at `refs/meta/redactions/<id>`.
148 Redact {
149 /// The redaction action to run.
150 #[facet(args::subcommand)]
151 action: RedactAction,
152 },
153 /// Plumbing invoked by git's own hooks on the single-node hosted root
154 /// (`git.ents.cloud`) — not part of the porcelain surface a developer
155 /// runs directly.
156 Hook {
157 /// Which hook is running.
158 #[facet(args::subcommand)]
159 action: HookAction,
160 },
161 /// Prove membership to a hosted web session (`roots.web-signin`):
162 /// fetch the one-time challenge the hosted `/login` page displayed,
163 /// sign it with your member key under the `git-ents-login` SSHSIG
164 /// namespace — locally, the key never leaves this machine — and post
165 /// the signature back, signing that browser session in.
166 Login {
167 /// The hosted root's base URL, e.g. `https://git.ents.cloud`.
168 #[facet(args::positional)]
169 url: String,
170 /// The one-time code the `/login` page displays (`XXXX-XXXX`).
171 #[facet(args::positional)]
172 code: String,
173 /// Key to prove membership with; defaults to `user.signingkey`,
174 /// else `~/.ssh/id_ed25519`.
175 #[facet(args::named)]
176 key: Option<PathBuf>,
177 },
178 /// Start the local web UI (`roots.local`): reuses this repository's
179 /// existing local composition root (the same loose-ref `RefStore`,
180 /// odb, null `EventSink`, and advisory gate `git ents members`,
181 /// `git ents comment`, and every other porcelain command already use)
182 /// and adds only the `ents-web` HTTP frontend, bound to loopback —
183 /// never git's own smart-HTTP transport, which this command does not
184 /// expose in any form. With `--hosted`, serves the single-node hosted
185 /// root's web UI instead (`roots.single-node-hosted`).
186 Serve {
187 /// Port to bind on loopback (`127.0.0.1`); `0` picks any free
188 /// port. Defaults to 4880.
189 #[facet(args::named)]
190 port: Option<u16>,
191 /// Key to sign web edits with; defaults to `user.signingkey`.
192 #[facet(args::named)]
193 key: Option<PathBuf>,
194 /// Serve the single-node hosted root's web UI instead
195 /// (`roots.single-node-hosted`): mandatory gate, sign-in
196 /// required, member-attributed edits, the server's own key as
197 /// signing identity. Still binds loopback — the front proxy is
198 /// the only external listener.
199 #[facet(args::named, default)]
200 hosted: bool,
201 /// The canonical public host bound into sign-in challenges with
202 /// `--hosted` (`roots.web-signin`), e.g. `git.ents.cloud`.
203 /// Required with `--hosted`; ignored without it.
204 #[facet(args::named)]
205 public_host: Option<String>,
206 /// The bare repository to serve with `--hosted`; defaults to the
207 /// current directory. Ignored without `--hosted`.
208 #[facet(args::positional, default)]
209 path: Option<PathBuf>,
210 },
211 /// Serve the editor lens (`lens.serve`): a Language Server Protocol
212 /// server over stdin/stdout that projects this repository's comments
213 /// (`refs/meta/comments/*`) into whatever buffer an editor has open,
214 /// and composes new ones through the same signed path `git ents
215 /// comment` uses (`lens.parity`).
216 ///
217 /// Speaks LSP over stdio only: it binds no network socket and adds no
218 /// git-serving transport. It reuses the very same local composition
219 /// root `git ents serve` and every other porcelain command use (the
220 /// same loose-ref `RefStore`, odb, null `EventSink`, and advisory
221 /// gate), adding only the LSP frontend and signing with the user's own
222 /// key. Meant to be launched by an editor extension (e.g. `ents-zed`),
223 /// not run interactively.
224 Lsp {
225 /// Key to sign composed comments with; defaults to
226 /// `user.signingkey`.
227 #[facet(args::named)]
228 key: Option<PathBuf>,
229 },
230}
231
232/// `git ents members` actions.
233#[derive(Facet)]
234#[repr(u8)]
235pub enum MembersAction {
236 /// List the members recorded in this repository.
237 List,
238 /// Enroll a new member, or update an existing one's key.
239 Add {
240 /// The member's username (`refs/meta/member/<username>`).
241 #[facet(args::positional)]
242 username: String,
243 /// The public key to enroll (an OpenSSH single-line public key);
244 /// defaults to the signer's own public key.
245 #[facet(args::named)]
246 pubkey: Option<String>,
247 /// Key to sign the enrollment with; defaults to `user.signingkey`.
248 #[facet(args::named)]
249 key: Option<PathBuf>,
250 },
251 /// Remove a member, deleting its ref.
252 Remove {
253 /// The member (username) to remove.
254 #[facet(args::positional)]
255 username: String,
256 /// Key to sign the removal with; defaults to `user.signingkey`.
257 #[facet(args::named)]
258 key: Option<PathBuf>,
259 },
260 /// Revoke a member's key (`model.member-revocation`): the record
261 /// stays, but the key no longer authorizes new signatures.
262 Revoke {
263 /// The member (username) to revoke.
264 #[facet(args::positional)]
265 username: String,
266 /// Key to sign the revocation with; defaults to `user.signingkey`.
267 #[facet(args::named)]
268 key: Option<PathBuf>,
269 },
270 /// Lift a revocation, restoring a member's key to active.
271 Unrevoke {
272 /// The member (username) to unrevoke.
273 #[facet(args::positional)]
274 username: String,
275 /// Key to sign the unrevocation with; defaults to
276 /// `user.signingkey`.
277 #[facet(args::named)]
278 key: Option<PathBuf>,
279 },
280 /// Report whether a key is an active member.
281 Check {
282 /// Key to look for; defaults to `user.signingkey`.
283 #[facet(args::named)]
284 key: Option<PathBuf>,
285 },
286}
287
288/// `git ents account` actions.
289#[derive(Facet)]
290#[repr(u8)]
291pub enum AccountAction {
292 /// Show this repository's account identity.
293 Show,
294 /// Create or update this repository's account identity.
295 Create {
296 /// The member this account belongs to; defaults to the signer's
297 /// own member (resolved by public key).
298 #[facet(args::named)]
299 member: Option<String>,
300 /// The login identity the member authenticates as.
301 #[facet(args::named)]
302 login: String,
303 /// Key to sign with; defaults to `user.signingkey`.
304 #[facet(args::named)]
305 key: Option<PathBuf>,
306 },
307}
308
309/// `git ents effect` actions.
310#[derive(Facet)]
311#[repr(u8)]
312pub enum EffectAction {
313 /// List the effects configured in this repository.
314 ///
315 /// With --porcelain, emits a stable machine-readable form:
316 /// blank-line-separated records, each starting with a `<name>` line,
317 /// followed by `trigger <query>`, `toolchains <a, b>` (when
318 /// non-empty), and `run <command>` lines.
319 List {
320 /// Emit the stable machine-readable form described above.
321 #[facet(args::named, default)]
322 porcelain: bool,
323 },
324 /// Show one effect's definition and, when a commit is given, its
325 /// result.
326 Show {
327 /// The effect's name.
328 #[facet(args::positional)]
329 name: String,
330 /// Commit to show the result for.
331 #[facet(args::named)]
332 at: Option<String>,
333 },
334 /// Define (or replace) an effect and push the update.
335 Add {
336 /// Name to record the effect under (`refs/meta/effects/<name>`).
337 #[facet(args::positional)]
338 name: String,
339 /// The query this effect triggers on (`query.grammar`).
340 #[facet(args::named)]
341 on: String,
342 /// The command the effect runs.
343 #[facet(args::positional)]
344 run: String,
345 /// Toolchain (`refs/meta/toolchains/<name>`) to activate before
346 /// the command runs (repeatable).
347 #[facet(args::named, args::label = "TOOLCHAIN", default)]
348 toolchain: Vec<String>,
349 /// Key to sign with; defaults to `user.signingkey`.
350 #[facet(args::named)]
351 key: Option<PathBuf>,
352 },
353 /// Run this repository's effects locally against every commit still
354 /// owed a result, or a single one with `--at`
355 /// (`effect.local-run`): identical toolchain materialization and
356 /// sandbox path to a hosted worker, the queue skipped entirely.
357 Run {
358 /// The effect's name.
359 #[facet(args::positional)]
360 name: String,
361 /// Commit to run against; omit to run every outstanding commit
362 /// (`query.workset`).
363 #[facet(args::named)]
364 at: Option<String>,
365 /// Key to sign the result with; defaults to `user.signingkey`.
366 #[facet(args::named)]
367 key: Option<PathBuf>,
368 },
369 /// Show recorded results for an effect, one row per judged commit.
370 ///
371 /// With --porcelain, emits a stable machine-readable form:
372 /// blank-line-separated records of one line each,
373 /// `<commit> <status>` — the full oid of the judged commit and
374 /// pass, fail, or error.
375 Log {
376 /// The effect's name.
377 #[facet(args::positional)]
378 name: String,
379 /// Emit the stable machine-readable form described above.
380 #[facet(args::named, default)]
381 porcelain: bool,
382 },
383}
384
385/// `git ents inbox` actions.
386#[derive(Facet)]
387#[repr(u8)]
388pub enum InboxAction {
389 /// List entities awaiting adoption.
390 List,
391 /// Adopt an inbox entity onto its canonical ref
392 /// (`sync.adoption-machinery`): a merge that keeps the author's
393 /// original signed commit in ancestry
394 /// (`sync.adoption-no-cherry-pick`).
395 Adopt {
396 /// The inbox entry to adopt, as `<member>/<id>`.
397 #[facet(args::positional)]
398 entry: String,
399 /// Key to sign the adoption merge with; defaults to
400 /// `user.signingkey`.
401 #[facet(args::named)]
402 key: Option<PathBuf>,
403 },
404}
405
406/// `git ents redact` actions.
407#[derive(Facet)]
408#[repr(u8)]
409pub enum RedactAction {
410 /// List the redactions recorded in this repository.
411 List,
412 /// Record that `oid` was redacted (`refs/meta/redactions/<id>`),
413 /// refusing any future push that would refill it
414 /// (`receive.redaction-ingest`). Admin-only: the gate's default
415 /// namespace-authorization arm requires admin-registered provenance
416 /// for `refs/meta/redactions/*`.
417 Add {
418 /// The object id to redact.
419 #[facet(args::positional)]
420 oid: String,
421 /// A human-readable reason recorded alongside the redaction.
422 #[facet(args::named)]
423 reason: String,
424 /// Key to sign with; defaults to `user.signingkey`.
425 #[facet(args::named)]
426 key: Option<PathBuf>,
427 },
428}
429
430/// Plumbing subcommands the single-node hosted root's git hooks invoke;
431/// see `crate::hook`'s own doc for what each does and why.
432#[derive(Facet)]
433#[repr(u8)]
434pub enum HookAction {
435 /// Run as git's own `pre-receive` hook: evaluate the gate against
436 /// every proposed transition read from stdin, refusing the whole
437 /// push under the mandatory gate if any fails.
438 PreReceive,
439 /// Run as git's own `post-receive` hook: reconcile outstanding effect
440 /// obligations (`receive.reconstructible`) and run them.
441 PostReceive,
442 /// Reconcile outstanding effect obligations without running anything
443 /// — the boot-time scan on its own, for operational use and testing.
444 Reconcile,
445}