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