╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ net.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
585 .title = "add a feature", 586 .description = "a draft patch", 587 .tags = "enhancement", 588 .target_branch = "master", 589 .author = .{ .name = "admin", .email = "admin@example.test" }, 590 .timestamp = 1, 591 }); 592 }; 593 defer allocator.free(draft_path); 594 595 const client_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "client" }); 596 defer allocator.free(client_path); 597 const ssh_cmd = (try sshCommand(true, allocator, cwd_path, temp_dir_name, port)).?; 598 defer allocator.free(ssh_cmd); 599 var client = try rp.Repo(.xit, .{ .is_test = true }).clone( 600 io, 601 allocator, 602 "git@localhost:admin/target", 603 client_path, 604 client_path, 605 null, 606 .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } }, 607 ); 608 defer client.deinit(io, allocator); 609 610 const source_oid = blk: { 611 const file = try client.core.work_dir.createFile(io, "feature.txt", .{ .truncate = true }); 612 defer file.close(io); 613 try file.writeStreamingAll(io, "fork contents\n"); 614 try client.add(io, allocator, &.{"feature.txt"}); 615 break :blk try client.commit(io, allocator, .{ .message = "add feature" }); 616 }; 617 618 const remote_url = try std.fmt.allocPrint(allocator, "git@localhost:fork/admin/target/patch:{s}", .{&fork_id_hex}); 619 defer allocator.free(remote_url); 620 try client.addRemote(io, allocator, .{ .name = "patch", .value = remote_url }); 621 try client.push(io, allocator, "patch", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } }); 622 623 var first_revision_id: [evt.event_id_size]u8 = undefined; 624 { 625 var draft = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = draft_path }); 626 defer draft.deinit(io, allocator); 627 var arena = std.heap.ArenaAllocator.init(allocator); 628 defer arena.deinit(); 629 const moment = try evt.currentMoment(.{}, &draft); 630 const patch = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &arena, &fork_id)) orelse return error.NotFound; 631 try std.testing.expect(!patch.removed); 632 try std.testing.expectEqualStrings(&source_oid, &(try draft.readRef(io, fork.ref) orelse return error.NotFound)); 633 const selected = patch.event.revision orelse return error.NotFound; 634 try std.testing.expectEqualStrings("master", patch.event.target_branch); 635 try std.testing.expectEqualStrings(&source_oid, selected.source_oid); 636 first_revision_id = try evt.parseEventId(&selected.id); 637 const revision = (try evt.PatchRev.readById(evt.EventDB(.sha1), .sha1, moment, &arena, &first_revision_id)) orelse return error.NotFound; 638 try std.testing.expectEqualStrings(&source_oid, revision.event.source_oid); 639 } 640 641 // 642 // publish, then push another revision 643 // 644 645 { 646 var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_path }); 647 defer admin.deinit(io, allocator); 648 var target = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = target_path }); 649 defer target.deinit(io, allocator); 650 try target.addBranch(io, .{ .name = "feature" }); 651 try pch.publish(.{}, io, allocator, &admin, &target, draft_path, .{ 652 .id = fork_id_hex, 653 .user_id = user_id, 654 .repo_id = repo_id, 655 .author = .{ .name = "admin", .email = "admin@example.test" }, 656 .timestamp = 2, 657 }); 658 var arena = std.heap.ArenaAllocator.init(allocator); 659 defer arena.deinit(); 660 const moment = try evt.currentMoment(.{}, &target); 661 const patch = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &arena, &fork_id)) orelse return error.NotFound; 662 try evt.Patch.update(.xit, .{}, io, allocator, &target, &fork_id, .{ .fields = .{ 663 .title = "edited feature", 664 .description = patch.event.description, 665 .tags = patch.event.tags, 666 .target_branch = "feature", 667 } }, .{ .name = "admin", .email = "admin@example.test" }); 668 } 669 670 const feature_remote_url = try std.fmt.allocPrint(allocator, "git@localhost:fork/admin/target/patch:{s}", .{&fork_id_hex}); 671 defer allocator.free(feature_remote_url); 672 try client.addRemote(io, allocator, .{ .name = "patch-to-feature", .value = feature_remote_url }); 673 674 const second_source_oid = blk: { 675 const file = try client.core.work_dir.createFile(io, "feature.txt", .{ .truncate = true }); 676 defer file.close(io); 677 try file.writeStreamingAll(io, "updated fork contents\n"); 678 try client.add(io, allocator, &.{"feature.txt"}); 679 break :blk try client.commit(io, allocator, .{ .message = "revise feature" }); 680 }; 681 try client.push(io, allocator, "patch-to-feature", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } }); 682 683 var second_revision_id: [evt.event_id_size]u8 = undefined; 684 var second_patch_oid: [hash.hexLen(.sha1)]u8 = undefined; 685 { 686 var draft = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = draft_path }); 687 defer draft.deinit(io, allocator); 688 var arena = std.heap.ArenaAllocator.init(allocator); 689 defer arena.deinit(); 690 const moment = try evt.currentMoment(.{}, &draft); 691 const patch = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &arena, &fork_id)) orelse return error.NotFound; 692 try std.testing.expect(patch.removed); 693 const newest = (try evt.PatchRev.readNewest(evt.EventDB(.sha1), .sha1, moment, &arena)) orelse return error.NotFound; 694 second_revision_id = newest.id; 695 @memcpy(&second_patch_oid, newest.record.patch_oid); 696 try std.testing.expect(!std.mem.eql(u8, &first_revision_id, &second_revision_id)); 697 try std.testing.expectEqualStrings(&second_source_oid, newest.record.event.source_oid); 698 try std.testing.expectEqualStrings("edited feature", newest.record.event.message); 699 } 700 { 701 var target = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = target_path }); 702 defer target.deinit(io, allocator); 703 var arena = std.heap.ArenaAllocator.init(allocator); 704 defer arena.deinit(); 705 const moment = try evt.currentMoment(.{}, &target); 706 const patch = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &arena, &fork_id)) orelse return error.NotFound; 707 try std.testing.expectEqualStrings("edited feature", patch.event.title); 708 const selected = patch.event.revision orelse return error.NotFound; 709 try std.testing.expectEqualStrings(&std.fmt.bytesToHex(second_revision_id, .lower), &selected.id); 710 try std.testing.expectEqualStrings(&second_source_oid, selected.source_oid); 711 try std.testing.expectEqualStrings("feature", patch.event.target_branch); 712 } 713 714 // 715 // reject extra refs without applying the patch update 716 // 717 718 const third_source_oid = blk: { 719 const file = try client.core.work_dir.createFile(io, "feature.txt", .{ .truncate = true }); 720 defer file.close(io); 721 try file.writeStreamingAll(io, "rejected fork contents\n"); 722 try client.add(io, allocator, &.{"feature.txt"}); 723 break :blk try client.commit(io, allocator, .{ .message = "rejected revision" }); 724 }; 725 const extra_ref_rejected = if (client.push(io, allocator, "patch-to-feature", "master:patch", false, .{ 726 .refspecs = &.{"refs/heads/master:refs/heads/extra"}, 727 .wire = .{ .ssh = .{ .command = ssh_cmd } }, 728 })) |_| false else |_| true; 729 try std.testing.expect(extra_ref_rejected); 730 { 731 var draft = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = draft_path }); 732 defer draft.deinit(io, allocator); 733 try std.testing.expectEqualStrings(&second_source_oid, &(try draft.readRef(io, fork.ref) orelse return error.NotFound)); 734 try std.testing.expectEqual(null, try draft.readRef(io, .{ .kind = .head, .name = "extra" })); 735 var arena = std.heap.ArenaAllocator.init(allocator); 736 defer arena.deinit(); 737 const moment = try evt.currentMoment(.{}, &draft); 738 const newest = (try evt.PatchRev.readNewest(evt.EventDB(.sha1), .sha1, moment, &arena)) orelse return error.NotFound; 739 try std.testing.expectEqualSlices(u8, &second_revision_id, &newest.id); 740 } 741 742 // 743 // a removed target patch stays removed after another push 744 // 745 746 { 747 var target = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = target_path }); 748 defer target.deinit(io, allocator); 749 try evt.remove(.repo, .xit, .{}, io, allocator, &target, &fork_id, .patch, .{ .name = "admin", .email = "admin@example.test" }); 750 } 751 try client.push(io, allocator, "patch-to-feature", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } }); 752 { 753 var target = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = target_path }); 754 defer target.deinit(io, allocator); 755 var arena = std.heap.ArenaAllocator.init(allocator); 756 defer arena.deinit(); 757 const moment = try evt.currentMoment(.{}, &target); 758 const patch = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &arena, &fork_id)) orelse return error.NotFound; 759 try std.testing.expect(patch.removed); 760 } 761 762 // 763 // restore the patch, merge its source oid, and push the merge 764 // 765 766 { 767 var target = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = target_path }); 768 defer target.deinit(io, allocator); 769 var arena = std.heap.ArenaAllocator.init(allocator); 770 defer arena.deinit(); 771 const moment = try evt.currentMoment(.{}, &target); 772 const removed = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &arena, &fork_id)) orelse return error.NotFound; 773 try evt.consume(.repo, .xit, .{}, io, allocator, &target, evt.events_ref, &.{.{ 774 .id = fork_id_hex, 775 .timestamp = 4, 776 .author = .{ .name = "admin", .email = "admin@example.test" }, 777 .event = .{ .patch = removed.event }, 778 }}); 779 } 780 const merge_parents = [_][hash.hexLen(.sha1)]u8{ third_source_oid, second_source_oid }; 781 _ = try client.commit(io, allocator, .{ .message = "merge patch", .parent_oids = &merge_parents }); 782 try client.push(io, allocator, "origin", "master:feature", false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } }); 783 { 784 var target = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = target_path }); 785 defer target.deinit(io, allocator); 786 var arena = std.heap.ArenaAllocator.init(allocator); 787 defer arena.deinit(); 788 const moment = try evt.currentMoment(.{}, &target); 789 const patch = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &arena, &fork_id)) orelse return error.NotFound; 790 try std.testing.expectEqual(.merged, patch.event.status.kind()); 791 const merged_oid = switch (patch.event.status) { 792 .merged => |oid| oid, 793 else => return error.InvalidPatch, 794 }; 795 try std.testing.expectEqualStrings(&second_source_oid, merged_oid); 796 const imported = (try evt.PatchRev.readById(evt.EventDB(.sha1), .sha1, moment, &arena, &second_revision_id)) orelse return error.NotFound; 797 try std.testing.expectEqualStrings(&second_patch_oid, imported.patch_oid); 798 } 799 800 const other_url = try std.fmt.allocPrint(allocator, "git@localhost:fork/admin/other/patch:{s}", .{&fork_id_hex}); 801 defer allocator.free(other_url); 802 try client.addRemote(io, allocator, .{ .name = "other", .value = other_url }); 803 const wrong_repo_rejected = if (client.push(io, allocator, "other", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } })) |_| false else |_| true; 804 try std.testing.expect(wrong_repo_rejected); 805 806 // 807 // the fork remains cloneable after the target repo is removed 808 // 809 810 { 811 var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_path }); 812 defer admin.deinit(io, allocator); 813 try evt.remove(.admin, .xit, evt.admin_repo_opts, io, allocator, &admin, &repo_id, .repo, .{ .name = "admin", .email = "admin@example.test" }); 814 } 815 const clone_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "fork-clone" }); 816 defer allocator.free(clone_path); 817 var fork_clone = try rp.Repo(.xit, .{ .is_test = true }).clone( 818 io, 819 allocator, 820 remote_url, 821 clone_path, 822 clone_path, 823 null, 824 .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } }, 825 ); 826 defer fork_clone.deinit(io, allocator); 827 try std.testing.expectEqualStrings(&third_source_oid, &(try fork_clone.readRef(io, fork.ref) orelse return error.NotFound)); 828 829 { 830 var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_path }); 831 defer admin.deinit(io, allocator); 832 try evt.consume(.admin, .xit, evt.admin_repo_opts, io, allocator, &admin, evt.events_ref, &.{ 833 .{ 834 .id = std.fmt.bytesToHex(repo_id, .lower), 835 .timestamp = 2, 836 .author = .{ .name = "admin", .email = "admin@example.test" }, 837 .event = .{ .repo = .{ 838 .user_id = &user_id, 839 .name = "target", 840 .description = "", 841 } }, 842 }, 843 .{ 844 .id = std.fmt.bytesToHex(user_id, .lower), 845 .timestamp = 2, 846 .author = .{ .name = "admin", .email = "admin@example.test" }, 847 .event = .{ .user = .{ 848 .name = "admin", 849 .email = "admin@example.test", 850 .password_hash = "", 851 .ssh_keys = "", 852 } }, 853 }, 854 }); 855 } 856 857 const denied_url = try std.fmt.allocPrint(allocator, "git@localhost:fork/admin/target/patch:{s}", .{&fork_id_hex}); 858 defer allocator.free(denied_url); 859 try client.addRemote(io, allocator, .{ .name = "denied", .value = denied_url }); 860 const unauthorized_rejected = if (client.push(io, allocator, "denied", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } })) |_| false else |_| true; 861 try std.testing.expect(unauthorized_rejected); 862 } 863 864 fn testPushCreatesMissingRepo( 865 comptime repo_kind: rp.RepoKind, 866 comptime transport_def: net.TransportDefinition, 867 comptime port: u16, 868 io: std.Io, 869 allocator: std.mem.Allocator, 870 ) !void { 871 const temp_dir_name = "temp-testnet-push-create"; 872 873 const cwd = std.Io.Dir.cwd(); 874 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 875 if (temp_dir_or_err) |*temp_dir| { 876 temp_dir.close(io); 877 try cwd.deleteTree(io, temp_dir_name); 878 } else |_| {} 879 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 880 defer cwd.deleteTree(io, temp_dir_name) catch {}; 881 defer temp_dir.close(io); 882 883 var server_process = try runServer(io, allocator, temp_dir_name, port); 884 defer _ = server_process.kill(io); 885 886 const cwd_path = try std.process.currentPathAlloc(io, allocator); 887 defer allocator.free(cwd_path); 888 889 const client_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "client" }); 890 defer allocator.free(client_path); 891 892 var client_repo = try rp.Repo(repo_kind, .{ .is_test = true }).init(io, allocator, .{ .path = client_path }); 893 defer client_repo.deinit(io, allocator); 894 895 const commit1 = blk: { 896 const hello_txt = try client_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 897 defer hello_txt.close(io); 898 try hello_txt.writeStreamingAll(io, "hello, world!"); 899 try client_repo.add(io, allocator, &.{"hello.txt"}); 900 break :blk try client_repo.commit(io, allocator, .{ .message = "let there be light" }); 901 }; 902 903 _ = try client_repo.addTag(io, allocator, .{ .name = "1.0.0", .message = "hi" }); 904 905 // the repo is not pre-registered; the push should mint the repo event under 906 // admin and create the repo on disk 907 { 908 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 909 defer allocator.free(remote_url); 910 911 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 912 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 913 } 914 915 const is_ssh = switch (transport_def) { 916 .file => false, 917 .wire => |wire_kind| .ssh == wire_kind, 918 }; 919 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, cwd_path, temp_dir_name, port); 920 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 921 922 try client_repo.push( 923 io, 924 allocator, 925 "origin", 926 "master", 927 false, 928 .{ .refspecs = &.{"refs/tags/1.0.0:refs/tags/1.0.0"}, .wire = .{ .ssh = .{ 929 .command = ssh_cmd_maybe, 930 } } }, 931 ); 932 933 // the push registered admin/server; resolve its on-disk directory 934 const server_path = (try repoOnDiskPath(io, allocator, temp_dir_name, "testrepo", false)).?; 935 defer allocator.free(server_path); 936 937 var server_repo = try rp.Repo(.xit, .{ .is_test = true }).open(io, allocator, .{ .path = server_path }); 938 defer server_repo.deinit(io, allocator); 939 940 try std.testing.expect(null != try server_repo.readRef(io, .{ .kind = .tag, .name = "1.0.0" })); 941 942 const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 943 try std.testing.expectEqualStrings(&commit1, &oid_master); 944 } 945 946 fn testPushEvents( 947 comptime repo_kind: rp.RepoKind, 948 comptime transport_def: net.TransportDefinition, 949 comptime port: u16, 950 io: std.Io, 951 allocator: std.mem.Allocator, 952 ) !void { 953 const temp_dir_name = "temp-testnet-push-events"; 954 955 // create the temp dir 956 const cwd = std.Io.Dir.cwd(); 957 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 958 if (temp_dir_or_err) |*temp_dir| { 959 temp_dir.close(io); 960 try cwd.deleteTree(io, temp_dir_name); 961 } else |_| {} 962 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 963 defer cwd.deleteTree(io, temp_dir_name) catch {}; 964 defer temp_dir.close(io); 965 966 // init server 967 var server_process = try runServer(io, allocator, temp_dir_name, port); 968 defer _ = server_process.kill(io); 969 970 // register the repo under admin and locate its on-disk directory 971 const server_path = (try repoOnDiskPath(io, allocator, temp_dir_name, "testrepo", true)).?; 972 defer allocator.free(server_path); 973 974 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 975 const ServerRepo = rp.Repo(.xit, repo_opts); 976 var server_repo = try ServerRepo.init(io, allocator, .{ .path = server_path }); 977 defer server_repo.deinit(io, allocator); 978 979 const cwd_path = try std.process.currentPathAlloc(io, allocator); 980 defer allocator.free(cwd_path); 981 982 const client_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "client" }); 983 defer allocator.free(client_path); 984 985 var client_repo = try rp.Repo(repo_kind, .{ .is_test = true }).init(io, allocator, .{ .path = client_path }); 986 defer client_repo.deinit(io, allocator); 987 988 // add remote 989 { 990 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 991 defer allocator.free(remote_url); 992 993 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 994 } 995 996 const is_ssh = switch (transport_def) { 997 .file => false, 998 .wire => |wire_kind| .ssh == wire_kind, 999 }; 1000 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, cwd_path, temp_dir_name, port); 1001 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1002 1003 var arena = std.heap.ArenaAllocator.init(allocator); 1004 defer arena.deinit(); 1005 1006 // 1007 // define test events 1008 // 1009 1010 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 1011 const issue_event_id = evt.EventWithId.randomId(prng.random()); 1012 1013 const events_to_push = [_]evt.EventWithId{ 1014 .{ 1015 .id = std.fmt.bytesToHex(issue_event_id, .lower), 1016 .author = .{ .name = "haxy", .email = "user@haxy" }, 1017 .event = .{ 1018 .issue = .{ 1019 .title = "Login form clears password on validation error", 1020 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 1021 .tags = "bug priority-high ui", 1022 }, 1023 }, 1024 }, 1025 }; 1026 1027 // commit the issue on the client and push it to the server 1028 try evt.consume(.repo, repo_kind, .{ .is_test = true }, io, allocator, &client_repo, evt.events_ref, &events_to_push); 1029 try client_repo.push( 1030 io, 1031 allocator, 1032 "origin", 1033 "haxy/events", 1034 false, 1035 .{ .wire = .{ .ssh = .{ 1036 .command = ssh_cmd_maybe, 1037 } } }, 1038 ); 1039 1040 // the push consumed the issue into the server repo's db 1041 { 1042 const haxy_moment = try evt.currentMoment(repo_opts, &server_repo); 1043 const records_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.record_map_key)) orelse return error.NotFound; 1044 const records = try ServerRepo.DB.HashMap(.read_only).init(records_cursor); 1045 const issue_cursor = try records.getCursor(hash.hashInt(repo_opts.hash, &issue_event_id)) orelse return error.NotFound; 1046 const issue = try evt.read(evt.Issue.Record, ServerRepo.DB, repo_opts.hash, &arena, try ServerRepo.DB.HashMap(.read_only).init(issue_cursor)); 1047 1048 try std.testing.expectEqualStrings(events_to_push[0].event.issue.?.description, issue.event.description); 1049 try std.testing.expectEqualStrings(events_to_push[0].event.issue.?.tags, issue.event.tags); 1050 } 1051 1052 // 1053 // edit the issue 1054 // 1055 1056 // this event edits the previous one because it has the same id 1057 const events_to_push2 = [_]evt.EventWithId{ 1058 .{ 1059 .id = std.fmt.bytesToHex(issue_event_id, .lower), 1060 .author = .{ .name = "haxy", .email = "user@haxy" }, 1061 .event = .{ 1062 .issue = .{ 1063 .title = "Login form clears password on validation error", 1064 .description = "Submitting an invalid email address resets the password field and removes typed input. Preserve the field value and show an inline validation message.", 1065 .tags = "bug priority-low ui", 1066 }, 1067 }, 1068 }, 1069 }; 1070 1071 // commit the edit on the client and push it to the server 1072 try evt.consume(.repo, repo_kind, .{ .is_test = true }, io, allocator, &client_repo, evt.events_ref, &events_to_push2); 1073 try client_repo.push( 1074 io, 1075 allocator, 1076 "origin", 1077 "haxy/events", 1078 false, 1079 .{ .wire = .{ .ssh = .{ 1080 .command = ssh_cmd_maybe, 1081 } } }, 1082 ); 1083 1084 // the push consumed the edit into the server repo's db 1085 { 1086 const haxy_moment = try evt.currentMoment(repo_opts, &server_repo); 1087 const records_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.record_map_key)) orelse return error.NotFound; 1088 const records = try ServerRepo.DB.HashMap(.read_only).init(records_cursor); 1089 const issue_cursor = try records.getCursor(hash.hashInt(repo_opts.hash, &issue_event_id)) orelse return error.NotFound; 1090 const issue = try evt.read(evt.Issue.Record, ServerRepo.DB, repo_opts.hash, &arena, try ServerRepo.DB.HashMap(.read_only).init(issue_cursor)); 1091 1092 // the description and tags were correctly edited 1093 try std.testing.expectEqualStrings(events_to_push2[0].event.issue.?.description, issue.event.description); 1094 try std.testing.expectEqualStrings(events_to_push2[0].event.issue.?.tags, issue.event.tags); 1095 } 1096 } 1097 1098 fn testClone( 1099 comptime repo_kind: rp.RepoKind, 1100 comptime transport_def: net.TransportDefinition, 1101 comptime shell_out_to_git: bool, 1102 comptime port: u16, 1103 io: std.Io, 1104 allocator: std.mem.Allocator, 1105 ) !void { 1106 const temp_dir_name = "temp-testnet-clone"; 1107 1108 // create the temp dir 1109 const cwd = std.Io.Dir.cwd(); 1110 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 1111 if (temp_dir_or_err) |*temp_dir| { 1112 temp_dir.close(io); 1113 try cwd.deleteTree(io, temp_dir_name); 1114 } else |_| {} 1115 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 1116 defer cwd.deleteTree(io, temp_dir_name) catch {}; 1117 defer temp_dir.close(io); 1118 1119 // init server 1120 var server_process = try runServer(io, allocator, temp_dir_name, port); 1121 defer _ = server_process.kill(io); 1122 1123 const cwd_path = try std.process.currentPathAlloc(io, allocator); 1124 defer allocator.free(cwd_path); 1125 1126 const temp_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 1127 defer allocator.free(temp_path); 1128 1129 // register the repo under admin and locate its on-disk directory 1130 const server_path = (try repoOnDiskPath(io, allocator, temp_dir_name, "testrepo", true)).?; 1131 defer allocator.free(server_path); 1132 1133 // init server repo with default branch name as main 1134 // is_test must be false when shell_out_to_git so commits get real timestamps (needed for --shallow-since) 1135 var server_repo = try rp.Repo(.xit, .{ .is_test = !shell_out_to_git }).init(io, allocator, .{ .path = server_path, .create_default_branch = "main" }); 1136 defer server_repo.deinit(io, allocator); 1137 1138 if (shell_out_to_git) { 1139 try server_repo.addConfig(io, allocator, .{ .name = "user.name", .value = "test" }); 1140 try server_repo.addConfig(io, allocator, .{ .name = "user.email", .value = "test@test" }); 1141 try server_repo.addConfig(io, allocator, .{ .name = "uploadpack.allowfilter", .value = "true" }); 1142 } 1143 1144 // make a commit 1145 { 1146 const hello_txt = try server_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 1147 defer hello_txt.close(io); 1148 try hello_txt.writeStreamingAll(io, "hello, world!"); 1149 try server_repo.add(io, allocator, &.{"hello.txt"}); 1150 _ = try server_repo.commit(io, allocator, .{ .message = "let there be light" }); 1151 } 1152 1153 // tag first commit 1154 _ = try server_repo.addTag(io, allocator, .{ .name = "v1", .message = "first" }); 1155 1156 // make a commit 1157 { 1158 const goodbye_txt = try server_repo.core.work_dir.createFile(io, "goodbye.txt", .{ .truncate = true }); 1159 defer goodbye_txt.close(io); 1160 try goodbye_txt.writeStreamingAll(io, "goodbye, world!"); 1161 try server_repo.add(io, allocator, &.{"goodbye.txt"}); 1162 _ = try server_repo.commit(io, allocator, .{ .message = "add goodbye file" }); 1163 } 1164 1165 // export server repo 1166 { 1167 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1168 defer export_file.close(io); 1169 } 1170 1171 const client_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "client" }); 1172 defer allocator.free(client_path); 1173 1174 // get remote url 1175 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 1176 defer allocator.free(remote_url); 1177 1178 const is_ssh = switch (transport_def) { 1179 .file => false, 1180 .wire => |wire_kind| .ssh == wire_kind, 1181 }; 1182 1183 if (shell_out_to_git) { 1184 const priv_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 1185 defer allocator.free(priv_key_path); 1186 const ssh_config_arg = try std.fmt.allocPrint(allocator, "core.sshCommand=ssh -p {} -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o IdentityFile={s}", .{ port + 1, priv_key_path }); 1187 defer allocator.free(ssh_config_arg); 1188 1189 { 1190 var process = try std.process.spawn(io, .{ 1191 .argv = if (is_ssh) 1192 &.{ "git", "-c", ssh_config_arg, "clone", "--depth", "1", remote_url, "client" } 1193 else 1194 &.{ "git", "clone", "--depth", "1", remote_url, "client" }, 1195 .cwd = .{ .path = temp_path }, 1196 .stdin = .ignore, 1197 .stdout = .ignore, 1198 .stderr = .ignore, 1199 }); 1200 const term = try process.wait(io); 1201 if (term != .exited or term.exited != 0) { 1202 return error.GitCommandFailed; 1203 } 1204 } 1205 1206 // make sure shallow clone was successful 1207 { 1208 const hello_txt = try temp_dir.openFile(io, "client/hello.txt", .{}); 1209 hello_txt.close(io); 1210 } 1211 1212 // make a third commit on the server 1213 { 1214 const extra_txt = try server_repo.core.work_dir.createFile(io, "extra.txt", .{ .truncate = true }); 1215 defer extra_txt.close(io); 1216 try extra_txt.writeStreamingAll(io, "extra content"); 1217 try server_repo.add(io, allocator, &.{"extra.txt"}); 1218 _ = try server_repo.commit(io, allocator, .{ .message = "add extra file" }); 1219 } 1220 1221 // pull --unshallow to deepen the clone and get the new commit 1222 { 1223 var process = try std.process.spawn(io, .{ 1224 .argv = if (is_ssh) 1225 &.{ "git", "-c", ssh_config_arg, "pull", "--unshallow" } 1226 else 1227 &.{ "git", "pull", "--unshallow" }, 1228 .cwd = .{ .path = client_path }, 1229 .stdin = .ignore, 1230 .stdout = .ignore, 1231 .stderr = .ignore, 1232 }); 1233 const term = try process.wait(io); 1234 if (term != .exited or term.exited != 0) { 1235 return error.GitCommandFailed; 1236 } 1237 } 1238 1239 // make sure unshallow pull was successful 1240 { 1241 const extra_txt = try temp_dir.openFile(io, "client/extra.txt", .{}); 1242 extra_txt.close(io); 1243 } 1244 1245 // delete client and clone again with --shallow-since 1246 try cwd.deleteTree(io, temp_dir_name ++ "/client"); 1247 1248 { 1249 var process = try std.process.spawn(io, .{ 1250 .argv = if (is_ssh) 1251 &.{ "git", "-c", ssh_config_arg, "clone", "--shallow-since=2000-01-01", remote_url, "client" } 1252 else 1253 &.{ "git", "clone", "--shallow-since=2000-01-01", remote_url, "client" }, 1254 .cwd = .{ .path = temp_path }, 1255 .stdin = .ignore, 1256 .stdout = .ignore, 1257 .stderr = .ignore, 1258 }); 1259 const term = try process.wait(io); 1260 if (term != .exited or term.exited != 0) { 1261 return error.GitCommandFailed; 1262 } 1263 } 1264 1265 // make sure shallow clone was successful 1266 { 1267 const hello_txt = try temp_dir.openFile(io, "client/hello.txt", .{}); 1268 hello_txt.close(io); 1269 } 1270 1271 // delete client and clone again with --shallow-exclude 1272 try cwd.deleteTree(io, temp_dir_name ++ "/client"); 1273 1274 { 1275 var process = try std.process.spawn(io, .{ 1276 .argv = if (is_ssh) 1277 &.{ "git", "-c", ssh_config_arg, "clone", "--shallow-exclude=v1", remote_url, "client" } 1278 else 1279 &.{ "git", "clone", "--shallow-exclude=v1", remote_url, "client" }, 1280 .cwd = .{ .path = temp_path }, 1281 .stdin = .ignore, 1282 .stdout = .ignore, 1283 .stderr = .ignore, 1284 }); 1285 const term = try process.wait(io); 1286 if (term != .exited or term.exited != 0) { 1287 return error.GitCommandFailed; 1288 } 1289 } 1290 1291 // make sure shallow clone was successful 1292 { 1293 const hello_txt = try temp_dir.openFile(io, "client/hello.txt", .{}); 1294 hello_txt.close(io); 1295 } 1296 1297 // delete client and clone again with --filter=blob:none 1298 try cwd.deleteTree(io, temp_dir_name ++ "/client"); 1299 1300 { 1301 var process = try std.process.spawn(io, .{ 1302 .argv = if (is_ssh) 1303 &.{ "git", "-c", ssh_config_arg, "clone", "--filter=blob:none", remote_url, "client" } 1304 else 1305 &.{ "git", "clone", "--filter=blob:none", remote_url, "client" }, 1306 .cwd = .{ .path = temp_path }, 1307 .stdin = .ignore, 1308 .stdout = .ignore, 1309 .stderr = .ignore, 1310 }); 1311 const term = try process.wait(io); 1312 if (term != .exited or term.exited != 0) { 1313 return error.GitCommandFailed; 1314 } 1315 } 1316 1317 // make sure partial clone was successful 1318 { 1319 const hello_txt = try temp_dir.openFile(io, "client/hello.txt", .{}); 1320 hello_txt.close(io); 1321 } 1322 1323 // delete client and clone again with --filter=tree:0 1324 try cwd.deleteTree(io, temp_dir_name ++ "/client"); 1325 1326 { 1327 var process = try std.process.spawn(io, .{ 1328 .argv = if (is_ssh) 1329 &.{ "git", "-c", ssh_config_arg, "clone", "--filter=tree:0", remote_url, "client" } 1330 else 1331 &.{ "git", "clone", "--filter=tree:0", remote_url, "client" }, 1332 .cwd = .{ .path = temp_path }, 1333 .stdin = .ignore, 1334 .stdout = .ignore, 1335 .stderr = .ignore, 1336 }); 1337 const term = try process.wait(io); 1338 if (term != .exited or term.exited != 0) { 1339 return error.GitCommandFailed; 1340 } 1341 } 1342 1343 // make sure treeless clone was successful 1344 { 1345 const goodbye_txt = try temp_dir.openFile(io, "client/goodbye.txt", .{}); 1346 goodbye_txt.close(io); 1347 } 1348 } else { 1349 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, cwd_path, temp_dir_name, port); 1350 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1351 1352 // clone repo 1353 var client_repo = try rp.Repo(repo_kind, .{ .is_test = true }).clone( 1354 io, 1355 allocator, 1356 remote_url, 1357 temp_path, 1358 client_path, 1359 null, 1360 .{ .wire = .{ .ssh = .{ 1361 .command = ssh_cmd_maybe, 1362 } } }, 1363 ); 1364 defer client_repo.deinit(io, allocator); 1365 1366 // make sure HEAD points to the right default branch 1367 var current_branch_buffer = [_]u8{0} ** rf.MAX_REF_CONTENT_SIZE; 1368 const head = try client_repo.head(io, &current_branch_buffer); 1369 try std.testing.expectEqualStrings("main", head.ref.name); 1370 1371 // make sure clone was successful 1372 const hello_txt = try temp_dir.openFile(io, "client/hello.txt", .{}); 1373 defer hello_txt.close(io); 1374 } 1375 } 1376 1377 fn testFetchLarge( 1378 comptime repo_kind: rp.RepoKind, 1379 comptime transport_def: net.TransportDefinition, 1380 comptime shell_out_to_git: bool, 1381 comptime port: u16, 1382 io: std.Io, 1383 allocator: std.mem.Allocator, 1384 ) !void { 1385 const temp_dir_name = "temp-testnet-fetch-large"; 1386 1387 // create the temp dir 1388 const cwd = std.Io.Dir.cwd(); 1389 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 1390 if (temp_dir_or_err) |*temp_dir| { 1391 temp_dir.close(io); 1392 try cwd.deleteTree(io, temp_dir_name); 1393 } else |_| {} 1394 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 1395 defer cwd.deleteTree(io, temp_dir_name) catch {}; 1396 defer temp_dir.close(io); 1397 1398 // init server 1399 var server_process = try runServer(io, allocator, temp_dir_name, port); 1400 defer _ = server_process.kill(io); 1401 1402 const cwd_path = try std.process.currentPathAlloc(io, allocator); 1403 defer allocator.free(cwd_path); 1404 1405 // register the repo under admin and locate its on-disk directory 1406 const server_path = (try repoOnDiskPath(io, allocator, temp_dir_name, "testrepo", true)).?; 1407 defer allocator.free(server_path); 1408 1409 var server_repo = try rp.Repo(.xit, .{ .is_test = true }).init(io, allocator, .{ .path = server_path }); 1410 defer server_repo.deinit(io, allocator); 1411 1412 var server_dir = try cwd.openDir(io, server_path, .{}); 1413 defer server_dir.close(io); 1414 1415 // copy files from current repo into server dir 1416 for (&[_][]const u8{"src"}) |dir_name| { 1417 var src_repo_dir = try cwd.openDir(io, dir_name, .{ .iterate = true }); 1418 defer src_repo_dir.close(io); 1419 1420 var dest_repo_dir = try server_dir.createDirPathOpen(io, dir_name, .{}); 1421 defer dest_repo_dir.close(io); 1422 1423 try copyDir(io, src_repo_dir, dest_repo_dir); 1424 1425 try server_repo.add(io, allocator, &.{dir_name}); 1426 } 1427 1428 // make a commit 1429 const commit1 = try server_repo.commit(io, allocator, .{ .message = "let there be light" }); 1430 1431 // export server repo 1432 { 1433 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1434 defer export_file.close(io); 1435 } 1436 1437 if (shell_out_to_git) { 1438 try server_repo.addConfig(io, allocator, .{ .name = "uploadpack.allowrefinwant", .value = "true" }); 1439 } 1440 1441 const client_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "client" }); 1442 defer allocator.free(client_path); 1443 1444 var client_repo = try rp.Repo(repo_kind, .{ .is_test = true }).init(io, allocator, .{ .path = client_path }); 1445 defer client_repo.deinit(io, allocator); 1446 1447 // add remote 1448 { 1449 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 1450 defer allocator.free(remote_url); 1451 1452 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 1453 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 1454 } 1455 1456 const is_ssh = switch (transport_def) { 1457 .file => false, 1458 .wire => |wire_kind| .ssh == wire_kind, 1459 }; 1460 1461 if (shell_out_to_git) { 1462 const priv_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 1463 defer allocator.free(priv_key_path); 1464 const ssh_config_arg = try std.fmt.allocPrint(allocator, "core.sshCommand=ssh -p {} -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o IdentityFile={s}", .{ port + 1, priv_key_path }); 1465 defer allocator.free(ssh_config_arg); 1466 1467 { 1468 var process = try std.process.spawn(io, .{ 1469 .argv = if (is_ssh) 1470 &.{ "git", "-c", ssh_config_arg, "pull", "origin", "master" } 1471 else 1472 &.{ "git", "pull", "origin", "master" }, 1473 .cwd = .{ .path = client_path }, 1474 .stdin = .ignore, 1475 .stdout = .ignore, 1476 .stderr = .ignore, 1477 }); 1478 const term = try process.wait(io); 1479 if (term != .exited or term.exited != 0) { 1480 return error.GitCommandFailed; 1481 } 1482 } 1483 1484 // make sure pull was successful 1485 { 1486 const oid_master = (try client_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1487 try std.testing.expectEqualStrings(&commit1, &oid_master); 1488 } 1489 1490 // make another commit on the server 1491 const commit2 = blk: { 1492 const extra_txt = try server_repo.core.work_dir.createFile(io, "extra.txt", .{ .truncate = true }); 1493 defer extra_txt.close(io); 1494 try extra_txt.writeStreamingAll(io, "extra content"); 1495 try server_repo.add(io, allocator, &.{"extra.txt"}); 1496 break :blk try server_repo.commit(io, allocator, .{ .message = "add extra file" }); 1497 }; 1498 1499 // fetch with ref-in-want (git uses want-ref in protocol v2 when fetching named refs) 1500 { 1501 var process = try std.process.spawn(io, .{ 1502 .argv = if (is_ssh) 1503 &.{ "git", "-c", ssh_config_arg, "fetch", "origin", "master" } 1504 else 1505 &.{ "git", "fetch", "origin", "master" }, 1506 .cwd = .{ .path = client_path }, 1507 .stdin = .ignore, 1508 .stdout = .ignore, 1509 .stderr = .ignore, 1510 }); 1511 const term = try process.wait(io); 1512 if (term != .exited or term.exited != 0) { 1513 return error.GitCommandFailed; 1514 } 1515 } 1516 1517 // make sure fetch with want-ref was successful 1518 { 1519 const oid_remote_master = (try client_repo.readRef(io, .{ .kind = .{ .remote = "origin" }, .name = "master" })).?; 1520 try std.testing.expectEqualStrings(&commit2, &oid_remote_master); 1521 } 1522 } else { 1523 const refspecs = &.{ 1524 "+refs/heads/master:refs/heads/master", 1525 }; 1526 1527 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, cwd_path, temp_dir_name, port); 1528 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1529 1530 try client_repo.fetch( 1531 io, 1532 allocator, 1533 "origin", 1534 .{ .refspecs = refspecs, .wire = .{ .ssh = .{ 1535 .command = ssh_cmd_maybe, 1536 } } }, 1537 ); 1538 1539 // update the working dir 1540 try client_repo.restore(io, allocator, "."); 1541 } 1542 1543 // make sure fetch was successful 1544 { 1545 const oid_master = (try client_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1546 try std.testing.expectEqualStrings(&commit1, &oid_master); 1547 } 1548 } 1549 1550 fn testPushLarge( 1551 comptime repo_kind: rp.RepoKind, 1552 comptime transport_def: net.TransportDefinition, 1553 comptime shell_out_to_git: bool, 1554 comptime port: u16, 1555 io: std.Io, 1556 allocator: std.mem.Allocator, 1557 ) !void { 1558 const temp_dir_name = "temp-testnet-push-large"; 1559 1560 // create the temp dir 1561 const cwd = std.Io.Dir.cwd(); 1562 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 1563 if (temp_dir_or_err) |*temp_dir| { 1564 temp_dir.close(io); 1565 try cwd.deleteTree(io, temp_dir_name); 1566 } else |_| {} 1567 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 1568 defer cwd.deleteTree(io, temp_dir_name) catch {}; 1569 defer temp_dir.close(io); 1570 1571 // init server 1572 var server_process = try runServer(io, allocator, temp_dir_name, port); 1573 defer _ = server_process.kill(io); 1574 1575 const cwd_path = try std.process.currentPathAlloc(io, allocator); 1576 defer allocator.free(cwd_path); 1577 1578 // register the repo under admin and locate its on-disk directory 1579 const server_path = (try repoOnDiskPath(io, allocator, temp_dir_name, "testrepo", true)).?; 1580 defer allocator.free(server_path); 1581 1582 var server_repo = try rp.Repo(.xit, .{ .is_test = true }).init(io, allocator, .{ .path = server_path }); 1583 defer server_repo.deinit(io, allocator); 1584 1585 // add config 1586 try server_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "false" }); 1587 try server_repo.addConfig(io, allocator, .{ .name = "receive.denycurrentbranch", .value = "updateinstead" }); 1588 try server_repo.addConfig(io, allocator, .{ .name = "http.receivepack", .value = "true" }); 1589 1590 // export server repo 1591 { 1592 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1593 defer export_file.close(io); 1594 } 1595 1596 const client_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "client" }); 1597 defer allocator.free(client_path); 1598 1599 var client_repo = try rp.Repo(repo_kind, .{ .is_test = true }).init(io, allocator, .{ .path = client_path }); 1600 defer client_repo.deinit(io, allocator); 1601 1602 var client_dir = try cwd.openDir(io, client_path, .{}); 1603 defer client_dir.close(io); 1604 1605 { 1606 const hello_txt = try client_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 1607 defer hello_txt.close(io); 1608 try hello_txt.writeStreamingAll(io, "hello, world!"); 1609 try client_repo.add(io, allocator, &.{"hello.txt"}); 1610 } 1611 1612 // copy files from current repo into client dir 1613 for (&[_][]const u8{"src"}) |dir_name| { 1614 var src_repo_dir = try cwd.openDir(io, dir_name, .{ .iterate = true }); 1615 defer src_repo_dir.close(io); 1616 1617 var dest_repo_dir = try client_dir.createDirPathOpen(io, dir_name, .{}); 1618 defer dest_repo_dir.close(io); 1619 1620 try copyDir(io, src_repo_dir, dest_repo_dir); 1621 1622 try client_repo.add(io, allocator, &.{dir_name}); 1623 } 1624 1625 _ = try client_repo.commit(io, allocator, .{ .message = "let there be light" }); 1626 1627 // change the files so git will send them as delta objects 1628 for (&[_][]const u8{"src"}) |dir_name| { 1629 var dest_repo_dir = try client_dir.createDirPathOpen(io, dir_name, .{ .open_options = .{ .iterate = true } }); 1630 defer dest_repo_dir.close(io); 1631 1632 { 1633 var iter = dest_repo_dir.iterate(); 1634 while (try iter.next(io)) |entry| { 1635 switch (entry.kind) { 1636 .file => { 1637 const file = try dest_repo_dir.openFile(io, entry.name, .{ .mode = .read_write }); 1638 defer file.close(io); 1639 var writer = file.writer(io, &.{}); 1640 try writer.interface.writeAll("EDIT"); 1641 }, 1642 else => {}, 1643 } 1644 } 1645 } 1646 1647 try client_repo.add(io, allocator, &.{dir_name}); 1648 } 1649 1650 const commit2 = try client_repo.commit(io, allocator, .{ .message = "more stuff" }); 1651 1652 // add remote 1653 { 1654 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 1655 defer allocator.free(remote_url); 1656 1657 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 1658 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 1659 } 1660 1661 const is_ssh = switch (transport_def) { 1662 .file => false, 1663 .wire => |wire_kind| .ssh == wire_kind, 1664 }; 1665 1666 if (shell_out_to_git) { 1667 const priv_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 1668 defer allocator.free(priv_key_path); 1669 const ssh_config_arg = try std.fmt.allocPrint(allocator, "core.sshCommand=ssh -p {} -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o IdentityFile={s}", .{ port + 1, priv_key_path }); 1670 defer allocator.free(ssh_config_arg); 1671 1672 // shell out to git so it will send delta objects 1673 var process = try std.process.spawn(io, .{ 1674 .argv = if (is_ssh) 1675 &.{ "git", "-c", ssh_config_arg, "push", "origin", "master" } 1676 else 1677 &.{ "git", "push", "origin", "master" }, 1678 .cwd = .{ .path = client_path }, 1679 .stdin = .ignore, 1680 .stdout = .ignore, 1681 .stderr = .ignore, 1682 }); 1683 const term = try process.wait(io); 1684 if (term != .exited or term.exited != 0) { 1685 return error.GitCommandFailed; 1686 } 1687 } else { 1688 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, cwd_path, temp_dir_name, port); 1689 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1690 1691 try client_repo.push( 1692 io, 1693 allocator, 1694 "origin", 1695 "master", 1696 false, 1697 .{ .wire = .{ .ssh = .{ 1698 .command = ssh_cmd_maybe, 1699 } } }, 1700 ); 1701 } 1702 1703 // make sure push was successful 1704 { 1705 const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1706 try std.testing.expectEqualStrings(&commit2, &oid_master); 1707 1708 const hello_txt = try server_repo.core.work_dir.openFile(io, "hello.txt", .{}); 1709 defer hello_txt.close(io); 1710 } 1711 } 1712 1713 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 1714 var iter = src_dir.iterate(); 1715 while (try iter.next(io)) |entry| { 1716 switch (entry.kind) { 1717 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 1718 .directory => { 1719 try dest_dir.createDirPath(io, entry.name); 1720 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1721 defer dest_entry_dir.close(io); 1722 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1723 defer src_entry_dir.close(io); 1724 try copyDir(io, src_entry_dir, dest_entry_dir); 1725 }, 1726 else => {}, 1727 } 1728 } 1729 } 1730 1731 // the dev SSH public key matching the private key written by runServer 1732 const admin_ssh_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKeIs8mJqigBZ5y84J4COgnAJJ5bHPKy+lM2SliMXbYm radar@roark"; 1733 1734 // create the admin event repo with a user holding the dev SSH public key 1735 fn setupAdmin(io: std.Io, allocator: std.mem.Allocator, data_dir_name: []const u8) !void { 1736 const cwd_path = try std.process.currentPathAlloc(io, allocator); 1737 defer allocator.free(cwd_path); 1738 1739 const admin_repo_path = try std.fs.path.join(allocator, &.{ cwd_path, data_dir_name, "admin" }); 1740 defer allocator.free(admin_repo_path); 1741 1742 var repo = try rp.Repo(.xit, evt.admin_repo_opts).init(io, allocator, .{ .path = admin_repo_path }); 1743 defer repo.deinit(io, allocator); 1744 1745 try repo.addConfig(io, allocator, .{ .name = "user.name", .value = "haxy" }); 1746 try repo.addConfig(io, allocator, .{ .name = "user.email", .value = "admin@haxy" }); 1747 1748 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 1749 const user_id = evt.EventWithId.randomId(prng.random()); 1750 1751 var password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 1752 const password_hash = try evt.User.hashPassword("password", &password_hash_buf, io); 1753 1754 try evt.consume(.admin, .xit, evt.admin_repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{.{ 1755 .id = std.fmt.bytesToHex(user_id, .lower), 1756 .author = .{ .name = "admin", .email = "admin@example.test" }, 1757 .event = .{ .user = .{ 1758 .name = "admin", 1759 .email = "admin@example.test", 1760 .password_hash = password_hash, 1761 .ssh_keys = admin_ssh_key, 1762 } }, 1763 }}); 1764 } 1765 1766 // resolve admin/<repo_name> to its on-disk directory under <data_dir>/repos via 1767 // the event store 1768 fn repoOnDiskPath( 1769 io: std.Io, 1770 allocator: std.mem.Allocator, 1771 data_dir_name: []const u8, 1772 repo_name: []const u8, 1773 create: bool, 1774 ) !?[]u8 { 1775 const cwd_path = try std.process.currentPathAlloc(io, allocator); 1776 defer allocator.free(cwd_path); 1777 1778 const admin_repo_path = try std.fs.path.join(allocator, &.{ cwd_path, data_dir_name, "admin" }); 1779 defer allocator.free(admin_repo_path); 1780 1781 const event_id_hex = (try evt.resolveOrCreateRepo( 1782 io, 1783 allocator, 1784 admin_repo_path, 1785 "admin", 1786 repo_name, 1787 if (create) .{ .read_access = .public } else null, 1788 )) orelse return null; 1789 return try std.fs.path.join(allocator, &.{ cwd_path, data_dir_name, "repos", &event_id_hex }); 1790 } 1791 1792 // build the remote URL addressing admin/<repo_name> over the given transport 1793 fn remoteUrl( 1794 comptime transport_def: net.TransportDefinition, 1795 allocator: std.mem.Allocator, 1796 repo_name: []const u8, 1797 port: u16, 1798 ) ![]u8 { 1799 return switch (transport_def) { 1800 .file => unreachable, 1801 .wire => |wire_kind| switch (wire_kind) { 1802 .http => try std.fmt.allocPrint(allocator, "http://localhost:{}/admin/{s}", .{ port, repo_name }), 1803 .raw => try std.fmt.allocPrint(allocator, "git://localhost:{}/admin/{s}", .{ port, repo_name }), 1804 .ssh => try std.fmt.allocPrint(allocator, "git@localhost:admin/{s}", .{repo_name}), 1805 }, 1806 }; 1807 } 1808 1809 // the ssh command the haxy git client should invoke, or null when not using 1810 // ssh. haxy generates a fresh host key on every run, so accept the unknown host 1811 // and skip writing to known_hosts. 1812 fn sshCommand( 1813 is_ssh: bool, 1814 allocator: std.mem.Allocator, 1815 cwd_path: []const u8, 1816 temp_dir_name: []const u8, 1817 port: u16, 1818 ) !?[]u8 { 1819 if (!is_ssh) return null; 1820 1821 const priv_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 1822 defer allocator.free(priv_key_path); 1823 1824 return try std.fmt.allocPrint(allocator, "ssh -p {} -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o IdentitiesOnly=yes -o IdentityFile=\"{s}\"", .{ port + 1, priv_key_path }); 1825 } 1826