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