beads_rs/git/
error.rs

1//! Git sync error types.
2
3use std::path::PathBuf;
4
5use thiserror::Error;
6
7use crate::core::BeadId;
8use crate::core::CoreError;
9use crate::error::{Effect, Transience};
10
11/// Errors that can occur during git sync operations.
12#[derive(Error, Debug)]
13#[non_exhaustive]
14pub enum SyncError {
15    #[error("failed to open repository at {0}: {1}")]
16    OpenRepo(PathBuf, #[source] git2::Error),
17
18    #[error("failed to fetch from remote: {0}")]
19    Fetch(#[source] git2::Error),
20
21    #[error("local ref not found: {0}")]
22    NoLocalRef(String),
23
24    #[error("remote ref not found: {0}")]
25    NoRemoteRef(String),
26
27    #[error("failed to find merge base: {0}")]
28    MergeBase(#[source] git2::Error),
29
30    #[error("missing file in tree: {0}")]
31    MissingFile(String),
32
33    #[error("expected blob but got different object type: {0}")]
34    NotABlob(&'static str),
35
36    #[error("failed to write blob: {0}")]
37    WriteBlob(#[source] git2::Error),
38
39    #[error("failed to build tree: {0}")]
40    BuildTree(#[source] git2::Error),
41
42    #[error("failed to create commit: {0}")]
43    Commit(#[source] git2::Error),
44
45    #[error("push rejected (non-fast-forward)")]
46    NonFastForward,
47
48    #[error("failed to push: {0}")]
49    Push(#[source] git2::Error),
50
51    #[error("too many sync retries ({0})")]
52    TooManyRetries(usize),
53
54    #[error("ID collision detected: {0} and {1} have same ID but different content")]
55    IdCollision(BeadId, BeadId),
56
57    #[error("no common ancestor between local and remote")]
58    NoCommonAncestor,
59
60    #[error(transparent)]
61    Wire(#[from] WireError),
62
63    #[error("merge conflict: {errors:?}")]
64    MergeConflict { errors: Vec<CoreError> },
65
66    #[error("collision resolution failed: {0}")]
67    CollisionResolution(CoreError),
68
69    #[error(transparent)]
70    PushRejected(#[from] PushRejected),
71
72    #[error("init failed: {0}")]
73    InitFailed(#[source] git2::Error),
74
75    #[error("git operation failed: {0}")]
76    Git(#[from] git2::Error),
77}
78
79impl SyncError {
80    /// Whether retrying this sync may succeed.
81    pub fn transience(&self) -> Transience {
82        match self {
83            SyncError::Fetch(_)
84            | SyncError::NonFastForward
85            | SyncError::Push(_)
86            | SyncError::PushRejected(_)
87            | SyncError::TooManyRetries(_)
88            | SyncError::InitFailed(_) => Transience::Retryable,
89
90            SyncError::OpenRepo(_, _)
91            | SyncError::NoLocalRef(_)
92            | SyncError::NoRemoteRef(_)
93            | SyncError::MergeBase(_)
94            | SyncError::MissingFile(_)
95            | SyncError::NotABlob(_)
96            | SyncError::WriteBlob(_)
97            | SyncError::BuildTree(_)
98            | SyncError::Commit(_)
99            | SyncError::IdCollision(_, _)
100            | SyncError::NoCommonAncestor
101            | SyncError::Wire(_)
102            | SyncError::MergeConflict { .. }
103            | SyncError::CollisionResolution(_)
104            | SyncError::Git(_) => Transience::Permanent,
105        }
106    }
107
108    /// What we know about side effects when this error is returned.
109    pub fn effect(&self) -> Effect {
110        match self {
111            // Push-phase errors occur after a local commit was created.
112            SyncError::NonFastForward
113            | SyncError::Push(_)
114            | SyncError::PushRejected(_)
115            | SyncError::TooManyRetries(_)
116            | SyncError::InitFailed(_) => Effect::Some,
117
118            // Low-level git2 errors can happen at any phase.
119            SyncError::Git(_) => Effect::Unknown,
120
121            // Everything else fails before committing.
122            _ => Effect::None,
123        }
124    }
125}
126
127/// Errors that can occur during wire format serialization/deserialization.
128#[derive(Error, Debug)]
129pub enum WireError {
130    #[error("JSON error: {0}")]
131    Json(#[from] serde_json::Error),
132
133    #[error("UTF-8 error: {0}")]
134    Utf8(#[from] std::string::FromUtf8Error),
135
136    #[error("missing required field: {0}")]
137    MissingField(&'static str),
138
139    #[error("invalid field value: {0}")]
140    InvalidValue(String),
141}
142
143/// Push was rejected by the remote with a status message.
144#[derive(Error, Debug)]
145#[error("push rejected: {message}")]
146pub struct PushRejected {
147    pub message: String,
148}