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