╭ changed ─────────╮ │src/test/event.zig│ ╰──────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │108 // │ │109 // read and parse all of the events from the repo │ │110 // │ │111 - │ │112 - const Commit = struct { │ │113 - parent_oid: ?[hash.byteLen(repo_opts.hash)]u8, │ │114 - oid: [hash.byteLen(repo_opts.hash)]u8, │ │115 - message: []const u8, │ │116 - │ │117 - fn readCommitsFromRepo(inner_arena: *std.heap.ArenaAllocator, inner_repo: *Repo, branch_name: []const u8) ![]@This() {│ │118 - const inner_allocator = inner_arena.allocator(); │ │119 - │ │120 - var commits: std.ArrayList(@This()) = .empty; │ │121 - │ │122 - const ref = try inner_repo.readRef(io, .{ .kind = .head, .name = branch_name }) orelse return error.RefNotFound; │ │123 - var commit_iter = try inner_repo.log(io, inner_allocator, &.{ref}); │ │124 - │ │125 - // read the message from each commit │ │126 - while (try commit_iter.next()) |commit_object| { │ │127 - try commit_object.object_reader.seekTo(commit_object.content.commit.message_position); │ │128 - const message = try commit_object.object_reader.interface.allocRemaining(inner_allocator, .unlimited); │ │129 - │ │130 - var commit: @This() = .{ │ │131 - .parent_oid = undefined, │ │132 - .oid = undefined, │ │133 - .message = message, │ │134 - }; │ │135 - │ │136 - _ = try std.fmt.hexToBytes(&commit.oid, &commit_object.oid); │ │137 - │ │138 - if (commit_object.content.commit.metadata.parent_oids) |parent_oids| { │ │139 - if (parent_oids.len > 0) { │ │140 - _ = try std.fmt.hexToBytes(&(commit.parent_oid orelse unreachable), &parent_oids[0]); │ │141 - } else { │ │142 - commit.parent_oid = null; │ │143 - } │ │144 - } else { │ │145 - commit.parent_oid = null; │ │146 - } │ │111 │ │148 - try commits.append(inner_allocator, commit); │ │149 - } │ │112 + const events = try readEventsFromRepo(Repo, repo_opts.hash, io, &arena, &repo, "haxy/meta"); │ │113 │ │151 - return commits.items; │ │152 - } │ │153 - }; │ │114 + const first_oid = events[events.len - 1].oid; │ │115 │ │155 - var events: std.ArrayList(evt.RepoEvent(repo_opts.hash)) = .empty; │ │156 - defer events.deinit(allocator); │ │157 - │ │158 - { │ │159 - const commits = try Commit.readCommitsFromRepo(&arena, &repo, "haxy/meta"); │ │160 - │ │161 - // parse commit messages as JSON into event values. │ │162 - // add events in reverse order so the earliest event is first. │ │163 - for (0..commits.len) |i| { │ │164 - const commit = commits[commits.len - i - 1]; │ │165 - const event = try std.json.parseFromSliceLeaky(evt.Event, arena.allocator(), commit.message, .{}); │ │166 - try events.append(allocator, .{ │ │167 - .parent_oid = commit.parent_oid, │ │168 - .oid = commit.oid, │ │169 - .event = event, │ │170 - }); │ │171 - } │ │172 - } │ │173 - │ │174 - const first_oid = events.items[0].oid; │ │175 - │ │116 // │ │117 // consume events into the database │ │118 // │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │119 │ │120 { │ │181 - try evt.consume(Repo.DB, repo_opts.hash, io, &repo.core.db, repo.core.db_file, events.items);│ │121 + try evt.consume(repo_opts, io, allocator, &repo, events); │ │122 │ │123 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly());│ │124 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │198 // read and parse all of the events from the repo │ │199 // │ │200 │ │261 - events.clearRetainingCapacity(); │ │201 + const events2 = try readEventsFromRepo(Repo, repo_opts.hash, io, &arena, &repo, "haxy/meta"); │ │202 │ │263 - { │ │264 - const commits = try Commit.readCommitsFromRepo(&arena, &repo, "haxy/meta"); │ │265 - │ │266 - // parse commit messages as JSON into event values. │ │267 - // add events in reverse order so the earliest event is first. │ │268 - for (0..commits.len) |i| { │ │269 - const commit = commits[commits.len - i - 1]; │ │270 - const event = try std.json.parseFromSliceLeaky(evt.Event, arena.allocator(), commit.message, .{});│ │271 - try events.append(allocator, .{ │ │272 - .parent_oid = commit.parent_oid, │ │273 - .oid = commit.oid, │ │274 - .event = event, │ │275 - }); │ │276 - } │ │277 - } │ │278 - │ │203 // │ │204 // consume events into the database │ │205 // │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │206 │ │207 { │ │284 - try evt.consume(Repo.DB, repo_opts.hash, io, &repo.core.db, repo.core.db_file, events.items);│ │208 + try evt.consume(repo_opts, io, allocator, &repo, events2); │ │209 │ │210 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly());│ │211 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │279 // │ │280 // read and parse all of the events from the repo │ │281 // │ │282 + │ │283 + const events3 = try readEventsFromRepo(Repo, repo_opts.hash, io, &arena, &repo, "haxy/meta"); │ │284 + │ │285 + // │ │286 + // consume events into the database │ │287 + // │ │288 + │ │289 + { │ │290 + try evt.consume(repo_opts, io, allocator, &repo, events3); │ │291 + │ │292 + const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); │ │293 │ │359 - events.clearRetainingCapacity(); │ │294 + // read the moment we just created │ │295 + const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; │ │296 + const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); │ │297 + │ │298 + // get the last object id │ │299 + const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; │ │300 + var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; │ │301 + _ = try last_object_id_cursor.readBytes(&last_object_id); │ │302 + │ │303 + const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; │ │304 + const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); │ │305 + │ │306 + try std.testing.expectEqual(1, try haxy.count()); │ │307 + │ │308 + const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; │ │309 + const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); │ │310 + │ │311 + const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; │ │312 + const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); │ │313 + │ │314 + // get the map of issues │ │315 + const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; │ │316 + const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); │ │317 + │ │318 + // the first issue no longer exists │ │319 + try std.testing.expect(null == try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id))); │ │320 + } │ │321 +} │ │322 + │ │323 +test "merge" { │ │324 + const io = std.testing.io; │ │325 + const allocator = std.testing.allocator; │ │326 + const temp_dir_name = "temp-event-merge"; │ │327 + │ │328 + // create the temp dir │ │329 + const cwd = std.Io.Dir.cwd(); │ │330 + var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); │ │331 + if (temp_dir_or_err) |*temp_dir| { │ │332 + temp_dir.close(io); │ │333 + try cwd.deleteTree(io, temp_dir_name); │ │334 + } else |_| {} │ │335 + var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); │ │336 + defer cwd.deleteTree(io, temp_dir_name) catch {}; │ │337 + defer temp_dir.close(io); │ │338 + │ │339 + const cwd_path = try std.process.currentPathAlloc(io, allocator); │ │340 + defer allocator.free(cwd_path); │ │341 + │ │342 + const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); │ │343 + defer allocator.free(work_path); │ │344 + │ │345 + const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; │ │346 + const Repo = rp.Repo(.xit, repo_opts); │ │347 + var repo = try Repo.init(io, allocator, .{ .path = work_path }); │ │348 + defer repo.deinit(io, allocator); │ │349 + │ │350 + // │ │351 + // define test events │ │352 + // │ │353 + │ │354 + var prng = std.Random.DefaultPrng.init(std.testing.random_seed); │ │355 + │ │356 + const events_to_consume = [_]evt.Event{ │ │357 + .{ │ │358 + .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), │ │359 + .data = .{ │ │360 + .issue = .{ │ │361 + .title = "Login form clears password on validation error", │ │362 + .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", │ │363 + .tags = "bug\x00priority-high\x00ui", │ │364 + }, │ │365 + }, │ │366 + }, │ │367 + .{ │ │368 + .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), │ │369 + .data = .{ │ │370 + .issue = .{ │ │371 + .title = "Search results ignore archived project filter", │ │372 + .description = "Filtering search results to active projects still returns issues from archived projects. Apply the archived flag before ranking results.",│ │373 + .tags = "bug\x00search\x00backend", │ │374 + }, │ │375 + }, │ │376 + }, │ │377 + .{ │ │378 + .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), │ │379 + .data = .{ │ │380 + .issue = .{ │ │381 + .title = "Issue list does not persist selected sort order", │ │382 + .description = "Changing the issue list sort order is lost after refresh. Store the selected sort field and direction with the user's view preferences.", │ │383 + .tags = "enhancement\x00frontend\x00preferences", │ │384 + }, │ │385 + }, │ │386 + }, │ │387 + }; │ │388 │ │389 + // │ │390 + // insert issues as commits in the repo │ │391 + // │ │392 + │ │393 + var arena = std.heap.ArenaAllocator.init(allocator); │ │394 + defer arena.deinit(); │ │395 + │ │396 { │ │362 - const commits = try Commit.readCommitsFromRepo(&arena, &repo, "haxy/meta"); │ │397 + var json: std.Io.Writer.Allocating = .init(std.testing.allocator); │ │398 + defer json.deinit(); │ │399 │ │364 - // parse commit messages as JSON into event values. │ │365 - // add events in reverse order so the earliest event is first. │ │366 - for (0..commits.len) |i| { │ │367 - const commit = commits[commits.len - i - 1]; │ │368 - const event = try std.json.parseFromSliceLeaky(evt.Event, arena.allocator(), commit.message, .{}); │ │369 - try events.append(allocator, .{ │ │370 - .parent_oid = commit.parent_oid, │ │371 - .oid = commit.oid, │ │372 - .event = event, │ │373 - }); │ │400 + for (events_to_consume) |event| { │ │401 + json.clearRetainingCapacity(); │ │402 + │ │403 + try std.json.Stringify.value(event, .{}, &json.writer); │ │404 + │ │405 + // commit the event into a special branch │ │406 + _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); │ │407 } │ │408 } │ │409 + │ │410 + // │ │411 + // read and parse all of the events from the repo │ │412 + // │ │413 │ │414 + const events = try readEventsFromRepo(Repo, repo_opts.hash, io, &arena, &repo, "haxy/meta"); │ │415 + │ │416 // │ │417 // consume events into the database │ │418 // │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │419 │ │420 { │ │382 - try evt.consume(Repo.DB, repo_opts.hash, io, &repo.core.db, repo.core.db_file, events.items);│ │421 + try evt.consume(repo_opts, io, allocator, &repo, events); │ │422 │ │423 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly());│ │424 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │445 // get the map of issues │ │446 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; │ │447 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); │ │448 + │ │449 + // get the issue out of the map │ │450 + var first_issue_id: [evt.event_id_size]u8 = undefined; │ │451 + _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume[0].id); │ │452 + const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; │ │453 + const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); │ │454 + const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); │ │455 + │ │456 + try std.testing.expectEqualStrings(events_to_consume[0].data.issue.description, first_issue.issue.description); │ │457 + try std.testing.expectEqualStrings(events_to_consume[0].data.issue.tags, first_issue.issue.tags); │ │458 + } │ │459 + │ │460 + // │ │461 + // define more test events │ │462 + // │ │463 │ │410 - // the first issue no longer exists │ │411 - try std.testing.expect(null == try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id))); │ │464 + const other_events_to_consume = [_]evt.Event{ │ │465 + .{ │ │466 + .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), │ │467 + .data = .{ │ │468 + .issue = .{ │ │469 + .title = "Kanban card status badge falls behind after drag", │ │470 + .description = "Moving an issue between columns updates the card position immediately, but the status badge keeps showing the previous state until refresh.",│ │471 + .tags = "bug\x00kanban\x00frontend", │ │472 + }, │ │473 + }, │ │474 + }, │ │475 + .{ │ │476 + .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), │ │477 + .data = .{ │ │478 + .issue = .{ │ │479 + .title = "Assignee autocomplete omits recently invited users", │ │480 + .description = "Users invited during the current session do not appear in the assignee picker until the project page is reloaded.", │ │481 + .tags = "bug\x00assignees\x00api", │ │482 + }, │ │483 + }, │ │484 + }, │ │485 + .{ │ │486 + .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), │ │487 + .data = .{ │ │488 + .issue = .{ │ │489 + .title = "Add due date warning for issues blocked by dependencies", │ │490 + .description = "Show a warning when an issue's due date is earlier than an unresolved blocking issue so planners can adjust the schedule.", │ │491 + .tags = "enhancement\x00planning\x00dependencies", │ │492 + }, │ │493 + }, │ │494 + }, │ │495 + }; │ │496 + │ │497 + // │ │498 + // insert issues as commits in the repo │ │499 + // │ │500 + │ │501 + { │ │502 + var json: std.Io.Writer.Allocating = .init(std.testing.allocator); │ │503 + defer json.deinit(); │ │504 + │ │505 + for (other_events_to_consume, 0..) |event, i| { │ │506 + json.clearRetainingCapacity(); │ │507 + │ │508 + try std.json.Stringify.value(event, .{}, &json.writer); │ │509 + │ │510 + // commit the event into a special branch │ │511 + _ = try repo.commitAtRef( │ │512 + io, │ │513 + allocator, │ │514 + // make the first commit a child of the first commit on the haxy/meta branch │ │515 + .{ .parent_oids = if (i == 0) &.{std.fmt.bytesToHex(events[events.len - 1].oid, .lower)} else null, .message = json.written() }, │ │516 + null, │ │517 + .{ .kind = .head, .name = "haxy/other" }, │ │518 + ); │ │519 + } │ │520 + } │ │521 + │ │522 + // │ │523 + // check out the haxy/meta branch and merge the haxy/other branch into it │ │524 + // │ │525 + │ │526 + { │ │527 + var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "haxy/meta" } } }); │ │528 + defer result.deinit(); │ │529 + │ │530 + const head = (try repo.readRef(io, .{ .kind = .head, .name = "haxy/meta" })).?; │ │531 + try std.testing.expectEqualStrings(&std.fmt.bytesToHex(events[0].oid, .lower), &head); │ │532 + │ │533 + var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = .{ .kind = .head, .name = "haxy/other" } }} } } }, null); │ │534 + defer merge.deinit(); │ │535 + │ │536 + try std.testing.expect(.success == merge.result); │ │537 + } │ │538 + │ │539 + // │ │540 + // read and parse all of the events from the repo │ │541 + // │ │542 + │ │543 + const events2 = try readEventsFromRepo(Repo, repo_opts.hash, io, &arena, &repo, "haxy/meta"); │ │544 + │ │545 + // │ │546 + // consume events into the database │ │547 + // │ │548 + │ │549 + { │ │550 + try evt.consume(repo_opts, io, allocator, &repo, events2); │ │551 + │ │552 + const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); │ │553 + │ │554 + // read the moment we just created │ │555 + const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; │ │556 + const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); │ │557 + │ │558 + // get the last object id │ │559 + const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; │ │560 + var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; │ │561 + _ = try last_object_id_cursor.readBytes(&last_object_id); │ │562 + │ │563 + const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; │ │564 + const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); │ │565 + │ │566 + try std.testing.expectEqual(2, try haxy.count()); │ │567 + │ │568 + const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; │ │569 + const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); │ │570 + │ │571 + const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; │ │572 + const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); │ │573 + │ │574 + // get the map of issues │ │575 + const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; │ │576 + const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); │ │577 + │ │578 + // make sure one of the original issues is still there │ │579 + { │ │580 + // get the issue out of the map │ │581 + var first_issue_id: [evt.event_id_size]u8 = undefined; │ │582 + _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume[0].id); │ │583 + const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; │ │584 + const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); │ │585 + const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); │ │586 + │ │587 + try std.testing.expectEqualStrings(events_to_consume[0].data.issue.description, first_issue.issue.description); │ │588 + try std.testing.expectEqualStrings(events_to_consume[0].data.issue.tags, first_issue.issue.tags); │ │589 + } │ │590 + │ │591 + // make sure one of the new issues is there │ │592 + { │ │593 + // get the issue out of the map │ │594 + var first_issue_id: [evt.event_id_size]u8 = undefined; │ │595 + _ = try std.fmt.hexToBytes(&first_issue_id, &other_events_to_consume[0].id); │ │596 + const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; │ │597 + const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); │ │598 + const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); │ │599 + │ │600 + try std.testing.expectEqualStrings(other_events_to_consume[0].data.issue.description, first_issue.issue.description); │ │601 + try std.testing.expectEqualStrings(other_events_to_consume[0].data.issue.tags, first_issue.issue.tags); │ │602 + } │ │603 } │ │604 } │ │605 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │606 +fn readEventsFromRepo( │ │607 + comptime Repo: type, │ │608 + comptime hash_kind: hash.HashKind, │ │609 + io: std.Io, │ │610 + arena: *std.heap.ArenaAllocator, │ │611 + repo: *Repo, │ │612 + branch_name: []const u8, │ │613 +) ![]evt.RepoEvent(hash_kind) { │ │614 + const allocator = arena.allocator(); │ │615 + │ │616 + var events: std.ArrayList(evt.RepoEvent(hash_kind)) = .empty; │ │617 + │ │618 + const ref = try repo.readRef(io, .{ .kind = .head, .name = branch_name }) orelse return error.RefNotFound; │ │619 + var commit_iter = try repo.log(io, allocator, &.{ref}); │ │620 + │ │621 + // read the message from each commit │ │622 + while (try commit_iter.next()) |commit_object| { │ │623 + var oid: [hash.byteLen(hash_kind)]u8 = undefined; │ │624 + _ = try std.fmt.hexToBytes(&oid, &commit_object.oid); │ │625 + │ │626 + const parent_oids = commit_object.content.commit.metadata.parent_oids orelse return error.ParentOidsNotFound;│ │627 + │ │628 + if (parent_oids.len > 1) { │ │629 + // this is a merge commit │ │630 + try events.append(allocator, .{ .parent_oid = null, .oid = oid, .event = null }); │ │631 + } else { │ │632 + try commit_object.object_reader.seekTo(commit_object.content.commit.message_position); │ │633 + const message = try commit_object.object_reader.interface.allocRemaining(allocator, .unlimited); │ │634 + const event = try std.json.parseFromSliceLeaky(evt.Event, allocator, message, .{}); │ │635 + │ │636 + if (parent_oids.len == 0) { │ │637 + try events.append(allocator, .{ .parent_oid = null, .oid = oid, .event = event }); │ │638 + } else { │ │639 + var parent_oid: [hash.byteLen(hash_kind)]u8 = undefined; │ │640 + _ = try std.fmt.hexToBytes(&parent_oid, &parent_oids[0]); │ │641 + try events.append(allocator, .{ .parent_oid = parent_oid, .oid = oid, .event = event }); │ │642 + } │ │643 + } │ │644 + } │ │645 + │ │646 + return events.items; │ │647 +} │ │648 + │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯