commit 2261fdb5c472c2a9292c83978a179676d81b2f8c
Author: tri <tri@thac.loan>
Date:   Sat Oct 4 22:00:29 2025 +0700

    use KHOE_FULL_REGEN=1 to force regen objects

diff --git a/Makefile b/Makefile
index a87886d..bb59934 100644
--- a/Makefile
+++ b/Makefile
@@ -17,7 +17,7 @@ deploy: build
 	scp zig-out/bin/khoe root@khoe:/usr/local/bin/
 
 regen: deploy
-	ssh khoe "cd pub/khoe; ~/global-hooks/post-update"
+	ssh khoe "cd pub/khoe; KHOE_FULL_REGEN=1 ~/global-hooks/post-update"
 
 clean:
 	rm -rf zig-out .zig-cache
diff --git a/README.md b/README.md
index fceaed0..9f8919d 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,7 @@ Using [flock][4] ensures that only 1 instance of the script could be running at
 
 ## Quirks
 
-Nothing is cached. Every page is regenerated every time, except for those in /objects/. Computers are fast though so performance hasn't been a problem for me. If it is for you, let me know and we can optimize it.
+Nothing is cached. Every page is regenerated every time, except for those in /objects/ (which can also be forced to regenerate with `KHOE_FULL_REGEN=1`). Computers are fast though so performance hasn't been a problem for me. If it is for you, let me know and we can optimize it.
 
 Worse still, every repo is regenerated every time. This one I'll fix... sometime. Khoe should allow choosing a specific repo to regenerate so that it can run efficiently as a post-update git hook. See Roadmap.
 
diff --git a/src/config.zig b/src/config.zig
new file mode 100644
index 0000000..2fd3c0f
--- /dev/null
+++ b/src/config.zig
@@ -0,0 +1,14 @@
+const std = @import("std");
+const mem = std.mem;
+const posix = std.posix;
+
+pub const Conf = struct {
+    full_regen: bool,
+};
+
+pub fn fromEnv() Conf {
+    const full_regen = posix.getenv("KHOE_FULL_REGEN") orelse "0";
+    return Conf{
+        .full_regen = mem.eql(u8, full_regen, "1"),
+    };
+}
diff --git a/src/main.zig b/src/main.zig
index 21ef889..994fd65 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -10,6 +10,7 @@ const html = @import("html.zig");
 const markdown = @import("markdown.zig");
 const templates = @import("templates.zig");
 const constants = @import("constants.zig");
+const config = @import("config.zig");
 
 const Mode = union(enum) {
     all: void,
@@ -37,6 +38,8 @@ pub fn main() !u8 {
             .{ .single_repo = std.os.argv[3] };
     _ = mode; // TODO
 
+    const conf = config.fromEnv();
+
     var dba_impl: std.heap.DebugAllocator(.{}) = .init;
     defer _ = dba_impl.deinit();
     const dba = dba_impl.allocator();
@@ -116,6 +119,7 @@ pub fn main() !u8 {
             .commits = commits,
             .git_dir_path = git_dir_path,
             .git_dir = git_dir,
+            .conf = conf,
         });
     }
 
@@ -226,6 +230,7 @@ const RepoArgs = struct {
     commits: []git.Commit,
     git_dir_path: []const u8,
     git_dir: fs.Dir,
+    conf: config.Conf,
 };
 
 pub fn processRepo(args: *const RepoArgs) !void {
@@ -528,21 +533,20 @@ pub fn writeCommitPage(
 
     const index_html = "index.html";
 
-    const file_exists = blk: {
-        _ = commit_dir.statFile(index_html) catch |err| switch (err) {
-            error.FileNotFound => break :blk false,
+    var commit_file = commit_dir.createFile(
+        index_html,
+        .{ .exclusive = !args.conf.full_regen },
+    ) catch |err| {
+        switch (err) {
+            error.PathAlreadyExists => return,
             else => return err,
-        };
-        break :blk true;
+        }
     };
-    if (file_exists) return;
-
-    println("   writing {s} commit {s}", .{ args.repo_name, commit.hash[0..10] });
-
-    var commit_file = try commit_dir.createFile(index_html, .{});
     errdefer commit_dir.deleteFile(index_html) catch {};
     defer commit_file.close();
 
+    println("   writing {s} commit {s}", .{ args.repo_name, commit.hash[0..10] });
+
     var buf: [4096]u8 = undefined;
     var file_writer = commit_file.writer(&buf);
     const writer = &file_writer.interface;
@@ -624,30 +628,27 @@ pub fn writeBlobPage(
     var out_dir = try objects_dir.makeOpenPath(src_file.hash, .{});
     defer out_dir.close();
 
-    const index_html = "index.html";
-
-    const should_skip = blk: {
-        _ = out_dir.statFile(index_html) catch |err| {
-            switch (err) {
-                error.FileNotFound => break :blk false,
-                else => return err,
-            }
-        };
-        break :blk true;
-    };
-    if (should_skip) return;
-
-    println("    writing {s} blob: {s}", .{ args.repo_name, src_file.hash });
-
     const object_type = try git.objectType(arena, args.in_repo_dir, src_file.hash);
     if (object_type != .blob) {
         return;
     }
 
-    var file = try out_dir.createFile(index_html, .{});
+    const index_html = "index.html";
+
+    var file = out_dir.createFile(
+        index_html,
+        .{ .exclusive = !args.conf.full_regen },
+    ) catch |err| {
+        switch (err) {
+            error.PathAlreadyExists => return,
+            else => return err,
+        }
+    };
     errdefer out_dir.deleteFile(index_html) catch {};
     defer file.close();
 
+    println("    writing {s} blob: {s}", .{ args.repo_name, src_file.hash });
+
     var buf: [4096]u8 = undefined;
     var file_writer = file.writer(&buf);
     const writer = &file_writer.interface;