git-ents.gitmain
⌘K
foforge
exe.rs551 lines · 20.2 KB · rusthistorycomment on this file
1//! `git ents`'s dispatch: the only place a [`crate::cli::Top`] variant is
2//! interpreted. Every branch is a thin call into [`crate::commands`]; no
3//! business logic lives here.
4#![expect(
5 clippy::let_underscore_must_use,
6 reason = "porcelain output to a writer (stdout in practice) is best-effort; a broken pipe \
7 here is not actionable and every write is one-shot, not chained"
8)]
9
10use crate::cli::{
11 AccountAction, Cli, CommentAction, EffectAction, HookAction, InboxAction, IssueAction,
12 MembersAction, RedactAction, ReviewAction, ToolchainAction, Top,
13};
14use crate::commands;
15use crate::error::Result;
16use crate::root::{HostedRoot, LocalRoot};
17
18/// Run `cli` against the repository discovered from the current
19/// directory, writing porcelain output to `out`.
20///
21/// # Errors
22///
23/// Any [`crate::Error`] the dispatched command reports.
24pub fn run(cli: Cli, out: &mut impl std::io::Write) -> Result<()> {
25 match cli.command {
26 Top::Setup {
27 key,
28 hosted: true,
29 path,
30 } => {
31 let target = path.unwrap_or_else(|| ".".into());
32 let key_path = commands::setup::run_hosted(&target, key)?;
33 let _ = writeln!(out, "signing key: {}", key_path.display());
34 let _ = writeln!(out, "hooks installed in {}/hooks", target.display());
35 Ok(())
36 }
37 Top::Setup {
38 key,
39 hosted: false,
40 path: _,
41 } => {
42 let root = LocalRoot::discover(".")?;
43 let key_path = commands::setup::run(&root, key)?;
44 commands::setup::configure_global_signing_defaults()?;
45 let _ = writeln!(out, "signing key: {}", key_path.display());
46 let _ = writeln!(
47 out,
48 "global git config: commit.gpgsign=true, tag.gpgsign=true, push.gpgsign=if-asked"
49 );
50 Ok(())
51 }
52 Top::Bootstrap {
53 username,
54 server_pubkey,
55 server_name,
56 remote,
57 key,
58 } => {
59 let root = LocalRoot::discover(".")?;
60 commands::bootstrap::run(
61 &root,
62 &username,
63 server_pubkey,
64 server_name.as_deref().unwrap_or("forge"),
65 remote.as_deref().unwrap_or("origin"),
66 key,
67 out,
68 )
69 }
70 Top::Members { action } => run_members(action, out),
71 Top::Account { action } => run_account(action, out),
72 Top::Effect { action } => run_effect(action, out),
73 Top::Toolchain { action } => run_toolchain(action, out),
74 Top::Comment { action } => run_comment(action, out),
75 Top::Issue { action } => run_issue(action, out),
76 Top::Review { action } => run_review(action, out),
77 Top::Inbox { action } => run_inbox(action, out),
78 Top::Redact { action } => run_redact(action, out),
79 Top::Hook { action } => run_hook(action, out),
80 Top::Login { url, code, key } => commands::login::run(&url, &code, key, out),
81 Top::Serve {
82 port,
83 key,
84 hosted: false,
85 ..
86 } => {
87 let root = LocalRoot::discover(".")?;
88 commands::serve::run(root, port, key, out)
89 }
90 // Root choice off the flag belongs exactly here, in the
91 // composition root's dispatch (`arch.no-hosted-branch` bans it in
92 // library code, not in `exe`).
93 Top::Serve {
94 port,
95 key,
96 hosted: true,
97 public_host,
98 path,
99 } => {
100 let root = HostedRoot::open(path.unwrap_or_else(|| ".".into()))?;
101 commands::serve::run_hosted(root, port, key, public_host, out)
102 }
103 Top::Lsp { key } => {
104 // The lens speaks LSP over stdin/stdout, so nothing may be
105 // written to `out` (the process's stdout) here — that stream is
106 // the protocol channel. It reuses the exact same local root
107 // `serve` does (`lens.serve`), adding only the LSP frontend.
108 let root = LocalRoot::discover(".")?;
109 commands::lsp::run(root, key)
110 }
111 }
112}
113
114fn run_members(action: MembersAction, out: &mut impl std::io::Write) -> Result<()> {
115 let root = LocalRoot::discover(".")?;
116 match action {
117 MembersAction::List => {
118 for (username, member) in commands::members::list(&root.refs, &root.objects)? {
119 let _ = writeln!(
120 out,
121 "{username}\t{}\t{:?}",
122 member.state, member.provenance
123 );
124 }
125 }
126 MembersAction::Add {
127 username,
128 pubkey,
129 key,
130 } => {
131 commands::members::add(&root, &username, pubkey, key)?;
132 let _ = writeln!(out, "enrolled {username}");
133 }
134 MembersAction::Remove { username, key } => {
135 commands::members::remove(&root, &username, key)?;
136 let _ = writeln!(out, "removed {username}");
137 }
138 MembersAction::Revoke { username, key } => {
139 commands::members::set_revoked(&root, &username, true, key)?;
140 let _ = writeln!(out, "revoked {username}");
141 }
142 MembersAction::Unrevoke { username, key } => {
143 commands::members::set_revoked(&root, &username, false, key)?;
144 let _ = writeln!(out, "unrevoked {username}");
145 }
146 MembersAction::Check { key } => match commands::members::check(&root, key)? {
147 Some((username, state)) => {
148 let _ = writeln!(out, "{username}\t{state}");
149 }
150 None => {
151 let _ = writeln!(out, "not a member");
152 }
153 },
154 }
155 Ok(())
156}
157
158fn run_account(action: AccountAction, out: &mut impl std::io::Write) -> Result<()> {
159 let root = LocalRoot::discover(".")?;
160 match action {
161 AccountAction::Show => {
162 let account = commands::account::show(&root)?;
163 let _ = writeln!(out, "member: {}", account.member);
164 let _ = writeln!(out, "login: {}", account.login);
165 }
166 AccountAction::Create { member, login, key } => {
167 commands::account::create(&root, member, login, key)?;
168 let _ = writeln!(out, "account created");
169 }
170 }
171 Ok(())
172}
173
174fn run_effect(action: EffectAction, out: &mut impl std::io::Write) -> Result<()> {
175 let root = LocalRoot::discover(".")?;
176 match action {
177 EffectAction::List { porcelain } => {
178 let rows = commands::effect::list(&root)?;
179 if porcelain {
180 let _ = write!(out, "{}", ents_forge::present::porcelain(&rows));
181 } else {
182 for (name, effect) in rows {
183 let _ = writeln!(
184 out,
185 "{name}\t{}",
186 ents_forge::present::columns(&effect).join("\t")
187 );
188 }
189 }
190 }
191 EffectAction::Show { name, at } => {
192 let (effect, status) = commands::effect::show(&root, &name, at)?;
193 let _ = write!(out, "{}", ents_forge::present::view(&effect));
194 let _ = writeln!(
195 out,
196 "result: {}",
197 status.map_or_else(|| "none".to_owned(), |status| status.to_string())
198 );
199 }
200 EffectAction::Add {
201 name,
202 on,
203 run,
204 toolchain,
205 key,
206 } => {
207 commands::effect::add(&root, &name, on, run, toolchain, key)?;
208 let _ = writeln!(out, "defined {name}");
209 }
210 EffectAction::Run { name, at, key } => {
211 let outcomes = commands::effect::run(&root, &name, at, key, root.executor.as_ref())?;
212 for (oid, outcome) in outcomes {
213 let _ = writeln!(out, "{oid}\t{:?}", outcome.result);
214 }
215 }
216 EffectAction::Log { name, porcelain } => {
217 let rows = commands::effect::log(&root, &name)?;
218 if porcelain {
219 let rows: Vec<_> = rows
220 .into_iter()
221 .map(|(oid, record)| (oid.to_string(), record))
222 .collect();
223 let _ = write!(out, "{}", ents_forge::present::porcelain(&rows));
224 } else {
225 for (oid, record) in rows {
226 let _ = writeln!(
227 out,
228 "{}\t{}",
229 ents_forge::abbreviate_id(&oid.to_string()),
230 ents_forge::present::columns(&record).join("\t")
231 );
232 }
233 }
234 }
235 }
236 Ok(())
237}
238
239fn run_toolchain(action: ToolchainAction, out: &mut impl std::io::Write) -> Result<()> {
240 let root = LocalRoot::discover(".")?;
241 match action {
242 ToolchainAction::List => {
243 for name in commands::toolchain::list(&root)? {
244 let _ = writeln!(out, "{name}");
245 }
246 }
247 ToolchainAction::Import { name, bin, key } => {
248 commands::toolchain::import(&root, &name, &bin, key)?;
249 let _ = writeln!(out, "imported {name}");
250 }
251 ToolchainAction::View { name } => {
252 let (toolchain, recipe) = commands::toolchain::view(&root, &name)?;
253 let _ = writeln!(out, "name: {}", toolchain.name);
254 let _ = writeln!(out, "recipe: {recipe:?}");
255 }
256 ToolchainAction::Log { name } => {
257 for oid in commands::toolchain::log(&root, &name)? {
258 let _ = writeln!(out, "{oid}");
259 }
260 }
261 }
262 Ok(())
263}
264
265fn run_comment(action: CommentAction, out: &mut impl std::io::Write) -> Result<()> {
266 let root = LocalRoot::discover(".")?;
267 match action {
268 CommentAction::List {
269 worktree,
270 state,
271 open,
272 context,
273 porcelain,
274 } => {
275 let state = match (state, open) {
276 (Some(state), false) => Some(state),
277 (None, true) => Some("open".to_owned()),
278 (None, false) => None,
279 (Some(_), true) => {
280 return Err(crate::Error::InvalidArgument(
281 "--open is shorthand for --state open; give one or the other".into(),
282 ));
283 }
284 };
285 let filter = ents_forge::comment::ListFilter { state, context };
286 let (rows, unreadable) = commands::comment::list_projected(&root, worktree, &filter)?;
287 if porcelain {
288 // Porcelain stays rows-only for format stability; a tool
289 // that wants the unreadable refs takes them from
290 // `comment::list_projected` itself.
291 let _ = write!(out, "{}", commands::comment::porcelain(&rows));
292 } else {
293 for row in rows {
294 let _ = writeln!(
295 out,
296 "{}\t{}",
297 ents_forge::abbreviate_id(&row.id),
298 ents_forge::present::columns(&row.comment).join("\t")
299 );
300 }
301 for entry in unreadable {
302 let _ = writeln!(out, "! {}\tunreadable: {}", entry.refname, entry.error);
303 }
304 }
305 }
306 CommentAction::Add {
307 path,
308 body,
309 lines,
310 rev,
311 worktree,
312 context,
313 parent,
314 key,
315 } => {
316 let new = ents_forge::comment::NewComment {
317 body: crate::compose::body::<CommentAction>("Add", body)?,
318 path,
319 lines,
320 rev,
321 worktree,
322 context,
323 parent,
324 };
325 let id = commands::comment::add(&root, new, key)?;
326 let _ = writeln!(out, "commented {id}");
327 }
328 CommentAction::Reply { id, body, key } => {
329 let reply_id = commands::comment::reply(&root, &id, body, key)?;
330 let _ = writeln!(out, "replied {reply_id}");
331 }
332 CommentAction::Resolve { id, key } => {
333 commands::comment::set_state(&root, &id, true, key)?;
334 let _ = writeln!(out, "resolved {id}");
335 }
336 CommentAction::Reopen { id, key } => {
337 commands::comment::set_state(&root, &id, false, key)?;
338 let _ = writeln!(out, "reopened {id}");
339 }
340 CommentAction::Show { id, rev, worktree } => {
341 let (comment, projected) = commands::comment::show(&root, &id, &rev, worktree)?;
342 let view = ents_forge::present::view(&comment);
343 for line in &view.lines {
344 let _ = writeln!(out, "{}: {}", line.name, line.value);
345 }
346 if let Some((anchor, projection)) = projected {
347 let _ = writeln!(out, "path: {}", anchor.path);
348 let target = if worktree { "worktree" } else { rev.as_str() };
349 let detail = match &projection {
350 ents_anchor::Projection::Relocated {
351 path,
352 lines: Some(range),
353 } => format!(" ({path}:{}-{})", range.start, range.end),
354 ents_anchor::Projection::Relocated { path, lines: None }
355 | ents_anchor::Projection::Outdated { path } => format!(" ({path})"),
356 ents_anchor::Projection::Current | ents_anchor::Projection::Deleted => {
357 String::new()
358 }
359 };
360 let _ = writeln!(out, "projection at {target}: {}{detail}", projection.label());
361 }
362 if let Some(body) = &view.body {
363 let _ = writeln!(out, "{}: {}", body.name, body.value);
364 }
365 }
366 }
367 Ok(())
368}
369
370fn run_issue(action: IssueAction, out: &mut impl std::io::Write) -> Result<()> {
371 let root = LocalRoot::discover(".")?;
372 match action {
373 IssueAction::List { porcelain } => {
374 let rows = commands::issue::list(&root)?;
375 if porcelain {
376 let _ = write!(out, "{}", ents_forge::present::porcelain(&rows));
377 } else {
378 for (id, issue) in rows {
379 let _ = writeln!(
380 out,
381 "{}\t{}",
382 ents_forge::abbreviate_id(&id),
383 ents_forge::present::columns(&issue).join("\t")
384 );
385 }
386 }
387 }
388 IssueAction::Show { id } => {
389 let issue = commands::issue::show(&root, &id)?;
390 let _ = write!(out, "{}", ents_forge::present::view(&issue));
391 }
392 IssueAction::New {
393 title,
394 body,
395 state,
396 label,
397 assignee,
398 key,
399 } => {
400 let (title, body) = crate::compose::title_body::<IssueAction>("New", title, body)?;
401 let id = commands::issue::new(&root, title, body, state, label, assignee, key)?;
402 let _ = writeln!(out, "opened {id}");
403 }
404 IssueAction::Edit {
405 id,
406 state,
407 label,
408 assignee,
409 key,
410 } => {
411 commands::issue::edit(&root, &id, state, label, assignee, key)?;
412 let _ = writeln!(out, "edited {id}");
413 }
414 }
415 Ok(())
416}
417
418fn run_review(action: ReviewAction, out: &mut impl std::io::Write) -> Result<()> {
419 let root = LocalRoot::discover(".")?;
420 match action {
421 ReviewAction::New {
422 target,
423 verdict,
424 body,
425 key,
426 } => {
427 let new = ents_forge::review::NewReview {
428 target,
429 verdict: verdict.parse()?,
430 body: crate::compose::body::<ReviewAction>("New", body)?,
431 };
432 let target = commands::review::new(&root, new, key)?;
433 let _ = writeln!(out, "reviewed {}", ents_forge::abbreviate_id(&target));
434 }
435 ReviewAction::Withdraw { target, key } => {
436 let target = commands::review::withdraw(&root, target, key)?;
437 let _ = writeln!(out, "withdrew {}", ents_forge::abbreviate_id(&target));
438 }
439 ReviewAction::List { target, porcelain } => {
440 let rows = commands::review::list(&root, target)?;
441 if porcelain {
442 let rows: Vec<_> = rows
443 .into_iter()
444 .map(|((review_target, member), review)| {
445 (format!("{review_target} {member}"), review)
446 })
447 .collect();
448 let _ = write!(out, "{}", ents_forge::present::porcelain(&rows));
449 } else {
450 for ((review_target, member), review) in rows {
451 let _ = writeln!(
452 out,
453 "{}\t{member}\t{}",
454 ents_forge::abbreviate_id(&review_target),
455 ents_forge::present::columns(&review).join("\t")
456 );
457 }
458 }
459 }
460 ReviewAction::Show { target, member } => {
461 let (review, thread) = commands::review::show(&root, &target, &member)?;
462 let _ = write!(out, "{}", ents_forge::present::view(&review));
463 for (comment_id, comment) in thread {
464 let _ = writeln!(
465 out,
466 "comment {}\t{}",
467 ents_forge::abbreviate_id(&comment_id),
468 ents_forge::present::columns(&comment).join("\t")
469 );
470 }
471 }
472 }
473 Ok(())
474}
475
476fn run_inbox(action: InboxAction, out: &mut impl std::io::Write) -> Result<()> {
477 let root = LocalRoot::discover(".")?;
478 match action {
479 InboxAction::List => {
480 for entry in commands::inbox::list(&root)? {
481 let _ = writeln!(out, "{entry}");
482 }
483 }
484 InboxAction::Adopt { entry, key } => {
485 commands::inbox::adopt(&root, &entry, key)?;
486 let _ = writeln!(out, "adopted {entry}");
487 }
488 }
489 Ok(())
490}
491
492fn run_redact(action: RedactAction, out: &mut impl std::io::Write) -> Result<()> {
493 let root = LocalRoot::discover(".")?;
494 match action {
495 RedactAction::List => {
496 for (id, redaction) in commands::redact::list(&root)? {
497 let _ = writeln!(out, "{id}\t{}", redaction.reason);
498 }
499 }
500 RedactAction::Add { oid, reason, key } => {
501 commands::redact::add(&root, &oid, reason, key)?;
502 let _ = writeln!(out, "redacted {oid}");
503 }
504 }
505 Ok(())
506}
507
508fn run_hook(action: HookAction, out: &mut impl std::io::Write) -> Result<()> {
509 let root = HostedRoot::open(".")?;
510 match action {
511 HookAction::PreReceive => {
512 let stdin = std::io::stdin();
513 crate::hook::pre_receive(&root, stdin.lock(), out)
514 }
515 HookAction::PostReceive => {
516 // Nothing to do: skip resolving a worker signing key and
517 // executor entirely rather than fail a repository that has
518 // not configured a hosted worker identity yet but also has no
519 // effects defined (the common case for a brand-new
520 // repository's very first pushes).
521 if root.events.pending().is_empty() {
522 let _ = writeln!(out, "ran 0 effect(s)");
523 return Ok(());
524 }
525 let scratch = tempfile::tempdir().map_err(|source| crate::Error::Io {
526 path: root.path.clone(),
527 source,
528 })?;
529 let cache = tempfile::tempdir().map_err(|source| crate::Error::Io {
530 path: root.path.clone(),
531 source,
532 })?;
533 let repo = gix::open(&root.path)?;
534 let key_path = crate::sign::resolve_key_path(&repo, None)?;
535 let signer = crate::sign::Signer::load(&key_path)?;
536 let ran = crate::hook::post_receive(
537 &root,
538 root.executor.as_ref(),
539 scratch.path(),
540 cache.path(),
541 &signer,
542 )?;
543 let _ = writeln!(out, "ran {ran} effect(s)");
544 Ok(())
545 }
546 HookAction::Reconcile => {
547 let _ = writeln!(out, "reconciled: {} pending", root.events.pending().len());
548 Ok(())
549 }
550 }
551}