╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │371 try haxy_moment.put(hash.hashInt(repo_opts.hash, "moment-index"), .{ .uint = try haxy.count() - 1 }); │ │372 │ │373 // consume the event unless it's a merge commit │ │411 - if (repo_event.parent_oids.len <= 1) { │ │412 - var commit_object = try obj.Object(.xit, repo_opts, .full).init(state.readOnly(), io, allocator, &std.fmt.bytesToHex(repo_event.oid, .lower));│ │413 - defer commit_object.deinit(); │ │414 - │ │374 + if (parent_oids.len <= 1) { │ │375 // read the message from the commit │ │376 try commit_object.object_reader.seekTo(commit_object.content.commit.message_position); │ │377 const message = try commit_object.object_reader.interface.allocRemaining(arena.allocator(), .unlimited); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │453 } │ │454 │ │455 // the current object id is now the last one │ │496 - last_object_id_maybe = repo_event.oid; │ │456 + last_object_id_maybe = repo_event_oid; │ │457 │ │458 // prevent any of the data created above from being mutated by future iterations of this loop│ │459 try state.core.db.freeze(); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │644 } │ │645 │ │646 return true; │ │647 +} │ │648 + │ │649 +pub fn RepoEventIterator(comptime repo_opts: rp.RepoOpts(.xit)) type { │ │650 + return struct { │ │651 + const Self = @This(); │ │652 + const DB = rp.Repo(.xit, repo_opts).DB; │ │653 + const db_name = "haxy-repo-events.db"; │ │654 + │ │655 + repo_dir: std.Io.Dir, │ │656 + db_file: std.Io.File, │ │657 + db: *DB, │ │658 + parent_to_children: DB.HashMap(.read_write), │ │659 + // tracking pending parents is necessary because a child can only │ │660 + // be returned after all of its unconsumed parents have been returned │ │661 + child_to_pending_parents: DB.HashMap(.read_write), │ │662 + oid_queue: std.AutoArrayHashMapUnmanaged([hash.byteLen(repo_opts.hash)]u8, void), │ │663 + allocator: std.mem.Allocator, │ │664 + newest_object_id: ?[hash.byteLen(repo_opts.hash)]u8, │ │665 + oldest_object_id: ?[hash.byteLen(repo_opts.hash)]u8, │ │666 + │ │667 + pub fn init( │ │668 + state: rp.Repo(.xit, repo_opts).State(.read_only), │ │669 + io: std.Io, │ │670 + allocator: std.mem.Allocator, │ │671 + haxy_moments: DB.HashMap(.read_only), │ │672 + ref: rf.Ref, │ │673 + ) !Self { │ │674 + const db_file = try state.core.repo_dir.createFile(io, db_name, .{ .truncate = true, .lock = .exclusive, .read = true }); │ │675 + errdefer { │ │676 + db_file.close(io); │ │677 + state.core.repo_dir.deleteFile(io, db_name) catch {}; │ │678 + } │ │679 + │ │680 + const buffer_ptr = try allocator.create(std.Io.Writer.Allocating); │ │681 + errdefer allocator.destroy(buffer_ptr); │ │682 + │ │683 + buffer_ptr.* = std.Io.Writer.Allocating.init(allocator); │ │684 + errdefer buffer_ptr.deinit(); │ │685 + │ │686 + const db_ptr = try allocator.create(DB); │ │687 + errdefer allocator.destroy(db_ptr); │ │688 + db_ptr.* = try DB.init(.{ .io = io, .file = db_file, .buffer = buffer_ptr }); │ │689 + │ │690 + const map = try DB.HashMap(.read_write).init(db_ptr.rootCursor()); │ │691 + │ │692 + const parent_to_children_cursor = try map.putCursor(hash.hashInt(repo_opts.hash, "parent->children")); │ │693 + const parent_to_children = try DB.HashMap(.read_write).init(parent_to_children_cursor); │ │694 + │ │695 + const child_to_pending_parents_cursor = try map.putCursor(hash.hashInt(repo_opts.hash, "child->pending-parents")); │ │696 + const child_to_pending_parents = try DB.HashMap(.read_write).init(child_to_pending_parents_cursor); │ │697 + │ │698 + var self = Self{ │ │699 + .repo_dir = state.core.repo_dir, │ │700 + .db_file = db_file, │ │701 + .db = db_ptr, │ │702 + .parent_to_children = parent_to_children, │ │703 + .child_to_pending_parents = child_to_pending_parents, │ │704 + .oid_queue = .{}, │ │705 + .allocator = allocator, │ │706 + .newest_object_id = null, │ │707 + .oldest_object_id = null, │ │708 + }; │ │709 + errdefer self.deinit(io, allocator); │ │710 + │ │711 + try self.collect(state, io, allocator, haxy_moments, ref); │ │712 + return self; │ │713 + } │ │714 + │ │715 + pub fn deinit(self: *Self, io: std.Io, allocator: std.mem.Allocator) void { │ │716 + self.db_file.close(io); │ │717 + self.db.core.memory.buffer.deinit(); │ │718 + allocator.destroy(self.db.core.memory.buffer); │ │719 + self.repo_dir.deleteFile(io, db_name) catch {}; │ │720 + allocator.destroy(self.db); │ │721 + self.oid_queue.deinit(allocator); │ │722 + } │ │723 + │ │724 + pub fn next(self: *Self) !?[hash.byteLen(repo_opts.hash)]u8 { │ │725 + if (self.oid_queue.count() == 0) return null; │ │726 + │ │727 + // OOM risk: large merge graphs can make this queue grow unbounded │ │728 + const oid = self.oid_queue.keys()[0]; │ │729 + self.oid_queue.swapRemoveAt(0); │ │730 + │ │731 + try self.enqueueChildren(&oid); │ │732 + return oid; │ │733 + } │ │734 + │ │735 + fn collect( │ │736 + self: *Self, │ │737 + state: rp.Repo(.xit, repo_opts).State(.read_only), │ │738 + io: std.Io, │ │739 + allocator: std.mem.Allocator, │ │740 + haxy_moments: DB.HashMap(.read_only), │ │741 + ref: rf.Ref, │ │742 + ) !void { │ │743 + // OOM risk: ObjectIterator keeps its queue and visited/excluded set │ │744 + // in memory, so an extremely large reachable history can grow unbounded │ │745 + var commit_iter = try obj.ObjectIterator(.xit, repo_opts, .full).init(state, io, allocator, .{ .kind = .commit }); │ │746 + defer commit_iter.deinit(); │ │747 + │ │748 + const head_oid = (try rf.readRecur(.xit, repo_opts, state, io, .{ .ref = ref })) orelse return error.OidNotFound; │ │749 + try commit_iter.include(&head_oid); │ │750 + │ │751 + while (try commit_iter.next(allocator)) |commit_object| { │ │752 + defer commit_object.deinit(); │ │753 + │ │754 + var oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; │ │755 + _ = try std.fmt.hexToBytes(&oid, &commit_object.oid); │ │756 + const oid_int = hash.bytesToInt(repo_opts.hash, &oid); │ │757 + │ │758 + // if this object id has already been consumed, prune its ancestry from │ │759 + // the iterator. other queued paths, such as the second parent of a │ │760 + // merge commit, can still be walked until they hit a consumed commit │ │761 + // or run out of history. │ │762 + if (null != try haxy_moments.getCursor(oid_int)) { │ │763 + try commit_iter.exclude(&commit_object.oid); │ │764 + continue; │ │765 + } │ │766 + │ │767 + if (self.newest_object_id == null) self.newest_object_id = oid; │ │768 + self.oldest_object_id = oid; │ │769 + │ │770 + const parent_oids = commit_object.content.commit.metadata.parent_oids orelse return error.ParentOidsNotFound; │ │771 + var pending_parent_count: u64 = 0; │ │772 + for (parent_oids) |*parent_oid| { │ │773 + var parent_oid_bytes: [hash.byteLen(repo_opts.hash)]u8 = undefined; │ │774 + _ = try std.fmt.hexToBytes(&parent_oid_bytes, parent_oid); │ │775 + const parent_oid_int = hash.bytesToInt(repo_opts.hash, &parent_oid_bytes); │ │776 + │ │777 + if (null != try haxy_moments.getCursor(parent_oid_int)) continue; │ │778 + │ │779 + const children_cursor = try self.parent_to_children.putCursor(parent_oid_int); │ │780 + const children = try DB.CountedHashSet(.read_write).init(children_cursor); │ │781 + try children.put(oid_int, .{ .bytes = &oid }); │ │782 + pending_parent_count += 1; │ │783 + } │ │784 + │ │785 + if (pending_parent_count == 0) { │ │786 + try self.oid_queue.put(allocator, oid, {}); │ │787 + } else { │ │788 + try self.child_to_pending_parents.put(oid_int, .{ .uint = pending_parent_count }); │ │789 + } │ │790 + } │ │791 + } │ │792 + │ │793 + fn enqueueChildren(self: *Self, oid: *const [hash.byteLen(repo_opts.hash)]u8) !void { │ │794 + const oid_int = hash.bytesToInt(repo_opts.hash, oid); │ │795 + const children_cursor = try self.parent_to_children.getCursor(oid_int) orelse return; │ │796 + const children = try DB.CountedHashSet(.read_only).init(children_cursor); │ │797 + var children_iter = try children.iterator(); │ │798 + │ │799 + while (try children_iter.next()) |*child_cursor| { │ │800 + const kv_pair = try child_cursor.readKeyValuePair(); │ │801 + const child_oid_int = kv_pair.hash; │ │802 + const pending_parent_cursor = try self.child_to_pending_parents.getCursor(child_oid_int) orelse return error.CursorNotFound;│ │803 + const pending_parent_count = try pending_parent_cursor.readUint(); │ │804 + │ │805 + if (pending_parent_count <= 1) { │ │806 + _ = try self.child_to_pending_parents.remove(child_oid_int); │ │807 + const child_oid = hash.intToBytes(hash.HashInt(repo_opts.hash), child_oid_int); │ │808 + try self.oid_queue.put(self.allocator, child_oid, {}); │ │809 + } else { │ │810 + try self.child_to_pending_parents.put(child_oid_int, .{ .uint = pending_parent_count - 1 }); │ │811 + } │ │812 + } │ │813 + } │ │814 + }; │ │815 } │ │816 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯