Skip to main content

omnifuse_git/
error.rs

1//! Git backend error taxonomy.
2
3use std::path::PathBuf;
4
5use omnifuse_core::ErrorKind;
6use thiserror::Error;
7
8/// Structured git backend error.
9#[derive(Debug, Error)]
10pub enum GitError {
11  /// Backend has not been initialized yet.
12  #[error("backend not initialized")]
13  NotInitialized,
14  /// Invalid or missing git repository.
15  #[error("not a git repository: {path}")]
16  InvalidRepository {
17    /// Repository path.
18    path: PathBuf
19  },
20  /// Nothing to commit after staging.
21  #[error("nothing to commit")]
22  NothingToCommit,
23  /// Commit requested with empty file list.
24  #[error("no files to commit")]
25  NoFilesToCommit,
26  /// Network unavailable while talking to remote.
27  #[error("network unavailable: {message}")]
28  NetworkUnavailable {
29    /// Underlying message.
30    message: String
31  },
32  /// Merge or push conflict.
33  #[error("{count} file(s) in conflict", count = .files.len())]
34  Conflict {
35    /// Files in conflict.
36    files: Vec<PathBuf>
37  },
38  /// Remote rejected the push repeatedly.
39  #[error("push rejected after {retries} attempts")]
40  PushRejected {
41    /// Retry count.
42    retries: u32
43  },
44  /// Generic git command failure.
45  #[error("{op} failed: {stderr}")]
46  CommandFailed {
47    /// Command operation.
48    op: &'static str,
49    /// Stderr output.
50    stderr: String
51  }
52}
53
54/// Classify a git error into shared core taxonomy.
55#[must_use]
56pub fn classify_git_error(error: &anyhow::Error) -> Option<ErrorKind> {
57  match error.downcast_ref::<GitError>() {
58    Some(GitError::NetworkUnavailable { .. }) => Some(ErrorKind::Offline),
59    Some(GitError::Conflict { .. } | GitError::PushRejected { .. }) => Some(ErrorKind::Conflict),
60    Some(GitError::InvalidRepository { .. }) => Some(ErrorKind::InvalidConfig),
61    Some(GitError::CommandFailed { .. }) => Some(ErrorKind::BackendCommandFailed),
62    Some(GitError::NotInitialized) => Some(ErrorKind::Internal),
63    Some(GitError::NothingToCommit | GitError::NoFilesToCommit) | None => None
64  }
65}
66
67/// Whether the error means there is nothing to commit.
68#[must_use]
69pub fn is_nothing_to_commit(error: &anyhow::Error) -> bool {
70  matches!(error.downcast_ref::<GitError>(), Some(GitError::NothingToCommit))
71}
72
73#[cfg(test)]
74mod tests {
75  use super::*;
76
77  #[test]
78  fn test_classify_git_error() {
79    let offline: anyhow::Error = GitError::NetworkUnavailable {
80      message: "dns".to_string()
81    }
82    .into();
83    let conflict: anyhow::Error = GitError::Conflict {
84      files: vec!["README.md".into()]
85    }
86    .into();
87    let nothing: anyhow::Error = GitError::NothingToCommit.into();
88
89    assert_eq!(classify_git_error(&offline), Some(ErrorKind::Offline));
90    assert_eq!(classify_git_error(&conflict), Some(ErrorKind::Conflict));
91    assert_eq!(classify_git_error(&nothing), None);
92    assert!(is_nothing_to_commit(&nothing));
93  }
94}