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