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