Skip to main content

grit_lib/
rev_parse_error.rs

1//! Typed failures from revision parsing ([`crate::rev_parse`]).
2
3use thiserror::Error;
4
5/// Failure modes when resolving revision specs, upstream/push refs, paths, and reflogs.
6#[derive(Debug, Error)]
7#[non_exhaustive]
8pub enum RevParseError {
9    /// `branch.<name>.merge` / remote tracking is not configured.
10    #[error("no upstream configured for branch '{branch}'")]
11    NoUpstream {
12        /// Short branch name shown to the user.
13        branch: String,
14    },
15
16    /// Upstream is configured but the remote-tracking ref is missing.
17    #[error("upstream branch '{merge}' is not stored as a remote-tracking branch")]
18    UpstreamNotTracked {
19        /// Value from `branch.*.merge` (may include `refs/heads/`).
20        merge: String,
21    },
22
23    /// No remote is configured for `@{push}` resolution.
24    #[error("branch has no configured push remote")]
25    NoPushRemote,
26
27    /// Remote has push refspecs but none map this branch.
28    #[error("push refspecs for '{remote}' do not include '{branch}'")]
29    PushRefspecMissing {
30        /// Remote name from config.
31        remote: String,
32        /// Short branch name.
33        branch: String,
34    },
35
36    /// `push.default = nothing` and no overriding push refspec applies.
37    #[error("push.default is nothing; no push destination")]
38    PushDefaultNothing,
39
40    /// `push.default = upstream` but no upstream tracking ref exists.
41    #[error("branch '{branch}' has no upstream for push.default upstream")]
42    NoUpstreamForPush {
43        /// Short branch name.
44        branch: String,
45    },
46
47    /// `push.default = simple` and upstream differs from the branch’s push ref.
48    #[error("push.default simple: upstream and push ref differ")]
49    PushDefaultSimpleMismatch,
50
51    /// Default push mode could not find a tracking ref for the branch.
52    #[error("no push tracking ref for branch '{branch}'")]
53    NoPushTrackingRef {
54        /// Short branch name.
55        branch: String,
56    },
57
58    /// `HEAD` is not on a local branch (detached or unborn).
59    #[error("HEAD does not point to a branch")]
60    HeadNotBranch,
61
62    /// Named branch does not exist under `refs/heads/`.
63    #[error("no such branch: '{branch}'")]
64    NoSuchBranch {
65        /// Branch name from the spec.
66        branch: String,
67    },
68
69    /// Short ref name matches more than one ref under DWIM rules.
70    #[error("refname '{spec}' is ambiguous")]
71    AmbiguousRefname {
72        /// User ref spec.
73        spec: String,
74    },
75
76    /// Could not read `.git/AUTO_MERGE`.
77    #[error("failed to read AUTO_MERGE: {detail}")]
78    AutoMergeRead {
79        /// Underlying I/O or parse detail.
80        detail: String,
81    },
82
83    /// Spec fragment is not a valid object name after other resolution failed.
84    #[error("invalid object name '{name}'")]
85    InvalidObjectName {
86        /// The unresolved token.
87        name: String,
88    },
89
90    /// Parent/ancestor navigation (`~`, `^`, …) ran past the first root commit.
91    #[error("revision '{spec}' is out of range")]
92    RevisionOutOfRange {
93        /// Full user revision spec that could not be resolved.
94        spec: String,
95    },
96
97    /// Path in `rev:path` lies outside the work tree.
98    #[error("'{path}' is outside repository at '{work_tree}'")]
99    OutsideRepository {
100        /// Path from the revision spec.
101        path: String,
102        /// Canonical work-tree path for the message.
103        work_tree: String,
104    },
105
106    /// Revision string matches neither an object, ref, nor index path.
107    #[error("ambiguous revision or path '{spec}'")]
108    AmbiguousArgument {
109        /// Full user spec.
110        spec: String,
111    },
112
113    /// A short hex prefix matches more than one object.
114    #[error("short object id '{spec}' is ambiguous")]
115    AmbiguousShortObject {
116        /// Abbreviated object id from the user spec.
117        spec: String,
118    },
119
120    /// Reflog selector used but the reflog has no entries.
121    #[error("log for '{ref_display}' is empty")]
122    ReflogEmpty {
123        /// Human-readable ref label.
124        ref_display: String,
125    },
126
127    /// Reflog index is out of range.
128    #[error("log for '{ref_display}' only has {available} entries")]
129    ReflogInsufficientEntries {
130        /// Human-readable ref label.
131        ref_display: String,
132        /// Number of entries available.
133        available: usize,
134    },
135
136    /// Tree path exists at `HEAD` but not at the requested revision.
137    #[error("path '{path}' exists on disk, but not in '{revision}'")]
138    PathOnDiskNotInRevision {
139        /// Path from the revision spec.
140        path: String,
141        /// Revision label that lacks the path.
142        revision: String,
143    },
144
145    /// Tree path missing; a related path exists under the current prefix.
146    #[error("path '{candidate}' exists, but not '{path}'")]
147    PathPrefixExists {
148        /// Related path that exists where the requested path does not.
149        candidate: String,
150        /// Repository-relative or filesystem path related to the failure.
151        path: String,
152        /// Optional alternate `rev:path` spelling (no Git-style hint prefix).
153        alternate_spec: Option<String>,
154    },
155
156    /// Path is absent from the named tree.
157    #[error("path '{path}' does not exist in '{revision}'")]
158    PathNotInRevision {
159        /// Path that is missing from the tree.
160        path: String,
161        /// Revision label for the tree that was searched.
162        revision: String,
163    },
164
165    /// Index path lookup failed with no work-tree match.
166    #[error("path '{path}' does not exist (neither on disk nor in the index)")]
167    PathMissingFromWorktreeAndIndex {
168        /// Path that is missing from both the index and work tree.
169        path: String,
170    },
171
172    /// Index path exists at another stage.
173    #[error("path '{path}' is in the index, but not at stage {stage}")]
174    PathWrongIndexStage {
175        /// Repository-relative or filesystem path related to the failure.
176        path: String,
177        /// Index stage number (0–3) that was expected.
178        stage: u8,
179        /// Optional `:stage:path` spelling to try.
180        alternate_spec: Option<String>,
181    },
182
183    /// Index path mismatch under a path prefix.
184    #[error("path '{candidate}' is in the index, but not '{path}'")]
185    PathIndexPrefixMismatch {
186        /// Related path that exists where the requested path does not.
187        candidate: String,
188        /// Repository-relative or filesystem path related to the failure.
189        path: String,
190        /// Optional alternate `rev:path` spelling to try.
191        alternate_spec: Option<String>,
192    },
193
194    /// File exists in the work tree but is not staged.
195    #[error("path '{path}' exists on disk, but not in the index")]
196    PathOnDiskNotInIndex {
197        /// Work-tree path that is not staged in the index.
198        path: String,
199    },
200
201    /// Path is not present in the index at all.
202    #[error("path '{path}' does not exist in the index")]
203    PathNotInIndex {
204        /// Path that is absent from the index.
205        path: String,
206    },
207
208    /// Internal describe helper failed to compile its regex.
209    #[error("internal describe regex")]
210    InternalDescribeRegex,
211
212    /// A change-id prefix matches more than one commit.
213    #[error("ambiguous change id '{prefix}'")]
214    AmbiguousChangeId {
215        /// User-supplied change-id or prefix.
216        prefix: String,
217        /// Matching commits in stable order (oid, then change id).
218        candidates: Vec<AmbiguousChangeIdHint>,
219    },
220}
221
222/// One commit when a change-id lookup is ambiguous.
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct AmbiguousChangeIdHint {
225    /// Full commit object id (hex).
226    pub oid_hex: String,
227    /// Resolved change id (stored or derived).
228    pub change_id: String,
229    /// First line of the commit message.
230    pub subject: String,
231}
232
233/// One candidate line when a short object id is ambiguous.
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct AmbiguousObjectHint {
236    /// Full or abbreviated hex object id.
237    pub oid_hex: String,
238    /// `None` when the object bytes are corrupt (`[bad object]` in Git).
239    pub kind: Option<&'static str>,
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use crate::error::Error;
246
247    fn assert_rev_parse_variant(result: Result<(), Error>, f: impl FnOnce(&RevParseError) -> bool) {
248        match result {
249            Err(Error::RevParse(e)) if f(&e) => {}
250            other => panic!("expected RevParse variant, got {other:?}"),
251        }
252    }
253
254    #[test]
255    fn variant_no_upstream() {
256        let err = Error::from(RevParseError::NoUpstream {
257            branch: "main".into(),
258        });
259        assert!(matches!(
260            err,
261            Error::RevParse(RevParseError::NoUpstream { .. })
262        ));
263    }
264
265    #[test]
266    fn variant_push_default_nothing() {
267        let err = Error::from(RevParseError::PushDefaultNothing);
268        assert!(matches!(
269            err,
270            Error::RevParse(RevParseError::PushDefaultNothing)
271        ));
272    }
273
274    #[test]
275    fn variant_ambiguous_argument() {
276        let err = Error::from(RevParseError::AmbiguousArgument { spec: "foo".into() });
277        assert!(matches!(
278            err,
279            Error::RevParse(RevParseError::AmbiguousArgument { .. })
280        ));
281    }
282
283    #[test]
284    fn display_is_neutral() {
285        let msg = RevParseError::NoPushRemote.to_string();
286        assert!(!msg.contains("fatal:"));
287        assert!(!msg.contains("hint:"));
288    }
289
290    // Placeholder so `assert_rev_parse_variant` is used when integration tests call it.
291    #[test]
292    fn assert_helper_compiles() {
293        let r: Result<(), Error> = Err(RevParseError::HeadNotBranch.into());
294        assert_rev_parse_variant(r, |e| matches!(e, RevParseError::HeadNotBranch));
295    }
296}