╭ 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 const fork = @import("../../fork.zig"); 9 10 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 11 const Repo = rp.Repo(.xit, repo_opts); 12 const author = evt.CommitAuthor{ .name = "alice", .email = "alice@example.test" }; 13 14 fn commitTree( 15 io: std.Io, 16 allocator: std.mem.Allocator, 17 repo: *Repo, 18 oid: *const [hash.hexLen(repo_opts.hash)]u8, 19 ) ![hash.hexLen(repo_opts.hash)]u8 { 20 var moment = try repo.core.latestMoment(); 21 const state = Repo.State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; 22 var object = try obj.Object(.xit, repo_opts).init(state, io, allocator, oid); 23 defer object.deinit(); 24 return switch (object.content) { 25 .commit => |commit| commit.tree, 26 else => error.InvalidObject, 27 }; 28 } 29 30 test "patch lifecycle" { 31 const io = std.testing.io; 32 const allocator = std.testing.allocator; 33 const temp_dir_name = "temp-event-patch"; 34 const cwd = std.Io.Dir.cwd(); 35 36 // create the temp dir 37 if (cwd.openDir(io, temp_dir_name, .{})) |dir_value| { 38 var dir = dir_value; 39 dir.close(io); 40 try cwd.deleteTree(io, temp_dir_name); 41 } else |_| {} 42 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 43 defer cwd.deleteTree(io, temp_dir_name) catch {}; 44 defer temp_dir.close(io); 45 46 const cwd_path = try std.process.currentPathAlloc(io, allocator); 47 defer allocator.free(cwd_path); 48 const upstream_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "upstream" }); 49 defer allocator.free(upstream_path); 50 const target_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "target" }); 51 defer allocator.free(target_path); 52 const source_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "source" }); 53 defer allocator.free(source_path); 54 55 // 56 // create the upstream repo and its base commit 57 // 58 59 const base_oid = blk: { 60 var upstream = try rp.Repo(.git, .{ .is_test = true }).init(io, allocator, .{ .path = upstream_path }); 61 defer upstream.deinit(io, allocator); 62 const file = try upstream.core.work_dir.createFile(io, "main.zig", .{ .truncate = true }); 63 defer file.close(io); 64 try file.writeStreamingAll(io, "pub fn main() void {}\n"); 65 try upstream.add(io, allocator, &.{"main.zig"}); 66 break :blk try upstream.commit(io, allocator, .{ .author = "alice <alice@example.test>", .message = "initial code", .timestamp = 1 }); 67 }; 68 69 var target = try Repo.clone(io, allocator, upstream_path, target_path, target_path, null, .{}); 70 defer target.deinit(io, allocator); 71 const base_tree_oid = try commitTree(io, allocator, &target, &base_oid); 72 73 // 74 // fork the target and commit the proposed change 75 // 76 77 var source = try Repo.clone(io, allocator, upstream_path, source_path, source_path, null, .{}); 78 defer source.deinit(io, allocator); 79 80 const source_oid = blk: { 81 const file = try source.core.work_dir.createFile(io, "feature.zig", .{ .truncate = true }); 82 defer file.close(io); 83 try file.writeStreamingAll(io, "pub const answer = 42;\n"); 84 try source.add(io, allocator, &.{"feature.zig"}); 85 break :blk try source.commit(io, allocator, .{ .author = "alice <alice@example.test>", .message = "add feature", .timestamp = 2 }); 86 }; 87 const head_tree_oid = try commitTree(io, allocator, &source, &source_oid); 88 89 // 90 // copy the proposed objects without creating a target ref 91 // 92 93 { 94 var source_moment = try source.core.latestMoment(); 95 const source_state = Repo.State(.read_only){ .core = &source.core, .extra = .{ .moment = &source_moment } }; 96 var objects = try obj.ObjectIterator(.xit, repo_opts).init(source_state, io, allocator, .{ .kind = .all }); 97 defer objects.deinit(); 98 try objects.include(&source_oid); 99 try target.copyObjects(.xit, repo_opts, &objects, io, null); 100 } 101 102 // 103 // define the patch revision and patch 104 // 105 106 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 107 const patchrev_id = evt.EventWithId.randomId(prng.random()); 108 const patch_id = evt.EventWithId.randomId(prng.random()); 109 const patch = evt.Patch{ 110 .title = "add the answer", 111 .description = "adds a reusable answer constant", 112 .tags = "enhancement", 113 .target_ref = "refs/heads/master", 114 .patchrev_id = std.fmt.bytesToHex(patchrev_id, .lower), 115 }; 116 const tree_entries = [_]evt.EventTreeEntry{ 117 .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, 118 .{ .tree = .{ .name = "head", .oid = &head_tree_oid } }, 119 }; 120 121 // 122 // consume the patch revision and patch 123 // 124 125 { 126 try evt.consume(.xit, repo_opts, io, allocator, &target, evt.events_ref, &.{ 127 .{ 128 .id = std.fmt.bytesToHex(patchrev_id, .lower), 129 .timestamp = 3, 130 .author = author, 131 .tree_entries = &tree_entries, 132 .event = .{ .patchrev = .{ .base_oid = &base_oid, .source_oid = &source_oid, .message = patch.title } }, 133 }, 134 .{ 135 .id = std.fmt.bytesToHex(patch_id, .lower), 136 .timestamp = 4, 137 .author = author, 138 .event = .{ .patch = patch }, 139 }, 140 }); 141 } 142 143 // 144 // check the records and derived commit 145 // 146 147 var arena = std.heap.ArenaAllocator.init(allocator); 148 defer arena.deinit(); 149 const moment = try evt.currentMoment(repo_opts, &target); 150 const patch_record = (try evt.Patch.readById(Repo.DB, repo_opts.hash, moment, &arena, &patch_id)) orelse return error.NotFound; 151 const patchrev_record = (try evt.PatchRev.readById(Repo.DB, repo_opts.hash, moment, &arena, &patchrev_id)) orelse return error.NotFound; 152 try std.testing.expectEqualStrings(&base_tree_oid, patchrev_record.base_tree_oid); 153 try std.testing.expectEqualStrings(&head_tree_oid, patchrev_record.head_tree_oid); 154 try std.testing.expectEqualStrings(&std.fmt.bytesToHex(patchrev_id, .lower), &patch_record.event.patchrev_id); 155 156 var initial_patch_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 157 @memcpy(&initial_patch_oid, patchrev_record.patch_oid); 158 { 159 var repo_moment = try target.core.latestMoment(); 160 const state = Repo.State(.read_only){ .core = &target.core, .extra = .{ .moment = &repo_moment } }; 161 var patch_object = try obj.Object(.xit, repo_opts).init(state, io, allocator, &initial_patch_oid); 162 defer patch_object.deinit(); 163 switch (patch_object.content) { 164 .commit => |commit| { 165 try std.testing.expectEqualStrings(&head_tree_oid, &commit.tree); 166 const parents = commit.metadata.parent_oids orelse return error.ParentOidsNotFound; 167 try std.testing.expectEqual(1, parents.len); 168 try std.testing.expectEqualStrings(&base_oid, &parents[0]); 169 }, 170 else => return error.InvalidObject, 171 } 172 } 173 174 // 175 // update metadata without creating another patch revision 176 // 177 178 var updated_patch = patch; 179 updated_patch.title = "add a reusable answer"; 180 { 181 try evt.consume(.xit, repo_opts, io, allocator, &target, evt.events_ref, &.{.{ 182 .id = std.fmt.bytesToHex(patch_id, .lower), 183 .timestamp = 5, 184 .author = author, 185 .event = .{ .patch = updated_patch }, 186 }}); 187 } 188 189 _ = arena.reset(.retain_capacity); 190 const updated_moment = try evt.currentMoment(repo_opts, &target); 191 const updated = (try evt.Patch.readById(Repo.DB, repo_opts.hash, updated_moment, &arena, &patch_id)) orelse return error.NotFound; 192 try std.testing.expectEqualStrings("add a reusable answer", updated.event.title); 193 194 // 195 // branch the events history before creating competing revisions 196 // 197 198 const side_events_ref: rf.Ref = .{ .kind = .head, .name = "haxy/patch-side" }; 199 { 200 var result = try target.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 201 defer result.deinit(); 202 try target.addBranch(io, .{ .name = side_events_ref.name }); 203 } 204 205 // 206 // create two competing code revisions 207 // 208 209 const source_a_oid = blk: { 210 const file = try source.core.work_dir.createFile(io, "feature.zig", .{ .truncate = true }); 211 defer file.close(io); 212 try file.writeStreamingAll(io, "pub const answer = 43;\n"); 213 try source.add(io, allocator, &.{"feature.zig"}); 214 break :blk try source.commit(io, allocator, .{ .author = "alice <alice@example.test>", .message = "revise feature one way", .timestamp = 6 }); 215 }; 216 const source_a_tree = try commitTree(io, allocator, &source, &source_a_oid); 217 const source_b_oid = blk: { 218 const file = try source.core.work_dir.createFile(io, "feature.zig", .{ .truncate = true }); 219 defer file.close(io); 220 try file.writeStreamingAll(io, "pub const answer = 44;\n"); 221 try source.add(io, allocator, &.{"feature.zig"}); 222 break :blk try source.commit(io, allocator, .{ .author = "alice <alice@example.test>", .message = "revise feature another way", .timestamp = 7 }); 223 }; 224 const source_b_tree = try commitTree(io, allocator, &source, &source_b_oid); 225 { 226 var source_moment = try source.core.latestMoment(); 227 const source_state = Repo.State(.read_only){ .core = &source.core, .extra = .{ .moment = &source_moment } }; 228 var objects = try obj.ObjectIterator(.xit, repo_opts).init(source_state, io, allocator, .{ .kind = .all }); 229 defer objects.deinit(); 230 try objects.include(&source_b_oid); 231 try target.copyObjects(.xit, repo_opts, &objects, io, null); 232 } 233 234 // 235 // select the first revision on the events branch 236 // 237 238 const patchrev_a_id = evt.EventWithId.randomId(prng.random()); 239 const target_entries = [_]evt.EventTreeEntry{ 240 .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, 241 .{ .tree = .{ .name = "head", .oid = &source_a_tree } }, 242 }; 243 var target_patch = updated_patch; 244 target_patch.patchrev_id = std.fmt.bytesToHex(patchrev_a_id, .lower); 245 { 246 try evt.consume(.xit, repo_opts, io, allocator, &target, evt.events_ref, &.{ 247 .{ 248 .id = std.fmt.bytesToHex(patchrev_a_id, .lower), 249 .timestamp = 8, 250 .author = author, 251 .tree_entries = &target_entries, 252 .event = .{ .patchrev = .{ .base_oid = &base_oid, .source_oid = &source_a_oid, .message = updated_patch.title } }, 253 }, 254 .{ 255 .id = std.fmt.bytesToHex(patch_id, .lower), 256 .timestamp = 9, 257 .author = author, 258 .event = .{ .patch = target_patch }, 259 }, 260 }); 261 } 262 263 // 264 // select the second revision on the side branch 265 // 266 267 const patchrev_b_id = evt.EventWithId.randomId(prng.random()); 268 const parent_entries = [_]evt.EventTreeEntry{ 269 .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, 270 .{ .tree = .{ .name = "head", .oid = &source_b_tree } }, 271 }; 272 var parent_patch = updated_patch; 273 parent_patch.patchrev_id = std.fmt.bytesToHex(patchrev_b_id, .lower); 274 { 275 try evt.consume(.xit, repo_opts, io, allocator, &target, side_events_ref, &.{ 276 .{ 277 .id = std.fmt.bytesToHex(patchrev_b_id, .lower), 278 .timestamp = 10, 279 .author = author, 280 .tree_entries = &parent_entries, 281 .event = .{ .patchrev = .{ .base_oid = &base_oid, .source_oid = &source_b_oid, .message = updated_patch.title } }, 282 }, 283 .{ 284 .id = std.fmt.bytesToHex(patch_id, .lower), 285 .timestamp = 11, 286 .author = author, 287 .event = .{ .patch = parent_patch }, 288 }, 289 }); 290 } 291 292 // 293 // merge the competing patch revisions 294 // 295 296 { 297 try evt.mergeEvents(.xit, repo_opts, io, allocator, &target, side_events_ref); 298 try evt.consume(.xit, repo_opts, io, allocator, &target, evt.events_ref, &.{}); 299 } 300 301 // 302 // check the selected revision and conflict 303 // 304 305 _ = arena.reset(.retain_capacity); 306 const merged_moment = try evt.currentMoment(repo_opts, &target); 307 const merged = (try evt.Patch.readById(Repo.DB, repo_opts.hash, merged_moment, &arena, &patch_id)) orelse return error.NotFound; 308 try std.testing.expectEqualStrings(&std.fmt.bytesToHex(patchrev_a_id, .lower), &merged.event.patchrev_id); 309 const merged_patchrev = (try evt.PatchRev.readById(Repo.DB, repo_opts.hash, merged_moment, &arena, &patchrev_a_id)) orelse return error.NotFound; 310 try std.testing.expectEqualStrings(&source_a_oid, merged_patchrev.event.source_oid); 311 try std.testing.expectEqualStrings(&source_a_tree, merged_patchrev.head_tree_oid); 312 313 const conflicts_cursor = try merged_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.conflicts_key)) orelse return error.NotFound; 314 const conflicts = try Repo.DB.SortedMap(.read_only).init(conflicts_cursor); 315 const conflict_cursor = try conflicts.getCursor(&evt.orderKeyDesc(merged.created_order, &patch_id)) orelse return error.NotFound; 316 const conflict = try Repo.DB.HashMap(.read_only).init(conflict_cursor); 317 const fields_cursor = try conflict.getCursor(hash.hashInt(repo_opts.hash, evt.conflicted_fields_key)) orelse return error.NotFound; 318 try std.testing.expectEqualStrings("patchrev_id", try fields_cursor.readBytesAlloc(arena.allocator(), null)); 319 const their_cursor = try conflict.getCursor(hash.hashInt(repo_opts.hash, evt.their_record_key)) orelse return error.NotFound; 320 const theirs = try evt.read(evt.Patch.Record, Repo.DB, repo_opts.hash, &arena, try Repo.DB.HashMap(.read_only).init(their_cursor)); 321 try std.testing.expectEqualStrings(&std.fmt.bytesToHex(patchrev_b_id, .lower), &theirs.event.patchrev_id); 322 323 // 324 // create a patch stacked on the selected revision 325 // 326 327 const child_patchrev_id = evt.EventWithId.randomId(prng.random()); 328 const child_id = evt.EventWithId.randomId(prng.random()); 329 const child_entries = [_]evt.EventTreeEntry{ 330 .{ .tree = .{ .name = "base", .oid = merged_patchrev.head_tree_oid } }, 331 .{ .tree = .{ .name = "head", .oid = &source_b_tree } }, 332 }; 333 { 334 try evt.consume(.xit, repo_opts, io, allocator, &target, evt.events_ref, &.{ 335 .{ 336 .id = std.fmt.bytesToHex(child_patchrev_id, .lower), 337 .timestamp = 12, 338 .author = author, 339 .tree_entries = &child_entries, 340 .event = .{ .patchrev = .{ .base_oid = merged_patchrev.patch_oid, .source_oid = &source_b_oid, .message = "stack another answer change" } }, 341 }, 342 .{ 343 .id = std.fmt.bytesToHex(child_id, .lower), 344 .timestamp = 13, 345 .author = author, 346 .event = .{ .patch = .{ 347 .title = "stack another answer change", 348 .description = "depends on the first patch", 349 .tags = "enhancement", 350 .target_ref = patch.target_ref, 351 .target_patch_id = std.fmt.bytesToHex(patch_id, .lower), 352 .patchrev_id = std.fmt.bytesToHex(child_patchrev_id, .lower), 353 } }, 354 }, 355 }); 356 } 357 358 // 359 // check the derived commit and reverse index 360 // 361 362 _ = arena.reset(.retain_capacity); 363 const stacked_moment = try evt.currentMoment(repo_opts, &target); 364 const child = (try evt.Patch.readById(Repo.DB, repo_opts.hash, stacked_moment, &arena, &child_id)) orelse return error.NotFound; 365 var selected_patchrev_id: [evt.event_id_size]u8 = undefined; 366 _ = try std.fmt.hexToBytes(&selected_patchrev_id, &child.event.patchrev_id); 367 const child_patchrev = (try evt.PatchRev.readById(Repo.DB, repo_opts.hash, stacked_moment, &arena, &selected_patchrev_id)) orelse return error.NotFound; 368 var child_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 369 @memcpy(&child_oid, child_patchrev.patch_oid); 370 { 371 var child_repo_moment = try target.core.latestMoment(); 372 const child_state = Repo.State(.read_only){ .core = &target.core, .extra = .{ .moment = &child_repo_moment } }; 373 var child_object = try obj.Object(.xit, repo_opts).init(child_state, io, allocator, &child_oid); 374 defer child_object.deinit(); 375 const child_commit = switch (child_object.content) { 376 .commit => |commit| commit, 377 else => return error.InvalidObject, 378 }; 379 const child_parents = child_commit.metadata.parent_oids orelse return error.ParentOidsNotFound; 380 try std.testing.expectEqualStrings(child_patchrev.event.base_oid, &child_parents[0]); 381 } 382 383 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; 384 const targets = try Repo.DB.HashMap(.read_only).init(targets_cursor); 385 const children_cursor = try targets.getCursor(hash.hashInt(repo_opts.hash, &patch_id)) orelse return error.NotFound; 386 const children = try Repo.DB.SortedSet(.read_only).init(children_cursor); 387 try std.testing.expectEqual(1, try children.count()); 388 389 // 390 // retain the ref-free commits during garbage collection 391 // 392 393 { 394 const roots = try evt.PatchRev.gcRoots(Repo.DB, repo_opts.hash, allocator, stacked_moment); 395 defer allocator.free(roots); 396 try std.testing.expectEqual(4, roots.len); 397 _ = try target.garbageCollect(io, allocator, roots); 398 399 var after_gc_moment = try target.core.latestMoment(); 400 const after_gc_state = Repo.State(.read_only){ .core = &target.core, .extra = .{ .moment = &after_gc_moment } }; 401 var retained = try obj.Object(.xit, repo_opts).init(after_gc_state, io, allocator, &roots[0]); 402 retained.deinit(); 403 } 404 } 405 406 test "publish a staged patch" { 407 const io = std.testing.io; 408 const allocator = std.testing.allocator; 409 const temp_dir_name = "temp-staged-patch"; 410 const cwd = std.Io.Dir.cwd(); 411 412 if (cwd.openDir(io, temp_dir_name, .{})) |dir_value| { 413 var dir = dir_value; 414 dir.close(io); 415 try cwd.deleteTree(io, temp_dir_name); 416 } else |_| {} 417 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 418 defer cwd.deleteTree(io, temp_dir_name) catch {}; 419 defer temp_dir.close(io); 420 421 const cwd_path = try std.process.currentPathAlloc(io, allocator); 422 defer allocator.free(cwd_path); 423 const root = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 424 defer allocator.free(root); 425 const repos_dir = try std.fs.path.join(allocator, &.{ root, "repos" }); 426 defer allocator.free(repos_dir); 427 const admin_path = try std.fs.path.join(allocator, &.{ root, "admin" }); 428 defer allocator.free(admin_path); 429 const upstream_path = try std.fs.path.join(allocator, &.{ root, "upstream" }); 430 defer allocator.free(upstream_path); 431 432 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 433 const user_id = evt.EventWithId.randomId(prng.random()); 434 const repo_id = evt.EventWithId.randomId(prng.random()); 435 const repo_id_hex = std.fmt.bytesToHex(repo_id, .lower); 436 const target_path = try std.fs.path.join(allocator, &.{ repos_dir, &repo_id_hex }); 437 defer allocator.free(target_path); 438 439 var admin = try rp.Repo(.xit, evt.admin_repo_opts).init(io, allocator, .{ .path = admin_path }); 440 defer admin.deinit(io, allocator); 441 try evt.consume(.xit, evt.admin_repo_opts, io, allocator, &admin, evt.events_ref, &.{ 442 .{ 443 .id = std.fmt.bytesToHex(user_id, .lower), 444 .timestamp = 1, 445 .author = author, 446 .event = .{ .user = .{ 447 .name = author.name, 448 .email = author.email, 449 .password_hash = "", 450 } }, 451 }, 452 .{ 453 .id = repo_id_hex, 454 .timestamp = 1, 455 .author = author, 456 .event = .{ .repo = .{ 457 .user_id = &user_id, 458 .name = "repo", 459 .description = "", 460 } }, 461 }, 462 }); 463 464 // 465 // create the target repo 466 // 467 468 { 469 var upstream = try rp.Repo(.git, .{ .is_test = true }).init(io, allocator, .{ .path = upstream_path }); 470 defer upstream.deinit(io, allocator); 471 const file = try upstream.core.work_dir.createFile(io, "main.zig", .{ .truncate = true }); 472 defer file.close(io); 473 try file.writeStreamingAll(io, "pub fn main() void {}\n"); 474 try upstream.add(io, allocator, &.{"main.zig"}); 475 _ = try upstream.commit(io, allocator, .{ .author = "alice <alice@example.test>", .message = "initial code", .timestamp = 1 }); 476 } 477 var target = try Repo.clone(io, allocator, upstream_path, target_path, target_path, null, .{}); 478 defer target.deinit(io, allocator); 479 try target.addBranch(io, .{ .name = "next" }); 480 481 // 482 // push a proposed commit into a draft 483 // 484 485 const patch_id = evt.EventWithId.randomId(prng.random()); 486 const patch_id_hex = std.fmt.bytesToHex(patch_id, .lower); 487 const draft_path = try fork.forkPath(allocator, repos_dir, &patch_id_hex); 488 defer allocator.free(draft_path); 489 try evt.consume(.xit, evt.admin_repo_opts, io, allocator, &admin, evt.events_ref, &.{.{ 490 .id = patch_id_hex, 491 .timestamp = 2, 492 .author = author, 493 .event = .{ .fork = .{ 494 .user_id = &user_id, 495 .repo_id = &repo_id, 496 .target = "master", 497 } }, 498 }}); 499 try cwd.createDirPath(io, std.fs.path.dirname(draft_path) orelse return error.InvalidPath); 500 var draft = try Repo.clone(io, allocator, upstream_path, draft_path, draft_path, null, .{}); 501 var source_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 502 { 503 defer draft.deinit(io, allocator); 504 const file = try draft.core.work_dir.createFile(io, "feature.zig", .{ .truncate = true }); 505 defer file.close(io); 506 try file.writeStreamingAll(io, "pub const answer = 42;\n"); 507 try draft.add(io, allocator, &.{"feature.zig"}); 508 source_oid = try draft.commit(io, allocator, .{ .author = "alice <alice@example.test>", .message = "add feature", .timestamp = 2 }); 509 try draft.addBranch(io, .{ .name = fork.ref.name }); 510 } 511 try fork.recordPush(io, allocator, &admin, &patch_id_hex, "master", &source_oid, author, 3); 512 513 // 514 // publish the staged code and metadata 515 // 516 517 try fork.publish(repo_opts, io, allocator, &admin, &target, draft_path, .{ 518 .id = patch_id_hex, 519 .user_id = user_id, 520 .repo_id = repo_id, 521 .title = "add the answer", 522 .tags = "enhancement", 523 .description = "adds a reusable answer constant", 524 .author = author, 525 .timestamp = 3, 526 }); 527 528 var arena = std.heap.ArenaAllocator.init(allocator); 529 defer arena.deinit(); 530 var moment = try evt.currentMoment(repo_opts, &target); 531 const patch = (try evt.Patch.readById(Repo.DB, repo_opts.hash, moment, &arena, &patch_id)) orelse return error.NotFound; 532 try std.testing.expectEqualStrings("refs/heads/master", patch.event.target_ref); 533 const first_patchrev_id = try evt.parseEventId(&patch.event.patchrev_id); 534 const first_patchrev = (try evt.PatchRev.readById(Repo.DB, repo_opts.hash, moment, &arena, &first_patchrev_id)) orelse return error.NotFound; 535 try std.testing.expectEqualStrings("add feature", first_patchrev.event.message); 536 const first_patchrev_hex = patch.event.patchrev_id; 537 538 var fork_arena = std.heap.ArenaAllocator.init(allocator); 539 defer fork_arena.deinit(); 540 const admin_moment = try evt.currentMoment(evt.admin_repo_opts, &admin); 541 const published_fork = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &fork_arena, &patch_id)) orelse return error.NotFound; 542 try std.testing.expect(!published_fork.removed); 543 544 // 545 // edit metadata without replacing the code revision 546 // 547 548 try fork.publish(repo_opts, io, allocator, &admin, &target, draft_path, .{ 549 .id = patch_id_hex, 550 .user_id = user_id, 551 .repo_id = repo_id, 552 .title = "add a reusable answer", 553 .tags = "enhancement", 554 .description = "adds a reusable answer constant", 555 .author = author, 556 .timestamp = 4, 557 }); 558 _ = arena.reset(.retain_capacity); 559 moment = try evt.currentMoment(repo_opts, &target); 560 const edited = (try evt.Patch.readById(Repo.DB, repo_opts.hash, moment, &arena, &patch_id)) orelse return error.NotFound; 561 try std.testing.expectEqualStrings(&first_patchrev_hex, &edited.event.patchrev_id); 562 const edited_patchrev_hex = edited.event.patchrev_id; 563 564 // 565 // retargeting creates another code revision 566 // 567 568 try fork.recordPush(io, allocator, &admin, &patch_id_hex, "next", &source_oid, author, 5); 569 try fork.publish(repo_opts, io, allocator, &admin, &target, draft_path, .{ 570 .id = patch_id_hex, 571 .user_id = user_id, 572 .repo_id = repo_id, 573 .title = "add a reusable answer", 574 .tags = "enhancement", 575 .description = "adds a reusable answer constant", 576 .author = author, 577 .timestamp = 5, 578 }); 579 _ = arena.reset(.retain_capacity); 580 moment = try evt.currentMoment(repo_opts, &target); 581 const retargeted = (try evt.Patch.readById(Repo.DB, repo_opts.hash, moment, &arena, &patch_id)) orelse return error.NotFound; 582 try std.testing.expectEqualStrings("refs/heads/next", retargeted.event.target_ref); 583 try std.testing.expect(!std.mem.eql(u8, &edited_patchrev_hex, &retargeted.event.patchrev_id)); 584 } 585