1use std::time::Duration;
2
3#[derive(Debug, thiserror::Error)]
8pub enum GitError {
9 #[error("{command}: {stderr}")]
11 CommandFailed {
12 command: String,
14 stderr: String,
16 },
17
18 #[error("{command} timed out after {timeout:?}")]
20 CommandTimedOut {
21 command: String,
23 timeout: Duration,
25 },
26
27 #[error("{0}")]
29 OutputParse(String),
30
31 #[error("{detail}")]
33 RepositoryUnavailable {
34 detail: String,
36 },
37
38 #[error("{0}")]
40 Io(#[from] std::io::Error),
41
42 #[error(
45 "pre-commit validation is configured by `{config_file}`, but the Git pre-commit hook is \
46 not installed or executable. Install it with one of these commands:\n\n prek install\n \
47 pre-commit install\n\nAgentty will continue for now, but missing configured hooks will \
48 become an error in a future release."
49 )]
50 PreCommitHookMissing {
51 config_file: String,
54 },
55
56 #[error("Join error: {0}")]
58 Join(#[from] tokio::task::JoinError),
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 #[test]
66 fn command_failed_display_includes_command_and_stderr() {
67 let error = GitError::CommandFailed {
69 command: "git push origin main".to_string(),
70 stderr: "fatal: could not read Username".to_string(),
71 };
72
73 let display = error.to_string();
75
76 assert!(matches!(
78 error,
79 GitError::CommandFailed {
80 ref command,
81 ref stderr,
82 } if command == "git push origin main" && stderr == "fatal: could not read Username"
83 ));
84 assert_eq!(
85 display,
86 "git push origin main: fatal: could not read Username"
87 );
88 }
89
90 #[test]
91 fn command_timed_out_display_includes_command_and_timeout() {
92 let error = GitError::CommandTimedOut {
94 command: "git worktree remove --force /tmp/worktree".to_string(),
95 timeout: Duration::from_secs(30),
96 };
97
98 let display = error.to_string();
100
101 assert_eq!(
103 display,
104 "git worktree remove --force /tmp/worktree timed out after 30s"
105 );
106 }
107
108 #[test]
109 fn output_parse_display_shows_message() {
110 let error = GitError::OutputParse("unexpected rev-parse output".to_string());
112
113 assert!(
115 matches!(error, GitError::OutputParse(ref message) if message == "unexpected rev-parse output")
116 );
117 assert_eq!(error.to_string(), "unexpected rev-parse output");
118 }
119
120 #[test]
121 fn repository_unavailable_display_shows_original_detail() {
122 let error = GitError::RepositoryUnavailable {
124 detail: "git rev-parse: not a git repository".to_string(),
125 };
126
127 let display = error.to_string();
129
130 assert_eq!(display, "git rev-parse: not a git repository");
132 }
133
134 #[test]
135 fn io_error_converts_via_from() {
136 let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
138
139 let error = GitError::from(io_error);
141
142 assert!(matches!(error, GitError::Io(_)));
144 assert!(error.to_string().contains("file missing"));
145 }
146
147 #[test]
148 fn pre_commit_hook_missing_display_explains_required_setup() {
149 let error = GitError::PreCommitHookMissing {
151 config_file: ".pre-commit-config.yaml".to_string(),
152 };
153
154 let display = error.to_string();
156
157 assert!(display.contains(".pre-commit-config.yaml"));
159 assert!(display.contains("not installed or executable"));
160 assert!(display.contains("prek install"));
161 assert!(display.contains("pre-commit install"));
162 assert!(display.contains("will become an error in a future release"));
163 }
164}