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}