const std = @import("std"); pub const Entry = struct { job_name: []const u8, commit_id: []const u8, queued_at: i64, }; fn historyDir(allocator: std.mem.Allocator) ![]const u8 { const home = std.posix.getenv("HOME") orelse return error.NoHomeDir; return std.fmt.allocPrint(allocator, "{s}/.ml", .{home}); } fn historyPath(allocator: std.mem.Allocator) ![]const u8 { const dir = try historyDir(allocator); defer allocator.free(dir); return std.fmt.allocPrint(allocator, "{s}/history.log", .{dir}); } pub fn record(allocator: std.mem.Allocator, job_name: []const u8, commit_id: []const u8) !void { const dir = try historyDir(allocator); defer allocator.free(dir); std.fs.makeDirAbsolute(dir) catch |err| { if (err != error.PathAlreadyExists) return err; }; const path = try historyPath(allocator); defer allocator.free(path); var file = std.fs.openFileAbsolute(path, .{ .mode = .read_write }) catch |err| switch (err) { error.FileNotFound => try std.fs.createFileAbsolute(path, .{}), else => return err, }; defer file.close(); // Append at end of file try file.seekFromEnd(0); const ts = std.time.timestamp(); // Format one line into a temporary buffer const line = try std.fmt.allocPrint( allocator, "{d}\t{s}\t{s}\n", .{ ts, job_name, commit_id }, ); defer allocator.free(line); try file.writeAll(line); } pub fn loadEntries(allocator: std.mem.Allocator) ![]Entry { const path = historyPath(allocator) catch |err| switch (err) { error.NoHomeDir => return error.NoHomeDir, else => return err, }; defer allocator.free(path); const file = std.fs.openFileAbsolute(path, .{}) catch |err| switch (err) { error.FileNotFound => return &.{}, else => return err, }; defer file.close(); const contents = try file.readToEndAlloc(allocator, 1024 * 1024); defer allocator.free(contents); var entries = std.ArrayListUnmanaged(Entry){}; defer entries.deinit(allocator); var it = std.mem.splitScalar(u8, contents, '\n'); while (it.next()) |line_full| { const line = std.mem.trim(u8, line_full, " \t\r"); if (line.len == 0) continue; var parts = std.mem.splitScalar(u8, line, '\t'); const ts_str = parts.next() orelse continue; const job = parts.next() orelse continue; const commit = parts.next() orelse continue; const ts = std.fmt.parseInt(i64, ts_str, 10) catch continue; const job_dup = try allocator.dupe(u8, job); const commit_dup = try allocator.dupe(u8, commit); try entries.append(allocator, Entry{ .job_name = job_dup, .commit_id = commit_dup, .queued_at = ts, }); } return try entries.toOwnedSlice(allocator); } pub fn freeEntries(allocator: std.mem.Allocator, entries: []Entry) void { for (entries) |entry| { allocator.free(entry.job_name); allocator.free(entry.commit_id); } allocator.free(entries); }