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