╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭──────────────────────────────────────╮ │1 const std = @import("std"); │ │2 +const xit = @import("xit"); │ │3 +const rp = xit.repo; │ │4 +const hash = xit.hash; │ │5 │ │3 -pub const id_size: usize = 32; │ │6 +const event_id_size: usize = 32; │ │7 │ │8 pub const EventData = union(enum) {│ │9 issue: struct { │ ╰──────────────────────────────────────╯ ╭──────────────────────────────────╮ │14 }; │ │15 │ │16 pub const Event = struct { │ │14 - id: [id_size * 2]u8, │ │17 + id: [event_id_size * 2]u8,│ │18 data: EventData, │ │19 }; │ │20 │ ╰──────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────╮ │18 -pub fn randomId(random: std.Random) [id_size]u8 { │ │19 - var id_bytes: [id_size]u8 = undefined; │ │21 +/// associates an Event with a git object id. │ │22 +/// we save this oid in the database so we can reliably re-consume│ │23 +/// events if their oid changed. this can happen if the branch │ │24 +/// is rebased and force-pushed. │ │25 +pub fn RepoEvent(comptime hash_kind: hash.HashKind) type { │ │26 + return struct { │ │27 + oid: [hash.byteLen(hash_kind)]u8, │ │28 + event: Event, │ │29 + }; │ │30 +} │ │31 + │ │32 +pub fn randomId(random: std.Random) [event_id_size]u8 { │ │33 + var id_bytes: [event_id_size]u8 = undefined; │ │34 random.bytes(&id_bytes); │ │35 return id_bytes; │ │36 } │ ╰──────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 37 + │ │ 38 +pub fn consume( │ │ 39 + comptime DB: type, │ │ 40 + comptime hash_kind: hash.HashKind, │ │ 41 + io: std.Io, │ │ 42 + db: *DB, │ │ 43 + db_file: std.Io.File, │ │ 44 + repo_events: []RepoEvent(hash_kind), │ │ 45 +) !void { │ │ 46 + const Ctx = struct { │ │ 47 + repo_events: []RepoEvent(hash_kind), │ │ 48 + │ │ 49 + pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { │ │ 50 + const moment = try DB.HashMap(.read_write).init(cursor.*); │ │ 51 + │ │ 52 + // the map with all of haxy's state and materialized views │ │ 53 + const haxy_cursor = try moment.putCursor(hash.hashInt(hash_kind, "haxy")); │ │ 54 + const haxy = try DB.HashMap(.read_write).init(haxy_cursor); │ │ 55 │ │ 56 + // try reading the last object id that was consumed │ │ 57 + var last_object_id_maybe: ?[hash.byteLen(hash_kind)]u8 = null; │ │ 58 + if (try haxy.getCursor(hash.hashInt(hash_kind, "last-object-id"))) |last_object_id_cursor| { │ │ 59 + var last_object_id_buffer: [hash.byteLen(hash_kind)]u8 = undefined; │ │ 60 + _ = try last_object_id_cursor.readBytes(&last_object_id_buffer); │ │ 61 + last_object_id_maybe = last_object_id_buffer; │ │ 62 + } │ │ 63 + │ │ 64 + // for each event we want to consume... │ │ 65 + for (ctx.repo_events) |repo_event| { │ │ 66 + // map with the views as they appeared when each event was consumed. │ │ 67 + // we can use this to see (and revert) the views to any previous state. │ │ 68 + const object_id_to_views_cursor = try haxy.putCursor(hash.hashInt(hash_kind, "object-id->views")); │ │ 69 + const object_id_to_views = try DB.HashMap(.read_write).init(object_id_to_views_cursor); │ │ 70 + │ │ 71 + // create a new views map for the current event we are consuming │ │ 72 + var views_cursor = try object_id_to_views.putCursor(hash.bytesToInt(hash_kind, &repo_event.oid)); │ │ 73 + │ │ 74 + // if there was a previous event, set the views map to have the same value as it. │ │ 75 + // this efficiently "clones" the map so we make further modifications based on it. │ │ 76 + if (last_object_id_maybe) |*last_object_id| { │ │ 77 + if (try object_id_to_views.getCursor(hash.bytesToInt(hash_kind, last_object_id))) |last_view_cursor| { │ │ 78 + try views_cursor.write(.{ .slot = last_view_cursor.slot() }); │ │ 79 + } │ │ 80 + } │ │ 81 + │ │ 82 + // consume the event into the views map │ │ 83 + { │ │ 84 + const event = repo_event.event; │ │ 85 + │ │ 86 + // get the id of the current event as bytes │ │ 87 + var current_event_id: [event_id_size]u8 = undefined; │ │ 88 + _ = try std.fmt.hexToBytes(&current_event_id, &event.id); │ │ 89 + │ │ 90 + const views = try DB.HashMap(.read_write).init(views_cursor); │ │ 91 + │ │ 92 + switch (event.data) { │ │ 93 + .issue => |data| { │ │ 94 + const event_id_to_issue_cursor = try views.putCursor(hash.hashInt(hash_kind, "event-id->issue"));│ │ 95 + const event_id_to_issue = try DB.HashMap(.read_write).init(event_id_to_issue_cursor); │ │ 96 + │ │ 97 + const issue_cursor = try event_id_to_issue.putCursor(hash.hashInt(hash_kind, &current_event_id));│ │ 98 + const issue = try DB.HashMap(.read_write).init(issue_cursor); │ │ 99 + │ │100 + try putIfDifferent(issue, hash.hashInt(hash_kind, "title"), data.title); │ │101 + try putIfDifferent(issue, hash.hashInt(hash_kind, "description"), data.description); │ │102 + }, │ │103 + } │ │104 + } │ │105 + │ │106 + // the current object id is now the last one │ │107 + last_object_id_maybe = repo_event.oid; │ │108 + │ │109 + // prevent any of the data created above from being mutated by future iterations of this loop │ │110 + try cursor.db.freeze(); │ │111 + } │ │112 + │ │113 + if (last_object_id_maybe) |*last_object_id| { │ │114 + try haxy.put(hash.hashInt(hash_kind, "last-object-id"), .{ .bytes = last_object_id }); │ │115 + } │ │116 + } │ │117 + │ │118 + fn putIfDifferent(map: DB.HashMap(.read_write), key: hash.HashInt(hash_kind), value: []const u8) !void { │ │119 + if (try map.getCursor(key)) |value_cursor| { │ │120 + var buffer: [4096]u8 = undefined; │ │121 + const existing_value = try value_cursor.readBytes(&buffer); │ │122 + if (std.mem.eql(u8, existing_value, value)) { │ │123 + return; │ │124 + } │ │125 + } │ │126 + │ │127 + try map.put(key, .{ .bytes = value }); │ │128 + } │ │129 + }; │ │130 + │ │131 + try db_file.lock(io, .exclusive); │ │132 + defer db_file.unlock(io); │ │133 + │ │134 + // create a new transaction in the database that runs the above-defined Ctx function │ │135 + const history = try DB.ArrayList(.read_write).init(db.rootCursor()); │ │136 + try history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ .repo_events = repo_events }); │ │137 +} │ │138 + │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/test/event.zig│ ╰──────────────────╯ ╭─────────────────────────────────────────────────────────────────╮ │40 │ │41 const first_event_id = evt.randomId(prng.random()); │ │42 │ │43 - const events_to_process = [_]evt.Event{ │ │43 + const events_to_consume = [_]evt.Event{ │ │44 .{ │ │45 .id = std.fmt.bytesToHex(first_event_id, .lower),│ │46 .data = .{ │ ╰─────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │ 95 var json: std.Io.Writer.Allocating = .init(std.testing.allocator);│ │ 96 defer json.deinit(); │ │ 97 │ │ 98 - for (events_to_process) |event| { │ │ 98 + for (events_to_consume) |event| { │ │ 99 json.clearRetainingCapacity(); │ │100 │ │101 try std.json.Stringify.value(event, .{}, &json.writer); │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────╮ │109 // read and parse all of the events from the repo │ │110 // │ │111 │ │112 - var events: std.ArrayList(evt.Event) = .empty; │ │112 + var events: std.ArrayList(evt.RepoEvent(repo_opts.hash)) = .empty;│ │113 defer events.deinit(allocator); │ │114 │ │115 { │ ╰───────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │116 - var event_strs: std.ArrayList([]const u8) = .empty; │ │117 - defer event_strs.deinit(allocator); │ │116 + const Commit = struct { │ │117 + oid: [hash.byteLen(repo_opts.hash)]u8, │ │118 + message: []const u8, │ │119 + }; │ │120 + var commits: std.ArrayList(Commit) = .empty; │ │121 + defer commits.deinit(allocator); │ │122 │ │123 const ref_haxy_meta = try repo.readRef(io, .{ .kind = .head, .name = "haxy/meta" }) orelse return error.RefNotFound;│ │124 var commit_iter = try repo.log(io, allocator, &.{ref_haxy_meta}); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │131 try commit_object.object_reader.seekTo(commit_object.content.commit.message_position); │ │132 const message = try commit_object.object_reader.interface.allocRemaining(arena.allocator(), .unlimited);│ │133 │ │130 - try event_strs.append(allocator, message); │ │134 + var commit: Commit = .{ │ │135 + .oid = undefined, │ │136 + .message = message, │ │137 + }; │ │138 + _ = try std.fmt.hexToBytes(&commit.oid, &commit_object.oid); │ │139 + │ │140 + try commits.append(allocator, commit); │ │141 } │ │142 │ │143 // parse commit messages as JSON into event values. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │144 // add events in reverse order so the earliest event is first. │ │135 - for (0..event_strs.items.len) |i| { │ │136 - const event_str = event_strs.items[event_strs.items.len - i - 1]; │ │137 - const event = try std.json.parseFromSliceLeaky(evt.Event, arena.allocator(), event_str, .{}); │ │138 - try events.append(allocator, event); │ │145 + for (0..commits.items.len) |i| { │ │146 + const commit = commits.items[commits.items.len - i - 1]; │ │147 + const event = try std.json.parseFromSliceLeaky(evt.Event, arena.allocator(), commit.message, .{});│ │148 + try events.append(allocator, .{ .oid = commit.oid, .event = event }); │ │149 } │ │150 } │ │151 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯