1use std::process;
7
8pub mod exit_code {
10 pub const ERROR: i32 = 1;
12 pub const USER_CANCELLED: i32 = 2;
14}
15
16#[derive(Debug, thiserror::Error)]
18pub enum PawError {
19 #[error("Not a git repository. Run git-paw from inside a git project.")]
21 NotAGitRepo,
22
23 #[error(
25 "tmux is required but not installed. Install with: brew install tmux (macOS) or apt install tmux (Linux)"
26 )]
27 TmuxNotInstalled,
28
29 #[error(
31 "No AI CLIs found on PATH. Install one or use `git paw add-cli` to register a custom CLI."
32 )]
33 NoCLIsFound,
34
35 #[error("Worktree error: {0}")]
37 WorktreeError(String),
38
39 #[error("Session error: {0}")]
41 SessionError(String),
42
43 #[error("Config error: {0}")]
45 ConfigError(String),
46
47 #[error("Branch error: {0}")]
49 BranchError(String),
50
51 #[error("Cancelled.")]
53 UserCancelled,
54
55 #[error("Tmux error: {0}")]
57 TmuxError(String),
58
59 #[error("CLI '{0}' not found in config")]
61 CliNotFound(String),
62}
63
64impl PawError {
65 pub fn exit_code(&self) -> i32 {
67 match self {
68 Self::UserCancelled => exit_code::USER_CANCELLED,
69 _ => exit_code::ERROR,
70 }
71 }
72
73 pub fn exit(&self) -> ! {
75 eprintln!("error: {self}");
76 process::exit(self.exit_code());
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn test_not_a_git_repo_is_actionable() {
86 let msg = PawError::NotAGitRepo.to_string();
87 assert!(msg.contains("git repository"), "should explain the problem");
88 assert!(msg.contains("git-paw"), "should name the tool");
89 }
90
91 #[test]
92 fn test_tmux_not_installed_includes_install_instructions() {
93 let msg = PawError::TmuxNotInstalled.to_string();
94 assert!(msg.contains("tmux"), "should name the missing dependency");
95 assert!(
96 msg.contains("brew install"),
97 "should include macOS install hint"
98 );
99 assert!(
100 msg.contains("apt install"),
101 "should include Linux install hint"
102 );
103 }
104
105 #[test]
106 fn test_no_clis_found_suggests_add_cli() {
107 let msg = PawError::NoCLIsFound.to_string();
108 assert!(
109 msg.contains("add-cli"),
110 "should suggest the add-cli command"
111 );
112 }
113
114 #[test]
115 fn test_worktree_error_includes_detail() {
116 let msg = PawError::WorktreeError("failed to create".into()).to_string();
117 assert!(
118 msg.contains("failed to create"),
119 "should include the inner detail"
120 );
121 }
122
123 #[test]
124 fn test_session_error_includes_detail() {
125 let msg = PawError::SessionError("file corrupt".into()).to_string();
126 assert!(
127 msg.contains("file corrupt"),
128 "should include the inner detail"
129 );
130 }
131
132 #[test]
133 fn test_config_error_includes_detail() {
134 let msg = PawError::ConfigError("invalid toml".into()).to_string();
135 assert!(
136 msg.contains("invalid toml"),
137 "should include the inner detail"
138 );
139 }
140
141 #[test]
142 fn test_branch_error_includes_detail() {
143 let msg = PawError::BranchError("not found".into()).to_string();
144 assert!(msg.contains("not found"), "should include the inner detail");
145 }
146
147 #[test]
148 fn test_user_cancelled_is_not_empty() {
149 let msg = PawError::UserCancelled.to_string();
150 assert!(!msg.is_empty(), "should have a message");
151 }
152
153 #[test]
154 fn test_tmux_error_includes_detail() {
155 let msg = PawError::TmuxError("session failed".into()).to_string();
156 assert!(
157 msg.contains("session failed"),
158 "should include the inner detail"
159 );
160 }
161
162 #[test]
163 fn test_cli_not_found_includes_cli_name() {
164 let msg = PawError::CliNotFound("my-agent".into()).to_string();
165 assert!(
166 msg.contains("my-agent"),
167 "should include the missing CLI name"
168 );
169 }
170
171 #[test]
172 fn test_user_cancelled_exit_code() {
173 assert_eq!(
174 PawError::UserCancelled.exit_code(),
175 exit_code::USER_CANCELLED
176 );
177 }
178
179 #[test]
180 fn test_general_errors_exit_code() {
181 let errors: Vec<PawError> = vec![
182 PawError::NotAGitRepo,
183 PawError::TmuxNotInstalled,
184 PawError::NoCLIsFound,
185 PawError::WorktreeError("test".into()),
186 PawError::SessionError("test".into()),
187 PawError::ConfigError("test".into()),
188 PawError::BranchError("test".into()),
189 PawError::TmuxError("test".into()),
190 PawError::CliNotFound("test".into()),
191 ];
192 for err in errors {
193 assert_eq!(err.exit_code(), exit_code::ERROR, "failed for {err:?}");
194 }
195 }
196
197 #[test]
198 fn test_debug_derived() {
199 let err = PawError::NotAGitRepo;
200 let debug = format!("{err:?}");
201 assert!(debug.contains("NotAGitRepo"));
202 }
203}