gitoxide_core/repository/
tree.rs

1use std::{borrow::Cow, io, io::BufWriter};
2
3use anyhow::bail;
4use gix::Tree;
5
6use crate::OutputFormat;
7
8mod entries {
9    use std::collections::VecDeque;
10
11    use gix::{
12        bstr::{BStr, BString, ByteSlice, ByteVec},
13        objs::tree::EntryRef,
14        traverse::tree::visit::Action,
15    };
16
17    use crate::repository::tree::format_entry;
18
19    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
20    #[derive(Default)]
21    pub struct Statistics {
22        pub num_trees: usize,
23        pub num_links: usize,
24        pub num_blobs: usize,
25        pub num_blobs_exec: usize,
26        pub num_submodules: usize,
27        #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
28        pub bytes: Option<u64>,
29        #[cfg_attr(feature = "serde", serde(skip))]
30        pub num_bytes: u64,
31    }
32
33    pub struct Traverse<'repo, 'a> {
34        pub stats: Statistics,
35        repo: Option<&'repo gix::Repository>,
36        out: Option<&'a mut dyn std::io::Write>,
37        path: BString,
38        path_deque: VecDeque<BString>,
39    }
40
41    impl<'repo, 'a> Traverse<'repo, 'a> {
42        pub fn new(repo: Option<&'repo gix::Repository>, out: Option<&'a mut dyn std::io::Write>) -> Self {
43            Traverse {
44                stats: Default::default(),
45                repo,
46                out,
47                path: BString::default(),
48                path_deque: VecDeque::new(),
49            }
50        }
51
52        fn pop_element(&mut self) {
53            if let Some(pos) = self.path.rfind_byte(b'/') {
54                self.path.resize(pos, 0);
55            } else {
56                self.path.clear();
57            }
58        }
59
60        fn push_element(&mut self, name: &BStr) {
61            if name.is_empty() {
62                return;
63            }
64            if !self.path.is_empty() {
65                self.path.push(b'/');
66            }
67            self.path.push_str(name);
68        }
69    }
70
71    impl gix::traverse::tree::Visit for Traverse<'_, '_> {
72        fn pop_back_tracked_path_and_set_current(&mut self) {
73            self.path = self.path_deque.pop_back().unwrap_or_default();
74        }
75
76        fn pop_front_tracked_path_and_set_current(&mut self) {
77            self.path = self.path_deque.pop_front().expect("every parent is set only once");
78        }
79
80        fn push_back_tracked_path_component(&mut self, component: &BStr) {
81            self.push_element(component);
82            self.path_deque.push_back(self.path.clone());
83        }
84
85        fn push_path_component(&mut self, component: &BStr) {
86            self.push_element(component);
87        }
88
89        fn pop_path_component(&mut self) {
90            self.pop_element();
91        }
92
93        fn visit_tree(&mut self, _entry: &EntryRef<'_>) -> Action {
94            self.stats.num_trees += 1;
95            Action::Continue
96        }
97
98        fn visit_nontree(&mut self, entry: &EntryRef<'_>) -> Action {
99            let size = self
100                .repo
101                .and_then(|repo| repo.find_header(entry.oid).map(|h| h.size()).ok());
102            if let Some(out) = &mut self.out {
103                format_entry(out, entry, self.path.as_bstr(), size).ok();
104            }
105            if let Some(size) = size {
106                self.stats.num_bytes += size;
107            }
108
109            use gix::object::tree::EntryKind::*;
110            match entry.mode.kind() {
111                Commit => self.stats.num_submodules += 1,
112                Blob => self.stats.num_blobs += 1,
113                BlobExecutable => self.stats.num_blobs_exec += 1,
114                Link => self.stats.num_links += 1,
115                Tree => unreachable!("BUG"),
116            }
117            Action::Continue
118        }
119    }
120}
121
122#[cfg_attr(not(feature = "serde"), allow(unused_variables))]
123pub fn info(
124    repo: gix::Repository,
125    treeish: Option<&str>,
126    extended: bool,
127    format: OutputFormat,
128    out: impl io::Write,
129    mut err: impl io::Write,
130) -> anyhow::Result<()> {
131    if format == OutputFormat::Human {
132        writeln!(err, "Only JSON is implemented - using that instead")?;
133    }
134
135    let tree = treeish_to_tree(treeish, &repo)?;
136
137    let mut delegate = entries::Traverse::new(extended.then_some(&repo), None);
138    tree.traverse().breadthfirst(&mut delegate)?;
139
140    #[cfg(feature = "serde")]
141    {
142        delegate.stats.bytes = extended.then_some(delegate.stats.num_bytes);
143        serde_json::to_writer_pretty(out, &delegate.stats)?;
144    }
145
146    Ok(())
147}
148
149pub fn entries(
150    repo: gix::Repository,
151    treeish: Option<&str>,
152    recursive: bool,
153    extended: bool,
154    format: OutputFormat,
155    mut out: impl io::Write,
156) -> anyhow::Result<()> {
157    if format != OutputFormat::Human {
158        bail!("Only human output format is supported at the moment");
159    }
160
161    let tree = treeish_to_tree(treeish, &repo)?;
162
163    if recursive {
164        let mut write = BufWriter::new(out);
165        let mut delegate = entries::Traverse::new(extended.then_some(&repo), Some(&mut write));
166        tree.traverse().depthfirst(&mut delegate)?;
167    } else {
168        for entry in tree.iter() {
169            let entry = entry?;
170            format_entry(
171                &mut out,
172                &entry.inner,
173                entry.inner.filename,
174                extended.then(|| entry.id().header().map(|o| o.size())).transpose()?,
175            )?;
176        }
177    }
178
179    Ok(())
180}
181
182fn treeish_to_tree<'repo>(treeish: Option<&str>, repo: &'repo gix::Repository) -> anyhow::Result<Tree<'repo>> {
183    let spec = treeish.map_or_else(|| "@^{tree}".into(), |spec| format!("{spec}^{{tree}}"));
184    Ok(repo.rev_parse_single(spec.as_str())?.object()?.into_tree())
185}
186
187fn format_entry(
188    mut out: impl io::Write,
189    entry: &gix::objs::tree::EntryRef<'_>,
190    filename: &gix::bstr::BStr,
191    size: Option<u64>,
192) -> std::io::Result<()> {
193    use gix::objs::tree::EntryKind::*;
194    write!(
195        out,
196        "{} {}{} ",
197        match entry.mode.kind() {
198            Tree => "TREE",
199            Blob => "BLOB",
200            BlobExecutable => " EXE",
201            Link => "LINK",
202            Commit => "SUBM",
203        },
204        entry.oid,
205        size.map_or_else(|| "".into(), |s| Cow::Owned(format!(" {s}")))
206    )?;
207    out.write_all(filename)?;
208    out.write_all(b"\n")
209}