crates/cli/ents-web/src/pages/redactions.rs
redactions.rshistorycomment 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 | |
| 6 | use std::sync::Arc; |
| 7 | |
| 8 | use axum::extract::{Path, State}; |
| 9 | use ents_model::Redaction; |
| 10 | use gix_object::{Find, Write}; |
| 11 | |
| 12 | use crate::error::{Error, Result}; |
| 13 | use crate::state::AppState; |
| 14 | |
| 15 | /// `GET /redactions`. |
| 16 | /// |
| 17 | /// # Errors |
| 18 | /// |
| 19 | /// Propagates a ref-store or object read failure. |
| 20 | pub async fn list<O>(State(state): State<Arc<AppState<O>>>) -> Result<maud::Markup> |
| 21 | where |
| 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). |
| 60 | pub async fn show<O>( |
| 61 | State(state): State<Arc<AppState<O>>>, |
| 62 | Path(id): Path<String>, |
| 63 | ) -> Result<maud::Markup> |
| 64 | where |
| 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). |
| 94 | fn 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 | } |