╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Patch.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
17 18 pub const Revision = struct { 19 id: [evt.event_id_size * 2]u8, 20 squash_oid: []const u8, 21 source_oid: []const u8, 22 commit_count: ?u64 = null, 23 24 pub fn fromRecord(id: [evt.event_id_size]u8, record: evt.PatchRev.Record) Revision { 25 return .{ 26 .id = std.fmt.bytesToHex(id, .lower), 27 .squash_oid = record.patch_oid, 28 .source_oid = record.event.source_oid, 29 .commit_count = record.commit_count, 30 }; 31 } 32 33 pub fn matches(self: Revision, record: evt.PatchRev.Record) bool { 34 return !record.removed and 35 std.mem.eql(u8, record.patch_oid, self.squash_oid) and 36 std.mem.eql(u8, record.event.source_oid, self.source_oid); 37 } 38 }; 39 40 pub const MergeRevision = enum { squash, source }; 41 42 pub const Record = struct { 43 event: Self, 44 removed: bool = false, 45 author_email: ?[]const u8 = null, 46 created_order: u64 = 0, 47 updated_order: u64 = 0, 48 }; 49 50 const Self = @This(); 51 52 pub const StatusKind = enum { 53 open, 54 closed, 55 merged, 56 57 const longest_len = blk: { 58 var len: usize = 0; 59 for (@typeInfo(StatusKind).@"enum".fields) |field| len = @max(len, field.name.len); 60 break :blk len; 61 }; 62 }; 63 64 pub const Status = union(StatusKind) { 65 open, 66 closed, 67 merged: Merged, 68 69 pub const Merged = struct { 70 revision: MergeRevision, 71 patchrev_id: [evt.event_id_size * 2]u8, 72 }; 73 74 pub fn kind(self: Status) StatusKind { 75 return std.meta.activeTag(self); 76 } 77 }; 78 79 pub const Resolve = struct { 80 title: ?[]const u8 = null, 81 tags: ?[]const u8 = null, 82 hunks: []const []const u8 = &.{}, 83 theirs: []const u8 = "", 84 }; 85 86 pub const Update = union(enum) { 87 status: StatusKind, 88 fields: struct { title: []const u8, tags: []const u8, description: []const u8, target_branch: []const u8 }, 89 resolve: Resolve, 90 }; 91 92 pub const tag_max_len = 64; 93 pub const merge_policy: evt.MergePolicy = .field_conflicts; 94 pub const record_map_key = "event-id->patch"; 95 pub const all_id_set_key = "patch-id-set"; 96 pub const conflicts_key = "conflicted-patch-id->conflict"; 97 pub const id_to_field_to_oid_key = "patch-id->field->oid"; 98 pub const target_patch_id_to_patch_id_set_key = "target-patch-id->patch-id-set"; 99 pub const status_to_id_set_key = "status->patch-id-set"; 100 pub const tag_status_to_id_set_key = "tag+status->patch-id-set"; 101 pub const revision_to_id_set_key = "target-branch+oid->patch-id-set"; 102 pub const source_to_id_set_key = "source-branch->patch-id-set"; 103 104 pub const TagStatusKey = [tag_max_len + 1 + StatusKind.longest_len]u8; 105 106 pub fn tagStatusKey(buffer: *TagStatusKey, tag: []const u8, status: StatusKind) ![]const u8 { 107 return std.fmt.bufPrint(buffer, "{s} {s}", .{ tag, @tagName(status) }); 108 } 109 110 pub fn tagIterator(tags: []const u8) std.mem.SplitIterator(u8, .scalar) { 111 return std.mem.splitScalar(u8, tags, ' '); 112 } 113 114 pub fn fieldsValid(title: []const u8, tags: []const u8) bool { 115 if (!evt.titleValid(title)) return false; 116 var tag_iter = tagIterator(tags); 117 while (tag_iter.next()) |tag| { 118 if (tag.len > tag_max_len) return false; 119 } 120 return true; 121 } 122 123 pub fn branchValid(branch: []const u8) bool { 124 return rf.validateName(branch) and !std.mem.eql(u8, branch, evt.events_ref.name); 125 } 126 127 pub fn consume( 128 comptime DB: type, 129 comptime hash_kind: hash.HashKind, 130 haxy_moment: DB.HashMap(.read_write), 131 event_id: *const [evt.event_id_size]u8, 132 record_maybe: ?Record, 133 arena: *std.heap.ArenaAllocator, 134 event_oid: ?[]const u8, 135 ) !void { 136 const record_key = hash.hashInt(hash_kind, event_id); 137 const records = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, record_map_key))); 138 const statuses = try DB.SortedMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, status_to_id_set_key))); 139 const tag_statuses = try DB.SortedMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, tag_status_to_id_set_key))); 140 const conflicts = try DB.SortedMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, conflicts_key))); 141 const field_oid_map = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, id_to_field_to_oid_key))); 142 143 var existing_maybe: ?Record = null; 144 const existing_cursor_maybe = try records.getCursor(record_key); 145 if (existing_cursor_maybe) |cursor| { 146 existing_maybe = try evt.read(Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(cursor)); 147 } 148 149 var record = record_maybe orelse try evt.removedRecord(Record, DB, hash_kind, haxy_moment.readOnly(), existing_maybe); 150 151 if (!fieldsValid(record.event.title, record.event.tags)) return error.InvalidPatch; 152 if (!branchValid(record.event.target_branch)) return error.InvalidTargetBranch; 153 if (record.event.source_branch) |branch| { 154 if (!branchValid(branch)) return error.InvalidSourceBranch; 155 if (std.mem.eql(u8, branch, record.event.target_branch)) return error.SameBranch; 156 } 157 const revision_id = if (record.event.revision) |*revision| blk: { 158 try evt.PatchRev.validateOid(hash_kind, revision.squash_oid); 159 try evt.PatchRev.validateOid(hash_kind, revision.source_oid); 160 break :blk try evt.parseEventId(&revision.id); 161 } else null; 162 const target_patch_id = if (record.event.target_patch_id) |*id| try evt.parseEventId(id) else null; 163 if (target_patch_id) |*id| { 164 if (std.mem.eql(u8, id, event_id)) return error.InvalidPatch; 165 } 166 const status_kind = record.event.status.kind(); 167 168 if (existing_maybe) |existing| { 169 if (!evt.fieldEqual(?[]const u8, existing.event.source_branch, record.event.source_branch)) return error.PatchSourceChanged; 170 record.created_order = existing.created_order; 171 record.author_email = existing.author_email; 172 const existing_status_kind = existing.event.status.kind(); 173 if (existing_status_kind == .merged and !evt.fieldEqual(Status, existing.event.status, record.event.status)) return error.PatchAlreadyMerged; 174 if (existing_status_kind == .merged and !std.mem.eql(u8, existing.event.target_branch, record.event.target_branch)) return error.PatchAlreadyMerged; 175 if (event_oid != null) { 176 if (!std.meta.eql(existing.event.target_patch_id, record.event.target_patch_id)) { 177 return error.TargetPatchChanged; 178 } 179 } 180 } 181 182 switch (record.event.status) { 183 .open, .closed => {}, 184 .merged => |merged| { 185 const merged_id = try evt.parseEventId(&merged.patchrev_id); 186 const merged_revision = (try evt.PatchRev.readById(DB, hash_kind, haxy_moment.readOnly(), arena, &merged_id)) orelse return error.InvalidPatch; 187 if (merged_revision.removed) return error.InvalidPatch; 188 const id = revision_id orelse return error.InvalidPatch; 189 const selected = record.event.revision orelse return error.InvalidPatch; 190 const revision = (try evt.PatchRev.readById(DB, hash_kind, haxy_moment.readOnly(), arena, &id)) orelse return error.InvalidPatch; 191 if (!selected.matches(revision)) return error.InvalidPatch; 192 }, 193 } 194 195 const order_key = evt.orderKeyDesc(record.created_order, event_id); 196 if (existing_maybe) |existing| { 197 if (event_oid != null or record.removed) _ = try conflicts.remove(&order_key); 198 if (!existing.removed) { 199 const existing_status_kind = existing.event.status.kind(); 200 const old_status = try statusSet(DB, statuses, existing_status_kind); 201 _ = try old_status.remove(&order_key); 202 try removeFromTagSets(DB, tag_statuses, existing.event.tags, existing_status_kind, &order_key); 203 if (existing_status_kind != .merged) { 204 if (existing.event.revision) |revision| { 205 const revisions = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, revision_to_id_set_key))); 206 for ([_][]const u8{ revision.squash_oid, revision.source_oid }, 0..) |oid, i| { 207 if (i != 0 and std.mem.eql(u8, revision.squash_oid, revision.source_oid)) continue; 208 const key_hash = hash.hashInt(hash_kind, try revisionKey(arena.allocator(), existing.event.target_branch, oid)); 209 const ids = try DB.CountedHashSet(.read_write).init(try revisions.putCursor(key_hash)); 210 _ = try ids.remove(record_key); 211 if (try ids.count() == 0) _ = try revisions.remove(key_hash); 212 } 213 } 214 } 215 } 216 } 217 218 if (record.event.source_branch) |branch| { 219 const sources = try DB.SortedMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, source_to_id_set_key))); 220 if (!record.removed and status_kind != .merged) { 221 const ids = try DB.CountedHashSet(.read_write).init(try sources.putCursor(branch)); 222 try ids.put(record_key, .{ .bytes = event_id }); 223 } else if (try sources.getCursor(branch) != null) { 224 const ids = try DB.CountedHashSet(.read_write).init(try sources.putCursor(branch)); 225 _ = try ids.remove(record_key); 226 if (try ids.count() == 0) _ = try sources.remove(branch); 227 } 228 } 229 230 try evt.writeOid(Self, DB, hash_kind, field_oid_map, record_key, if (existing_maybe) |existing| existing.event else null, record.event, event_oid); 231 232 const cursor = try records.putCursor(record_key); 233 try evt.upsert(Record, DB, hash_kind, try DB.HashMap(.read_write).init(cursor), record); 234 try evt.indexEvent(DB, hash_kind, haxy_moment, event_id, .patch, existing_maybe, record); 235 236 if (existing_cursor_maybe == null) { 237 const ids = try DB.SortedSet(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, all_id_set_key))); 238 try ids.put(&order_key); 239 if (target_patch_id) |*target_id| { 240 const targets = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, target_patch_id_to_patch_id_set_key))); 241 const children = try DB.SortedSet(.read_write).init(try targets.putCursor(hash.hashInt(hash_kind, target_id))); 242 try children.put(&order_key); 243 } 244 } 245 246 if (!record.removed) { 247 const status = try statusSet(DB, statuses, status_kind); 248 try status.put(&order_key); 249 250 var tag_iter = tagIterator(record.event.tags); 251 while (tag_iter.next()) |tag| { 252 if (tag.len == 0 or tag.len > tag_max_len) continue; 253 var key_buffer: TagStatusKey = undefined; 254 const set = try DB.SortedSet(.read_write).init(try tag_statuses.putCursor(try tagStatusKey(&key_buffer, tag, status_kind))); 255 try set.put(&order_key); 256 } 257 258 if (status_kind != .merged) { 259 if (record.event.revision) |revision| { 260 const revisions = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, revision_to_id_set_key))); 261 for ([_][]const u8{ revision.squash_oid, revision.source_oid }, 0..) |oid, i| { 262 if (i != 0 and std.mem.eql(u8, revision.squash_oid, revision.source_oid)) continue; 263 const key = try revisionKey(arena.allocator(), record.event.target_branch, oid); 264 const ids = try DB.CountedHashSet(.read_write).init(try revisions.putCursor(hash.hashInt(hash_kind, key))); 265 try ids.put(record_key, .{ .bytes = event_id }); 266 } 267 } 268 } 269 } 270 } 271 272 pub fn resolveMerge( 273 target: Self, 274 parent: Self, 275 merged: *Self, 276 outcome: *[std.meta.fields(Self).len]evt.FieldMerge, 277 ) void { 278 const status_index = std.meta.fieldIndex(Self, "status") orelse @compileError("Patch.status not found"); 279 const revision_index = std.meta.fieldIndex(Self, "revision") orelse @compileError("Patch.revision not found"); 280 if (outcome[status_index] != .conflicted or outcome[revision_index] == .conflicted) return; 281 282 if (target.status.kind() == .merged and evt.fieldEqual(?Revision, target.revision, merged.revision)) { 283 merged.status = target.status; 284 outcome[status_index] = .kept; 285 } else if (parent.status.kind() == .merged and evt.fieldEqual(?Revision, parent.revision, merged.revision)) { 286 merged.status = parent.status; 287 outcome[status_index] = .parent; 288 } 289 } 290 291 pub fn update( 292 host_kind: evt.HostKind, 293 comptime repo_kind: rp.RepoKind, 294 comptime repo_opts: rp.RepoOpts(repo_kind), 295 io: std.Io, 296 allocator: std.mem.Allocator, 297 repo: *rp.Repo(repo_kind, repo_opts), 298 id: *const [evt.event_id_size]u8, 299 change: Update, 300 author: evt.CommitAuthor, 301 ) !void { 302 var arena = std.heap.ArenaAllocator.init(allocator); 303 defer arena.deinit(); 304 const record = (try evt.readFromRepo(Self, repo_kind, repo_opts, io, allocator, &arena, repo, id)) orelse return error.NotFound; 305 if (record.removed) return error.NotFound; 306 307 var updated = record.event; 308 switch (change) { 309 .status => |status| { 310 if (updated.status.kind() == status) return; 311 updated.status = switch (status) { 312 .open => .open, 313 .closed => .closed, 314 .merged => return error.InvalidPatchStatus, 315 }; 316 // a closed patch may have missed source branch updates 317 if (status == .open and updated.source_branch != null) updated.revision = null; 318 }, 319 .fields => |fields| { 320 if (!branchValid(fields.target_branch)) return error.InvalidTargetBranch; 321 if ((try repo.readRef(io, .{ .kind = .head, .name = fields.target_branch })) == null) return error.InvalidTargetBranch; 322 updated.title = fields.title; 323 updated.tags = fields.tags; 324 updated.description = fields.description; 325 if (!std.mem.eql(u8, updated.target_branch, fields.target_branch)) updated.revision = null; 326 updated.target_branch = fields.target_branch; 327 }, 328 .resolve => |resolve| { 329 updated = try resolveFields(repo_kind, repo_opts, io, allocator, &arena, repo, id, record, resolve); 330 }, 331 } 332 if (!fieldsValid(updated.title, updated.tags)) return error.InvalidFields; 333 334 if (updated.source_branch != null and updated.revision == null) return @import("../patch.zig").writeBranchPatch(host_kind, repo_kind, repo_opts, io, allocator, repo, std.fmt.bytesToHex(id.*, .lower), updated, record.event, author); 335 try evt.consume(host_kind, .repo, repo_kind, repo_opts, io, allocator, repo, evt.events_ref, &.{.{ 336 .id = std.fmt.bytesToHex(id.*, .lower), 337 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), 338 .author = author, 339 .event = .{ .patch = updated }, 340 }}); 341 } 342 343 fn resolveFields( 344 comptime repo_kind: rp.RepoKind, 345 comptime repo_opts: rp.RepoOpts(repo_kind), 346 io: std.Io, 347 allocator: std.mem.Allocator, 348 arena: *std.heap.ArenaAllocator, 349 repo: *rp.Repo(repo_kind, repo_opts), 350 id: *const [evt.event_id_size]u8, 351 live: Record, 352 resolve: Resolve, 353 ) !Self { 354 const DB = evt.EventDB(repo_opts.hash); 355 var updated = live.event; 356 if (resolve.title) |title| updated.title = title; 357 if (resolve.tags) |tags| updated.tags = tags; 358 359 var event_db_maybe: ?evt.LocalEventDB(repo_opts.hash) = if (repo_kind == .git) try evt.LocalEventDB(repo_opts.hash).openReadOnly(io, allocator, repo.core.repo_dir) else null; 360 defer if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); 361 const moment = (if (event_db_maybe) |*event_db| 362 evt.currentMomentFromDb(repo_opts.hash, event_db.db) 363 else if (repo_kind == .git) 364 return updated 365 else 366 evt.currentMoment(repo_opts, repo)) catch return updated; 367 const conflicts_cursor = (try moment.getCursor(hash.hashInt(repo_opts.hash, conflicts_key))) orelse return updated; 368 const conflicts = try DB.SortedMap(.read_only).init(conflicts_cursor); 369 const conflict_cursor = (try conflicts.getCursor(&evt.orderKeyDesc(live.created_order, id))) orelse return updated; 370 const entry = try DB.HashMap(.read_only).init(conflict_cursor); 371 const fields_cursor = (try entry.getCursor(hash.hashInt(repo_opts.hash, evt.conflicted_fields_key))) orelse return updated; 372 const fields = try fields_cursor.readBytesAlloc(arena.allocator(), null); 373 const their_cursor = (try entry.getCursor(hash.hashInt(repo_opts.hash, evt.their_record_key))) orelse return updated; 374 const theirs = try evt.read(Record, DB, repo_opts.hash, arena, try DB.HashMap(.read_only).init(their_cursor)); 375 376 const original_target_branch = updated.target_branch; 377 var field_iter = std.mem.splitScalar(u8, fields, ' '); 378 while (field_iter.next()) |field| { 379 if (std.mem.eql(u8, field, "status") and fieldListed(resolve.theirs, ',', field)) updated.status = theirs.event.status; 380 if (std.mem.eql(u8, field, "revision") and fieldListed(resolve.theirs, ',', field)) updated.revision = theirs.event.revision; 381 if (std.mem.eql(u8, field, "target_branch") and fieldListed(resolve.theirs, ',', field)) updated.target_branch = theirs.event.target_branch; 382 } 383 if (!std.mem.eql(u8, original_target_branch, updated.target_branch)) updated.revision = null; 384 385 if (!fieldListed(fields, ' ', "description")) return updated; 386 var base_description: []const u8 = ""; 387 if (try entry.getCursor(hash.hashInt(repo_opts.hash, evt.base_record_key))) |base_cursor| { 388 const base = try evt.read(Record, DB, repo_opts.hash, arena, try DB.HashMap(.read_only).init(base_cursor)); 389 base_description = base.event.description; 390 } 391 const chunks = try diff3.chunks(allocator, arena, base_description, live.event.description, theirs.event.description); 392 var resolutions: std.ArrayList([]const u8) = .empty; 393 var hunk_index: usize = 0; 394 for (chunks) |chunk| { 395 if (chunk != .conflict) continue; 396 try resolutions.append(arena.allocator(), if (hunk_index < resolve.hunks.len) resolve.hunks[hunk_index] else (chunk.conflict.ours orelse "")); 397 hunk_index += 1; 398 } 399 updated.description = try diff3.assemble(arena.allocator(), chunks, resolutions.items); 400 return updated; 401 } 402 403 fn fieldListed(fields: []const u8, delimiter: u8, name: []const u8) bool { 404 var iter = std.mem.splitScalar(u8, fields, delimiter); 405 while (iter.next()) |field| { 406 if (std.mem.eql(u8, field, name)) return true; 407 } 408 return false; 409 } 410 411 fn revisionKey(allocator: std.mem.Allocator, target_branch: []const u8, oid: []const u8) ![]u8 { 412 return std.mem.concat(allocator, u8, &.{ target_branch, "\x00", oid }); 413 } 414 415 fn statusSet( 416 comptime DB: type, 417 statuses: DB.SortedMap(.read_write), 418 status: StatusKind, 419 ) !DB.SortedSet(.read_write) { 420 return DB.SortedSet(.read_write).init(try statuses.putCursor(@tagName(status))); 421 } 422 423 fn removeFromTagSets( 424 comptime DB: type, 425 tag_statuses: DB.SortedMap(.read_write), 426 tags: []const u8, 427 status: StatusKind, 428 order_key: []const u8, 429 ) !void { 430 var tag_iter = tagIterator(tags); 431 while (tag_iter.next()) |tag| { 432 if (tag.len == 0 or tag.len > tag_max_len) continue; 433 var key_buffer: TagStatusKey = undefined; 434 const key = try tagStatusKey(&key_buffer, tag, status); 435 const set = try DB.SortedSet(.read_write).init(try tag_statuses.putCursor(key)); 436 _ = try set.remove(order_key); 437 if (0 == try set.count()) _ = try tag_statuses.remove(key); 438 } 439 } 440 441 pub fn readById( 442 comptime DB: type, 443 comptime hash_kind: hash.HashKind, 444 haxy_moment: DB.HashMap(.read_only), 445 arena: *std.heap.ArenaAllocator, 446 id: *const [evt.event_id_size]u8, 447 ) !?Record { 448 const records_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, record_map_key)) orelse return null; 449 const records = try DB.HashMap(.read_only).init(records_cursor); 450 const record_cursor = try records.getCursor(hash.hashInt(hash_kind, id)) orelse return null; 451 return try evt.read(Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(record_cursor)); 452 } 453