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