╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 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; 9 const allocator = std.testing.allocator; 10 const temp_dir_name = "temp-event-simple"; 11 12 // create the temp dir 13 const cwd = std.Io.Dir.cwd(); 14 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 15 if (temp_dir_or_err) |*temp_dir| { 16 temp_dir.close(io); 17 try cwd.deleteTree(io, temp_dir_name); 18 } else |_| {} 19 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 20 defer cwd.deleteTree(io, temp_dir_name) catch {}; 21 defer temp_dir.close(io); 22 23 const cwd_path = try std.process.currentPathAlloc(io, allocator); 24 defer allocator.free(cwd_path); 25 26 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 27 defer allocator.free(work_path); 28 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, 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 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", 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 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