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}