Skip to main content

zub/ops/
stats.rs

1//! repository statistics
2
3use 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/// repository statistics
16#[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
31/// collect repository statistics
32pub fn stats(repo: &Repo) -> Result<RepoStats> {
33    let mut s = RepoStats::default();
34
35    // count refs
36    s.total_refs = list_refs(repo)?.len();
37
38    // count and measure objects on disk
39    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    // mark reachable objects
51    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    // calculate unreachable blob bytes
71    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
133/// recursively mark a commit and all its reachable objects
134fn 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
162/// recursively mark a tree and all its reachable objects
163fn 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/// ref size entry
195#[derive(Debug)]
196pub struct RefSize {
197    pub ref_name: String,
198    pub bytes: u64,
199}
200
201/// calculate size per ref (disk usage)
202/// optionally filter refs by glob pattern
203pub fn du(repo: &Repo, pattern: Option<&str>) -> Result<Vec<RefSize>> {
204    // first build a map of blob hash -> size on disk
205    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        // collect all blobs reachable from this ref's tree
219        let mut blobs = HashSet::new();
220        collect_tree_blobs(repo, &commit.tree, &mut blobs)?;
221
222        // sum up sizes
223        let bytes: u64 = blobs.iter().filter_map(|h| blob_sizes.get(h)).sum();
224
225        results.push(RefSize { ref_name, bytes });
226    }
227
228    // sort by size descending
229    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}