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