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