Skip to main content

git_paw/
error.rs

1//! Error types for git-paw.
2//!
3//! Defines [`PawError`], the central error enum used across all modules.
4//! Each variant carries an actionable, user-facing message.
5
6use std::process;
7
8/// Exit codes for git-paw.
9pub mod exit_code {
10    /// General error.
11    pub const ERROR: i32 = 1;
12    /// User cancelled (Ctrl+C or empty selection).
13    pub const USER_CANCELLED: i32 = 2;
14}
15
16/// Central error type for git-paw operations.
17#[derive(Debug, thiserror::Error)]
18pub enum PawError {
19    /// Not inside a git repository.
20    #[error("Not a git repository. Run git-paw from inside a git project.")]
21    NotAGitRepo,
22
23    /// tmux is not installed.
24    #[error(
25        "tmux is required but not installed. Install with: brew install tmux (macOS) or apt install tmux (Linux)"
26    )]
27    TmuxNotInstalled,
28
29    /// No AI CLIs found on PATH or in config.
30    #[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    /// Git worktree operation failed.
36    #[error("Worktree error: {0}")]
37    WorktreeError(String),
38
39    /// Session state read/write failed.
40    #[error("Session error: {0}")]
41    SessionError(String),
42
43    /// Config file parsing failed.
44    #[error("Config error: {0}")]
45    ConfigError(String),
46
47    /// Branch operation failed.
48    #[error("Branch error: {0}")]
49    BranchError(String),
50
51    /// User cancelled via Ctrl+C or empty selection.
52    #[error("Cancelled.")]
53    UserCancelled,
54
55    /// tmux operation failed.
56    #[error("Tmux error: {0}")]
57    TmuxError(String),
58
59    /// Custom CLI not found in config.
60    #[error("CLI '{0}' not found in config")]
61    CliNotFound(String),
62}
63
64impl PawError {
65    /// Returns the process exit code for this error.
66    pub fn exit_code(&self) -> i32 {
67        match self {
68            Self::UserCancelled => exit_code::USER_CANCELLED,
69            _ => exit_code::ERROR,
70        }
71    }
72
73    /// Prints the error message to stderr and exits with the appropriate code.
74    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}