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