commit cdcc684516342490378dcb0561260847181eb1ea
Author: tri <tri@thac.loan>
Date:   Tue Sep 30 22:50:07 2025 +0700

    remove gratuitous dependency on du

diff --git a/README.md b/README.md
index 9f8c49b..76ed89b 100644
--- a/README.md
+++ b/README.md
@@ -18,8 +18,7 @@ zig build -Doptimize=ReleaseSafe
 Runtime dependencies (khoe shells out to these commands at runtime):
 
 - git
-- [cmark][7]
-- [du][6]
+- [cmark][6]
 
 ## Use
 
@@ -85,5 +84,4 @@ You should have received a copy of the GNU Affero General Public License along w
 [3]: https://github.com/theZiz/aha
 [4]: https://manpages.debian.org/trixie/util-linux/flock.1.en.html
 [5]: https://www.chiark.greenend.org.uk/~sgtatham/quasiblog/git-no-forge/#submissions
-[6]: https://manpages.debian.org/trixie/coreutils/du.1.en.html
-[7]: https://github.com/commonmark/cmark
+[6]: https://github.com/commonmark/cmark
diff --git a/src/git.zig b/src/git.zig
index e422aa7..24ee54a 100644
--- a/src/git.zig
+++ b/src/git.zig
@@ -147,12 +147,3 @@ pub fn readFileAlloc(arena: mem.Allocator, dir: fs.Dir, file_name: []const u8) !
 
     return proc.stdout;
 }
-
-pub fn repoSize(arena: mem.Allocator, git_dir: fs.Dir) ![]const u8 {
-    var proc = try std.process.Child.run(.{
-        .allocator = arena,
-        .cwd_dir = git_dir,
-        .argv = &.{ "du", "-h", "-d0", "." },
-    });
-    return proc.stdout[0..std.mem.indexOfAny(u8, proc.stdout, " \t").?];
-}
diff --git a/src/main.zig b/src/main.zig
index 1ed5562..5c8aca4 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -2,7 +2,8 @@ const std = @import("std");
 const fs = std.fs;
 const mem = std.mem;
 const ascii = std.ascii;
-const println = @import("utils.zig").println;
+const utils = @import("utils.zig");
+const println = utils.println;
 const git = @import("git.zig");
 const html = @import("html.zig");
 const markdown = @import("markdown.zig");
@@ -92,7 +93,7 @@ pub fn main() !u8 {
         const commits = try git.getCommits(repo_arena, repo_dir);
         println("Found repo {s}: {d} commits", .{ entry.name, commits.len });
 
-        var git_dir = try repo_dir.openDir(git_dir_path, .{});
+        var git_dir = try repo_dir.openDir(git_dir_path, .{ .iterate = true });
         defer git_dir.close();
 
         const repo_description = try git.getDescription(arena, git_dir);
@@ -290,7 +291,7 @@ pub fn writeRepoPage(args: RepoArgs) !void {
         else
             try std.fmt.bufPrint(&buf, "{s}/{s}", .{ repo_name, args.git_dir_path }),
         try html.escapeAlloc(arena, args.description),
-        try git.repoSize(arena, args.git_dir),
+        try utils.humanReadableSize(arena, try utils.dirSize(arena, args.git_dir)),
     });
 
     // If there's a readme file, include it in repo index:
diff --git a/src/utils.zig b/src/utils.zig
index dbe8266..e6a5508 100644
--- a/src/utils.zig
+++ b/src/utils.zig
@@ -1,5 +1,62 @@
 const std = @import("std");
+const fs = std.fs;
+const mem = std.mem;
+const t = std.testing;
 
 pub fn println(comptime fmt: []const u8, args: anytype) void {
     std.debug.print(fmt ++ "\n", args);
 }
+
+pub fn dirSize(gpa: mem.Allocator, dir: fs.Dir) !u64 {
+    var walker = try dir.walk(gpa);
+    defer walker.deinit();
+
+    var size: u64 = 0;
+    while (try walker.next()) |entry| {
+        if (entry.kind != .sym_link) {
+            size += (try dir.statFile(entry.path)).size;
+        }
+    }
+    return size;
+}
+
+/// Not ideal because it truncates instead of rounding to the nearest value.
+pub fn humanReadableSize(arena: mem.Allocator, n_bytes: u64) ![]const u8 {
+    const units: []const []const u8 = &.{ "B", "KiB", "MiB", "GiB", "TiB" };
+    const multiplier: u64 = 1024;
+    var threshold: u64 = multiplier;
+    var unitIdx: usize = 0;
+
+    for (units, 0..) |_, i| {
+        unitIdx = i;
+        if (n_bytes < threshold) {
+            break;
+        }
+        threshold *= multiplier;
+    }
+
+    return std.fmt.allocPrint(
+        arena,
+        "{d} {s}",
+        .{
+            n_bytes / std.math.pow(u64, 1024, unitIdx),
+            units[unitIdx],
+        },
+    );
+}
+
+test "humanReadableSize" {
+    var arena_impl = std.heap.ArenaAllocator.init(t.allocator_instance.allocator());
+    defer arena_impl.deinit();
+    const arena = arena_impl.allocator();
+
+    try t.expectEqualStrings("12 B", try humanReadableSize(arena, 12));
+    try t.expectEqualStrings("1023 B", try humanReadableSize(arena, 1023));
+    try t.expectEqualStrings("1 KiB", try humanReadableSize(arena, 1024));
+    try t.expectEqualStrings("1 KiB", try humanReadableSize(arena, 1024 + 1023));
+    try t.expectEqualStrings("2 KiB", try humanReadableSize(arena, 1024 + 1024));
+    try t.expectEqualStrings("2 MiB", try humanReadableSize(arena, 1024 * 1024 * 2));
+    try t.expectEqualStrings("3 GiB", try humanReadableSize(arena, 1024 * 1024 * 1024 * 3));
+    try t.expectEqualStrings("4 TiB", try humanReadableSize(arena, 1024 * 1024 * 1024 * 1024 * 4));
+    try t.expectEqualStrings("1024 TiB", try humanReadableSize(arena, 1024 * 1024 * 1024 * 1024 * 1024));
+}