Skip to main content

cargo_hold/
error.rs

1//! Error types for cargo-hold.
2//!
3//! This module defines all error types used throughout cargo-hold, using
4//! a combination of `thiserror` for ergonomic error definitions and `miette`
5//! for rich diagnostic output.
6//!
7//! # Error Handling Strategy
8//!
9//! - All errors derive from [`HoldError`]
10//! - Each variant includes helpful error messages and diagnostic codes
11//! - Context is preserved through the error chain
12//! - Errors are automatically converted to `miette::Result` for CLI output
13//!
14//! # Example
15//!
16//! ```no_run
17//! use std::path::Path;
18//!
19//! use cargo_hold::error::{HoldError, Result};
20//!
21//! fn check_repo(path: &Path) -> Result<()> {
22//!     // Example of returning a specific error
23//!     if !path.join(".git").exists() {
24//!         return Err(HoldError::RepoNotFound(path.to_path_buf()));
25//!     }
26//!     Ok(())
27//! }
28//! ```
29
30use std::path::PathBuf;
31
32use miette::Diagnostic;
33use thiserror::Error;
34
35/// Error types that can occur in cargo-hold operations
36#[derive(Error, Debug, Diagnostic)]
37pub enum HoldError {
38    /// Git repository not found in the current directory or any parent.
39    ///
40    /// Raised when `git2::Repository::discover()` fails or when the repository
41    /// is bare (no working directory). cargo-hold requires a Git repository to
42    /// determine which files to track for timestamp management.
43    #[error("Git repository not found in '{0}' or any parent directories")]
44    #[diagnostic(
45        code(cargo_hold::git::repo_not_found),
46        help("Ensure 'cargo hold' is run from within a Git repository.")
47    )]
48    RepoNotFound(
49        /// The path where the Git repository was searched for
50        PathBuf,
51    ),
52
53    /// Failed to read the Git index to enumerate tracked files.
54    ///
55    /// Wraps errors from `repo.index()` when cargo-hold tries to read
56    /// the list of files tracked by Git. The Git index contains the staged
57    /// and tracked files that cargo-hold needs to manage.
58    #[error("Failed to access Git index")]
59    #[diagnostic(code(cargo_hold::git::index_error))]
60    IndexError(#[from] git2::Error),
61
62    /// File system I/O error during cargo-hold operations.
63    ///
64    /// Common causes: permission denied, file not found, disk full,
65    /// or memory mapping failures. Used throughout for file operations,
66    /// directory creation/removal, and metadata access.
67    #[error("I/O error accessing '{path}'")]
68    #[diagnostic(code(cargo_hold::io_error))]
69    IoError {
70        /// The path that caused the I/O error
71        path: PathBuf,
72        /// The underlying I/O error
73        #[source]
74        source: std::io::Error,
75    },
76
77    /// Failed to serialize StateMetadata to rkyv format.
78    ///
79    /// Occurs in `save_metadata()` when rkyv serialization fails.
80    /// This is typically an internal error. The metadata file can be
81    /// reset using `cargo hold bilge`.
82    #[error("Failed to serialize metadata")]
83    #[diagnostic(
84        code(cargo_hold::metadata::serialization_error),
85        help(
86            "An internal error occurred while trying to save the metadata. Try running 'cargo \
87             hold bilge' to reset."
88        )
89    )]
90    SerializationError(#[source] Box<dyn std::error::Error + Send + Sync>),
91
92    /// Failed to deserialize metadata from rkyv format.
93    ///
94    /// Occurs when loading metadata if the file is corrupted or from
95    /// an incompatible format. cargo-hold automatically attempts recovery
96    /// by resetting the metadata when this error is encountered.
97    #[error("Failed to deserialize metadata: {0}")]
98    #[diagnostic(
99        code(cargo_hold::metadata::deserialization_error),
100        help("The metadata file may be corrupted. Run 'cargo hold bilge' to reset it.")
101    )]
102    DeserializationError(
103        /// The underlying deserialization error
104        #[source]
105        rkyv::rancor::BoxedError,
106    ),
107
108    /// Git index path contains invalid UTF-8.
109    ///
110    /// Raised when converting Git index entry paths from bytes to UTF-8
111    /// strings fails. All paths tracked by Git must be valid UTF-8 for
112    /// cargo-hold to process them.
113    #[error("Invalid path: {message}")]
114    #[diagnostic(code(cargo_hold::path::invalid))]
115    InvalidPath {
116        /// Description of why the path is invalid
117        message: String,
118    },
119
120    /// Attempted to process a non-regular file (symlink or directory).
121    ///
122    /// cargo-hold only supports regular files. This error occurs when
123    /// trying to hash, get size of, or set timestamps on symlinks or
124    /// directories, which are explicitly unsupported.
125    #[error("Invalid file type for '{0}': {1}")]
126    #[diagnostic(
127        code(cargo_hold::file::invalid_type),
128        help("cargo-hold only processes regular files tracked by Git.")
129    )]
130    InvalidFileType(
131        /// The path of the invalid file
132        PathBuf,
133        /// Description of the file type issue
134        String,
135    ),
136
137    /// Failed to restore a file's modification time.
138    ///
139    /// Occurs during the salvage operation when cargo-hold cannot
140    /// open a file for writing or call `set_modified()`. Common causes
141    /// are insufficient permissions or file system restrictions.
142    #[error("Failed to set file modification time for '{0}'")]
143    #[diagnostic(
144        code(cargo_hold::timestamp::set_error),
145        help("Ensure you have write permissions for the file.")
146    )]
147    SetTimestampError(
148        /// The file whose timestamp couldn't be set
149        PathBuf,
150        /// The underlying I/O error
151        #[source]
152        std::io::Error,
153    ),
154
155    /// Failed to create parent directory for metadata file.
156    ///
157    /// Raised when `fs::create_dir_all()` fails while preparing to
158    /// save metadata. The metadata file is typically stored at
159    /// `target/cargo-hold.metadata`.
160    #[error("Failed to create metadata directory '{0}'")]
161    #[diagnostic(
162        code(cargo_hold::metadata::create_dir_error),
163        help("Ensure you have write permissions for the parent directory.")
164    )]
165    CreateMetadataDirError(
166        /// The directory path that couldn't be created
167        PathBuf,
168        /// The underlying I/O error
169        #[source]
170        std::io::Error,
171    ),
172
173    /// Invalid size specification for --max-target-size.
174    ///
175    /// Raised when parsing size strings like "5G" or "500M" fails.
176    /// Valid suffixes are B (bytes), K (kilobytes), M (megabytes),
177    /// G (gigabytes), or T (terabytes). Numbers without suffix are bytes.
178    #[error("Invalid metadata size: '{0}' - {1}")]
179    #[diagnostic(
180        code(cargo_hold::gc::invalid_metadata_size),
181        help(
182            "Specify metadata size as a number with optional suffix (e.g., '5G', '500M', '1024K', \
183             or raw bytes)"
184        )
185    )]
186    InvalidMetadataSize(
187        /// The invalid size value provided
188        String,
189        /// Description of the parsing error
190        String,
191    ),
192
193    /// Cannot determine home directory for cargo cache cleanup.
194    ///
195    /// Raised when `home::cargo_home()` returns None during garbage
196    /// collection of ~/.cargo/registry or ~/.cargo/bin. The home
197    /// directory is needed to locate cargo's cache directories.
198    #[error("Garbage collection error: {0}")]
199    #[diagnostic(
200        code(cargo_hold::gc::error),
201        help("Check permissions and disk space, then try again.")
202    )]
203    GcError(
204        /// Description of the garbage collection error
205        String,
206    ),
207
208    /// Metadata version is newer than supported or configuration invalid.
209    ///
210    /// Raised when: 1) loaded metadata has version > METADATA_VERSION,
211    /// indicating it was created by a newer cargo-hold version, or
212    /// 2) required parameters are missing for the voyage command.
213    #[error("Configuration error: {0}")]
214    #[diagnostic(
215        code(cargo_hold::config::error),
216        help("Check the required configuration parameters.")
217    )]
218    ConfigError(
219        /// Description of the configuration error
220        String,
221    ),
222
223    /// One or more tracked files could not be hashed or read during
224    /// stow/salvage.
225    ///
226    /// The command stops instead of writing partial metadata or restoring
227    /// timestamps for only a subset of files, which would make CI report
228    /// success while incremental compilation state is wrong.
229    #[error("failed to process {failed} of {total} tracked file(s); run with -v for details")]
230    #[diagnostic(
231        code(cargo_hold::files::partial_failure),
232        help("Fix file permissions or paths, then re-run the command.")
233    )]
234    PartialFileProcessing {
235        /// Number of files that failed
236        failed: usize,
237        /// Total tracked files attempted
238        total: usize,
239    },
240
241    /// PathBuf cannot be converted to UTF-8 string for storage.
242    ///
243    /// Raised in StateMetadata operations when a PathBuf contains
244    /// non-UTF-8 sequences. All paths must be valid UTF-8 for storage
245    /// in the metadata format and compatibility with Git.
246    #[error("Invalid UTF-8 in path: {0}")]
247    #[diagnostic(
248        code(cargo_hold::path::invalid_utf8),
249        help("File paths must be valid UTF-8. This is a requirement for Git-tracked files.")
250    )]
251    InvalidUtf8Path(
252        /// The path containing invalid UTF-8
253        PathBuf,
254    ),
255}
256
257/// Type alias for Results in this crate
258pub type Result<T> = std::result::Result<T, HoldError>;