╭ changed ─────────╮ │src/test/event.zig│ ╰──────────────────╯ ╭──────────────────────────────────────╮ │2 const builtin = @import("builtin");│ │3 const xit = @import("xit"); │ │4 const rp = xit.repo; │ │5 +const hash = xit.hash; │ │6 │ │7 test "simple" { │ │8 const io = std.testing.io; │ ╰──────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ │26 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); │ │27 defer allocator.free(work_path); │ │28 │ │28 - var repo = try rp.Repo(.xit, .{ .is_test = true }).init(io, allocator, .{ .path = work_path });│ │29 + const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; │ │30 + const Repo = rp.Repo(.xit, repo_opts); │ │31 + var repo = try Repo.init(io, allocator, .{ .path = work_path }); │ │32 defer repo.deinit(io, allocator); │ │33 + │ │34 + // define test events │ │35 + │ │36 + const AddIssueData = struct { │ │37 + title: []const u8, │ │38 + description: []const u8, │ │39 + tags: []const []const u8, │ │40 + }; │ │41 │ │42 const id_size: usize = 20; │ │43 const AddIssueEvent = struct { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │44 id: [2 * id_size]u8, │ │45 kind: []const u8, │ │35 - data: struct { │ │36 - title: []const u8, │ │37 - description: []const u8, │ │38 - tags: []const []const u8, │ │46 + data: AddIssueData, │ │47 + }; │ │48 + │ │49 + const issues_data = [_]AddIssueData{ │ │50 + .{ │ │51 + .title = "Login form clears password on validation error", │ │52 + .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", │ │53 + .tags = &[_][]const u8{ "bug", "priority-high", "ui" }, │ │54 + }, │ │55 + .{ │ │56 + .title = "Search results ignore archived project filter", │ │57 + .description = "Filtering search results to active projects still returns issues from archived projects. Apply the archived flag before ranking results.",│ │58 + .tags = &[_][]const u8{ "bug", "search", "backend" }, │ │59 + }, │ │60 + .{ │ │61 + .title = "Issue list does not persist selected sort order", │ │62 + .description = "Changing the issue list sort order is lost after refresh. Store the selected sort field and direction with the user's view preferences.", │ │63 + .tags = &[_][]const u8{ "enhancement", "frontend", "preferences" }, │ │64 + }, │ │65 + .{ │ │66 + .title = "Webhook retries stop after transient timeout", │ │67 + .description = "A single gateway timeout marks webhook delivery as failed permanently. Retry transient network errors with exponential backoff.", │ │68 + .tags = &[_][]const u8{ "bug", "webhooks", "reliability" }, │ │69 + }, │ │70 + .{ │ │71 + .title = "CSV export omits labels with commas", │ │72 + .description = "Exported issue rows drop labels that contain commas instead of escaping them. Quote CSV fields according to RFC 4180.", │ │73 + .tags = &[_][]const u8{ "bug", "export", "data-integrity" }, │ │74 }, │ │75 }; │ │76 + │ │77 + // insert issues as commits in the repo │ │78 │ │79 + var prng = std.Random.DefaultPrng.init(std.testing.random_seed); │ │80 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); │ │81 defer json.deinit(); │ │82 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────╮ │45 - // create event as json │ │46 - { │ │47 - var prng = std.Random.DefaultPrng.init(std.testing.random_seed);│ │83 + for (issues_data) |issue_data| { │ │84 + json.clearRetainingCapacity(); │ │85 + │ │86 var entropy: [id_size]u8 = undefined; │ │87 prng.random().bytes(&entropy); │ │88 │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │89 const event = AddIssueEvent{ │ │90 .id = std.fmt.bytesToHex(entropy, .lower), │ │91 .kind = "add-issue", │ │54 - .data = .{ │ │55 - .title = "Login form clears password on validation error", │ │56 - .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.",│ │57 - .tags = &[_][]const u8{ "bug", "priority-high", "ui" }, │ │58 - }, │ │92 + .data = issue_data, │ │93 }; │ │94 │ │95 try std.json.Stringify.value(event, .{}, &json.writer); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 96 + _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" });│ │ 97 } │ │ 98 │ │ 64 - _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); │ │ 99 + // read and parse all of the events from the repo │ │100 + │ │101 + var events: std.ArrayList(AddIssueEvent) = .empty; │ │102 + defer events.deinit(allocator); │ │103 + │ │104 + var event_strs: std.ArrayList([]const u8) = .empty; │ │105 + defer event_strs.deinit(allocator); │ │106 + │ │107 + var arena = std.heap.ArenaAllocator.init(allocator); │ │108 + defer arena.deinit(); │ │109 + │ │110 + const ref_haxy_meta = try repo.readRef(io, .{ .kind = .head, .name = "haxy/meta" }) orelse return error.RefNotFound; │ │111 + var commit_iter = try repo.log(io, allocator, &.{ref_haxy_meta}); │ │112 + defer commit_iter.deinit(); │ │113 + │ │114 + while (try commit_iter.next()) |commit_object| { │ │115 + defer commit_object.deinit(); │ │116 + │ │117 + try commit_object.object_reader.seekTo(commit_object.content.commit.message_position); │ │118 + const message = try commit_object.object_reader.interface.allocRemaining(arena.allocator(), .unlimited); │ │119 + │ │120 + try event_strs.append(allocator, message); │ │121 + } │ │122 + │ │123 + for (0..event_strs.items.len) |i| { │ │124 + const event_str = event_strs.items[event_strs.items.len - i - 1]; │ │125 + const event = try std.json.parseFromSliceLeaky(AddIssueEvent, arena.allocator(), event_str, .{}); │ │126 + try events.append(allocator, event); │ │127 + } │ │128 + │ │129 + // process events into the database │ │130 + │ │131 + for (events.items) |event| { │ │132 + const Ctx = struct { │ │133 + event: AddIssueEvent, │ │134 + │ │135 + pub fn run(ctx: @This(), cursor: *Repo.DB.Cursor(.read_write)) !void { │ │136 + const moment = try Repo.DB.HashMap(.read_write).init(cursor.*); │ │137 + │ │138 + const haxy_views_cursor = try moment.putCursor(hash.hashInt(repo_opts.hash, "haxy-views")); │ │139 + const haxy_views = try Repo.DB.HashMap(.read_write).init(haxy_views_cursor); │ │140 + │ │141 + const issues_cursor = try haxy_views.putCursor(hash.hashInt(repo_opts.hash, "issues")); │ │142 + const issues = try Repo.DB.ArrayList(.read_write).init(issues_cursor); │ │143 + │ │144 + const issue_cursor = try issues.appendCursor(); │ │145 + const issue = try Repo.DB.HashMap(.read_write).init(issue_cursor); │ │146 + │ │147 + try issue.put(hash.hashInt(repo_opts.hash, "id"), .{ .bytes = &ctx.event.id }); │ │148 + try issue.put(hash.hashInt(repo_opts.hash, "title"), .{ .bytes = ctx.event.data.title }); │ │149 + try issue.put(hash.hashInt(repo_opts.hash, "description"), .{ .bytes = ctx.event.data.description }); │ │150 + } │ │151 + }; │ │152 + │ │153 + try repo.core.db_file.lock(io, .exclusive); │ │154 + defer repo.core.db_file.unlock(io); │ │155 + │ │156 + const history = try Repo.DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); │ │157 + try history.appendContext( │ │158 + .{ .slot = try history.getSlot(-1) }, │ │159 + Ctx{ .event = event }, │ │160 + ); │ │161 + } │ │162 } │ │163 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯