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};
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}