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