use std::time::Duration;
#[derive(Debug, thiserror::Error)]
pub enum GitError {
#[error("{command}: {stderr}")]
CommandFailed {
command: String,
stderr: String,
},
#[error("{command} timed out after {timeout:?}")]
CommandTimedOut {
command: String,
timeout: Duration,
},
#[error("{0}")]
OutputParse(String),
#[error("{0}")]
Io(#[from] std::io::Error),
#[error(
"pre-commit validation is configured by `{config_file}`, but the Git pre-commit hook is \
not installed or executable. Install it with one of these commands:\n\n prek install\n \
pre-commit install\n\nAgentty will continue for now, but missing configured hooks will \
become an error in a future release."
)]
PreCommitHookMissing {
config_file: String,
},
#[error("Join error: {0}")]
Join(#[from] tokio::task::JoinError),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn command_failed_display_includes_command_and_stderr() {
let error = GitError::CommandFailed {
command: "git push origin main".to_string(),
stderr: "fatal: could not read Username".to_string(),
};
let display = error.to_string();
assert!(matches!(
error,
GitError::CommandFailed {
ref command,
ref stderr,
} if command == "git push origin main" && stderr == "fatal: could not read Username"
));
assert_eq!(
display,
"git push origin main: fatal: could not read Username"
);
}
#[test]
fn command_timed_out_display_includes_command_and_timeout() {
let error = GitError::CommandTimedOut {
command: "git worktree remove --force /tmp/worktree".to_string(),
timeout: Duration::from_secs(30),
};
let display = error.to_string();
assert_eq!(
display,
"git worktree remove --force /tmp/worktree timed out after 30s"
);
}
#[test]
fn output_parse_display_shows_message() {
let error = GitError::OutputParse("unexpected rev-parse output".to_string());
assert!(
matches!(error, GitError::OutputParse(ref message) if message == "unexpected rev-parse output")
);
assert_eq!(error.to_string(), "unexpected rev-parse output");
}
#[test]
fn io_error_converts_via_from() {
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
let error = GitError::from(io_error);
assert!(matches!(error, GitError::Io(_)));
assert!(error.to_string().contains("file missing"));
}
#[test]
fn pre_commit_hook_missing_display_explains_required_setup() {
let error = GitError::PreCommitHookMissing {
config_file: ".pre-commit-config.yaml".to_string(),
};
let display = error.to_string();
assert!(display.contains(".pre-commit-config.yaml"));
assert!(display.contains("not installed or executable"));
assert!(display.contains("prek install"));
assert!(display.contains("pre-commit install"));
assert!(display.contains("will become an error in a future release"));
}
}