gitoxide_core/repository/revision/
explain.rs1use anyhow::bail;
2use gix::{
3 bstr::{BStr, BString},
4 revision::plumbing::{
5 spec,
6 spec::parse::{
7 delegate,
8 delegate::{PeelTo, ReflogLookup, SiblingBranch, Traversal},
9 Delegate,
10 },
11 },
12 Exn,
13};
14
15pub fn explain(spec: std::ffi::OsString, mut out: impl std::io::Write) -> anyhow::Result<()> {
16 let mut explain = Explain::new(&mut out);
17 let spec = gix::path::os_str_into_bstr(&spec)?;
18 gix::revision::plumbing::spec::parse(spec, &mut explain).map_err(gix::Error::from)?;
19 if let Some(err) = explain.err {
20 bail!(err);
21 }
22 Ok(())
23}
24
25struct Explain<'a> {
26 out: &'a mut dyn std::io::Write,
27 call: usize,
28 ref_name: Option<BString>,
29 oid_prefix: Option<gix::hash::Prefix>,
30 has_implicit_anchor: bool,
31 err: Option<String>,
32}
33
34impl<'a> Explain<'a> {
35 fn new(out: &'a mut impl std::io::Write) -> Self {
36 Explain {
37 out,
38 call: 0,
39 ref_name: None,
40 oid_prefix: None,
41 has_implicit_anchor: false,
42 err: None,
43 }
44 }
45 fn prefix(&mut self) -> Result<(), Exn> {
46 self.call += 1;
47 write!(self.out, "{:02}. ", self.call).ok();
48 Ok(())
49 }
50 fn revision_name(&self) -> BString {
51 self.ref_name.clone().unwrap_or_else(|| {
52 self.oid_prefix
53 .expect("parser must have set some object value")
54 .to_string()
55 .into()
56 })
57 }
58}
59
60impl delegate::Revision for Explain<'_> {
61 fn find_ref(&mut self, name: &BStr) -> Result<(), Exn> {
62 self.prefix()?;
63 self.ref_name = Some(name.into());
64 writeln!(self.out, "Lookup the '{name}' reference").ok();
65 Ok(())
66 }
67
68 fn disambiguate_prefix(
69 &mut self,
70 prefix: gix::hash::Prefix,
71 hint: Option<delegate::PrefixHint<'_>>,
72 ) -> Result<(), Exn> {
73 self.prefix()?;
74 self.oid_prefix = Some(prefix);
75 writeln!(
76 self.out,
77 "Disambiguate the '{}' object name ({})",
78 prefix,
79 match hint {
80 None => "any object".to_string(),
81 Some(delegate::PrefixHint::MustBeCommit) => "commit".into(),
82 Some(delegate::PrefixHint::DescribeAnchor { ref_name, generation }) =>
83 format!("commit {generation} generations in future of reference {ref_name:?}"),
84 }
85 )
86 .ok();
87 Ok(())
88 }
89
90 fn reflog(&mut self, query: ReflogLookup) -> Result<(), Exn> {
91 self.prefix()?;
92 self.has_implicit_anchor = true;
93 let ref_name: &BStr = self.ref_name.as_ref().map_or_else(|| "HEAD".into(), AsRef::as_ref);
94 match query {
95 ReflogLookup::Entry(no) => writeln!(self.out, "Find entry {no} in reflog of '{ref_name}' reference").ok(),
96 ReflogLookup::Date(time) => writeln!(
97 self.out,
98 "Find entry closest to time {} in reflog of '{}' reference",
99 time.format_or_unix(gix::date::time::format::ISO8601),
100 ref_name
101 )
102 .ok(),
103 };
104 Ok(())
105 }
106
107 fn nth_checked_out_branch(&mut self, branch_no: usize) -> Result<(), Exn> {
108 self.prefix()?;
109 self.has_implicit_anchor = true;
110 writeln!(self.out, "Find the {branch_no}th checked-out branch of 'HEAD'").ok();
111 Ok(())
112 }
113
114 fn sibling_branch(&mut self, kind: SiblingBranch) -> Result<(), Exn> {
115 self.prefix()?;
116 self.has_implicit_anchor = true;
117 let ref_info = match self.ref_name.as_ref() {
118 Some(ref_name) => format!("'{ref_name}'"),
119 None => "behind 'HEAD'".into(),
120 };
121 writeln!(
122 self.out,
123 "Lookup the remote '{}' branch of local reference {}",
124 match kind {
125 SiblingBranch::Upstream => "upstream",
126 SiblingBranch::Push => "push",
127 },
128 ref_info
129 )
130 .ok();
131 Ok(())
132 }
133}
134
135impl delegate::Navigate for Explain<'_> {
136 fn traverse(&mut self, kind: Traversal) -> Result<(), Exn> {
137 self.prefix()?;
138 let name = self.revision_name();
139 writeln!(
140 self.out,
141 "{}",
142 match kind {
143 Traversal::NthAncestor(no) => format!("Traverse to the {no}. ancestor of revision named '{name}'"),
144 Traversal::NthParent(no) => format!("Select the {no}. parent of revision named '{name}'"),
145 }
146 )
147 .ok();
148 Ok(())
149 }
150
151 fn peel_until(&mut self, kind: PeelTo<'_>) -> Result<(), Exn> {
152 self.prefix()?;
153 writeln!(
154 self.out,
155 "{}",
156 match kind {
157 PeelTo::ValidObject => "Assure the current object exists".to_string(),
158 PeelTo::RecursiveTagObject => "Follow the current annotated tag until an object is found".into(),
159 PeelTo::ObjectKind(kind) => format!("Peel the current object until it is a {kind}"),
160 PeelTo::Path(path) => format!("Lookup the object at '{path}' from the current tree-ish"),
161 }
162 )
163 .ok();
164 Ok(())
165 }
166
167 fn find(&mut self, regex: &BStr, negated: bool) -> Result<(), Exn> {
168 self.prefix()?;
169 self.has_implicit_anchor = true;
170 let negate_text = if negated { "does not match" } else { "matches" };
171 writeln!(
172 self.out,
173 "{}",
174 match self
175 .ref_name
176 .as_ref()
177 .map(ToString::to_string)
178 .or_else(|| self.oid_prefix.map(|p| p.to_string()))
179 {
180 Some(obj_name) => format!(
181 "Follow the ancestry of revision '{obj_name}' until a commit message {negate_text} regex '{regex}'"
182 ),
183 None => format!(
184 "Find the most recent commit from any reference including 'HEAD' that {negate_text} regex '{regex}'"
185 ),
186 }
187 )
188 .ok();
189 Ok(())
190 }
191
192 fn index_lookup(&mut self, path: &BStr, stage: u8) -> Result<(), Exn> {
193 self.prefix()?;
194 self.has_implicit_anchor = true;
195 writeln!(
196 self.out,
197 "Lookup the index at path '{}' stage {} ({})",
198 path,
199 stage,
200 match stage {
201 0 => "base",
202 1 => "ours",
203 2 => "theirs",
204 _ => unreachable!("BUG: parser assures of that"),
205 }
206 )
207 .ok();
208 Ok(())
209 }
210}
211
212impl delegate::Kind for Explain<'_> {
213 fn kind(&mut self, kind: spec::Kind) -> Result<(), Exn> {
214 self.prefix()?;
215 self.call = 0;
216 writeln!(
217 self.out,
218 "Set revision specification to {} mode",
219 match kind {
220 spec::Kind::RangeBetween => "range",
221 spec::Kind::ReachableToMergeBase => "merge-base",
222 spec::Kind::ExcludeReachable => "exclude",
223 spec::Kind::IncludeReachableFromParents => "include parents",
224 spec::Kind::ExcludeReachableFromParents => "exclude parents",
225 spec::Kind::IncludeReachable =>
226 unreachable!("BUG: 'single' mode is implied but cannot be set explicitly"),
227 }
228 )
229 .ok();
230 Ok(())
231 }
232}
233
234impl Delegate for Explain<'_> {
235 fn done(&mut self) -> Result<(), Exn> {
236 if !self.has_implicit_anchor && self.ref_name.is_none() && self.oid_prefix.is_none() {
237 self.err = Some("Incomplete specification lacks its anchor, like a reference or object name".into());
238 }
239 Ok(())
240 }
241}