gitoxide_core/repository/revision/
explain.rs

1use 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}