╭ 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 upstream_path = try std.fs.path.join(allocator, &.{ root, "upstream" }); 428 defer allocator.free(upstream_path); 429 const target_path = try std.fs.path.join(allocator, &.{ repos_dir, "target" }); 430 defer allocator.free(target_path); 431 432 // 433 // create the target repo 434 // 435 436 { 437 var upstream = try rp.Repo(.git, .{ .is_test = true }).init(io, allocator, .{ .path = upstream_path }); 438 defer upstream.deinit(io, allocator); 439 const file = try upstream.core.work_dir.createFile(io, "main.zig", .{ .truncate = true }); 440 defer file.close(io); 441 try file.writeStreamingAll(io, "pub fn main() void {}\n"); 442 try upstream.add(io, allocator, &.{"main.zig"}); 443 _ = try upstream.commit(io, allocator, .{ .author = "alice <alice@example.test>", .message = "initial code", .timestamp = 1 }); 444 } 445 var target = try Repo.clone(io, allocator, upstream_path, target_path, target_path, null, .{}); 446 defer target.deinit(io, allocator); 447 try target.addBranch(io, .{ .name = "next" }); 448 449 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 450 const unused_id = evt.EventWithId.randomId(prng.random()); 451 const unused_hex = std.fmt.bytesToHex(unused_id, .lower); 452 453 const route_text = try std.fmt.allocPrint(allocator, "alice/repo/patch:{s}/branch:feature/topic", .{&unused_hex}); 454 defer allocator.free(route_text); 455 const route = fork.parseRoute(route_text) orelse return error.InvalidRoute; 456 try std.testing.expectEqualStrings("alice/repo", route.identity); 457 try std.testing.expectEqualStrings("feature/topic", route.target); 458 459 // 460 // reserve an author-bound draft 461 // 462 463 const unused_path = try fork.create(repo_opts, io, allocator, repos_dir, &unused_hex, author.name); 464 defer allocator.free(unused_path); 465 var unused = try Repo.open(io, allocator, .{ .path = unused_path }); 466 defer unused.deinit(io, allocator); 467 const stored_author = (try fork.draftAuthor(repo_opts, &unused, io, allocator)) orelse return error.NotFound; 468 defer allocator.free(stored_author); 469 try std.testing.expectEqualStrings(author.name, stored_author); 470 var head_buf: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; 471 const head = try unused.head(io, &head_buf); 472 switch (head) { 473 .ref => |head_ref| try std.testing.expectEqualStrings(fork.ref.name, head_ref.name), 474 .oid => return error.InvalidRef, 475 } 476 477 // 478 // push a proposed commit into another draft 479 // 480 481 const patch_id = evt.EventWithId.randomId(prng.random()); 482 const patch_id_hex = std.fmt.bytesToHex(patch_id, .lower); 483 const draft_path = try fork.forkPath(allocator, repos_dir, &patch_id_hex); 484 defer allocator.free(draft_path); 485 try cwd.createDirPath(io, std.fs.path.dirname(draft_path) orelse return error.InvalidPath); 486 var draft = try Repo.clone(io, allocator, upstream_path, draft_path, draft_path, null, .{}); 487 { 488 defer draft.deinit(io, allocator); 489 try draft.addConfig(io, allocator, .{ .name = "haxy.author", .value = author.name }); 490 try draft.addConfig(io, allocator, .{ .name = "haxy.target", .value = "master" }); 491 const file = try draft.core.work_dir.createFile(io, "feature.zig", .{ .truncate = true }); 492 defer file.close(io); 493 try file.writeStreamingAll(io, "pub const answer = 42;\n"); 494 try draft.add(io, allocator, &.{"feature.zig"}); 495 _ = try draft.commit(io, allocator, .{ .author = "alice <alice@example.test>", .message = "add feature", .timestamp = 2 }); 496 try draft.addBranch(io, .{ .name = fork.ref.name }); 497 } 498 499 // 500 // publish the staged code and metadata 501 // 502 503 try fork.publish(repo_opts, io, allocator, &target, draft_path, .{ 504 .id = patch_id_hex, 505 .title = "add the answer", 506 .tags = "enhancement", 507 .description = "adds a reusable answer constant", 508 .author = author, 509 .timestamp = 3, 510 }); 511 512 var arena = std.heap.ArenaAllocator.init(allocator); 513 defer arena.deinit(); 514 var moment = try evt.currentMoment(repo_opts, &target); 515 const patch = (try evt.Patch.readById(Repo.DB, repo_opts.hash, moment, &arena, &patch_id)) orelse return error.NotFound; 516 try std.testing.expectEqualStrings("refs/heads/master", patch.event.target_ref); 517 const first_patchrev_id = try evt.parseEventId(&patch.event.patchrev_id); 518 const first_patchrev = (try evt.PatchRev.readById(Repo.DB, repo_opts.hash, moment, &arena, &first_patchrev_id)) orelse return error.NotFound; 519 try std.testing.expectEqualStrings("add feature", first_patchrev.event.message); 520 521 // 522 // edit metadata without replacing the code revision 523 // 524 525 try fork.publish(repo_opts, io, allocator, &target, draft_path, .{ 526 .id = patch_id_hex, 527 .title = "add a reusable answer", 528 .tags = "enhancement", 529 .description = "adds a reusable answer constant", 530 .author = author, 531 .timestamp = 4, 532 }); 533 _ = arena.reset(.retain_capacity); 534 moment = try evt.currentMoment(repo_opts, &target); 535 const edited = (try evt.Patch.readById(Repo.DB, repo_opts.hash, moment, &arena, &patch_id)) orelse return error.NotFound; 536 try std.testing.expectEqualStrings(&patch.event.patchrev_id, &edited.event.patchrev_id); 537 538 // 539 // retargeting creates another code revision 540 // 541 542 var reopened = try Repo.open(io, allocator, .{ .path = draft_path }); 543 defer reopened.deinit(io, allocator); 544 try fork.setTarget(repo_opts, &reopened, io, allocator, "next"); 545 try fork.publish(repo_opts, io, allocator, &target, draft_path, .{ 546 .id = patch_id_hex, 547 .title = edited.event.title, 548 .tags = edited.event.tags, 549 .description = edited.event.description, 550 .author = author, 551 .timestamp = 5, 552 }); 553 _ = arena.reset(.retain_capacity); 554 moment = try evt.currentMoment(repo_opts, &target); 555 const retargeted = (try evt.Patch.readById(Repo.DB, repo_opts.hash, moment, &arena, &patch_id)) orelse return error.NotFound; 556 try std.testing.expectEqualStrings("refs/heads/next", retargeted.event.target_ref); 557 try std.testing.expect(!std.mem.eql(u8, &edited.event.patchrev_id, &retargeted.event.patchrev_id)); 558 } 559