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