╭ Issue.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("../event.zig"); 3 const xit = @import("xit"); 4 const hash = xit.hash; 5 6 title: []const u8, 7 description: []const u8, 8 tags: []const u8, // space-separated 9 status: Status = .open, 10 created_ts: u64 = 0, // the commit timestamp of the event that first created this issue 11 12 pub const Status = enum { 13 open, 14 closed, 15 16 const longest_len = blk: { 17 var len: usize = 0; 18 for (@typeInfo(Status).@"enum".fields) |field| len = @max(len, field.name.len); 19 break :blk len; 20 }; 21 }; 22 23 pub const tag_max_len = 64; 24 25 // a "tag status" key names a tag's per-status issue set (tags can't contain 26 // spaces, so the pair is unambiguous) 27 pub const TagStatusKey = [tag_max_len + 1 + Status.longest_len]u8; 28 29 pub fn tagStatusKey(buffer: *TagStatusKey, tag: []const u8, status: Status) ![]const u8 { 30 return std.fmt.bufPrint(buffer, "{s} {s}", .{ tag, @tagName(status) }); 31 } 32 33 pub fn tagIterator(tags: []const u8) std.mem.SplitIterator(u8, .scalar) { 34 return std.mem.splitScalar(u8, tags, ' '); 35 } 36 37 // `created_ts` comes from the commit timestamp, not the event 38 // payload, so it must not appear in the JSON we output 39 pub fn jsonStringify(self: @This(), jw: anytype) !void { 40 try jw.beginObject(); 41 inline for (std.meta.fields(@This())) |field| { 42 if (comptime std.mem.eql(u8, field.name, "created_ts")) continue; 43 try jw.objectField(field.name); 44 try jw.write(@field(self, field.name)); 45 } 46 try jw.endObject(); 47 } 48 49 pub fn consume( 50 comptime DB: type, 51 comptime hash_kind: hash.HashKind, 52 haxy_moment: DB.HashMap(.read_write), 53 event_id: *const [evt.event_id_size]u8, 54 event_maybe: ?@This(), 55 arena: *std.heap.ArenaAllocator, 56 created_ts: u64, 57 ) !void { 58 const issue_key = hash.hashInt(hash_kind, event_id); 59 60 const event_id_to_issue_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "event-id->issue")); 61 const event_id_to_issue = try DB.HashMap(.read_write).init(event_id_to_issue_cursor); 62 63 // the per-status sets the issues views list, ordered by creation time, 64 // keyed by status name 65 const status_to_issues_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "status->issue-id-set")); 66 const status_to_issues = try DB.SortedMap(.read_write).init(status_to_issues_cursor); 67 68 // the per-tag, per-status sets, keyed "tag,status" 69 const tag_to_issues_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "tag+status->issue-id-set")); 70 const tag_to_issues = try DB.SortedMap(.read_write).init(tag_to_issues_cursor); 71 72 if (event_maybe) |event| { 73 var event_to_write = event; 74 75 const existing_cursor_maybe = try event_id_to_issue.getCursor(issue_key); 76 if (existing_cursor_maybe) |existing_cursor| { 77 // updates preserve the original creation timestamp 78 const existing_issue = try DB.HashMap(.read_only).init(existing_cursor); 79 const existing_event = try evt.read(@This(), DB, hash_kind, arena, existing_issue); 80 event_to_write.created_ts = existing_event.created_ts; 81 82 // drop the old status's and tags' entries; the current ones are re-added below 83 const order_key = evt.orderKeyDesc(existing_event.created_ts, event_id); 84 const status_set = try statusSet(DB, status_to_issues, existing_event.status); 85 _ = try status_set.remove(&order_key); 86 try removeFromTagSets(DB, tag_to_issues, existing_event.tags, existing_event.status, &order_key); 87 } else { 88 // first time we've seen this issue: stamp it with the commit timestamp 89 event_to_write.created_ts = created_ts; 90 } 91 92 const issue_cursor = try event_id_to_issue.putCursor(issue_key); 93 const issue = try DB.HashMap(.read_write).init(issue_cursor); 94 try evt.upsert(@This(), DB, hash_kind, issue, event_to_write); 95 96 const order_key = evt.orderKeyDesc(event_to_write.created_ts, event_id); 97 98 const status_set = try statusSet(DB, status_to_issues, event.status); 99 try status_set.put(&order_key); 100 101 var tag_iter = tagIterator(event.tags); 102 while (tag_iter.next()) |tag| { 103 if (tag.len == 0) continue; 104 if (tag.len > tag_max_len) return error.TagTooLong; 105 var key_buffer: TagStatusKey = undefined; 106 const tag_set_cursor = try tag_to_issues.putCursor(try tagStatusKey(&key_buffer, tag, event.status)); 107 const tag_set = try DB.SortedSet(.read_write).init(tag_set_cursor); 108 try tag_set.put(&order_key); 109 } 110 } else { 111 // drop it from the ordered sets using its recorded creation timestamp and status 112 if (try event_id_to_issue.getCursor(issue_key)) |existing_cursor| { 113 const existing_issue = try DB.HashMap(.read_only).init(existing_cursor); 114 const existing_event = try evt.read(@This(), DB, hash_kind, arena, existing_issue); 115 const order_key = evt.orderKeyDesc(existing_event.created_ts, event_id); 116 117 const status_set = try statusSet(DB, status_to_issues, existing_event.status); 118 _ = try status_set.remove(&order_key); 119 120 try removeFromTagSets(DB, tag_to_issues, existing_event.tags, existing_event.status, &order_key); 121 } 122 if (!try event_id_to_issue.remove(issue_key)) return error.EventNotFound; 123 } 124 } 125 126 // `status`'s sorted set within `statuses`, keyed by status name 127 fn statusSet( 128 comptime DB: type, 129 statuses: DB.SortedMap(.read_write), 130 status: Status, 131 ) !DB.SortedSet(.read_write) { 132 const cursor = try statuses.putCursor(@tagName(status)); 133 return DB.SortedSet(.read_write).init(cursor); 134 } 135 136 // remove an issue's order key from its tags' `status` sets, pruning entries 137 // whose set becomes empty 138 fn removeFromTagSets( 139 comptime DB: type, 140 tag_to_issues: DB.SortedMap(.read_write), 141 tags: []const u8, 142 status: Status, 143 order_key: []const u8, 144 ) !void { 145 var tag_iter = tagIterator(tags); 146 while (tag_iter.next()) |tag| { 147 if (tag.len == 0) continue; 148 var key_buffer: TagStatusKey = undefined; 149 const key = try tagStatusKey(&key_buffer, tag, status); 150 const tag_set_cursor = try tag_to_issues.putCursor(key); 151 const tag_set = try DB.SortedSet(.read_write).init(tag_set_cursor); 152 _ = try tag_set.remove(order_key); 153 if (0 == try tag_set.count()) _ = try tag_to_issues.remove(key); 154 } 155 } 156