1use std::collections::{HashMap, HashSet};
4use std::fs;
5
6use walkdir::WalkDir;
7
8use crate::error::Result;
9use crate::hash::Hash;
10use crate::object::{read_commit, read_tree};
11use crate::refs::{list_refs, list_refs_matching, read_ref};
12use crate::repo::Repo;
13use crate::types::EntryKind;
14
15#[derive(Debug, Default)]
17pub struct RepoStats {
18 pub total_blobs: usize,
19 pub total_trees: usize,
20 pub total_commits: usize,
21 pub total_refs: usize,
22 pub total_blobs_bytes: u64,
23 pub total_trees_bytes: u64,
24 pub total_commits_bytes: u64,
25 pub reachable_blobs: usize,
26 pub reachable_trees: usize,
27 pub reachable_commits: usize,
28 pub unreachable_blobs_bytes: u64,
29}
30
31pub fn stats(repo: &Repo) -> Result<RepoStats> {
33 let mut s = RepoStats::default();
34
35 s.total_refs = list_refs(repo)?.len();
37
38 let (blobs, blob_bytes) = count_objects(&repo.blobs_path());
40 let (trees, tree_bytes) = count_objects(&repo.trees_path());
41 let (commits, commit_bytes) = count_objects(&repo.commits_path());
42
43 s.total_blobs = blobs;
44 s.total_blobs_bytes = blob_bytes;
45 s.total_trees = trees;
46 s.total_trees_bytes = tree_bytes;
47 s.total_commits = commits;
48 s.total_commits_bytes = commit_bytes;
49
50 let mut reachable_blobs = HashSet::new();
52 let mut reachable_trees = HashSet::new();
53 let mut reachable_commits = HashSet::new();
54
55 for ref_name in list_refs(repo)? {
56 let commit_hash = crate::refs::read_ref(repo, &ref_name)?;
57 mark_commit(
58 repo,
59 &commit_hash,
60 &mut reachable_blobs,
61 &mut reachable_trees,
62 &mut reachable_commits,
63 )?;
64 }
65
66 s.reachable_blobs = reachable_blobs.len();
67 s.reachable_trees = reachable_trees.len();
68 s.reachable_commits = reachable_commits.len();
69
70 s.unreachable_blobs_bytes = calculate_unreachable_bytes(&repo.blobs_path(), &reachable_blobs);
72
73 Ok(s)
74}
75
76fn count_objects(dir: &std::path::Path) -> (usize, u64) {
77 if !dir.exists() {
78 return (0, 0);
79 }
80
81 let mut count = 0;
82 let mut bytes = 0;
83
84 for entry in WalkDir::new(dir).min_depth(2).max_depth(2) {
85 if let Ok(entry) = entry {
86 if entry.file_type().is_file() {
87 count += 1;
88 if let Ok(meta) = fs::metadata(entry.path()) {
89 bytes += meta.len();
90 }
91 }
92 }
93 }
94
95 (count, bytes)
96}
97
98fn calculate_unreachable_bytes(dir: &std::path::Path, reachable: &HashSet<Hash>) -> u64 {
99 if !dir.exists() {
100 return 0;
101 }
102
103 let mut bytes = 0;
104
105 for entry in WalkDir::new(dir).min_depth(2).max_depth(2) {
106 if let Ok(entry) = entry {
107 if !entry.file_type().is_file() {
108 continue;
109 }
110
111 let path = entry.path();
112 let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
113 let parent_name = path
114 .parent()
115 .and_then(|p| p.file_name())
116 .and_then(|n| n.to_str())
117 .unwrap_or("");
118
119 let hex = format!("{}{}", parent_name, file_name);
120 if let Ok(hash) = Hash::from_hex(&hex) {
121 if !reachable.contains(&hash) {
122 if let Ok(meta) = fs::metadata(path) {
123 bytes += meta.len();
124 }
125 }
126 }
127 }
128 }
129
130 bytes
131}
132
133fn mark_commit(
135 repo: &Repo,
136 commit_hash: &Hash,
137 reachable_blobs: &mut HashSet<Hash>,
138 reachable_trees: &mut HashSet<Hash>,
139 reachable_commits: &mut HashSet<Hash>,
140) -> Result<()> {
141 if reachable_commits.contains(commit_hash) {
142 return Ok(());
143 }
144 reachable_commits.insert(*commit_hash);
145
146 let commit = read_commit(repo, commit_hash)?;
147 mark_tree(repo, &commit.tree, reachable_blobs, reachable_trees)?;
148
149 for parent in &commit.parents {
150 mark_commit(
151 repo,
152 parent,
153 reachable_blobs,
154 reachable_trees,
155 reachable_commits,
156 )?;
157 }
158
159 Ok(())
160}
161
162fn mark_tree(
164 repo: &Repo,
165 tree_hash: &Hash,
166 reachable_blobs: &mut HashSet<Hash>,
167 reachable_trees: &mut HashSet<Hash>,
168) -> Result<()> {
169 if reachable_trees.contains(tree_hash) {
170 return Ok(());
171 }
172 reachable_trees.insert(*tree_hash);
173
174 let tree = read_tree(repo, tree_hash)?;
175
176 for entry in tree.entries() {
177 match &entry.kind {
178 EntryKind::Regular { hash, .. } => {
179 reachable_blobs.insert(*hash);
180 }
181 EntryKind::Symlink { hash } => {
182 reachable_blobs.insert(*hash);
183 }
184 EntryKind::Directory { hash, .. } => {
185 mark_tree(repo, hash, reachable_blobs, reachable_trees)?;
186 }
187 _ => {}
188 }
189 }
190
191 Ok(())
192}
193
194#[derive(Debug)]
196pub struct RefSize {
197 pub ref_name: String,
198 pub bytes: u64,
199}
200
201pub fn du(repo: &Repo, pattern: Option<&str>) -> Result<Vec<RefSize>> {
204 let blob_sizes = build_blob_size_map(repo)?;
206
207 let mut results = Vec::new();
208
209 let refs = match pattern {
210 Some(p) => list_refs_matching(repo, p)?,
211 None => list_refs(repo)?,
212 };
213
214 for ref_name in refs {
215 let commit_hash = read_ref(repo, &ref_name)?;
216 let commit = read_commit(repo, &commit_hash)?;
217
218 let mut blobs = HashSet::new();
220 collect_tree_blobs(repo, &commit.tree, &mut blobs)?;
221
222 let bytes: u64 = blobs.iter().filter_map(|h| blob_sizes.get(h)).sum();
224
225 results.push(RefSize { ref_name, bytes });
226 }
227
228 results.sort_by(|a, b| b.bytes.cmp(&a.bytes));
230
231 Ok(results)
232}
233
234fn build_blob_size_map(repo: &Repo) -> Result<HashMap<Hash, u64>> {
235 let mut sizes = HashMap::new();
236 let blobs_path = repo.blobs_path();
237
238 if !blobs_path.exists() {
239 return Ok(sizes);
240 }
241
242 for entry in WalkDir::new(&blobs_path).min_depth(2).max_depth(2) {
243 if let Ok(entry) = entry {
244 if !entry.file_type().is_file() {
245 continue;
246 }
247
248 let path = entry.path();
249 let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
250 let parent_name = path
251 .parent()
252 .and_then(|p| p.file_name())
253 .and_then(|n| n.to_str())
254 .unwrap_or("");
255
256 let hex = format!("{}{}", parent_name, file_name);
257 if let Ok(hash) = Hash::from_hex(&hex) {
258 if let Ok(meta) = fs::metadata(path) {
259 sizes.insert(hash, meta.len());
260 }
261 }
262 }
263 }
264
265 Ok(sizes)
266}
267
268fn collect_tree_blobs(repo: &Repo, tree_hash: &Hash, blobs: &mut HashSet<Hash>) -> Result<()> {
269 let tree = read_tree(repo, tree_hash)?;
270
271 for entry in tree.entries() {
272 match &entry.kind {
273 EntryKind::Regular { hash, .. } => {
274 blobs.insert(*hash);
275 }
276 EntryKind::Symlink { hash } => {
277 blobs.insert(*hash);
278 }
279 EntryKind::Directory { hash, .. } => {
280 collect_tree_blobs(repo, hash, blobs)?;
281 }
282 _ => {}
283 }
284 }
285
286 Ok(())
287}