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}
98
99impl RemoteUnreadableReason {
100 /// Stable snake-case name for machine-readable output (`repository_not_found`,
101 /// `transport`, `http_status`, `not_git_server`, `no_handshake`).
102 #[must_use]
103 pub fn slug(&self) -> &'static str {
104 match self {
105 Self::RepositoryNotFound => "repository_not_found",
106 Self::Transport { .. } => "transport",
107 Self::HttpStatus { .. } => "http_status",
108 Self::NotGitServer { .. } => "not_git_server",
109 Self::NoHandshake => "no_handshake",
110 }
111 }
112}
113
114fn remote_unreadable_message(url: Option<&str>, reason: &RemoteUnreadableReason) -> String {
115 let target = url.map_or_else(|| "the remote".to_owned(), |u| format!("'{u}'"));
116 match reason {
117 RemoteUnreadableReason::RepositoryNotFound => {
118 format!("could not find repository at {target}")
119 }
120 RemoteUnreadableReason::Transport { detail } => {
121 format!("could not reach {target}: {detail}")
122 }
123 RemoteUnreadableReason::HttpStatus { status } => match url {
124 Some(u) => format!("HTTP {status} from {u}"),
125 None => format!("HTTP {status} from the remote"),
126 },
127 RemoteUnreadableReason::NotGitServer {
128 status,
129 content_type,
130 } => {
131 let got = match content_type
132 .as_deref()
133 .map(|ct| ct.split(';').next().unwrap_or(ct).trim())
134 .filter(|ct| !ct.is_empty())
135 {
136 Some(ct) => format!("{status} {ct}"),
137 None => status.to_string(),
138 };
139 format!("{target} did not answer like a Git server (got {got})")
140 }
141 RemoteUnreadableReason::NoHandshake => {
142 format!("could not read from {target}: no Git protocol response")
143 }
144 }
145}
146
147/// Why an HTTP remote refused access ([`Error::Auth`]).
148#[derive(Debug, Clone, PartialEq, Eq, Error)]
149#[non_exhaustive]
150pub enum AuthFailure {
151 /// The server asked for credentials (`401`) and no credential provider is wired.
152 #[error("no credential provider is configured")]
153 NoCredentialProvider,
154 /// The credential provider could not supply credentials (no helper has them, and
155 /// prompting is not possible).
156 #[error("{detail}")]
157 CredentialsUnavailable {
158 /// The provider's explanation (for example that no helper has credentials and
159 /// prompting is disabled).
160 detail: String,
161 },
162 /// The provider answered without a usable username and password.
163 #[error("the credential helper returned no usable username and password")]
164 UnusableCredentials,
165 /// The URL could not be split into the credential fields helpers expect.
166 #[error("the URL cannot be described to a credential helper")]
167 UrlNotDescribable,
168 /// The server only offers authentication schemes grit does not speak (no `Basic`).
169 #[error("unsupported authentication scheme: {}", schemes.join(", "))]
170 UnsupportedScheme {
171 /// The `WWW-Authenticate` challenges the server sent.
172 schemes: Vec<String>,
173 },
174 /// Credentials were sent and the server rejected them (`401` again).
175 #[error("the server rejected the credentials")]
176 Rejected,
177}
178
179impl AuthFailure {
180 /// Whether credentials were sent and refused, as opposed to never being available.
181 #[must_use]
182 pub fn is_rejected(&self) -> bool {
183 matches!(self, Self::Rejected)
184 }
185}
186
187/// The command a dirty worktree blocked ([`Error::DirtyWorktree`]).
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
189#[non_exhaustive]
190pub enum WorktreeOperation {
191 /// Switching branches or checking out a commit.
192 Switch,
193 /// Picking (cherry-picking) or reverting commits.
194 Pick,
195 /// Merging.
196 Merge,
197 /// Rebasing with `--exec`.
198 RebaseExec,
199 /// Starting a rebase that stops on conflicts (the stop rewrites the index and worktree).
200 Rebase,
201}
202
203impl std::fmt::Display for WorktreeOperation {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 f.write_str(match self {
206 Self::Switch => "switching",
207 Self::Pick => "picking",
208 Self::Merge => "merging",
209 Self::RebaseExec => "rebasing with --exec",
210 Self::Rebase => "rebasing",
211 })
212 }
213}
214
215/// Configuration parse and validation failures.
216#[derive(Debug, Error, Clone, PartialEq, Eq)]
217#[non_exhaustive]
218pub enum ConfigError {
219 /// A line in a config file could not be parsed.
220 #[error("bad config line {line} in {location}")]
221 BadConfigLine {
222 /// Config source label or path where the error was detected.
223 location: String,
224 /// One-based line number within that source.
225 line: usize,
226 },
227
228 /// Same as [`Self::BadConfigLine`] with an explicit file path (Git file diagnostics).
229 #[error("bad config line {line} in file '{file}'")]
230 BadConfigLineInFile {
231 /// Path to the config file involved in the error.
232 file: String,
233 /// One-based line number within that file.
234 line: usize,
235 },
236
237 /// A config key requires a value but none was given.
238 #[error("missing value for '{key}'")]
239 MissingValue {
240 /// Config key name that failed validation.
241 key: String,
242 },
243
244 /// `fetch.negotiationalgorithm` and similar keys with missing values.
245 #[error("bad config variable '{key}' in file '{file}' at line {line}")]
246 BadConfigVariable {
247 /// Config key name that failed validation.
248 key: String,
249 /// Path to the config file involved in the error.
250 file: String,
251 /// One-based line number within the config source.
252 line: usize,
253 },
254
255 /// `unset` or a scoped read/write was requested for a key that is not set in that file.
256 #[error("key '{key}' is not set")]
257 KeyNotSet {
258 /// Config key that was not present.
259 key: String,
260 },
261
262 /// Integer config value could not be parsed or is out of range.
263 #[error("bad numeric config value '{value}' for '{key}': {reason}")]
264 BadNumericValue {
265 /// Config key name that failed validation.
266 key: String,
267 /// Raw config value that could not be parsed.
268 value: String,
269 /// Why the value or operation was rejected.
270 reason: BadNumericSource,
271 },
272
273 /// Same as [`Self::BadNumericValue`] with file context.
274 #[error("bad numeric config value '{value}' for '{key}' in file {file}: {reason}")]
275 BadNumericValueInFile {
276 /// Config key name that failed validation.
277 key: String,
278 /// Raw config value that could not be parsed.
279 value: String,
280 /// Path to the config file involved in the error.
281 file: String,
282 /// Why the value or operation was rejected.
283 reason: BadNumericSource,
284 },
285
286 /// `diff.context` (or similar) failed variable validation.
287 #[error("bad config variable '{key}' in file '{file}' at line {line}")]
288 BadConfigVariableAtLine {
289 /// Config key name that failed validation.
290 key: String,
291 /// Path to the config file involved in the error.
292 file: String,
293 /// One-based line number within the config source.
294 line: usize,
295 },
296
297 /// Command-line config could not supply a parseable value for a key.
298 #[error("unable to parse '{key}' from command-line config")]
299 UnableToParseFromCommandLine {
300 /// Config key name that failed validation.
301 key: String,
302 },
303
304 /// `includeIf.hasconfig:remote.*.url` cannot pull remote URLs from included files.
305 #[error(
306 "remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url"
307 )]
308 RemoteUrlInHasconfigInclude,
309
310 /// `[include]` / `[includeIf]` nesting exceeded the Git-compatible depth limit.
311 #[error("exceeded maximum include depth (depth {depth} exceeds limit {limit})")]
312 IncludeDepthExceeded {
313 /// Current include nesting depth when the limit was hit.
314 depth: usize,
315 /// Maximum allowed include nesting depth.
316 limit: usize,
317 },
318
319 /// Generic config error with free-form detail (legacy call sites).
320 #[error("{0}")]
321 Other(String),
322
323 /// The config file could not be written because a `.lock` file is already present.
324 #[error("could not lock config file {path}: File exists")]
325 ConfigFileLocked {
326 /// Path to the config file that could not be locked.
327 path: String,
328 },
329
330 /// A single-value operation was requested but the key has multiple values.
331 #[error("cannot overwrite multiple values with a single value for '{key}'")]
332 MultipleValues {
333 /// Config key name that failed validation.
334 key: String,
335 },
336
337 /// Per-worktree config was requested but `extensions.worktreeConfig` is off.
338 #[error(
339 "worktree config is only allowed when extensions.worktreeConfig is enabled in the common config"
340 )]
341 WorktreeConfigNotEnabled,
342
343 /// `remove-section` or `rename-section` did not find the named section.
344 #[error("section '{section}' not found")]
345 SectionNotFound {
346 /// Section header name (e.g. `remote.origin`).
347 section: String,
348 },
349
350 /// Inline `--comment` text must not span lines.
351 #[error("no multi-line comment allowed")]
352 MultilineCommentNotAllowed,
353
354 /// `status.showUntrackedFiles` must be `no`, `normal`, `all`, or a Git boolean synonym.
355 #[error(
356 "invalid value '{value}' for 'status.showUntrackedFiles' (expected no, normal, all, boolean words, or integer 0 vs nonzero)"
357 )]
358 InvalidUntrackedFilesMode {
359 /// Raw config value that could not be parsed.
360 value: String,
361 },
362}
363
364impl From<String> for ConfigError {
365 fn from(value: String) -> Self {
366 Self::Other(value)
367 }
368}
369
370impl From<&str> for ConfigError {
371 fn from(value: &str) -> Self {
372 Self::Other(value.to_owned())
373 }
374}
375
376impl ConfigError {
377 /// Git stderr body for this error (without a `fatal:` prefix).
378 #[must_use]
379 pub fn message_body(&self) -> String {
380 self.to_string()
381 }
382}
383
384/// Reference lock failures while creating or updating a ref.
385#[derive(Debug, Error, Clone, PartialEq, Eq)]
386#[non_exhaustive]
387pub enum RefLockError {
388 /// A non-empty directory occupies the path where the ref file must be created.
389 #[error(
390 "cannot lock ref '{refname}': there is a non-empty directory '{path}' blocking reference '{refname}'"
391 )]
392 DirectoryInTheWay {
393 /// Full ref name (for example `refs/heads/main`).
394 refname: String,
395 /// Directory path blocking creation of the ref file.
396 path: String,
397 },
398
399 /// Another process holds the `packed-refs.lock` file (or it already exists).
400 #[error("Unable to create '{lock_path}': File exists.")]
401 PackedRefsLockHeld {
402 /// Path to the lock file that could not be created.
403 lock_path: String,
404 },
405}
406
407/// Clean/smudge filter and EOL conversion failures.
408#[derive(Debug, Error, Clone, PartialEq, Eq)]
409#[non_exhaustive]
410pub enum FilterError {
411 /// External clean or smudge filter command failed.
412 #[error("{path}: {phase} filter '{driver}' failed")]
413 ExternalFilterFailed {
414 /// Filter driver name from `.gitattributes`.
415 driver: String,
416 /// Repository-relative or filesystem path related to the failure.
417 path: String,
418 /// Whether the clean or smudge filter hook failed.
419 phase: FilterPhase,
420 },
421
422 /// A required clean or smudge filter could not be started (for example, no shell to run
423 /// it), so the content was neither filtered nor stored unfiltered.
424 #[error("{path}: cannot start {phase} filter '{driver}' ({program}): {detail}")]
425 ExternalFilterSpawnFailed {
426 /// Filter driver name from `.gitattributes`.
427 driver: String,
428 /// Repository-relative path being filtered.
429 path: String,
430 /// Whether the clean or smudge filter was being started.
431 phase: FilterPhase,
432 /// Program grit tried to spawn (`sh` for shell-command filters).
433 program: String,
434 /// OS error kind from the failed spawn.
435 kind: std::io::ErrorKind,
436 /// OS error message from the failed spawn.
437 detail: String,
438 },
439
440 /// Filter output did not round-trip (clean/smudge mismatch).
441 #[error("{path}: {detail}")]
442 NotFilteredProperly {
443 /// Repository-relative or filesystem path related to the failure.
444 path: String,
445 /// Why clean/smudge output did not round-trip.
446 detail: String,
447 },
448
449 /// Invalid `working-tree-encoding` attribute value.
450 #[error("true/false are no valid working-tree-encodings")]
451 InvalidWorkingTreeEncoding,
452
453 /// Line-ending conversion would modify the worktree against user settings.
454 #[error("{detail}")]
455 LineEndingWouldChange {
456 /// Additional human-readable detail for the failure.
457 detail: String,
458 },
459
460 /// Shell filter subprocess exited non-zero.
461 #[error("filter command exited with status {status}")]
462 Failed {
463 /// Subprocess exit status from the filter command.
464 status: i32,
465 },
466}
467
468/// Which filter hook failed.
469#[derive(Debug, Clone, Copy, PartialEq, Eq)]
470pub enum FilterPhase {
471 /// Index → worktree (smudge).
472 Smudge,
473 /// Worktree → index (clean).
474 Clean,
475}
476
477impl std::fmt::Display for FilterPhase {
478 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
479 match self {
480 Self::Smudge => f.write_str("smudge"),
481 Self::Clean => f.write_str("clean"),
482 }
483 }
484}
485
486/// Sparse-checkout cone pattern parse warnings (non-fatal).
487#[derive(Debug, Clone, PartialEq, Eq)]
488pub enum SparseCheckoutWarning {
489 /// A cone pattern line was not recognized.
490 UnrecognizedPattern {
491 /// Raw pattern text from the sparse-checkout file.
492 pattern: String,
493 },
494 /// A negative cone pattern line was not recognized.
495 UnrecognizedNegativePattern {
496 /// Raw negative pattern text from the sparse-checkout file.
497 pattern: String,
498 },
499 /// The same cone pattern appears more than once in the sparse-checkout file.
500 RepeatedPattern {
501 /// Duplicated pattern text.
502 pattern: String,
503 },
504 /// Cone pattern matching was disabled due to invalid sparse-checkout contents.
505 DisablingConePatternMatching,
506}
507
508impl SparseCheckoutWarning {
509 /// Human-readable body without a `warning:` prefix.
510 #[must_use]
511 pub fn body(&self) -> String {
512 match self {
513 Self::UnrecognizedPattern { pattern } => {
514 format!("unrecognized pattern: '{pattern}'")
515 }
516 Self::UnrecognizedNegativePattern { pattern } => {
517 format!("unrecognized negative pattern: '{pattern}'")
518 }
519 Self::RepeatedPattern { pattern } => format!(
520 "your sparse-checkout file may have issues: pattern '{pattern}' is repeated"
521 ),
522 Self::DisablingConePatternMatching => "disabling cone pattern matching".to_owned(),
523 }
524 }
525
526 /// Git-style `warning: …` line.
527 #[must_use]
528 pub fn format_line(&self) -> String {
529 crate::diagnostics::warning_line(&self.body())
530 }
531}
532
533/// Unified diff / patch application errors.
534#[derive(Debug, Error, Clone, PartialEq, Eq)]
535#[non_exhaustive]
536pub enum ApplyError {
537 /// Patch bytes are malformed at a known input location.
538 #[error("corrupt patch at {input}:{line}")]
539 CorruptPatch {
540 /// Patch input label (file name or `stdin`).
541 input: String,
542 /// One-based line number within the patch input.
543 line: usize,
544 },
545
546 /// Preimage context could not be located in the blob at any search offset.
547 #[error(
548 "patch failed: context mismatch at hunk {hunk_index} (expected near line {expected_line})"
549 )]
550 ContextMismatch {
551 /// Zero-based index of the hunk in the input slice.
552 hunk_index: usize,
553 /// One-based line number from the hunk header (`old_start`).
554 expected_line: usize,
555 },
556
557 /// A later hunk would modify lines already changed by an earlier hunk.
558 #[error("patch failed: overlapping hunks at hunk {hunk_index}")]
559 HunksOverlap {
560 /// Zero-based index of the hunk that overlaps prior applied changes.
561 hunk_index: usize,
562 },
563
564 /// The patch removes more lines than exist in the blob (without a valid EOF match).
565 #[error("patch failed: unexpected end of file at hunk {hunk_index}")]
566 UnexpectedEof {
567 /// Zero-based index of the offending hunk.
568 hunk_index: usize,
569 },
570
571 /// Other apply parse failures.
572 #[error("{0}")]
573 Other(String),
574
575 /// A text hunk could not be matched or applied.
576 #[error("patch failed: {path}:{line}")]
577 PatchFailed {
578 /// File path from the patch.
579 path: String,
580 /// One-based line number in the patch input.
581 line: usize,
582 },
583
584 /// Index or blob apply failed for a path.
585 #[error("could not apply patch to {path}: {message}")]
586 Failed {
587 /// Repository-relative path.
588 path: String,
589 /// Failure detail.
590 message: String,
591 },
592
593 /// `--cached` apply requires a stage-0 index entry at the preimage path.
594 #[error("'{path}' is not in the index")]
595 NotInIndex {
596 /// Missing path.
597 path: String,
598 },
599
600 /// A binary diff marker without `GIT binary patch` payload cannot be applied.
601 #[error("binary patch data missing for '{path}'")]
602 BinaryDataMissing {
603 /// Path from the patch.
604 path: String,
605 },
606}
607
608/// Path lookup failures while formatting a diff.
609#[derive(Debug, Error, Clone, PartialEq, Eq)]
610#[non_exhaustive]
611pub enum DiffPathError {
612 /// The requested path is not part of the diff being formatted.
613 #[error("No such path '{path}' in the diff")]
614 NoSuchPathInDiff {
615 /// Path that was requested from the diff.
616 path: String,
617 },
618}
619
620/// `--pathspec-from-file` / `--pathspec-file-nul` parse failure.
621#[derive(Debug, Error, Clone, PartialEq, Eq)]
622#[non_exhaustive]
623pub enum PathspecFromFileParseError {
624 /// A NUL-separated entry is not valid UTF-8.
625 #[error("invalid UTF-8 in pathspec-from-file (NUL mode)")]
626 InvalidUtf8InNulRecord,
627 /// A NUL-terminated entry used C-style quoting (forbidden in NUL mode).
628 #[error("pathspec-from-file: line is not NUL terminated: {line}")]
629 QuotedLineInNulMode {
630 /// Offending entry text.
631 line: String,
632 },
633 /// A line-mode entry had invalid double-quote wrapping.
634 #[error("invalid C-style quoting: {line}")]
635 InvalidCStyleQuoting {
636 /// Raw line from the file.
637 line: String,
638 },
639 /// Backslash escape at end of a quoted line.
640 #[error("invalid escape at end of string")]
641 InvalidEscapeAtEnd,
642 /// Octal escape shorter than three digits.
643 #[error("truncated octal escape")]
644 TruncatedOctalEscape,
645 /// Octal escape bytes are not valid UTF-8.
646 #[error("invalid octal bytes")]
647 InvalidOctalBytes,
648 /// Octal digits do not form a valid byte value.
649 #[error("invalid octal escape value")]
650 InvalidOctalValue,
651 /// Unknown escape after backslash in a quoted line.
652 #[error("invalid escape sequence \\{escape}")]
653 InvalidEscapeSequence {
654 /// Character after the backslash.
655 escape: char,
656 },
657 /// Quoted line decoded to invalid UTF-8.
658 #[error("invalid UTF-8 in quoted pathspec")]
659 InvalidUtf8InQuotedLine,
660}
661
662/// Failure while resolving argv and `--pathspec-from-file` pathspecs against a work tree.
663#[derive(Debug, Error, Clone, PartialEq, Eq)]
664#[non_exhaustive]
665pub enum PathspecResolveError {
666 /// An argv pathspec was the empty string.
667 #[error("invalid path ''")]
668 EmptyPathspec,
669 /// Resolved path lies outside the repository work tree.
670 #[error("{elt}: '{path}' is outside repository at '{work_tree}'")]
671 OutsideRepository {
672 /// User-facing pathspec token (argv element).
673 elt: String,
674 /// Resolved absolute path outside the work tree.
675 path: String,
676 /// Canonical work tree root.
677 work_tree: String,
678 },
679 /// `--pathspec-from-file` bytes could not be parsed.
680 #[error(transparent)]
681 FromFile(#[from] PathspecFromFileParseError),
682 /// Command requires a working tree but the repository is bare.
683 #[error("this operation needs a working tree")]
684 NeedsWorkTree,
685}
686
687/// Why a `:(attr:...)` pathspec clause was rejected.
688#[derive(Debug, Error, Clone, PartialEq, Eq)]
689#[non_exhaustive]
690pub enum AttrPathspecError {
691 /// The clause names no attribute requirement (`:(attr:)`).
692 #[error("attr requirement list is empty")]
693 Empty,
694 /// More than one `attr:` clause appears in one pathspec.
695 #[error("a pathspec may contain at most one attr clause")]
696 MultipleAttrClauses,
697 /// A requirement names an attribute that is not valid (`-a=b`, `bad name`, `bad/name`).
698 #[error("'{0}' is not a usable attribute name")]
699 InvalidName(String),
700 /// A `\` escape ends the value or precedes whitespace.
701 #[error("attr value ends with a dangling escape")]
702 DanglingEscape,
703 /// A `\` escape precedes something other than `,`.
704 #[error("attr values may only escape ','")]
705 UnsupportedEscape,
706}
707
708/// Path outside the repository during pathspec or transport resolution.
709#[derive(Debug, Error, Clone, PartialEq, Eq)]
710#[non_exhaustive]
711pub enum PathOutsideRepoError {
712 /// A resolved path escapes the repository work tree or git directory.
713 #[error("{path}: '{subpath}' is outside repository at '{repo_root}'")]
714 OutsideRepository {
715 /// Repository-relative or filesystem path related to the failure.
716 path: String,
717 /// Path component that lies outside the repository root.
718 subpath: String,
719 /// Canonical repository root used for the outside-path check.
720 repo_root: String,
721 },
722}
723
724fn maintenance_running_message(holder: &crate::maintenance::GcLockHolder) -> String {
725 match (holder.pid, holder.host.as_deref()) {
726 (Some(pid), Some(host)) => {
727 format!("repository maintenance is already running (process {pid} on {host})")
728 }
729 (Some(pid), None) => format!("repository maintenance is already running (process {pid})"),
730 _ => "repository maintenance is already running".to_owned(),
731 }
732}
733
734fn restore_unmerged_message(paths: &[String]) -> String {
735 match paths {
736 [path] => format!("path '{path}' is unmerged"),
737 _ => format!(
738 "paths are unmerged: {}",
739 paths
740 .iter()
741 .map(|p| format!("'{p}'"))
742 .collect::<Vec<_>>()
743 .join(", ")
744 ),
745 }
746}
747
748/// The top-level error type for all grit-lib operations.
749///
750/// # Examples
751///
752/// ```
753/// use grit_lib::error::Error;
754///
755/// let err = Error::Message("repository not found".into());
756/// assert!(err.to_string().contains("repository not found"));
757/// ```
758#[derive(Debug, Error)]
759#[non_exhaustive]
760pub enum Error {
761 /// A repository could not be found or is structurally invalid.
762 #[error("not a git repository (or any of the parent directories): {0}")]
763 NotARepository(String),
764
765 /// A bare repository was found but access is forbidden by safe.bareRepository.
766 #[error("cannot use bare repository '{0}' (safe.bareRepository is 'explicit')")]
767 ForbiddenBareRepository(String),
768
769 /// The repository is owned by a different user (safe.directory).
770 #[error("detected dubious ownership in repository at '{0}'")]
771 DubiousOwnership(String),
772
773 /// Repository format version is not supported by this implementation.
774 #[error("unsupported repository format version '{0}'")]
775 UnsupportedRepositoryFormatVersion(u32),
776
777 /// Repository declares an unsupported extension.
778 #[error("unknown repository extension '{0}'")]
779 UnsupportedRepositoryExtension(String),
780
781 /// `extensions.refStorage` names an unsupported backend.
782 #[error("invalid value for 'extensions.refstorage': '{value}'")]
783 InvalidRefStorageFormat {
784 /// Raw `extensions.refStorage` value from the repository config.
785 value: String,
786 },
787
788 /// [`crate::repo::Repository::init_with_outcome`] refused to change object format on reinit.
789 #[error("cannot change object format in an existing repository")]
790 InitObjectFormatChange,
791
792 /// [`crate::repo::Repository::init_with_outcome`] with incompatible `--bare` and separate git dir.
793 #[error("cannot combine bare repository with separate git directory")]
794 InitBareWithSeparateGitDir,
795
796 /// A supplied object ID string was not valid hex or the wrong length.
797 #[error("invalid object id '{0}'")]
798 InvalidObjectId(String),
799
800 /// SHA-1 object hashing detected a chosen-prefix collision (sha1dc).
801 #[error("SHA-1 hash collision detected for object {oid}")]
802 HashCollision {
803 /// Plain SHA-1 digest for the colliding input (not safe to use as an object id).
804 oid: crate::objects::ObjectId,
805 },
806
807 /// The requested object does not exist in the object store.
808 #[error("object not found: {0}")]
809 ObjectNotFound(String),
810
811 /// A revision expression (branch, tag, object id, `x~n`, `ORIG_HEAD`, …) names nothing in
812 /// the repository. Holds the expression as the caller wrote it.
813 #[error("unknown revision '{0}'")]
814 RevisionNotFound(String),
815
816 /// A partial-clone promisor object is not present locally and no fetcher ran.
817 #[error("promised object not available locally: {0}")]
818 PromisedObjectMissing(crate::objects::ObjectId),
819
820 /// An object's stored data is corrupt or malformed.
821 #[error("corrupt object: {0}")]
822 CorruptObject(String),
823
824 /// A pack index (`.idx`) file's trailing checksum does not match its contents.
825 ///
826 /// Only reported by reads that verify the index trailer, such as
827 /// [`crate::pack::read_pack_index`]; the checksum of the `.pack` the index
828 /// describes is checked separately.
829 #[error("corrupt object: index checksum mismatch for {}: {mismatch}", .path.display())]
830 PackIndexChecksumMismatch {
831 /// The `.idx` file that failed verification.
832 path: std::path::PathBuf,
833 /// Expected and stored trailer digests.
834 mismatch: crate::hash::TrailerMismatch,
835 },
836
837 /// Indexing or unpacking a pack left deltas whose bases are neither in the pack nor
838 /// in the object database: a thin pack without its bases, or a ref-delta cycle.
839 #[error("corrupt object: {count} delta(s) could not be resolved")]
840 UnresolvedDeltas {
841 /// Number of deltas left unresolved.
842 count: u64,
843 },
844
845 /// [`crate::objects::Objects::write`] refused a payload that `git fsck` would reject;
846 /// nothing was written.
847 #[error("refusing to write invalid {kind} object: {}", .check.report_line())]
848 InvalidObject {
849 /// Kind the payload was meant to be stored as.
850 kind: crate::objects::ObjectKind,
851 /// The first fsck rule the payload breaks (`check.id` is Git's fsck message id).
852 check: crate::fsck_standalone::FsckError,
853 },
854
855 /// [`crate::objects::Objects::write_tree`] refused an entry.
856 #[error(transparent)]
857 InvalidTreeEntry(#[from] crate::objects::TreeEntryError),
858
859 /// A [`crate::ident::Signature`] cannot be written as an author, committer or tagger line.
860 #[error(transparent)]
861 InvalidSignature(#[from] crate::ident::SignatureError),
862
863 /// An object exists but has a different kind than the operation requires, such as a
864 /// tree or blob where a commit was expected.
865 #[error("object {oid} is a {actual}, not a {expected}")]
866 UnexpectedObjectKind {
867 /// The object that was read.
868 oid: crate::objects::ObjectId,
869 /// The kind the operation required.
870 expected: crate::objects::ObjectKind,
871 /// The kind the object actually has.
872 actual: crate::objects::ObjectKind,
873 },
874
875 /// A commit-graph write was given a commit whose parent is neither among the commits
876 /// being written nor in the base graph layers, so its parent edge cannot be encoded.
877 #[error("commit-graph cannot encode parent {parent} of commit {commit}: parent not in graph")]
878 CommitGraphParentMissing {
879 /// Commit whose parent list was being encoded.
880 commit: crate::objects::ObjectId,
881 /// Parent that is missing from the graph.
882 parent: crate::objects::ObjectId,
883 },
884
885 /// In-pack delta resolution exceeded the read-side chain depth limit (guards
886 /// against cyclic ref-delta chains and unbounded recursion; Git has no read limit).
887 #[error("delta chain too deep (limit {limit})")]
888 DeltaChainTooDeep {
889 /// Maximum delta chain depth allowed while reading packed objects.
890 limit: usize,
891 },
892
893 /// An unsupported or unknown object type was encountered.
894 #[error("unknown object type '{0}'")]
895 UnknownObjectType(String),
896
897 /// Loose object header type field exceeds Git's 32-byte limit.
898 #[error("header for {oid} too long, exceeds 32 bytes")]
899 ObjectHeaderTooLong {
900 /// Object id (hex) of the loose object with an oversized header.
901 oid: String,
902 },
903
904 /// An I/O error from the underlying filesystem.
905 #[error("I/O error: {0}")]
906 Io(#[from] std::io::Error),
907
908 /// A lock file (for example `.git/index.lock` or `refs/heads/main.lock`) already exists, so
909 /// another process is updating the same file or a crashed one left the lock behind.
910 ///
911 /// Operations that return this before changing anything leave the repository untouched.
912 /// The lock file belongs to whoever created it and is never removed. Retry once the other
913 /// process finishes, or delete `path` when no process owns it (a `holder` with
914 /// `running: false` means the lock is stale).
915 #[error("lock file {} already exists", path.display())]
916 LockHeld {
917 /// The lock file that could not be created.
918 path: std::path::PathBuf,
919 /// Owner recorded in the lock's pid sidecar (`core.lockfilePid`), when one was written.
920 holder: Option<crate::lockfile::LockHolder>,
921 },
922
923 /// A zlib compression or decompression failure.
924 #[error("zlib error: {0}")]
925 Zlib(String),
926
927 /// Loose object bytes hash to a different OID than the file path implies (`git fsck` / `read_loose_object`).
928 #[error("{real_oid}: hash-path mismatch, found at: {path}")]
929 LooseHashMismatch {
930 /// Repository-relative or filesystem path to the loose object file.
931 path: String,
932 /// Hex object id of the unpacked contents.
933 real_oid: String,
934 },
935
936 /// A prepared loose object could not be written during parallel index staging.
937 #[error("could not store {path}: {source}")]
938 LooseObjectWrite {
939 /// Index-relative path of the blob being stored.
940 path: String,
941 /// Underlying filesystem error from the object store.
942 source: std::io::Error,
943 },
944
945 /// Filesystem I/O failed on a repository-relative worktree path (checkout, staging, diff).
946 #[error("{}", crate::worktree::io_error::format_worktree_path_io_message(.path, *operation, .source))]
947 WorktreePathIo {
948 /// Index- or worktree-relative path grit was operating on.
949 path: String,
950 /// What the operation was trying to do when I/O failed.
951 operation: WorktreePathOperation,
952 /// Underlying OS error.
953 source: std::io::Error,
954 },
955
956 /// Filesystem I/O failed on a path under the repository git directory (objects, refs, logs, …).
957 #[error("{}", crate::git_dir::io_error::format_git_dir_io_message(.path, *operation, .source))]
958 GitDirPathIo {
959 /// Display path such as `.git/objects` or `.git/refs/heads/main`.
960 path: String,
961 /// What the operation was trying to do when I/O failed.
962 operation: GitDirPathOperation,
963 /// Underlying OS error.
964 source: std::io::Error,
965 },
966
967 /// A streamed object write or hash ended before the declared size was read.
968 #[error("object stream ended after {actual} of {expected} bytes")]
969 ObjectStreamTruncated {
970 /// Payload size the caller declared.
971 expected: u64,
972 /// Bytes the stream delivered before end of file.
973 actual: u64,
974 },
975
976 /// A worktree file hashed earlier in an operation had different content when stored.
977 #[error("{path} changed while it was being added")]
978 WorktreeFileChanged {
979 /// Index-relative path of the file.
980 path: String,
981 },
982
983 /// The index file is missing, truncated, or has a bad header.
984 #[error("index error: {0}")]
985 IndexError(String),
986
987 /// Stage-1/2/3 entries are present; a stage-0 tree cannot be built.
988 #[error("index contains unmerged entries")]
989 IndexUnmerged,
990
991 /// Another maintenance run holds the repository's `gc.pid` lock
992 /// ([`crate::maintenance::GcLock`]); nothing was changed.
993 #[error("{}", maintenance_running_message(holder))]
994 MaintenanceRunning {
995 /// Process recorded in `gc.pid`.
996 holder: crate::maintenance::GcLockHolder,
997 },
998
999 /// Explicit [`crate::porcelain::add::stage`] refused paths that still contain conflict markers.
1000 #[error("unmerged paths still have conflict markers")]
1001 UnmergedConflictMarkers {
1002 /// Repository-relative paths that were not staged because markers remain.
1003 paths: Vec<String>,
1004 },
1005
1006 /// A file path is a prefix of another index path at the same tree level.
1007 #[error(
1008 "index path prefix conflict between '{}' and '{}'",
1009 directory.display(),
1010 file.display()
1011 )]
1012 IndexPathPrefixConflict {
1013 /// Shorter path (treated as the ambiguous directory prefix).
1014 directory: std::path::PathBuf,
1015 /// Longer path that extends the prefix with an additional component.
1016 file: std::path::PathBuf,
1017 },
1018
1019 /// The `FSMN` index extension ends before its header, token, or EWAH payload.
1020 #[error("index fsmonitor extension payload is truncated")]
1021 IndexFsmonitorExtensionTruncated,
1022
1023 /// The `FSMN` extension header version is not supported.
1024 #[error("index fsmonitor extension has unsupported header version {0}")]
1025 IndexFsmonitorExtensionBadVersion(u32),
1026
1027 /// The EWAH bitmap embedded in `FSMN` could not be decoded.
1028 #[error("index fsmonitor extension EWAH bitmap is invalid")]
1029 IndexFsmonitorExtensionEwahInvalid,
1030
1031 /// The cache-tree extension references more entries than the index contains. Git emits this
1032 /// (verbatim, prefixed with `error: `) when a tree with duplicate path entries is read into the
1033 /// index (`t4058-diff-duplicates`).
1034 #[error("corrupted cache-tree has entries not present in index")]
1035 CacheTreeCorrupt,
1036
1037 /// A reference name or value is invalid.
1038 #[error("invalid ref: {0}")]
1039 InvalidRef(String),
1040
1041 /// A `packed-refs` line Git would reject when reading the ref database.
1042 #[error("unexpected line in {path}: {line}")]
1043 PackedRefsUnexpectedLine {
1044 /// Path to the `packed-refs` file being read.
1045 path: String,
1046 /// Offending line contents.
1047 line: String,
1048 },
1049
1050 /// A general path-related error (invalid UTF-8, out-of-bounds, etc.).
1051 #[error("path error: {0}")]
1052 PathError(String),
1053
1054 /// `--pathspec-from-file` parse failure.
1055 #[error(transparent)]
1056 PathspecFromFile(#[from] PathspecFromFileParseError),
1057
1058 /// A tree/index path component is forbidden (`.git`, HFS/NTFS aliases, etc.).
1059 #[error("invalid path '{0}'")]
1060 InvalidPath(String),
1061
1062 /// A repository-relative path failed [`crate::path_validate`] rules while populating the index or checkout.
1063 #[error("invalid path '{path}': {reason}")]
1064 InvalidPathComponent {
1065 /// Full repository-relative path that was rejected.
1066 path: String,
1067 /// Which component rule failed.
1068 reason: crate::path_validate::PathComponentError,
1069 },
1070
1071 /// A configuration file parsing or access error.
1072 #[error(transparent)]
1073 Config(#[from] ConfigError),
1074
1075 /// Branch create, delete, or upstream configuration failure.
1076 #[error(transparent)]
1077 Branch(#[from] crate::porcelain::branch_error::BranchError),
1078
1079 /// Reference lock failure while updating refs.
1080 #[error(transparent)]
1081 RefLock(#[from] RefLockError),
1082
1083 /// Reference store transaction or iteration failure.
1084 #[error(transparent)]
1085 RefStore(crate::refs::store::RefStoreError),
1086
1087 /// Reftable file, stack, or transaction failure.
1088 #[error(transparent)]
1089 Reftable(#[from] crate::reftable::ReftableError),
1090
1091 /// Filter or EOL conversion failure.
1092 #[error(transparent)]
1093 Filter(#[from] FilterError),
1094
1095 /// Patch application failure.
1096 #[error(transparent)]
1097 Apply(#[from] ApplyError),
1098
1099 /// Diff path selection failure.
1100 #[error(transparent)]
1101 DiffPath(#[from] DiffPathError),
1102
1103 /// Path resolves outside the repository.
1104 #[error(transparent)]
1105 PathOutsideRepo(#[from] PathOutsideRepoError),
1106
1107 /// A commit/tag signing or signature-verification error.
1108 #[error("{0}")]
1109 Signing(String),
1110
1111 /// HTTP authentication failed: the server required credentials (`401`) and
1112 /// either no credential provider was wired, the provider could not supply a
1113 /// usable username/password, the server demanded an unsupported auth scheme,
1114 /// or the supplied credentials were rejected.
1115 ///
1116 /// Distinct from [`Error::Message`] so embedders can detect an authentication
1117 /// failure (and e.g. fall back to an interactive/subprocess path) rather than
1118 /// string-matching, and so the failure surfaces typed instead of hanging.
1119 #[error("authentication failed for {url}: {reason}")]
1120 Auth {
1121 /// Repository URL with credentials redacted and smart-HTTP endpoint suffixes
1122 /// (`/info/refs?service=…`, `/git-upload-pack`, `/git-receive-pack`) removed.
1123 url: String,
1124 /// What went wrong.
1125 reason: AuthFailure,
1126 },
1127
1128 /// A push carried `--push-option` values but the remote `git-receive-pack`
1129 /// did not advertise the `push-options` capability, so the options cannot be
1130 /// transmitted.
1131 ///
1132 /// Distinct from [`Error::Message`] so embedders can detect this specific
1133 /// negotiation failure (and e.g. fall back to a subprocess push) rather than
1134 /// string-matching. The message matches Git's
1135 /// `fatal: the receiving end does not support push options`.
1136 #[error("the receiving end does not support push options")]
1137 PushOptionsUnsupported,
1138
1139 /// `git fetch --unshallow` on a repository that is not shallow.
1140 #[error("repository is not shallow")]
1141 FetchUnshallowNotShallow,
1142
1143 /// A deepen/shallow fetch was requested but the remote did not advertise the
1144 /// `shallow` capability.
1145 #[error("the remote does not support shallow fetches")]
1146 FetchShallowUnsupported,
1147
1148 /// A `--deepen` fetch was requested but the remote did not advertise
1149 /// `deepen-relative`.
1150 #[error("the remote does not support deepen-relative")]
1151 FetchDeepenRelativeUnsupported,
1152
1153 /// A bundle fetch or verify found prerequisite commits missing from history.
1154 #[error("repository lacks these prerequisite commits")]
1155 BundleMissingPrerequisites,
1156
1157 /// Streaming fetch/clone temp pack disappeared before index-pack (often real-time antivirus).
1158 #[error(
1159 "received pack file missing: {}. If this persists, check real-time antivirus or disk cleanup for quarantined pack temp files.",
1160 path.display()
1161 )]
1162 ReceivedPackTempMissing {
1163 /// Path to the temp pack that was expected on disk.
1164 path: std::path::PathBuf,
1165 },
1166
1167 /// A pathspec's `:(attr:...)` magic is malformed.
1168 #[error("invalid attr magic in pathspec '{spec}': {reason}")]
1169 InvalidAttrPathspec {
1170 /// The offending pathspec as given.
1171 spec: String,
1172 /// What is wrong with its attr clause.
1173 #[source]
1174 reason: AttrPathspecError,
1175 },
1176
1177 /// A pathspec did not match any file known to Git (index or HEAD tree).
1178 #[error("pathspec '{spec}' did not match any file(s) known to git")]
1179 PathspecNoMatch {
1180 /// User-supplied pathspec that matched nothing.
1181 spec: String,
1182 },
1183
1184 /// [`crate::porcelain::restore::restore_paths`] conflict options were combined with `--staged`.
1185 #[error("conflict restore options cannot be used with --staged")]
1186 RestoreConflictWithStaged,
1187
1188 /// [`crate::porcelain::restore::restore_paths`] conflict options were combined with `--source`.
1189 #[error("conflict restore options cannot be used with --source")]
1190 RestoreConflictWithSource,
1191
1192 /// A conflict side was missing from the index for the requested path.
1193 #[error("path '{path}' does not have their version")]
1194 RestoreConflictMissingTheirs {
1195 /// Repository-relative path.
1196 path: String,
1197 },
1198
1199 /// A conflict side was missing from the index for the requested path.
1200 #[error("path '{path}' does not have our version")]
1201 RestoreConflictMissingOurs {
1202 /// Repository-relative path.
1203 path: String,
1204 },
1205
1206 /// Three-way remerge needs stages 1, 2, and 3 in the index.
1207 #[error("path '{path}' is not unmerged")]
1208 RestoreConflictNotUnmerged {
1209 /// Repository-relative path.
1210 path: String,
1211 },
1212
1213 /// [`crate::porcelain::restore::restore_paths`] was asked to restore the worktree from the
1214 /// index for paths that still have conflict stages; pick a side or rebuild the markers instead.
1215 ///
1216 /// Nothing is written when this is returned.
1217 #[error("{}", restore_unmerged_message(paths))]
1218 RestorePathUnmerged {
1219 /// Matched repository-relative paths with conflict stages (sorted, deduplicated).
1220 paths: Vec<String>,
1221 },
1222
1223 /// A literal pathspec names a file under a symlink directory component.
1224 #[error("pathspec '{spec}' is beyond a symbolic link")]
1225 PathspecBeyondSymlink {
1226 /// User-supplied pathspec that crosses a symlink directory.
1227 spec: String,
1228 },
1229
1230 /// [`crate::porcelain::commit::CommitScope::Paths`] was used while a merge is in progress.
1231 #[error("cannot do a partial commit during a merge")]
1232 PartialCommitDuringMerge,
1233
1234 /// Explicit [`crate::porcelain::add::stage`] pathspecs name paths excluded by ignore rules.
1235 #[error("the following paths are ignored:\n{}", paths.join("\n"))]
1236 PathspecIgnored {
1237 /// Repository-relative paths (or user labels) that cannot be staged while ignored.
1238 paths: Vec<String>,
1239 },
1240
1241 /// [`crate::porcelain::paths::remove_paths`] refused paths whose removal would lose
1242 /// content, because [`crate::porcelain::paths::RemoveOptions::force`] was not set.
1243 #[error(
1244 "refusing to remove paths with unsaved content:\n{}",
1245 paths.iter().map(ToString::to_string).collect::<Vec<_>>().join("\n")
1246 )]
1247 RemoveBlocked {
1248 /// Each refused path with the reason it was refused.
1249 paths: Vec<crate::porcelain::paths::BlockedRemoval>,
1250 },
1251
1252 /// A directory pathspec was used without `recursive` ([`crate::porcelain::paths::remove_paths`]).
1253 #[error("not removing '{0}' recursively without -r")]
1254 NotRemovingRecursively(String),
1255
1256 /// A porcelain path operation requires a checked-out work tree.
1257 #[error("this operation must be run in a work tree")]
1258 RequiresWorkTree,
1259
1260 /// A commit was requested with an empty message body after normalization.
1261 #[error("empty commit message")]
1262 EmptyCommitMessage,
1263
1264 /// [`crate::porcelain::paths::remove_paths`] was called with no pathspecs.
1265 #[error("you must specify path(s) to remove")]
1266 RemovePathsRequired,
1267
1268 /// [`crate::porcelain::paths::move_path`] / [`crate::porcelain::paths::move_paths`] source is not tracked.
1269 #[error("not under version control, source={src}")]
1270 MoveSourceNotTracked {
1271 /// Untracked or missing source path.
1272 src: String,
1273 },
1274
1275 /// Move refused because the destination already exists without `--force`.
1276 #[error("destination exists, source={src}, destination={destination}")]
1277 MoveDestinationExists {
1278 /// Source path in the move request.
1279 src: String,
1280 /// Existing destination path.
1281 destination: String,
1282 },
1283
1284 /// Move refused because the destination lies under the source directory.
1285 #[error("cannot move '{src}' to a subdirectory of itself, '{destination}'")]
1286 MoveIntoSubdirectory {
1287 /// Source directory path.
1288 src: String,
1289 /// Invalid destination under the source.
1290 destination: String,
1291 },
1292
1293 /// Move refused because source and destination are the same path.
1294 #[error("cannot move into itself, source={src}, destination={destination}")]
1295 MoveIntoSelf {
1296 /// Source path.
1297 src: String,
1298 /// Destination equal to the source.
1299 destination: String,
1300 },
1301
1302 /// Move refused because the source has no index entries.
1303 #[error("nothing to move, source={src}")]
1304 MoveNothingToMove {
1305 /// Source path with no index entries.
1306 src: String,
1307 },
1308
1309 /// Multi-source move would write more than one source to the same destination path.
1310 #[error("multiple sources for the same target, destination={destination}")]
1311 MoveDuplicateDestination {
1312 /// Destination path that would receive more than one source.
1313 destination: String,
1314 },
1315
1316 /// A worktree source path is missing or not a file, directory, or symlink.
1317 #[error("bad source, source={src}, destination={destination}")]
1318 MoveBadSource {
1319 /// Source path.
1320 src: String,
1321 /// Planned destination path.
1322 destination: String,
1323 },
1324
1325 /// Internal invariant: a planned rename references a missing index entry.
1326 #[error("internal error: missing index entry for '{path}'")]
1327 MoveMissingIndexEntry {
1328 /// Index path that should exist during a move.
1329 path: String,
1330 },
1331
1332 /// The remote could not be read or written: a missing repository, a failed connection, an
1333 /// HTTP error status, or no Git handshake.
1334 ///
1335 /// Use [`Error::is_remote_unreadable`] to test for it through wrapper errors.
1336 ///
1337 /// Not used for cancellation; see [`Error::is_cancelled`].
1338 #[error("{}", remote_unreadable_message(url.as_deref(), reason))]
1339 RemoteUnreadable {
1340 /// Remote URL or resolved local path, with credentials redacted. `None` when the
1341 /// failing layer only had a connection, not the URL it was opened from.
1342 url: Option<String>,
1343 /// What went wrong.
1344 reason: RemoteUnreadableReason,
1345 },
1346
1347 /// Explicit fetch refspec resolution failed (missing or ambiguous source).
1348 #[error("{0}")]
1349 FetchRefspec(#[from] crate::fetch_request::FetchRefspecError),
1350
1351 /// Fetch refused to overwrite a local branch with a non-fast-forward update.
1352 #[error("non-fast-forward update for {local_ref}")]
1353 FetchNonFastForward {
1354 /// Local ref that would have been overwritten.
1355 local_ref: String,
1356 },
1357
1358 /// Fetch refused to update the branch checked out in this or a linked worktree.
1359 #[error("cannot fetch into checked-out branch '{branch}'")]
1360 FetchIntoCheckedOutBranch {
1361 /// Short branch name under `refs/heads/`.
1362 branch: String,
1363 },
1364
1365 /// Checkout or switch would overwrite local or untracked worktree content.
1366 #[error("{message}")]
1367 WouldOverwrite {
1368 /// Whether tracked local edits or untracked paths block the operation.
1369 reason: OverwriteReason,
1370 /// Repository-relative paths that would be overwritten (sorted, deduplicated).
1371 paths: Vec<String>,
1372 /// Human-readable summary (depends on [`OverwriteReason`]).
1373 message: String,
1374 },
1375
1376 /// Staged or unstaged changes block an operation that needs a clean worktree.
1377 #[error("you have uncommitted changes — commit them before {operation}")]
1378 DirtyWorktree {
1379 /// The operation that was refused.
1380 operation: WorktreeOperation,
1381 /// Repository-relative paths with staged or unstaged changes (sorted, deduplicated;
1382 /// empty when the caller only knew that something changed).
1383 paths: Vec<String>,
1384 },
1385
1386 /// Author or committer on [`crate::porcelain::commit::CommitRequest`] is invalid.
1387 #[error(transparent)]
1388 InvalidIdentity(#[from] crate::ident::CommitIdentityError),
1389
1390 /// A long-running library operation stopped at a cancellation checkpoint.
1391 ///
1392 /// Use [`Error::is_cancelled`] to test for it through wrapper errors
1393 /// ([`crate::remote::RemoteError::is_cancelled`], [`crate::remote::clone::CloneError::is_cancelled`], …).
1394 #[error("operation cancelled")]
1395 Cancelled,
1396
1397 /// No usable author or committer identity is configured
1398 /// ([`crate::repo::Repository::identity`]).
1399 #[error(transparent)]
1400 Identity(#[from] crate::config::ident_resolve::IdentityError),
1401
1402 /// User-facing message that should be printed verbatim (no extra prefix).
1403 ///
1404 /// Used for revision errors that must match Git's `fatal:` lines exactly.
1405 #[error("{0}")]
1406 Message(String),
1407
1408 /// A merge was requested with fast-forward only, but the histories diverged.
1409 #[error("not possible to fast-forward; refusing to merge")]
1410 NotFastForward,
1411
1412 /// The index tree matches the parent commit tree and [`allow_empty`](crate::porcelain::commit::CommitRequest::allow_empty) is false.
1413 #[error("nothing to commit")]
1414 NothingToCommit,
1415
1416 /// No staged changes are available for a [`CommitScope::Staged`](crate::porcelain::commit::CommitScope::Staged)
1417 /// commit, but the work tree has unstaged or untracked paths.
1418 #[error("nothing staged to commit")]
1419 NothingStagedToCommit {
1420 /// Tracked paths that differ between the index and the work tree.
1421 unstaged: usize,
1422 /// Untracked paths under the work tree.
1423 untracked: usize,
1424 },
1425
1426 /// [`create_commit`](crate::porcelain::commit::create_commit) requires a branch checkout.
1427 #[error("HEAD is detached")]
1428 DetachedHead,
1429
1430 /// `HEAD` is on a branch with no commits yet, but the operation needs a commit
1431 /// ([`Repository::head_commit`](crate::repo::Repository::head_commit),
1432 /// [`replay_commit`](crate::porcelain::replay::replay_commit)).
1433 #[error("no commits yet on this branch")]
1434 UnbornHead,
1435
1436 /// `HEAD` could not be resolved to a branch, detached commit, or unborn branch.
1437 #[error("HEAD is in an unknown state")]
1438 InvalidHead,
1439
1440 /// [`replay_commit`](crate::porcelain::replay::replay_commit) does not replay merge commits
1441 /// until callers can select a mainline parent explicitly.
1442 #[error("merge commit {oid}")]
1443 MergeCommit {
1444 /// Object id of the merge commit that cannot be replayed yet.
1445 oid: crate::objects::ObjectId,
1446 },
1447
1448 /// Cherry-pick source is already checked out as `HEAD`.
1449 #[error("commit already at HEAD")]
1450 ReplaySourceAtHead,
1451
1452 /// Sequencer todo lists a command grit does not implement yet.
1453 #[error("unknown sequencer command '{command}'")]
1454 SequencerUnknownCommand {
1455 /// Verb read from `.git/sequencer/todo`.
1456 command: String,
1457 },
1458
1459 /// [`abort_replay_sequence`](crate::porcelain::replay_sequence::abort_replay_sequence) refuses when `HEAD` moved since the replay started.
1460 #[error("cannot abort replay: HEAD moved since replay started")]
1461 ReplayAbortHeadMoved,
1462
1463 /// [`continue_replay_sequence`](crate::porcelain::replay_sequence::continue_replay_sequence) refuses while unmerged index entries remain.
1464 #[error("cannot continue replay: unmerged index entries remain")]
1465 ReplayContinueUnmerged,
1466
1467 /// No cherry-pick or revert sequence is in progress.
1468 #[error("no cherry-pick or revert in progress")]
1469 NoReplayInProgress,
1470
1471 /// The command would move `HEAD` while a paused operation still depends on it
1472 /// (see [`operation_blocking_switch`](crate::state::operation_blocking_switch)).
1473 #[error("{} in progress", .0.description())]
1474 OperationInProgress(crate::state::InProgressOperation),
1475
1476 /// A local branch is checked out as `HEAD` in a linked or main worktree.
1477 #[error("branch '{branch}' is already checked out in '{worktree_path}'")]
1478 BranchCheckedOutInWorktree {
1479 /// Short branch name (e.g. `main`).
1480 branch: String,
1481 /// Path to the worktree that has the branch checked out.
1482 worktree_path: String,
1483 },
1484
1485 /// `refs/heads/<branch>` does not exist.
1486 #[error("no branch named '{branch}'")]
1487 BranchNotFound {
1488 /// Short branch name (e.g. `topic`).
1489 branch: String,
1490 },
1491
1492 /// [`list_branches`](crate::branch_tracking::list_branches) with [`BranchDetail::Names`](crate::branch_tracking::BranchDetail::Names) cannot use date sort.
1493 #[error("branch list with Names detail cannot use date sort (date sort reads commit objects)")]
1494 BranchListNamesDateSort,
1495
1496 /// [`delete_branch`](crate::porcelain::branch::delete_branch) refuses to delete the branch
1497 /// `HEAD` points at.
1498 #[error("cannot delete '{branch}' — it is the current branch")]
1499 CannotDeleteCurrentBranch {
1500 /// Short branch name (e.g. `main`).
1501 branch: String,
1502 },
1503
1504 /// [`delete_branch`](crate::porcelain::branch::delete_branch) without `force` found commits
1505 /// on the branch that are not reachable from its upstream (or from `HEAD` when it has none).
1506 #[error("the branch '{branch}' is not fully merged")]
1507 BranchNotMerged {
1508 /// Short branch name (e.g. `topic`).
1509 branch: String,
1510 },
1511
1512 /// `refs/tags/<tag>` does not exist.
1513 #[error("no tag named '{tag}'")]
1514 TagNotFound {
1515 /// Short tag name (e.g. `v1.0`).
1516 tag: String,
1517 },
1518
1519 /// Tag create/delete validation failure ([`crate::porcelain::tag::TagError`]).
1520 #[error(transparent)]
1521 Tag(#[from] crate::porcelain::tag::TagError),
1522
1523 /// Linked worktree add/remove/move/lock/repair failure ([`crate::worktree::WorktreeError`]).
1524 #[error(transparent)]
1525 Worktree(#[from] crate::worktree::WorktreeError),
1526
1527 /// Stash commit object has fewer parents or an invalid shape for apply/show.
1528 #[error("corrupt stash commit: {0}")]
1529 CorruptStash(&'static str),
1530
1531 /// `stash@{{n}}` is out of range for the current stash reflog.
1532 #[error("stash entry stash@{{{n}}} not found")]
1533 StashNotFound {
1534 /// Stash reflog index (`stash@{{n}}`).
1535 n: usize,
1536 },
1537
1538 /// Ref does not exist and has no reflog entries.
1539 #[error("unknown ref '{refname}'")]
1540 ReflogUnknownRef {
1541 /// User-supplied or DWIM ref name.
1542 refname: String,
1543 },
1544
1545 /// A reflog selector is missing its reference or `@{n}` delimiters.
1546 #[error("expected ref@{{n}} form, got '{spec}'")]
1547 ReflogInvalidSelector {
1548 /// Selector after trimming surrounding whitespace.
1549 spec: String,
1550 },
1551
1552 /// A reflog selector index is not a nonnegative integer that fits in `usize`.
1553 #[error("invalid reflog index in '{spec}'")]
1554 ReflogInvalidIndex {
1555 /// Selector after trimming surrounding whitespace.
1556 spec: String,
1557 /// Integer parsing failure, including overflow.
1558 #[source]
1559 source: std::num::ParseIntError,
1560 },
1561
1562 /// `ref@{{n}}` is out of range for the current reflog.
1563 #[error("reflog entry {refname}@{{{index}}} not found")]
1564 ReflogIndexOutOfRange {
1565 /// Ref whose reflog was read.
1566 refname: String,
1567 /// Requested newest-first index.
1568 index: usize,
1569 },
1570
1571 /// Pushing or creating a stash ([`crate::repo_handles::RepositoryStash::push`]) requires at least one commit.
1572 #[error("cannot stash without a commit")]
1573 StashNoInitialCommit,
1574
1575 /// Stash create/apply/pop requires a working tree (not a bare repository).
1576 #[error("cannot stash in a bare repository")]
1577 StashBareRepository,
1578
1579 /// [`crate::repo_handles::RepositoryStash::apply`] with `restore_index` could
1580 /// not merge the current index with the stashed index parent.
1581 #[error("conflicts in index while applying stash with --index")]
1582 StashIndexConflict,
1583
1584 /// Incompatible [`StashPushOptions`](crate::porcelain::stash::StashPushOptions) flags.
1585 #[error("{0}")]
1586 StashOptionConflict(String),
1587
1588 /// Partial [`crate::repo_handles::RepositoryStash::push`] hunk selection failed.
1589 #[error(transparent)]
1590 HunkSelection(#[from] crate::porcelain::hunk_select::HunkSelectError),
1591
1592 /// HEAD does not resolve while applying a stash entry.
1593 #[error("missing HEAD while applying stash")]
1594 StashMissingHead,
1595
1596 /// Applying a stash would overwrite locally modified worktree content.
1597 #[error(
1598 "your local changes to the following files would be overwritten by stash apply: {paths}"
1599 )]
1600 StashWouldOverwriteLocalChanges {
1601 /// Space-separated list of paths that would be overwritten.
1602 paths: String,
1603 },
1604
1605 /// The index could not be written after a successful stash apply.
1606 #[error("writing index after stash apply: {0}")]
1607 StashIndexWrite(String),
1608
1609 /// A symlink blob in a stash commit is not valid UTF-8.
1610 #[error("symlink target is not UTF-8")]
1611 StashSymlinkNotUtf8,
1612
1613 /// Applying a stash with an untracked-files parent would replace an existing worktree path.
1614 #[error("stash untracked restore blocked: '{path}' already exists on disk")]
1615 StashCouldNotRestoreUntracked {
1616 /// Worktree path that already exists and blocks untracked restore.
1617 path: String,
1618 },
1619
1620 /// [`CommitRequest::amend`](crate::porcelain::commit::CommitRequest::amend) was set on an unborn branch.
1621 #[error("cannot amend: no commits on this branch yet")]
1622 AmendUnborn,
1623
1624 /// [`RepositoryNotes::add`](crate::repo_handles::RepositoryNotes::add) was called without
1625 /// `force` when a note already exists on the object.
1626 #[error("note for object {object} already exists")]
1627 NoteAlreadyExists {
1628 /// Annotated object that already has a note.
1629 object: crate::objects::ObjectId,
1630 },
1631
1632 /// A configured or environment notes ref is not under `refs/notes/`.
1633 #[error("notes ref '{0}' must start with 'refs/notes/'")]
1634 InvalidNotesRef(String),
1635
1636 /// A notes ref resolves to an object that cannot hold notes (notes reads accept a commit or
1637 /// a tree; notes merges require a commit).
1638 #[error("notes ref '{notes_ref}' points to a {found}, not a notes commit")]
1639 NotesRefNotCommit {
1640 /// Notes ref (or revision) that was read.
1641 notes_ref: String,
1642 /// Kind of object the ref resolved to.
1643 found: crate::objects::ObjectKind,
1644 },
1645
1646 /// An object reached while reading a notes tree has the wrong kind (for example a note
1647 /// entry that is not a blob).
1648 #[error("notes object {oid} is a {found}, expected a {expected}")]
1649 NotesObjectKind {
1650 /// Object that was read.
1651 oid: crate::objects::ObjectId,
1652 /// Kind the notes layout requires at this position.
1653 expected: crate::objects::ObjectKind,
1654 /// Kind actually stored.
1655 found: crate::objects::ObjectKind,
1656 },
1657
1658 /// [`notes_merge_inner`](crate::notes::notes_merge_inner) was asked to merge two notes refs
1659 /// that both have no notes commit.
1660 #[error("cannot merge notes: neither '{local}' nor '{remote}' has any notes")]
1661 NotesMergeBothEmpty {
1662 /// Local notes ref (merge destination).
1663 local: String,
1664 /// Remote notes ref (merge source).
1665 remote: String,
1666 },
1667
1668 /// A manual notes merge was started while an earlier one still has conflict files in the
1669 /// notes merge worktree.
1670 #[error("unfinished notes merge: conflict files remain in {}", path.display())]
1671 NotesMergeInProgress {
1672 /// The notes merge worktree directory.
1673 path: std::path::PathBuf,
1674 },
1675
1676 /// Multi-pack-index load or write failure.
1677 #[error(transparent)]
1678 Midx(#[from] MidxError),
1679
1680 /// Revision parsing failed ([`crate::rev_parse_error::RevParseError`]).
1681 #[error(transparent)]
1682 RevParse(#[from] crate::rev_parse_error::RevParseError),
1683
1684 /// Revision walking / `rev-list` option parsing failed ([`crate::rev_list_error::RevListError`]).
1685 #[error(transparent)]
1686 RevList(#[from] crate::rev_list_error::RevListError),
1687
1688 /// A Git hook subprocess failed or could not be started.
1689 #[error(transparent)]
1690 Hook(#[from] crate::hooks::HookError),
1691
1692 /// History rewrite planning failed ([`crate::history::HistoryError`]).
1693 #[error(transparent)]
1694 History(#[from] crate::history::HistoryError),
1695
1696 /// The operation needs the files-backed `objects/` directory, but the repository's
1697 /// [`Odb`](crate::odb::Odb) uses a custom [`OdbBuilder::primary`](crate::odb::OdbBuilder::primary)
1698 /// store. Nothing was written; use a files-backed repository for this operation.
1699 #[error("{operation} requires the files-backed object store; it is not supported with a custom primary")]
1700 UnsupportedObjectStore {
1701 /// Operation that was refused (for example `"fetch"`, `"push"`, `"gc"`).
1702 operation: &'static str,
1703 },
1704
1705 /// [`RepositoryBuilder::open`](crate::repo::RepositoryBuilder::open) was given an
1706 /// [`OdbBuilder`](crate::odb::OdbBuilder) rooted somewhere other than the repository's
1707 /// `objects/` directory.
1708 #[error(
1709 "object database {} is not the repository's objects directory {}",
1710 builder.display(),
1711 repository.display()
1712 )]
1713 OdbObjectsDirMismatch {
1714 /// The builder's objects directory, canonicalized when it exists.
1715 builder: std::path::PathBuf,
1716 /// The repository's canonical objects directory.
1717 repository: std::path::PathBuf,
1718 },
1719
1720 /// Parsing a [`crate::tree_sha256::TreeSha256`] from hex failed (expected 64 hex digits).
1721 #[error("invalid tree-sha256 hex")]
1722 TreeSha256InvalidHex,
1723
1724 /// A revision passed to [`crate::tree_sha256::tree_sha256`] did not peel to a commit or tree.
1725 #[error("tree-sha256: revision resolves to {found:?}, not a commit or tree")]
1726 TreeSha256InvalidRevision {
1727 /// Object kind after peeling annotated tags.
1728 found: crate::objects::ObjectKind,
1729 },
1730
1731 /// One or more submodules required for [`crate::tree_sha256::tree_sha256`] are missing.
1732 #[error("tree-sha256: missing submodules")]
1733 TreeSha256MissingSubmodules(Vec<crate::tree_sha256::MissingSubmodule>),
1734
1735 /// `--tree-sha256` was requested with signing explicitly disabled.
1736 #[error("tree-sha256 requires a signed tag")]
1737 TreeSha256RequiresSigning,
1738
1739 /// [`crate::tree_sha256::verify_tag`] was given an object that is not an annotated tag.
1740 #[error("verify-tag: object {oid} is a {found:?}, not an annotated tag")]
1741 TagVerifyNotTag {
1742 /// Object id passed to verification.
1743 oid: crate::objects::ObjectId,
1744 /// Kind stored in the ODB.
1745 found: crate::objects::ObjectKind,
1746 },
1747
1748 /// Annotated tag header block has no `object` line.
1749 #[error("verify-tag: tag has no object header")]
1750 TagVerifyMissingObjectHeader,
1751
1752 /// Annotated tag carries more than one `tree-sha256` header line.
1753 #[error("verify-tag: tag has more than one tree-sha256 header")]
1754 TagVerifyDuplicateTreeSha256Header,
1755
1756 /// Annotated tag tree-sha256 header parsing failed.
1757 #[error(transparent)]
1758 TreeSha256Claim(#[from] crate::tree_sha256::TreeSha256ClaimError),
1759
1760 /// `grit blame --ignore-rev` did not resolve to a commit.
1761 #[error("cannot find revision {rev} to ignore")]
1762 BlameIgnoreRevUnknown {
1763 /// Revision expression from `--ignore-rev`.
1764 rev: String,
1765 },
1766
1767 /// `--ignore-revs-file` could not be read.
1768 #[error("could not open object name list: {path}")]
1769 BlameIgnoreRevsFileOpen {
1770 /// Path argument (before expansion).
1771 path: String,
1772 /// Underlying open error.
1773 #[source]
1774 source: std::io::Error,
1775 },
1776
1777 /// A line in an ignore-revs file is not a full-length object id.
1778 #[error("invalid object name: {rev}")]
1779 BlameIgnoreRevsFileInvalidRev {
1780 /// Path argument for the file.
1781 path: String,
1782 /// Offending line text after comment/trim handling.
1783 rev: String,
1784 },
1785}
1786
1787impl Error {
1788 /// Build [`Error::DirtyWorktree`], sorting and deduplicating `paths`.
1789 #[must_use]
1790 pub fn dirty_worktree(operation: WorktreeOperation, mut paths: Vec<String>) -> Self {
1791 paths.sort();
1792 paths.dedup();
1793 Self::DirtyWorktree { operation, paths }
1794 }
1795
1796 /// Build [`Error::WouldOverwrite`] for switch and similar tree transitions.
1797 #[must_use]
1798 pub fn would_overwrite(reason: OverwriteReason, mut paths: Vec<String>) -> Self {
1799 paths.sort();
1800 paths.dedup();
1801 let message = match reason {
1802 OverwriteReason::LocalChanges => {
1803 "you have uncommitted changes — commit them before switching".into()
1804 }
1805 OverwriteReason::Untracked => {
1806 let first = paths.first().map(String::as_str).unwrap_or("<path>");
1807 format!("untracked file '{first}' would be overwritten — move or remove it first")
1808 }
1809 };
1810 Self::WouldOverwrite {
1811 reason,
1812 paths,
1813 message,
1814 }
1815 }
1816
1817 /// Build [`Error::RemoteUnreadable`] for `url`.
1818 #[must_use]
1819 pub fn remote_unreadable(url: impl Into<String>, reason: RemoteUnreadableReason) -> Self {
1820 Self::RemoteUnreadable {
1821 url: Some(url.into()),
1822 reason,
1823 }
1824 }
1825
1826 /// Whether this is [`Error::RemoteUnreadable`]: the remote could not be read or written
1827 /// at all (as opposed to a rejected update or an empty remote).
1828 #[must_use]
1829 pub fn is_remote_unreadable(&self) -> bool {
1830 matches!(self, Self::RemoteUnreadable { .. })
1831 }
1832
1833 /// Whether this is [`Error::Cancelled`] or a nested history failure that was cancelled.
1834 #[must_use]
1835 pub fn is_cancelled(&self) -> bool {
1836 match self {
1837 Self::Cancelled => true,
1838 Self::History(e) => e.is_cancelled(),
1839 _ => false,
1840 }
1841 }
1842
1843 /// Build [`Error::InvalidPathComponent`].
1844 #[must_use]
1845 pub fn invalid_path_component(
1846 path: impl Into<String>,
1847 reason: crate::path_validate::PathComponentError,
1848 ) -> Self {
1849 Self::InvalidPathComponent {
1850 path: path.into(),
1851 reason,
1852 }
1853 }
1854
1855 /// When set, the CLI should prefix stderr with `fatal: ` for Git-compatible commands.
1856 #[must_use]
1857 pub fn wants_fatal_prefix(&self) -> bool {
1858 matches!(
1859 self,
1860 Error::Config(_)
1861 | Error::RefLock(_)
1862 | Error::Filter(_)
1863 | Error::PathOutsideRepo(_)
1864 | Error::DiffPath(_)
1865 )
1866 }
1867
1868 /// Format for Git-style stderr (adds `fatal:` / `error:` when appropriate).
1869 #[must_use]
1870 pub fn git_stderr_message(&self) -> String {
1871 match self {
1872 Error::Apply(ApplyError::CorruptPatch { .. }) => {
1873 crate::diagnostics::error_line(&self.to_string())
1874 }
1875 Error::Message(s) => s.clone(),
1876 Error::WouldOverwrite { message, .. } => message.clone(),
1877 e if e.wants_fatal_prefix() => crate::diagnostics::fatal_line(&e.to_string()),
1878 other => other.to_string(),
1879 }
1880 }
1881}
1882
1883impl From<crate::refs::store::RefStoreError> for Error {
1884 fn from(err: crate::refs::store::RefStoreError) -> Self {
1885 match err {
1886 crate::refs::store::RefStoreError::GitDirPathIo {
1887 path,
1888 operation,
1889 os_error,
1890 } => Error::GitDirPathIo {
1891 path,
1892 operation,
1893 source: crate::refs::store::RefStoreError::git_dir_io_source(os_error),
1894 },
1895 other => Error::RefStore(other),
1896 }
1897 }
1898}
1899
1900impl From<crate::lockfile::LockError> for Error {
1901 /// [`LockError::Held`](crate::lockfile::LockError::Held) becomes [`Error::LockHeld`] with the
1902 /// recorded [`LockHolder`](crate::lockfile::LockHolder), and I/O failures become [`Error::Io`].
1903 #[cold]
1904 fn from(err: crate::lockfile::LockError) -> Self {
1905 use crate::lockfile::{LockError, LockHolder};
1906 match err {
1907 LockError::Held { path } => Self::LockHeld {
1908 holder: LockHolder::read(&path),
1909 path,
1910 },
1911 LockError::Io { source, .. } => Self::Io(source),
1912 }
1913 }
1914}
1915
1916/// Convenience alias for `Result<T, Error>`.
1917pub type Result<T> = std::result::Result<T, Error>;