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    /// Reflog `@{date}` inner text could not be parsed as a date.
137    #[error("invalid reflog date '{date}'")]
138    ReflogDateUnparseable {
139        /// Text inside `@{...}` that was treated as a date selector.
140        date: String,
141    },
142
143    /// Tree path exists at `HEAD` but not at the requested revision.
144    #[error("path '{path}' exists on disk, but not in '{revision}'")]
145    PathOnDiskNotInRevision {
146        /// Path from the revision spec.
147        path: String,
148        /// Revision label that lacks the path.
149        revision: String,
150    },
151
152    /// Tree path missing; a related path exists under the current prefix.
153    #[error("path '{candidate}' exists, but not '{path}'")]
154    PathPrefixExists {
155        /// Related path that exists where the requested path does not.
156        candidate: String,
157        /// Repository-relative or filesystem path related to the failure.
158        path: String,
159        /// Optional alternate `rev:path` spelling (no Git-style hint prefix).
160        alternate_spec: Option<String>,
161    },
162
163    /// Path is absent from the named tree.
164    #[error("path '{path}' does not exist in '{revision}'")]
165    PathNotInRevision {
166        /// Path that is missing from the tree.
167        path: String,
168        /// Revision label for the tree that was searched.
169        revision: String,
170    },
171
172    /// Index path lookup failed with no work-tree match.
173    #[error("path '{path}' does not exist (neither on disk nor in the index)")]
174    PathMissingFromWorktreeAndIndex {
175        /// Path that is missing from both the index and work tree.
176        path: String,
177    },
178
179    /// Index path exists at another stage.
180    #[error("path '{path}' is in the index, but not at stage {stage}")]
181    PathWrongIndexStage {
182        /// Repository-relative or filesystem path related to the failure.
183        path: String,
184        /// Index stage number (0–3) that was expected.
185        stage: u8,
186        /// Optional `:stage:path` spelling to try.
187        alternate_spec: Option<String>,
188    },
189
190    /// Index path mismatch under a path prefix.
191    #[error("path '{candidate}' is in the index, but not '{path}'")]
192    PathIndexPrefixMismatch {
193        /// Related path that exists where the requested path does not.
194        candidate: String,
195        /// Repository-relative or filesystem path related to the failure.
196        path: String,
197        /// Optional alternate `rev:path` spelling to try.
198        alternate_spec: Option<String>,
199    },
200
201    /// File exists in the work tree but is not staged.
202    #[error("path '{path}' exists on disk, but not in the index")]
203    PathOnDiskNotInIndex {
204        /// Work-tree path that is not staged in the index.
205        path: String,
206    },
207
208    /// Path is not present in the index at all.
209    #[error("path '{path}' does not exist in the index")]
210    PathNotInIndex {
211        /// Path that is absent from the index.
212        path: String,
213    },
214
215    /// Internal describe helper failed to compile its regex.
216    #[error("internal describe regex")]
217    InternalDescribeRegex,
218
219    /// A change-id prefix matches more than one commit.
220    #[error("ambiguous change id '{prefix}'")]
221    AmbiguousChangeId {
222        /// User-supplied change-id or prefix.
223        prefix: String,
224        /// Matching commits in stable order (oid, then change id).
225        candidates: Vec<AmbiguousChangeIdHint>,
226    },
227}
228
229/// One commit when a change-id lookup is ambiguous.
230#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct AmbiguousChangeIdHint {
232    /// Full commit object id (hex).
233    pub oid_hex: String,
234    /// Resolved change id (stored or derived).
235    pub change_id: String,
236    /// First line of the commit message.
237    pub subject: String,
238}
239
240/// One candidate line when a short object id is ambiguous.
241#[derive(Debug, Clone, PartialEq, Eq)]
242pub struct AmbiguousObjectHint {
243    /// Full or abbreviated hex object id.
244    pub oid_hex: String,
245    /// `None` when the object bytes are corrupt (`[bad object]` in Git).
246    pub kind: Option<&'static str>,
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use crate::error::Error;
253
254    fn assert_rev_parse_variant(result: Result<(), Error>, f: impl FnOnce(&RevParseError) -> bool) {
255        match result {
256            Err(Error::RevParse(e)) if f(&e) => {}
257            other => panic!("expected RevParse variant, got {other:?}"),
258        }
259    }
260
261    #[test]
262    fn variant_no_upstream() {
263        let err = Error::from(RevParseError::NoUpstream {
264            branch: "main".into(),
265        });
266        assert!(matches!(
267            err,
268            Error::RevParse(RevParseError::NoUpstream { .. })
269        ));
270    }
271
272    #[test]
273    fn variant_push_default_nothing() {
274        let err = Error::from(RevParseError::PushDefaultNothing);
275        assert!(matches!(
276            err,
277            Error::RevParse(RevParseError::PushDefaultNothing)
278        ));
279    }
280
281    #[test]
282    fn variant_ambiguous_argument() {
283        let err = Error::from(RevParseError::AmbiguousArgument { spec: "foo".into() });
284        assert!(matches!(
285            err,
286            Error::RevParse(RevParseError::AmbiguousArgument { .. })
287        ));
288    }
289
290    #[test]
291    fn display_is_neutral() {
292        let msg = RevParseError::NoPushRemote.to_string();
293        assert!(!msg.contains("fatal:"));
294        assert!(!msg.contains("hint:"));
295    }
296
297    // Placeholder so `assert_rev_parse_variant` is used when integration tests call it.
298    #[test]
299    fn assert_helper_compiles() {
300        let r: Result<(), Error> = Err(RevParseError::HeadNotBranch.into());
301        assert_rev_parse_variant(r, |e| matches!(e, RevParseError::HeadNotBranch));
302    }
303}