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