1use std::fmt;
4
5pub type Result<T> = std::result::Result<T, Error>;
7
8#[derive(Debug)]
10pub enum Error {
11 NotATracker {
15 root: std::path::PathBuf,
17 prefix: String,
19 },
20 IssueNotFound {
22 id: String,
24 },
25 DuplicateId {
27 id: String,
29 paths: Vec<std::path::PathBuf>,
31 },
32 ClaimConflict {
34 id: String,
36 holder: String,
38 claimed_at: Option<String>,
40 },
41 BlockerCycle {
43 blocker: String,
45 issue: String,
47 },
48 InvalidState {
50 id: String,
52 state: String,
54 },
55 StaleWrite {
57 id: String,
59 expected_state: Option<String>,
61 actual_state: String,
63 expected_gen: Option<u64>,
65 actual_gen: Option<u64>,
67 },
68 TerminalConflict {
70 id: String,
72 held: String,
74 attempted: String,
76 },
77 Other(anyhow::Error),
79}
80
81impl fmt::Display for Error {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 match self {
84 Error::NotATracker { root, prefix } => write!(
85 f,
86 "{} is not a tracker: it holds neither vissue.toml nor {prefix}/. \
87 Name one with --root, or set VISSUE_ROOT",
88 root.display()
89 ),
90 Error::IssueNotFound { id } => write!(f, "issue {id} not found"),
91 Error::DuplicateId { id, paths } => {
92 let listed = paths
93 .iter()
94 .map(|p| p.display().to_string())
95 .collect::<Vec<_>>()
96 .join(", ");
97 write!(f, "id {id} is defined in more than one tracker: {listed}")
98 }
99 Error::ClaimConflict {
100 id,
101 holder,
102 claimed_at,
103 } => write!(
104 f,
105 "{id} is claimed by {holder} since {}; pass --force to take it over",
106 claimed_at.as_deref().unwrap_or("an unknown time")
107 ),
108 Error::BlockerCycle { blocker, issue } if blocker == issue => {
109 write!(f, "issue {issue} cannot block itself")
110 }
111 Error::BlockerCycle { blocker, issue } => {
112 write!(
113 f,
114 "adding {blocker} -> {issue} would create a blocker cycle"
115 )
116 }
117 Error::InvalidState { id, state } => {
118 write!(f, "{id} is already {state}; cannot claim")
119 }
120 Error::StaleWrite {
121 id,
122 expected_state,
123 actual_state,
124 expected_gen,
125 actual_gen,
126 } => match (expected_state, expected_gen) {
127 (Some(want), Some(want_gen)) => write!(
128 f,
129 "{id} is {actual_state} at generation {}, not {want} at {want_gen}; write refused",
130 actual_gen.map_or_else(|| "?".into(), |g| g.to_string())
131 ),
132 (Some(want), None) => {
133 write!(f, "{id} is {actual_state}, not {want}; write refused")
134 }
135 (None, Some(want_gen)) => write!(
136 f,
137 "{id} generation is {}, not {want_gen}; write refused",
138 actual_gen.map_or_else(|| "?".into(), |g| g.to_string())
139 ),
140 (None, None) => write!(f, "{id} write refused: stale"),
141 },
142 Error::TerminalConflict {
143 id,
144 held,
145 attempted,
146 } => write!(
147 f,
148 "{id} already closed as {held}; {attempted} kept as a sibling"
149 ),
150 Error::Other(err) => write!(f, "{err}"),
151 }
152 }
153}
154
155impl std::error::Error for Error {
156 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
157 match self {
158 Error::Other(err) => Some(err.as_ref()),
159 _ => None,
160 }
161 }
162}
163
164impl From<anyhow::Error> for Error {
165 fn from(err: anyhow::Error) -> Self {
166 match err.downcast::<Error>() {
167 Ok(typed) => typed,
168 Err(other) => Error::Other(other),
169 }
170 }
171}
172
173impl From<std::io::Error> for Error {
174 fn from(err: std::io::Error) -> Self {
175 Error::Other(err.into())
176 }
177}
178
179impl From<fmt::Error> for Error {
180 fn from(err: fmt::Error) -> Self {
181 Error::Other(err.into())
182 }
183}
184
185impl From<serde_json::Error> for Error {
186 fn from(err: serde_json::Error) -> Self {
187 Error::Other(err.into())
188 }
189}