grit_lib/error.rs
1//! Shared error types for grit-lib.
2//!
3//! Library code uses [`enum@Error`] (a `thiserror` enum) so callers can match on
4//! specific failure modes. The binary wraps these with `anyhow` for human-
5//! readable top-level reporting.
6//!
7//! Library guide: [errors](https://grit-scm.com/docs/library/embedding)
8//!
9//! ```rust
10//! use grit_lib::error::Error;
11//! # fn main() {
12//! let _ = Error::Message("example".into());
13//! # }
14//! ```
15
16use thiserror::Error;
17
18pub use crate::git_dir::GitDirPathOperation;
19pub use crate::midx_error::MidxError;
20pub use crate::worktree::io_error::WorktreePathOperation;
21
22/// Why a Git config numeric value was rejected (`git config int` strict parsing).
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum BadNumericSource {
25 /// Suffix unit letter is invalid or missing where required.
26 InvalidUnit,
27 /// Parsed value or scaled product does not fit.
28 OutOfRange,
29}
30
31impl BadNumericSource {
32 fn detail(self) -> &'static str {
33 match self {
34 Self::InvalidUnit => "invalid unit",
35 Self::OutOfRange => "out of range",
36 }
37 }
38}
39
40impl std::fmt::Display for BadNumericSource {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.write_str(self.detail())
43 }
44}
45
46/// Why a tree transition would overwrite existing worktree content.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48pub enum OverwriteReason {
49 /// Staged or unstaged edits on paths that differ between trees.
50 LocalChanges,
51 /// Untracked paths that a checkout would replace.
52 Untracked,
53}
54
55impl std::fmt::Display for OverwriteReason {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 match self {
58 Self::LocalChanges => f.write_str("local_changes"),
59 Self::Untracked => f.write_str("untracked"),
60 }
61 }
62}
63
64/// Why a remote could not be read or written ([`Error::RemoteUnreadable`]).
65///
66/// Every reason is a failure to talk to the remote at all, as opposed to the remote refusing
67/// an update. [`Self::Transport`] is the one usually worth retrying on a flaky network.
68#[derive(Debug, Clone, PartialEq, Eq)]
69#[non_exhaustive]
70pub enum RemoteUnreadableReason {
71 /// A local path or `file://` URL does not name a Git directory (valid `HEAD`, plus
72 /// `objects/` and `refs/` in the common directory).
73 RepositoryNotFound,
74 /// The connection failed or broke: name resolution, refused or reset connection, TLS, or
75 /// timeouts.
76 Transport {
77 /// Transport-level description of the failure, with URL credentials redacted.
78 detail: String,
79 },
80 /// A smart-HTTP server answered with an error status other than an authentication
81 /// challenge (those are [`Error::Auth`]).
82 HttpStatus {
83 /// HTTP status code (`>= 400`).
84 status: u16,
85 },
86 /// An HTTP response looked successful but was not a Git smart-HTTP ref advertisement
87 /// (for example `200 text/html` from a parked or mistyped host).
88 NotGitServer {
89 /// HTTP status code (often `200`).
90 status: u16,
91 /// Response `Content-Type` when present (media type only, without parameters).
92 content_type: Option<String>,
93 },
94 /// The transport ran, but the remote never completed the Git protocol handshake (for
95 /// example `ssh` exited, or a `git://` peer closed, before any ref advertisement).
96 NoHandshake,
97}
98
99impl RemoteUnreadableReason {
100 /// Stable snake-case name for machine-readable output (`repository_not_found`,
101 /// `transport`, `http_status`, `not_git_server`, `no_handshake`).
102 #[must_use]
103 pub fn slug(&self) -> &'static str {
104 match self {
105 Self::RepositoryNotFound => "repository_not_found",
106 Self::Transport { .. } => "transport",
107 Self::HttpStatus { .. } => "http_status",
108 Self::NotGitServer { .. } => "not_git_server",
109 Self::NoHandshake => "no_handshake",
110 }
111 }
112}
113
114fn remote_unreadable_message(url: Option<&str>, reason: &RemoteUnreadableReason) -> String {
115 let target = url.map_or_else(|| "the remote".to_owned(), |u| format!("'{u}'"));
116 match reason {
117 RemoteUnreadableReason::RepositoryNotFound => {
118 format!("could not find repository at {target}")
119 }
120 RemoteUnreadableReason::Transport { detail } => {
121 format!("could not reach {target}: {detail}")
122 }
123 RemoteUnreadableReason::HttpStatus { status } => match url {
124 Some(u) => format!("HTTP {status} from {u}"),
125 None => format!("HTTP {status} from the remote"),
126 },
127 RemoteUnreadableReason::NotGitServer {
128 status,
129 content_type,
130 } => {
131 let got = match content_type
132 .as_deref()
133 .map(|ct| ct.split(';').next().unwrap_or(ct).trim())
134 .filter(|ct| !ct.is_empty())
135 {
136 Some(ct) => format!("{status} {ct}"),
137 None => status.to_string(),
138 };
139 format!("{target} did not answer like a Git server (got {got})")
140 }
141 RemoteUnreadableReason::NoHandshake => {
142 format!("could not read from {target}: no Git protocol response")
143 }
144 }
145}
146
147/// Why an HTTP remote refused access ([`Error::Auth`]).
148#[derive(Debug, Clone, PartialEq, Eq, Error)]
149#[non_exhaustive]
150pub enum AuthFailure {
151 /// The server asked for credentials (`401`) and no credential provider is wired.
152 #[error("no credential provider is configured")]
153 NoCredentialProvider,
154 /// The credential provider could not supply credentials (no helper has them, and
155 /// prompting is not possible).
156 #[error("{detail}")]
157 CredentialsUnavailable {
158 /// The provider's explanation (for example that no helper has credentials and
159 /// prompting is disabled).
160 detail: String,
161 },
162 /// The provider answered without a usable username and password.
163 #[error("the credential helper returned no usable username and password")]
164 UnusableCredentials,
165 /// The URL could not be split into the credential fields helpers expect.
166 #[error("the URL cannot be described to a credential helper")]
167 UrlNotDescribable,
168 /// The server only offers authentication schemes grit does not speak (no `Basic`).
169 #[error("unsupported authentication scheme: {}", schemes.join(", "))]
170 UnsupportedScheme {
171 /// The `WWW-Authenticate` challenges the server sent.
172 schemes: Vec<String>,
173 },
174 /// Credentials were sent and the server rejected them (`401` again).
175 #[error("the server rejected the credentials")]
176 Rejected,
177}
178
179impl AuthFailure {
180 /// Whether credentials were sent and refused, as opposed to never being available.
181 #[must_use]
182 pub fn is_rejected(&self) -> bool {
183 matches!(self, Self::Rejected)
184 }
185}
186
187/// The command a dirty worktree blocked ([`Error::DirtyWorktree`]).
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
189#[non_exhaustive]
190pub enum WorktreeOperation {
191 /// Switching branches or checking out a commit.
192 Switch,
193 /// Picking (cherry-picking) or reverting commits.
194 Pick,
195 /// Merging.
196 Merge,
197 /// Rebasing with `--exec`.
198 RebaseExec,
199 /// Starting a rebase that stops on conflicts (the stop rewrites the index and worktree).
200 Rebase,
201}
202
203impl std::fmt::Display for WorktreeOperation {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 f.write_str(match self {
206 Self::Switch => "switching",
207 Self::Pick => "picking",
208 Self::Merge => "merging",
209 Self::RebaseExec => "rebasing with --exec",
210 Self::Rebase => "rebasing",
211 })
212 }
213}
214
215/// Configuration parse and validation failures.
216#[derive(Debug, Error, Clone, PartialEq, Eq)]
217#[non_exhaustive]
218pub enum ConfigError {
219 /// A line in a config file could not be parsed.
220 #[error("bad config line {line} in {location}")]
221 BadConfigLine {
222 /// Config source label or path where the error was detected.
223 location: String,
224 /// One-based line number within that source.
225 line: usize,
226 },
227
228 /// Same as [`Self::BadConfigLine`] with an explicit file path (Git file diagnostics).
229 #[error("bad config line {line} in file '{file}'")]
230 BadConfigLineInFile {
231 /// Path to the config file involved in the error.
232 file: String,
233 /// One-based line number within that file.
234 line: usize,
235 },
236
237 /// A config key requires a value but none was given.
238 #[error("missing value for '{key}'")]
239 MissingValue {
240 /// Config key name that failed validation.
241 key: String,
242 },
243
244 /// `fetch.negotiationalgorithm` and similar keys with missing values.
245 #[error("bad config variable '{key}' in file '{file}' at line {line}")]
246 BadConfigVariable {
247 /// Config key name that failed validation.
248 key: String,
249 /// Path to the config file involved in the error.
250 file: String,
251 /// One-based line number within the config source.
252 line: usize,
253 },
254
255 /// `unset` or a scoped read/write was requested for a key that is not set in that file.
256 #[error("key '{key}' is not set")]
257 KeyNotSet {
258 /// Config key that was not present.
259 key: String,
260 },
261
262 /// Integer config value could not be parsed or is out of range.
263 #[error("bad numeric config value '{value}' for '{key}': {reason}")]
264 BadNumericValue {
265 /// Config key name that failed validation.
266 key: String,
267 /// Raw config value that could not be parsed.
268 value: String,
269 /// Why the value or operation was rejected.
270 reason: BadNumericSource,
271 },
272
273 /// Same as [`Self::BadNumericValue`] with file context.
274 #[error("bad numeric config value '{value}' for '{key}' in file {file}: {reason}")]
275 BadNumericValueInFile {
276 /// Config key name that failed validation.
277 key: String,
278 /// Raw config value that could not be parsed.
279 value: String,
280 /// Path to the config file involved in the error.
281 file: String,
282 /// Why the value or operation was rejected.
283 reason: BadNumericSource,
284 },
285
286 /// `diff.context` (or similar) failed variable validation.
287 #[error("bad config variable '{key}' in file '{file}' at line {line}")]
288 BadConfigVariableAtLine {
289 /// Config key name that failed validation.
290 key: String,
291 /// Path to the config file involved in the error.
292 file: String,
293 /// One-based line number within the config source.
294 line: usize,
295 },
296
297 /// Command-line config could not supply a parseable value for a key.
298 #[error("unable to parse '{key}' from command-line config")]
299 UnableToParseFromCommandLine {
300 /// Config key name that failed validation.
301 key: String,
302 },
303
304 /// `includeIf.hasconfig:remote.*.url` cannot pull remote URLs from included files.
305 #[error(
306 "remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url"
307 )]
308 RemoteUrlInHasconfigInclude,
309
310 /// `[include]` / `[includeIf]` nesting exceeded the Git-compatible depth limit.
311 #[error("exceeded maximum include depth (depth {depth} exceeds limit {limit})")]
312 IncludeDepthExceeded {
313 /// Current include nesting depth when the limit was hit.
314 depth: usize,
315 /// Maximum allowed include nesting depth.
316 limit: usize,
317 },
318
319 /// Generic config error with free-form detail (legacy call sites).
320 #[error("{0}")]
321 Other(String),
322
323 /// The config file could not be written because a `.lock` file is already present.
324 #[error("could not lock config file {path}: File exists")]
325 ConfigFileLocked {
326 /// Path to the config file that could not be locked.
327 path: String,
328 },
329
330 /// A single-value operation was requested but the key has multiple values.
331 #[error("cannot overwrite multiple values with a single value for '{key}'")]
332 MultipleValues {
333 /// Config key name that failed validation.
334 key: String,
335 },
336
337 /// Per-worktree config was requested but `extensions.worktreeConfig` is off.
338 #[error(
339 "worktree config is only allowed when extensions.worktreeConfig is enabled in the common config"
340 )]
341 WorktreeConfigNotEnabled,
342
343 /// `remove-section` or `rename-section` did not find the named section.
344 #[error("section '{section}' not found")]
345 SectionNotFound {
346 /// Section header name (e.g. `remote.origin`).
347 section: String,
348 },
349
350 /// Inline `--comment` text must not span lines.
351 #[error("no multi-line comment allowed")]
352 MultilineCommentNotAllowed,
353
354 /// `status.showUntrackedFiles` must be `no`, `normal`, `all`, or a Git boolean synonym.
355 #[error(
356 "invalid value '{value}' for 'status.showUntrackedFiles' (expected no, normal, all, boolean words, or integer 0 vs nonzero)"
357 )]
358 InvalidUntrackedFilesMode {
359 /// Raw config value that could not be parsed.
360 value: String,
361 },
362}
363
364impl From<String> for ConfigError {
365 fn from(value: String) -> Self {
366 Self::Other(value)
367 }
368}
369
370impl From<&str> for ConfigError {
371 fn from(value: &str) -> Self {
372 Self::Other(value.to_owned())
373 }
374}
375
376impl ConfigError {
377 /// Git stderr body for this error (without a `fatal:` prefix).
378 #[must_use]
379 pub fn message_body(&self) -> String {
380 self.to_string()
381 }
382}
383
384/// Reference lock failures while creating or updating a ref.
385#[derive(Debug, Error, Clone, PartialEq, Eq)]
386#[non_exhaustive]
387pub enum RefLockError {
388 /// A non-empty directory occupies the path where the ref file must be created.
389 #[error(
390 "cannot lock ref '{refname}': there is a non-empty directory '{path}' blocking reference '{refname}'"
391 )]
392 DirectoryInTheWay {
393 /// Full ref name (for example `refs/heads/main`).
394 refname: String,
395 /// Directory path blocking creation of the ref file.
396 path: String,
397 },
398
399 /// Another process holds the `packed-refs.lock` file (or it already exists).
400 #[error("Unable to create '{lock_path}': File exists.")]
401 PackedRefsLockHeld {
402 /// Path to the lock file that could not be created.
403 lock_path: String,
404 },
405}
406
407/// Clean/smudge filter and EOL conversion failures.
408#[derive(Debug, Error, Clone, PartialEq, Eq)]
409#[non_exhaustive]
410pub enum FilterError {
411 /// External clean or smudge filter command failed.
412 #[error("{path}: {phase} filter '{driver}' failed")]
413 ExternalFilterFailed {
414 /// Filter driver name from `.gitattributes`.
415 driver: String,
416 /// Repository-relative or filesystem path related to the failure.
417 path: String,
418 /// Whether the clean or smudge filter hook failed.
419 phase: FilterPhase,
420 },
421
422 /// A required clean or smudge filter could not be started (for example, no shell to run
423 /// it), so the content was neither filtered nor stored unfiltered.
424 #[error("{path}: cannot start {phase} filter '{driver}' ({program}): {detail}")]
425 ExternalFilterSpawnFailed {
426 /// Filter driver name from `.gitattributes`.
427 driver: String,
428 /// Repository-relative path being filtered.
429 path: String,
430 /// Whether the clean or smudge filter was being started.
431 phase: FilterPhase,
432 /// Program grit tried to spawn (`sh` for shell-command filters).
433 program: String,
434 /// OS error kind from the failed spawn.
435 kind: std::io::ErrorKind,
436 /// OS error message from the failed spawn.
437 detail: String,
438 },
439
440 /// Filter output did not round-trip (clean/smudge mismatch).
441 #[error("{path}: {detail}")]
442 NotFilteredProperly {
443 /// Repository-relative or filesystem path related to the failure.
444 path: String,
445 /// Why clean/smudge output did not round-trip.
446 detail: String,
447 },
448
449 /// Invalid `working-tree-encoding` attribute value.
450 #[error("true/false are no valid working-tree-encodings")]
451 InvalidWorkingTreeEncoding,
452
453 /// Line-ending conversion would modify the worktree against user settings.
454 #[error("{detail}")]
455 LineEndingWouldChange {
456 /// Additional human-readable detail for the failure.
457 detail: String,
458 },
459
460 /// Shell filter subprocess exited non-zero.
461 #[error("filter command exited with status {status}")]
462 Failed {
463 /// Subprocess exit status from the filter command.
464 status: i32,
465 },
466}
467
468/// Which filter hook failed.
469#[derive(Debug, Clone, Copy, PartialEq, Eq)]
470pub enum FilterPhase {
471 /// Index → worktree (smudge).
472 Smudge,
473 /// Worktree → index (clean).
474 Clean,
475}
476
477impl std::fmt::Display for FilterPhase {
478 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
479 match self {
480 Self::Smudge => f.write_str("smudge"),
481 Self::Clean => f.write_str("clean"),
482 }
483 }
484}
485
486/// Sparse-checkout cone pattern parse warnings (non-fatal).
487#[derive(Debug, Clone, PartialEq, Eq)]
488pub enum SparseCheckoutWarning {
489 /// A cone pattern line was not recognized.
490 UnrecognizedPattern {
491 /// Raw pattern text from the sparse-checkout file.
492 pattern: String,
493 },
494 /// A negative cone pattern line was not recognized.
495 UnrecognizedNegativePattern {
496 /// Raw negative pattern text from the sparse-checkout file.
497 pattern: String,
498 },
499 /// The same cone pattern appears more than once in the sparse-checkout file.
500 RepeatedPattern {
501 /// Duplicated pattern text.
502 pattern: String,
503 },
504 /// Cone pattern matching was disabled due to invalid sparse-checkout contents.
505 DisablingConePatternMatching,
506}
507
508impl SparseCheckoutWarning {
509 /// Human-readable body without a `warning:` prefix.
510 #[must_use]
511 pub fn body(&self) -> String {
512 match self {
513 Self::UnrecognizedPattern { pattern } => {
514 format!("unrecognized pattern: '{pattern}'")
515 }
516 Self::UnrecognizedNegativePattern { pattern } => {
517 format!("unrecognized negative pattern: '{pattern}'")
518 }
519 Self::RepeatedPattern { pattern } => format!(
520 "your sparse-checkout file may have issues: pattern '{pattern}' is repeated"
521 ),
522 Self::DisablingConePatternMatching => "disabling cone pattern matching".to_owned(),
523 }
524 }
525
526 /// Git-style `warning: …` line.
527 #[must_use]
528 pub fn format_line(&self) -> String {
529 crate::diagnostics::warning_line(&self.body())
530 }
531}
532
533/// Unified diff / patch application errors.
534#[derive(Debug, Error, Clone, PartialEq, Eq)]
535#[non_exhaustive]
536pub enum ApplyError {
537 /// Patch bytes are malformed at a known input location.
538 #[error("corrupt patch at {input}:{line}")]
539 CorruptPatch {
540 /// Patch input label (file name or `stdin`).
541 input: String,
542 /// One-based line number within the patch input.
543 line: usize,
544 },
545
546 /// Preimage context could not be located in the blob at any search offset.
547 #[error(
548 "patch failed: context mismatch at hunk {hunk_index} (expected near line {expected_line})"
549 )]
550 ContextMismatch {
551 /// Zero-based index of the hunk in the input slice.
552 hunk_index: usize,
553 /// One-based line number from the hunk header (`old_start`).
554 expected_line: usize,
555 },
556
557 /// A later hunk would modify lines already changed by an earlier hunk.
558 #[error("patch failed: overlapping hunks at hunk {hunk_index}")]
559 HunksOverlap {
560 /// Zero-based index of the hunk that overlaps prior applied changes.
561 hunk_index: usize,
562 },
563
564 /// The patch removes more lines than exist in the blob (without a valid EOF match).
565 #[error("patch failed: unexpected end of file at hunk {hunk_index}")]
566 UnexpectedEof {
567 /// Zero-based index of the offending hunk.
568 hunk_index: usize,
569 },
570
571 /// Other apply parse failures.
572 #[error("{0}")]
573 Other(String),
574
575 /// A text hunk could not be matched or applied.
576 #[error("patch failed: {path}:{line}")]
577 PatchFailed {
578 /// File path from the patch.
579 path: String,
580 /// One-based line number in the patch input.
581 line: usize,
582 },
583
584 /// Index or blob apply failed for a path.
585 #[error("could not apply patch to {path}: {message}")]
586 Failed {
587 /// Repository-relative path.
588 path: String,
589 /// Failure detail.
590 message: String,
591 },
592
593 /// `--cached` apply requires a stage-0 index entry at the preimage path.
594 #[error("'{path}' is not in the index")]
595 NotInIndex {
596 /// Missing path.
597 path: String,
598 },
599
600 /// A binary diff marker without `GIT binary patch` payload cannot be applied.
601 #[error("binary patch data missing for '{path}'")]
602 BinaryDataMissing {
603 /// Path from the patch.
604 path: String,
605 },
606
607 /// The caller's [`crate::progress::Cancel`] or
608 /// [`crate::progress::ProgressSink::should_cancel`] stopped the apply between files.
609 #[error("operation cancelled")]
610 Cancelled,
611}
612
613/// Path lookup failures while formatting a diff.
614#[derive(Debug, Error, Clone, PartialEq, Eq)]
615#[non_exhaustive]
616pub enum DiffPathError {
617 /// The requested path is not part of the diff being formatted.
618 #[error("No such path '{path}' in the diff")]
619 NoSuchPathInDiff {
620 /// Path that was requested from the diff.
621 path: String,
622 },
623}
624
625/// `--pathspec-from-file` / `--pathspec-file-nul` parse failure.
626#[derive(Debug, Error, Clone, PartialEq, Eq)]
627#[non_exhaustive]
628pub enum PathspecFromFileParseError {
629 /// A NUL-separated entry is not valid UTF-8.
630 #[error("invalid UTF-8 in pathspec-from-file (NUL mode)")]
631 InvalidUtf8InNulRecord,
632 /// A NUL-terminated entry used C-style quoting (forbidden in NUL mode).
633 #[error("pathspec-from-file: line is not NUL terminated: {line}")]
634 QuotedLineInNulMode {
635 /// Offending entry text.
636 line: String,
637 },
638 /// A line-mode entry had invalid double-quote wrapping.
639 #[error("invalid C-style quoting: {line}")]
640 InvalidCStyleQuoting {
641 /// Raw line from the file.
642 line: String,
643 },
644 /// Backslash escape at end of a quoted line.
645 #[error("invalid escape at end of string")]
646 InvalidEscapeAtEnd,
647 /// Octal escape shorter than three digits.
648 #[error("truncated octal escape")]
649 TruncatedOctalEscape,
650 /// Octal escape bytes are not valid UTF-8.
651 #[error("invalid octal bytes")]
652 InvalidOctalBytes,
653 /// Octal digits do not form a valid byte value.
654 #[error("invalid octal escape value")]
655 InvalidOctalValue,
656 /// Unknown escape after backslash in a quoted line.
657 #[error("invalid escape sequence \\{escape}")]
658 InvalidEscapeSequence {
659 /// Character after the backslash.
660 escape: char,
661 },
662 /// Quoted line decoded to invalid UTF-8.
663 #[error("invalid UTF-8 in quoted pathspec")]
664 InvalidUtf8InQuotedLine,
665}
666
667/// Failure while resolving argv and `--pathspec-from-file` pathspecs against a work tree.
668#[derive(Debug, Error, Clone, PartialEq, Eq)]
669#[non_exhaustive]
670pub enum PathspecResolveError {
671 /// An argv pathspec was the empty string.
672 #[error("invalid path ''")]
673 EmptyPathspec,
674 /// Resolved path lies outside the repository work tree.
675 #[error("{elt}: '{path}' is outside repository at '{work_tree}'")]
676 OutsideRepository {
677 /// User-facing pathspec token (argv element).
678 elt: String,
679 /// Resolved absolute path outside the work tree.
680 path: String,
681 /// Canonical work tree root.
682 work_tree: String,
683 },
684 /// `--pathspec-from-file` bytes could not be parsed.
685 #[error(transparent)]
686 FromFile(#[from] PathspecFromFileParseError),
687 /// Command requires a working tree but the repository is bare.
688 #[error("this operation needs a working tree")]
689 NeedsWorkTree,
690}
691
692/// Why a `:(attr:...)` pathspec clause was rejected.
693#[derive(Debug, Error, Clone, PartialEq, Eq)]
694#[non_exhaustive]
695pub enum AttrPathspecError {
696 /// The clause names no attribute requirement (`:(attr:)`).
697 #[error("attr requirement list is empty")]
698 Empty,
699 /// More than one `attr:` clause appears in one pathspec.
700 #[error("a pathspec may contain at most one attr clause")]
701 MultipleAttrClauses,
702 /// A requirement names an attribute that is not valid (`-a=b`, `bad name`, `bad/name`).
703 #[error("'{0}' is not a usable attribute name")]
704 InvalidName(String),
705 /// A `\` escape ends the value or precedes whitespace.
706 #[error("attr value ends with a dangling escape")]
707 DanglingEscape,
708 /// A `\` escape precedes something other than `,`.
709 #[error("attr values may only escape ','")]
710 UnsupportedEscape,
711}
712
713/// Path outside the repository during pathspec or transport resolution.
714#[derive(Debug, Error, Clone, PartialEq, Eq)]
715#[non_exhaustive]
716pub enum PathOutsideRepoError {
717 /// A resolved path escapes the repository work tree or git directory.
718 #[error("{path}: '{subpath}' is outside repository at '{repo_root}'")]
719 OutsideRepository {
720 /// Repository-relative or filesystem path related to the failure.
721 path: String,
722 /// Path component that lies outside the repository root.
723 subpath: String,
724 /// Canonical repository root used for the outside-path check.
725 repo_root: String,
726 },
727}
728
729/// Why a path could not be opened as a Git repository; carried by [`Error::NotARepository`].
730#[derive(Debug, Error)]
731#[non_exhaustive]
732pub enum NotARepositoryReason {
733 /// No usable git directory exists at `path`, or discovery found none at or above it.
734 #[error("{}", crate::git_path::path_for_error_message(path))]
735 NoRepositoryAt {
736 /// Directory, git directory, or `.git` path that was probed.
737 path: std::path::PathBuf,
738 },
739 /// A `.git` entry is a FIFO, socket or device rather than a regular file or directory.
740 #[error(
741 "invalid gitfile format: {} is not a regular file",
742 crate::git_path::path_for_error_message(path)
743 )]
744 GitfileNotRegularFile {
745 /// The offending `.git` entry.
746 path: std::path::PathBuf,
747 },
748 /// A gitfile has no `gitdir: <path>` line.
749 #[error("invalid gitfile format")]
750 GitfileMissingGitdir {
751 /// The gitfile that was parsed.
752 path: std::path::PathBuf,
753 },
754 /// A gitfile exists but could not be read (permissions, invalid UTF-8, ...).
755 #[error("{error}")]
756 GitfileUnreadable {
757 /// The gitfile that could not be read.
758 path: std::path::PathBuf,
759 /// The underlying read failure.
760 error: std::io::Error,
761 },
762}
763
764fn maintenance_running_message(holder: &crate::maintenance::GcLockHolder) -> String {
765 match (holder.pid, holder.host.as_deref()) {
766 (Some(pid), Some(host)) => {
767 format!("repository maintenance is already running (process {pid} on {host})")
768 }
769 (Some(pid), None) => format!("repository maintenance is already running (process {pid})"),
770 _ => "repository maintenance is already running".to_owned(),
771 }
772}
773
774fn restore_unmerged_message(paths: &[String]) -> String {
775 match paths {
776 [path] => format!("path '{path}' is unmerged"),
777 _ => format!(
778 "paths are unmerged: {}",
779 paths
780 .iter()
781 .map(|p| format!("'{p}'"))
782 .collect::<Vec<_>>()
783 .join(", ")
784 ),
785 }
786}
787
788/// The top-level error type for all grit-lib operations.
789///
790/// # Examples
791///
792/// ```
793/// use grit_lib::error::Error;
794///
795/// let err = Error::Message("repository not found".into());
796/// assert!(err.to_string().contains("repository not found"));
797/// ```
798#[derive(Debug, Error)]
799#[non_exhaustive]
800pub enum Error {
801 /// A repository could not be found or is structurally invalid.
802 #[error("not a git repository (or any of the parent directories): {0}")]
803 NotARepository(NotARepositoryReason),
804
805 /// A bare repository was found but access is forbidden by safe.bareRepository.
806 #[error("cannot use bare repository '{0}' (safe.bareRepository is 'explicit')")]
807 ForbiddenBareRepository(String),
808
809 /// The repository is owned by a different user (safe.directory).
810 #[error("detected dubious ownership in repository at '{0}'")]
811 DubiousOwnership(String),
812
813 /// Repository format version is not supported by this implementation.
814 #[error("unsupported repository format version '{0}'")]
815 UnsupportedRepositoryFormatVersion(u32),
816
817 /// Repository declares an unsupported extension.
818 #[error("unknown repository extension '{0}'")]
819 UnsupportedRepositoryExtension(String),
820
821 /// `extensions.refStorage` names an unsupported backend.
822 #[error("invalid value for 'extensions.refstorage': '{value}'")]
823 InvalidRefStorageFormat {
824 /// Raw `extensions.refStorage` value from the repository config.
825 value: String,
826 },
827
828 /// [`crate::repo::Repository::init_with_outcome`] refused to change object format on reinit.
829 #[error("cannot change object format in an existing repository")]
830 InitObjectFormatChange,
831
832 /// [`crate::repo::Repository::init_with_outcome`] with incompatible `--bare` and separate git dir.
833 #[error("cannot combine bare repository with separate git directory")]
834 InitBareWithSeparateGitDir,
835
836 /// A supplied object ID string was not valid hex or the wrong length.
837 #[error("invalid object id '{0}'")]
838 InvalidObjectId(String),
839
840 /// SHA-1 object hashing detected a chosen-prefix collision (sha1dc).
841 #[error("SHA-1 hash collision detected for object {oid}")]
842 HashCollision {
843 /// Plain SHA-1 digest for the colliding input (not safe to use as an object id).
844 oid: crate::objects::ObjectId,
845 },
846
847 /// The requested object does not exist in the object store.
848 #[error("object not found: {0}")]
849 ObjectNotFound(String),
850
851 /// A revision expression (branch, tag, object id, `x~n`, `ORIG_HEAD`, …) names nothing in
852 /// the repository. Holds the expression as the caller wrote it.
853 #[error("unknown revision '{0}'")]
854 RevisionNotFound(String),
855
856 /// A partial-clone promisor object is not present locally and no fetcher ran.
857 #[error("promised object not available locally: {0}")]
858 PromisedObjectMissing(crate::objects::ObjectId),
859
860 /// An object's stored data is corrupt or malformed.
861 #[error("corrupt object: {0}")]
862 CorruptObject(String),
863
864 /// A pack index (`.idx`) file's trailing checksum does not match its contents.
865 ///
866 /// Only reported by reads that verify the index trailer, such as
867 /// [`crate::pack::read_pack_index`]; the checksum of the `.pack` the index
868 /// describes is checked separately.
869 #[error("corrupt object: index checksum mismatch for {}: {mismatch}", .path.display())]
870 PackIndexChecksumMismatch {
871 /// The `.idx` file that failed verification.
872 path: std::path::PathBuf,
873 /// Expected and stored trailer digests.
874 mismatch: crate::hash::TrailerMismatch,
875 },
876
877 /// Indexing or unpacking a pack left deltas whose bases are neither in the pack nor
878 /// in the object database: a thin pack without its bases, or a ref-delta cycle.
879 #[error("corrupt object: {count} delta(s) could not be resolved")]
880 UnresolvedDeltas {
881 /// Number of deltas left unresolved.
882 count: u64,
883 },
884
885 /// [`crate::objects::Objects::write`] refused a payload that `git fsck` would reject;
886 /// nothing was written.
887 #[error("refusing to write invalid {kind} object: {}", .check.report_line())]
888 InvalidObject {
889 /// Kind the payload was meant to be stored as.
890 kind: crate::objects::ObjectKind,
891 /// The first fsck rule the payload breaks (`check.id` is Git's fsck message id).
892 check: crate::fsck_standalone::FsckError,
893 },
894
895 /// [`crate::objects::Objects::write_tree`] refused an entry.
896 #[error(transparent)]
897 InvalidTreeEntry(#[from] crate::objects::TreeEntryError),
898
899 /// A [`crate::ident::Signature`] cannot be written as an author, committer or tagger line.
900 #[error(transparent)]
901 InvalidSignature(#[from] crate::ident::SignatureError),
902
903 /// An object exists but has a different kind than the operation requires, such as a
904 /// tree or blob where a commit was expected.
905 #[error("object {oid} is a {actual}, not a {expected}")]
906 UnexpectedObjectKind {
907 /// The object that was read.
908 oid: crate::objects::ObjectId,
909 /// The kind the operation required.
910 expected: crate::objects::ObjectKind,
911 /// The kind the object actually has.
912 actual: crate::objects::ObjectKind,
913 },
914
915 /// Two commits that were asked to be merged share no merge base, and the caller did not
916 /// allow merging unrelated histories.
917 #[error("commits {ours} and {theirs} have no common ancestor")]
918 UnrelatedHistories {
919 /// The commit being merged into.
920 ours: crate::objects::ObjectId,
921 /// The commit being merged in.
922 theirs: crate::objects::ObjectId,
923 },
924
925 /// A commit-graph write was given a commit whose parent is neither among the commits
926 /// being written nor in the base graph layers, so its parent edge cannot be encoded.
927 #[error("commit-graph cannot encode parent {parent} of commit {commit}: parent not in graph")]
928 CommitGraphParentMissing {
929 /// Commit whose parent list was being encoded.
930 commit: crate::objects::ObjectId,
931 /// Parent that is missing from the graph.
932 parent: crate::objects::ObjectId,
933 },
934
935 /// In-pack delta resolution exceeded the read-side chain depth limit (guards
936 /// against cyclic ref-delta chains and unbounded recursion; Git has no read limit).
937 #[error("delta chain too deep (limit {limit})")]
938 DeltaChainTooDeep {
939 /// Maximum delta chain depth allowed while reading packed objects.
940 limit: usize,
941 },
942
943 /// An unsupported or unknown object type was encountered.
944 #[error("unknown object type '{0}'")]
945 UnknownObjectType(String),
946
947 /// Loose object header type field exceeds Git's 32-byte limit.
948 #[error("header for {oid} too long, exceeds 32 bytes")]
949 ObjectHeaderTooLong {
950 /// Object id (hex) of the loose object with an oversized header.
951 oid: String,
952 },
953
954 /// An I/O error from the underlying filesystem.
955 #[error("I/O error: {0}")]
956 Io(#[from] std::io::Error),
957
958 /// A lock file (for example `.git/index.lock` or `refs/heads/main.lock`) already exists, so
959 /// another process is updating the same file or a crashed one left the lock behind.
960 ///
961 /// Operations that return this before changing anything leave the repository untouched.
962 /// The lock file belongs to whoever created it and is never removed. Retry once the other
963 /// process finishes, or delete `path` when no process owns it (a `holder` with
964 /// `running: false` means the lock is stale).
965 #[error("lock file {} already exists", path.display())]
966 LockHeld {
967 /// The lock file that could not be created.
968 path: std::path::PathBuf,
969 /// Owner recorded in the lock's pid sidecar (`core.lockfilePid`), when one was written.
970 holder: Option<crate::lockfile::LockHolder>,
971 },
972
973 /// A zlib compression or decompression failure.
974 #[error("zlib error: {0}")]
975 Zlib(String),
976
977 /// Loose object bytes hash to a different OID than the file path implies (`git fsck` / `read_loose_object`).
978 #[error("{real_oid}: hash-path mismatch, found at: {path}")]
979 LooseHashMismatch {
980 /// Repository-relative or filesystem path to the loose object file.
981 path: String,
982 /// Hex object id of the unpacked contents.
983 real_oid: String,
984 },
985
986 /// A prepared loose object could not be written during parallel index staging.
987 #[error("could not store {path}: {source}")]
988 LooseObjectWrite {
989 /// Index-relative path of the blob being stored.
990 path: String,
991 /// Underlying filesystem error from the object store.
992 source: std::io::Error,
993 },
994
995 /// Filesystem I/O failed on a repository-relative worktree path (checkout, staging, diff).
996 #[error("{}", crate::worktree::io_error::format_worktree_path_io_message(.path, *operation, .source))]
997 WorktreePathIo {
998 /// Index- or worktree-relative path grit was operating on.
999 path: String,
1000 /// What the operation was trying to do when I/O failed.
1001 operation: WorktreePathOperation,
1002 /// Underlying OS error.
1003 source: std::io::Error,
1004 },
1005
1006 /// Filesystem I/O failed on a path under the repository git directory (objects, refs, logs, …).
1007 #[error("{}", crate::git_dir::io_error::format_git_dir_io_message(.path, *operation, .source))]
1008 GitDirPathIo {
1009 /// Display path such as `.git/objects` or `.git/refs/heads/main`.
1010 path: String,
1011 /// What the operation was trying to do when I/O failed.
1012 operation: GitDirPathOperation,
1013 /// Underlying OS error.
1014 source: std::io::Error,
1015 },
1016
1017 /// A streamed object write or hash ended before the declared size was read.
1018 #[error("object stream ended after {actual} of {expected} bytes")]
1019 ObjectStreamTruncated {
1020 /// Payload size the caller declared.
1021 expected: u64,
1022 /// Bytes the stream delivered before end of file.
1023 actual: u64,
1024 },
1025
1026 /// A worktree file hashed earlier in an operation had different content when stored.
1027 #[error("{path} changed while it was being added")]
1028 WorktreeFileChanged {
1029 /// Index-relative path of the file.
1030 path: String,
1031 },
1032
1033 /// The index file is missing, truncated, or has a bad header.
1034 #[error("index error: {0}")]
1035 IndexError(String),
1036
1037 /// Stage-1/2/3 entries are present; a stage-0 tree cannot be built.
1038 #[error("index contains unmerged entries")]
1039 IndexUnmerged,
1040
1041 /// Another maintenance run holds the repository's `gc.pid` lock
1042 /// ([`crate::maintenance::GcLock`]); nothing was changed.
1043 #[error("{}", maintenance_running_message(holder))]
1044 MaintenanceRunning {
1045 /// Process recorded in `gc.pid`.
1046 holder: crate::maintenance::GcLockHolder,
1047 },
1048
1049 /// Explicit [`crate::porcelain::add::stage`] refused paths that still contain conflict markers.
1050 #[error("unmerged paths still have conflict markers")]
1051 UnmergedConflictMarkers {
1052 /// Repository-relative paths that were not staged because markers remain.
1053 paths: Vec<String>,
1054 },
1055
1056 /// A file path is a prefix of another index path at the same tree level.
1057 #[error(
1058 "index path prefix conflict between '{}' and '{}'",
1059 directory.display(),
1060 file.display()
1061 )]
1062 IndexPathPrefixConflict {
1063 /// Shorter path (treated as the ambiguous directory prefix).
1064 directory: std::path::PathBuf,
1065 /// Longer path that extends the prefix with an additional component.
1066 file: std::path::PathBuf,
1067 },
1068
1069 /// The `FSMN` index extension ends before its header, token, or EWAH payload.
1070 #[error("index fsmonitor extension payload is truncated")]
1071 IndexFsmonitorExtensionTruncated,
1072
1073 /// The `FSMN` extension header version is not supported.
1074 #[error("index fsmonitor extension has unsupported header version {0}")]
1075 IndexFsmonitorExtensionBadVersion(u32),
1076
1077 /// The EWAH bitmap embedded in `FSMN` could not be decoded.
1078 #[error("index fsmonitor extension EWAH bitmap is invalid")]
1079 IndexFsmonitorExtensionEwahInvalid,
1080
1081 /// The cache-tree extension references more entries than the index contains. Git emits this
1082 /// (verbatim, prefixed with `error: `) when a tree with duplicate path entries is read into the
1083 /// index (`t4058-diff-duplicates`).
1084 #[error("corrupted cache-tree has entries not present in index")]
1085 CacheTreeCorrupt,
1086
1087 /// A reference name or value is invalid.
1088 #[error("invalid ref: {0}")]
1089 InvalidRef(String),
1090
1091 /// A `packed-refs` line Git would reject when reading the ref database.
1092 #[error("unexpected line in {path}: {line}")]
1093 PackedRefsUnexpectedLine {
1094 /// Path to the `packed-refs` file being read.
1095 path: String,
1096 /// Offending line contents.
1097 line: String,
1098 },
1099
1100 /// A general path-related error (invalid UTF-8, out-of-bounds, etc.).
1101 #[error("path error: {0}")]
1102 PathError(String),
1103
1104 /// `--pathspec-from-file` parse failure.
1105 #[error(transparent)]
1106 PathspecFromFile(#[from] PathspecFromFileParseError),
1107
1108 /// A tree/index path component is forbidden (`.git`, HFS/NTFS aliases, etc.).
1109 #[error("invalid path '{0}'")]
1110 InvalidPath(String),
1111
1112 /// A repository-relative path failed [`crate::path_validate`] rules while populating the index or checkout.
1113 #[error("invalid path '{path}': {reason}")]
1114 InvalidPathComponent {
1115 /// Full repository-relative path that was rejected.
1116 path: String,
1117 /// Which component rule failed.
1118 reason: crate::path_validate::PathComponentError,
1119 },
1120
1121 /// A configuration file parsing or access error.
1122 #[error(transparent)]
1123 Config(#[from] ConfigError),
1124
1125 /// Branch create, delete, or upstream configuration failure.
1126 #[error(transparent)]
1127 Branch(#[from] crate::porcelain::branch_error::BranchError),
1128
1129 /// Reference lock failure while updating refs.
1130 #[error(transparent)]
1131 RefLock(#[from] RefLockError),
1132
1133 /// Reference store transaction or iteration failure.
1134 #[error(transparent)]
1135 RefStore(crate::refs::store::RefStoreError),
1136
1137 /// Reftable file, stack, or transaction failure.
1138 #[error(transparent)]
1139 Reftable(#[from] crate::reftable::ReftableError),
1140
1141 /// Filter or EOL conversion failure.
1142 #[error(transparent)]
1143 Filter(#[from] FilterError),
1144
1145 /// Patch application failure.
1146 #[error(transparent)]
1147 Apply(#[from] ApplyError),
1148
1149 /// Diff path selection failure.
1150 #[error(transparent)]
1151 DiffPath(#[from] DiffPathError),
1152
1153 /// Path resolves outside the repository.
1154 #[error(transparent)]
1155 PathOutsideRepo(#[from] PathOutsideRepoError),
1156
1157 /// A commit/tag signing or signature-verification error.
1158 #[error("{0}")]
1159 Signing(String),
1160
1161 /// HTTP authentication failed: the server required credentials (`401`) and
1162 /// either no credential provider was wired, the provider could not supply a
1163 /// usable username/password, the server demanded an unsupported auth scheme,
1164 /// or the supplied credentials were rejected.
1165 ///
1166 /// Distinct from [`Error::Message`] so embedders can detect an authentication
1167 /// failure (and e.g. fall back to an interactive/subprocess path) rather than
1168 /// string-matching, and so the failure surfaces typed instead of hanging.
1169 #[error("authentication failed for {url}: {reason}")]
1170 Auth {
1171 /// Repository URL with credentials redacted and smart-HTTP endpoint suffixes
1172 /// (`/info/refs?service=…`, `/git-upload-pack`, `/git-receive-pack`) removed.
1173 url: String,
1174 /// What went wrong.
1175 reason: AuthFailure,
1176 },
1177
1178 /// An atomic wire push was requested, but the remote did not advertise the
1179 /// `atomic` capability. No ref-update commands or pack data were sent.
1180 #[error("the remote does not support atomic pushes")]
1181 PushAtomicUnsupported,
1182
1183 /// A push carried `--push-option` values but the remote `git-receive-pack`
1184 /// did not advertise the `push-options` capability, so the options cannot be
1185 /// transmitted.
1186 ///
1187 /// Distinct from [`Error::Message`] so embedders can detect this specific
1188 /// negotiation failure (and e.g. fall back to a subprocess push) rather than
1189 /// string-matching. The message matches Git's
1190 /// `fatal: the receiving end does not support push options`.
1191 #[error("the receiving end does not support push options")]
1192 PushOptionsUnsupported,
1193
1194 /// `git fetch --unshallow` on a repository that is not shallow.
1195 #[error("repository is not shallow")]
1196 FetchUnshallowNotShallow,
1197
1198 /// A deepen/shallow fetch was requested but the remote did not advertise the
1199 /// `shallow` capability.
1200 #[error("the remote does not support shallow fetches")]
1201 FetchShallowUnsupported,
1202
1203 /// A `--deepen` fetch was requested but the remote did not advertise
1204 /// `deepen-relative`.
1205 #[error("the remote does not support deepen-relative")]
1206 FetchDeepenRelativeUnsupported,
1207
1208 /// A bundle fetch or verify found prerequisite commits missing from history.
1209 #[error("repository lacks these prerequisite commits")]
1210 BundleMissingPrerequisites,
1211
1212 /// Streaming fetch/clone temp pack disappeared before index-pack (often real-time antivirus).
1213 #[error(
1214 "received pack file missing: {}. If this persists, check real-time antivirus or disk cleanup for quarantined pack temp files.",
1215 path.display()
1216 )]
1217 ReceivedPackTempMissing {
1218 /// Path to the temp pack that was expected on disk.
1219 path: std::path::PathBuf,
1220 },
1221
1222 /// A pathspec's `:(attr:...)` magic is malformed.
1223 #[error("invalid attr magic in pathspec '{spec}': {reason}")]
1224 InvalidAttrPathspec {
1225 /// The offending pathspec as given.
1226 spec: String,
1227 /// What is wrong with its attr clause.
1228 #[source]
1229 reason: AttrPathspecError,
1230 },
1231
1232 /// A pathspec did not match any file known to Git (index or HEAD tree).
1233 #[error("pathspec '{spec}' did not match any file(s) known to git")]
1234 PathspecNoMatch {
1235 /// User-supplied pathspec that matched nothing.
1236 spec: String,
1237 },
1238
1239 /// [`crate::porcelain::restore::restore_paths`] conflict options were combined with `--staged`.
1240 #[error("conflict restore options cannot be used with --staged")]
1241 RestoreConflictWithStaged,
1242
1243 /// [`crate::porcelain::restore::restore_paths`] conflict options were combined with `--source`.
1244 #[error("conflict restore options cannot be used with --source")]
1245 RestoreConflictWithSource,
1246
1247 /// A conflict side was missing from the index for the requested path.
1248 #[error("path '{path}' does not have their version")]
1249 RestoreConflictMissingTheirs {
1250 /// Repository-relative path.
1251 path: String,
1252 },
1253
1254 /// A conflict side was missing from the index for the requested path.
1255 #[error("path '{path}' does not have our version")]
1256 RestoreConflictMissingOurs {
1257 /// Repository-relative path.
1258 path: String,
1259 },
1260
1261 /// Three-way remerge needs stages 1, 2, and 3 in the index.
1262 #[error("path '{path}' is not unmerged")]
1263 RestoreConflictNotUnmerged {
1264 /// Repository-relative path.
1265 path: String,
1266 },
1267
1268 /// [`crate::porcelain::restore::restore_paths`] was asked to restore the worktree from the
1269 /// index for paths that still have conflict stages; pick a side or rebuild the markers instead.
1270 ///
1271 /// Nothing is written when this is returned.
1272 #[error("{}", restore_unmerged_message(paths))]
1273 RestorePathUnmerged {
1274 /// Matched repository-relative paths with conflict stages (sorted, deduplicated).
1275 paths: Vec<String>,
1276 },
1277
1278 /// A literal pathspec names a file under a symlink directory component.
1279 #[error("pathspec '{spec}' is beyond a symbolic link")]
1280 PathspecBeyondSymlink {
1281 /// User-supplied pathspec that crosses a symlink directory.
1282 spec: String,
1283 },
1284
1285 /// [`crate::porcelain::commit::CommitScope::Paths`] was used while a merge is in progress.
1286 #[error("cannot do a partial commit during a merge")]
1287 PartialCommitDuringMerge,
1288
1289 /// Explicit [`crate::porcelain::add::stage`] pathspecs name paths excluded by ignore rules.
1290 #[error("the following paths are ignored:\n{}", paths.join("\n"))]
1291 PathspecIgnored {
1292 /// Repository-relative paths (or user labels) that cannot be staged while ignored.
1293 paths: Vec<String>,
1294 },
1295
1296 /// [`crate::porcelain::paths::remove_paths`] refused paths whose removal would lose
1297 /// content, because [`crate::porcelain::paths::RemoveOptions::force`] was not set.
1298 #[error(
1299 "refusing to remove paths with unsaved content:\n{}",
1300 paths.iter().map(ToString::to_string).collect::<Vec<_>>().join("\n")
1301 )]
1302 RemoveBlocked {
1303 /// Each refused path with the reason it was refused.
1304 paths: Vec<crate::porcelain::paths::BlockedRemoval>,
1305 },
1306
1307 /// A directory pathspec was used without `recursive` ([`crate::porcelain::paths::remove_paths`]).
1308 #[error("not removing '{0}' recursively without -r")]
1309 NotRemovingRecursively(String),
1310
1311 /// A porcelain path operation requires a checked-out work tree.
1312 #[error("this operation must be run in a work tree")]
1313 RequiresWorkTree,
1314
1315 /// A commit was requested with an empty message body after normalization.
1316 #[error("empty commit message")]
1317 EmptyCommitMessage,
1318
1319 /// [`crate::porcelain::paths::remove_paths`] was called with no pathspecs.
1320 #[error("you must specify path(s) to remove")]
1321 RemovePathsRequired,
1322
1323 /// [`crate::porcelain::commit::commit_with_scope`] was called with an empty
1324 /// [`CommitScope::Paths`](crate::porcelain::commit::CommitScope::Paths) pathspec list.
1325 #[error("you must specify path(s)")]
1326 CommitPathsRequired,
1327
1328 /// [`crate::porcelain::paths::move_path`] / [`crate::porcelain::paths::move_paths`] source is not tracked.
1329 #[error("not under version control, source={src}")]
1330 MoveSourceNotTracked {
1331 /// Untracked or missing source path.
1332 src: String,
1333 },
1334
1335 /// Move refused because the destination already exists without `--force`.
1336 #[error("destination exists, source={src}, destination={destination}")]
1337 MoveDestinationExists {
1338 /// Source path in the move request.
1339 src: String,
1340 /// Existing destination path.
1341 destination: String,
1342 },
1343
1344 /// Move refused because the destination lies under the source directory.
1345 #[error("cannot move '{src}' to a subdirectory of itself, '{destination}'")]
1346 MoveIntoSubdirectory {
1347 /// Source directory path.
1348 src: String,
1349 /// Invalid destination under the source.
1350 destination: String,
1351 },
1352
1353 /// Move refused because source and destination are the same path.
1354 #[error("cannot move into itself, source={src}, destination={destination}")]
1355 MoveIntoSelf {
1356 /// Source path.
1357 src: String,
1358 /// Destination equal to the source.
1359 destination: String,
1360 },
1361
1362 /// Move refused because the source has no index entries.
1363 #[error("nothing to move, source={src}")]
1364 MoveNothingToMove {
1365 /// Source path with no index entries.
1366 src: String,
1367 },
1368
1369 /// Multi-source move would write more than one source to the same destination path.
1370 #[error("multiple sources for the same target, destination={destination}")]
1371 MoveDuplicateDestination {
1372 /// Destination path that would receive more than one source.
1373 destination: String,
1374 },
1375
1376 /// A worktree source path is missing or not a file, directory, or symlink.
1377 #[error("bad source, source={src}, destination={destination}")]
1378 MoveBadSource {
1379 /// Source path.
1380 src: String,
1381 /// Planned destination path.
1382 destination: String,
1383 },
1384
1385 /// Internal invariant: a planned rename references a missing index entry.
1386 #[error("internal error: missing index entry for '{path}'")]
1387 MoveMissingIndexEntry {
1388 /// Index path that should exist during a move.
1389 path: String,
1390 },
1391
1392 /// The remote could not be read or written: a missing repository, a failed connection, an
1393 /// HTTP error status, or no Git handshake.
1394 ///
1395 /// Use [`Error::is_remote_unreadable`] to test for it through wrapper errors.
1396 ///
1397 /// Not used for cancellation; see [`Error::is_cancelled`].
1398 #[error("{}", remote_unreadable_message(url.as_deref(), reason))]
1399 RemoteUnreadable {
1400 /// Remote URL or resolved local path, with credentials redacted. `None` when the
1401 /// failing layer only had a connection, not the URL it was opened from.
1402 url: Option<String>,
1403 /// What went wrong.
1404 reason: RemoteUnreadableReason,
1405 },
1406
1407 /// A resumable bundle download or upload failed.
1408 #[error(transparent)]
1409 Resumable(#[from] Box<crate::resumable::ResumableError>),
1410
1411 /// Explicit fetch refspec resolution failed (missing or ambiguous source).
1412 #[error("{0}")]
1413 FetchRefspec(#[from] crate::fetch_request::FetchRefspecError),
1414
1415 /// Fetch refused to overwrite a local branch with a non-fast-forward update.
1416 #[error("non-fast-forward update for {local_ref}")]
1417 FetchNonFastForward {
1418 /// Local ref that would have been overwritten.
1419 local_ref: String,
1420 },
1421
1422 /// Fetch refused to update the branch checked out in this or a linked worktree.
1423 #[error("cannot fetch into checked-out branch '{branch}'")]
1424 FetchIntoCheckedOutBranch {
1425 /// Short branch name under `refs/heads/`.
1426 branch: String,
1427 },
1428
1429 /// Checkout or switch would overwrite local or untracked worktree content.
1430 #[error("{message}")]
1431 WouldOverwrite {
1432 /// Whether tracked local edits or untracked paths block the operation.
1433 reason: OverwriteReason,
1434 /// Repository-relative paths that would be overwritten (sorted, deduplicated).
1435 paths: Vec<String>,
1436 /// Human-readable summary (depends on [`OverwriteReason`]).
1437 message: String,
1438 },
1439
1440 /// Staged or unstaged changes block an operation that needs a clean worktree.
1441 #[error("you have uncommitted changes — commit them before {operation}")]
1442 DirtyWorktree {
1443 /// The operation that was refused.
1444 operation: WorktreeOperation,
1445 /// Repository-relative paths with staged or unstaged changes (sorted, deduplicated;
1446 /// empty when the caller only knew that something changed).
1447 paths: Vec<String>,
1448 },
1449
1450 /// Author or committer on [`crate::porcelain::commit::CommitRequest`] is invalid.
1451 #[error(transparent)]
1452 InvalidIdentity(#[from] crate::ident::CommitIdentityError),
1453
1454 /// A long-running library operation stopped at a cancellation checkpoint.
1455 ///
1456 /// Use [`Error::is_cancelled`] to test for it through wrapper errors
1457 /// ([`crate::remote::RemoteError::is_cancelled`], [`crate::remote::clone::CloneError::is_cancelled`], …).
1458 #[error("operation cancelled")]
1459 Cancelled,
1460
1461 /// A checkout failed or was cancelled before it finished, and putting the work tree back
1462 /// failed for some paths.
1463 ///
1464 /// Refs and the index are unchanged, as after any failed checkout; only `unrestored`
1465 /// differ from the index. Use [`Error::is_cancelled`] to see whether `source` was a
1466 /// cancellation.
1467 #[error("{source}; {} work-tree path(s) could not be restored", unrestored.len())]
1468 CheckoutRollbackIncomplete {
1469 /// Why the checkout stopped.
1470 source: Box<Error>,
1471 /// Repository-relative paths left different from the index (sorted, deduplicated).
1472 unrestored: Vec<String>,
1473 },
1474
1475 /// No usable author or committer identity is configured
1476 /// ([`crate::repo::Repository::identity`]).
1477 #[error(transparent)]
1478 Identity(#[from] crate::config::ident_resolve::IdentityError),
1479
1480 /// User-facing message that should be printed verbatim (no extra prefix).
1481 ///
1482 /// Used for revision errors that must match Git's `fatal:` lines exactly.
1483 #[error("{0}")]
1484 Message(String),
1485
1486 /// A merge was requested with fast-forward only, but the histories diverged.
1487 #[error("not possible to fast-forward; refusing to merge")]
1488 NotFastForward,
1489
1490 /// The index tree matches the parent commit tree and [`allow_empty`](crate::porcelain::commit::CommitRequest::allow_empty) is false.
1491 #[error("nothing to commit")]
1492 NothingToCommit,
1493
1494 /// No staged changes are available for a [`CommitScope::Staged`](crate::porcelain::commit::CommitScope::Staged)
1495 /// commit, but the work tree has unstaged or untracked paths.
1496 #[error("nothing staged to commit")]
1497 NothingStagedToCommit {
1498 /// Tracked paths that differ between the index and the work tree.
1499 unstaged: usize,
1500 /// Untracked paths under the work tree.
1501 untracked: usize,
1502 },
1503
1504 /// [`create_commit`](crate::porcelain::commit::create_commit) requires a branch checkout.
1505 #[error("HEAD is detached")]
1506 DetachedHead,
1507
1508 /// `HEAD` is on a branch with no commits yet, but the operation needs a commit
1509 /// ([`Repository::head_commit`](crate::repo::Repository::head_commit),
1510 /// [`replay_commit`](crate::porcelain::replay::replay_commit)).
1511 #[error("no commits yet on this branch")]
1512 UnbornHead,
1513
1514 /// `HEAD` could not be resolved to a branch, detached commit, or unborn branch.
1515 #[error("HEAD is in an unknown state")]
1516 InvalidHead,
1517
1518 /// [`replay_commit`](crate::porcelain::replay::replay_commit) does not replay merge commits
1519 /// until callers can select a mainline parent explicitly.
1520 #[error("merge commit {oid}")]
1521 MergeCommit {
1522 /// Object id of the merge commit that cannot be replayed yet.
1523 oid: crate::objects::ObjectId,
1524 },
1525
1526 /// Cherry-pick source is already checked out as `HEAD`.
1527 #[error("commit already at HEAD")]
1528 ReplaySourceAtHead,
1529
1530 /// Sequencer todo lists a command grit does not implement yet.
1531 #[error("unknown sequencer command '{command}'")]
1532 SequencerUnknownCommand {
1533 /// Verb read from `.git/sequencer/todo`.
1534 command: String,
1535 },
1536
1537 /// [`abort_replay_sequence`](crate::porcelain::replay_sequence::abort_replay_sequence) refuses when `HEAD` moved since the replay started.
1538 #[error("cannot abort replay: HEAD moved since replay started")]
1539 ReplayAbortHeadMoved,
1540
1541 /// [`continue_replay_sequence`](crate::porcelain::replay_sequence::continue_replay_sequence) refuses while unmerged index entries remain.
1542 #[error("cannot continue replay: unmerged index entries remain")]
1543 ReplayContinueUnmerged,
1544
1545 /// No cherry-pick or revert sequence is in progress.
1546 #[error("no cherry-pick or revert in progress")]
1547 NoReplayInProgress,
1548
1549 /// The command would move `HEAD` while a paused operation still depends on it
1550 /// (see [`operation_blocking_switch`](crate::state::operation_blocking_switch)).
1551 #[error("{} in progress", .0.description())]
1552 OperationInProgress(crate::state::InProgressOperation),
1553
1554 /// A local branch is checked out as `HEAD` in a linked or main worktree.
1555 #[error("branch '{branch}' is already checked out in '{worktree_path}'")]
1556 BranchCheckedOutInWorktree {
1557 /// Short branch name (e.g. `main`).
1558 branch: String,
1559 /// Path to the worktree that has the branch checked out.
1560 worktree_path: String,
1561 },
1562
1563 /// `refs/heads/<branch>` does not exist.
1564 #[error("no branch named '{branch}'")]
1565 BranchNotFound {
1566 /// Short branch name (e.g. `topic`).
1567 branch: String,
1568 },
1569
1570 /// [`list_branches`](crate::branch_tracking::list_branches) with [`BranchDetail::Names`](crate::branch_tracking::BranchDetail::Names) cannot use date sort.
1571 #[error("branch list with Names detail cannot use date sort (date sort reads commit objects)")]
1572 BranchListNamesDateSort,
1573
1574 /// [`delete_branch`](crate::porcelain::branch::delete_branch) refuses to delete the branch
1575 /// `HEAD` points at.
1576 #[error("cannot delete '{branch}' — it is the current branch")]
1577 CannotDeleteCurrentBranch {
1578 /// Short branch name (e.g. `main`).
1579 branch: String,
1580 },
1581
1582 /// [`delete_branch`](crate::porcelain::branch::delete_branch) without `force` found commits
1583 /// on the branch that are not reachable from its upstream (or from `HEAD` when it has none).
1584 #[error("the branch '{branch}' is not fully merged")]
1585 BranchNotMerged {
1586 /// Short branch name (e.g. `topic`).
1587 branch: String,
1588 },
1589
1590 /// `refs/tags/<tag>` does not exist.
1591 #[error("no tag named '{tag}'")]
1592 TagNotFound {
1593 /// Short tag name (e.g. `v1.0`).
1594 tag: String,
1595 },
1596
1597 /// Tag create/delete validation failure ([`crate::porcelain::tag::TagError`]).
1598 #[error(transparent)]
1599 Tag(#[from] crate::porcelain::tag::TagError),
1600
1601 /// Linked worktree add/remove/move/lock/repair failure ([`crate::worktree::WorktreeError`]).
1602 #[error(transparent)]
1603 Worktree(#[from] crate::worktree::WorktreeError),
1604
1605 /// Stash commit object has fewer parents or an invalid shape for apply/show.
1606 #[error("corrupt stash commit: {0}")]
1607 CorruptStash(&'static str),
1608
1609 /// `stash@{{n}}` is out of range for the current stash reflog.
1610 #[error("stash entry stash@{{{n}}} not found")]
1611 StashNotFound {
1612 /// Stash reflog index (`stash@{{n}}`).
1613 n: usize,
1614 },
1615
1616 /// Ref does not exist and has no reflog entries.
1617 #[error("unknown ref '{refname}'")]
1618 ReflogUnknownRef {
1619 /// User-supplied or DWIM ref name.
1620 refname: String,
1621 },
1622
1623 /// A reflog selector is missing its reference or `@{n}` delimiters.
1624 #[error("expected ref@{{n}} form, got '{spec}'")]
1625 ReflogInvalidSelector {
1626 /// Selector after trimming surrounding whitespace.
1627 spec: String,
1628 },
1629
1630 /// A reflog selector index is not a nonnegative integer that fits in `usize`.
1631 #[error("invalid reflog index in '{spec}'")]
1632 ReflogInvalidIndex {
1633 /// Selector after trimming surrounding whitespace.
1634 spec: String,
1635 /// Integer parsing failure, including overflow.
1636 #[source]
1637 source: std::num::ParseIntError,
1638 },
1639
1640 /// `ref@{{n}}` is out of range for the current reflog.
1641 #[error("reflog entry {refname}@{{{index}}} not found")]
1642 ReflogIndexOutOfRange {
1643 /// Ref whose reflog was read.
1644 refname: String,
1645 /// Requested newest-first index.
1646 index: usize,
1647 },
1648
1649 /// Pushing or creating a stash ([`crate::repo_handles::RepositoryStash::push`]) requires at least one commit.
1650 #[error("cannot stash without a commit")]
1651 StashNoInitialCommit,
1652
1653 /// Stash create/apply/pop requires a working tree (not a bare repository).
1654 #[error("cannot stash in a bare repository")]
1655 StashBareRepository,
1656
1657 /// [`crate::repo_handles::RepositoryStash::apply`] with `restore_index` could
1658 /// not merge the current index with the stashed index parent.
1659 #[error("conflicts in index while applying stash with --index")]
1660 StashIndexConflict,
1661
1662 /// Incompatible [`StashPushOptions`](crate::porcelain::stash::StashPushOptions) flags.
1663 #[error("{0}")]
1664 StashOptionConflict(String),
1665
1666 /// Partial [`crate::repo_handles::RepositoryStash::push`] hunk selection failed.
1667 #[error(transparent)]
1668 HunkSelection(#[from] crate::porcelain::hunk_select::HunkSelectError),
1669
1670 /// HEAD does not resolve while applying a stash entry.
1671 #[error("missing HEAD while applying stash")]
1672 StashMissingHead,
1673
1674 /// Applying a stash would overwrite locally modified worktree content.
1675 #[error(
1676 "your local changes to the following files would be overwritten by stash apply: {paths}"
1677 )]
1678 StashWouldOverwriteLocalChanges {
1679 /// Space-separated list of paths that would be overwritten.
1680 paths: String,
1681 },
1682
1683 /// The index could not be written after a successful stash apply.
1684 #[error("writing index after stash apply: {0}")]
1685 StashIndexWrite(String),
1686
1687 /// A symlink blob in a stash commit is not valid UTF-8.
1688 #[error("symlink target is not UTF-8")]
1689 StashSymlinkNotUtf8,
1690
1691 /// Applying a stash with an untracked-files parent would replace an existing worktree path.
1692 #[error("stash untracked restore blocked: '{path}' already exists on disk")]
1693 StashCouldNotRestoreUntracked {
1694 /// Worktree path that already exists and blocks untracked restore.
1695 path: String,
1696 },
1697
1698 /// [`CommitRequest::amend`](crate::porcelain::commit::CommitRequest::amend) was set on an unborn branch.
1699 #[error("cannot amend: no commits on this branch yet")]
1700 AmendUnborn,
1701
1702 /// [`RepositoryNotes::add`](crate::repo_handles::RepositoryNotes::add) was called without
1703 /// `force` when a note already exists on the object.
1704 #[error("note for object {object} already exists")]
1705 NoteAlreadyExists {
1706 /// Annotated object that already has a note.
1707 object: crate::objects::ObjectId,
1708 },
1709
1710 /// A configured or environment notes ref is not under `refs/notes/`.
1711 #[error("notes ref '{0}' must start with 'refs/notes/'")]
1712 InvalidNotesRef(String),
1713
1714 /// A notes ref resolves to an object that cannot hold notes (notes reads accept a commit or
1715 /// a tree; notes merges require a commit).
1716 #[error("notes ref '{notes_ref}' points to a {found}, not a notes commit")]
1717 NotesRefNotCommit {
1718 /// Notes ref (or revision) that was read.
1719 notes_ref: String,
1720 /// Kind of object the ref resolved to.
1721 found: crate::objects::ObjectKind,
1722 },
1723
1724 /// An object reached while reading a notes tree has the wrong kind (for example a note
1725 /// entry that is not a blob).
1726 #[error("notes object {oid} is a {found}, expected a {expected}")]
1727 NotesObjectKind {
1728 /// Object that was read.
1729 oid: crate::objects::ObjectId,
1730 /// Kind the notes layout requires at this position.
1731 expected: crate::objects::ObjectKind,
1732 /// Kind actually stored.
1733 found: crate::objects::ObjectKind,
1734 },
1735
1736 /// [`notes_merge_inner`](crate::notes::notes_merge_inner) was asked to merge two notes refs
1737 /// that both have no notes commit.
1738 #[error("cannot merge notes: neither '{local}' nor '{remote}' has any notes")]
1739 NotesMergeBothEmpty {
1740 /// Local notes ref (merge destination).
1741 local: String,
1742 /// Remote notes ref (merge source).
1743 remote: String,
1744 },
1745
1746 /// A manual notes merge was started while an earlier one still has conflict files in the
1747 /// notes merge worktree.
1748 #[error("unfinished notes merge: conflict files remain in {}", path.display())]
1749 NotesMergeInProgress {
1750 /// The notes merge worktree directory.
1751 path: std::path::PathBuf,
1752 },
1753
1754 /// Multi-pack-index load or write failure.
1755 #[error(transparent)]
1756 Midx(#[from] MidxError),
1757
1758 /// Revision parsing failed ([`crate::rev_parse_error::RevParseError`]).
1759 #[error(transparent)]
1760 RevParse(#[from] crate::rev_parse_error::RevParseError),
1761
1762 /// Revision walking / `rev-list` option parsing failed ([`crate::rev_list_error::RevListError`]).
1763 #[error(transparent)]
1764 RevList(#[from] crate::rev_list_error::RevListError),
1765
1766 /// A Git hook subprocess failed or could not be started.
1767 #[error(transparent)]
1768 Hook(#[from] crate::hooks::HookError),
1769
1770 /// History rewrite planning failed ([`crate::history::HistoryError`]).
1771 #[error(transparent)]
1772 History(#[from] crate::history::HistoryError),
1773
1774 /// The operation needs the files-backed `objects/` directory, but the repository's
1775 /// [`Odb`](crate::odb::Odb) uses a custom [`OdbBuilder::primary`](crate::odb::OdbBuilder::primary)
1776 /// store. Nothing was written; use a files-backed repository for this operation.
1777 #[error("{operation} requires the files-backed object store; it is not supported with a custom primary")]
1778 UnsupportedObjectStore {
1779 /// Operation that was refused (for example `"fetch"`, `"push"`, `"gc"`).
1780 operation: &'static str,
1781 },
1782
1783 /// [`RepositoryBuilder::open`](crate::repo::RepositoryBuilder::open) was given an
1784 /// [`OdbBuilder`](crate::odb::OdbBuilder) rooted somewhere other than the repository's
1785 /// `objects/` directory.
1786 #[error(
1787 "object database {} is not the repository's objects directory {}",
1788 builder.display(),
1789 repository.display()
1790 )]
1791 OdbObjectsDirMismatch {
1792 /// The builder's objects directory, canonicalized when it exists.
1793 builder: std::path::PathBuf,
1794 /// The repository's canonical objects directory.
1795 repository: std::path::PathBuf,
1796 },
1797
1798 /// [`RepositoryBuilder::open`](crate::repo::RepositoryBuilder::open) was given an
1799 /// [`OdbBuilder`](crate::odb::OdbBuilder) whose custom primary or read source hashes
1800 /// objects with a different algorithm than the repository's `extensions.objectformat`
1801 /// (SHA-1 when unset).
1802 #[error(
1803 "object store hashes with {} but the repository's object format is {}",
1804 store.name(),
1805 repository.name()
1806 )]
1807 #[non_exhaustive]
1808 ObjectFormatMismatch {
1809 /// The repository's object format from its config.
1810 repository: crate::objects::HashAlgo,
1811 /// The first injected store's [`hash_algo`](crate::odb::store::ObjectStore::hash_algo)
1812 /// that differs from `repository`.
1813 store: crate::objects::HashAlgo,
1814 },
1815
1816 /// Parsing a [`crate::tree_sha256::TreeSha256`] from hex failed (expected 64 hex digits).
1817 #[error("invalid tree-sha256 hex")]
1818 TreeSha256InvalidHex,
1819
1820 /// A revision passed to [`crate::tree_sha256::tree_sha256`] did not peel to a commit or tree.
1821 #[error("tree-sha256: revision resolves to {found:?}, not a commit or tree")]
1822 TreeSha256InvalidRevision {
1823 /// Object kind after peeling annotated tags.
1824 found: crate::objects::ObjectKind,
1825 },
1826
1827 /// One or more submodules required for [`crate::tree_sha256::tree_sha256`] are missing.
1828 #[error("tree-sha256: missing submodules")]
1829 TreeSha256MissingSubmodules(Vec<crate::tree_sha256::MissingSubmodule>),
1830
1831 /// `--tree-sha256` was requested with signing explicitly disabled.
1832 #[error("tree-sha256 requires a signed tag")]
1833 TreeSha256RequiresSigning,
1834
1835 /// [`crate::tree_sha256::verify_tag`] was given an object that is not an annotated tag.
1836 #[error("verify-tag: object {oid} is a {found:?}, not an annotated tag")]
1837 TagVerifyNotTag {
1838 /// Object id passed to verification.
1839 oid: crate::objects::ObjectId,
1840 /// Kind stored in the ODB.
1841 found: crate::objects::ObjectKind,
1842 },
1843
1844 /// Annotated tag header block has no `object` line.
1845 #[error("verify-tag: tag has no object header")]
1846 TagVerifyMissingObjectHeader,
1847
1848 /// Annotated tag carries more than one `tree-sha256` header line.
1849 #[error("verify-tag: tag has more than one tree-sha256 header")]
1850 TagVerifyDuplicateTreeSha256Header,
1851
1852 /// Annotated tag tree-sha256 header parsing failed.
1853 #[error(transparent)]
1854 TreeSha256Claim(#[from] crate::tree_sha256::TreeSha256ClaimError),
1855
1856 /// `grit blame --ignore-rev` did not resolve to a commit.
1857 #[error("cannot find revision {rev} to ignore")]
1858 BlameIgnoreRevUnknown {
1859 /// Revision expression from `--ignore-rev`.
1860 rev: String,
1861 },
1862
1863 /// `--ignore-revs-file` could not be read.
1864 #[error("could not open object name list: {path}")]
1865 BlameIgnoreRevsFileOpen {
1866 /// Path argument (before expansion).
1867 path: String,
1868 /// Underlying open error.
1869 #[source]
1870 source: std::io::Error,
1871 },
1872
1873 /// A line in an ignore-revs file is not a full-length object id.
1874 #[error("invalid object name: {rev}")]
1875 BlameIgnoreRevsFileInvalidRev {
1876 /// Path argument for the file.
1877 path: String,
1878 /// Offending line text after comment/trim handling.
1879 rev: String,
1880 },
1881}
1882
1883impl Error {
1884 /// Stable snake_case name of this error's variant (for example `"not_a_repository"`),
1885 /// for scripts and serialized output that must branch on the failure kind.
1886 ///
1887 /// The name is the variant name in snake_case; it changes only when the variant is
1888 /// renamed. Match on the error value itself in Rust code.
1889 ///
1890 /// # Examples
1891 ///
1892 /// ```
1893 /// use grit_lib::error::Error;
1894 ///
1895 /// assert_eq!(Error::NothingToCommit.kind_name(), "nothing_to_commit");
1896 /// assert_eq!(Error::Cancelled.kind_name(), "cancelled");
1897 /// ```
1898 #[must_use]
1899 pub fn kind_name(&self) -> &'static str {
1900 match self {
1901 Self::NotARepository(..) => "not_a_repository",
1902 Self::ForbiddenBareRepository(..) => "forbidden_bare_repository",
1903 Self::DubiousOwnership(..) => "dubious_ownership",
1904 Self::UnsupportedRepositoryFormatVersion(..) => "unsupported_repository_format_version",
1905 Self::UnsupportedRepositoryExtension(..) => "unsupported_repository_extension",
1906 Self::InvalidRefStorageFormat { .. } => "invalid_ref_storage_format",
1907 Self::InitObjectFormatChange => "init_object_format_change",
1908 Self::InitBareWithSeparateGitDir => "init_bare_with_separate_git_dir",
1909 Self::InvalidObjectId(..) => "invalid_object_id",
1910 Self::HashCollision { .. } => "hash_collision",
1911 Self::ObjectNotFound(..) => "object_not_found",
1912 Self::RevisionNotFound(..) => "revision_not_found",
1913 Self::PromisedObjectMissing(..) => "promised_object_missing",
1914 Self::CorruptObject(..) => "corrupt_object",
1915 Self::PackIndexChecksumMismatch { .. } => "pack_index_checksum_mismatch",
1916 Self::UnresolvedDeltas { .. } => "unresolved_deltas",
1917 Self::InvalidObject { .. } => "invalid_object",
1918 Self::InvalidTreeEntry(..) => "invalid_tree_entry",
1919 Self::InvalidSignature(..) => "invalid_signature",
1920 Self::UnexpectedObjectKind { .. } => "unexpected_object_kind",
1921 Self::CommitGraphParentMissing { .. } => "commit_graph_parent_missing",
1922 Self::DeltaChainTooDeep { .. } => "delta_chain_too_deep",
1923 Self::UnknownObjectType(..) => "unknown_object_type",
1924 Self::ObjectHeaderTooLong { .. } => "object_header_too_long",
1925 Self::Io(..) => "io",
1926 Self::LockHeld { .. } => "lock_held",
1927 Self::Zlib(..) => "zlib",
1928 Self::LooseHashMismatch { .. } => "loose_hash_mismatch",
1929 Self::LooseObjectWrite { .. } => "loose_object_write",
1930 Self::WorktreePathIo { .. } => "worktree_path_io",
1931 Self::GitDirPathIo { .. } => "git_dir_path_io",
1932 Self::ObjectStreamTruncated { .. } => "object_stream_truncated",
1933 Self::WorktreeFileChanged { .. } => "worktree_file_changed",
1934 Self::IndexError(..) => "index_error",
1935 Self::IndexUnmerged => "index_unmerged",
1936 Self::MaintenanceRunning { .. } => "maintenance_running",
1937 Self::UnmergedConflictMarkers { .. } => "unmerged_conflict_markers",
1938 Self::IndexPathPrefixConflict { .. } => "index_path_prefix_conflict",
1939 Self::IndexFsmonitorExtensionTruncated => "index_fsmonitor_extension_truncated",
1940 Self::IndexFsmonitorExtensionBadVersion(..) => "index_fsmonitor_extension_bad_version",
1941 Self::IndexFsmonitorExtensionEwahInvalid => "index_fsmonitor_extension_ewah_invalid",
1942 Self::CacheTreeCorrupt => "cache_tree_corrupt",
1943 Self::InvalidRef(..) => "invalid_ref",
1944 Self::PackedRefsUnexpectedLine { .. } => "packed_refs_unexpected_line",
1945 Self::PathError(..) => "path_error",
1946 Self::PathspecFromFile(..) => "pathspec_from_file",
1947 Self::InvalidPath(..) => "invalid_path",
1948 Self::InvalidPathComponent { .. } => "invalid_path_component",
1949 Self::Config(..) => "config",
1950 Self::Branch(..) => "branch",
1951 Self::RefLock(..) => "ref_lock",
1952 Self::RefStore(..) => "ref_store",
1953 Self::Reftable(..) => "reftable",
1954 Self::Filter(..) => "filter",
1955 Self::Apply(..) => "apply",
1956 Self::DiffPath(..) => "diff_path",
1957 Self::PathOutsideRepo(..) => "path_outside_repo",
1958 Self::Signing(..) => "signing",
1959 Self::Auth { .. } => "auth",
1960 Self::PushAtomicUnsupported => "push_atomic_unsupported",
1961 Self::PushOptionsUnsupported => "push_options_unsupported",
1962 Self::FetchUnshallowNotShallow => "fetch_unshallow_not_shallow",
1963 Self::FetchShallowUnsupported => "fetch_shallow_unsupported",
1964 Self::FetchDeepenRelativeUnsupported => "fetch_deepen_relative_unsupported",
1965 Self::BundleMissingPrerequisites => "bundle_missing_prerequisites",
1966 Self::ReceivedPackTempMissing { .. } => "received_pack_temp_missing",
1967 Self::InvalidAttrPathspec { .. } => "invalid_attr_pathspec",
1968 Self::PathspecNoMatch { .. } => "pathspec_no_match",
1969 Self::RestoreConflictWithStaged => "restore_conflict_with_staged",
1970 Self::RestoreConflictWithSource => "restore_conflict_with_source",
1971 Self::RestoreConflictMissingTheirs { .. } => "restore_conflict_missing_theirs",
1972 Self::RestoreConflictMissingOurs { .. } => "restore_conflict_missing_ours",
1973 Self::RestoreConflictNotUnmerged { .. } => "restore_conflict_not_unmerged",
1974 Self::RestorePathUnmerged { .. } => "restore_path_unmerged",
1975 Self::PathspecBeyondSymlink { .. } => "pathspec_beyond_symlink",
1976 Self::PartialCommitDuringMerge => "partial_commit_during_merge",
1977 Self::PathspecIgnored { .. } => "pathspec_ignored",
1978 Self::RemoveBlocked { .. } => "remove_blocked",
1979 Self::NotRemovingRecursively(..) => "not_removing_recursively",
1980 Self::RequiresWorkTree => "requires_work_tree",
1981 Self::EmptyCommitMessage => "empty_commit_message",
1982 Self::RemovePathsRequired => "remove_paths_required",
1983 Self::CommitPathsRequired => "commit_paths_required",
1984 Self::MoveSourceNotTracked { .. } => "move_source_not_tracked",
1985 Self::MoveDestinationExists { .. } => "move_destination_exists",
1986 Self::MoveIntoSubdirectory { .. } => "move_into_subdirectory",
1987 Self::MoveIntoSelf { .. } => "move_into_self",
1988 Self::MoveNothingToMove { .. } => "move_nothing_to_move",
1989 Self::MoveDuplicateDestination { .. } => "move_duplicate_destination",
1990 Self::MoveBadSource { .. } => "move_bad_source",
1991 Self::MoveMissingIndexEntry { .. } => "move_missing_index_entry",
1992 Self::RemoteUnreadable { .. } => "remote_unreadable",
1993 Self::Resumable(..) => "resumable",
1994 Self::FetchRefspec(..) => "fetch_refspec",
1995 Self::FetchNonFastForward { .. } => "fetch_non_fast_forward",
1996 Self::FetchIntoCheckedOutBranch { .. } => "fetch_into_checked_out_branch",
1997 Self::WouldOverwrite { .. } => "would_overwrite",
1998 Self::DirtyWorktree { .. } => "dirty_worktree",
1999 Self::InvalidIdentity(..) => "invalid_identity",
2000 Self::Cancelled => "cancelled",
2001 Self::Identity(..) => "identity",
2002 Self::Message(..) => "message",
2003 Self::NotFastForward => "not_fast_forward",
2004 Self::NothingToCommit => "nothing_to_commit",
2005 Self::UnrelatedHistories { .. } => "unrelated_histories",
2006 Self::ObjectFormatMismatch { .. } => "object_format_mismatch",
2007 Self::CheckoutRollbackIncomplete { .. } => "checkout_rollback_incomplete",
2008 Self::NothingStagedToCommit { .. } => "nothing_staged_to_commit",
2009 Self::DetachedHead => "detached_head",
2010 Self::UnbornHead => "unborn_head",
2011 Self::InvalidHead => "invalid_head",
2012 Self::MergeCommit { .. } => "merge_commit",
2013 Self::ReplaySourceAtHead => "replay_source_at_head",
2014 Self::SequencerUnknownCommand { .. } => "sequencer_unknown_command",
2015 Self::ReplayAbortHeadMoved => "replay_abort_head_moved",
2016 Self::ReplayContinueUnmerged => "replay_continue_unmerged",
2017 Self::NoReplayInProgress => "no_replay_in_progress",
2018 Self::OperationInProgress(..) => "operation_in_progress",
2019 Self::BranchCheckedOutInWorktree { .. } => "branch_checked_out_in_worktree",
2020 Self::BranchNotFound { .. } => "branch_not_found",
2021 Self::BranchListNamesDateSort => "branch_list_names_date_sort",
2022 Self::CannotDeleteCurrentBranch { .. } => "cannot_delete_current_branch",
2023 Self::BranchNotMerged { .. } => "branch_not_merged",
2024 Self::TagNotFound { .. } => "tag_not_found",
2025 Self::Tag(..) => "tag",
2026 Self::Worktree(..) => "worktree",
2027 Self::CorruptStash(..) => "corrupt_stash",
2028 Self::StashNotFound { .. } => "stash_not_found",
2029 Self::ReflogUnknownRef { .. } => "reflog_unknown_ref",
2030 Self::ReflogInvalidSelector { .. } => "reflog_invalid_selector",
2031 Self::ReflogInvalidIndex { .. } => "reflog_invalid_index",
2032 Self::ReflogIndexOutOfRange { .. } => "reflog_index_out_of_range",
2033 Self::StashNoInitialCommit => "stash_no_initial_commit",
2034 Self::StashBareRepository => "stash_bare_repository",
2035 Self::StashIndexConflict => "stash_index_conflict",
2036 Self::StashOptionConflict(..) => "stash_option_conflict",
2037 Self::HunkSelection(..) => "hunk_selection",
2038 Self::StashMissingHead => "stash_missing_head",
2039 Self::StashWouldOverwriteLocalChanges { .. } => "stash_would_overwrite_local_changes",
2040 Self::StashIndexWrite(..) => "stash_index_write",
2041 Self::StashSymlinkNotUtf8 => "stash_symlink_not_utf8",
2042 Self::StashCouldNotRestoreUntracked { .. } => "stash_could_not_restore_untracked",
2043 Self::AmendUnborn => "amend_unborn",
2044 Self::NoteAlreadyExists { .. } => "note_already_exists",
2045 Self::InvalidNotesRef(..) => "invalid_notes_ref",
2046 Self::NotesRefNotCommit { .. } => "notes_ref_not_commit",
2047 Self::NotesObjectKind { .. } => "notes_object_kind",
2048 Self::NotesMergeBothEmpty { .. } => "notes_merge_both_empty",
2049 Self::NotesMergeInProgress { .. } => "notes_merge_in_progress",
2050 Self::Midx(..) => "midx",
2051 Self::RevParse(..) => "rev_parse",
2052 Self::RevList(..) => "rev_list",
2053 Self::Hook(..) => "hook",
2054 Self::History(..) => "history",
2055 Self::UnsupportedObjectStore { .. } => "unsupported_object_store",
2056 Self::OdbObjectsDirMismatch { .. } => "odb_objects_dir_mismatch",
2057 Self::TreeSha256InvalidHex => "tree_sha256_invalid_hex",
2058 Self::TreeSha256InvalidRevision { .. } => "tree_sha256_invalid_revision",
2059 Self::TreeSha256MissingSubmodules(..) => "tree_sha256_missing_submodules",
2060 Self::TreeSha256RequiresSigning => "tree_sha256_requires_signing",
2061 Self::TagVerifyNotTag { .. } => "tag_verify_not_tag",
2062 Self::TagVerifyMissingObjectHeader => "tag_verify_missing_object_header",
2063 Self::TagVerifyDuplicateTreeSha256Header => "tag_verify_duplicate_tree_sha256_header",
2064 Self::TreeSha256Claim(..) => "tree_sha256_claim",
2065 Self::BlameIgnoreRevUnknown { .. } => "blame_ignore_rev_unknown",
2066 Self::BlameIgnoreRevsFileOpen { .. } => "blame_ignore_revs_file_open",
2067 Self::BlameIgnoreRevsFileInvalidRev { .. } => "blame_ignore_revs_file_invalid_rev",
2068 }
2069 }
2070
2071 /// Build [`Error::DirtyWorktree`], sorting and deduplicating `paths`.
2072 #[must_use]
2073 pub fn dirty_worktree(operation: WorktreeOperation, mut paths: Vec<String>) -> Self {
2074 paths.sort();
2075 paths.dedup();
2076 Self::DirtyWorktree { operation, paths }
2077 }
2078
2079 /// Build [`Error::WouldOverwrite`] for switch and similar tree transitions.
2080 #[must_use]
2081 pub fn would_overwrite(reason: OverwriteReason, mut paths: Vec<String>) -> Self {
2082 paths.sort();
2083 paths.dedup();
2084 let message = match reason {
2085 OverwriteReason::LocalChanges => {
2086 "you have uncommitted changes — commit them before switching".into()
2087 }
2088 OverwriteReason::Untracked => {
2089 let first = paths.first().map(String::as_str).unwrap_or("<path>");
2090 format!("untracked file '{first}' would be overwritten — move or remove it first")
2091 }
2092 };
2093 Self::WouldOverwrite {
2094 reason,
2095 paths,
2096 message,
2097 }
2098 }
2099
2100 /// Build [`Error::RemoteUnreadable`] for `url`.
2101 #[must_use]
2102 pub fn remote_unreadable(url: impl Into<String>, reason: RemoteUnreadableReason) -> Self {
2103 Self::RemoteUnreadable {
2104 url: Some(url.into()),
2105 reason,
2106 }
2107 }
2108
2109 /// Whether this is [`Error::RemoteUnreadable`]: the remote could not be read or written
2110 /// at all (as opposed to a rejected update or an empty remote).
2111 #[must_use]
2112 pub fn is_remote_unreadable(&self) -> bool {
2113 matches!(self, Self::RemoteUnreadable { .. })
2114 }
2115
2116 /// Whether this is [`Error::Cancelled`] or a nested history failure that was cancelled.
2117 #[must_use]
2118 pub fn is_cancelled(&self) -> bool {
2119 match self {
2120 Self::Cancelled => true,
2121 Self::History(e) => e.is_cancelled(),
2122 Self::CheckoutRollbackIncomplete { source, .. } => source.is_cancelled(),
2123 _ => false,
2124 }
2125 }
2126
2127 /// Build [`Error::InvalidPathComponent`].
2128 #[must_use]
2129 pub fn invalid_path_component(
2130 path: impl Into<String>,
2131 reason: crate::path_validate::PathComponentError,
2132 ) -> Self {
2133 Self::InvalidPathComponent {
2134 path: path.into(),
2135 reason,
2136 }
2137 }
2138
2139 /// When set, the CLI should prefix stderr with `fatal: ` for Git-compatible commands.
2140 #[must_use]
2141 pub fn wants_fatal_prefix(&self) -> bool {
2142 matches!(
2143 self,
2144 Error::Config(_)
2145 | Error::RefLock(_)
2146 | Error::Filter(_)
2147 | Error::PathOutsideRepo(_)
2148 | Error::DiffPath(_)
2149 )
2150 }
2151
2152 /// Format for Git-style stderr (adds `fatal:` / `error:` when appropriate).
2153 #[must_use]
2154 pub fn git_stderr_message(&self) -> String {
2155 match self {
2156 Error::Apply(ApplyError::CorruptPatch { .. }) => {
2157 crate::diagnostics::error_line(&self.to_string())
2158 }
2159 Error::Message(s) => s.clone(),
2160 Error::WouldOverwrite { message, .. } => message.clone(),
2161 e if e.wants_fatal_prefix() => crate::diagnostics::fatal_line(&e.to_string()),
2162 other => other.to_string(),
2163 }
2164 }
2165}
2166
2167impl From<crate::refs::store::RefStoreError> for Error {
2168 fn from(err: crate::refs::store::RefStoreError) -> Self {
2169 match err {
2170 crate::refs::store::RefStoreError::GitDirPathIo {
2171 path,
2172 operation,
2173 os_error,
2174 } => Error::GitDirPathIo {
2175 path,
2176 operation,
2177 source: crate::refs::store::RefStoreError::git_dir_io_source(os_error),
2178 },
2179 other => Error::RefStore(other),
2180 }
2181 }
2182}
2183
2184impl From<crate::lockfile::LockError> for Error {
2185 /// [`LockError::Held`](crate::lockfile::LockError::Held) becomes [`Error::LockHeld`] with the
2186 /// recorded [`LockHolder`](crate::lockfile::LockHolder), and I/O failures become [`Error::Io`].
2187 #[cold]
2188 fn from(err: crate::lockfile::LockError) -> Self {
2189 use crate::lockfile::{LockError, LockHolder};
2190 match err {
2191 LockError::Held { path } => Self::LockHeld {
2192 holder: LockHolder::read(&path),
2193 path,
2194 },
2195 LockError::Io { source, .. } => Self::Io(source),
2196 }
2197 }
2198}
2199
2200/// Convenience alias for `Result<T, Error>`.
2201pub type Result<T> = std::result::Result<T, Error>;
2202
2203/// Serializes as `{"kind": <Error::kind_name>, "message": <Display text>}`.
2204#[cfg(feature = "serde-models")]
2205impl serde::Serialize for Error {
2206 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
2207 where
2208 S: serde::Serializer,
2209 {
2210 use serde::ser::SerializeStruct;
2211
2212 let mut out = serializer.serialize_struct("Error", 2)?;
2213 out.serialize_field("kind", self.kind_name())?;
2214 out.serialize_field("message", &self.to_string())?;
2215 out.end()
2216 }
2217}