Something went wrong. Try again.
A history browser for git, a replacement for gitk, drawn with Vulkan on Wayland.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The user's own clock: what a commit's time reads where they are.//!//! git hands over each date as seconds since the epoch and the author's//! offset, and the conversion to local time is done here, with//! zig-datetime and the IANA timezone database, rather than by git — so//! that a date is a number grebe can compare and format as it likes, not a//! string fixed when it was read.//!//! The zone is the one `TZ` names, as the C library reads it: unset for//! the machine's own (`/etc/localtime`), empty for UTC, a name under//! `TZDIR`, a path, or a POSIX rule. Anything that cannot be read is UTC,//! with a warning, rather than a failure to start.//!//! Windows has no zone files. The machine's own zone is the one Windows is//! set to, by its IANA name, and a zone `TZ` names is looked up by name;//! the data for both is the copy of the database built into grebe.
const std = @import("std");const Allocator = std.mem.Allocator;const datetime = @import("datetime");const tzdb = datetime.tzdb;const builtin = @import("builtin");
const Clock = @This();
const log = std.log.scoped(.clock);
zone: union(enum) { tz: datetime.TimeZone, rule: datetime.posixtz.Posix, utc,},
/// The zone `env` says the user is in. `env` must outlive the clock, since/// a rule borrows its text.pub fn load(gpa: Allocator, io: std.Io, env: *const std.process.Environ.Map) Clock { return loadTz(gpa, io, env.get("TZ"), env.get("TZDIR")) catch |err| { log.warn("reading the time zone: {t}; showing times in UTC", .{err}); return .{ .zone = .utc }; };}
fn loadTz(gpa: Allocator, io: std.Io, tz: ?[]const u8, tzdir: ?[]const u8) !Clock { if (builtin.os.tag == .windows) return loadWindows(tz); switch (tzdb.system.resolveTz(tz)) { .local => return .{ .zone = .{ .tz = try tzdb.system.loadLocal(io, gpa, null) } }, .path => |path| return .{ .zone = .{ .tz = try tzdb.system.loadLocal(io, gpa, path) } }, .utc => return .{ .zone = .utc }, .rule => |rule| return .{ .zone = .{ .rule = try datetime.posixtz.parse(rule) } }, .name => |name| { const zone = if (tzdir) |dir| tzdb.system.load(io, gpa, dir, name) else blk: { for (tzdb.system.search_directories) |dir| { break :blk tzdb.system.load(io, gpa, dir, name) catch continue; } break :blk error.FileNotFound; }; if (zone) |z| return .{ .zone = .{ .tz = z } } else |err| switch (err) { // Not a zone after all, so a rule that looks like a name. error.FileNotFound => return .{ .zone = .{ .rule = try datetime.posixtz.parse(name) } }, else => return err, } }, }}
fn loadWindows(tz: ?[]const u8) !Clock { switch (tzdb.system.resolveTz(tz)) { .local => return .{ .zone = .{ .tz = try tzdb.windows.loadLocal() } }, .utc => return .{ .zone = .utc }, .rule => |rule| return .{ .zone = .{ .rule = try datetime.posixtz.parse(rule) } }, .name => |name| if (try tzdb.embedded.load(name)) |z| return .{ .zone = .{ .tz = z } } else return .{ .zone = .{ .rule = try datetime.posixtz.parse(name) } }, .path => return error.FileNotFound, }}
pub fn deinit(c: *Clock, gpa: Allocator) void { switch (c.zone) { .tz => |*z| z.deinit(gpa), else => {}, } c.* = undefined;}
/// What the user's clock read at `time`, seconds since the epoch.pub fn local(c: *const Clock, time: i64) datetime.DateTime { switch (c.zone) { .tz => |z| return z.atTimestamp(time), .rule => |r| return shifted(time, r.typeAt(time).offset), .utc => return shifted(time, 0), }}
/// `time` as a clock `offset` seconds east of UTC reads it.pub fn shifted(time: i64, offset: i32) datetime.DateTime { const instant: datetime.Instant = .fromNanoTimeStamp(@as(i128, time + offset) * std.time.ns_per_s); var dt = instant.asDateTime(); dt.offset = offset; return dt;}
/// `time` on the user's clock, to the minute, as the commit list shows it:/// `2026-10-25 13:29`.pub fn short(c: *const Clock, time: i64, buf: *[32]u8) []const u8 { var w: std.Io.Writer = .fixed(buf); c.local(time).format("YYYY-MM-DD HH:mm", &w) catch return ""; return w.buffered();}
const testing = std.testing;
test "times read in UTC" { const c: Clock = .{ .zone = .utc }; var buf: [32]u8 = undefined; try testing.expectEqualStrings("1970-01-01 00:00", c.short(0, &buf)); try testing.expectEqualStrings("2026-09-21 14:13", c.short(1790000000, &buf)); // Before the epoch, as imported histories can be. try testing.expectEqualStrings("1969-12-31 00:00", c.short(-86400, &buf));}
test "times read under a POSIX rule" { const c = try loadTz(testing.allocator, testing.io, "EST5EDT,M3.2.0,M11.1.0", null); var buf: [32]u8 = undefined; // In September, daylight saving time: four hours behind. try testing.expectEqualStrings("2026-09-21 10:13", c.short(1790000000, &buf)); // In January, five. try testing.expectEqualStrings("2026-01-01 07:00", c.short(1767268800, &buf));}
test "an empty TZ is UTC" { const c = try loadTz(testing.allocator, testing.io, "", null); try testing.expect(c.zone == .utc);}
test "a clock at a fixed offset" { var buf: [32]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); try shifted(1790000000, -5 * 3600).format("YYYY-MM-DD HH:mm", &w); try testing.expectEqualStrings("2026-09-21 09:13", w.buffered());}