1use thiserror::Error;
7
8#[derive(Error, Debug)]
10pub enum GitError {
11 #[error("Git not found. Please install git and ensure it's in your PATH")]
13 GitNotFound,
14
15 #[error("Clone failed for {repo}: {message}")]
17 CloneFailed {
18 repo: String,
20 message: String,
22 },
23
24 #[error("Fetch failed for {repo}: {message}")]
26 FetchFailed {
27 repo: String,
29 message: String,
31 },
32
33 #[error("Pull failed for {repo}: {message}")]
35 PullFailed {
36 repo: String,
38 message: String,
40 },
41
42 #[error("Repository has uncommitted changes: {path}")]
44 UncommittedRepository {
45 path: String,
47 },
48
49 #[error("Not a git repository: {path}")]
51 NotARepository {
52 path: String,
54 },
55
56 #[error("Permission denied: {0}")]
58 PermissionDenied(String),
59
60 #[error("SSH key not configured for {host}. Run 'ssh -T git@{host}' to test")]
62 SshKeyMissing {
63 host: String,
65 },
66
67 #[error("SSH authentication failed for {host}: {message}")]
69 SshAuthFailed {
70 host: String,
72 message: String,
74 },
75
76 #[error("Git command failed: {0}")]
78 CommandFailed(String),
79
80 #[error("Git operation timed out after {seconds} seconds")]
82 Timeout {
83 seconds: u64,
85 },
86}
87
88impl GitError {
89 pub fn clone_failed(repo: impl Into<String>, message: impl Into<String>) -> Self {
91 GitError::CloneFailed {
92 repo: repo.into(),
93 message: message.into(),
94 }
95 }
96
97 pub fn fetch_failed(repo: impl AsRef<std::path::Path>, message: impl Into<String>) -> Self {
99 GitError::FetchFailed {
100 repo: repo.as_ref().to_string_lossy().to_string(),
101 message: message.into(),
102 }
103 }
104
105 pub fn pull_failed(repo: impl AsRef<std::path::Path>, message: impl Into<String>) -> Self {
107 GitError::PullFailed {
108 repo: repo.as_ref().to_string_lossy().to_string(),
109 message: message.into(),
110 }
111 }
112
113 pub fn command_failed(command: impl Into<String>, message: impl Into<String>) -> Self {
115 GitError::CommandFailed(format!("{}: {}", command.into(), message.into()))
116 }
117
118 pub fn is_skippable(&self) -> bool {
121 matches!(
122 self,
123 GitError::UncommittedRepository { .. }
124 | GitError::PermissionDenied(_)
125 | GitError::SshKeyMissing { .. }
126 | GitError::SshAuthFailed { .. }
127 )
128 }
129
130 pub fn is_retryable(&self) -> bool {
132 matches!(self, GitError::Timeout { .. })
133 }
134
135 pub fn suggested_action(&self) -> &'static str {
137 match self {
138 GitError::GitNotFound => "Install git from https://git-scm.com/downloads",
139 GitError::CloneFailed { .. } => "Check the repository URL and your network connection",
140 GitError::FetchFailed { .. } | GitError::PullFailed { .. } => {
141 "Check your network connection and repository access"
142 }
143 GitError::UncommittedRepository { .. } => "Commit or stash your changes before syncing",
144 GitError::NotARepository { .. } => {
145 "The directory exists but is not a git repository. Remove it to clone fresh"
146 }
147 GitError::PermissionDenied(_) => "Check file permissions and your authentication",
148 GitError::SshKeyMissing { .. } => {
149 "Add your SSH key to the git hosting service, or use HTTPS authentication"
150 }
151 GitError::SshAuthFailed { .. } => {
152 "Check your SSH key configuration with your git host (e.g. 'ssh -T git@<host>')"
153 }
154 GitError::CommandFailed(_) => "Check the error message and try again",
155 GitError::Timeout { .. } => {
156 "The operation took too long. Try with a smaller repository or better connection"
157 }
158 }
159 }
160
161 pub fn repo_identifier(&self) -> Option<&str> {
163 match self {
164 GitError::CloneFailed { repo, .. }
165 | GitError::FetchFailed { repo, .. }
166 | GitError::PullFailed { repo, .. } => Some(repo),
167 GitError::UncommittedRepository { path } | GitError::NotARepository { path } => {
168 Some(path)
169 }
170 _ => None,
171 }
172 }
173}
174
175#[cfg(test)]
176#[path = "git_tests.rs"]
177mod tests;