╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ───────────────╮ │src/test/event/patch.zig│ ╰────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │566 // │ │567 │ │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, .{ │ │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, │ │547 - .title = edited.event.title, │ │548 - .tags = edited.event.tags, │ │549 - .description = edited.event.description, │ │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 }); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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); │ │557 - try std.testing.expect(!std.mem.eql(u8, &edited.event.patchrev_id, &retargeted.event.patchrev_id)); │ │583 + try std.testing.expect(!std.mem.eql(u8, &edited_patchrev_hex, &retargeted.event.patchrev_id)); │ │584 } │ │585 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ added ─────────────────╮ │src/test/event/admin.zig│ ╰────────────────────────╯ ╭ changed ───────╮ │src/test/net.zig│ ╰────────────────╯ ╭──────────────────────────────╮ │3 const hx = @import("haxy");│ │4 const xit = hx.xit; │ │5 const evt = hx.event; │ │6 +const fork = hx.fork; │ │7 const rp = xit.repo; │ │8 const rf = xit.ref; │ │9 const work = xit.workdir; │ ╰──────────────────────────────╯ ╭──────────────────────────────────────────────────╮ │78 } │ │79 } │ │80 │ │81 +test "push fork" { │ │82 + const io = std.testing.io; │ │83 + const allocator = std.testing.allocator; │ │84 + if (.windows != builtin.os.tag) { │ │85 + try testPushFork(3024, io, allocator);│ │86 + } │ │87 +} │ │88 + │ │89 fn runServer( │ │90 io: std.Io, │ │91 allocator: std.mem.Allocator, │ ╰──────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │521 try std.testing.expect(null == try server_repo.readRef(io, .{ .kind = .tag, .name = "1.0.0" })); │ │522 } │ │523 │ │524 +fn testPushFork( │ │525 + comptime port: u16, │ │526 + io: std.Io, │ │527 + allocator: std.mem.Allocator, │ │528 +) !void { │ │529 + const temp_dir_name = "temp-testnet-push-fork"; │ │530 + const cwd = std.Io.Dir.cwd(); │ │531 + var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); │ │532 + if (temp_dir_or_err) |*temp_dir| { │ │533 + temp_dir.close(io); │ │534 + try cwd.deleteTree(io, temp_dir_name); │ │535 + } else |_| {} │ │536 + var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); │ │537 + defer cwd.deleteTree(io, temp_dir_name) catch {}; │ │538 + defer temp_dir.close(io); │ │539 + │ │540 + var server_process = try runServer(io, allocator, temp_dir_name, port); │ │541 + defer _ = server_process.kill(io); │ │542 + │ │543 + const target_path = (try repoOnDiskPath(io, allocator, temp_dir_name, "target", true)).?; │ │544 + defer allocator.free(target_path); │ │545 + var target_repo = try rp.Repo(.xit, .{}).init(io, allocator, .{ .path = target_path }); │ │546 + target_repo.deinit(io, allocator); │ │547 + │ │548 + const other_path = (try repoOnDiskPath(io, allocator, temp_dir_name, "other", true)).?; │ │549 + defer allocator.free(other_path); │ │550 + │ │551 + const cwd_path = try std.process.currentPathAlloc(io, allocator); │ │552 + defer allocator.free(cwd_path); │ │553 + const admin_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "admin" }); │ │554 + defer allocator.free(admin_path); │ │555 + const repos_dir = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "repos" }); │ │556 + defer allocator.free(repos_dir); │ │557 + │ │558 + var prng = std.Random.DefaultPrng.init(std.testing.random_seed); │ │559 + const fork_id = evt.EventWithId.randomId(prng.random()); │ │560 + const fork_id_hex = std.fmt.bytesToHex(fork_id, .lower); │ │561 + var user_id: [evt.event_id_size]u8 = undefined; │ │562 + var repo_id: [evt.event_id_size]u8 = undefined; │ │563 + const draft_path = blk: { │ │564 + var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_path }); │ │565 + defer admin.deinit(io, allocator); │ │566 + var arena = std.heap.ArenaAllocator.init(allocator); │ │567 + defer arena.deinit(); │ │568 + const moment = try evt.currentMoment(evt.admin_repo_opts, &admin); │ │569 + const target = (try evt.Repo.readByOwnerAndName(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, "admin", "target")) orelse return error.NotFound; │ │570 + repo_id = target.event_id; │ │571 + @memcpy(&user_id, target.repo.event.user_id); │ │572 + break :blk try fork.create(.{}, io, allocator, repos_dir, &admin, .{ │ │573 + .id = fork_id_hex, │ │574 + .user_id = user_id, │ │575 + .repo_id = repo_id, │ │576 + .target = "master", │ │577 + .author = .{ .name = "admin", .email = "admin@example.test" }, │ │578 + .timestamp = 1, │ │579 + }); │ │580 + }; │ │581 + defer allocator.free(draft_path); │ │582 + │ │583 + const client_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "client" }); │ │584 + defer allocator.free(client_path); │ │585 + var client = try rp.Repo(.xit, .{ .is_test = true }).init(io, allocator, .{ .path = client_path, .create_default_branch = fork.ref.name }); │ │586 + defer client.deinit(io, allocator); │ │587 + │ │588 + const source_oid = blk: { │ │589 + const file = try client.core.work_dir.createFile(io, "feature.txt", .{ .truncate = true }); │ │590 + defer file.close(io); │ │591 + try file.writeStreamingAll(io, "fork contents\n"); │ │592 + try client.add(io, allocator, &.{"feature.txt"}); │ │593 + break :blk try client.commit(io, allocator, .{ .message = "add feature" }); │ │594 + }; │ │595 + │ │596 + const remote_url = try std.fmt.allocPrint(allocator, "git@localhost:admin/target/patch:{s}/branch:master", .{&fork_id_hex}); │ │597 + defer allocator.free(remote_url); │ │598 + try client.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); │ │599 + const ssh_cmd = (try sshCommand(true, allocator, cwd_path, temp_dir_name, port)).?; │ │600 + defer allocator.free(ssh_cmd); │ │601 + try client.push(io, allocator, "origin", fork.ref.name, false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } }); │ │602 + │ │603 + { │ │604 + var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_path }); │ │605 + defer admin.deinit(io, allocator); │ │606 + var arena = std.heap.ArenaAllocator.init(allocator); │ │607 + defer arena.deinit(); │ │608 + const moment = try evt.currentMoment(evt.admin_repo_opts, &admin); │ │609 + const record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, &fork_id)) orelse return error.NotFound; │ │610 + try std.testing.expect(!record.removed); │ │611 + try std.testing.expectEqualStrings("master", record.event.target); │ │612 + try std.testing.expectEqualStrings(&source_oid, record.event.source_oid orelse return error.NotFound); │ │613 + } │ │614 + │ │615 + const other_url = try std.fmt.allocPrint(allocator, "git@localhost:admin/other/patch:{s}/branch:other", .{&fork_id_hex}); │ │616 + defer allocator.free(other_url); │ │617 + try client.addRemote(io, allocator, .{ .name = "other", .value = other_url }); │ │618 + const wrong_repo_rejected = if (client.push(io, allocator, "other", fork.ref.name, false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } })) |_| false else |_| true; │ │619 + try std.testing.expect(wrong_repo_rejected); │ │620 + │ │621 + { │ │622 + var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_path }); │ │623 + defer admin.deinit(io, allocator); │ │624 + try evt.consume(.xit, evt.admin_repo_opts, io, allocator, &admin, evt.events_ref, &.{.{ │ │625 + .id = std.fmt.bytesToHex(user_id, .lower), │ │626 + .timestamp = 2, │ │627 + .author = .{ .name = "admin", .email = "admin@example.test" }, │ │628 + .event = .{ .user = .{ │ │629 + .name = "admin", │ │630 + .email = "admin@example.test", │ │631 + .password_hash = "", │ │632 + .ssh_keys = "", │ │633 + } }, │ │634 + }}); │ │635 + } │ │636 + │ │637 + const denied_url = try std.fmt.allocPrint(allocator, "git@localhost:admin/target/patch:{s}/branch:denied", .{&fork_id_hex}); │ │638 + defer allocator.free(denied_url); │ │639 + try client.addRemote(io, allocator, .{ .name = "denied", .value = denied_url }); │ │640 + const unauthorized_rejected = if (client.push(io, allocator, "denied", fork.ref.name, false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } })) |_| false else |_| true;│ │641 + try std.testing.expect(unauthorized_rejected); │ │642 +} │ │643 + │ │644 fn testPushCreatesMissingRepo( │ │645 comptime repo_kind: rp.RepoKind, │ │646 comptime transport_def: net.TransportDefinition, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Events.zig│ ╰──────────────────────╯ ╭────────────────────────────────────────────────╮ │189 const T = switch (event_kind) {│ │190 .user => evt.User, │ │191 .repo => evt.Repo, │ │192 + .fork => evt.Fork, │ │193 .issue => evt.Issue, │ │194 .discuss => evt.Discussion,│ │195 .comment => evt.Comment, │ ╰────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │226 }, │ │227 .repo => { │ │228 const record = (try readRecord(evt.Repo, hash_kind, arena, haxy_moment, id)) orelse return null; │ │229 + if (admin_moment) |moment| { │ │230 + if (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, record.event.user_id)) |user|│ │231 + item.author = .{ .user_name = user.event.name }; │ │232 + } │ │233 + }, │ │234 + .fork => { │ │235 + const record = (try readRecord(evt.Fork, hash_kind, arena, haxy_moment, id)) orelse return null; │ │236 if (admin_moment) |moment| { │ │237 if (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, record.event.user_id)) |user|│ │238 item.author = .{ .user_name = user.event.name }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯