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("{0}")]
33 Io(#[from] std::io::Error),
34
35 #[error(
38 "pre-commit validation is configured by `{config_file}`, but the Git pre-commit hook is \
39 not installed or executable. Install it with one of these commands:\n\n prek install\n \
40 pre-commit install\n\nAgentty will continue for now, but missing configured hooks will \
41 become an error in a future release."
42 )]
43 PreCommitHookMissing {
44 config_file: String,
47 },
48
49 #[error("Join error: {0}")]
51 Join(#[from] tokio::task::JoinError),
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57
58 #[test]
59 fn command_failed_display_includes_command_and_stderr() {
60 let error = GitError::CommandFailed {
62 command: "git push origin main".to_string(),
63 stderr: "fatal: could not read Username".to_string(),
64 };
65
66 let display = error.to_string();
68
69 assert!(matches!(
71 error,
72 GitError::CommandFailed {
73 ref command,
74 ref stderr,
75 } if command == "git push origin main" && stderr == "fatal: could not read Username"
76 ));
77 assert_eq!(
78 display,
79 "git push origin main: fatal: could not read Username"
80 );
81 }
82
83 #[test]
84 fn command_timed_out_display_includes_command_and_timeout() {
85 let error = GitError::CommandTimedOut {
87 command: "git worktree remove --force /tmp/worktree".to_string(),
88 timeout: Duration::from_secs(30),
89 };
90
91 let display = error.to_string();
93
94 assert_eq!(
96 display,
97 "git worktree remove --force /tmp/worktree timed out after 30s"
98 );
99 }
100
101 #[test]
102 fn output_parse_display_shows_message() {
103 let error = GitError::OutputParse("unexpected rev-parse output".to_string());
105
106 assert!(
108 matches!(error, GitError::OutputParse(ref message) if message == "unexpected rev-parse output")
109 );
110 assert_eq!(error.to_string(), "unexpected rev-parse output");
111 }
112
113 #[test]
114 fn io_error_converts_via_from() {
115 let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
117
118 let error = GitError::from(io_error);
120
121 assert!(matches!(error, GitError::Io(_)));
123 assert!(error.to_string().contains("file missing"));
124 }
125
126 #[test]
127 fn pre_commit_hook_missing_display_explains_required_setup() {
128 let error = GitError::PreCommitHookMissing {
130 config_file: ".pre-commit-config.yaml".to_string(),
131 };
132
133 let display = error.to_string();
135
136 assert!(display.contains(".pre-commit-config.yaml"));
138 assert!(display.contains("not installed or executable"));
139 assert!(display.contains("prek install"));
140 assert!(display.contains("pre-commit install"));
141 assert!(display.contains("will become an error in a future release"));
142 }
143}