╭ patch.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("../../event.zig"); 3 const xit = @import("xit"); 4 const rp = xit.repo; 5 const obj = xit.object; 6 const hash = xit.hash; 7 const rf = xit.ref; 8 9 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 10 const Repo = rp.Repo(.xit, repo_opts); 11 const author = evt.CommitAuthor{ .name = "alice", .email = "alice@example.test" }; 12 13 fn commitTree( 14 io: std.Io, 15 allocator: std.mem.Allocator, 16 repo: *Repo, 17 oid: *const [hash.hexLen(repo_opts.hash)]u8, 18 ) ![hash.hexLen(repo_opts.hash)]u8 { 19 var moment = try repo.core.latestMoment(); 20 const state = Repo.State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; 21 var object = try obj.Object(.xit, repo_opts).init(state, io, allocator, oid); 22 defer object.deinit(); 23 return switch (object.content) { 24 .commit => |commit| commit.tree, 25 else => error.InvalidObject, 26 }; 27 } 28 29 test "patch lifecycle" { 30 const io = std.testing.io; 31 const allocator = std.testing.allocator; 32 const temp_dir_name = "temp-event-patch"; 33 const cwd = std.Io.Dir.cwd(); 34 35 // create the temp dir 36 if (cwd.openDir(io, temp_dir_name, .{})) |dir_value| { 37 var dir = dir_value; 38 dir.close(io); 39 try cwd.deleteTree(io, temp_dir_name); 40 } else |_| {} 41 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 42 defer cwd.deleteTree(io, temp_dir_name) catch {}; 43 defer temp_dir.close(io); 44 45 const cwd_path = try std.process.currentPathAlloc(io, allocator); 46 defer allocator.free(cwd_path); 47 const upstream_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "upstream" }); 48 defer allocator.free(upstream_path); 49 const target_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "target" }); 50 defer allocator.free(target_path); 51 const source_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "source" }); 52 defer allocator.free(source_path); 53 54 // 55 // create the upstream repo and its base commit 56 // 57 58 const base_oid = blk: { 59 var upstream = try rp.Repo(.git, .{ .is_test = true }).init(io, allocator, .{ .path = upstream_path }); 60 defer upstream.deinit(io, allocator); 61 const file = try upstream.core.work_dir.createFile(io, "main.zig", .{ .truncate = true }); 62 defer file.close(io); 63 try file.writeStreamingAll(io, "pub fn main() void {}\n"); 64 try upstream.add(io, allocator, &.{"main.zig"}); 65 break :blk try upstream.commit(io, allocator, .{ .author = "alice <alice@example.test>", .message = "initial code", .timestamp = 1 }); 66 }; 67 68 var target = try Repo.clone(io, allocator, upstream_path, target_path, target_path, null, .{}); 69 defer target.deinit(io, allocator); 70 const base_tree_oid = try commitTree(io, allocator, &target, &base_oid); 71 72 // 73 // fork the target and commit the proposed change 74 // 75 76 var source = try Repo.clone(io, allocator, upstream_path, source_path, source_path, null, .{}); 77 defer source.deinit(io, allocator); 78 79 const source_oid = blk: { 80 const file = try source.core.work_dir.createFile(io, "feature.zig", .{ .truncate = true }); 81 defer file.close(io); 82 try file.writeStreamingAll(io, "pub const answer = 42;\n"); 83 try source.add(io, allocator, &.{"feature.zig"}); 84 break :blk try source.commit(io, allocator, .{ .author = "alice <alice@example.test>", .message = "add feature", .timestamp = 2 }); 85 }; 86 const head_tree_oid = try commitTree(io, allocator, &source, &source_oid); 87 88 // 89 // copy the proposed objects without creating a target ref 90 // 91 92 { 93 var source_moment = try source.core.latestMoment(); 94 const source_state = Repo.State(.read_only){ .core = &source.core, .extra = .{ .moment = &source_moment } }; 95 var objects = try obj.ObjectIterator(.xit, repo_opts).init(source_state, io, allocator, .{ .kind = .all }); 96 defer objects.deinit(); 97 try objects.include(&source_oid); 98 try target.copyObjects(.xit, repo_opts, &objects, io, null); 99 } 100 101 // 102 // define the patch revision and patch 103 // 104 105 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 106 const patchrev_id = evt.EventWithId.randomId(prng.random()); 107 const patch_id = evt.EventWithId.randomId(prng.random()); 108 const patch = evt.Patch{ 109 .title = "add the answer", 110 .description = "adds a reusable answer constant", 111 .tags = "enhancement", 112 .target_ref = "refs/heads/master", 113 .source_url = source_path, 114 .source_ref = "refs/heads/master", 115 .patchrev_id = std.fmt.bytesToHex(patchrev_id, .lower), 116 }; 117 const tree_entries = [_]evt.EventTreeEntry{ 118 .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, 119 .{ .tree = .{ .name = "head", .oid = &head_tree_oid } }, 120 }; 121 122 // 123 // consume the patch revision and patch 124 // 125 126 { 127 try evt.consume(.xit, repo_opts, io, allocator, &target, evt.events_ref, &.{ 128 .{ 129 .id = std.fmt.bytesToHex(patchrev_id, .lower), 130 .timestamp = 3, 131 .author = author, 132 .tree_entries = &tree_entries, 133 .event = .{ .patchrev = .{ .base_oid = &base_oid, .source_oid = &source_oid, .message = patch.title } }, 134 }, 135 .{ 136 .id = std.fmt.bytesToHex(patch_id, .lower), 137 .timestamp = 4, 138 .author = author, 139 .event = .{ .patch = patch }, 140 }, 141 }); 142 } 143 144 // 145 // check the records and derived commit 146 // 147 148 var arena = std.heap.ArenaAllocator.init(allocator); 149 defer arena.deinit(); 150 const moment = try evt.currentMoment(repo_opts, &target); 151 const patch_record = (try evt.Patch.readById(Repo.DB, repo_opts.hash, moment, &arena, &patch_id)) orelse return error.NotFound; 152 const patchrev_record = (try evt.PatchRev.readById(Repo.DB, repo_opts.hash, moment, &arena, &patchrev_id)) orelse return error.NotFound; 153 try std.testing.expectEqualStrings(&base_tree_oid, patchrev_record.base_tree_oid); 154 try std.testing.expectEqualStrings(&head_tree_oid, patchrev_record.head_tree_oid); 155 try std.testing.expectEqualStrings(&std.fmt.bytesToHex(patchrev_id, .lower), &patch_record.event.patchrev_id); 156 157 var initial_patch_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 158 @memcpy(&initial_patch_oid, patchrev_record.patch_oid); 159 { 160 var repo_moment = try target.core.latestMoment(); 161 const state = Repo.State(.read_only){ .core = &target.core, .extra = .{ .moment = &repo_moment } }; 162 var patch_object = try obj.Object(.xit, repo_opts).init(state, io, allocator, &initial_patch_oid); 163 defer patch_object.deinit(); 164 switch (patch_object.content) { 165 .commit => |commit| { 166 try std.testing.expectEqualStrings(&head_tree_oid, &commit.tree); 167 const parents = commit.metadata.parent_oids orelse return error.ParentOidsNotFound; 168 try std.testing.expectEqual(1, parents.len); 169 try std.testing.expectEqualStrings(&base_oid, &parents[0]); 170 }, 171 else => return error.InvalidObject, 172 } 173 } 174 175 // 176 // update metadata without creating another patch revision 177 // 178 179 var updated_patch = patch; 180 updated_patch.title = "add a reusable answer"; 181 { 182 try evt.consume(.xit, repo_opts, io, allocator, &target, evt.events_ref, &.{.{ 183 .id = std.fmt.bytesToHex(patch_id, .lower), 184 .timestamp = 5, 185 .author = author, 186 .event = .{ .patch = updated_patch }, 187 }}); 188 } 189 190 _ = arena.reset(.retain_capacity); 191 const updated_moment = try evt.currentMoment(repo_opts, &target); 192 const updated = (try evt.Patch.readById(Repo.DB, repo_opts.hash, updated_moment, &arena, &patch_id)) orelse return error.NotFound; 193 try std.testing.expectEqualStrings("add a reusable answer", updated.event.title); 194 195 // 196 // branch the events history before creating competing revisions 197 // 198 199 const side_events_ref: rf.Ref = .{ .kind = .head, .name = "haxy/patch-side" }; 200 { 201 var result = try target.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 202 defer result.deinit(); 203 try target.addBranch(io, .{ .name = side_events_ref.name }); 204 } 205 206 // 207 // create two competing code revisions 208 // 209 210 const source_a_oid = blk: { 211 const file = try source.core.work_dir.createFile(io, "feature.zig", .{ .truncate = true }); 212 defer file.close(io); 213 try file.writeStreamingAll(io, "pub const answer = 43;\n"); 214 try source.add(io, allocator, &.{"feature.zig"}); 215 break :blk try source.commit(io, allocator, .{ .author = "alice <alice@example.test>", .message = "revise feature one way", .timestamp = 6 }); 216 }; 217 const source_a_tree = try commitTree(io, allocator, &source, &source_a_oid); 218 const source_b_oid = blk: { 219 const file = try source.core.work_dir.createFile(io, "feature.zig", .{ .truncate = true }); 220 defer file.close(io); 221 try file.writeStreamingAll(io, "pub const answer = 44;\n"); 222 try source.add(io, allocator, &.{"feature.zig"}); 223 break :blk try source.commit(io, allocator, .{ .author = "alice <alice@example.test>", .message = "revise feature another way", .timestamp = 7 }); 224 }; 225 const source_b_tree = try commitTree(io, allocator, &source, &source_b_oid); 226 { 227 var source_moment = try source.core.latestMoment(); 228 const source_state = Repo.State(.read_only){ .core = &source.core, .extra = .{ .moment = &source_moment } }; 229 var objects = try obj.ObjectIterator(.xit, repo_opts).init(source_state, io, allocator, .{ .kind = .all }); 230 defer objects.deinit(); 231 try objects.include(&source_b_oid); 232 try target.copyObjects(.xit, repo_opts, &objects, io, null); 233 } 234 235 // 236 // select the first revision on the events branch 237 // 238 239 const patchrev_a_id = evt.EventWithId.randomId(prng.random()); 240 const target_entries = [_]evt.EventTreeEntry{ 241 .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, 242 .{ .tree = .{ .name = "head", .oid = &source_a_tree } }, 243 }; 244 var target_patch = updated_patch; 245 target_patch.patchrev_id = std.fmt.bytesToHex(patchrev_a_id, .lower); 246 { 247 try evt.consume(.xit, repo_opts, io, allocator, &target, evt.events_ref, &.{ 248 .{ 249 .id = std.fmt.bytesToHex(patchrev_a_id, .lower), 250 .timestamp = 8, 251 .author = author, 252 .tree_entries = &target_entries, 253 .event = .{ .patchrev = .{ .base_oid = &base_oid, .source_oid = &source_a_oid, .message = updated_patch.title } }, 254 }, 255 .{ 256 .id = std.fmt.bytesToHex(patch_id, .lower), 257 .timestamp = 9, 258 .author = author, 259 .event = .{ .patch = target_patch }, 260 }, 261 }); 262 } 263 264 // 265 // select the second revision on the side branch 266 // 267 268 const patchrev_b_id = evt.EventWithId.randomId(prng.random()); 269 const parent_entries = [_]evt.EventTreeEntry{ 270 .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, 271 .{ .tree = .{ .name = "head", .oid = &source_b_tree } }, 272 }; 273 var parent_patch = updated_patch; 274 parent_patch.patchrev_id = std.fmt.bytesToHex(patchrev_b_id, .lower); 275 { 276 try evt.consume(.xit, repo_opts, io, allocator, &target, side_events_ref, &.{ 277 .{ 278 .id = std.fmt.bytesToHex(patchrev_b_id, .lower), 279 .timestamp = 10, 280 .author = author, 281 .tree_entries = &parent_entries, 282 .event = .{ .patchrev = .{ .base_oid = &base_oid, .source_oid = &source_b_oid, .message = updated_patch.title } }, 283 }, 284 .{ 285 .id = std.fmt.bytesToHex(patch_id, .lower), 286 .timestamp = 11, 287 .author = author, 288 .event = .{ .patch = parent_patch }, 289 }, 290 }); 291 } 292 293 // 294 // merge the competing patch revisions 295 // 296 297 { 298 try evt.mergeEvents(.xit, repo_opts, io, allocator, &target, side_events_ref); 299 try evt.consume(.xit, repo_opts, io, allocator, &target, evt.events_ref, &.{}); 300 } 301 302 // 303 // check the selected revision and conflict 304 // 305 306 _ = arena.reset(.retain_capacity); 307 const merged_moment = try evt.currentMoment(repo_opts, &target); 308 const merged = (try evt.Patch.readById(Repo.DB, repo_opts.hash, merged_moment, &arena, &patch_id)) orelse return error.NotFound; 309 try std.testing.expectEqualStrings(&std.fmt.bytesToHex(patchrev_a_id, .lower), &merged.event.patchrev_id); 310 const merged_patchrev = (try evt.PatchRev.readById(Repo.DB, repo_opts.hash, merged_moment, &arena, &patchrev_a_id)) orelse return error.NotFound; 311 try std.testing.expectEqualStrings(&source_a_oid, merged_patchrev.event.source_oid); 312 try std.testing.expectEqualStrings(&source_a_tree, merged_patchrev.head_tree_oid); 313 314 const conflicts_cursor = try merged_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.conflicts_key)) orelse return error.NotFound; 315 const conflicts = try Repo.DB.SortedMap(.read_only).init(conflicts_cursor); 316 const conflict_cursor = try conflicts.getCursor(&evt.orderKeyDesc(merged.created_order, &patch_id)) orelse return error.NotFound; 317 const conflict = try Repo.DB.HashMap(.read_only).init(conflict_cursor); 318 const fields_cursor = try conflict.getCursor(hash.hashInt(repo_opts.hash, evt.conflicted_fields_key)) orelse return error.NotFound; 319 try std.testing.expectEqualStrings("patchrev_id", try fields_cursor.readBytesAlloc(arena.allocator(), null)); 320 const their_cursor = try conflict.getCursor(hash.hashInt(repo_opts.hash, evt.their_record_key)) orelse return error.NotFound; 321 const theirs = try evt.read(evt.Patch.Record, Repo.DB, repo_opts.hash, &arena, try Repo.DB.HashMap(.read_only).init(their_cursor)); 322 try std.testing.expectEqualStrings(&std.fmt.bytesToHex(patchrev_b_id, .lower), &theirs.event.patchrev_id); 323 324 // 325 // create a patch stacked on the selected revision 326 // 327 328 const child_patchrev_id = evt.EventWithId.randomId(prng.random()); 329 const child_id = evt.EventWithId.randomId(prng.random()); 330 const child_entries = [_]evt.EventTreeEntry{ 331 .{ .tree = .{ .name = "base", .oid = merged_patchrev.head_tree_oid } }, 332 .{ .tree = .{ .name = "head", .oid = &source_b_tree } }, 333 }; 334 { 335 try evt.consume(.xit, repo_opts, io, allocator, &target, evt.events_ref, &.{ 336 .{ 337 .id = std.fmt.bytesToHex(child_patchrev_id, .lower), 338 .timestamp = 12, 339 .author = author, 340 .tree_entries = &child_entries, 341 .event = .{ .patchrev = .{ .base_oid = merged_patchrev.patch_oid, .source_oid = &source_b_oid, .message = "stack another answer change" } }, 342 }, 343 .{ 344 .id = std.fmt.bytesToHex(child_id, .lower), 345 .timestamp = 13, 346 .author = author, 347 .event = .{ .patch = .{ 348 .title = "stack another answer change", 349 .description = "depends on the first patch", 350 .tags = "enhancement", 351 .target_ref = patch.target_ref, 352 .target_patch_id = std.fmt.bytesToHex(patch_id, .lower), 353 .source_url = patch.source_url, 354 .source_ref = patch.source_ref, 355 .patchrev_id = std.fmt.bytesToHex(child_patchrev_id, .lower), 356 } }, 357 }, 358 }); 359 } 360 361 // 362 // check the derived commit and reverse index 363 // 364 365 _ = arena.reset(.retain_capacity); 366 const stacked_moment = try evt.currentMoment(repo_opts, &target); 367 const child = (try evt.Patch.readById(Repo.DB, repo_opts.hash, stacked_moment, &arena, &child_id)) orelse return error.NotFound; 368 var selected_patchrev_id: [evt.event_id_size]u8 = undefined; 369 _ = try std.fmt.hexToBytes(&selected_patchrev_id, &child.event.patchrev_id); 370 const child_patchrev = (try evt.PatchRev.readById(Repo.DB, repo_opts.hash, stacked_moment, &arena, &selected_patchrev_id)) orelse return error.NotFound; 371 var child_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 372 @memcpy(&child_oid, child_patchrev.patch_oid); 373 { 374 var child_repo_moment = try target.core.latestMoment(); 375 const child_state = Repo.State(.read_only){ .core = &target.core, .extra = .{ .moment = &child_repo_moment } }; 376 var child_object = try obj.Object(.xit, repo_opts).init(child_state, io, allocator, &child_oid); 377 defer child_object.deinit(); 378 const child_commit = switch (child_object.content) { 379 .commit => |commit| commit, 380 else => return error.InvalidObject, 381 }; 382 const child_parents = child_commit.metadata.parent_oids orelse return error.ParentOidsNotFound; 383 try std.testing.expectEqualStrings(child_patchrev.event.base_oid, &child_parents[0]); 384 } 385 386 const targets_cursor = try stacked_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.target_patch_id_to_patch_id_set_key)) orelse return error.NotFound; 387 const targets = try Repo.DB.HashMap(.read_only).init(targets_cursor); 388 const children_cursor = try targets.getCursor(hash.hashInt(repo_opts.hash, &patch_id)) orelse return error.NotFound; 389 const children = try Repo.DB.SortedSet(.read_only).init(children_cursor); 390 try std.testing.expectEqual(1, try children.count()); 391 392 // 393 // retain the ref-free commits during garbage collection 394 // 395 396 { 397 const roots = try evt.PatchRev.gcRoots(Repo.DB, repo_opts.hash, allocator, stacked_moment); 398 defer allocator.free(roots); 399 try std.testing.expectEqual(4, roots.len); 400 _ = try target.garbageCollect(io, allocator, roots); 401 402 var after_gc_moment = try target.core.latestMoment(); 403 const after_gc_state = Repo.State(.read_only){ .core = &target.core, .extra = .{ .moment = &after_gc_moment } }; 404 var retained = try obj.Object(.xit, repo_opts).init(after_gc_state, io, allocator, &roots[0]); 405 retained.deinit(); 406 } 407 } 408