gitoxide_core/repository/
tree.rs

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