Skip to main content

git_same/errors/
git.rs

1//! Git operation error types.
2//!
3//! These errors represent failures that occur when executing git commands
4//! via the shell (clone, fetch, pull, etc.).
5
6use thiserror::Error;
7
8/// Errors that occur during git operations.
9#[derive(Error, Debug)]
10pub enum GitError {
11    /// Git executable not found in PATH.
12    #[error("Git not found. Please install git and ensure it's in your PATH")]
13    GitNotFound,
14
15    /// Clone operation failed.
16    #[error("Clone failed for {repo}: {message}")]
17    CloneFailed {
18        /// Repository URL or name
19        repo: String,
20        /// Error message from git
21        message: String,
22    },
23
24    /// Fetch operation failed.
25    #[error("Fetch failed for {repo}: {message}")]
26    FetchFailed {
27        /// Repository path or name
28        repo: String,
29        /// Error message from git
30        message: String,
31    },
32
33    /// Pull operation failed.
34    #[error("Pull failed for {repo}: {message}")]
35    PullFailed {
36        /// Repository path or name
37        repo: String,
38        /// Error message from git
39        message: String,
40    },
41
42    /// Repository has uncommitted changes that would be overwritten.
43    #[error("Repository has uncommitted changes: {path}")]
44    UncommittedRepository {
45        /// Path to the repository
46        path: String,
47    },
48
49    /// Path is not a git repository.
50    #[error("Not a git repository: {path}")]
51    NotARepository {
52        /// Path that was expected to be a repository
53        path: String,
54    },
55
56    /// Permission denied during git operation.
57    #[error("Permission denied: {0}")]
58    PermissionDenied(String),
59
60    /// SSH key not configured for the host.
61    #[error("SSH key not configured for {host}. Run 'ssh -T git@{host}' to test")]
62    SshKeyMissing {
63        /// The git host (e.g., github.com)
64        host: String,
65    },
66
67    /// SSH authentication failed.
68    #[error("SSH authentication failed for {host}: {message}")]
69    SshAuthFailed {
70        /// The git host
71        host: String,
72        /// Error message
73        message: String,
74    },
75
76    /// Generic command execution failure.
77    #[error("Git command failed: {0}")]
78    CommandFailed(String),
79
80    /// Timeout during git operation.
81    #[error("Git operation timed out after {seconds} seconds")]
82    Timeout {
83        /// Number of seconds before timeout
84        seconds: u64,
85    },
86}
87
88impl GitError {
89    /// Creates a clone failed error.
90    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    /// Creates a fetch failed error.
98    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    /// Creates a pull failed error.
106    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    /// Creates a command failed error.
114    pub fn command_failed(command: impl Into<String>, message: impl Into<String>) -> Self {
115        GitError::CommandFailed(format!("{}: {}", command.into(), message.into()))
116    }
117
118    /// Returns `true` if this error indicates the repository can be skipped
119    /// safely without affecting other operations.
120    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    /// Returns `true` if this error might be resolved by retrying.
131    pub fn is_retryable(&self) -> bool {
132        matches!(self, GitError::Timeout { .. })
133    }
134
135    /// Returns a user-friendly suggestion for how to resolve this error.
136    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    /// Extracts the repository identifier from the error, if available.
162    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;