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