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