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