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("worktree is locked{}; use --locked", lock_reason_suffix(reason.as_deref()))]
126 WorktreeLocked {
127 reason: Option<String>,
129 },
130
131 #[error("{0}")]
133 Operation(String),
134
135 #[error("{0}")]
137 Io(#[from] std::io::Error),
138
139 #[error("json error: {0}")]
141 Json(#[from] serde_json::Error),
142}
143
144fn guard_reasons(dirty: bool, unpushed: bool) -> String {
147 let mut reasons = Vec::new();
148 if dirty {
149 reasons.push("has uncommitted changes");
150 }
151 if unpushed {
152 reasons.push("has unpushed work");
153 }
154 reasons.join(" and ")
155}
156
157fn lock_reason_suffix(reason: Option<&str>) -> String {
160 reason.map(|r| format!(" ({r})")).unwrap_or_default()
161}
162
163impl Error {
164 pub fn exit_code(&self) -> u8 {
167 match self {
168 Error::Usage(_) => 2,
169 Error::Ambiguous { .. } | Error::NothingSelected => 3,
170 _ => 1,
171 }
172 }
173
174 pub fn operation(message: impl Into<String>) -> Self {
176 Error::Operation(message.into())
177 }
178
179 pub fn usage(message: impl Into<String>) -> Self {
181 Error::Usage(message.into())
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn exit_codes_match_spec() {
191 assert_eq!(Error::usage("x").exit_code(), 2);
192 assert_eq!(
193 Error::Ambiguous {
194 query: "f".into(),
195 candidates: vec!["a".into(), "b".into()],
196 }
197 .exit_code(),
198 3
199 );
200 assert_eq!(Error::NothingSelected.exit_code(), 3);
201 assert_eq!(Error::NotFound { query: "f".into() }.exit_code(), 1);
202 assert_eq!(Error::NotInRepo.exit_code(), 1);
203 assert_eq!(Error::NoCurrentWorktree.exit_code(), 1);
204 assert_eq!(
205 Error::Config {
206 file: "c".into(),
207 key: "k".into(),
208 reason: "r".into(),
209 }
210 .exit_code(),
211 1
212 );
213 assert_eq!(
214 Error::Subprocess {
215 program: "git".into(),
216 stderr: "boom".into(),
217 }
218 .exit_code(),
219 1
220 );
221 assert_eq!(
222 Error::RemoveGuarded {
223 dirty: true,
224 unpushed: false,
225 }
226 .exit_code(),
227 1
228 );
229 assert_eq!(Error::GhUnavailable("gh".into()).exit_code(), 1);
230 assert_eq!(Error::AgentUnavailable("a".into()).exit_code(), 1);
231 assert_eq!(Error::operation("op").exit_code(), 1);
232 assert_eq!(Error::from(std::io::Error::other("io")).exit_code(), 1);
233 let json_err = serde_json::from_str::<i32>("nope").unwrap_err();
234 assert_eq!(Error::from(json_err).exit_code(), 1);
235 }
236
237 #[test]
238 fn display_messages_are_descriptive() {
239 assert!(Error::NotInRepo.to_string().contains("git repository"));
240 assert!(
241 Error::NoCurrentWorktree
242 .to_string()
243 .contains("no current worktree")
244 );
245 assert!(
246 Error::Ambiguous {
247 query: "feat".into(),
248 candidates: vec!["a".into(), "b".into()],
249 }
250 .to_string()
251 .contains("ambiguous")
252 );
253 assert!(
254 Error::NotFound { query: "x".into() }
255 .to_string()
256 .contains("no worktree")
257 );
258 assert_eq!(Error::NothingSelected.to_string(), "nothing selected");
259 assert_eq!(Error::usage("oops").to_string(), "oops");
260 assert_eq!(
261 Error::Config {
262 file: "f".into(),
263 key: "k".into(),
264 reason: "r".into(),
265 }
266 .to_string(),
267 "f: k: r"
268 );
269 assert_eq!(
270 Error::Subprocess {
271 program: "gh".into(),
272 stderr: "no auth".into(),
273 }
274 .to_string(),
275 "gh failed: no auth"
276 );
277 assert_eq!(
278 Error::RemoveGuarded {
279 dirty: true,
280 unpushed: true,
281 }
282 .to_string(),
283 "worktree has uncommitted changes and has unpushed work; use --force to remove anyway"
284 );
285 assert_eq!(
286 Error::RemoveGuarded {
287 dirty: false,
288 unpushed: true,
289 }
290 .to_string(),
291 "worktree has unpushed work; use --force to remove anyway"
292 );
293 assert_eq!(
295 Error::WorktreeLocked {
296 reason: Some("some reason".into()),
297 }
298 .to_string(),
299 "worktree is locked (some reason); use --locked"
300 );
301 assert_eq!(
302 Error::WorktreeLocked { reason: None }.to_string(),
303 "worktree is locked; use --locked"
304 );
305 assert_eq!(Error::WorktreeLocked { reason: None }.exit_code(), 1);
306 assert_eq!(Error::GhUnavailable("nope".into()).to_string(), "nope");
307 assert_eq!(Error::AgentUnavailable("nope".into()).to_string(), "nope");
308 assert_eq!(Error::operation("op").to_string(), "op");
309 assert_eq!(Error::from(std::io::Error::other("io")).to_string(), "io");
310 }
311}