╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Patch.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
135 pub const revision_to_id_set_key = "target-branch+oid->patch-id-set"; 136 137 pub const TagStatusKey = [tag_max_len + 1 + StatusKind.longest_len]u8; 138 139 pub fn tagStatusKey(buffer: *TagStatusKey, tag: []const u8, status: StatusKind) ![]const u8 { 140 return std.fmt.bufPrint(buffer, "{s} {s}", .{ tag, @tagName(status) }); 141 } 142 143 pub fn tagIterator(tags: []const u8) std.mem.SplitIterator(u8, .scalar) { 144 return std.mem.splitScalar(u8, tags, ' '); 145 } 146 147 pub fn fieldsValid(title: []const u8, tags: []const u8) bool { 148 if (!evt.titleValid(title)) return false; 149 var tag_iter = tagIterator(tags); 150 while (tag_iter.next()) |tag| { 151 if (tag.len > tag_max_len) return false; 152 } 153 return true; 154 } 155 156 pub fn consume( 157 comptime DB: type, 158 comptime hash_kind: hash.HashKind, 159 haxy_moment: DB.HashMap(.read_write), 160 event_id: *const [evt.event_id_size]u8, 161 record_maybe: ?Record, 162 arena: *std.heap.ArenaAllocator, 163 event_oid: ?[]const u8, 164 ) !void { 165 const record_key = hash.hashInt(hash_kind, event_id); 166 const records = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, record_map_key))); 167 const statuses = try DB.SortedMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, status_to_id_set_key))); 168 const tag_statuses = try DB.SortedMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, tag_status_to_id_set_key))); 169 const conflicts = try DB.SortedMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, conflicts_key))); 170 const field_oid_map = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, id_to_field_to_oid_key))); 171 172 var existing_maybe: ?Record = null; 173 const existing_cursor_maybe = try records.getCursor(record_key); 174 if (existing_cursor_maybe) |cursor| { 175 existing_maybe = try evt.read(Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(cursor)); 176 } 177 178 var record = record_maybe orelse try evt.removedRecord(Record, DB, hash_kind, haxy_moment.readOnly(), existing_maybe); 179 180 if (!fieldsValid(record.event.title, record.event.tags)) return error.InvalidPatch; 181 if (!rf.validateName(record.event.target_branch)) return error.InvalidTarget; 182 const revision_id = if (record.event.revision) |*revision| blk: { 183 try evt.PatchRev.validateOid(hash_kind, revision.squash_oid); 184 try evt.PatchRev.validateOid(hash_kind, revision.source_oid); 185 break :blk try evt.parseEventId(&revision.id); 186 } else null; 187 const target_patch_id = if (record.event.target_patch_id) |*id| try evt.parseEventId(id) else null; 188 if (target_patch_id) |*id| { 189 if (std.mem.eql(u8, id, event_id)) return error.InvalidPatch; 190 } 191 const status_kind = record.event.status.kind(); 192 193 if (existing_maybe) |existing| { 194 record.created_order = existing.created_order; 195 record.author_email = existing.author_email; 196 const existing_status_kind = existing.event.status.kind(); 197 if (existing_status_kind == .merged and !evt.fieldEqual(Status, existing.event.status, record.event.status)) return error.PatchAlreadyMerged; 198 if (existing_status_kind == .merged and !std.mem.eql(u8, existing.event.target_branch, record.event.target_branch)) return error.PatchAlreadyMerged; 199 if (event_oid != null) { 200 if (!std.meta.eql(existing.event.target_patch_id, record.event.target_patch_id)) { 201 return error.TargetPatchChanged; 202 } 203 } 204 } 205 206 switch (record.event.status) { 207 .open, .closed => {}, 208 .merged => |merged_oid| { 209 try evt.PatchRev.validateOid(hash_kind, merged_oid); 210 const id = revision_id orelse return error.InvalidPatch; 211 const selected = record.event.revision orelse return error.InvalidPatch; 212 if (!selected.includesOid(merged_oid)) return error.InvalidPatch; 213 const revision = (try evt.PatchRev.readById(DB, hash_kind, haxy_moment.readOnly(), arena, &id)) orelse return error.InvalidPatch; 214 if (!selected.matches(revision)) return error.InvalidPatch; 215 }, 216 } 217 218 const order_key = evt.orderKeyDesc(record.created_order, event_id); 219 if (existing_maybe) |existing| { 220 if (event_oid != null or record.removed) _ = try conflicts.remove(&order_key); 221 if (!existing.removed) { 222 const existing_status_kind = existing.event.status.kind(); 223 const old_status = try statusSet(DB, statuses, existing_status_kind); 224 _ = try old_status.remove(&order_key); 225 try removeFromTagSets(DB, tag_statuses, existing.event.tags, existing_status_kind, &order_key); 226 if (existing_status_kind != .merged) { 227 if (existing.event.revision) |revision| { 228 const revisions = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, revision_to_id_set_key))); 229 for ([_][]const u8{ revision.squash_oid, revision.source_oid }, 0..) |oid, i| { 230 if (i != 0 and std.mem.eql(u8, revision.squash_oid, revision.source_oid)) continue; 231 const key_hash = hash.hashInt(hash_kind, try revisionKey(arena.allocator(), existing.event.target_branch, oid)); 232 const ids = try DB.CountedHashSet(.read_write).init(try revisions.putCursor(key_hash)); 233 _ = try ids.remove(record_key); 234 if (try ids.count() == 0) _ = try revisions.remove(key_hash); 235 } 236 } 237 } 238 } 239 } 240 241 try evt.writeOid(Self, DB, hash_kind, field_oid_map, record_key, if (existing_maybe) |existing| existing.event else null, record.event, event_oid); 242 243 const cursor = try records.putCursor(record_key); 244 try evt.upsert(Record, DB, hash_kind, try DB.HashMap(.read_write).init(cursor), record); 245 try evt.indexEvent(DB, hash_kind, haxy_moment, event_id, .patch, existing_maybe, record); 246 247 if (existing_cursor_maybe == null) { 248 const ids = try DB.SortedSet(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, all_id_set_key))); 249 try ids.put(&order_key); 250 if (target_patch_id) |*target_id| { 251 const targets = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, target_patch_id_to_patch_id_set_key))); 252 const children = try DB.SortedSet(.read_write).init(try targets.putCursor(hash.hashInt(hash_kind, target_id))); 253 try children.put(&order_key); 254 } 255 } 256 257 if (!record.removed) { 258 const status = try statusSet(DB, statuses, status_kind); 259 try status.put(&order_key); 260 261 var tag_iter = tagIterator(record.event.tags); 262 while (tag_iter.next()) |tag| { 263 if (tag.len == 0 or tag.len > tag_max_len) continue; 264 var key_buffer: TagStatusKey = undefined; 265 const set = try DB.SortedSet(.read_write).init(try tag_statuses.putCursor(try tagStatusKey(&key_buffer, tag, status_kind))); 266 try set.put(&order_key); 267 } 268 269 if (status_kind != .merged) { 270 if (record.event.revision) |revision| { 271 const revisions = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, revision_to_id_set_key))); 272 for ([_][]const u8{ revision.squash_oid, revision.source_oid }, 0..) |oid, i| { 273 if (i != 0 and std.mem.eql(u8, revision.squash_oid, revision.source_oid)) continue; 274 const key = try revisionKey(arena.allocator(), record.event.target_branch, oid); 275 const ids = try DB.CountedHashSet(.read_write).init(try revisions.putCursor(hash.hashInt(hash_kind, key))); 276 try ids.put(record_key, .{ .bytes = event_id }); 277 } 278 } 279 } 280 } 281 } 282 283 pub fn resolveMerge( 284 target: Self, 285 parent: Self, 286 merged: *Self, 287 outcome: *[std.meta.fields(Self).len]evt.FieldMerge, 288 ) void { 289 const status_index = std.meta.fieldIndex(Self, "status") orelse @compileError("Patch.status not found"); 290 const revision_index = std.meta.fieldIndex(Self, "revision") orelse @compileError("Patch.revision not found"); 291 if (outcome[status_index] != .conflicted or outcome[revision_index] == .conflicted) return; 292 293 if (target.status.kind() == .merged and evt.fieldEqual(?Revision, target.revision, merged.revision)) { 294 merged.status = target.status; 295 outcome[status_index] = .kept; 296 } else if (parent.status.kind() == .merged and evt.fieldEqual(?Revision, parent.revision, merged.revision)) { 297 merged.status = parent.status; 298 outcome[status_index] = .parent; 299 } 300 } 301 302 pub fn update( 303 comptime repo_kind: rp.RepoKind, 304 comptime repo_opts: rp.RepoOpts(repo_kind), 305 io: std.Io, 306 allocator: std.mem.Allocator, 307 repo: *rp.Repo(repo_kind, repo_opts), 308 id: *const [evt.event_id_size]u8, 309 change: Update, 310 author: evt.CommitAuthor, 311 ) !void { 312 var arena = std.heap.ArenaAllocator.init(allocator); 313 defer arena.deinit(); 314 const record = (try readFromRepo(repo_kind, repo_opts, io, allocator, &arena, repo, id)) orelse return error.NotFound; 315 if (record.removed) return error.NotFound; 316 317 var updated = record.event; 318 switch (change) { 319 .status => |status| { 320 if (updated.status.kind() == status) return; 321 updated.status = switch (status) { 322 .open => .open, 323 .closed => .closed, 324 .merged => return error.InvalidPatchStatus, 325 }; 326 }, 327 .fields => |fields| { 328 if (!rf.validateName(fields.target_branch)) return error.InvalidFields; 329 if ((try repo.readRef(io, .{ .kind = .head, .name = fields.target_branch })) == null) return error.InvalidFields; 330 updated.title = fields.title; 331 updated.tags = fields.tags; 332 updated.description = fields.description; 333 if (!std.mem.eql(u8, updated.target_branch, fields.target_branch)) updated.revision = null; 334 updated.target_branch = fields.target_branch; 335 }, 336 .resolve => |resolve| { 337 updated = try resolveFields(repo_kind, repo_opts, io, allocator, &arena, repo, id, record, resolve); 338 }, 339 } 340 if (!fieldsValid(updated.title, updated.tags)) return error.InvalidFields; 341 342 try evt.consume(.repo, repo_kind, repo_opts, io, allocator, repo, evt.events_ref, &.{.{ 343 .id = std.fmt.bytesToHex(id.*, .lower), 344 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), 345 .author = author, 346 .event = .{ .patch = updated }, 347 }}); 348 } 349 350 fn resolveFields( 351 comptime repo_kind: rp.RepoKind, 352 comptime repo_opts: rp.RepoOpts(repo_kind), 353 io: std.Io, 354 allocator: std.mem.Allocator, 355 arena: *std.heap.ArenaAllocator, 356 repo: *rp.Repo(repo_kind, repo_opts), 357 id: *const [evt.event_id_size]u8, 358 live: Record, 359 resolve: Resolve, 360 ) !Self { 361 const DB = evt.EventDB(repo_opts.hash); 362 var updated = live.event; 363 if (resolve.title) |title| updated.title = title; 364 if (resolve.tags) |tags| updated.tags = tags; 365 366 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; 367 defer if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); 368 const moment = (if (event_db_maybe) |*event_db| 369 evt.currentMomentFromDb(repo_opts.hash, event_db.db) 370 else if (repo_kind == .git) 371 return updated 372 else 373 evt.currentMoment(repo_opts, repo)) catch return updated; 374 const conflicts_cursor = (try moment.getCursor(hash.hashInt(repo_opts.hash, conflicts_key))) orelse return updated; 375 const conflicts = try DB.SortedMap(.read_only).init(conflicts_cursor); 376 const conflict_cursor = (try conflicts.getCursor(&evt.orderKeyDesc(live.created_order, id))) orelse return updated; 377 const entry = try DB.HashMap(.read_only).init(conflict_cursor); 378 const fields_cursor = (try entry.getCursor(hash.hashInt(repo_opts.hash, evt.conflicted_fields_key))) orelse return updated; 379 const fields = try fields_cursor.readBytesAlloc(arena.allocator(), null); 380 const their_cursor = (try entry.getCursor(hash.hashInt(repo_opts.hash, evt.their_record_key))) orelse return updated; 381 const theirs = try evt.read(Record, DB, repo_opts.hash, arena, try DB.HashMap(.read_only).init(their_cursor)); 382 383 const original_target_branch = updated.target_branch; 384 var field_iter = std.mem.splitScalar(u8, fields, ' '); 385 while (field_iter.next()) |field| { 386 if (std.mem.eql(u8, field, "status") and fieldListed(resolve.theirs, ',', field)) updated.status = theirs.event.status; 387 if (std.mem.eql(u8, field, "revision") and fieldListed(resolve.theirs, ',', field)) updated.revision = theirs.event.revision; 388 if (std.mem.eql(u8, field, "target_branch") and fieldListed(resolve.theirs, ',', field)) updated.target_branch = theirs.event.target_branch; 389 } 390 if (!std.mem.eql(u8, original_target_branch, updated.target_branch)) updated.revision = null; 391 392 if (!fieldListed(fields, ' ', "description")) return updated; 393 var base_description: []const u8 = ""; 394 if (try entry.getCursor(hash.hashInt(repo_opts.hash, evt.base_record_key))) |base_cursor| { 395 const base = try evt.read(Record, DB, repo_opts.hash, arena, try DB.HashMap(.read_only).init(base_cursor)); 396 base_description = base.event.description; 397 } 398 const chunks = try diff3.chunks(io, allocator, arena, base_description, live.event.description, theirs.event.description); 399 var resolutions: std.ArrayList([]const u8) = .empty; 400 var hunk_index: usize = 0; 401 for (chunks) |chunk| { 402 if (chunk != .conflict) continue; 403 try resolutions.append(arena.allocator(), if (hunk_index < resolve.hunks.len) resolve.hunks[hunk_index] else (chunk.conflict.ours orelse "")); 404 hunk_index += 1; 405 } 406 updated.description = try diff3.assemble(arena.allocator(), chunks, resolutions.items); 407 return updated; 408 } 409 410 fn fieldListed(fields: []const u8, delimiter: u8, name: []const u8) bool { 411 var iter = std.mem.splitScalar(u8, fields, delimiter); 412 while (iter.next()) |field| { 413 if (std.mem.eql(u8, field, name)) return true; 414 } 415 return false; 416 } 417 418 fn readFromRepo( 419 comptime repo_kind: rp.RepoKind, 420 comptime repo_opts: rp.RepoOpts(repo_kind), 421 io: std.Io, 422 allocator: std.mem.Allocator, 423 arena: *std.heap.ArenaAllocator, 424 repo: *rp.Repo(repo_kind, repo_opts), 425 id: *const [evt.event_id_size]u8, 426 ) !?Record { 427 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; 428 defer if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); 429 const moment = (if (event_db_maybe) |*event_db| 430 evt.currentMomentFromDb(repo_opts.hash, event_db.db) 431 else if (repo_kind == .git) 432 return null 433 else 434 evt.currentMoment(repo_opts, repo)) catch return null; 435 return readById(evt.EventDB(repo_opts.hash), repo_opts.hash, moment, arena, id); 436 } 437 438 fn revisionKey(allocator: std.mem.Allocator, target_branch: []const u8, oid: []const u8) ![]u8 { 439 return std.mem.concat(allocator, u8, &.{ target_branch, "\x00", oid }); 440 } 441 442 fn statusSet( 443 comptime DB: type, 444 statuses: DB.SortedMap(.read_write), 445 status: StatusKind, 446 ) !DB.SortedSet(.read_write) { 447 return DB.SortedSet(.read_write).init(try statuses.putCursor(@tagName(status))); 448 } 449 450 fn removeFromTagSets( 451 comptime DB: type, 452 tag_statuses: DB.SortedMap(.read_write), 453 tags: []const u8, 454 status: StatusKind, 455 order_key: []const u8, 456 ) !void { 457 var tag_iter = tagIterator(tags); 458 while (tag_iter.next()) |tag| { 459 if (tag.len == 0 or tag.len > tag_max_len) continue; 460 var key_buffer: TagStatusKey = undefined; 461 const key = try tagStatusKey(&key_buffer, tag, status); 462 const set = try DB.SortedSet(.read_write).init(try tag_statuses.putCursor(key)); 463 _ = try set.remove(order_key); 464 if (0 == try set.count()) _ = try tag_statuses.remove(key); 465 } 466 } 467 468 pub fn readById( 469 comptime DB: type, 470 comptime hash_kind: hash.HashKind, 471 haxy_moment: DB.HashMap(.read_only), 472 arena: *std.heap.ArenaAllocator, 473 id: *const [evt.event_id_size]u8, 474 ) !?Record { 475 const records_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, record_map_key)) orelse return null; 476 const records = try DB.HashMap(.read_only).init(records_cursor); 477 const record_cursor = try records.getCursor(hash.hashInt(hash_kind, id)) orelse return null; 478 return try evt.read(Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(record_cursor)); 479 } 480