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