Skip to main content

gitoxide_core/repository/revision/
explain.rs

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