╭ test.zig ─────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const builtin = @import("builtin"); 3 const xit = @import("xit"); 4 const rp = xit.repo; 5 const rf = xit.ref; 6 const work = xit.workdir; 7 const hash = xit.hash; 8 const net = xit.net; 9 10 test "fetch small" { 11 const io = std.testing.io; 12 const allocator = std.testing.allocator; 13 try testFetch(.xit, .xit, .{ .wire = .http }, 3004, io, allocator); 14 if (.windows != builtin.os.tag) { 15 try testFetch(.xit, .xit, .{ .wire = .ssh }, 3006, io, allocator); 16 } 17 } 18 19 test "push small" { 20 const io = std.testing.io; 21 const allocator = std.testing.allocator; 22 try testPush(.xit, .xit, .{ .wire = .http }, 3010, io, allocator); 23 if (.windows != builtin.os.tag) { 24 try testPush(.xit, .xit, .{ .wire = .ssh }, 3012, io, allocator); 25 } 26 } 27 28 test "clone small" { 29 const io = std.testing.io; 30 const allocator = std.testing.allocator; 31 try testClone(.xit, .xit, .{ .wire = .http }, 3016, false, io, allocator); 32 if (.windows != builtin.os.tag) { 33 try testClone(.xit, .xit, .{ .wire = .ssh }, 3018, false, io, allocator); 34 } 35 } 36 37 test "clone small subprocess" { 38 const io = std.testing.io; 39 const allocator = std.testing.allocator; 40 try testClone(.git, .xit, .{ .wire = .http }, 3031, true, io, allocator); 41 if (.windows != builtin.os.tag) { 42 try testClone(.git, .xit, .{ .wire = .ssh }, 3033, true, io, allocator); 43 } 44 } 45 46 test "fetch large" { 47 const io = std.testing.io; 48 const allocator = std.testing.allocator; 49 try testFetchLarge(.git, .git, .{ .wire = .http }, 3019, false, io, allocator); 50 if (.windows != builtin.os.tag) { 51 try testFetchLarge(.git, .git, .{ .wire = .ssh }, 3021, false, io, allocator); 52 } 53 } 54 55 test "fetch large subprocess" { 56 const io = std.testing.io; 57 const allocator = std.testing.allocator; 58 try testFetchLarge(.git, .xit, .{ .wire = .http }, 3022, true, io, allocator); 59 if (.windows != builtin.os.tag) { 60 try testFetchLarge(.git, .xit, .{ .wire = .ssh }, 3024, true, io, allocator); 61 } 62 } 63 64 test "push large" { 65 const io = std.testing.io; 66 const allocator = std.testing.allocator; 67 try testPushLarge(.git, .git, .{ .wire = .http }, 3025, false, io, allocator); 68 if (.windows != builtin.os.tag) { 69 try testPushLarge(.git, .git, .{ .wire = .ssh }, 3027, false, io, allocator); 70 } 71 } 72 73 test "git push large subprocess" { 74 const io = std.testing.io; 75 const allocator = std.testing.allocator; 76 try testPushLarge(.git, .xit, .{ .wire = .http }, 3028, true, io, allocator); 77 if (.windows != builtin.os.tag) { 78 try testPushLarge(.git, .xit, .{ .wire = .ssh }, 3030, true, io, allocator); 79 } 80 } 81 82 fn Server( 83 comptime server_repo_kind: rp.RepoKind, 84 comptime transport_def: net.TransportDefinition, 85 comptime temp_dir_name: []const u8, 86 comptime port: u16, 87 ) type { 88 return struct { 89 core: Core, 90 91 const Core = switch (transport_def) { 92 .file => void, 93 .wire => |wire_kind| switch (wire_kind) { 94 .http => struct { 95 io: std.Io, 96 allocator: std.mem.Allocator, 97 temp_dir_name: []const u8, 98 stop_server_endpoint: []const u8, 99 net_server: std.Io.net.Server, 100 server_thread: std.Thread, 101 }, 102 .raw => unreachable, 103 .ssh => struct { 104 io: std.Io, 105 process: ?std.process.Child, 106 }, 107 }, 108 }; 109 110 fn init( 111 io: std.Io, 112 allocator: std.mem.Allocator, 113 ) !Server(server_repo_kind, transport_def, temp_dir_name, port) { 114 switch (transport_def) { 115 .file => return .{ .core = {} }, 116 .wire => |wire_kind| switch (wire_kind) { 117 .http => { 118 const address = try std.Io.net.IpAddress.parseIp4("127.0.0.1", port); 119 const net_server = try address.listen(io, .{ .reuse_address = true }); 120 errdefer net_server.deinit(); 121 return .{ 122 .core = .{ 123 .io = io, 124 .allocator = allocator, 125 .temp_dir_name = temp_dir_name, 126 .stop_server_endpoint = std.fmt.comptimePrint("http://127.0.0.1:{}/stop-server", .{port}), 127 .net_server = net_server, 128 .server_thread = undefined, 129 }, 130 }; 131 }, 132 .raw => unreachable, 133 .ssh => { 134 // create priv host key 135 const host_key_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/host_key", .{}); 136 defer host_key_file.close(io); 137 try host_key_file.writeStreamingAll(io, 138 \\-----BEGIN OPENSSH PRIVATE KEY----- 139 \\b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS 140 \\1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQS1ppUfk8n7yvVKEgz3tXjt4q76VGuj 141 \\LcQlRwmogzovV40LLcX0aTObZlQaLWfzJMNpCa/ztMpQlr86nsarE4lEAAAAqLe43zK3uN 142 \\8yAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLWmlR+TyfvK9UoS 143 \\DPe1eO3irvpUa6MtxCVHCaiDOi9XjQstxfRpM5tmVBotZ/Mkw2kJr/O0ylCWvzqexqsTiU 144 \\QAAAAgQ+LCk30ZNJxb2Da5JL+QOFWCMf7bgXCWcEzhEGGvFWYAAAALcmFkYXJAcm9hcmsB 145 \\AgMEBQ== 146 \\-----END OPENSSH PRIVATE KEY----- 147 \\ 148 ); 149 if (.windows != builtin.os.tag) { 150 try host_key_file.setPermissions(io, @enumFromInt(0o600)); 151 } 152 153 // create priv client key 154 const priv_key_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/key", .{}); 155 defer priv_key_file.close(io); 156 try priv_key_file.writeStreamingAll(io, 157 \\-----BEGIN OPENSSH PRIVATE KEY----- 158 \\b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 159 \\QyNTUxOQAAACCniLPJiaooAWecvOCeAjoJwCSeWxzysvpTNkpYjF22JgAAAJA+7hikPu4Y 160 \\pAAAAAtzc2gtZWQyNTUxOQAAACCniLPJiaooAWecvOCeAjoJwCSeWxzysvpTNkpYjF22Jg 161 \\AAAEDVlopOMnKt/7by/IA8VZvQXUS/O6VLkixOqnnahUdPCKeIs8mJqigBZ5y84J4COgnA 162 \\JJ5bHPKy+lM2SliMXbYmAAAAC3JhZGFyQHJvYXJrAQI= 163 \\-----END OPENSSH PRIVATE KEY----- 164 \\ 165 ); 166 if (.windows != builtin.os.tag) { 167 try priv_key_file.setPermissions(io, @enumFromInt(0o600)); 168 } 169 170 // create pub key 171 const pub_key_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/key.pub", .{}); 172 defer pub_key_file.close(io); 173 try pub_key_file.writeStreamingAll(io, 174 \\ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKeIs8mJqigBZ5y84J4COgnAJJ5bHPKy+lM2SliMXbYm radar@roark 175 \\ 176 ); 177 if (.windows != builtin.os.tag) { 178 try pub_key_file.setPermissions(io, @enumFromInt(0o600)); 179 } 180 181 // create authorized_keys file 182 const auth_keys_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/authorized_keys", .{}); 183 defer auth_keys_file.close(io); 184 try auth_keys_file.writeStreamingAll(io, 185 \\ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKeIs8mJqigBZ5y84J4COgnAJJ5bHPKy+lM2SliMXbYm radar@roark 186 \\ 187 ); 188 if (.windows != builtin.os.tag) { 189 try auth_keys_file.setPermissions(io, @enumFromInt(0o600)); 190 } 191 192 // create known_hosts file 193 const known_hosts_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/known_hosts", .{}); 194 defer known_hosts_file.close(io); 195 const port_str = std.fmt.comptimePrint("{}", .{port}); 196 try known_hosts_file.writeStreamingAll(io, "[localhost]:" ++ port_str ++ " ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLWmlR+TyfvK9UoSDPe1eO3irvpUa6MtxCVHCaiDOi9XjQstxfRpM5tmVBotZ/Mkw2kJr/O0ylCWvzqexqsTiUQ="); 197 if (.windows != builtin.os.tag) { 198 try known_hosts_file.setPermissions(io, @enumFromInt(0o600)); 199 } 200 201 // create sshd_config file 202 const sshd_config_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/sshd_config", .{}); 203 defer sshd_config_file.close(io); 204 try sshd_config_file.writeStreamingAll(io, 205 \\AuthenticationMethods publickey 206 \\PubkeyAuthentication yes 207 \\PasswordAuthentication no 208 \\StrictModes no 209 \\ 210 ); 211 if (.windows != builtin.os.tag) { 212 try sshd_config_file.setPermissions(io, @enumFromInt(0o600)); 213 } 214 215 // create sshd.sh contents 216 const cwd_path = try std.process.currentPathAlloc(io, allocator); 217 defer allocator.free(cwd_path); 218 const host_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "host_key" }); 219 defer allocator.free(host_key_path); 220 const auth_keys_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "authorized_keys" }); 221 defer allocator.free(auth_keys_path); 222 const sshd_contents = try std.fmt.allocPrint( 223 allocator, 224 "#!/bin/sh\nexec $(which sshd) -p {} -f sshd_config -h \"{s}\" -D -e -o AuthorizedKeysFile=\"{s}\"", 225 .{ port, host_key_path, auth_keys_path }, 226 ); 227 defer allocator.free(sshd_contents); 228 229 // if path has a space char, it fucks up sshd 230 try std.testing.expect(null == std.mem.indexOfScalar(u8, auth_keys_path, ' ')); 231 232 // create sshd.sh 233 { 234 const sshd_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/sshd.sh", .{}); 235 defer sshd_file.close(io); 236 try sshd_file.writeStreamingAll(io, sshd_contents); 237 if (.windows != builtin.os.tag) { 238 try sshd_file.setPermissions(io, .executable_file); 239 } 240 } 241 242 return .{ 243 .core = .{ .io = io, .process = null }, 244 }; 245 }, 246 }, 247 } 248 } 249 250 fn start(self: *Server(server_repo_kind, transport_def, temp_dir_name, port)) !void { 251 switch (transport_def) { 252 .file => {}, 253 .wire => |wire_kind| switch (wire_kind) { 254 .http => { 255 const ServerHandler = struct { 256 fn run(core: *Core) !void { 257 var send_buffer = [_]u8{0} ** 1024; 258 var recv_buffer = [_]u8{0} ** 1024; 259 260 accept: while (true) { 261 const stream = try core.net_server.accept(core.io); 262 defer stream.close(core.io); 263 264 var conn_br = stream.reader(core.io, &recv_buffer); 265 var conn_bw = stream.writer(core.io, &send_buffer); 266 var http_server = std.http.Server.init(&conn_br.interface, &conn_bw.interface); 267 268 while (http_server.reader.state == .ready) { 269 var request = http_server.receiveHead() catch |err| switch (err) { 270 error.HttpConnectionClosing => continue :accept, 271 else => |e| return e, 272 }; 273 if (std.mem.eql(u8, request.head.target, "/stop-server")) { 274 break :accept; 275 } 276 277 const uri = try std.Uri.parseAfterScheme("", request.head.target); 278 if (uri.path.percent_encoded[0] != '/') return error.PathMustStartWithSlash; 279 const path = if (std.mem.indexOfScalar(u8, uri.path.percent_encoded[1..], '/')) |idx| 280 uri.path.percent_encoded[idx + 1 ..] 281 else 282 return error.SlashNotFound; 283 284 const cwd_path = try std.process.currentPathAlloc(core.io, core.allocator); 285 defer core.allocator.free(cwd_path); 286 const temp_dir_path = try std.fs.path.join(core.allocator, &.{ cwd_path, core.temp_dir_name }); 287 defer core.allocator.free(temp_dir_path); 288 const path_translated = try std.fmt.allocPrint(core.allocator, "{s}{s}", .{ 289 temp_dir_path, 290 uri.path.percent_encoded, 291 }); 292 defer core.allocator.free(path_translated); 293 294 // init env map 295 var env_map = std.process.Environ.Map.init(core.allocator); 296 defer env_map.deinit(); 297 try env_map.put("GATEWAY_INTERFACE", "CGI/1.1"); 298 try env_map.put("REQUEST_METHOD", @tagName(request.head.method)); 299 try env_map.put("PATH_INFO", path); 300 try env_map.put("PATH_TRANSLATED", path_translated); 301 if (uri.query) |query| { 302 try env_map.put("QUERY_STRING", query.percent_encoded); 303 } 304 305 var accept: std.ArrayList([]const u8) = .empty; 306 defer accept.deinit(core.allocator); 307 308 var keep_alive = true; // HTTP 1.1 defaults to keep-alive 309 310 // iterate over headers to fill env map 311 var req_header_it = request.iterateHeaders(); 312 while (req_header_it.next()) |header| { 313 const header_name = header.name; 314 const header_value = header.value; 315 316 if (std.ascii.eqlIgnoreCase(header_name, "content-type")) { 317 try env_map.put("CONTENT_TYPE", header_value); 318 } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { 319 try env_map.put("CONTENT_LENGTH", header_value); 320 } else if (std.ascii.eqlIgnoreCase(header_name, "referer")) { 321 try env_map.put("HTTP_REFERER", header_value); 322 } else if (std.ascii.eqlIgnoreCase(header_name, "accept")) { 323 try accept.append(core.allocator, header_value); 324 } else if (std.ascii.eqlIgnoreCase(header_name, "user-agent")) { 325 try env_map.put("HTTP_USER_AGENT", header_value); 326 } else if (std.ascii.eqlIgnoreCase(header_name, "connection")) { 327 if (std.ascii.eqlIgnoreCase(header_value, "close")) { 328 keep_alive = false; 329 } 330 } else if (std.ascii.eqlIgnoreCase(header_name, "git-protocol")) { 331 try env_map.put("GIT_PROTOCOL", header_value); 332 } 333 } 334 335 const accept_str = try std.mem.join(core.allocator, ",", accept.items); 336 defer core.allocator.free(accept_str); 337 if (accept_str.len > 0) { 338 try env_map.put("HTTP_ACCEPT", accept_str); 339 } 340 341 var process = try std.process.spawn(core.io, .{ 342 .argv = switch (server_repo_kind) { 343 .git => &.{ "git", "http-backend" }, 344 .xit => &.{ "../zig-out/bin/haxy", "http-backend" }, 345 }, 346 .cwd = switch (server_repo_kind) { 347 .git => .inherit, 348 .xit => .{ .path = temp_dir_name }, 349 }, 350 .environ_map = &env_map, 351 .stdin = .pipe, 352 .stdout = .pipe, 353 .stderr = .pipe, 354 }); 355 defer process.kill(core.io); 356 357 if (request.head.method == .POST) { 358 const reader = try request.readerExpectContinue(&.{}); 359 const request_body = try reader.allocRemaining(core.allocator, .unlimited); 360 defer core.allocator.free(request_body); 361 try process.stdin.?.writeStreamingAll(core.io, request_body); 362 } 363 process.stdin.?.close(core.io); 364 process.stdin = null; 365 366 var multi_reader_buffer: std.Io.File.MultiReader.Buffer(2) = undefined; 367 var multi_reader: std.Io.File.MultiReader = undefined; 368 multi_reader.init(core.allocator, core.io, multi_reader_buffer.toStreams(), &.{ process.stdout.?, process.stderr.? }); 369 defer multi_reader.deinit(); 370 371 while (multi_reader.fill(64, .none)) |_| {} else |err| switch (err) { 372 error.EndOfStream => {}, 373 else => |e| return e, 374 } 375 376 try multi_reader.checkAnyError(); 377 378 _ = try process.wait(core.io); 379 380 const stdout_slice = try multi_reader.toOwnedSlice(0); 381 defer core.allocator.free(stdout_slice); 382 const stderr_slice = try multi_reader.toOwnedSlice(1); 383 defer core.allocator.free(stderr_slice); 384 385 // transition the http state machine so it can 386 // read the next request on this connection 387 if (http_server.reader.state == .received_head) { 388 http_server.reader.state = .ready; 389 } 390 391 if (stderr_slice.len > 0) { 392 std.debug.print("Error from git-http-backend:\n{s}\n", .{stderr_slice}); 393 try http_server.out.writeAll("HTTP/1.1 500 Internal Server Error\r\n\r\n"); 394 } else { 395 try http_server.out.writeAll("HTTP/1.1 200 OK\r\n"); 396 const double_newline = "\r\n\r\n"; 397 const double_newline_idx = std.mem.indexOf(u8, stdout_slice, double_newline) orelse unreachable; 398 try http_server.out.writeAll(stdout_slice[0..double_newline_idx]); 399 try http_server.out.print("\r\nContent-Length: {}", .{stdout_slice.len - (double_newline_idx + double_newline.len)}); 400 try http_server.out.writeAll(stdout_slice[double_newline_idx..]); 401 } 402 try http_server.out.flush(); 403 404 if (!keep_alive) { 405 continue :accept; 406 } 407 } 408 } 409 } 410 }; 411 self.core.server_thread = try std.Thread.spawn(.{}, ServerHandler.run, .{&self.core}); 412 }, 413 .raw => unreachable, 414 .ssh => { 415 std.debug.assert(self.core.process == null); 416 self.core.process = try std.process.spawn(self.core.io, .{ 417 .argv = &.{"./sshd.sh"}, 418 .cwd = .{ .path = temp_dir_name }, 419 .stdin = .pipe, 420 .stdout = .ignore, 421 .stderr = .ignore, 422 }); 423 }, 424 }, 425 } 426 427 // wait for server to be ready by polling the port 428 if (transport_def == .wire) { 429 const address = try std.Io.net.IpAddress.parseIp4("127.0.0.1", port); 430 for (0..50) |_| { 431 const stream = address.connect(self.core.io, .{ .mode = .stream }) catch { 432 try std.Io.sleep(self.core.io, .fromMilliseconds(100), .real); 433 continue; 434 }; 435 stream.close(self.core.io); 436 break; 437 } 438 } 439 } 440 441 fn stop(self: *Server(server_repo_kind, transport_def, temp_dir_name, port)) void { 442 switch (transport_def) { 443 .file => {}, 444 .wire => |wire_kind| switch (wire_kind) { 445 .http => { 446 var client = std.http.Client{ .io = self.core.io, .allocator = self.core.allocator }; 447 defer client.deinit(); 448 _ = client.fetch(.{ .location = .{ .url = self.core.stop_server_endpoint } }) catch return; 449 self.core.server_thread.join(); 450 }, 451 .raw => unreachable, 452 .ssh => { 453 _ = self.core.process.?.kill(self.core.io); 454 self.core.process = null; 455 }, 456 }, 457 } 458 } 459 }; 460 } 461 462 fn testFetch( 463 comptime repo_kind: rp.RepoKind, 464 comptime server_repo_kind: rp.RepoKind, 465 comptime transport_def: net.TransportDefinition, 466 comptime port: u16, 467 io: std.Io, 468 allocator: std.mem.Allocator, 469 ) !void { 470 const temp_dir_name = "temp-testnet-fetch"; 471 472 // create the temp dir 473 const cwd = std.Io.Dir.cwd(); 474 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 475 if (temp_dir_or_err) |*temp_dir| { 476 temp_dir.close(io); 477 try cwd.deleteTree(io, temp_dir_name); 478 } else |_| {} 479 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 480 defer cwd.deleteTree(io, temp_dir_name) catch {}; 481 defer temp_dir.close(io); 482 483 // init server 484 var server = try Server(server_repo_kind, transport_def, temp_dir_name, port).init(io, allocator); 485 try server.start(); 486 defer server.stop(); 487 488 const cwd_path = try std.process.currentPathAlloc(io, allocator); 489 defer allocator.free(cwd_path); 490 491 const server_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server" }); 492 defer allocator.free(server_path); 493 494 var server_repo = try rp.Repo(server_repo_kind, .{ .is_test = true }).init(io, allocator, .{ .path = server_path }); 495 defer server_repo.deinit(io, allocator); 496 497 // make a commit 498 const commit1 = blk: { 499 const hello_txt = try server_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 500 defer hello_txt.close(io); 501 try hello_txt.writeStreamingAll(io, "hello, world!"); 502 try server_repo.add(io, allocator, &.{"hello.txt"}); 503 break :blk try server_repo.commit(io, allocator, .{ .message = "let there be light" }); 504 }; 505 506 // export server repo 507 { 508 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 509 defer export_file.close(io); 510 511 try server_repo.addConfig(io, allocator, .{ .name = "uploadpack.allowAnySHA1InWant", .value = "true" }); 512 } 513 514 // add a tag 515 _ = try server_repo.addTag(io, allocator, .{ .name = "1.0.0", .message = "hi" }); 516 517 const client_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "client" }); 518 defer allocator.free(client_path); 519 520 var client_repo = try rp.Repo(repo_kind, .{ .is_test = true }).init(io, allocator, .{ .path = client_path }); 521 defer client_repo.deinit(io, allocator); 522 523 // add remote 524 { 525 if (.windows == builtin.os.tag) { 526 std.mem.replaceScalar(u8, server_path, '\\', '/'); 527 } 528 const separator = if (server_path[0] == '/') "" else "/"; 529 530 const remote_url = switch (transport_def) { 531 //.file => try std.fmt.allocPrint(allocator, "file://{s}{s}", .{ separator, server_path }), 532 .file => try std.fmt.allocPrint(allocator, "../server", .{}), // relative file paths work too 533 .wire => |wire_kind| switch (wire_kind) { 534 .http => try std.fmt.allocPrint(allocator, "http://localhost:{}/server", .{port}), 535 .raw => try std.fmt.allocPrint(allocator, "git://localhost:{}/server", .{port}), 536 .ssh => try std.fmt.allocPrint(allocator, "ssh://localhost:{}{s}{s}", .{ port, separator, server_path }), 537 }, 538 }; 539 defer allocator.free(remote_url); 540 541 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 542 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 543 } 544 545 // create refspec with oid as a test 546 const oid_refspec = try std.fmt.allocPrint(allocator, "+{s}:refs/heads/foo", .{&commit1}); 547 defer allocator.free(oid_refspec); 548 549 const refspecs = &.{ 550 "+refs/heads/master:refs/heads/master", 551 oid_refspec, 552 }; 553 554 const is_ssh = switch (transport_def) { 555 .file => false, 556 .wire => |wire_kind| .ssh == wire_kind, 557 }; 558 const ssh_cmd_maybe: ?[]const u8 = if (is_ssh) blk: { 559 const known_hosts_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "known_hosts" }); 560 defer allocator.free(known_hosts_path); 561 562 const priv_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 563 defer allocator.free(priv_key_path); 564 565 break :blk try std.fmt.allocPrint(allocator, "ssh -o UserKnownHostsFile=\"{s}\" -o IdentityFile=\"{s}\"", .{ known_hosts_path, priv_key_path }); 566 } else null; 567 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 568 569 const upload_pack_command = try switch (server_repo_kind) { 570 .xit => std.fmt.allocPrint(allocator, "{s}/zig-out/bin/haxy upload-pack", .{cwd_path}), 571 .git => allocator.dupe(u8, "git-upload-pack"), 572 }; 573 defer allocator.free(upload_pack_command); 574 575 try client_repo.fetch( 576 io, 577 allocator, 578 "origin", 579 .{ .refspecs = refspecs, .wire = .{ .ssh = .{ 580 .command = ssh_cmd_maybe, 581 .upload_pack_command = upload_pack_command, 582 } } }, 583 ); 584 585 // update the working dir 586 try client_repo.restore(io, allocator, "."); 587 588 // make sure fetch was successful 589 { 590 const hello_txt = try temp_dir.openFile(io, "client/hello.txt", .{}); 591 defer hello_txt.close(io); 592 593 try std.testing.expect(null != try client_repo.readRef(io, .{ .kind = .tag, .name = "1.0.0" })); 594 try std.testing.expect(null != try client_repo.readRef(io, .{ .kind = .head, .name = "foo" })); 595 596 const oid_master = (try client_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 597 try std.testing.expectEqualStrings(&commit1, &oid_master); 598 } 599 600 // make another commit 601 const commit2 = blk: { 602 const goodbye_txt = try server_repo.core.work_dir.createFile(io, "goodbye.txt", .{ .truncate = true }); 603 defer goodbye_txt.close(io); 604 try goodbye_txt.writeStreamingAll(io, "goodbye, world!"); 605 try server_repo.add(io, allocator, &.{"goodbye.txt"}); 606 break :blk try server_repo.commit(io, allocator, .{ .message = "goodbye" }); 607 }; 608 609 try client_repo.fetch( 610 io, 611 allocator, 612 "origin", 613 .{ .refspecs = refspecs, .wire = .{ .ssh = .{ 614 .command = ssh_cmd_maybe, 615 .upload_pack_command = upload_pack_command, 616 } } }, 617 ); 618 619 // update the working dir 620 try client_repo.restore(io, allocator, "."); 621 622 // make sure fetch was successful 623 { 624 const goodbye_txt = try temp_dir.openFile(io, "client/goodbye.txt", .{}); 625 defer goodbye_txt.close(io); 626 627 const oid_master = (try client_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 628 try std.testing.expectEqualStrings(&commit2, &oid_master); 629 } 630 } 631 632 fn testPush( 633 comptime repo_kind: rp.RepoKind, 634 comptime server_repo_kind: rp.RepoKind, 635 comptime transport_def: net.TransportDefinition, 636 comptime port: u16, 637 io: std.Io, 638 allocator: std.mem.Allocator, 639 ) !void { 640 const temp_dir_name = "temp-testnet-push"; 641 642 // create the temp dir 643 const cwd = std.Io.Dir.cwd(); 644 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 645 if (temp_dir_or_err) |*temp_dir| { 646 temp_dir.close(io); 647 try cwd.deleteTree(io, temp_dir_name); 648 } else |_| {} 649 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 650 defer cwd.deleteTree(io, temp_dir_name) catch {}; 651 defer temp_dir.close(io); 652 653 // init server 654 var server = try Server(server_repo_kind, transport_def, temp_dir_name, port).init(io, allocator); 655 try server.start(); 656 defer server.stop(); 657 658 const cwd_path = try std.process.currentPathAlloc(io, allocator); 659 defer allocator.free(cwd_path); 660 661 const server_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server" }); 662 defer allocator.free(server_path); 663 664 var server_repo = try rp.Repo(server_repo_kind, .{ .is_test = true }).init(io, allocator, .{ .path = server_path }); 665 defer server_repo.deinit(io, allocator); 666 667 // add config 668 switch (transport_def) { 669 .file => try server_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "true" }), 670 .wire => { 671 try server_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "false" }); 672 try server_repo.addConfig(io, allocator, .{ .name = "receive.denycurrentbranch", .value = "updateinstead" }); 673 }, 674 } 675 try server_repo.addConfig(io, allocator, .{ .name = "http.receivepack", .value = "true" }); 676 677 // export server repo 678 { 679 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 680 defer export_file.close(io); 681 } 682 683 const client_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "client" }); 684 defer allocator.free(client_path); 685 686 var client_repo = try rp.Repo(repo_kind, .{ .is_test = true }).init(io, allocator, .{ .path = client_path }); 687 defer client_repo.deinit(io, allocator); 688 689 // make a commit 690 const commit1 = blk: { 691 const hello_txt = try client_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 692 defer hello_txt.close(io); 693 try hello_txt.writeStreamingAll(io, "hello, world!"); 694 try client_repo.add(io, allocator, &.{"hello.txt"}); 695 break :blk try client_repo.commit(io, allocator, .{ .message = "let there be light" }); 696 }; 697 698 // add a tag 699 _ = try client_repo.addTag(io, allocator, .{ .name = "1.0.0", .message = "hi" }); 700 701 // add remote 702 { 703 if (.windows == builtin.os.tag) { 704 std.mem.replaceScalar(u8, server_path, '\\', '/'); 705 } 706 const separator = if (server_path[0] == '/') "" else "/"; 707 708 const remote_url = switch (transport_def) { 709 //.file => try std.fmt.allocPrint(allocator, "file://{s}{s}", .{ separator, server_path }), 710 .file => try std.fmt.allocPrint(allocator, "../server", .{}), // relative file paths work too 711 .wire => |wire_kind| switch (wire_kind) { 712 .http => try std.fmt.allocPrint(allocator, "http://localhost:{}/server", .{port}), 713 .raw => try std.fmt.allocPrint(allocator, "git://localhost:{}/server", .{port}), 714 .ssh => try std.fmt.allocPrint(allocator, "ssh://localhost:{}{s}{s}", .{ port, separator, server_path }), 715 }, 716 }; 717 defer allocator.free(remote_url); 718 719 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 720 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 721 } 722 723 const refspecs = &.{ 724 "refs/tags/1.0.0:refs/tags/1.0.0", 725 }; 726 727 const is_ssh = switch (transport_def) { 728 .file => false, 729 .wire => |wire_kind| .ssh == wire_kind, 730 }; 731 const ssh_cmd_maybe: ?[]const u8 = if (is_ssh) blk: { 732 const known_hosts_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "known_hosts" }); 733 defer allocator.free(known_hosts_path); 734 735 const priv_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 736 defer allocator.free(priv_key_path); 737 738 break :blk try std.fmt.allocPrint(allocator, "ssh -o UserKnownHostsFile=\"{s}\" -o IdentityFile=\"{s}\"", .{ known_hosts_path, priv_key_path }); 739 } else null; 740 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 741 742 const upload_pack_command = try switch (server_repo_kind) { 743 .xit => std.fmt.allocPrint(allocator, "{s}/zig-out/bin/haxy upload-pack", .{cwd_path}), 744 .git => allocator.dupe(u8, "git-upload-pack"), 745 }; 746 defer allocator.free(upload_pack_command); 747 const receive_pack_command = try switch (server_repo_kind) { 748 .xit => std.fmt.allocPrint(allocator, "{s}/zig-out/bin/haxy receive-pack", .{cwd_path}), 749 .git => allocator.dupe(u8, "git-receive-pack"), 750 }; 751 defer allocator.free(receive_pack_command); 752 753 try client_repo.push( 754 io, 755 allocator, 756 "origin", 757 "master", 758 false, 759 .{ .refspecs = refspecs, .wire = .{ .ssh = .{ 760 .command = ssh_cmd_maybe, 761 .receive_pack_command = receive_pack_command, 762 } } }, 763 ); 764 765 // make sure push was successful 766 { 767 try std.testing.expect(null != try server_repo.readRef(io, .{ .kind = .tag, .name = "1.0.0" })); 768 769 const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 770 try std.testing.expectEqualStrings(&commit1, &oid_master); 771 } 772 773 // make a commit on the server 774 { 775 const hello_txt = try server_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 776 defer hello_txt.close(io); 777 try hello_txt.writeStreamingAll(io, "hello, world from the server!"); 778 try server_repo.add(io, allocator, &.{"hello.txt"}); 779 _ = try server_repo.commit(io, allocator, .{ .message = "new commit from the server" }); 780 } 781 782 // make another commit 783 const commit2 = blk: { 784 const goodbye_txt = try client_repo.core.work_dir.createFile(io, "goodbye.txt", .{ .truncate = true }); 785 defer goodbye_txt.close(io); 786 try goodbye_txt.writeStreamingAll(io, "goodbye, world!"); 787 try client_repo.add(io, allocator, &.{"goodbye.txt"}); 788 break :blk try client_repo.commit(io, allocator, .{ .message = "goodbye" }); 789 }; 790 791 // can't push because server has commit not found locally 792 try std.testing.expectError(error.RemoteRefContainsCommitsNotFoundLocally, client_repo.push( 793 io, 794 allocator, 795 "origin", 796 "master", 797 false, 798 .{ .wire = .{ .ssh = .{ 799 .command = ssh_cmd_maybe, 800 .receive_pack_command = receive_pack_command, 801 } } }, 802 )); 803 804 // make a commit on the server with no parents, thus creating an incompatible git history 805 { 806 const hello_txt = try server_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 807 defer hello_txt.close(io); 808 try hello_txt.writeStreamingAll(io, "hello, world from the server again!"); 809 try server_repo.add(io, allocator, &.{"hello.txt"}); 810 _ = try server_repo.commit(io, allocator, .{ .message = "new git history on the server", .parent_oids = &.{} }); 811 } 812 813 // can't push because commit doesn't exist locally 814 try std.testing.expectError(error.RemoteRefContainsCommitsNotFoundLocally, client_repo.push( 815 io, 816 allocator, 817 "origin", 818 "master", 819 false, 820 .{ .wire = .{ .ssh = .{ 821 .command = ssh_cmd_maybe, 822 .receive_pack_command = receive_pack_command, 823 } } }, 824 )); 825 826 // retrieve the commit object 827 try client_repo.fetch( 828 io, 829 allocator, 830 "origin", 831 .{ .wire = .{ .ssh = .{ 832 .command = ssh_cmd_maybe, 833 .upload_pack_command = upload_pack_command, 834 } } }, 835 ); 836 837 // can't push because server's history is incompatible 838 try std.testing.expectError(error.RemoteRefContainsIncompatibleHistory, client_repo.push( 839 io, 840 allocator, 841 "origin", 842 "master", 843 false, 844 .{ .wire = .{ .ssh = .{ 845 .command = ssh_cmd_maybe, 846 .receive_pack_command = receive_pack_command, 847 } } }, 848 )); 849 850 // test denyNonFastForwards (only for wire transports, file transport bypasses receive-pack) 851 switch (transport_def) { 852 .file => {}, 853 .wire => { 854 // set denyNonFastForwards on server 855 try server_repo.addConfig(io, allocator, .{ .name = "receive.denynonfastforwards", .value = "true" }); 856 857 // save the server's current master ref 858 const oid_before_denied_push = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 859 860 // force push should be rejected by server due to denyNonFastForwards 861 try client_repo.push( 862 io, 863 allocator, 864 "origin", 865 "master", 866 true, 867 .{ .wire = .{ .ssh = .{ 868 .command = ssh_cmd_maybe, 869 .receive_pack_command = receive_pack_command, 870 } } }, 871 ); 872 873 // verify the server ref was not updated (push was denied) 874 { 875 const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 876 try std.testing.expectEqualStrings(&oid_before_denied_push, &oid_master); 877 } 878 879 // remove denyNonFastForwards from server 880 try server_repo.removeConfig(io, allocator, .{ .name = "receive.denynonfastforwards" }); 881 }, 882 } 883 884 // force push 885 try client_repo.push( 886 io, 887 allocator, 888 "origin", 889 "master", 890 true, 891 .{ .wire = .{ .ssh = .{ 892 .command = ssh_cmd_maybe, 893 .receive_pack_command = receive_pack_command, 894 } } }, 895 ); 896 897 // make sure push was successful 898 { 899 const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 900 try std.testing.expectEqualStrings(&commit2, &oid_master); 901 } 902 903 // remove the remote tag 904 try client_repo.push( 905 io, 906 allocator, 907 "origin", 908 ":refs/tags/1.0.0", 909 false, 910 .{ .wire = .{ .ssh = .{ 911 .command = ssh_cmd_maybe, 912 .receive_pack_command = receive_pack_command, 913 } } }, 914 ); 915 916 // make sure push was successful 917 try std.testing.expect(null == try server_repo.readRef(io, .{ .kind = .tag, .name = "1.0.0" })); 918 } 919 920 fn testClone( 921 comptime repo_kind: rp.RepoKind, 922 comptime server_repo_kind: rp.RepoKind, 923 comptime transport_def: net.TransportDefinition, 924 comptime port: u16, 925 comptime shell_out_to_git: bool, 926 io: std.Io, 927 allocator: std.mem.Allocator, 928 ) !void { 929 const temp_dir_name = "temp-testnet-clone"; 930 931 // create the temp dir 932 const cwd = std.Io.Dir.cwd(); 933 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 934 if (temp_dir_or_err) |*temp_dir| { 935 temp_dir.close(io); 936 try cwd.deleteTree(io, temp_dir_name); 937 } else |_| {} 938 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 939 defer cwd.deleteTree(io, temp_dir_name) catch {}; 940 defer temp_dir.close(io); 941 942 // init server 943 var server = try Server(server_repo_kind, transport_def, temp_dir_name, port).init(io, allocator); 944 try server.start(); 945 defer server.stop(); 946 947 const cwd_path = try std.process.currentPathAlloc(io, allocator); 948 defer allocator.free(cwd_path); 949 950 const temp_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 951 defer allocator.free(temp_path); 952 953 const server_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server" }); 954 defer allocator.free(server_path); 955 956 // init server repo with default branch name as main 957 // is_test must be false when shell_out_to_git so commits get real timestamps (needed for --shallow-since) 958 var server_repo = try rp.Repo(server_repo_kind, .{ .is_test = !shell_out_to_git }).init(io, allocator, .{ .path = server_path, .create_default_branch = "main" }); 959 defer server_repo.deinit(io, allocator); 960 961 if (shell_out_to_git) { 962 try server_repo.addConfig(io, allocator, .{ .name = "user.name", .value = "test" }); 963 try server_repo.addConfig(io, allocator, .{ .name = "user.email", .value = "test@test" }); 964 try server_repo.addConfig(io, allocator, .{ .name = "uploadpack.allowfilter", .value = "true" }); 965 } 966 967 // make a commit 968 { 969 const hello_txt = try server_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 970 defer hello_txt.close(io); 971 try hello_txt.writeStreamingAll(io, "hello, world!"); 972 try server_repo.add(io, allocator, &.{"hello.txt"}); 973 _ = try server_repo.commit(io, allocator, .{ .message = "let there be light" }); 974 } 975 976 // tag first commit 977 _ = try server_repo.addTag(io, allocator, .{ .name = "v1", .message = "first" }); 978 979 // make a commit 980 { 981 const goodbye_txt = try server_repo.core.work_dir.createFile(io, "goodbye.txt", .{ .truncate = true }); 982 defer goodbye_txt.close(io); 983 try goodbye_txt.writeStreamingAll(io, "goodbye, world!"); 984 try server_repo.add(io, allocator, &.{"goodbye.txt"}); 985 _ = try server_repo.commit(io, allocator, .{ .message = "add goodbye file" }); 986 } 987 988 // export server repo 989 { 990 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 991 defer export_file.close(io); 992 } 993 994 const client_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "client" }); 995 defer allocator.free(client_path); 996 997 // get remote url 998 const remote_url = blk: { 999 if (.windows == builtin.os.tag) { 1000 std.mem.replaceScalar(u8, server_path, '\\', '/'); 1001 } 1002 const separator = if (server_path[0] == '/') "" else "/"; 1003 1004 break :blk switch (transport_def) { 1005 //.file => try std.fmt.allocPrint(allocator, "file://{s}{s}", .{ separator, server_path }), 1006 .file => try std.fmt.allocPrint(allocator, "server", .{}), // relative file paths work too 1007 .wire => |wire_kind| switch (wire_kind) { 1008 .http => try std.fmt.allocPrint(allocator, "http://localhost:{}/server", .{port}), 1009 .raw => try std.fmt.allocPrint(allocator, "git://localhost:{}/server", .{port}), 1010 .ssh => try std.fmt.allocPrint(allocator, "ssh://localhost:{}{s}{s}", .{ port, separator, server_path }), 1011 }, 1012 }; 1013 }; 1014 defer allocator.free(remote_url); 1015 1016 const is_ssh = switch (transport_def) { 1017 .file => false, 1018 .wire => |wire_kind| .ssh == wire_kind, 1019 }; 1020 1021 const upload_pack_command = try switch (server_repo_kind) { 1022 .xit => std.fmt.allocPrint(allocator, "{s}/zig-out/bin/haxy upload-pack", .{cwd_path}), 1023 .git => allocator.dupe(u8, "git-upload-pack"), 1024 }; 1025 defer allocator.free(upload_pack_command); 1026 1027 if (shell_out_to_git) { 1028 const priv_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 1029 defer allocator.free(priv_key_path); 1030 const ssh_config_arg = try std.fmt.allocPrint(allocator, "core.sshCommand=ssh -o StrictHostKeyChecking=no -o IdentityFile={s}", .{priv_key_path}); 1031 defer allocator.free(ssh_config_arg); 1032 1033 { 1034 var process = try std.process.spawn(io, .{ 1035 .argv = if (is_ssh) 1036 &.{ "git", "-c", ssh_config_arg, "clone", "--upload-pack", upload_pack_command, "--depth", "1", remote_url, "client" } 1037 else 1038 &.{ "git", "clone", "--depth", "1", remote_url, "client" }, 1039 .cwd = .{ .path = temp_path }, 1040 .stdin = .ignore, 1041 .stdout = .ignore, 1042 .stderr = .ignore, 1043 }); 1044 const term = try process.wait(io); 1045 if (term != .exited) { 1046 return error.GitCommandFailed; 1047 } 1048 } 1049 1050 // make sure shallow clone was successful 1051 { 1052 const hello_txt = try temp_dir.openFile(io, "client/hello.txt", .{}); 1053 hello_txt.close(io); 1054 } 1055 1056 // make a third commit on the server 1057 { 1058 const extra_txt = try server_repo.core.work_dir.createFile(io, "extra.txt", .{ .truncate = true }); 1059 defer extra_txt.close(io); 1060 try extra_txt.writeStreamingAll(io, "extra content"); 1061 try server_repo.add(io, allocator, &.{"extra.txt"}); 1062 _ = try server_repo.commit(io, allocator, .{ .message = "add extra file" }); 1063 } 1064 1065 // pull --unshallow to deepen the clone and get the new commit 1066 { 1067 var process = try std.process.spawn(io, .{ 1068 .argv = if (is_ssh) 1069 &.{ "git", "-c", ssh_config_arg, "pull", "--upload-pack", upload_pack_command, "--unshallow" } 1070 else 1071 &.{ "git", "pull", "--unshallow" }, 1072 .cwd = .{ .path = client_path }, 1073 .stdin = .ignore, 1074 .stdout = .ignore, 1075 .stderr = .ignore, 1076 }); 1077 const term = try process.wait(io); 1078 if (term != .exited) { 1079 return error.GitCommandFailed; 1080 } 1081 } 1082 1083 // make sure unshallow pull was successful 1084 { 1085 const extra_txt = try temp_dir.openFile(io, "client/extra.txt", .{}); 1086 extra_txt.close(io); 1087 } 1088 1089 // delete client and clone again with --shallow-since 1090 try cwd.deleteTree(io, temp_dir_name ++ "/client"); 1091 1092 { 1093 var process = try std.process.spawn(io, .{ 1094 .argv = if (is_ssh) 1095 &.{ "git", "-c", ssh_config_arg, "clone", "--upload-pack", upload_pack_command, "--shallow-since=2000-01-01", remote_url, "client" } 1096 else 1097 &.{ "git", "clone", "--shallow-since=2000-01-01", remote_url, "client" }, 1098 .cwd = .{ .path = temp_path }, 1099 .stdin = .ignore, 1100 .stdout = .ignore, 1101 .stderr = .ignore, 1102 }); 1103 const term = try process.wait(io); 1104 if (term != .exited) { 1105 return error.GitCommandFailed; 1106 } 1107 } 1108 1109 // make sure shallow clone was successful 1110 { 1111 const hello_txt = try temp_dir.openFile(io, "client/hello.txt", .{}); 1112 hello_txt.close(io); 1113 } 1114 1115 // delete client and clone again with --shallow-exclude 1116 try cwd.deleteTree(io, temp_dir_name ++ "/client"); 1117 1118 { 1119 var process = try std.process.spawn(io, .{ 1120 .argv = if (is_ssh) 1121 &.{ "git", "-c", ssh_config_arg, "clone", "--upload-pack", upload_pack_command, "--shallow-exclude=v1", remote_url, "client" } 1122 else 1123 &.{ "git", "clone", "--shallow-exclude=v1", remote_url, "client" }, 1124 .cwd = .{ .path = temp_path }, 1125 .stdin = .ignore, 1126 .stdout = .ignore, 1127 .stderr = .ignore, 1128 }); 1129 const term = try process.wait(io); 1130 if (term != .exited) { 1131 return error.GitCommandFailed; 1132 } 1133 } 1134 1135 // make sure shallow clone was successful 1136 { 1137 const hello_txt = try temp_dir.openFile(io, "client/hello.txt", .{}); 1138 hello_txt.close(io); 1139 } 1140 1141 // delete client and clone again with --filter=blob:none 1142 try cwd.deleteTree(io, temp_dir_name ++ "/client"); 1143 1144 { 1145 var process = try std.process.spawn(io, .{ 1146 .argv = if (is_ssh) 1147 &.{ "git", "-c", ssh_config_arg, "clone", "--upload-pack", upload_pack_command, "--filter=blob:none", remote_url, "client" } 1148 else 1149 &.{ "git", "clone", "--filter=blob:none", remote_url, "client" }, 1150 .cwd = .{ .path = temp_path }, 1151 .stdin = .ignore, 1152 .stdout = .ignore, 1153 .stderr = .ignore, 1154 }); 1155 const term = try process.wait(io); 1156 if (term != .exited) { 1157 return error.GitCommandFailed; 1158 } 1159 } 1160 1161 // make sure partial clone was successful 1162 { 1163 const hello_txt = try temp_dir.openFile(io, "client/hello.txt", .{}); 1164 hello_txt.close(io); 1165 } 1166 1167 // delete client and clone again with --filter=tree:0 1168 try cwd.deleteTree(io, temp_dir_name ++ "/client"); 1169 1170 { 1171 var process = try std.process.spawn(io, .{ 1172 .argv = if (is_ssh) 1173 &.{ "git", "-c", ssh_config_arg, "clone", "--upload-pack", upload_pack_command, "--filter=tree:0", remote_url, "client" } 1174 else 1175 &.{ "git", "clone", "--filter=tree:0", remote_url, "client" }, 1176 .cwd = .{ .path = temp_path }, 1177 .stdin = .ignore, 1178 .stdout = .ignore, 1179 .stderr = .ignore, 1180 }); 1181 const term = try process.wait(io); 1182 if (term != .exited) { 1183 return error.GitCommandFailed; 1184 } 1185 } 1186 1187 // make sure treeless clone was successful 1188 { 1189 const goodbye_txt = try temp_dir.openFile(io, "client/goodbye.txt", .{}); 1190 goodbye_txt.close(io); 1191 } 1192 } else { 1193 const ssh_cmd_maybe: ?[]const u8 = if (is_ssh) blk: { 1194 const known_hosts_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "known_hosts" }); 1195 defer allocator.free(known_hosts_path); 1196 1197 const priv_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 1198 defer allocator.free(priv_key_path); 1199 1200 break :blk try std.fmt.allocPrint(allocator, "ssh -o UserKnownHostsFile=\"{s}\" -o IdentityFile=\"{s}\"", .{ known_hosts_path, priv_key_path }); 1201 } else null; 1202 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1203 1204 // clone repo 1205 var client_repo = try rp.Repo(repo_kind, .{ .is_test = true }).clone( 1206 io, 1207 allocator, 1208 remote_url, 1209 temp_path, 1210 client_path, 1211 .{ .wire = .{ .ssh = .{ 1212 .command = ssh_cmd_maybe, 1213 .upload_pack_command = upload_pack_command, 1214 } } }, 1215 ); 1216 defer client_repo.deinit(io, allocator); 1217 1218 // make sure HEAD points to the right default branch 1219 var current_branch_buffer = [_]u8{0} ** rf.MAX_REF_CONTENT_SIZE; 1220 const head = try client_repo.head(io, &current_branch_buffer); 1221 try std.testing.expectEqualStrings("main", head.ref.name); 1222 1223 // make sure clone was successful 1224 const hello_txt = try temp_dir.openFile(io, "client/hello.txt", .{}); 1225 defer hello_txt.close(io); 1226 } 1227 } 1228 1229 fn testFetchLarge( 1230 comptime repo_kind: rp.RepoKind, 1231 comptime server_repo_kind: rp.RepoKind, 1232 comptime transport_def: net.TransportDefinition, 1233 comptime port: u16, 1234 comptime shell_out_to_git: bool, 1235 io: std.Io, 1236 allocator: std.mem.Allocator, 1237 ) !void { 1238 const temp_dir_name = "temp-testnet-fetch-large"; 1239 1240 // create the temp dir 1241 const cwd = std.Io.Dir.cwd(); 1242 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 1243 if (temp_dir_or_err) |*temp_dir| { 1244 temp_dir.close(io); 1245 try cwd.deleteTree(io, temp_dir_name); 1246 } else |_| {} 1247 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 1248 defer cwd.deleteTree(io, temp_dir_name) catch {}; 1249 defer temp_dir.close(io); 1250 1251 // init server 1252 var server = try Server(server_repo_kind, transport_def, temp_dir_name, port).init(io, allocator); 1253 try server.start(); 1254 defer server.stop(); 1255 1256 const cwd_path = try std.process.currentPathAlloc(io, allocator); 1257 defer allocator.free(cwd_path); 1258 1259 const server_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server" }); 1260 defer allocator.free(server_path); 1261 1262 var server_repo = try rp.Repo(server_repo_kind, .{ .is_test = true }).init(io, allocator, .{ .path = server_path }); 1263 defer server_repo.deinit(io, allocator); 1264 1265 var server_dir = try cwd.openDir(io, server_path, .{}); 1266 defer server_dir.close(io); 1267 1268 // copy files from current repo into server dir 1269 for (&[_][]const u8{"src"}) |dir_name| { 1270 var src_repo_dir = try cwd.openDir(io, dir_name, .{ .iterate = true }); 1271 defer src_repo_dir.close(io); 1272 1273 var dest_repo_dir = try server_dir.createDirPathOpen(io, dir_name, .{}); 1274 defer dest_repo_dir.close(io); 1275 1276 try copyDir(io, src_repo_dir, dest_repo_dir); 1277 1278 try server_repo.add(io, allocator, &.{dir_name}); 1279 } 1280 1281 // make a commit 1282 const commit1 = try server_repo.commit(io, allocator, .{ .message = "let there be light" }); 1283 1284 // export server repo 1285 { 1286 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1287 defer export_file.close(io); 1288 } 1289 1290 if (shell_out_to_git) { 1291 try server_repo.addConfig(io, allocator, .{ .name = "uploadpack.allowrefinwant", .value = "true" }); 1292 } 1293 1294 const client_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "client" }); 1295 defer allocator.free(client_path); 1296 1297 var client_repo = try rp.Repo(repo_kind, .{ .is_test = true }).init(io, allocator, .{ .path = client_path }); 1298 defer client_repo.deinit(io, allocator); 1299 1300 // add remote 1301 { 1302 if (.windows == builtin.os.tag) { 1303 std.mem.replaceScalar(u8, server_path, '\\', '/'); 1304 } 1305 const separator = if (server_path[0] == '/') "" else "/"; 1306 1307 const remote_url = switch (transport_def) { 1308 //.file => try std.fmt.allocPrint(allocator, "file://{s}{s}", .{ separator, server_path }), 1309 .file => try std.fmt.allocPrint(allocator, "../server", .{}), // relative file paths work too 1310 .wire => |wire_kind| switch (wire_kind) { 1311 .http => try std.fmt.allocPrint(allocator, "http://localhost:{}/server", .{port}), 1312 .raw => try std.fmt.allocPrint(allocator, "git://localhost:{}/server", .{port}), 1313 .ssh => try std.fmt.allocPrint(allocator, "ssh://localhost:{}{s}{s}", .{ port, separator, server_path }), 1314 }, 1315 }; 1316 defer allocator.free(remote_url); 1317 1318 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 1319 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 1320 } 1321 1322 const is_ssh = switch (transport_def) { 1323 .file => false, 1324 .wire => |wire_kind| .ssh == wire_kind, 1325 }; 1326 1327 const upload_pack_command = try switch (server_repo_kind) { 1328 .xit => std.fmt.allocPrint(allocator, "{s}/zig-out/bin/haxy upload-pack", .{cwd_path}), 1329 .git => allocator.dupe(u8, "git-upload-pack"), 1330 }; 1331 defer allocator.free(upload_pack_command); 1332 1333 if (shell_out_to_git) { 1334 const priv_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 1335 defer allocator.free(priv_key_path); 1336 const ssh_config_arg = try std.fmt.allocPrint(allocator, "core.sshCommand=ssh -o StrictHostKeyChecking=no -o IdentityFile={s}", .{priv_key_path}); 1337 defer allocator.free(ssh_config_arg); 1338 1339 { 1340 var process = try std.process.spawn(io, .{ 1341 .argv = if (is_ssh) 1342 &.{ "git", "-c", ssh_config_arg, "pull", "--upload-pack", upload_pack_command, "origin", "master" } 1343 else 1344 &.{ "git", "pull", "origin", "master" }, 1345 .cwd = .{ .path = client_path }, 1346 .stdin = .ignore, 1347 .stdout = .ignore, 1348 .stderr = .ignore, 1349 }); 1350 const term = try process.wait(io); 1351 if (term != .exited) { 1352 return error.GitCommandFailed; 1353 } 1354 } 1355 1356 // make sure pull was successful 1357 { 1358 const oid_master = (try client_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1359 try std.testing.expectEqualStrings(&commit1, &oid_master); 1360 } 1361 1362 // make another commit on the server 1363 const commit2 = blk: { 1364 const extra_txt = try server_repo.core.work_dir.createFile(io, "extra.txt", .{ .truncate = true }); 1365 defer extra_txt.close(io); 1366 try extra_txt.writeStreamingAll(io, "extra content"); 1367 try server_repo.add(io, allocator, &.{"extra.txt"}); 1368 break :blk try server_repo.commit(io, allocator, .{ .message = "add extra file" }); 1369 }; 1370 1371 // fetch with ref-in-want (git uses want-ref in protocol v2 when fetching named refs) 1372 { 1373 var process = try std.process.spawn(io, .{ 1374 .argv = if (is_ssh) 1375 &.{ "git", "-c", ssh_config_arg, "fetch", "--upload-pack", upload_pack_command, "origin", "master" } 1376 else 1377 &.{ "git", "fetch", "origin", "master" }, 1378 .cwd = .{ .path = client_path }, 1379 .stdin = .ignore, 1380 .stdout = .ignore, 1381 .stderr = .ignore, 1382 }); 1383 const term = try process.wait(io); 1384 if (term != .exited) { 1385 return error.GitCommandFailed; 1386 } 1387 } 1388 1389 // make sure fetch with want-ref was successful 1390 { 1391 const oid_remote_master = (try client_repo.readRef(io, .{ .kind = .{ .remote = "origin" }, .name = "master" })).?; 1392 try std.testing.expectEqualStrings(&commit2, &oid_remote_master); 1393 } 1394 } else { 1395 const refspecs = &.{ 1396 "+refs/heads/master:refs/heads/master", 1397 }; 1398 1399 const ssh_cmd_maybe: ?[]const u8 = if (is_ssh) blk: { 1400 const known_hosts_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "known_hosts" }); 1401 defer allocator.free(known_hosts_path); 1402 1403 const priv_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 1404 defer allocator.free(priv_key_path); 1405 1406 break :blk try std.fmt.allocPrint(allocator, "ssh -o UserKnownHostsFile=\"{s}\" -o IdentityFile=\"{s}\"", .{ known_hosts_path, priv_key_path }); 1407 } else null; 1408 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1409 1410 try client_repo.fetch( 1411 io, 1412 allocator, 1413 "origin", 1414 .{ .refspecs = refspecs, .wire = .{ .ssh = .{ 1415 .command = ssh_cmd_maybe, 1416 .upload_pack_command = upload_pack_command, 1417 } } }, 1418 ); 1419 1420 // update the working dir 1421 try client_repo.restore(io, allocator, "."); 1422 } 1423 1424 // make sure fetch was successful 1425 { 1426 const oid_master = (try client_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1427 try std.testing.expectEqualStrings(&commit1, &oid_master); 1428 } 1429 } 1430 1431 fn testPushLarge( 1432 comptime repo_kind: rp.RepoKind, 1433 comptime server_repo_kind: rp.RepoKind, 1434 comptime transport_def: net.TransportDefinition, 1435 comptime port: u16, 1436 comptime shell_out_to_git: bool, 1437 io: std.Io, 1438 allocator: std.mem.Allocator, 1439 ) !void { 1440 const temp_dir_name = "temp-testnet-push-large"; 1441 1442 // create the temp dir 1443 const cwd = std.Io.Dir.cwd(); 1444 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 1445 if (temp_dir_or_err) |*temp_dir| { 1446 temp_dir.close(io); 1447 try cwd.deleteTree(io, temp_dir_name); 1448 } else |_| {} 1449 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 1450 defer cwd.deleteTree(io, temp_dir_name) catch {}; 1451 defer temp_dir.close(io); 1452 1453 // init server 1454 var server = try Server(server_repo_kind, transport_def, temp_dir_name, port).init(io, allocator); 1455 try server.start(); 1456 defer server.stop(); 1457 1458 const cwd_path = try std.process.currentPathAlloc(io, allocator); 1459 defer allocator.free(cwd_path); 1460 1461 const server_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server" }); 1462 defer allocator.free(server_path); 1463 1464 var server_repo = try rp.Repo(server_repo_kind, .{ .is_test = true }).init(io, allocator, .{ .path = server_path }); 1465 defer server_repo.deinit(io, allocator); 1466 1467 // add config 1468 switch (transport_def) { 1469 .file => try server_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "true" }), 1470 .wire => { 1471 try server_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "false" }); 1472 try server_repo.addConfig(io, allocator, .{ .name = "receive.denycurrentbranch", .value = "updateinstead" }); 1473 }, 1474 } 1475 try server_repo.addConfig(io, allocator, .{ .name = "http.receivepack", .value = "true" }); 1476 1477 // export server repo 1478 { 1479 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1480 defer export_file.close(io); 1481 } 1482 1483 const client_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "client" }); 1484 defer allocator.free(client_path); 1485 1486 var client_repo = try rp.Repo(repo_kind, .{ .is_test = true }).init(io, allocator, .{ .path = client_path }); 1487 defer client_repo.deinit(io, allocator); 1488 1489 var client_dir = try cwd.openDir(io, client_path, .{}); 1490 defer client_dir.close(io); 1491 1492 { 1493 const hello_txt = try client_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 1494 defer hello_txt.close(io); 1495 try hello_txt.writeStreamingAll(io, "hello, world!"); 1496 try client_repo.add(io, allocator, &.{"hello.txt"}); 1497 } 1498 1499 // copy files from current repo into client dir 1500 for (&[_][]const u8{"src"}) |dir_name| { 1501 var src_repo_dir = try cwd.openDir(io, dir_name, .{ .iterate = true }); 1502 defer src_repo_dir.close(io); 1503 1504 var dest_repo_dir = try client_dir.createDirPathOpen(io, dir_name, .{}); 1505 defer dest_repo_dir.close(io); 1506 1507 try copyDir(io, src_repo_dir, dest_repo_dir); 1508 1509 try client_repo.add(io, allocator, &.{dir_name}); 1510 } 1511 1512 _ = try client_repo.commit(io, allocator, .{ .message = "let there be light" }); 1513 1514 // change the files so git will send them as delta objects 1515 for (&[_][]const u8{"src"}) |dir_name| { 1516 var dest_repo_dir = try client_dir.createDirPathOpen(io, dir_name, .{ .open_options = .{ .iterate = true } }); 1517 defer dest_repo_dir.close(io); 1518 1519 { 1520 var iter = dest_repo_dir.iterate(); 1521 while (try iter.next(io)) |entry| { 1522 switch (entry.kind) { 1523 .file => { 1524 const file = try dest_repo_dir.openFile(io, entry.name, .{ .mode = .read_write }); 1525 defer file.close(io); 1526 var writer = file.writer(io, &.{}); 1527 try writer.interface.writeAll("EDIT"); 1528 }, 1529 else => {}, 1530 } 1531 } 1532 } 1533 1534 try client_repo.add(io, allocator, &.{dir_name}); 1535 } 1536 1537 const commit2 = try client_repo.commit(io, allocator, .{ .message = "more stuff" }); 1538 1539 // add remote 1540 { 1541 if (.windows == builtin.os.tag) { 1542 std.mem.replaceScalar(u8, server_path, '\\', '/'); 1543 } 1544 const separator = if (server_path[0] == '/') "" else "/"; 1545 1546 const remote_url = switch (transport_def) { 1547 //.file => try std.fmt.allocPrint(allocator, "file://{s}{s}", .{ separator, server_path }), 1548 .file => try std.fmt.allocPrint(allocator, "../server", .{}), // relative file paths work too 1549 .wire => |wire_kind| switch (wire_kind) { 1550 .http => try std.fmt.allocPrint(allocator, "http://localhost:{}/server", .{port}), 1551 .raw => try std.fmt.allocPrint(allocator, "git://localhost:{}/server", .{port}), 1552 .ssh => try std.fmt.allocPrint(allocator, "ssh://localhost:{}{s}{s}", .{ port, separator, server_path }), 1553 }, 1554 }; 1555 defer allocator.free(remote_url); 1556 1557 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 1558 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 1559 } 1560 1561 const is_ssh = switch (transport_def) { 1562 .file => false, 1563 .wire => |wire_kind| .ssh == wire_kind, 1564 }; 1565 1566 const receive_pack_command = try switch (server_repo_kind) { 1567 .xit => std.fmt.allocPrint(allocator, "{s}/zig-out/bin/haxy receive-pack", .{cwd_path}), 1568 .git => allocator.dupe(u8, "git-receive-pack"), 1569 }; 1570 defer allocator.free(receive_pack_command); 1571 1572 if (shell_out_to_git) { 1573 const priv_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 1574 defer allocator.free(priv_key_path); 1575 const ssh_config_arg = try std.fmt.allocPrint(allocator, "core.sshCommand=ssh -o StrictHostKeyChecking=no -o IdentityFile={s}", .{priv_key_path}); 1576 defer allocator.free(ssh_config_arg); 1577 1578 // shell out to git so it will send delta objects 1579 var process = try std.process.spawn(io, .{ 1580 .argv = if (is_ssh) 1581 &.{ "git", "-c", ssh_config_arg, "push", "--receive-pack", receive_pack_command, "origin", "master" } 1582 else 1583 &.{ "git", "push", "origin", "master" }, 1584 .cwd = .{ .path = client_path }, 1585 .stdin = .ignore, 1586 .stdout = .ignore, 1587 .stderr = .ignore, 1588 }); 1589 const term = try process.wait(io); 1590 if (term != .exited) { 1591 return error.GitCommandFailed; 1592 } 1593 } else { 1594 const ssh_cmd_maybe: ?[]const u8 = if (is_ssh) blk: { 1595 const known_hosts_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "known_hosts" }); 1596 defer allocator.free(known_hosts_path); 1597 1598 const priv_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 1599 defer allocator.free(priv_key_path); 1600 1601 break :blk try std.fmt.allocPrint(allocator, "ssh -o UserKnownHostsFile=\"{s}\" -o IdentityFile=\"{s}\"", .{ known_hosts_path, priv_key_path }); 1602 } else null; 1603 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1604 1605 try client_repo.push( 1606 io, 1607 allocator, 1608 "origin", 1609 "master", 1610 false, 1611 .{ .wire = .{ .ssh = .{ 1612 .command = ssh_cmd_maybe, 1613 .receive_pack_command = receive_pack_command, 1614 } } }, 1615 ); 1616 1617 if (transport_def == .file) { 1618 // update the working dir 1619 try server_repo.restore(io, allocator, "."); 1620 } 1621 } 1622 1623 // make sure push was successful 1624 { 1625 const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1626 try std.testing.expectEqualStrings(&commit2, &oid_master); 1627 1628 const hello_txt = try temp_dir.openFile(io, "server/hello.txt", .{}); 1629 defer hello_txt.close(io); 1630 } 1631 } 1632 1633 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 1634 var iter = src_dir.iterate(); 1635 while (try iter.next(io)) |entry| { 1636 switch (entry.kind) { 1637 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 1638 .directory => { 1639 try dest_dir.createDirPath(io, entry.name); 1640 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1641 defer dest_entry_dir.close(io); 1642 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1643 defer src_entry_dir.close(io); 1644 try copyDir(io, src_entry_dir, dest_entry_dir); 1645 }, 1646 else => {}, 1647 } 1648 } 1649 } 1650