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