╭ changed ───────────────╮ │src/test/event/patch.zig│ ╰────────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────╮ │ 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); │ ╰──────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────╮ │111 .description = "adds a reusable answer constant", │ │112 .tags = "enhancement", │ │113 .target_ref = "refs/heads/master", │ │113 - .source_url = source_path, │ │114 - .source_ref = "refs/heads/master", │ │114 .patchrev_id = std.fmt.bytesToHex(patchrev_id, .lower),│ │115 }; │ │116 const tree_entries = [_]evt.EventTreeEntry{ │ ╰────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │349 .tags = "enhancement", │ │350 .target_ref = patch.target_ref, │ │351 .target_patch_id = std.fmt.bytesToHex(patch_id, .lower), │ │353 - .source_url = patch.source_url, │ │354 - .source_ref = patch.source_ref, │ │352 .patchrev_id = std.fmt.bytesToHex(child_patchrev_id, .lower),│ │353 } }, │ │354 }, │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 + │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯