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