╭ net.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭ find file ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ ╭────╮ ╭ 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 web_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", "--web-listen", web_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 .repo_name = "target", 566 .title = "add a feature", 567 .description = "a draft patch", 568 .labels = "enhancement", 569 .target_branch = "master", 570 .author = .{ .name = "admin", .email = "admin@example.test" }, 571 .timestamp = 1, 572 }); 573 }; 574 defer allocator.free(draft_path); 575 576 const client_path = try std.Io.Dir.path.join(allocator, &.{ temp_path, "client" }); 577 defer allocator.free(client_path); 578 const ssh_cmd = (try sshCommand(true, allocator, temp_path, port)).?; 579 defer allocator.free(ssh_cmd); 580 var client = try rp.Repo(.xit, .{ .hash = hash_kind, .is_test = true }).clone( 581 io, 582 allocator, 583 "git@localhost:admin:target", 584 client_path, 585 client_path, 586 null, 587 .{ .transport = .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } } }, 588 ); 589 defer client.deinit(io, allocator); 590 591 const source_oid = blk: { 592 const file = try client.core.work_dir.createFile(io, "feature.txt", .{ .truncate = true }); 593 defer file.close(io); 594 try file.writeStreamingAll(io, "fork contents\n"); 595 try client.add(io, allocator, &.{"feature.txt"}); 596 break :blk try client.commit(io, allocator, .{ .message = "add feature" }); 597 }; 598 599 const remote_url = try allocator.print("git@localhost:admin:target+{s}", .{&fork_id_hex}); 600 defer allocator.free(remote_url); 601 try client.addRemote(io, allocator, .{ .name = "patch", .value = remote_url }); 602 try client.push(io, allocator, "patch", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } }); 603 604 var first_revision_id: [evt.event_id_size]u8 = undefined; 605 { 606 var draft = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = draft_path }); 607 defer draft.deinit(io, allocator); 608 try std.testing.expect(try draft.isBare(io, allocator)); 609 var arena = std.heap.ArenaAllocator.init(allocator); 610 defer arena.deinit(); 611 const moment = try evt.currentMoment(repo_opts, &draft); 612 const patch = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 613 try std.testing.expect(!patch.removed); 614 try std.testing.expectEqualStrings(&source_oid, &(try draft.readRef(io, fork.ref) orelse return error.NotFound)); 615 const selected = patch.event.revision orelse return error.NotFound; 616 try std.testing.expectEqualStrings("master", patch.event.target_branch); 617 try std.testing.expectEqualStrings(&source_oid, selected.source_oid); 618 try std.testing.expectEqual(@as(?u64, 1), selected.commit_count); 619 first_revision_id = try evt.parseEventId(&selected.id); 620 const revision = (try evt.PatchRev.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &first_revision_id)) orelse return error.NotFound; 621 try std.testing.expectEqualStrings(&source_oid, revision.event.source_oid); 622 } 623 624 // 625 // publish, then push another revision 626 // 627 628 { 629 var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_path }); 630 defer admin.deinit(io, allocator); 631 var user_repo = (try evt.openUserRepo(io, allocator, users_dir, &user_id)) orelse return error.NotFound; 632 defer user_repo.deinit(io, allocator); 633 var target = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path }); 634 defer target.deinit(io, allocator); 635 // pushing a draft does not create haxy state or a mergeability cache 636 try std.testing.expectError(error.NotFound, evt.currentMoment(repo_opts, &target)); 637 try std.testing.expectEqual(null, try (try target.core.latestMoment()).getCursor(hash.hashInt(hash_kind, evt.Patch.patch_id_to_mergeability_key))); 638 try target.addBranch(io, .{ .name = "feature" }); 639 try pch.publish(repo_opts, io, allocator, .{ .users_dir = users_dir }, &admin, &target, .{ 640 .id = fork_id_hex, 641 .user_id = user_id, 642 .repo_id = repo_id, 643 .author = .{ .name = "admin", .email = "admin@example.test" }, 644 .timestamp = 2, 645 }); 646 var arena = std.heap.ArenaAllocator.init(allocator); 647 defer arena.deinit(); 648 const moment = try evt.currentMoment(repo_opts, &target); 649 const patch = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 650 try evt.Patch.update(.{ .server = .{ .users_dir = users_dir } }, .xit, repo_opts, io, allocator, &target, &fork_id, .{ .fields = .{ 651 .title = "edited feature", 652 .description = patch.event.description, 653 .labels = patch.event.labels, 654 .target_branch = "feature", 655 } }, .{ .name = "admin", .email = "admin@example.test" }); 656 } 657 658 const feature_remote_url = try allocator.print("git@localhost:admin:target+{s}", .{&fork_id_hex}); 659 defer allocator.free(feature_remote_url); 660 try client.addRemote(io, allocator, .{ .name = "patch-to-feature", .value = feature_remote_url }); 661 662 const second_source_oid = blk: { 663 const file = try client.core.work_dir.createFile(io, "feature.txt", .{ .truncate = true }); 664 defer file.close(io); 665 try file.writeStreamingAll(io, "updated fork contents\n"); 666 try client.add(io, allocator, &.{"feature.txt"}); 667 break :blk try client.commit(io, allocator, .{ .message = "revise feature" }); 668 }; 669 try client.push(io, allocator, "patch-to-feature", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } }); 670 671 var second_revision_id: [evt.event_id_size]u8 = undefined; 672 var second_patch_oid: [hash.hexLen(hash_kind)]u8 = undefined; 673 { 674 var draft = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = draft_path }); 675 defer draft.deinit(io, allocator); 676 var arena = std.heap.ArenaAllocator.init(allocator); 677 defer arena.deinit(); 678 const moment = try evt.currentMoment(repo_opts, &draft); 679 const patch = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 680 try std.testing.expect(patch.removed); 681 const newest = (try evt.PatchRev.readNewest(evt.EventDB(hash_kind), hash_kind, moment, &arena)) orelse return error.NotFound; 682 second_revision_id = newest.id; 683 @memcpy(&second_patch_oid, newest.record.patch_oid); 684 try std.testing.expect(!std.mem.eql(u8, &first_revision_id, &second_revision_id)); 685 try std.testing.expectEqualStrings(&second_source_oid, newest.record.event.source_oid); 686 try std.testing.expectEqualStrings("edited feature", newest.record.event.message); 687 } 688 { 689 var target = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path }); 690 defer target.deinit(io, allocator); 691 var arena = std.heap.ArenaAllocator.init(allocator); 692 defer arena.deinit(); 693 const moment = try evt.currentMoment(repo_opts, &target); 694 const patch = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 695 try std.testing.expectEqualStrings("edited feature", patch.event.title); 696 const selected = patch.event.revision orelse return error.NotFound; 697 try std.testing.expectEqualStrings(&std.fmt.bytesToHex(second_revision_id, .lower), &selected.id); 698 try std.testing.expectEqualStrings(&second_source_oid, selected.source_oid); 699 try std.testing.expectEqual(@as(?u64, 2), selected.commit_count); 700 try std.testing.expectEqualStrings("feature", patch.event.target_branch); 701 } 702 703 // 704 // reject extra refs without applying the patch update 705 // 706 707 const third_source_oid = blk: { 708 const file = try client.core.work_dir.createFile(io, "feature.txt", .{ .truncate = true }); 709 defer file.close(io); 710 try file.writeStreamingAll(io, "rejected fork contents\n"); 711 try client.add(io, allocator, &.{"feature.txt"}); 712 break :blk try client.commit(io, allocator, .{ .message = "rejected revision" }); 713 }; 714 const extra_ref_rejected = if (client.push(io, allocator, "patch-to-feature", "master:patch", false, .{ 715 .refspecs = &.{"refs/heads/master:refs/heads/extra"}, 716 .wire = .{ .ssh = .{ .command = ssh_cmd } }, 717 })) |_| false else |_| true; 718 try std.testing.expect(extra_ref_rejected); 719 { 720 var draft = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = draft_path }); 721 defer draft.deinit(io, allocator); 722 try std.testing.expectEqualStrings(&second_source_oid, &(try draft.readRef(io, fork.ref) orelse return error.NotFound)); 723 try std.testing.expectEqual(null, try draft.readRef(io, .{ .kind = .head, .name = "extra" })); 724 var arena = std.heap.ArenaAllocator.init(allocator); 725 defer arena.deinit(); 726 const moment = try evt.currentMoment(repo_opts, &draft); 727 const newest = (try evt.PatchRev.readNewest(evt.EventDB(hash_kind), hash_kind, moment, &arena)) orelse return error.NotFound; 728 try std.testing.expectEqualSlices(u8, &second_revision_id, &newest.id); 729 } 730 731 // 732 // a removed target patch stays removed after another push 733 // 734 735 { 736 var target = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path }); 737 defer target.deinit(io, allocator); 738 try evt.remove(.{ .server = .{ .users_dir = users_dir } }, .repo, .xit, repo_opts, io, allocator, &target, &fork_id, .patch, .{ .name = "admin", .email = "admin@example.test" }); 739 } 740 try client.push(io, allocator, "patch-to-feature", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } }); 741 { 742 var target = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path }); 743 defer target.deinit(io, allocator); 744 var arena = std.heap.ArenaAllocator.init(allocator); 745 defer arena.deinit(); 746 const moment = try evt.currentMoment(repo_opts, &target); 747 const patch = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 748 try std.testing.expect(patch.removed); 749 } 750 751 // 752 // restore the patch, merge a descendant of its source oid, and push the merge 753 // 754 755 { 756 var target = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path }); 757 defer target.deinit(io, allocator); 758 var arena = std.heap.ArenaAllocator.init(allocator); 759 defer arena.deinit(); 760 const moment = try evt.currentMoment(repo_opts, &target); 761 const removed = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 762 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .repo, .xit, repo_opts, io, allocator, &target, evt.events_ref, &.{.{ 763 .id = fork_id_hex, 764 .timestamp = 4, 765 .author = .{ .name = "admin", .email = "admin@example.test" }, 766 .event = .{ .patch = removed.event }, 767 }}); 768 } 769 const merge_parents = [_][hash.hexLen(hash_kind)]u8{ base_oid, third_source_oid }; 770 const merge_oid = try client.commit(io, allocator, .{ .message = "merge patch", .parent_oids = &merge_parents }); 771 // later commits in the push must not be mistaken for the merge itself 772 _ = try client.commit(io, allocator, .{ .message = "after merge", .allow_empty = true }); 773 try client.push(io, allocator, "origin", "master:feature", false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } }); 774 { 775 var target = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path }); 776 defer target.deinit(io, allocator); 777 var arena = std.heap.ArenaAllocator.init(allocator); 778 defer arena.deinit(); 779 const moment = try evt.currentMoment(repo_opts, &target); 780 const patch = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 781 try std.testing.expectEqual(.merged, patch.event.status.kind()); 782 const merged_status = switch (patch.event.status) { 783 .merged => |value| value, 784 else => return error.InvalidPatch, 785 }; 786 try std.testing.expectEqual(.source, merged_status.revision); 787 const result_id = try evt.parseEventId(&merged_status.patchrev_id); 788 const result = (try evt.PatchRev.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &result_id)) orelse return error.NotFound; 789 try std.testing.expectEqualStrings(&base_oid, result.event.base_oid); 790 try std.testing.expectEqualStrings(&merge_oid, result.event.source_oid); 791 const imported = (try evt.PatchRev.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &second_revision_id)) orelse return error.NotFound; 792 try std.testing.expectEqualStrings(&second_patch_oid, imported.patch_oid); 793 } 794 795 const other_url = try allocator.print("git@localhost:admin:other+{s}", .{&fork_id_hex}); 796 defer allocator.free(other_url); 797 try client.addRemote(io, allocator, .{ .name = "other", .value = other_url }); 798 // early rejections need no input. give ssh eof so windows openssh 799 // doesn't hang closing stdin before forwarding the error. 800 const reject_ssh_cmd = try allocator.print("{s} -n", .{ssh_cmd}); 801 defer allocator.free(reject_ssh_cmd); 802 try std.testing.expectError(error.ServerReportedError, client.push(io, allocator, "other", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = reject_ssh_cmd } } })); 803 804 // 805 // the fork remains cloneable after the target repo is removed 806 // 807 808 { 809 var user_repo = (try evt.openUserRepo(io, allocator, users_dir, &user_id)) orelse return error.NotFound; 810 defer user_repo.deinit(io, allocator); 811 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" }); 812 } 813 const clone_path = try std.Io.Dir.path.join(allocator, &.{ temp_path, "fork-clone" }); 814 defer allocator.free(clone_path); 815 var fork_clone = try rp.Repo(.xit, .{ .hash = hash_kind, .is_test = true }).clone( 816 io, 817 allocator, 818 remote_url, 819 clone_path, 820 clone_path, 821 null, 822 .{ .transport = .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } } }, 823 ); 824 defer fork_clone.deinit(io, allocator); 825 try std.testing.expectEqualStrings(&third_source_oid, &(try fork_clone.readRef(io, fork.ref) orelse return error.NotFound)); 826 827 { 828 var user_repo = (try evt.openUserRepo(io, allocator, users_dir, &user_id)) orelse return error.NotFound; 829 defer user_repo.deinit(io, allocator); 830 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .user, .xit, evt.user_repo_opts, io, allocator, &user_repo, evt.events_ref, &.{ 831 .{ 832 .id = std.fmt.bytesToHex(repo_id, .lower), 833 .timestamp = 2, 834 .author = .{ .name = "admin", .email = "admin@example.test" }, 835 .event = .{ .repo = .{ 836 .user_id = std.fmt.bytesToHex(user_id, .lower), 837 .name = "target", 838 .description = "", 839 .patch_role = .read, 840 } }, 841 }, 842 }); 843 var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_path }); 844 defer admin.deinit(io, allocator); 845 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .admin, .xit, evt.admin_repo_opts, io, allocator, &admin, evt.events_ref, &.{ 846 .{ 847 .id = std.fmt.bytesToHex(user_id, .lower), 848 .timestamp = 2, 849 .author = .{ .name = "admin", .email = "admin@example.test" }, 850 .event = .{ .user = .{ 851 .name = "admin", 852 .email = "admin@example.test", 853 .password_hash = "", 854 .ssh_keys = "", 855 } }, 856 }, 857 }); 858 } 859 860 const denied_url = try allocator.print("git@localhost:admin:target+{s}", .{&fork_id_hex}); 861 defer allocator.free(denied_url); 862 try client.addRemote(io, allocator, .{ .name = "denied", .value = denied_url }); 863 try std.testing.expectError(error.ServerReportedError, client.push(io, allocator, "denied", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = reject_ssh_cmd } } })); 864 // a fork is as readable as its private target, so a key nobody holds can't fetch it 865 try std.testing.expectError(error.ServerReportedError, client.fetch(io, allocator, "denied", .{ .wire = .{ .ssh = .{ .command = reject_ssh_cmd } } })); 866 867 // once the target is gone, only the forker's key may fetch the fork 868 { 869 var user_repo = (try evt.openUserRepo(io, allocator, users_dir, &user_id)) orelse return error.NotFound; 870 defer user_repo.deinit(io, allocator); 871 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" }); 872 } 873 try std.testing.expectError(error.ServerReportedError, client.fetch(io, allocator, "denied", .{ .wire = .{ .ssh = .{ .command = reject_ssh_cmd } } })); 874 } 875 876 fn testPushCreatesMissingRepo( 877 comptime repo_kind: rp.RepoKind, 878 comptime transport_def: net.TransportDefinition, 879 comptime hash_kind: hash.HashKind, 880 comptime port: u16, 881 io: std.Io, 882 allocator: std.mem.Allocator, 883 ) !void { 884 var temp = std.testing.tmpDir(.{}); 885 defer temp.cleanup(); 886 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 887 defer allocator.free(temp_path); 888 889 var server_process = try runServer(io, allocator, temp_path, port); 890 defer _ = server_process.kill(io); 891 892 const client_path = try std.Io.Dir.path.join(allocator, &.{ temp_path, "client" }); 893 defer allocator.free(client_path); 894 895 var client_repo = try rp.Repo(repo_kind, .{ .is_test = true, .hash = hash_kind }).init(io, allocator, .{ .path = client_path }); 896 defer client_repo.deinit(io, allocator); 897 898 const commit1 = blk: { 899 const hello_txt = try client_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 900 defer hello_txt.close(io); 901 try hello_txt.writeStreamingAll(io, "hello, world!"); 902 try client_repo.add(io, allocator, &.{"hello.txt"}); 903 break :blk try client_repo.commit(io, allocator, .{ .message = "let there be light" }); 904 }; 905 906 _ = try client_repo.addTag(io, allocator, .{ .name = "1.0.0", .message = "hi" }); 907 908 // the repo is not pre-registered; the push should mint the repo event under 909 // admin and create the repo on disk 910 { 911 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 912 defer allocator.free(remote_url); 913 914 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 915 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 916 } 917 918 const is_ssh = switch (transport_def) { 919 .file => false, 920 .wire => |wire_kind| .ssh == wire_kind, 921 }; 922 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 923 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 924 925 try client_repo.push( 926 io, 927 allocator, 928 "origin", 929 "master", 930 false, 931 .{ .refspecs = &.{"refs/tags/1.0.0:refs/tags/1.0.0"}, .wire = .{ .ssh = .{ 932 .command = ssh_cmd_maybe, 933 } } }, 934 ); 935 936 // the push registered admin:testrepo in the client's hash format; resolve its on-disk directory 937 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", false)).?; 938 defer allocator.free(server_path); 939 940 var server_repo = try rp.Repo(.xit, .{ .is_test = true, .hash = hash_kind }).open(io, allocator, .{ .path = server_path }); 941 defer server_repo.deinit(io, allocator); 942 try std.testing.expect(try server_repo.isBare(io, allocator)); 943 944 try std.testing.expect(null != try server_repo.readRef(io, .{ .kind = .tag, .name = "1.0.0" })); 945 946 const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 947 try std.testing.expectEqualStrings(&commit1, &oid_master); 948 } 949 950 fn testPushEvents( 951 comptime repo_kind: rp.RepoKind, 952 comptime transport_def: net.TransportDefinition, 953 comptime hash_kind: hash.HashKind, 954 comptime port: u16, 955 io: std.Io, 956 allocator: std.mem.Allocator, 957 ) !void { 958 // create the temp dir 959 var temp = std.testing.tmpDir(.{}); 960 defer temp.cleanup(); 961 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 962 defer allocator.free(temp_path); 963 964 // init server 965 var server_process = try runServer(io, allocator, temp_path, port); 966 defer _ = server_process.kill(io); 967 968 // register the repo under admin and locate its on-disk directory 969 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; 970 defer allocator.free(server_path); 971 972 const repo_opts: rp.RepoOpts(.xit) = .{ .hash = hash_kind, .is_test = true }; 973 const ServerRepo = rp.Repo(.xit, repo_opts); 974 var server_repo = try ServerRepo.init(io, allocator, .{ .bare = true, .path = server_path }); 975 defer server_repo.deinit(io, allocator); 976 977 const client_path = try std.Io.Dir.path.join(allocator, &.{ temp_path, "client" }); 978 defer allocator.free(client_path); 979 980 var client_repo = try rp.Repo(repo_kind, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .path = client_path }); 981 defer client_repo.deinit(io, allocator); 982 983 // add remote 984 { 985 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 986 defer allocator.free(remote_url); 987 988 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 989 } 990 991 const is_ssh = switch (transport_def) { 992 .file => false, 993 .wire => |wire_kind| .ssh == wire_kind, 994 }; 995 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 996 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 997 998 var arena = std.heap.ArenaAllocator.init(allocator); 999 defer arena.deinit(); 1000 1001 // 1002 // define test events 1003 // 1004 1005 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 1006 const issue_event_id = evt.EventWithId.randomId(prng.random()); 1007 1008 const events_to_push = [_]evt.EventWithId{ 1009 .{ 1010 .id = std.fmt.bytesToHex(issue_event_id, .lower), 1011 .author = .{ .name = "haxy", .email = "user@haxy" }, 1012 .event = .{ 1013 .issue = .{ 1014 .title = "Login form clears password on validation error", 1015 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 1016 .labels = "bug priority-high ui", 1017 }, 1018 }, 1019 }, 1020 }; 1021 1022 // commit the issue on the client and push it to the server 1023 try evt.consume(.local, .repo, repo_kind, .{ .hash = hash_kind, .is_test = true }, io, allocator, &client_repo, evt.events_ref, &events_to_push); 1024 try client_repo.push( 1025 io, 1026 allocator, 1027 "origin", 1028 "haxy/events", 1029 false, 1030 .{ .wire = .{ .ssh = .{ 1031 .command = ssh_cmd_maybe, 1032 } } }, 1033 ); 1034 1035 // the push consumed the issue into the server repo's db 1036 { 1037 const haxy_moment = try evt.currentMoment(repo_opts, &server_repo); 1038 const records_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.record_map_key)) orelse return error.NotFound; 1039 const records = try ServerRepo.DB.HashMap(.read_only).init(records_cursor); 1040 const issue_cursor = try records.getCursor(hash.hashInt(repo_opts.hash, &issue_event_id)) orelse return error.NotFound; 1041 const issue = try evt.read(evt.Issue.Record, ServerRepo.DB, repo_opts.hash, &arena, try ServerRepo.DB.HashMap(.read_only).init(issue_cursor)); 1042 1043 try std.testing.expectEqualStrings(events_to_push[0].event.issue.?.description, issue.event.description); 1044 try std.testing.expectEqualStrings(events_to_push[0].event.issue.?.labels, issue.event.labels); 1045 } 1046 1047 // 1048 // edit the issue 1049 // 1050 1051 // this event edits the previous one because it has the same id 1052 const events_to_push2 = [_]evt.EventWithId{ 1053 .{ 1054 .id = std.fmt.bytesToHex(issue_event_id, .lower), 1055 .author = .{ .name = "haxy", .email = "user@haxy" }, 1056 .event = .{ 1057 .issue = .{ 1058 .title = "Login form clears password on validation error", 1059 .description = "Submitting an invalid email address resets the password field and removes typed input. Preserve the field value and show an inline validation message.", 1060 .labels = "bug priority-low ui", 1061 }, 1062 }, 1063 }, 1064 }; 1065 1066 // commit the edit on the client and push it to the server 1067 try evt.consume(.local, .repo, repo_kind, .{ .hash = hash_kind, .is_test = true }, io, allocator, &client_repo, evt.events_ref, &events_to_push2); 1068 try client_repo.push( 1069 io, 1070 allocator, 1071 "origin", 1072 "haxy/events", 1073 false, 1074 .{ .wire = .{ .ssh = .{ 1075 .command = ssh_cmd_maybe, 1076 } } }, 1077 ); 1078 1079 // the push consumed the edit into the server repo's db 1080 { 1081 const haxy_moment = try evt.currentMoment(repo_opts, &server_repo); 1082 const records_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.record_map_key)) orelse return error.NotFound; 1083 const records = try ServerRepo.DB.HashMap(.read_only).init(records_cursor); 1084 const issue_cursor = try records.getCursor(hash.hashInt(repo_opts.hash, &issue_event_id)) orelse return error.NotFound; 1085 const issue = try evt.read(evt.Issue.Record, ServerRepo.DB, repo_opts.hash, &arena, try ServerRepo.DB.HashMap(.read_only).init(issue_cursor)); 1086 1087 // the description and labels were correctly edited 1088 try std.testing.expectEqualStrings(events_to_push2[0].event.issue.?.description, issue.event.description); 1089 try std.testing.expectEqualStrings(events_to_push2[0].event.issue.?.labels, issue.event.labels); 1090 } 1091 } 1092 1093 fn testClone( 1094 comptime repo_kind: rp.RepoKind, 1095 comptime transport_def: net.TransportDefinition, 1096 comptime shell_out_to_git: bool, 1097 comptime hash_kind: hash.HashKind, 1098 comptime port: u16, 1099 io: std.Io, 1100 allocator: std.mem.Allocator, 1101 ) !void { 1102 // create the temp dir 1103 var temp = std.testing.tmpDir(.{}); 1104 defer temp.cleanup(); 1105 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 1106 defer allocator.free(temp_path); 1107 1108 // init server 1109 var server_process = try runServer(io, allocator, temp_path, port); 1110 defer _ = server_process.kill(io); 1111 1112 // register the repo under admin and locate its on-disk directory 1113 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; 1114 defer allocator.free(server_path); 1115 1116 // init server repo with default branch name as main 1117 // is_test must be false when shell_out_to_git so commits get real timestamps (needed for --shallow-since) 1118 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" }); 1119 defer server_repo.deinit(io, allocator); 1120 1121 if (shell_out_to_git) { 1122 try server_repo.addConfig(io, allocator, .{ .name = "user.name", .value = "test" }); 1123 try server_repo.addConfig(io, allocator, .{ .name = "user.email", .value = "test@test" }); 1124 try server_repo.addConfig(io, allocator, .{ .name = "uploadpack.allowfilter", .value = "true" }); 1125 } 1126 1127 // make a commit 1128 _ = try commitServer(&server_repo, io, allocator, .{ .files = &.{.{ .path = "hello.txt", .content = "hello, world!" }} }, .{ .message = "let there be light" }); 1129 1130 // tag first commit 1131 _ = try server_repo.addTag(io, allocator, .{ .name = "v1", .message = "first" }); 1132 1133 // make a commit 1134 _ = try commitServer(&server_repo, io, allocator, .{ .files = &.{.{ .path = "goodbye.txt", .content = "goodbye, world!" }} }, .{ .message = "add goodbye file" }); 1135 1136 // export server repo 1137 { 1138 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1139 defer export_file.close(io); 1140 } 1141 1142 const client_path = try std.Io.Dir.path.join(allocator, &.{ temp_path, "client" }); 1143 defer allocator.free(client_path); 1144 1145 // get remote url 1146 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 1147 defer allocator.free(remote_url); 1148 1149 const is_ssh = switch (transport_def) { 1150 .file => false, 1151 .wire => |wire_kind| .ssh == wire_kind, 1152 }; 1153 1154 if (shell_out_to_git) { 1155 const ssh_cmd = (try sshCommand(true, allocator, temp_path, port)).?; 1156 defer allocator.free(ssh_cmd); 1157 const ssh_config_arg = try allocator.print("core.sshCommand={s}", .{ssh_cmd}); 1158 defer allocator.free(ssh_config_arg); 1159 1160 { 1161 var process = try std.process.spawn(io, .{ 1162 .argv = if (is_ssh) 1163 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--depth", "1", remote_url, "client" } 1164 else 1165 &.{ "git", "clone", "--quiet", "--depth", "1", remote_url, "client" }, 1166 .cwd = .{ .path = temp_path }, 1167 .stdin = .ignore, 1168 .stdout = .ignore, 1169 .stderr = .inherit, 1170 }); 1171 defer process.kill(io); 1172 const term = try process.wait(io); 1173 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1174 } 1175 1176 // make sure shallow clone was successful 1177 { 1178 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 1179 hello_txt.close(io); 1180 } 1181 1182 // make a third commit on the server 1183 _ = try commitServer(&server_repo, io, allocator, .{ .files = &.{.{ .path = "extra.txt", .content = "extra content" }} }, .{ .message = "add extra file" }); 1184 1185 // pull --unshallow to deepen the clone and get the new commit 1186 { 1187 var process = try std.process.spawn(io, .{ 1188 .argv = if (is_ssh) 1189 &.{ "git", "-c", ssh_config_arg, "pull", "--quiet", "--unshallow" } 1190 else 1191 &.{ "git", "pull", "--quiet", "--unshallow" }, 1192 .cwd = .{ .path = client_path }, 1193 .stdin = .ignore, 1194 .stdout = .ignore, 1195 .stderr = .inherit, 1196 }); 1197 defer process.kill(io); 1198 const term = try process.wait(io); 1199 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1200 } 1201 1202 // make sure unshallow pull was successful 1203 { 1204 const extra_txt = try temp.dir.openFile(io, "client/extra.txt", .{}); 1205 extra_txt.close(io); 1206 } 1207 1208 // delete client and clone again with --shallow-since 1209 try temp.dir.deleteTree(io, "client"); 1210 1211 { 1212 var process = try std.process.spawn(io, .{ 1213 .argv = if (is_ssh) 1214 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--shallow-since=2000-01-01", remote_url, "client" } 1215 else 1216 &.{ "git", "clone", "--quiet", "--shallow-since=2000-01-01", remote_url, "client" }, 1217 .cwd = .{ .path = temp_path }, 1218 .stdin = .ignore, 1219 .stdout = .ignore, 1220 .stderr = .inherit, 1221 }); 1222 defer process.kill(io); 1223 const term = try process.wait(io); 1224 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 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 temp.dir.deleteTree(io, "client"); 1235 1236 { 1237 var process = try std.process.spawn(io, .{ 1238 .argv = if (is_ssh) 1239 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--shallow-exclude=v1", remote_url, "client" } 1240 else 1241 &.{ "git", "clone", "--quiet", "--shallow-exclude=v1", remote_url, "client" }, 1242 .cwd = .{ .path = temp_path }, 1243 .stdin = .ignore, 1244 .stdout = .ignore, 1245 .stderr = .inherit, 1246 }); 1247 defer process.kill(io); 1248 const term = try process.wait(io); 1249 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1250 } 1251 1252 // make sure shallow clone was successful 1253 { 1254 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 1255 hello_txt.close(io); 1256 } 1257 1258 // delete client and clone again with --filter=blob:none 1259 try temp.dir.deleteTree(io, "client"); 1260 1261 { 1262 var process = try std.process.spawn(io, .{ 1263 .argv = if (is_ssh) 1264 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--filter=blob:none", remote_url, "client" } 1265 else 1266 &.{ "git", "clone", "--quiet", "--filter=blob:none", remote_url, "client" }, 1267 .cwd = .{ .path = temp_path }, 1268 .stdin = .ignore, 1269 .stdout = .ignore, 1270 .stderr = .inherit, 1271 }); 1272 defer process.kill(io); 1273 const term = try process.wait(io); 1274 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1275 } 1276 1277 // make sure partial clone was successful 1278 { 1279 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 1280 hello_txt.close(io); 1281 } 1282 1283 // delete client and clone again with --filter=tree:0 1284 try temp.dir.deleteTree(io, "client"); 1285 1286 { 1287 var process = try std.process.spawn(io, .{ 1288 .argv = if (is_ssh) 1289 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--filter=tree:0", remote_url, "client" } 1290 else 1291 &.{ "git", "clone", "--quiet", "--filter=tree:0", remote_url, "client" }, 1292 .cwd = .{ .path = temp_path }, 1293 .stdin = .ignore, 1294 .stdout = .ignore, 1295 .stderr = .inherit, 1296 }); 1297 defer process.kill(io); 1298 const term = try process.wait(io); 1299 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1300 } 1301 1302 // make sure treeless clone was successful 1303 { 1304 const goodbye_txt = try temp.dir.openFile(io, "client/goodbye.txt", .{}); 1305 goodbye_txt.close(io); 1306 } 1307 } else { 1308 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 1309 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1310 1311 // clone repo 1312 var clone_result = try rp.AnyRepo(repo_kind, .{ .is_test = true }).clone( 1313 io, 1314 allocator, 1315 remote_url, 1316 temp_path, 1317 client_path, 1318 null, 1319 .{ .transport = .{ .wire = .{ .ssh = .{ 1320 .command = ssh_cmd_maybe, 1321 } } } }, 1322 ); 1323 defer clone_result.deinit(io, allocator); 1324 const client_repo = &@field(clone_result, @tagName(hash_kind)); 1325 1326 // make sure HEAD points to the right default branch 1327 var current_branch_buffer: [rf.MAX_REF_CONTENT_SIZE]u8 = @splat(0); 1328 const head = try client_repo.head(io, &current_branch_buffer); 1329 try std.testing.expectEqualStrings("main", head.ref.name); 1330 1331 // make sure clone was successful 1332 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 1333 defer hello_txt.close(io); 1334 } 1335 } 1336 1337 fn testFetchLarge( 1338 comptime repo_kind: rp.RepoKind, 1339 comptime transport_def: net.TransportDefinition, 1340 comptime shell_out_to_git: bool, 1341 comptime hash_kind: hash.HashKind, 1342 comptime port: u16, 1343 io: std.Io, 1344 allocator: std.mem.Allocator, 1345 ) !void { 1346 // create the temp dir 1347 var temp = std.testing.tmpDir(.{}); 1348 defer temp.cleanup(); 1349 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 1350 defer allocator.free(temp_path); 1351 1352 // init server 1353 var server_process = try runServer(io, allocator, temp_path, port); 1354 defer _ = server_process.kill(io); 1355 1356 // register the repo under admin and locate its on-disk directory 1357 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; 1358 defer allocator.free(server_path); 1359 1360 var server_repo = try rp.Repo(.xit, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .bare = true, .path = server_path }); 1361 defer server_repo.deinit(io, allocator); 1362 1363 // make a commit with files from the current repo 1364 const commit1 = try commitServer(&server_repo, io, allocator, .{ .dirs = &.{"src"} }, .{ .message = "let there be light" }); 1365 1366 // export server repo 1367 { 1368 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1369 defer export_file.close(io); 1370 } 1371 1372 if (shell_out_to_git) { 1373 try server_repo.addConfig(io, allocator, .{ .name = "uploadpack.allowrefinwant", .value = "true" }); 1374 } 1375 1376 const client_path = try std.Io.Dir.path.join(allocator, &.{ temp_path, "client" }); 1377 defer allocator.free(client_path); 1378 1379 var client_repo = try rp.Repo(repo_kind, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .path = client_path }); 1380 defer client_repo.deinit(io, allocator); 1381 1382 // add remote 1383 { 1384 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 1385 defer allocator.free(remote_url); 1386 1387 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 1388 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 1389 } 1390 1391 const is_ssh = switch (transport_def) { 1392 .file => false, 1393 .wire => |wire_kind| .ssh == wire_kind, 1394 }; 1395 1396 if (shell_out_to_git) { 1397 const ssh_cmd = (try sshCommand(true, allocator, temp_path, port)).?; 1398 defer allocator.free(ssh_cmd); 1399 const ssh_config_arg = try allocator.print("core.sshCommand={s}", .{ssh_cmd}); 1400 defer allocator.free(ssh_config_arg); 1401 1402 { 1403 var process = try std.process.spawn(io, .{ 1404 .argv = if (is_ssh) 1405 &.{ "git", "-c", ssh_config_arg, "pull", "--quiet", "origin", "master" } 1406 else 1407 &.{ "git", "pull", "--quiet", "origin", "master" }, 1408 .cwd = .{ .path = client_path }, 1409 .stdin = .ignore, 1410 .stdout = .ignore, 1411 .stderr = .inherit, 1412 }); 1413 defer process.kill(io); 1414 const term = try process.wait(io); 1415 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1416 } 1417 1418 // make sure pull was successful 1419 { 1420 const oid_master = (try client_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1421 try std.testing.expectEqualStrings(&commit1, &oid_master); 1422 } 1423 1424 // make another commit on the server 1425 const commit2 = try commitServer(&server_repo, io, allocator, .{ .files = &.{.{ .path = "extra.txt", .content = "extra content" }} }, .{ .message = "add extra file" }); 1426 1427 // fetch with ref-in-want (git uses want-ref in protocol v2 when fetching named refs) 1428 { 1429 var process = try std.process.spawn(io, .{ 1430 .argv = if (is_ssh) 1431 &.{ "git", "-c", ssh_config_arg, "fetch", "--quiet", "origin", "master" } 1432 else 1433 &.{ "git", "fetch", "--quiet", "origin", "master" }, 1434 .cwd = .{ .path = client_path }, 1435 .stdin = .ignore, 1436 .stdout = .ignore, 1437 .stderr = .inherit, 1438 }); 1439 defer process.kill(io); 1440 const term = try process.wait(io); 1441 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1442 } 1443 1444 // make sure fetch with want-ref was successful 1445 { 1446 const oid_remote_master = (try client_repo.readRef(io, .{ .kind = .{ .remote = "origin" }, .name = "master" })).?; 1447 try std.testing.expectEqualStrings(&commit2, &oid_remote_master); 1448 } 1449 } else { 1450 const refspecs = &.{ 1451 "+refs/heads/master:refs/heads/master", 1452 }; 1453 1454 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 1455 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1456 1457 try client_repo.fetch( 1458 io, 1459 allocator, 1460 "origin", 1461 .{ .refspecs = refspecs, .wire = .{ .ssh = .{ 1462 .command = ssh_cmd_maybe, 1463 } } }, 1464 ); 1465 1466 // update the working dir 1467 try client_repo.restore(io, allocator, "."); 1468 } 1469 1470 // make sure fetch was successful 1471 { 1472 const oid_master = (try client_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1473 try std.testing.expectEqualStrings(&commit1, &oid_master); 1474 } 1475 } 1476 1477 fn testPushLarge( 1478 comptime repo_kind: rp.RepoKind, 1479 comptime transport_def: net.TransportDefinition, 1480 comptime shell_out_to_git: bool, 1481 comptime hash_kind: hash.HashKind, 1482 comptime port: u16, 1483 io: std.Io, 1484 allocator: std.mem.Allocator, 1485 ) !void { 1486 // create the temp dir 1487 var temp = std.testing.tmpDir(.{}); 1488 defer temp.cleanup(); 1489 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 1490 defer allocator.free(temp_path); 1491 1492 // init server 1493 var server_process = try runServer(io, allocator, temp_path, port); 1494 defer _ = server_process.kill(io); 1495 1496 // register the repo under admin and locate its on-disk directory 1497 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; 1498 defer allocator.free(server_path); 1499 1500 var server_repo = try rp.Repo(.xit, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .bare = true, .path = server_path }); 1501 defer server_repo.deinit(io, allocator); 1502 1503 // add config 1504 try server_repo.addConfig(io, allocator, .{ .name = "http.receivepack", .value = "true" }); 1505 1506 // export server repo 1507 { 1508 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1509 defer export_file.close(io); 1510 } 1511 1512 const client_path = try std.Io.Dir.path.join(allocator, &.{ temp_path, "client" }); 1513 defer allocator.free(client_path); 1514 1515 var client_repo = try rp.Repo(repo_kind, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .path = client_path }); 1516 defer client_repo.deinit(io, allocator); 1517 1518 var client_dir = try temp.dir.openDir(io, "client", .{}); 1519 defer client_dir.close(io); 1520 1521 { 1522 const hello_txt = try client_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 1523 defer hello_txt.close(io); 1524 try hello_txt.writeStreamingAll(io, "hello, world!"); 1525 try client_repo.add(io, allocator, &.{"hello.txt"}); 1526 } 1527 1528 // copy files from current repo into client dir 1529 const cwd = std.Io.Dir.cwd(); 1530 for (&[_][]const u8{"src"}) |dir_name| { 1531 var src_repo_dir = try cwd.openDir(io, dir_name, .{ .iterate = true }); 1532 defer src_repo_dir.close(io); 1533 1534 var dest_repo_dir = try client_dir.createDirPathOpen(io, dir_name, .{}); 1535 defer dest_repo_dir.close(io); 1536 1537 try copyDir(io, src_repo_dir, dest_repo_dir); 1538 1539 try client_repo.add(io, allocator, &.{dir_name}); 1540 } 1541 1542 _ = try client_repo.commit(io, allocator, .{ .message = "let there be light" }); 1543 1544 // change the files so git will send them as delta objects 1545 for (&[_][]const u8{"src"}) |dir_name| { 1546 var dest_repo_dir = try client_dir.createDirPathOpen(io, dir_name, .{ .open_options = .{ .iterate = true } }); 1547 defer dest_repo_dir.close(io); 1548 1549 { 1550 var iter = dest_repo_dir.iterate(); 1551 while (try iter.next(io)) |entry| { 1552 switch (entry.kind) { 1553 .file => { 1554 const file = try dest_repo_dir.openFile(io, entry.name, .{ .mode = .read_write }); 1555 defer file.close(io); 1556 var writer = file.writer(io, &.{}); 1557 try writer.interface.writeAll("EDIT"); 1558 }, 1559 else => {}, 1560 } 1561 } 1562 } 1563 1564 try client_repo.add(io, allocator, &.{dir_name}); 1565 } 1566 1567 const commit2 = try client_repo.commit(io, allocator, .{ .message = "more stuff" }); 1568 1569 // add remote 1570 { 1571 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 1572 defer allocator.free(remote_url); 1573 1574 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 1575 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 1576 } 1577 1578 const is_ssh = switch (transport_def) { 1579 .file => false, 1580 .wire => |wire_kind| .ssh == wire_kind, 1581 }; 1582 1583 if (shell_out_to_git) { 1584 const ssh_cmd = (try sshCommand(true, allocator, temp_path, port)).?; 1585 defer allocator.free(ssh_cmd); 1586 const ssh_config_arg = try allocator.print("core.sshCommand={s}", .{ssh_cmd}); 1587 defer allocator.free(ssh_config_arg); 1588 1589 // shell out to git so it will send delta objects 1590 var process = try std.process.spawn(io, .{ 1591 .argv = if (is_ssh) 1592 &.{ "git", "-c", ssh_config_arg, "push", "--quiet", "origin", "master" } 1593 else 1594 &.{ "git", "push", "--quiet", "origin", "master" }, 1595 .cwd = .{ .path = client_path }, 1596 .stdin = .ignore, 1597 .stdout = .ignore, 1598 .stderr = .inherit, 1599 }); 1600 defer process.kill(io); 1601 const term = try process.wait(io); 1602 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1603 } else { 1604 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 1605 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1606 1607 try client_repo.push( 1608 io, 1609 allocator, 1610 "origin", 1611 "master", 1612 false, 1613 .{ .wire = .{ .ssh = .{ 1614 .command = ssh_cmd_maybe, 1615 } } }, 1616 ); 1617 } 1618 1619 // make sure push was successful 1620 { 1621 const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1622 try std.testing.expectEqualStrings(&commit2, &oid_master); 1623 1624 try std.testing.expectError(error.FileNotFound, server_repo.core.work_dir.openFile(io, "hello.txt", .{})); 1625 } 1626 } 1627 1628 fn testPushClearUndo( 1629 comptime hash_kind: hash.HashKind, 1630 comptime port: u16, 1631 io: std.Io, 1632 allocator: std.mem.Allocator, 1633 ) !void { 1634 const repo_opts: rp.RepoOpts(.xit) = .{ .hash = hash_kind, .is_test = true }; 1635 1636 // create the temp dir 1637 var temp = std.testing.tmpDir(.{}); 1638 defer temp.cleanup(); 1639 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 1640 defer allocator.free(temp_path); 1641 1642 // init server 1643 var server_process = try runServer(io, allocator, temp_path, port); 1644 defer _ = server_process.kill(io); 1645 1646 // register the repo under admin and locate its on-disk directory 1647 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; 1648 defer allocator.free(server_path); 1649 { 1650 var server_repo = try rp.Repo(.xit, repo_opts).init(io, allocator, .{ .bare = true, .path = server_path }); 1651 defer server_repo.deinit(io, allocator); 1652 } 1653 1654 // a git client with one commit to push 1655 const client_path = try std.Io.Dir.path.join(allocator, &.{ temp_path, "client" }); 1656 defer allocator.free(client_path); 1657 const commit = blk: { 1658 var client_repo = try rp.Repo(.git, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .path = client_path }); 1659 defer client_repo.deinit(io, allocator); 1660 const hello_txt = try client_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 1661 defer hello_txt.close(io); 1662 try hello_txt.writeStreamingAll(io, "hello, world!"); 1663 try client_repo.add(io, allocator, &.{"hello.txt"}); 1664 const remote_url = try remoteUrl(.{ .wire = .ssh }, allocator, "testrepo", port); 1665 defer allocator.free(remote_url); 1666 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 1667 break :blk try client_repo.commit(io, allocator, .{ .message = "let there be light" }); 1668 }; 1669 1670 const ssh_cmd = (try sshCommand(true, allocator, temp_path, port)).?; 1671 defer allocator.free(ssh_cmd); 1672 const ssh_config_arg = try allocator.print("core.sshCommand={s}", .{ssh_cmd}); 1673 defer allocator.free(ssh_config_arg); 1674 1675 { 1676 const result = try gitPush(io, allocator, client_path, ssh_config_arg, &.{"master"}); 1677 defer allocator.free(result.stderr); 1678 try std.testing.expect(result.ok); 1679 } 1680 const pushed_count = try historyCount(repo_opts, io, allocator, server_path); 1681 try std.testing.expect(pushed_count > 1); 1682 1683 // the clear ref can only be deleted 1684 { 1685 const result = try gitPush(io, allocator, client_path, ssh_config_arg, &.{"master:refs/haxy/undo"}); 1686 defer allocator.free(result.stderr); 1687 try std.testing.expect(!result.ok); 1688 try std.testing.expect(std.mem.indexOf(u8, result.stderr, "can only be deleted") != null); 1689 } 1690 1691 // and only alone, refusing the whole push 1692 { 1693 const result = try gitPush(io, allocator, client_path, ssh_config_arg, &.{ "master:refs/heads/other", ":refs/haxy/undo" }); 1694 defer allocator.free(result.stderr); 1695 try std.testing.expect(!result.ok); 1696 try std.testing.expect(std.mem.indexOf(u8, result.stderr, "must be deleted alone") != null); 1697 } 1698 1699 // a refused push leaves no transaction behind 1700 try std.testing.expectEqual(pushed_count, try historyCount(repo_opts, io, allocator, server_path)); 1701 1702 { 1703 const result = try gitPush(io, allocator, client_path, ssh_config_arg, &.{":refs/haxy/undo"}); 1704 defer allocator.free(result.stderr); 1705 try std.testing.expect(result.ok); 1706 try std.testing.expect(std.mem.indexOf(u8, result.stderr, "Compacting database") != null); 1707 } 1708 1709 // one transaction is left, and it still holds the pushed branch 1710 try std.testing.expectEqual(1, try historyCount(repo_opts, io, allocator, server_path)); 1711 { 1712 var server_repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = server_path }); 1713 defer server_repo.deinit(io, allocator); 1714 try std.testing.expectEqualStrings(&commit, &(try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?); 1715 try std.testing.expectEqual(null, try server_repo.readRef(io, .{ .kind = .head, .name = "other" })); 1716 } 1717 } 1718 1719 // whether a git push from the client succeeded, and what it printed. progress 1720 // is forced on, since git hides it when stderr isn't a terminal. 1721 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 } { 1722 var argv: std.ArrayList([]const u8) = .empty; 1723 defer argv.deinit(allocator); 1724 try argv.appendSlice(allocator, &.{ "git", "-c", ssh_config_arg, "push", "--progress", "origin" }); 1725 try argv.appendSlice(allocator, refspecs); 1726 const result = try std.process.run(allocator, io, .{ .argv = argv.items, .cwd = .{ .path = client_path } }); 1727 allocator.free(result.stdout); 1728 return .{ .ok = result.term == .exited and result.term.exited == 0, .stderr = result.stderr }; 1729 } 1730 1731 // the number of transactions in a repo's history. opened afresh each time, 1732 // since gc replaces the database file. 1733 fn historyCount(comptime repo_opts: rp.RepoOpts(.xit), io: std.Io, allocator: std.mem.Allocator, path: []const u8) !u64 { 1734 var repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = path }); 1735 defer repo.deinit(io, allocator); 1736 const history = try rp.Repo(.xit, repo_opts).DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 1737 return try history.count(); 1738 } 1739 1740 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 1741 var iter = src_dir.iterate(); 1742 while (try iter.next(io)) |entry| { 1743 switch (entry.kind) { 1744 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 1745 .directory => { 1746 try dest_dir.createDirPath(io, entry.name); 1747 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1748 defer dest_entry_dir.close(io); 1749 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1750 defer src_entry_dir.close(io); 1751 try copyDir(io, src_entry_dir, dest_entry_dir); 1752 }, 1753 else => {}, 1754 } 1755 } 1756 } 1757 1758 // the dev SSH public key matching the private key written by runServer 1759 const admin_ssh_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKeIs8mJqigBZ5y84J4COgnAJJ5bHPKy+lM2SliMXbYm radar@roark"; 1760 1761 // create the admin event repo with a user holding the dev SSH public key, plus 1762 // that user's repo 1763 fn setupAdmin(io: std.Io, allocator: std.mem.Allocator, data_path: []const u8) !void { 1764 const admin_repo_path = try std.Io.Dir.path.join(allocator, &.{ data_path, "admin" }); 1765 defer allocator.free(admin_repo_path); 1766 1767 var repo = try rp.Repo(.xit, evt.admin_repo_opts).init(io, allocator, .{ .bare = true, .path = admin_repo_path }); 1768 defer repo.deinit(io, allocator); 1769 1770 try repo.addConfig(io, allocator, .{ .name = "user.name", .value = "haxy" }); 1771 try repo.addConfig(io, allocator, .{ .name = "user.email", .value = "admin@haxy" }); 1772 1773 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 1774 const user_id = evt.EventWithId.randomId(prng.random()); 1775 1776 var password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 1777 const password_hash = try evt.User.hashPassword("password", &password_hash_buf, io); 1778 1779 const users_dir = try std.Io.Dir.path.join(allocator, &.{ data_path, "users" }); 1780 defer allocator.free(users_dir); 1781 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .admin, .xit, evt.admin_repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{.{ 1782 .id = std.fmt.bytesToHex(user_id, .lower), 1783 .author = .{ .name = "admin", .email = "admin@example.test" }, 1784 .event = .{ .user = .{ 1785 .name = "admin", 1786 .email = "admin@example.test", 1787 .password_hash = password_hash, 1788 .ssh_keys = admin_ssh_key, 1789 } }, 1790 }}); 1791 1792 const user_repo_path = try evt.userRepoPath(allocator, users_dir, &user_id); 1793 defer allocator.free(user_repo_path); 1794 var user_repo = try evt.initUserRepo(io, allocator, user_repo_path); 1795 defer user_repo.deinit(io, allocator); 1796 } 1797 1798 // resolve admin:<repo_name> to its on-disk directory under <data_dir>/users via 1799 // the event store 1800 fn repoOnDiskPath( 1801 io: std.Io, 1802 allocator: std.mem.Allocator, 1803 data_path: []const u8, 1804 repo_name: []const u8, 1805 create: bool, 1806 ) !?[]u8 { 1807 const admin_repo_path = try std.Io.Dir.path.join(allocator, &.{ data_path, "admin" }); 1808 defer allocator.free(admin_repo_path); 1809 const users_dir = try std.Io.Dir.path.join(allocator, &.{ data_path, "users" }); 1810 defer allocator.free(users_dir); 1811 1812 const location = (try evt.resolveOrCreateRepo( 1813 io, 1814 allocator, 1815 users_dir, 1816 admin_repo_path, 1817 "admin", 1818 repo_name, 1819 if (create) .{ .read_access = .public } else null, 1820 )) orelse return null; 1821 return try evt.repoPath(allocator, users_dir, &location.owner_id, &location.repo_id); 1822 } 1823 1824 // build the remote URL addressing admin:<repo_name> over the given transport 1825 fn remoteUrl( 1826 comptime transport_def: net.TransportDefinition, 1827 allocator: std.mem.Allocator, 1828 repo_name: []const u8, 1829 port: u16, 1830 ) ![]u8 { 1831 return switch (transport_def) { 1832 .file => unreachable, 1833 .wire => |wire_kind| switch (wire_kind) { 1834 .http => try allocator.print("http://localhost:{}/admin:{s}", .{ port, repo_name }), 1835 .raw => try allocator.print("git://localhost:{}/admin:{s}", .{ port, repo_name }), 1836 .ssh => try allocator.print("git@localhost:admin:{s}", .{repo_name}), 1837 }, 1838 }; 1839 } 1840 1841 // the ssh command the haxy git client should invoke, or null when not using 1842 // ssh. haxy generates a fresh host key on every run, so accept the unknown host 1843 // and skip writing to known_hosts. 1844 fn sshCommand( 1845 is_ssh: bool, 1846 allocator: std.mem.Allocator, 1847 temp_path: []const u8, 1848 port: u16, 1849 ) !?[]u8 { 1850 if (!is_ssh) return null; 1851 1852 const priv_key_path = try std.Io.Dir.path.join(allocator, &.{ temp_path, "key" }); 1853 defer allocator.free(priv_key_path); 1854 1855 if (builtin.target.os.tag == .windows) std.mem.replaceScalar(u8, priv_key_path, '\\', '/'); 1856 const null_path = if (builtin.target.os.tag == .windows) "NUL" else "/dev/null"; 1857 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 }); 1858 } 1859 1860 const ServerFiles = union(enum) { 1861 files: []const struct { path: []const u8, content: []const u8 }, 1862 dirs: []const []const u8, 1863 }; 1864 1865 /// commit files without writing the server's index or worktree. 1866 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 { 1867 const R = @TypeOf(repo.*); 1868 const opts = repo.self_repo_opts; 1869 const Index = xit.index.Index(.xit, opts); 1870 var result: [xit.hash.hexLen(opts.hash)]u8 = undefined; 1871 const Ctx = struct { 1872 core: *R.Core, 1873 io: std.Io, 1874 allocator: std.mem.Allocator, 1875 files: ServerFiles, 1876 metadata: xit.object.CommitMetadata(opts.hash), 1877 result: *[xit.hash.hexLen(opts.hash)]u8, 1878 1879 fn addFile(ctx: @This(), state: R.State(.read_write), index: *Index, path: []const u8, content: []const u8) !void { 1880 var reader = std.Io.Reader.fixed(content); 1881 var oid: [xit.hash.byteLen(opts.hash)]u8 = undefined; 1882 try xit.object.writeObject(.xit, opts, state, ctx.io, ctx.allocator, &reader, .{ .kind = .blob, .size = content.len }, &oid); 1883 const path_parts = try xit.fs.splitPath(ctx.allocator, path); 1884 defer ctx.allocator.free(path_parts); 1885 try index.addTreeEntryFile(&.{ .oid = oid, .mode = @bitCast(@as(u32, 0o100644)) }, path_parts, content.len, 0); 1886 } 1887 1888 fn addDir(ctx: @This(), state: R.State(.read_write), index: *Index, path: []const u8) anyerror!void { 1889 var dir = try std.Io.Dir.cwd().openDir(ctx.io, path, .{ .iterate = true }); 1890 defer dir.close(ctx.io); 1891 var entries = dir.iterate(); 1892 while (try entries.next(ctx.io)) |entry| { 1893 const child = try std.Io.Dir.path.join(ctx.allocator, &.{ path, entry.name }); 1894 defer ctx.allocator.free(child); 1895 switch (entry.kind) { 1896 .directory => try ctx.addDir(state, index, child), 1897 .file => { 1898 const content = try dir.readFileAlloc(ctx.io, entry.name, ctx.allocator, .unlimited); 1899 defer ctx.allocator.free(content); 1900 try ctx.addFile(state, index, child, content); 1901 }, 1902 else => {}, 1903 } 1904 } 1905 } 1906 1907 pub fn run(ctx: @This(), cursor: *R.DB.Cursor(.read_write)) !void { 1908 var moment = try R.DB.HashMap(.read_write).init(cursor.*); 1909 const state = R.State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 1910 const head_oid = try rf.readHeadRecurMaybe(.xit, opts, state.readOnly(), ctx.io); 1911 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); 1912 defer index.deinit(); 1913 switch (ctx.files) { 1914 .files => |updates| for (updates) |file| try ctx.addFile(state, &index, file.path, file.content), 1915 .dirs => |dirs| for (dirs) |dir| try ctx.addDir(state, &index, dir), 1916 } 1917 var tree = try xit.object.Tree.initFromIndex(.xit, opts, state, ctx.io, ctx.allocator, &index); 1918 defer tree.deinit(); 1919 var head_buffer: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; 1920 const head = try rf.readHead(.xit, opts, state.readOnly(), ctx.io, &head_buffer) orelse return error.HeadNotFound; 1921 ctx.result.* = try xit.object.writeCommit(.xit, opts, state, ctx.io, ctx.allocator, ctx.metadata, &tree, head.ref); 1922 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) }); 1923 } 1924 }; 1925 try repo.core.db_file.lock(io, .exclusive); 1926 defer repo.core.db_file.unlock(io); 1927 const history = try R.DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); 1928 try history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ 1929 .core = &repo.core, 1930 .io = io, 1931 .allocator = allocator, 1932 .files = files, 1933 .metadata = metadata, 1934 .result = &result, 1935 }); 1936 return result; 1937 } 1938