Skip to main content

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    /// An embedded git repository (nested `.git` checkout) has no commit checked out on its
1377    /// current branch, so it cannot be recorded as a gitlink or as regular files.
1378    #[error("embedded git repository '{path}' has no commit checked out")]
1379    EmbeddedRepoWithoutCommit {
1380        /// Repository-relative path to the nested checkout directory.
1381        path: String,
1382    },
1383
1384    /// Explicit [`crate::porcelain::add::stage`] pathspecs name paths excluded by ignore rules.
1385    #[error("the following paths are ignored:\n{}", paths.join("\n"))]
1386    PathspecIgnored {
1387        /// Repository-relative paths (or user labels) that cannot be staged while ignored.
1388        paths: Vec<String>,
1389    },
1390
1391    /// [`crate::porcelain::paths::remove_paths`] refused paths whose removal would lose
1392    /// content, because [`crate::porcelain::paths::RemoveOptions::force`] was not set.
1393    #[error(
1394        "refusing to remove paths with unsaved content:\n{}",
1395        paths.iter().map(ToString::to_string).collect::<Vec<_>>().join("\n")
1396    )]
1397    RemoveBlocked {
1398        /// Each refused path with the reason it was refused.
1399        paths: Vec<crate::porcelain::paths::BlockedRemoval>,
1400    },
1401
1402    /// A directory pathspec was used without `recursive` ([`crate::porcelain::paths::remove_paths`]).
1403    #[error("not removing '{0}' recursively without -r")]
1404    NotRemovingRecursively(String),
1405
1406    /// A porcelain path operation requires a checked-out work tree.
1407    #[error("this operation must be run in a work tree")]
1408    RequiresWorkTree,
1409
1410    /// A commit was requested with an empty message body after normalization.
1411    #[error("empty commit message")]
1412    EmptyCommitMessage,
1413
1414    /// [`crate::porcelain::paths::remove_paths`] was called with no pathspecs.
1415    #[error("you must specify path(s) to remove")]
1416    RemovePathsRequired,
1417
1418    /// [`crate::porcelain::commit::commit_with_scope`] was called with an empty
1419    /// [`CommitScope::Paths`](crate::porcelain::commit::CommitScope::Paths) pathspec list.
1420    #[error("you must specify path(s)")]
1421    CommitPathsRequired,
1422
1423    /// [`crate::porcelain::paths::move_path`] / [`crate::porcelain::paths::move_paths`] source is not tracked.
1424    #[error("not under version control, source={src}")]
1425    MoveSourceNotTracked {
1426        /// Untracked or missing source path.
1427        src: String,
1428    },
1429
1430    /// Move refused because the destination already exists without `--force`.
1431    #[error("destination exists, source={src}, destination={destination}")]
1432    MoveDestinationExists {
1433        /// Source path in the move request.
1434        src: String,
1435        /// Existing destination path.
1436        destination: String,
1437    },
1438
1439    /// Move refused because the destination lies under the source directory.
1440    #[error("cannot move '{src}' to a subdirectory of itself, '{destination}'")]
1441    MoveIntoSubdirectory {
1442        /// Source directory path.
1443        src: String,
1444        /// Invalid destination under the source.
1445        destination: String,
1446    },
1447
1448    /// Move refused because source and destination are the same path.
1449    #[error("cannot move into itself, source={src}, destination={destination}")]
1450    MoveIntoSelf {
1451        /// Source path.
1452        src: String,
1453        /// Destination equal to the source.
1454        destination: String,
1455    },
1456
1457    /// Move refused because the source has no index entries.
1458    #[error("nothing to move, source={src}")]
1459    MoveNothingToMove {
1460        /// Source path with no index entries.
1461        src: String,
1462    },
1463
1464    /// Multi-source move would write more than one source to the same destination path.
1465    #[error("multiple sources for the same target, destination={destination}")]
1466    MoveDuplicateDestination {
1467        /// Destination path that would receive more than one source.
1468        destination: String,
1469    },
1470
1471    /// A worktree source path is missing or not a file, directory, or symlink.
1472    #[error("bad source, source={src}, destination={destination}")]
1473    MoveBadSource {
1474        /// Source path.
1475        src: String,
1476        /// Planned destination path.
1477        destination: String,
1478    },
1479
1480    /// Internal invariant: a planned rename references a missing index entry.
1481    #[error("internal error: missing index entry for '{path}'")]
1482    MoveMissingIndexEntry {
1483        /// Index path that should exist during a move.
1484        path: String,
1485    },
1486
1487    /// The remote could not be read or written: a missing repository, a failed connection, an
1488    /// HTTP error status, or no Git handshake.
1489    ///
1490    /// Use [`Error::is_remote_unreadable`] to test for it through wrapper errors.
1491    ///
1492    /// Not used for cancellation; see [`Error::is_cancelled`].
1493    #[error("{}", remote_unreadable_message(url.as_deref(), reason))]
1494    RemoteUnreadable {
1495        /// Remote URL or resolved local path, with credentials redacted. `None` when the
1496        /// failing layer only had a connection, not the URL it was opened from.
1497        url: Option<String>,
1498        /// What went wrong.
1499        reason: RemoteUnreadableReason,
1500    },
1501
1502    /// The remote was reached but aborted the conversation with its own error text: an
1503    /// `ERR <message>` pkt-line (for example upload-pack declining a ref it does not
1504    /// serve) or a packet on side-band channel 3.
1505    ///
1506    /// The text is the server's, not grit's: show it to the user, but do not parse it.
1507    #[error("remote error: {message}")]
1508    RemoteFatal {
1509        /// The server's message, without the `ERR ` prefix or trailing whitespace.
1510        message: String,
1511    },
1512
1513    /// The upload-pack serving a local path or `file://` URL in this process failed
1514    /// (for example it was asked for an object it does not have). Repository errors
1515    /// of the served side are returned as themselves instead.
1516    #[error("local upload-pack: {0}")]
1517    LocalUploadPack(Box<crate::serve::ServeError>),
1518
1519    /// The thread serving a local path or `file://` URL panicked.
1520    #[error("local upload-pack stopped unexpectedly")]
1521    LocalUploadPackPanicked,
1522
1523    /// A resumable bundle download or upload failed.
1524    #[error(transparent)]
1525    Resumable(#[from] Box<crate::resumable::ResumableError>),
1526
1527    /// Explicit fetch refspec resolution failed (missing or ambiguous source).
1528    #[error("{0}")]
1529    FetchRefspec(#[from] crate::fetch_request::FetchRefspecError),
1530
1531    /// Fetch refused to overwrite a local branch with a non-fast-forward update.
1532    #[error("non-fast-forward update for {local_ref}")]
1533    FetchNonFastForward {
1534        /// Local ref that would have been overwritten.
1535        local_ref: String,
1536    },
1537
1538    /// Fetch refused to update the branch checked out in this or a linked worktree.
1539    #[error("cannot fetch into checked-out branch '{branch}'")]
1540    FetchIntoCheckedOutBranch {
1541        /// Short branch name under `refs/heads/`.
1542        branch: String,
1543    },
1544
1545    /// Checkout or switch would overwrite local or untracked worktree content.
1546    #[error("{message}")]
1547    WouldOverwrite {
1548        /// Whether tracked local edits or untracked paths block the operation.
1549        reason: OverwriteReason,
1550        /// Repository-relative paths that would be overwritten (sorted, deduplicated).
1551        paths: Vec<String>,
1552        /// Human-readable summary (depends on [`OverwriteReason`]).
1553        message: String,
1554    },
1555
1556    /// Staged or unstaged changes block an operation that needs a clean worktree.
1557    #[error("you have uncommitted changes — commit them before {operation}")]
1558    DirtyWorktree {
1559        /// The operation that was refused.
1560        operation: WorktreeOperation,
1561        /// Repository-relative paths with staged or unstaged changes (sorted, deduplicated;
1562        /// empty when the caller only knew that something changed).
1563        paths: Vec<String>,
1564    },
1565
1566    /// Author or committer on [`crate::porcelain::commit::CommitRequest`] is invalid.
1567    #[error(transparent)]
1568    InvalidIdentity(#[from] crate::ident::CommitIdentityError),
1569
1570    /// A long-running library operation stopped at a cancellation checkpoint.
1571    ///
1572    /// Use [`Error::is_cancelled`] to test for it through wrapper errors
1573    /// ([`crate::remote::RemoteError::is_cancelled`], [`crate::remote::clone::CloneError::is_cancelled`], …).
1574    #[error("operation cancelled")]
1575    Cancelled,
1576
1577    /// A checkout failed or was cancelled before it finished, and putting the work tree back
1578    /// failed for some paths.
1579    ///
1580    /// Refs and the index are unchanged, as after any failed checkout; only `unrestored`
1581    /// differ from the index. Use [`Error::is_cancelled`] to see whether `source` was a
1582    /// cancellation.
1583    #[error("{source}; {} work-tree path(s) could not be restored", unrestored.len())]
1584    CheckoutRollbackIncomplete {
1585        /// Why the checkout stopped.
1586        source: Box<Error>,
1587        /// Repository-relative paths left different from the index (sorted, deduplicated).
1588        unrestored: Vec<String>,
1589    },
1590
1591    /// No usable author or committer identity is configured
1592    /// ([`crate::repo::Repository::identity`]).
1593    #[error(transparent)]
1594    Identity(#[from] crate::config::ident_resolve::IdentityError),
1595
1596    /// User-facing message that should be printed verbatim (no extra prefix).
1597    ///
1598    /// Used for revision errors that must match Git's `fatal:` lines exactly.
1599    #[error("{0}")]
1600    Message(String),
1601
1602    /// A merge was requested with fast-forward only, but the histories diverged.
1603    #[error("not possible to fast-forward; refusing to merge")]
1604    NotFastForward,
1605
1606    /// The index tree matches the parent commit tree and [`allow_empty`](crate::porcelain::commit::CommitRequest::allow_empty) is false.
1607    #[error("nothing to commit")]
1608    NothingToCommit,
1609
1610    /// No staged changes are available for a [`CommitScope::Staged`](crate::porcelain::commit::CommitScope::Staged)
1611    /// commit, but the work tree has unstaged or untracked paths.
1612    #[error("nothing staged to commit")]
1613    NothingStagedToCommit {
1614        /// Tracked paths that differ between the index and the work tree.
1615        unstaged: usize,
1616        /// Untracked paths under the work tree.
1617        untracked: usize,
1618    },
1619
1620    /// [`create_commit`](crate::porcelain::commit::create_commit) requires a branch checkout.
1621    #[error("HEAD is detached")]
1622    DetachedHead,
1623
1624    /// `HEAD` is on a branch with no commits yet, but the operation needs a commit
1625    /// ([`Repository::head_commit`](crate::repo::Repository::head_commit),
1626    /// [`replay_commit`](crate::porcelain::replay::replay_commit)).
1627    #[error("no commits yet on this branch")]
1628    UnbornHead,
1629
1630    /// `HEAD` could not be resolved to a branch, detached commit, or unborn branch.
1631    #[error("HEAD is in an unknown state")]
1632    InvalidHead,
1633
1634    /// [`replay_commit`](crate::porcelain::replay::replay_commit) does not replay merge commits
1635    /// until callers can select a mainline parent explicitly.
1636    #[error("merge commit {oid}")]
1637    MergeCommit {
1638        /// Object id of the merge commit that cannot be replayed yet.
1639        oid: crate::objects::ObjectId,
1640    },
1641
1642    /// Cherry-pick source is already checked out as `HEAD`.
1643    #[error("commit already at HEAD")]
1644    ReplaySourceAtHead,
1645
1646    /// Sequencer todo lists a command grit does not implement yet.
1647    #[error("unknown sequencer command '{command}'")]
1648    SequencerUnknownCommand {
1649        /// Verb read from `.git/sequencer/todo`.
1650        command: String,
1651    },
1652
1653    /// Legacy: abort no longer fails when `HEAD` moved;
1654    /// [`abort_replay_sequence`](crate::porcelain::replay_sequence::abort_replay_sequence) returns
1655    /// [`ReplayAbortOutcome::ClearedInPlace`](crate::porcelain::replay_sequence::ReplayAbortOutcome::ClearedInPlace) instead.
1656    #[error("cannot abort replay: HEAD moved since replay started")]
1657    ReplayAbortHeadMoved,
1658
1659    /// [`continue_replay_sequence`](crate::porcelain::replay_sequence::continue_replay_sequence) refuses while unmerged index entries remain.
1660    #[error("cannot continue replay: unmerged index entries remain")]
1661    ReplayContinueUnmerged,
1662
1663    /// No cherry-pick or revert sequence is in progress.
1664    #[error("no cherry-pick or revert in progress")]
1665    NoReplayInProgress,
1666
1667    /// [`crate::porcelain::merge::abort_branch_merge`] was called with no merge in progress.
1668    #[error("there is no merge in progress")]
1669    NoMergeInProgress,
1670
1671    /// The command would move `HEAD` while a paused operation still depends on it
1672    /// (see [`operation_blocking_switch`](crate::state::operation_blocking_switch)).
1673    #[error("{} in progress", .0.description())]
1674    OperationInProgress(crate::state::InProgressOperation),
1675
1676    /// A local branch is checked out as `HEAD` in a linked or main worktree.
1677    #[error("branch '{branch}' is already checked out in '{worktree_path}'")]
1678    BranchCheckedOutInWorktree {
1679        /// Short branch name (e.g. `main`).
1680        branch: String,
1681        /// Path to the worktree that has the branch checked out.
1682        worktree_path: String,
1683    },
1684
1685    /// `refs/heads/<branch>` does not exist.
1686    #[error("no branch named '{branch}'")]
1687    BranchNotFound {
1688        /// Short branch name (e.g. `topic`).
1689        branch: String,
1690    },
1691
1692    /// [`list_branches`](crate::branch_tracking::list_branches) with [`BranchDetail::Names`](crate::branch_tracking::BranchDetail::Names) cannot use date sort.
1693    #[error("branch list with Names detail cannot use date sort (date sort reads commit objects)")]
1694    BranchListNamesDateSort,
1695
1696    /// [`delete_branch`](crate::porcelain::branch::delete_branch) refuses to delete the branch
1697    /// `HEAD` points at.
1698    #[error("cannot delete '{branch}' — it is the current branch")]
1699    CannotDeleteCurrentBranch {
1700        /// Short branch name (e.g. `main`).
1701        branch: String,
1702    },
1703
1704    /// [`delete_branch`](crate::porcelain::branch::delete_branch) without `force` found commits
1705    /// on the branch that are not reachable from its upstream (or from `HEAD` when it has none).
1706    #[error("the branch '{branch}' is not fully merged")]
1707    BranchNotMerged {
1708        /// Short branch name (e.g. `topic`).
1709        branch: String,
1710    },
1711
1712    /// `refs/tags/<tag>` does not exist.
1713    #[error("no tag named '{tag}'")]
1714    TagNotFound {
1715        /// Short tag name (e.g. `v1.0`).
1716        tag: String,
1717    },
1718
1719    /// Tag create/delete validation failure ([`crate::porcelain::tag::TagError`]).
1720    #[error(transparent)]
1721    Tag(#[from] crate::porcelain::tag::TagError),
1722
1723    /// Linked worktree add/remove/move/lock/repair failure ([`crate::worktree::WorktreeError`]).
1724    #[error(transparent)]
1725    Worktree(#[from] crate::worktree::WorktreeError),
1726
1727    /// Stash commit object has fewer parents or an invalid shape for apply/show.
1728    #[error("corrupt stash commit: {0}")]
1729    CorruptStash(&'static str),
1730
1731    /// `stash@{{n}}` is out of range for the current stash reflog.
1732    #[error("stash entry stash@{{{n}}} not found")]
1733    StashNotFound {
1734        /// Stash reflog index (`stash@{{n}}`).
1735        n: usize,
1736    },
1737
1738    /// Ref does not exist and has no reflog entries.
1739    #[error("unknown ref '{refname}'")]
1740    ReflogUnknownRef {
1741        /// User-supplied or DWIM ref name.
1742        refname: String,
1743    },
1744
1745    /// A reflog selector is missing its reference or `@{n}` delimiters.
1746    #[error("expected ref@{{n}} form, got '{spec}'")]
1747    ReflogInvalidSelector {
1748        /// Selector after trimming surrounding whitespace.
1749        spec: String,
1750    },
1751
1752    /// A reflog selector index is not a nonnegative integer that fits in `usize`.
1753    #[error("invalid reflog index in '{spec}'")]
1754    ReflogInvalidIndex {
1755        /// Selector after trimming surrounding whitespace.
1756        spec: String,
1757        /// Integer parsing failure, including overflow.
1758        #[source]
1759        source: std::num::ParseIntError,
1760    },
1761
1762    /// `ref@{{n}}` is out of range for the current reflog.
1763    #[error("reflog entry {refname}@{{{index}}} not found")]
1764    ReflogIndexOutOfRange {
1765        /// Ref whose reflog was read.
1766        refname: String,
1767        /// Requested newest-first index.
1768        index: usize,
1769    },
1770
1771    /// Pushing or creating a stash ([`crate::repo_handles::RepositoryStash::push`]) requires at least one commit.
1772    #[error("cannot stash without a commit")]
1773    StashNoInitialCommit,
1774
1775    /// Stash create/apply/pop requires a working tree (not a bare repository).
1776    #[error("cannot stash in a bare repository")]
1777    StashBareRepository,
1778
1779    /// [`crate::blame::BlameFinalSource::Worktree`] was requested in a bare repository.
1780    #[error("cannot blame the working tree of a bare repository")]
1781    BlameWorktreeBareRepository,
1782
1783    /// [`crate::repo_handles::RepositoryStash::apply`] with `restore_index` could
1784    /// not merge the current index with the stashed index parent.
1785    #[error("conflicts in index while applying stash with --index")]
1786    StashIndexConflict,
1787
1788    /// Incompatible [`StashPushOptions`](crate::porcelain::stash::StashPushOptions) flags.
1789    #[error("{0}")]
1790    StashOptionConflict(String),
1791
1792    /// Partial [`crate::repo_handles::RepositoryStash::push`] hunk selection failed.
1793    #[error(transparent)]
1794    HunkSelection(#[from] crate::porcelain::hunk_select::HunkSelectError),
1795
1796    /// HEAD does not resolve while applying a stash entry.
1797    #[error("missing HEAD while applying stash")]
1798    StashMissingHead,
1799
1800    /// Applying a stash would overwrite locally modified worktree content.
1801    #[error(
1802        "your local changes to the following files would be overwritten by stash apply: {paths}"
1803    )]
1804    StashWouldOverwriteLocalChanges {
1805        /// Space-separated list of paths that would be overwritten.
1806        paths: String,
1807    },
1808
1809    /// The index could not be written after a successful stash apply.
1810    #[error("writing index after stash apply: {0}")]
1811    StashIndexWrite(String),
1812
1813    /// A symlink blob in a stash commit is not valid UTF-8.
1814    #[error("symlink target is not UTF-8")]
1815    StashSymlinkNotUtf8,
1816
1817    /// Applying a stash with an untracked-files parent would replace an existing worktree path.
1818    #[error("stash untracked restore blocked: '{path}' already exists on disk")]
1819    StashCouldNotRestoreUntracked {
1820        /// Worktree path that already exists and blocks untracked restore.
1821        path: String,
1822    },
1823
1824    /// [`CommitRequest::amend`](crate::porcelain::commit::CommitRequest::amend) was set on an unborn branch.
1825    #[error("cannot amend: no commits on this branch yet")]
1826    AmendUnborn,
1827
1828    /// [`RepositoryNotes::add`](crate::repo_handles::RepositoryNotes::add) was called without
1829    /// `force` when a note already exists on the object.
1830    #[error("note for object {object} already exists")]
1831    NoteAlreadyExists {
1832        /// Annotated object that already has a note.
1833        object: crate::objects::ObjectId,
1834    },
1835
1836    /// A configured or environment notes ref is not under `refs/notes/`.
1837    #[error("notes ref '{0}' must start with 'refs/notes/'")]
1838    InvalidNotesRef(String),
1839
1840    /// A notes ref resolves to an object that cannot hold notes (notes reads accept a commit or
1841    /// a tree; notes merges require a commit).
1842    #[error("notes ref '{notes_ref}' points to a {found}, not a notes commit")]
1843    NotesRefNotCommit {
1844        /// Notes ref (or revision) that was read.
1845        notes_ref: String,
1846        /// Kind of object the ref resolved to.
1847        found: crate::objects::ObjectKind,
1848    },
1849
1850    /// An object reached while reading a notes tree has the wrong kind (for example a note
1851    /// entry that is not a blob).
1852    #[error("notes object {oid} is a {found}, expected a {expected}")]
1853    NotesObjectKind {
1854        /// Object that was read.
1855        oid: crate::objects::ObjectId,
1856        /// Kind the notes layout requires at this position.
1857        expected: crate::objects::ObjectKind,
1858        /// Kind actually stored.
1859        found: crate::objects::ObjectKind,
1860    },
1861
1862    /// [`notes_merge_inner`](crate::notes::notes_merge_inner) was asked to merge two notes refs
1863    /// that both have no notes commit.
1864    #[error("cannot merge notes: neither '{local}' nor '{remote}' has any notes")]
1865    NotesMergeBothEmpty {
1866        /// Local notes ref (merge destination).
1867        local: String,
1868        /// Remote notes ref (merge source).
1869        remote: String,
1870    },
1871
1872    /// A manual notes merge was started while an earlier one still has conflict files in the
1873    /// notes merge worktree.
1874    #[error("unfinished notes merge: conflict files remain in {}", path.display())]
1875    NotesMergeInProgress {
1876        /// The notes merge worktree directory.
1877        path: std::path::PathBuf,
1878    },
1879
1880    /// Multi-pack-index load or write failure.
1881    #[error(transparent)]
1882    Midx(#[from] MidxError),
1883
1884    /// Offloading blobs to a large-object promisor was refused
1885    /// ([`crate::maintenance::offload_blobs`]).
1886    #[error(transparent)]
1887    Offload(#[from] crate::offload::OffloadError),
1888
1889    /// Revision parsing failed ([`crate::rev_parse_error::RevParseError`]).
1890    #[error(transparent)]
1891    RevParse(#[from] crate::rev_parse_error::RevParseError),
1892
1893    /// Revision walking / `rev-list` option parsing failed ([`crate::rev_list_error::RevListError`]).
1894    #[error(transparent)]
1895    RevList(#[from] crate::rev_list_error::RevListError),
1896
1897    /// A Git hook subprocess failed or could not be started.
1898    #[error(transparent)]
1899    Hook(#[from] crate::hooks::HookError),
1900
1901    /// History rewrite planning failed ([`crate::history::HistoryError`]).
1902    #[error(transparent)]
1903    History(#[from] crate::history::HistoryError),
1904
1905    /// The operation needs the files-backed `objects/` directory, but the repository's
1906    /// [`Odb`](crate::odb::Odb) uses a custom [`OdbBuilder::primary`](crate::odb::OdbBuilder::primary)
1907    /// store. Nothing was written; use a files-backed repository for this operation.
1908    #[error("{operation} requires the files-backed object store; it is not supported with a custom primary")]
1909    UnsupportedObjectStore {
1910        /// Operation that was refused (for example `"fetch"`, `"push"`, `"gc"`).
1911        operation: &'static str,
1912    },
1913
1914    /// The operation fetches objects back from a promisor remote, and the repository
1915    /// has none: no `remote.<name>.promisor = true` and no `extensions.partialClone`.
1916    /// Nothing was changed.
1917    #[error("{operation} needs a promisor remote to fetch objects back from")]
1918    NoPromisorRemote {
1919        /// Operation that was refused (for example `"drop-large-blobs"`).
1920        operation: &'static str,
1921    },
1922
1923    /// The operation would delete objects, and `extensions.preciousObjects` says this
1924    /// repository never loses any (it may be shared as an alternate). Nothing was
1925    /// changed.
1926    #[error("{operation} deletes objects, which extensions.preciousObjects forbids here")]
1927    PreciousObjectsRepository {
1928        /// Operation that was refused (for example `"drop-large-blobs"`).
1929        operation: &'static str,
1930    },
1931
1932    /// [`RepositoryBuilder::open`](crate::repo::RepositoryBuilder::open) was given an
1933    /// [`OdbBuilder`](crate::odb::OdbBuilder) rooted somewhere other than the repository's
1934    /// `objects/` directory.
1935    #[error(
1936        "object database {} is not the repository's objects directory {}",
1937        builder.display(),
1938        repository.display()
1939    )]
1940    OdbObjectsDirMismatch {
1941        /// The builder's objects directory, canonicalized when it exists.
1942        builder: std::path::PathBuf,
1943        /// The repository's canonical objects directory.
1944        repository: std::path::PathBuf,
1945    },
1946
1947    /// [`RepositoryBuilder::open`](crate::repo::RepositoryBuilder::open) was given an
1948    /// [`OdbBuilder`](crate::odb::OdbBuilder) whose custom primary or read source hashes
1949    /// objects with a different algorithm than the repository's `extensions.objectformat`
1950    /// (SHA-1 when unset).
1951    #[error(
1952        "object store hashes with {} but the repository's object format is {}",
1953        store.name(),
1954        repository.name()
1955    )]
1956    #[non_exhaustive]
1957    ObjectFormatMismatch {
1958        /// The repository's object format from its config.
1959        repository: crate::objects::HashAlgo,
1960        /// The first injected store's [`hash_algo`](crate::odb::store::ObjectStore::hash_algo)
1961        /// that differs from `repository`.
1962        store: crate::objects::HashAlgo,
1963    },
1964
1965    /// Parsing a [`crate::tree_sha256::TreeSha256`] from hex failed (expected 64 hex digits).
1966    #[error("invalid tree-sha256 hex")]
1967    TreeSha256InvalidHex,
1968
1969    /// A revision passed to [`crate::tree_sha256::tree_sha256`] did not peel to a commit or tree.
1970    #[error("tree-sha256: revision resolves to {found:?}, not a commit or tree")]
1971    TreeSha256InvalidRevision {
1972        /// Object kind after peeling annotated tags.
1973        found: crate::objects::ObjectKind,
1974    },
1975
1976    /// One or more submodules required for [`crate::tree_sha256::tree_sha256`] are missing.
1977    #[error("tree-sha256: missing submodules")]
1978    TreeSha256MissingSubmodules(Vec<crate::tree_sha256::MissingSubmodule>),
1979
1980    /// `--tree-sha256` was requested with signing explicitly disabled.
1981    #[error("tree-sha256 requires a signed tag")]
1982    TreeSha256RequiresSigning,
1983
1984    /// [`crate::tree_sha256::verify_tag`] was given an object that is not an annotated tag.
1985    #[error("verify-tag: object {oid} is a {found:?}, not an annotated tag")]
1986    TagVerifyNotTag {
1987        /// Object id passed to verification.
1988        oid: crate::objects::ObjectId,
1989        /// Kind stored in the ODB.
1990        found: crate::objects::ObjectKind,
1991    },
1992
1993    /// Annotated tag header block has no `object` line.
1994    #[error("verify-tag: tag has no object header")]
1995    TagVerifyMissingObjectHeader,
1996
1997    /// Annotated tag carries more than one `tree-sha256` header line.
1998    #[error("verify-tag: tag has more than one tree-sha256 header")]
1999    TagVerifyDuplicateTreeSha256Header,
2000
2001    /// Annotated tag tree-sha256 header parsing failed.
2002    #[error(transparent)]
2003    TreeSha256Claim(#[from] crate::tree_sha256::TreeSha256ClaimError),
2004
2005    /// `grit blame --ignore-rev` did not resolve to a commit.
2006    #[error("cannot find revision {rev} to ignore")]
2007    BlameIgnoreRevUnknown {
2008        /// Revision expression from `--ignore-rev`.
2009        rev: String,
2010    },
2011
2012    /// `--ignore-revs-file` could not be read.
2013    #[error("could not open object name list: {path}")]
2014    BlameIgnoreRevsFileOpen {
2015        /// Path argument (before expansion).
2016        path: String,
2017        /// Underlying open error.
2018        #[source]
2019        source: std::io::Error,
2020    },
2021
2022    /// A line in an ignore-revs file is not a full-length object id.
2023    #[error("invalid object name: {rev}")]
2024    BlameIgnoreRevsFileInvalidRev {
2025        /// Path argument for the file.
2026        path: String,
2027        /// Offending line text after comment/trim handling.
2028        rev: String,
2029    },
2030}
2031
2032impl Error {
2033    /// Stable snake_case name of this error's variant (for example `"not_a_repository"`),
2034    /// for scripts and serialized output that must branch on the failure kind.
2035    ///
2036    /// The name is the variant name in snake_case; it changes only when the variant is
2037    /// renamed. Match on the error value itself in Rust code.
2038    ///
2039    /// # Examples
2040    ///
2041    /// ```
2042    /// use grit_lib::error::Error;
2043    ///
2044    /// assert_eq!(Error::NothingToCommit.kind_name(), "nothing_to_commit");
2045    /// assert_eq!(Error::Cancelled.kind_name(), "cancelled");
2046    /// ```
2047    #[must_use]
2048    pub fn kind_name(&self) -> &'static str {
2049        match self {
2050            Self::NotARepository(..) => "not_a_repository",
2051            Self::ForbiddenBareRepository(..) => "forbidden_bare_repository",
2052            Self::DubiousOwnership(..) => "dubious_ownership",
2053            Self::UnsupportedRepositoryFormatVersion(..) => "unsupported_repository_format_version",
2054            Self::UnsupportedRepositoryExtension(..) => "unsupported_repository_extension",
2055            Self::InvalidRefStorageFormat { .. } => "invalid_ref_storage_format",
2056            Self::InitObjectFormatChange => "init_object_format_change",
2057            Self::InitBareWithSeparateGitDir => "init_bare_with_separate_git_dir",
2058            Self::InvalidObjectId(..) => "invalid_object_id",
2059            Self::HashCollision { .. } => "hash_collision",
2060            Self::ObjectNotFound(..) => "object_not_found",
2061            Self::RevisionNotFound(..) => "revision_not_found",
2062            Self::PromisedObjectMissing(..) => "promised_object_missing",
2063            Self::PromisorFetchFailed { .. } => "promisor_fetch_failed",
2064            Self::CorruptObject(..) => "corrupt_object",
2065            Self::PackIndexChecksumMismatch { .. } => "pack_index_checksum_mismatch",
2066            Self::UnresolvedDeltas { .. } => "unresolved_deltas",
2067            Self::PackDeltaBaseNotWritten { .. } => "pack_delta_base_not_written",
2068            Self::InvalidObject { .. } => "invalid_object",
2069            Self::InvalidTreeEntry(..) => "invalid_tree_entry",
2070            Self::InvalidSignature(..) => "invalid_signature",
2071            Self::UnexpectedObjectKind { .. } => "unexpected_object_kind",
2072            Self::CommitGraphParentMissing { .. } => "commit_graph_parent_missing",
2073            Self::DeltaChainTooDeep { .. } => "delta_chain_too_deep",
2074            Self::UnknownObjectType(..) => "unknown_object_type",
2075            Self::ObjectHeaderTooLong { .. } => "object_header_too_long",
2076            Self::Io(..) => "io",
2077            Self::LockHeld { .. } => "lock_held",
2078            Self::Zlib(..) => "zlib",
2079            Self::LooseHashMismatch { .. } => "loose_hash_mismatch",
2080            Self::LooseObjectWrite { .. } => "loose_object_write",
2081            Self::WorktreePathIo { .. } => "worktree_path_io",
2082            Self::GitDirPathIo { .. } => "git_dir_path_io",
2083            Self::ObjectStreamTruncated { .. } => "object_stream_truncated",
2084            Self::WorktreeFileChanged { .. } => "worktree_file_changed",
2085            Self::IndexError(..) => "index_error",
2086            Self::IndexUnmerged => "index_unmerged",
2087            Self::MaintenanceRunning { .. } => "maintenance_running",
2088            Self::UnmergedConflictMarkers { .. } => "unmerged_conflict_markers",
2089            Self::IndexPathPrefixConflict { .. } => "index_path_prefix_conflict",
2090            Self::IndexFsmonitorExtensionTruncated => "index_fsmonitor_extension_truncated",
2091            Self::IndexFsmonitorExtensionBadVersion(..) => "index_fsmonitor_extension_bad_version",
2092            Self::IndexFsmonitorExtensionEwahInvalid => "index_fsmonitor_extension_ewah_invalid",
2093            Self::CacheTreeCorrupt => "cache_tree_corrupt",
2094            Self::InvalidRef(..) => "invalid_ref",
2095            Self::PackedRefsUnexpectedLine { .. } => "packed_refs_unexpected_line",
2096            Self::PathError(..) => "path_error",
2097            Self::PathspecFromFile(..) => "pathspec_from_file",
2098            Self::InvalidPath(..) => "invalid_path",
2099            Self::InvalidPathComponent { .. } => "invalid_path_component",
2100            Self::Config(..) => "config",
2101            Self::Branch(..) => "branch",
2102            Self::RefLock(..) => "ref_lock",
2103            Self::RefStore(..) => "ref_store",
2104            Self::Reftable(..) => "reftable",
2105            Self::Filter(..) => "filter",
2106            Self::Apply(..) => "apply",
2107            Self::PathOutsideRepo(..) => "path_outside_repo",
2108            Self::Signing(..) => "signing",
2109            Self::Auth { .. } => "auth",
2110            Self::CredentialsUnavailable { .. } => "credentials_unavailable",
2111            Self::PushAtomicUnsupported => "push_atomic_unsupported",
2112            Self::PushOptionsUnsupported => "push_options_unsupported",
2113            Self::FetchUnshallowNotShallow => "fetch_unshallow_not_shallow",
2114            Self::FetchShallowUnsupported => "fetch_shallow_unsupported",
2115            Self::FetchDeepenRelativeUnsupported => "fetch_deepen_relative_unsupported",
2116            Self::BundleMissingPrerequisites => "bundle_missing_prerequisites",
2117            Self::ReceivedPackTempMissing { .. } => "received_pack_temp_missing",
2118            Self::InvalidAttrPathspec { .. } => "invalid_attr_pathspec",
2119            Self::PathspecNoMatch { .. } => "pathspec_no_match",
2120            Self::RestoreConflictWithStaged => "restore_conflict_with_staged",
2121            Self::RestoreConflictWithSource => "restore_conflict_with_source",
2122            Self::RestoreConflictMissingTheirs { .. } => "restore_conflict_missing_theirs",
2123            Self::RestoreConflictMissingOurs { .. } => "restore_conflict_missing_ours",
2124            Self::RestoreConflictNotUnmerged { .. } => "restore_conflict_not_unmerged",
2125            Self::RestorePathUnmerged { .. } => "restore_path_unmerged",
2126            Self::PathspecBeyondSymlink { .. } => "pathspec_beyond_symlink",
2127            Self::PartialCommitDuringMerge => "partial_commit_during_merge",
2128            Self::PartialCommitDuringReplay => "partial_commit_during_replay",
2129            Self::ReplayContinueDirtyIndex => "replay_continue_dirty_index",
2130            Self::ReplaySkipAlreadyCommitted => "replay_skip_already_committed",
2131            Self::EmbeddedRepoWithoutCommit { .. } => "embedded_repo_without_commit",
2132            Self::PathspecIgnored { .. } => "pathspec_ignored",
2133            Self::RemoveBlocked { .. } => "remove_blocked",
2134            Self::NotRemovingRecursively(..) => "not_removing_recursively",
2135            Self::RequiresWorkTree => "requires_work_tree",
2136            Self::EmptyCommitMessage => "empty_commit_message",
2137            Self::RemovePathsRequired => "remove_paths_required",
2138            Self::CommitPathsRequired => "commit_paths_required",
2139            Self::MoveSourceNotTracked { .. } => "move_source_not_tracked",
2140            Self::MoveDestinationExists { .. } => "move_destination_exists",
2141            Self::MoveIntoSubdirectory { .. } => "move_into_subdirectory",
2142            Self::MoveIntoSelf { .. } => "move_into_self",
2143            Self::MoveNothingToMove { .. } => "move_nothing_to_move",
2144            Self::MoveDuplicateDestination { .. } => "move_duplicate_destination",
2145            Self::MoveBadSource { .. } => "move_bad_source",
2146            Self::MoveMissingIndexEntry { .. } => "move_missing_index_entry",
2147            Self::RemoteUnreadable { .. } => "remote_unreadable",
2148            Self::RemoteFatal { .. } => "remote_fatal",
2149            Self::LocalUploadPack(..) => "local_upload_pack",
2150            Self::LocalUploadPackPanicked => "local_upload_pack_panicked",
2151            Self::Resumable(..) => "resumable",
2152            Self::FetchRefspec(..) => "fetch_refspec",
2153            Self::FetchNonFastForward { .. } => "fetch_non_fast_forward",
2154            Self::FetchIntoCheckedOutBranch { .. } => "fetch_into_checked_out_branch",
2155            Self::WouldOverwrite { .. } => "would_overwrite",
2156            Self::DirtyWorktree { .. } => "dirty_worktree",
2157            Self::InvalidIdentity(..) => "invalid_identity",
2158            Self::Cancelled => "cancelled",
2159            Self::Identity(..) => "identity",
2160            Self::Message(..) => "message",
2161            Self::NotFastForward => "not_fast_forward",
2162            Self::NothingToCommit => "nothing_to_commit",
2163            Self::UnrelatedHistories { .. } => "unrelated_histories",
2164            Self::ObjectFormatMismatch { .. } => "object_format_mismatch",
2165            Self::CheckoutRollbackIncomplete { .. } => "checkout_rollback_incomplete",
2166            Self::NothingStagedToCommit { .. } => "nothing_staged_to_commit",
2167            Self::DetachedHead => "detached_head",
2168            Self::UnbornHead => "unborn_head",
2169            Self::InvalidHead => "invalid_head",
2170            Self::MergeCommit { .. } => "merge_commit",
2171            Self::ReplaySourceAtHead => "replay_source_at_head",
2172            Self::SequencerUnknownCommand { .. } => "sequencer_unknown_command",
2173            Self::ReplayAbortHeadMoved => "replay_abort_head_moved",
2174            Self::ReplayContinueUnmerged => "replay_continue_unmerged",
2175            Self::NoReplayInProgress => "no_replay_in_progress",
2176            Self::NoMergeInProgress => "no_merge_in_progress",
2177            Self::OperationInProgress(..) => "operation_in_progress",
2178            Self::BranchCheckedOutInWorktree { .. } => "branch_checked_out_in_worktree",
2179            Self::BranchNotFound { .. } => "branch_not_found",
2180            Self::BranchListNamesDateSort => "branch_list_names_date_sort",
2181            Self::CannotDeleteCurrentBranch { .. } => "cannot_delete_current_branch",
2182            Self::BranchNotMerged { .. } => "branch_not_merged",
2183            Self::TagNotFound { .. } => "tag_not_found",
2184            Self::Tag(..) => "tag",
2185            Self::Worktree(..) => "worktree",
2186            Self::CorruptStash(..) => "corrupt_stash",
2187            Self::StashNotFound { .. } => "stash_not_found",
2188            Self::ReflogUnknownRef { .. } => "reflog_unknown_ref",
2189            Self::ReflogInvalidSelector { .. } => "reflog_invalid_selector",
2190            Self::ReflogInvalidIndex { .. } => "reflog_invalid_index",
2191            Self::ReflogIndexOutOfRange { .. } => "reflog_index_out_of_range",
2192            Self::StashNoInitialCommit => "stash_no_initial_commit",
2193            Self::StashBareRepository => "stash_bare_repository",
2194            Self::BlameWorktreeBareRepository => "blame_worktree_bare_repository",
2195            Self::StashIndexConflict => "stash_index_conflict",
2196            Self::StashOptionConflict(..) => "stash_option_conflict",
2197            Self::HunkSelection(..) => "hunk_selection",
2198            Self::StashMissingHead => "stash_missing_head",
2199            Self::StashWouldOverwriteLocalChanges { .. } => "stash_would_overwrite_local_changes",
2200            Self::StashIndexWrite(..) => "stash_index_write",
2201            Self::StashSymlinkNotUtf8 => "stash_symlink_not_utf8",
2202            Self::StashCouldNotRestoreUntracked { .. } => "stash_could_not_restore_untracked",
2203            Self::AmendUnborn => "amend_unborn",
2204            Self::NoteAlreadyExists { .. } => "note_already_exists",
2205            Self::InvalidNotesRef(..) => "invalid_notes_ref",
2206            Self::NotesRefNotCommit { .. } => "notes_ref_not_commit",
2207            Self::NotesObjectKind { .. } => "notes_object_kind",
2208            Self::NotesMergeBothEmpty { .. } => "notes_merge_both_empty",
2209            Self::NotesMergeInProgress { .. } => "notes_merge_in_progress",
2210            Self::Midx(..) => "midx",
2211            Self::Offload(..) => "offload",
2212            Self::RevParse(..) => "rev_parse",
2213            Self::RevList(..) => "rev_list",
2214            Self::Hook(..) => "hook",
2215            Self::History(..) => "history",
2216            Self::UnsupportedObjectStore { .. } => "unsupported_object_store",
2217            Self::NoPromisorRemote { .. } => "no_promisor_remote",
2218            Self::PreciousObjectsRepository { .. } => "precious_objects_repository",
2219            Self::OdbObjectsDirMismatch { .. } => "odb_objects_dir_mismatch",
2220            Self::TreeSha256InvalidHex => "tree_sha256_invalid_hex",
2221            Self::TreeSha256InvalidRevision { .. } => "tree_sha256_invalid_revision",
2222            Self::TreeSha256MissingSubmodules(..) => "tree_sha256_missing_submodules",
2223            Self::TreeSha256RequiresSigning => "tree_sha256_requires_signing",
2224            Self::TagVerifyNotTag { .. } => "tag_verify_not_tag",
2225            Self::TagVerifyMissingObjectHeader => "tag_verify_missing_object_header",
2226            Self::TagVerifyDuplicateTreeSha256Header => "tag_verify_duplicate_tree_sha256_header",
2227            Self::TreeSha256Claim(..) => "tree_sha256_claim",
2228            Self::BlameIgnoreRevUnknown { .. } => "blame_ignore_rev_unknown",
2229            Self::BlameIgnoreRevsFileOpen { .. } => "blame_ignore_revs_file_open",
2230            Self::BlameIgnoreRevsFileInvalidRev { .. } => "blame_ignore_revs_file_invalid_rev",
2231        }
2232    }
2233
2234    /// Build [`Error::DirtyWorktree`], sorting and deduplicating `paths`.
2235    #[must_use]
2236    pub fn dirty_worktree(operation: WorktreeOperation, mut paths: Vec<String>) -> Self {
2237        paths.sort();
2238        paths.dedup();
2239        Self::DirtyWorktree { operation, paths }
2240    }
2241
2242    /// Build [`Error::WouldOverwrite`] for switch and similar tree transitions.
2243    #[must_use]
2244    pub fn would_overwrite(reason: OverwriteReason, mut paths: Vec<String>) -> Self {
2245        paths.sort();
2246        paths.dedup();
2247        let message = match reason {
2248            OverwriteReason::LocalChanges => {
2249                "you have uncommitted changes — commit them before switching".into()
2250            }
2251            OverwriteReason::Untracked => {
2252                let first = paths.first().map(String::as_str).unwrap_or("<path>");
2253                format!("untracked file '{first}' would be overwritten — move or remove it first")
2254            }
2255        };
2256        Self::WouldOverwrite {
2257            reason,
2258            paths,
2259            message,
2260        }
2261    }
2262
2263    /// Build [`Error::RemoteUnreadable`] for `url`.
2264    #[must_use]
2265    pub fn remote_unreadable(url: impl Into<String>, reason: RemoteUnreadableReason) -> Self {
2266        Self::RemoteUnreadable {
2267            url: Some(url.into()),
2268            reason,
2269        }
2270    }
2271
2272    /// Whether this is [`Error::RemoteUnreadable`]: the remote could not be read or written
2273    /// at all (as opposed to a rejected update or an empty remote).
2274    #[must_use]
2275    pub fn is_remote_unreadable(&self) -> bool {
2276        matches!(self, Self::RemoteUnreadable { .. })
2277    }
2278
2279    /// Whether this is [`Error::WorktreePathIo`] caused by another process holding the file
2280    /// open (a Windows sharing or lock violation, `EBUSY`, or would-block), so retrying after
2281    /// closing that program can succeed.
2282    #[must_use]
2283    pub fn is_worktree_file_in_use(&self) -> bool {
2284        matches!(self, Self::WorktreePathIo { source, .. } if crate::worktree::io_error::is_file_in_use(source))
2285    }
2286
2287    /// Whether this is [`Error::GitDirPathIo`] caused by another process holding a git-dir file
2288    /// open (Windows sharing or access-denied during replace, `EBUSY`, …).
2289    #[must_use]
2290    pub fn is_git_dir_file_in_use(&self) -> bool {
2291        matches!(
2292            self,
2293            Self::GitDirPathIo {
2294                source,
2295                operation,
2296                ..
2297            } if crate::git_dir::io_error::is_git_dir_transient_contention(*operation, source)
2298        )
2299    }
2300
2301    /// Whether this is [`Error::Cancelled`] or a nested history failure that was cancelled.
2302    #[must_use]
2303    pub fn is_cancelled(&self) -> bool {
2304        match self {
2305            Self::Cancelled => true,
2306            Self::History(e) => e.is_cancelled(),
2307            Self::CheckoutRollbackIncomplete { source, .. } => source.is_cancelled(),
2308            _ => false,
2309        }
2310    }
2311
2312    /// Build [`Error::InvalidPathComponent`].
2313    #[must_use]
2314    pub fn invalid_path_component(
2315        path: impl Into<String>,
2316        reason: crate::path_validate::PathComponentError,
2317    ) -> Self {
2318        Self::InvalidPathComponent {
2319            path: path.into(),
2320            reason,
2321        }
2322    }
2323
2324    /// When set, the CLI should prefix stderr with `fatal: ` for Git-compatible commands.
2325    #[must_use]
2326    pub fn wants_fatal_prefix(&self) -> bool {
2327        matches!(
2328            self,
2329            Error::Config(_) | Error::RefLock(_) | Error::Filter(_) | Error::PathOutsideRepo(_)
2330        )
2331    }
2332
2333    /// Format for Git-style stderr (adds `fatal:` / `error:` when appropriate).
2334    #[must_use]
2335    pub fn git_stderr_message(&self) -> String {
2336        match self {
2337            Error::Apply(ApplyError::CorruptPatch { .. }) => {
2338                crate::diagnostics::error_line(&self.to_string())
2339            }
2340            Error::Message(s) => s.clone(),
2341            Error::WouldOverwrite { message, .. } => message.clone(),
2342            e if e.wants_fatal_prefix() => crate::diagnostics::fatal_line(&e.to_string()),
2343            other => other.to_string(),
2344        }
2345    }
2346}
2347
2348impl From<crate::refs::store::RefStoreError> for Error {
2349    fn from(err: crate::refs::store::RefStoreError) -> Self {
2350        match err {
2351            crate::refs::store::RefStoreError::GitDirPathIo {
2352                path,
2353                operation,
2354                os_error,
2355            } => Error::GitDirPathIo {
2356                path,
2357                operation,
2358                source: crate::refs::store::RefStoreError::git_dir_io_source(os_error),
2359            },
2360            crate::refs::store::RefStoreError::Hook(hook) => Error::Hook(hook.into_hook_error()),
2361            other => Error::RefStore(other),
2362        }
2363    }
2364}
2365
2366impl From<crate::lockfile::LockError> for Error {
2367    /// [`LockError::Held`](crate::lockfile::LockError::Held) becomes [`Error::LockHeld`] with the
2368    /// recorded [`LockHolder`](crate::lockfile::LockHolder), and I/O failures become [`Error::Io`].
2369    #[cold]
2370    fn from(err: crate::lockfile::LockError) -> Self {
2371        use crate::lockfile::{LockError, LockHolder};
2372        match err {
2373            LockError::Held { path } => Self::LockHeld {
2374                holder: LockHolder::read(&path),
2375                path,
2376            },
2377            LockError::Io { source, .. } => Self::Io(source),
2378        }
2379    }
2380}
2381
2382/// Convenience alias for `Result<T, Error>`.
2383pub type Result<T> = std::result::Result<T, Error>;
2384
2385fn promisor_fetch_failed_message(
2386    missing: &crate::objects::ObjectId,
2387    failures: &[(String, Error)],
2388) -> String {
2389    let causes = failures
2390        .iter()
2391        .map(|(remote, err)| format!("{remote}: {err}"))
2392        .collect::<Vec<_>>()
2393        .join("; ");
2394    format!("could not fetch promised object {missing} ({causes})")
2395}
2396
2397/// Serializes as `{"kind": <Error::kind_name>, "message": <Display text>}`.
2398#[cfg(feature = "serde-models")]
2399impl serde::Serialize for Error {
2400    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
2401    where
2402        S: serde::Serializer,
2403    {
2404        use serde::ser::SerializeStruct;
2405
2406        match self {
2407            Self::LockHeld { path, holder } => {
2408                let mut out = serializer.serialize_struct("Error", 3)?;
2409                out.serialize_field("kind", self.kind_name())?;
2410                out.serialize_field("message", &self.to_string())?;
2411                out.serialize_field("path", &crate::git_path::user_display_path(path.as_path()))?;
2412                if let Some(holder) = holder {
2413                    out.serialize_field(
2414                        "holder",
2415                        &LockHolderSerde {
2416                            pid: holder.pid,
2417                            running: holder.running,
2418                        },
2419                    )?;
2420                }
2421                out.end()
2422            }
2423            Self::WouldOverwrite { reason, paths, .. } => {
2424                let mut out = serializer.serialize_struct("Error", 4)?;
2425                out.serialize_field("kind", self.kind_name())?;
2426                out.serialize_field("message", &self.to_string())?;
2427                out.serialize_field("reason", &reason.to_string())?;
2428                out.serialize_field("paths", paths)?;
2429                out.end()
2430            }
2431            Self::StashCouldNotRestoreUntracked { path } => {
2432                let mut out = serializer.serialize_struct("Error", 3)?;
2433                out.serialize_field("kind", self.kind_name())?;
2434                out.serialize_field("message", &self.to_string())?;
2435                out.serialize_field("path", path)?;
2436                out.end()
2437            }
2438            Self::DirtyWorktree { operation, paths } => {
2439                let mut out = serializer.serialize_struct("Error", 4)?;
2440                out.serialize_field("kind", self.kind_name())?;
2441                out.serialize_field("message", &self.to_string())?;
2442                out.serialize_field("operation", &operation.to_string())?;
2443                out.serialize_field("paths", paths)?;
2444                out.end()
2445            }
2446            Self::UnmergedConflictMarkers { paths } => {
2447                let mut out = serializer.serialize_struct("Error", 3)?;
2448                out.serialize_field("kind", self.kind_name())?;
2449                out.serialize_field("message", &self.to_string())?;
2450                out.serialize_field("paths", paths)?;
2451                out.end()
2452            }
2453            _ => {
2454                let mut out = serializer.serialize_struct("Error", 2)?;
2455                out.serialize_field("kind", self.kind_name())?;
2456                out.serialize_field("message", &self.to_string())?;
2457                out.end()
2458            }
2459        }
2460    }
2461}
2462
2463#[cfg(feature = "serde-models")]
2464#[derive(serde::Serialize)]
2465struct LockHolderSerde {
2466    pid: u64,
2467    running: bool,
2468}