1use std::path::Path;
7
8pub fn dir_size_skipping_git<'a, I: IntoIterator<Item = &'a Path>>(roots: I) -> u64 {
12 let mut total: u64 = 0;
13 #[cfg(unix)]
14 let mut seen_inodes: std::collections::HashSet<(u64, u64)> =
15 std::collections::HashSet::new();
16
17 for root in roots {
18 if !root.exists() {
19 continue;
20 }
21 for entry in jwalk::WalkDir::new(root)
22 .process_read_dir(|_, _, _, children| {
23 children.retain(|child| {
24 child
25 .as_ref()
26 .map(|e| e.file_name().to_string_lossy() != ".git")
27 .unwrap_or(true)
28 });
29 })
30 .into_iter()
31 .flatten()
32 {
33 if let Ok(meta) = std::fs::metadata(entry.path()) {
34 if meta.is_file() {
35 #[cfg(unix)]
36 {
37 use std::os::unix::fs::MetadataExt;
38 if meta.nlink() > 1 && !seen_inodes.insert((meta.dev(), meta.ino())) {
39 continue;
40 }
41 }
42 total += filesize::file_real_size_fast(entry.path(), &meta)
43 .unwrap_or(meta.len());
44 }
45 }
46 }
47 }
48 total
49}
50
51pub fn filesystem_capacity_bytes(path: &Path) -> Option<u64> {
56 use sysinfo::Disks;
57
58 let probe = if path.exists() {
59 path.to_path_buf()
60 } else {
61 path.parent().unwrap_or(Path::new("/")).to_path_buf()
62 };
63
64 let disks = Disks::new_with_refreshed_list();
65 let mut best: Option<(&sysinfo::Disk, usize)> = None;
66 for disk in disks.list() {
67 let mount = disk.mount_point();
68 if probe.starts_with(mount) {
69 let len = mount.as_os_str().len();
70 if best.map_or(true, |(_, cur)| len > cur) {
71 best = Some((disk, len));
72 }
73 }
74 }
75 best.map(|(d, _)| d.total_space())
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use tempfile::TempDir;
82
83 #[test]
84 fn dir_size_skips_git_dir() {
85 let dir = TempDir::new().unwrap();
90 let git = dir.path().join(".git");
91 std::fs::create_dir_all(&git).unwrap();
92 std::fs::write(git.join("HEAD"), vec![0u8; 100_000]).unwrap();
94 std::fs::write(dir.path().join("a.txt"), b"hello").unwrap();
95
96 let size = dir_size_skipping_git([dir.path()].iter().copied());
97 assert!(
98 size < 100_000,
99 "size {size} must exclude the 100KB blob under .git"
100 );
101 assert!(size > 0, "size must count a.txt");
102 }
103
104 #[test]
105 fn dir_size_missing_root_is_zero() {
106 let p = std::path::Path::new("/tmp/definitely-not-here-xyz-1234567890");
107 let size = dir_size_skipping_git([p].iter().copied());
108 assert_eq!(size, 0);
109 }
110
111 #[test]
112 fn filesystem_capacity_is_some_for_tmp() {
113 let cap = filesystem_capacity_bytes(std::env::temp_dir().as_path());
114 assert!(cap.is_some_and(|c| c > 0), "expected positive capacity");
115 }
116}