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}