git-ents.gitmain
⌘K
foforge
error.rs194 lines · 7.1 KB · rusthistorycomment on this file
1//! `ents-web`'s error type: every failure a page handler can hit, rendered
2//! as an HTTP response by rendered per-page (via the `IntoResponse` impl below) rather than at the
3//! type itself — a web frontend renders failures as pages/status codes, not
4//! terminal text, so this module stays data-only (mirrors `git-ents`'s own
5//! `error.rs` shape, one variant per failure source).
6
7/// Every way a page handler in this crate can fail.
8#[derive(Debug, thiserror::Error)]
9pub enum Error {
10 /// The named entity does not exist.
11 #[error("not found: {what}")]
12 NotFound {
13 /// What was being looked up.
14 what: String,
15 },
16
17 /// A malformed request: a bad line-range, an unparsable object id, a
18 /// missing required form field.
19 #[error("invalid request: {0}")]
20 InvalidArgument(String),
21
22 /// The gate refused the proposed mutation (`gate.verdict-reason`).
23 #[error("rejected: {0}")]
24 Refused(String),
25
26 /// `receive` rejected the batch as a stale compare-and-swap.
27 #[error("rejected: {name} changed concurrently, retry")]
28 Stale {
29 /// The ref whose precondition was stale.
30 name: String,
31 },
32
33 /// A previously redacted object would have been refilled by this
34 /// mutation (`receive.redaction-ingest`).
35 #[error("refused: object {oid} was redacted and cannot be refilled")]
36 Redacted {
37 /// The redacted object id.
38 oid: gix_hash::ObjectId,
39 },
40
41 /// The request's CSRF token was missing or did not match the session's
42 /// (`roots.web-session`).
43 #[error("invalid or missing CSRF token")]
44 BadCsrf,
45
46 /// No session cookie was presented, or it named a session this server
47 /// no longer holds in memory (`roots.web-session`): the process
48 /// restarted, or the cookie is forged.
49 #[error("no valid session")]
50 NoSession,
51
52 /// A `gix-ref-store` failure: reading or writing a ref.
53 #[error(transparent)]
54 Refs(#[from] gix_ref_store::Error),
55
56 /// An `ents-model` failure: building or validating a refname or typed
57 /// tree.
58 #[error(transparent)]
59 Model(#[from] ents_model::Error),
60
61 /// A `facet-git-tree` (de)serialization failure.
62 #[error(transparent)]
63 Tree(#[from] facet_git_tree::Error),
64
65 /// An `ents-anchor` failure: capturing or projecting a code anchor.
66 #[error(transparent)]
67 Anchor(#[from] ents_anchor::Error),
68
69 /// An `ents-forge` failure: anchoring, serializing, or proposing a
70 /// comment mutation. Boxed: `ents_forge::Error` is large enough on its
71 /// own to trip `clippy::result_large_err` if stored inline (mirrors
72 /// `git-ents::error::Error::Forge`'s identical boxing).
73 #[error(transparent)]
74 Forge(Box<ents_forge::Error>),
75
76 /// An `ents-effect` failure: toolchain resolution or import (the error
77 /// type `ents-kiln`'s own toolchain module reuses as-is, per that
78 /// crate's own doc). Boxed; see [`Error::Forge`]'s own doc.
79 #[error(transparent)]
80 Effect(Box<ents_effect::Error>),
81
82 /// An `ents-receive` failure: `receive` itself could not reach an
83 /// outcome. Boxed; see [`Error::Forge`]'s own doc.
84 #[error(transparent)]
85 Receive(Box<ents_receive::Error>),
86
87 /// `crate::asciidoc::to_html` could not parse or convert an AsciiDoc
88 /// blob (`acdc` reported no more specific error than "could not
89 /// convert").
90 #[error("could not render asciidoc: {0}")]
91 Asciidoc(String),
92
93 /// `crate::pages::files` could not open the served repository or read
94 /// its `HEAD` tree/a tree or blob within it (`gix::open`, a tree
95 /// lookup, or a blob read).
96 #[error("could not read repository: {0}")]
97 Repo(String),
98}
99
100impl From<ents_forge::Error> for Error {
101 fn from(source: ents_forge::Error) -> Self {
102 Self::Forge(Box::new(source))
103 }
104}
105
106impl From<ents_effect::Error> for Error {
107 fn from(source: ents_effect::Error) -> Self {
108 Self::Effect(Box::new(source))
109 }
110}
111
112impl From<ents_receive::Error> for Error {
113 fn from(source: ents_receive::Error) -> Self {
114 Self::Receive(Box::new(source))
115 }
116}
117
118/// Translate a reached [`ents_receive::Outcome`] into `Ok(())` on success or
119/// an [`Error`] otherwise — this crate's counterpart to
120/// `git_ents::mutate::outcome_to_result`, kept as a free function here for
121/// exactly the same reason: every page that proposes a mutation renders a
122/// refusal identically.
123///
124/// # Errors
125///
126/// [`Error::Refused`], [`Error::Stale`], or [`Error::Redacted`]; see
127/// `git_ents::mutate::outcome_to_result` for the identical mapping this
128/// mirrors.
129pub fn outcome_to_result(outcome: ents_receive::Outcome) -> Result<()> {
130 match outcome.result {
131 ents_receive::TxResult::Applied => Ok(()),
132 ents_receive::TxResult::Refused => {
133 let reasons = outcome
134 .verdicts
135 .iter()
136 .filter_map(|(_, verdict)| match verdict {
137 ents_gate::Verdict::Fail(refusal) => Some(refusal.to_string()),
138 ents_gate::Verdict::Pass(_) => None,
139 })
140 .collect::<Vec<_>>()
141 .join("; ");
142 Err(Error::Refused(reasons))
143 }
144 ents_receive::TxResult::Rejected { name } => Err(Error::Stale {
145 name: name.as_bstr().to_string(),
146 }),
147 ents_receive::TxResult::Redacted { oid } => Err(Error::Redacted { oid }),
148 }
149}
150
151/// This crate's `Result` alias.
152pub type Result<T> = std::result::Result<T, Error>;
153
154impl axum::response::IntoResponse for Error {
155 fn into_response(self) -> axum::response::Response {
156 use axum::http::StatusCode;
157
158 let status = match &self {
159 Error::NotFound { .. } => StatusCode::NOT_FOUND,
160 // A forge entity with no ref at all is as much a 404 as this
161 // crate's own NotFound -- the box exists for variant-size
162 // hygiene, not to demote the status to a 500.
163 Error::Forge(inner) if matches!(inner.as_ref(), ents_forge::Error::NotFound { .. }) => {
164 StatusCode::NOT_FOUND
165 }
166 Error::InvalidArgument(_) | Error::BadCsrf => StatusCode::BAD_REQUEST,
167 Error::NoSession => StatusCode::UNAUTHORIZED,
168 Error::Refused(_) | Error::Stale { .. } | Error::Redacted { .. } => {
169 StatusCode::CONFLICT
170 }
171 _ => StatusCode::INTERNAL_SERVER_ERROR,
172 };
173 let reason = status.canonical_reason().unwrap_or("Error");
174 let body = maud::html! {
175 (maud::DOCTYPE)
176 html lang="en" {
177 head {
178 meta charset="utf-8";
179 meta name="viewport" content="width=device-width, initial-scale=1";
180 meta name="color-scheme" content="light dark";
181 title { "git ents: " (status.as_u16()) " " (reason) }
182 link rel="stylesheet" href="/style.css";
183 }
184 body {
185 main.content {
186 div.page-header { h1.page-title { (status.as_u16()) " " (reason) } }
187 div.blankslate { h2 { (reason) } p { (self.to_string()) } }
188 }
189 }
190 }
191 };
192 (status, body).into_response()
193 }
194}