1pub type Result<T> = std::result::Result<T, Error>;
10
11#[derive(Debug, thiserror::Error)]
13pub enum Error {
14 #[error("not in a git repository")]
16 NotInRepo,
17
18 #[error("no current worktree (bare repository); pass a query")]
21 NoCurrentWorktree,
22
23 #[error("query {query:?} is ambiguous ({} candidates)", candidates.len())]
25 Ambiguous {
26 query: String,
28 candidates: Vec<String>,
30 },
31
32 #[error("no worktree matches {query:?}")]
34 NotFound {
35 query: String,
37 },
38
39 #[error("nothing selected")]
41 NothingSelected,
42
43 #[error("{0}")]
45 Usage(String),
46
47 #[error("{file}: {key}: {reason}")]
49 Config {
50 file: String,
52 key: String,
54 reason: String,
56 },
57
58 #[error("{program} failed: {stderr}")]
61 Subprocess {
62 program: String,
64 stderr: String,
66 },
67
68 #[error("{0}")]
70 GhUnavailable(String),
71
72 #[error("{0}")]
74 AgentUnavailable(String),
75
76 #[error("{binary} did not respond within {seconds}s")]
80 AgentTimeout {
81 binary: String,
83 seconds: u64,
85 },
86
87 #[error(
91 "repository metadata schema (wt.schema = {found}) is newer than this wt supports \
92 (up to {supported}); upgrade wt to work with this repository"
93 )]
94 SchemaTooNew {
95 found: u64,
97 supported: u64,
99 },
100
101 #[error("could not acquire the wt repository lock at {path}: {reason}")]
104 LockUnavailable {
105 path: String,
107 reason: String,
109 },
110
111 #[error("worktree {}; use --force to remove anyway", guard_reasons(*dirty, *unpushed))]
114 RemoveGuarded {
115 dirty: bool,
118 unpushed: bool,
120 },
121
122 #[error("{0}")]
124 Operation(String),
125
126 #[error("{0}")]
128 Io(#[from] std::io::Error),
129
130 #[error("json error: {0}")]
132 Json(#[from] serde_json::Error),
133}
134
135fn guard_reasons(dirty: bool, unpushed: bool) -> String {
138 let mut reasons = Vec::new();
139 if dirty {
140 reasons.push("has uncommitted changes");
141 }
142 if unpushed {
143 reasons.push("has unpushed work");
144 }
145 reasons.join(" and ")
146}
147
148impl Error {
149 pub fn exit_code(&self) -> u8 {
152 match self {
153 Error::Usage(_) => 2,
154 Error::Ambiguous { .. } | Error::NothingSelected => 3,
155 _ => 1,
156 }
157 }
158
159 pub fn operation(message: impl Into<String>) -> Self {
161 Error::Operation(message.into())
162 }
163
164 pub fn usage(message: impl Into<String>) -> Self {
166 Error::Usage(message.into())
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn exit_codes_match_spec() {
176 assert_eq!(Error::usage("x").exit_code(), 2);
177 assert_eq!(
178 Error::Ambiguous {
179 query: "f".into(),
180 candidates: vec!["a".into(), "b".into()],
181 }
182 .exit_code(),
183 3
184 );
185 assert_eq!(Error::NothingSelected.exit_code(), 3);
186 assert_eq!(Error::NotFound { query: "f".into() }.exit_code(), 1);
187 assert_eq!(Error::NotInRepo.exit_code(), 1);
188 assert_eq!(Error::NoCurrentWorktree.exit_code(), 1);
189 assert_eq!(
190 Error::Config {
191 file: "c".into(),
192 key: "k".into(),
193 reason: "r".into(),
194 }
195 .exit_code(),
196 1
197 );
198 assert_eq!(
199 Error::Subprocess {
200 program: "git".into(),
201 stderr: "boom".into(),
202 }
203 .exit_code(),
204 1
205 );
206 assert_eq!(
207 Error::RemoveGuarded {
208 dirty: true,
209 unpushed: false,
210 }
211 .exit_code(),
212 1
213 );
214 assert_eq!(Error::GhUnavailable("gh".into()).exit_code(), 1);
215 assert_eq!(Error::AgentUnavailable("a".into()).exit_code(), 1);
216 assert_eq!(Error::operation("op").exit_code(), 1);
217 assert_eq!(Error::from(std::io::Error::other("io")).exit_code(), 1);
218 let json_err = serde_json::from_str::<i32>("nope").unwrap_err();
219 assert_eq!(Error::from(json_err).exit_code(), 1);
220 }
221
222 #[test]
223 fn display_messages_are_descriptive() {
224 assert!(Error::NotInRepo.to_string().contains("git repository"));
225 assert!(
226 Error::NoCurrentWorktree
227 .to_string()
228 .contains("no current worktree")
229 );
230 assert!(
231 Error::Ambiguous {
232 query: "feat".into(),
233 candidates: vec!["a".into(), "b".into()],
234 }
235 .to_string()
236 .contains("ambiguous")
237 );
238 assert!(
239 Error::NotFound { query: "x".into() }
240 .to_string()
241 .contains("no worktree")
242 );
243 assert_eq!(Error::NothingSelected.to_string(), "nothing selected");
244 assert_eq!(Error::usage("oops").to_string(), "oops");
245 assert_eq!(
246 Error::Config {
247 file: "f".into(),
248 key: "k".into(),
249 reason: "r".into(),
250 }
251 .to_string(),
252 "f: k: r"
253 );
254 assert_eq!(
255 Error::Subprocess {
256 program: "gh".into(),
257 stderr: "no auth".into(),
258 }
259 .to_string(),
260 "gh failed: no auth"
261 );
262 assert_eq!(
263 Error::RemoveGuarded {
264 dirty: true,
265 unpushed: true,
266 }
267 .to_string(),
268 "worktree has uncommitted changes and has unpushed work; use --force to remove anyway"
269 );
270 assert_eq!(
271 Error::RemoveGuarded {
272 dirty: false,
273 unpushed: true,
274 }
275 .to_string(),
276 "worktree has unpushed work; use --force to remove anyway"
277 );
278 assert_eq!(Error::GhUnavailable("nope".into()).to_string(), "nope");
279 assert_eq!(Error::AgentUnavailable("nope".into()).to_string(), "nope");
280 assert_eq!(Error::operation("op").to_string(), "op");
281 assert_eq!(Error::from(std::io::Error::other("io")).to_string(), "io");
282 }
283}