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("{0}")]
78 Operation(String),
79
80 #[error("{0}")]
82 Io(#[from] std::io::Error),
83
84 #[error("json error: {0}")]
86 Json(#[from] serde_json::Error),
87}
88
89impl Error {
90 pub fn exit_code(&self) -> u8 {
93 match self {
94 Error::Usage(_) => 2,
95 Error::Ambiguous { .. } | Error::NothingSelected => 3,
96 _ => 1,
97 }
98 }
99
100 pub fn operation(message: impl Into<String>) -> Self {
102 Error::Operation(message.into())
103 }
104
105 pub fn usage(message: impl Into<String>) -> Self {
107 Error::Usage(message.into())
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn exit_codes_match_spec() {
117 assert_eq!(Error::usage("x").exit_code(), 2);
118 assert_eq!(
119 Error::Ambiguous {
120 query: "f".into(),
121 candidates: vec!["a".into(), "b".into()],
122 }
123 .exit_code(),
124 3
125 );
126 assert_eq!(Error::NothingSelected.exit_code(), 3);
127 assert_eq!(Error::NotFound { query: "f".into() }.exit_code(), 1);
128 assert_eq!(Error::NotInRepo.exit_code(), 1);
129 assert_eq!(Error::NoCurrentWorktree.exit_code(), 1);
130 assert_eq!(
131 Error::Config {
132 file: "c".into(),
133 key: "k".into(),
134 reason: "r".into(),
135 }
136 .exit_code(),
137 1
138 );
139 assert_eq!(
140 Error::Subprocess {
141 program: "git".into(),
142 stderr: "boom".into(),
143 }
144 .exit_code(),
145 1
146 );
147 assert_eq!(Error::GhUnavailable("gh".into()).exit_code(), 1);
148 assert_eq!(Error::AgentUnavailable("a".into()).exit_code(), 1);
149 assert_eq!(Error::operation("op").exit_code(), 1);
150 assert_eq!(Error::from(std::io::Error::other("io")).exit_code(), 1);
151 let json_err = serde_json::from_str::<i32>("nope").unwrap_err();
152 assert_eq!(Error::from(json_err).exit_code(), 1);
153 }
154
155 #[test]
156 fn display_messages_are_descriptive() {
157 assert!(Error::NotInRepo.to_string().contains("git repository"));
158 assert!(
159 Error::NoCurrentWorktree
160 .to_string()
161 .contains("no current worktree")
162 );
163 assert!(
164 Error::Ambiguous {
165 query: "feat".into(),
166 candidates: vec!["a".into(), "b".into()],
167 }
168 .to_string()
169 .contains("ambiguous")
170 );
171 assert!(
172 Error::NotFound { query: "x".into() }
173 .to_string()
174 .contains("no worktree")
175 );
176 assert_eq!(Error::NothingSelected.to_string(), "nothing selected");
177 assert_eq!(Error::usage("oops").to_string(), "oops");
178 assert_eq!(
179 Error::Config {
180 file: "f".into(),
181 key: "k".into(),
182 reason: "r".into(),
183 }
184 .to_string(),
185 "f: k: r"
186 );
187 assert_eq!(
188 Error::Subprocess {
189 program: "gh".into(),
190 stderr: "no auth".into(),
191 }
192 .to_string(),
193 "gh failed: no auth"
194 );
195 assert_eq!(Error::GhUnavailable("nope".into()).to_string(), "nope");
196 assert_eq!(Error::AgentUnavailable("nope".into()).to_string(), "nope");
197 assert_eq!(Error::operation("op").to_string(), "op");
198 assert_eq!(Error::from(std::io::Error::other("io")).to_string(), "io");
199 }
200}