Skip to main content

git_closure/
error.rs

1use std::io;
2
3use thiserror::Error;
4
5/// Canonical error type returned by `git-closure` library operations.
6#[derive(Debug, Error)]
7pub enum GitClosureError {
8    /// Any filesystem or OS-level I/O failure.
9    #[error("I/O error: {0}")]
10    Io(#[from] io::Error),
11    /// Snapshot syntax or semantic parse error.
12    #[error("parse error: {0}")]
13    Parse(String),
14    /// Top-level snapshot integrity mismatch.
15    ///
16    /// This compares the header `snapshot-hash` against the hash recomputed
17    /// from all parsed entries.
18    #[error("snapshot hash mismatch: expected {expected}, got {actual}")]
19    HashMismatch { expected: String, actual: String },
20    /// Per-file content digest mismatch.
21    ///
22    /// Unlike [`Self::HashMismatch`], this indicates one specific file entry
23    /// had bytes whose SHA-256 does not match its declared `:sha256` value.
24    #[error("content hash mismatch for {path}: expected {expected}, got {actual}")]
25    ContentHashMismatch {
26        /// Snapshot-relative path of the failing file.
27        path: String,
28        /// Declared digest from snapshot metadata.
29        expected: String,
30        /// Digest recomputed from decoded file content.
31        actual: String,
32    },
33    /// Per-file decoded byte-size mismatch.
34    #[error("size mismatch for {path}: metadata {expected}, decoded {actual}")]
35    SizeMismatch {
36        /// Snapshot-relative path of the failing file.
37        path: String,
38        /// Declared `:size` metadata value.
39        expected: u64,
40        /// Actual decoded content length in bytes.
41        actual: u64,
42    },
43    /// Path or symlink target failed safety checks.
44    #[error("unsafe path in snapshot: {0}")]
45    UnsafePath(String),
46    /// Required snapshot header is missing.
47    #[error("missing required header: {0}")]
48    MissingHeader(&'static str),
49    /// Legacy `format-hash` header encountered.
50    #[error("legacy format-hash header found; re-snapshot with current tool")]
51    LegacyHeader,
52    /// External command could not be started.
53    #[error("command '{command}' failed to spawn: {source}")]
54    CommandSpawnFailed {
55        /// Name of the command that failed.
56        command: &'static str,
57        /// Underlying process-spawn error.
58        #[source]
59        source: io::Error,
60    },
61    /// External command ran but returned a non-zero exit status.
62    #[error(
63        "command '{command}' exited with status {status}{stderr_suffix}",
64        stderr_suffix = format_command_stderr(stderr)
65    )]
66    CommandExitFailure {
67        /// Name of the command that failed.
68        command: &'static str,
69        /// Exit status rendered as text.
70        status: String,
71        /// Captured standard error output.
72        stderr: String,
73    },
74}
75
76fn format_command_stderr(stderr: &str) -> String {
77    if stderr.is_empty() {
78        String::new()
79    } else {
80        format!(":\n{stderr}")
81    }
82}