git-ents.gitmain
⌘K
foforge
redactions.rs118 lines · 3.9 KB · rusthistorycomment on this file
1//! `GET /redactions`, `GET /redactions/{id}`: the generic list/view pair
2//! for [`ents_model::Redaction`] -- read-only in this phase (recording a
3//! redaction stays a `git ents redact add` operation, admin-only per the
4//! gate's default namespace-authorization arm).
5
6use std::sync::Arc;
7
8use axum::extract::{Path, State};
9use ents_model::Redaction;
10use gix_object::{Find, Write};
11
12use crate::error::{Error, Result};
13use crate::state::AppState;
14
15/// `GET /redactions`.
16///
17/// # Errors
18///
19/// Propagates a ref-store or object read failure.
20pub async fn list<O>(State(state): State<Arc<AppState<O>>>) -> Result<maud::Markup>
21where
22 O: Find + Write + Send + 'static,
23{
24 let mut rows = Vec::new();
25 let mut failures = Vec::new();
26 for (id, redaction) in read_all(&state)? {
27 match redaction {
28 Ok(redaction) => rows.push((id, redaction)),
29 Err(error) => failures.push((format!("refs/meta/redactions/{id}"), error)),
30 }
31 }
32 let table = if rows.is_empty() {
33 super::blankslate(
34 "No redactions yet",
35 maud::html! { "Record one with " code { "git ents redact add" } "." },
36 )
37 } else {
38 crate::render::list_table(&rows, "id", |id| format!("/redactions/{id}"))
39 };
40 Ok(super::layout_meta(
41 &super::RepoHeader::from_state(&state),
42 &super::identity_label(&state),
43 "/redactions",
44 "Redactions",
45 maud::html! {
46 (crate::render::unreadable_disclosure(&failures))
47 (table)
48 },
49 ))
50}
51
52/// `GET /redactions/{id}`.
53///
54/// # Errors
55///
56/// [`Error::NotFound`] if `id` has no redaction ref at all -- a redaction
57/// ref that exists but whose stored tree does not match this build's
58/// [`Redaction`] shape degrades to [`crate::render::unreadable`] instead
59/// (`roots.web-agnostic`'s graceful-degradation stance).
60pub async fn show<O>(
61 State(state): State<Arc<AppState<O>>>,
62 Path(id): Path<String>,
63) -> Result<maud::Markup>
64where
65 O: Find + Write + Send + 'static,
66{
67 let (_, redaction) = read_all(&state)?
68 .into_iter()
69 .find(|(rid, _)| *rid == id)
70 .ok_or_else(|| Error::NotFound {
71 what: format!("redaction {id}"),
72 })?;
73 let body = match redaction {
74 Ok(redaction) => crate::render::view(&redaction),
75 Err(detail) => crate::render::unreadable(&detail),
76 };
77 Ok(super::layout_meta(
78 &super::RepoHeader::from_state(&state),
79 &super::identity_label(&state),
80 "/redactions",
81 &id,
82 maud::html! {
83 (super::child_crumbs("redactions", "/redactions", &id))
84 (body)
85 },
86 ))
87}
88
89/// Every `refs/meta/redactions/*` ref, with its tip's tree deserialized as
90/// a [`Redaction`] -- `Err(detail)` for a ref this build's
91/// `#[derive(Facet)]` shape could not read back, kept in the listing
92/// rather than dropped (see `crate::pages::members::read_all`'s identical
93/// rationale).
94fn read_all<O: Find>(
95 state: &AppState<O>,
96) -> Result<Vec<(String, std::result::Result<Redaction, String>)>> {
97 let mut out = Vec::new();
98 for entry in state.refs.iter_prefix("refs/meta/redactions/")? {
99 let (name, tip) = entry?;
100 let path = name.as_bstr().to_string();
101 let Some(id) = path.strip_prefix("refs/meta/redactions/") else {
102 continue;
103 };
104 // One `state.objects()` lock per iteration, reused for both reads
105 // -- see `crate::pages::members::read_all`'s identical comment for
106 // why a second `state.objects()` within the same statement would
107 // self-deadlock on this non-reentrant `Mutex`.
108 let objects = state.objects();
109 let redaction = super::commit_tree(&*objects, tip)
110 .map_err(|error| error.to_string())
111 .and_then(|tree| {
112 facet_git_tree::deserialize::<Redaction>(&tree, &*objects)
113 .map_err(|error| error.to_string())
114 });
115 out.push((id.to_owned(), redaction));
116 }
117 Ok(out)
118}