gitoxide_core/repository/
tree.rs1use 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}