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}