Skip to main content

git_stub_vcs/
errors.rs

1// Copyright 2026 Oxide Computer Company
2
3//! Error types for git stub VCS operations and materialization.
4
5use crate::VcsName;
6use camino::Utf8PathBuf;
7use git_stub::{GitStub, GitStubParseError};
8use std::{ffi::OsString, io};
9use thiserror::Error;
10
11// ---- VCS errors ----
12
13/// An error from reading a VCS binary path from the environment.
14#[derive(Debug, Error)]
15#[non_exhaustive]
16pub enum VcsEnvError {
17    /// The environment variable is set but is not valid UTF-8.
18    #[error(
19        "${var} environment variable is not valid \
20         UTF-8: {value:?}"
21    )]
22    NonUtf8 {
23        /// The environment variable name.
24        var: &'static str,
25        /// The non-UTF-8 value.
26        value: OsString,
27    },
28}
29
30/// An error that occurs during VCS detection.
31#[derive(Debug, Error)]
32#[non_exhaustive]
33pub enum VcsDetectError {
34    /// The provided repository root does not exist.
35    #[error(
36        "{repo_root} does not exist \
37         (expected a repository root with .git or .jj)"
38    )]
39    PathNotFound {
40        /// The path that was provided.
41        repo_root: Utf8PathBuf,
42    },
43
44    /// The provided repository root is not a directory.
45    #[error(
46        "{repo_root} is not a directory \
47         (expected a repository root with .git or .jj)"
48    )]
49    NotADirectory {
50        /// The path that was provided.
51        repo_root: Utf8PathBuf,
52    },
53
54    /// An I/O error occurred while probing the repository root.
55    #[error("I/O error while checking for VCS at {path}")]
56    Io {
57        /// The path being checked when the error occurred.
58        path: Utf8PathBuf,
59        /// The underlying I/O error.
60        #[source]
61        source: io::Error,
62    },
63
64    /// Neither `.git` nor `.jj` was found at the repository root.
65    #[error("no VCS found at {repo_root} (expected .git or .jj)")]
66    NotFound {
67        /// The repository root that was searched.
68        repo_root: Utf8PathBuf,
69    },
70
71    /// A VCS environment variable is not valid UTF-8.
72    #[error(transparent)]
73    Env(#[from] VcsEnvError),
74}
75
76/// An error that occurs while checking for a shallow clone.
77#[derive(Debug, Error)]
78#[non_exhaustive]
79pub enum ShallowCloneError {
80    /// Failed to spawn the VCS process.
81    #[error("failed to run {vcs_name} at {binary_path:?} in {repo_root}")]
82    SpawnFailed {
83        /// The name of the VCS.
84        vcs_name: VcsName,
85        /// The path to the VCS executable.
86        binary_path: String,
87        /// The working directory where the command was run.
88        repo_root: Utf8PathBuf,
89        /// The underlying I/O error.
90        #[source]
91        source: io::Error,
92    },
93
94    /// The VCS command to check for a shallow repository failed.
95    #[error(
96        "{vcs_name} failed to check for shallow clone \
97         ({exit_status}): {stderr}"
98    )]
99    VcsFailed {
100        /// The name of the VCS.
101        vcs_name: VcsName,
102        /// A human-readable description of the exit status (e.g.,
103        /// "exit code 128" or "killed by signal").
104        exit_status: String,
105        /// The stderr output from the VCS.
106        stderr: String,
107    },
108
109    /// An I/O error occurred while checking the shallow-clone marker.
110    #[error("I/O error while checking for shallow clone at {path}")]
111    Io {
112        /// The path being checked when the error occurred.
113        path: Utf8PathBuf,
114        /// The underlying I/O error.
115        #[source]
116        source: io::Error,
117    },
118
119    /// The VCS command succeeded but returned unexpected output.
120    #[error(
121        "{vcs_name} returned unexpected output for shallow clone \
122         check: expected \"true\" or \"false\", got {stdout:?}"
123    )]
124    UnexpectedOutput {
125        /// The name of the VCS.
126        vcs_name: VcsName,
127        /// The stdout content that could not be interpreted.
128        stdout: String,
129    },
130}
131
132/// An error that occurs while reading the contents of a
133/// [`GitStub`].
134#[derive(Debug, Error)]
135#[non_exhaustive]
136pub enum ReadContentsError {
137    /// Failed to spawn the VCS process.
138    #[error("failed to run {vcs_name} at {binary_path:?} in {repo_root}")]
139    SpawnFailed {
140        /// The name of the VCS.
141        vcs_name: VcsName,
142        /// The path to the VCS executable.
143        binary_path: String,
144        /// The working directory where the command was run.
145        repo_root: Utf8PathBuf,
146        /// The underlying I/O error.
147        #[source]
148        source: io::Error,
149    },
150
151    /// The VCS command failed.
152    #[error("{vcs_name} failed to read {stub} ({exit_status}): {stderr}")]
153    VcsFailed {
154        /// The name of the VCS.
155        vcs_name: VcsName,
156        /// The stub that was requested.
157        stub: GitStub,
158        /// A human-readable description of the exit status (e.g.,
159        /// "exit code 128" or "killed by signal").
160        exit_status: String,
161        /// The stderr output from the VCS.
162        stderr: String,
163    },
164}
165
166// ---- Materialization errors ----
167
168/// Errors that can occur during git stub materialization.
169#[derive(Debug, Error)]
170#[non_exhaustive]
171pub enum MaterializeError {
172    /// A path argument contains a non-normal component (e.g., `..`,
173    /// `.`, `/`, or a Windows prefix).
174    ///
175    /// Only relative paths with plain file and directory names are accepted.
176    #[error(
177        "path {path:?} contains non-normal component {component:?} \
178         (only plain file/directory names are allowed)"
179    )]
180    InvalidPathComponent {
181        /// The full path that failed validation.
182        path: Utf8PathBuf,
183        /// The non-normal component that was found (e.g., `..`, `.`,
184        /// `/`).
185        component: String,
186    },
187
188    /// The path does not have a `.gitstub` extension.
189    #[error("path does not end with .gitstub: {path}")]
190    NotGitStub {
191        /// The path that was provided.
192        path: Utf8PathBuf,
193    },
194
195    /// Failed to read the Git stub.
196    #[error("failed to read Git stub {path}")]
197    ReadGitStub {
198        /// The path to the Git stub.
199        path: Utf8PathBuf,
200        /// The underlying I/O error.
201        #[source]
202        error: io::Error,
203    },
204
205    /// The git stub has an invalid format.
206    #[error("invalid Git stub format in {path}")]
207    InvalidGitStub {
208        /// The path to the Git stub.
209        path: Utf8PathBuf,
210        /// Details about the parsing error.
211        #[source]
212        error: GitStubParseError,
213    },
214
215    /// VCS detection failed.
216    #[error("VCS detection failed")]
217    VcsDetect(#[from] VcsDetectError),
218
219    /// Failed to read contents from Git.
220    #[error("failed to read git stub contents")]
221    ReadContents(#[from] ReadContentsError),
222
223    /// Failed to check whether the repository is a shallow clone.
224    #[error("failed to check for shallow clone at {repo_root}")]
225    ShallowCloneCheck {
226        /// The repository root.
227        repo_root: Utf8PathBuf,
228        /// The underlying error.
229        #[source]
230        error: ShallowCloneError,
231    },
232
233    /// The repository is a shallow clone.
234    #[error(
235        "shallow clone detected at {repo_root}: cannot dereference \
236         git stubs without full history{}", shallow_clone_msg(.vcs),
237    )]
238    ShallowClone {
239        /// The VCS detected.
240        vcs: VcsName,
241
242        /// The repository root.
243        repo_root: Utf8PathBuf,
244    },
245
246    /// Failed to create output directory.
247    #[error("failed to create output directory {path}")]
248    CreateDir {
249        /// The directory path.
250        path: Utf8PathBuf,
251        /// The underlying I/O error.
252        #[source]
253        error: io::Error,
254    },
255
256    /// Failed to write the materialized file.
257    #[error("failed to write materialized spec to {path}")]
258    WriteOutput {
259        /// The path where the write failed.
260        path: Utf8PathBuf,
261        /// The underlying write error.
262        #[source]
263        error: AtomicWriteError,
264    },
265}
266
267fn shallow_clone_msg(vcs: &VcsName) -> &'static str {
268    match vcs {
269        VcsName::Git => "(run `git fetch --unshallow`)",
270        VcsName::Jj => {
271            "(if this is a colocated repository, \
272              run `git fetch --unshallow`)"
273        }
274    }
275}
276
277/// An error that occurred during an atomic file write.
278#[derive(Debug, Error)]
279#[non_exhaustive]
280pub enum AtomicWriteError {
281    /// Writing contents to the temporary file failed.
282    #[error("writing file contents failed")]
283    Write(#[source] io::Error),
284
285    /// The atomic write infrastructure failed (e.g., creating the
286    /// temporary file, or renaming it into place).
287    #[error("atomic create or rename failed")]
288    Rename(#[source] io::Error),
289}