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