Skip to main content

onevcs/
error.rs

1//! The one error type every fallible entry point in this crate returns.
2//!
3//! The variants are the failures `docs/contract.md` gives `onevcs publish` a
4//! distinct exit code for, plus the refusal an interface-only build owes a
5//! caller. Marked `#[non_exhaustive]` so a later seam can add its own failure
6//! without breaking a matching consumer.
7
8use thiserror::Error;
9
10/// Everything an `onevcs` operation can fail with.
11#[derive(Debug, Error)]
12#[non_exhaustive]
13pub enum Error {
14    /// The request parsed and validated, but the seam behind it has no
15    /// implementation yet. The CLI reports this as exit code 70.
16    #[error(
17        "{operation} is not implemented yet: this build of onevcs is interface-only \
18         (the approved contract is in docs/contract.md)"
19    )]
20    NotImplemented {
21        /// The contract operation that was asked for, e.g. `Vcs::open_session`.
22        operation: &'static str,
23    },
24
25    /// A publication was refused by something that judged it, where no narrower
26    /// kind says which. Today that is the repository's own `commit-msg` hook turning
27    /// down the subject the publication would land under, and a host that took a
28    /// merge and then reported it unperformed. The CLI reports this as exit code 1.
29    ///
30    /// What the *host's checks* reported is [`Error::ChecksFailed`] or
31    /// [`Error::ChecksUnsettled`], and what its `pre-push` hook said is
32    /// [`Error::PushRejected`].
33    ///
34    /// Named for the tier this crate used to run itself and kept under that name:
35    /// the contract fixes this vocabulary across the three libraries that route on
36    /// it, so a variant is not renamed because the tier behind it went away.
37    // llmlint: ignore[names_match_behavior] the approved contract fixes the spelling.
38    #[error("gate failed: {reason}")]
39    GateFailed {
40        /// What failed, and what turned it down.
41        reason: String,
42    },
43
44    /// Input was rejected at a trust boundary — a malformed registry document, a
45    /// rules file that does not parse, an unusable argument. The CLI reports this
46    /// as exit code 2.
47    #[error("invalid input: {reason}")]
48    Invalid {
49        /// What was rejected and why.
50        reason: String,
51    },
52
53    /// The base moved under a publication and the bounded resolve-and-requeue did
54    /// not converge. The CLI reports this as exit code 3.
55    #[error("sync conflict: {reason}")]
56    SyncConflict {
57        /// The conflict that survived the bounded retry.
58        reason: String,
59    },
60
61    /// A required check the host reports concluded red. The CLI reports this as
62    /// exit code 1, which is the code the contract fixes for a verification
63    /// failure — this says *which* verification failed, not a different one.
64    #[error("required check failed: {reason}")]
65    ChecksFailed {
66        /// The check that concluded red, named, and a bounded excerpt of its log.
67        reason: String,
68    },
69
70    /// The bound on watching the host elapsed with the change still outstanding.
71    /// The CLI reports this as exit code 1.
72    ///
73    /// Named for the commonest way that happens and shared with the others,
74    /// because the failure vocabulary is fixed across the three libraries that
75    /// route on it. Distinct from [`Error::ChecksFailed`] because the operator's
76    /// next move differs: nothing has failed, and the publication stopped watching.
77    // The vocabulary is fixed across the three libraries that route on it, and gives
78    // the bound one kind whichever of its endings this was; `reason` says which.
79    #[error("checks unsettled: {reason}")]
80    // llmlint: ignore[names_match_behavior] one fixed kind for the bound's three endings.
81    ChecksUnsettled {
82        /// What was still outstanding when the bound elapsed: the required checks
83        /// that had not settled, that the host declared none at all, or — where
84        /// every one of them had settled — that it never performed the merge.
85        reason: String,
86    },
87
88    /// The publishing push was refused by the merge path. The CLI reports this as
89    /// exit code 1.
90    #[error("push rejected: {reason}")]
91    PushRejected {
92        /// git's own per-ref refusal, which is what an operator acts on.
93        reason: String,
94    },
95
96    /// The publishing push was refused by the merge path because this host is
97    /// missing a tool or a credential one of its hooks needs — a failure that does not
98    /// depend on the tree being pushed. The CLI reports this as exit code 1.
99    ///
100    /// Said by the hook itself, on the one line
101    /// [`HOST_PREREQUISITE_MARKER`](crate::HOST_PREREQUISITE_MARKER) begins, and never
102    /// inferred: a refused push carrying no such line is [`Error::PushRejected`]. A
103    /// kind of its own for the reason [`Error::PushedUnverified`] is one: a router
104    /// branches on the kind, and no change to the work can clear this refusal.
105    #[error("host prerequisite missing: {reason}")]
106    // llmlint: ignore[invalid_states_unrepresentable] the approved contract fixes every
107    // variant of this enum as one `reason: String`, and three libraries route on that
108    // shape; the remediation and the evidence pointers are both prose for a reader.
109    HostPrerequisite {
110        /// What the hook said is missing and how to install it, and where the whole
111        /// of what the merge path wrote is kept.
112        reason: String,
113    },
114
115    /// The publishing push **reached the remote**, and the merge path could not
116    /// then be read. The CLI reports this as exit code 1.
117    ///
118    /// A kind of its own rather than a reason clause on the three above, because a
119    /// router branches on the *kind*: a sentence added to [`Error::ChecksFailed`]
120    /// still routes as "the checks said no". Widening a vocabulary fixed across the
121    /// libraries that route on it is the amendment in `docs/contract.md`, which
122    /// records why.
123    #[error("pushed, merge path unverified: {reason}")]
124    // llmlint: ignore[invalid_states_unrepresentable] the approved contract fixes every
125    // variant of this enum as one `reason: String`, and three libraries route on that
126    // shape. Splitting this one into a landing field and a cause field would add public
127    // surface the contract does not name and leave one variant unlike its siblings.
128    PushedUnverified {
129        /// Where the push landed, and what stopped the merge path being read.
130        reason: String,
131    },
132
133    /// A session could not be placed: every slot of the identity's pool is held and
134    /// its overflow admits no more. `onevcs session open` reports this as exit code 4,
135    /// and nothing waits — a caller decides whether to queue, close something, or open
136    /// with `--overflow unlimited`.
137    ///
138    /// Never a publication failure, so [`FailureKind`](crate::FailureKind) — the
139    /// vocabulary fixed across the three libraries that route a publication's outcome
140    /// — has no kind for it and reports it as `Invalid` should one ever reach there.
141    #[error("pool exhausted: {reason}")]
142    // llmlint: ignore[invalid_states_unrepresentable] one `reason: String` like every
143    // sibling variant, which is the shape three libraries route on; the identity, the
144    // limits with their sources, and every holder are prose for the operator who reads
145    // the refusal, and `workspace_capacity` is the typed form of the same answer.
146    PoolExhausted {
147        /// The identity, its pool and overflow with where each limit came from, and
148        /// every holder: token, branch, worktree, owner pid and liveness.
149        reason: String,
150    },
151
152    /// A reference named no work this host knows — it is not a change request
153    /// `onevcs` opened, a session token it printed, a branch any checkout or run
154    /// clone of a registered identity holds, or a commit one of those branches
155    /// carries. The CLI reports this as exit code 2.
156    ///
157    /// A kind of its own for the reason [`Error::PushedUnverified`] is one: a caller
158    /// branches on the *kind*. A consumer that falls back from one spelling of a
159    /// reference to another — onepipeline's release asker, from a squash commit to
160    /// the change request it settled — has to fire on exactly this case and never on
161    /// an I/O failure or a host that refused, and telling them apart by matching the
162    /// reason's prose breaks silently the day the wording changes.
163    ///
164    /// It says nothing new to whoever reads the refusal, so it renders and exits
165    /// exactly as [`Error::Invalid`] does, down to the `invalid input:` prefix: the
166    /// typed answer is for the router, and a rewording of the CLI's output would be
167    /// the breaking change this exists to prevent. [`FailureKind`](crate::FailureKind)
168    /// — the vocabulary fixed across the three libraries that route a publication's
169    /// outcome — gains no kind for it and reports it as `Invalid`, which is what fixes
170    /// the exit code at 2.
171    // llmlint: ignore[names_match_behavior] the wording of the refusal an operator
172    // reads is unchanged on purpose, so this renders under `Invalid`'s prefix rather
173    // than its own; the variant name is what the router matches on.
174    #[error("invalid input: {reason}")]
175    UnresolvableReference {
176        /// The reference as it was asked about, so a caller that routes on this kind
177        /// names what it could not resolve without parsing the reason.
178        reference: String,
179        /// What the reference was searched for as, and where the spellings that do
180        /// answer are listed.
181        reason: String,
182    },
183}
184
185/// The result type every fallible entry point in this crate returns.
186pub type Result<T> = std::result::Result<T, Error>;
187
188/// Reject something, naming what was wrong with it.
189pub(crate) fn invalid(reason: impl Into<String>) -> Error {
190    Error::Invalid {
191        reason: reason.into(),
192    }
193}
194
195/// An operation on a path that the filesystem refused, as the rejection a caller
196/// reads: what was being done, where, and what the system said.
197///
198/// One helper rather than a closure at each site, so the message every one of them
199/// produces has the same shape — and so a path that cannot be written reads the
200/// same whether it was the registry, a session record, or an artifact.
201pub(crate) fn at<'a, E: std::fmt::Display>(
202    action: &'static str,
203    path: &'a std::path::Path,
204) -> impl FnOnce(E) -> Error + 'a {
205    move |error| invalid(format!("cannot {action} {}: {error}", path.display()))
206}