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