╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯
1 //! start a server with some fake data and then launch 2 //! the TUI. this provides a nice way to test things 3 //! out safely. 4 5 const std = @import("std"); 6 const builtin = @import("builtin"); 7 const hx = @import("haxy"); 8 const srv = hx.serve; 9 const evt = hx.event; 10 const xit = hx.xit; 11 const rp = xit.repo; 12 const hash = xit.hash; 13 const obj = xit.object; 14 const rf = xit.ref; 15 const bch = xit.branch; 16 const ui = hx.ui; 17 const find = hx.find; 18 const srch_cmmt = hx.search_commit; 19 const fork = hx.fork; 20 const pch = hx.pch; 21 const push = hx.push; 22 23 // cook the terminal before a panic/segfault trace is printed, so the trace 24 // isn't mangled by raw mode and the alternate buffer 25 pub const std_options_debug_io = xit.xitui.terminal.crash_debug_io; 26 27 pub fn main(init: std.process.Init) !void { 28 const allocator = std.heap.smp_allocator; 29 30 var threaded = std.Io.Threaded.init(allocator, .{ 31 .environ = init.minimal.environ, 32 }); 33 defer threaded.deinit(); 34 const io = threaded.io(); 35 const now: u64 = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()); 36 37 const temp_dir_name = "temp-try"; 38 39 // create the temp dir 40 const cwd = std.Io.Dir.cwd(); 41 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 42 if (temp_dir_or_err) |*temp_dir| { 43 temp_dir.close(io); 44 try cwd.deleteTree(io, temp_dir_name); 45 } else |_| {} 46 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 47 defer cwd.deleteTree(io, temp_dir_name) catch {}; 48 defer temp_dir.close(io); 49 50 // write the dev SSH private key so pushes can authenticate against the 51 // matching public key seeded on the admin account below 52 { 53 const priv_key_file = try temp_dir.createFile(io, "key", .{}); 54 defer priv_key_file.close(io); 55 try priv_key_file.writeStreamingAll(io, 56 \\-----BEGIN OPENSSH PRIVATE KEY----- 57 \\b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 58 \\QyNTUxOQAAACCniLPJiaooAWecvOCeAjoJwCSeWxzysvpTNkpYjF22JgAAAJA+7hikPu4Y 59 \\pAAAAAtzc2gtZWQyNTUxOQAAACCniLPJiaooAWecvOCeAjoJwCSeWxzysvpTNkpYjF22Jg 60 \\AAAEDVlopOMnKt/7by/IA8VZvQXUS/O6VLkixOqnnahUdPCKeIs8mJqigBZ5y84J4COgnA 61 \\JJ5bHPKy+lM2SliMXbYmAAAAC3JhZGFyQHJvYXJrAQI= 62 \\-----END OPENSSH PRIVATE KEY----- 63 \\ 64 ); 65 if (.windows != builtin.target.os.tag) { 66 try priv_key_file.setPermissions(io, @fromBackingInt(@intCast(0o600))); 67 } 68 } 69 70 const cwd_path = try std.process.currentPathAlloc(io, allocator); 71 defer allocator.free(cwd_path); 72 const key_path = try std.Io.Dir.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 73 defer allocator.free(key_path); 74 75 var cli = false; 76 var serve_args: std.ArrayList([]const u8) = .empty; 77 defer serve_args.deinit(allocator); 78 var arg_it = try init.minimal.args.iterateAllocator(allocator); 79 defer arg_it.deinit(); 80 _ = arg_it.skip(); 81 while (arg_it.next()) |arg| { 82 if (std.mem.eql(u8, "--cli", arg)) { 83 cli = true; 84 } else { 85 try serve_args.append(allocator, arg); 86 } 87 } 88 var serve_options = try hx.command.parseServeOptions(allocator, serve_args.items); 89 const git_ssh_prefix = if (builtin.mode == .debug) 90 try allocator.print("GIT_SSH_COMMAND='ssh -i \"{s}\" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR' \\\r\n", .{key_path}) 91 else 92 ""; 93 defer if (git_ssh_prefix.len != 0) allocator.free(git_ssh_prefix); 94 serve_options.git_ssh_prefix = git_ssh_prefix; 95 serve_options.fallback_on_address_in_use = true; 96 97 const server_path = if (std.mem.eql(u8, serve_options.data_dir, ".")) 98 try std.Io.Dir.path.join(allocator, &.{ cwd_path, temp_dir_name, "server" }) 99 else 100 try std.Io.Dir.path.resolveAlloc(allocator, &.{ cwd_path, serve_options.data_dir }); 101 defer allocator.free(server_path); 102 serve_options.data_dir = server_path; 103 104 const work_path = try std.Io.Dir.path.join(allocator, &.{ server_path, "admin" }); 105 defer allocator.free(work_path); 106 107 const Repo = rp.Repo(.xit, evt.admin_repo_opts); 108 var repo = try evt.initAdminRepo(io, allocator, work_path); 109 defer repo.deinit(io, allocator); 110 111 var session_arena = std.heap.ArenaAllocator.init(allocator); 112 defer session_arena.deinit(); 113 114 // the seeded admin's event id, so both uis start logged in as them 115 var admin_user_id: [evt.event_id_size]u8 = undefined; 116 117 var session: ui.Session = blk: { 118 var arena = std.heap.ArenaAllocator.init(allocator); 119 defer arena.deinit(); 120 121 // define test events 122 123 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 124 125 const user_data = [_]struct { 126 name: []const u8, 127 email: []const u8, 128 }{ 129 .{ .name = "admin", .email = "admin@example.test" }, 130 .{ .name = "alice", .email = "alice@example.test" }, 131 .{ .name = "bobby", .email = "bobby@example.test" }, 132 .{ .name = "carol", .email = "carol@example.test" }, 133 .{ .name = "david", .email = "david@example.test" }, 134 .{ .name = "evelyn", .email = "evelyn@example.test" }, 135 .{ .name = "frank", .email = "frank@example.test" }, 136 .{ .name = "grace", .email = "grace@example.test" }, 137 .{ .name = "henry", .email = "henry@example.test" }, 138 .{ .name = "irene", .email = "irene@example.test" }, 139 .{ .name = "jacob", .email = "jacob@example.test" }, 140 .{ .name = "katie", .email = "katie@example.test" }, 141 .{ .name = "logan", .email = "logan@example.test" }, 142 .{ .name = "molly", .email = "molly@example.test" }, 143 .{ .name = "nolan", .email = "nolan@example.test" }, 144 .{ .name = "olivia", .email = "olivia@example.test" }, 145 .{ .name = "peter", .email = "peter@example.test" }, 146 .{ .name = "quinn", .email = "quinn@example.test" }, 147 .{ .name = "rachel", .email = "rachel@example.test" }, 148 .{ .name = "simon", .email = "simon@example.test" }, 149 .{ .name = "tessa", .email = "tessa@example.test" }, 150 }; 151 152 const repo_data = [_]struct { 153 user_index: usize, 154 name: []const u8, 155 description: []const u8, 156 read_access: evt.Repo.Access = .public, 157 }{ 158 .{ .user_index = 1, .name = "ziglings", .description = "Learn the Zig programming language by fixing tiny broken programs" }, 159 .{ .user_index = 2, .name = "linux", .description = "Linux kernel source tree" }, 160 .{ .user_index = 3, .name = "kubernetes", .description = "Production-grade container orchestration" }, 161 .{ .user_index = 4, .name = "react", .description = "A declarative, efficient, and flexible JavaScript library for building user interfaces" }, 162 .{ .user_index = 5, .name = "typescript", .description = "TypeScript is a superset of JavaScript that compiles to clean JavaScript output" }, 163 .{ .user_index = 6, .name = "rust", .description = "Empowering everyone to build reliable and efficient software" }, 164 .{ .user_index = 7, .name = "go", .description = "The Go programming language" }, 165 .{ .user_index = 8, .name = "nodejs", .description = "Node.js JavaScript runtime" }, 166 .{ .user_index = 9, .name = "cpython", .description = "The Python programming language" }, 167 .{ .user_index = 10, .name = "docker", .description = "Container platform for developing, shipping, and running applications" }, 168 .{ .user_index = 1, .name = "vim", .description = "The ubiquitous text editor" }, 169 // private, so only bobby and admin can see it 170 .{ .user_index = 2, .name = "neovim", .description = "Hyperextensible Vim-based text editor", .read_access = .private }, 171 // private and owned by admin 172 .{ .user_index = 0, .name = "dotfiles", .description = "Shell, editor, and terminal configuration", .read_access = .private }, 173 .{ .user_index = 3, .name = "emacs", .description = "GNU Emacs source code mirror" }, 174 .{ .user_index = 4, .name = "tmux", .description = "Terminal multiplexer" }, 175 .{ .user_index = 5, .name = "zsh", .description = "Mirror of the Z shell source code repository" }, 176 .{ .user_index = 6, .name = "git", .description = "Distributed version control system" }, 177 .{ .user_index = 7, .name = "mercurial", .description = "Source-control management tool" }, 178 .{ .user_index = 8, .name = "tensorflow", .description = "An end-to-end open source machine learning platform" }, 179 .{ .user_index = 9, .name = "pytorch", .description = "Tensors and dynamic neural networks in Python with strong GPU acceleration" }, 180 .{ .user_index = 10, .name = "numpy", .description = "The fundamental package for scientific computing with Python" }, 181 .{ .user_index = 1, .name = "pandas", .description = "Flexible and powerful data analysis and manipulation library for Python" }, 182 .{ .user_index = 2, .name = "scikit-learn", .description = "Machine learning in Python" }, 183 .{ .user_index = 3, .name = "nginx", .description = "High performance HTTP server and reverse proxy" }, 184 .{ .user_index = 4, .name = "redis", .description = "In-memory data structure store, used as a database, cache, and message broker" }, 185 .{ .user_index = 5, .name = "postgres", .description = "The world's most advanced open source relational database" }, 186 .{ .user_index = 6, .name = "sqlite", .description = "Self-contained, serverless, zero-configuration SQL database engine" }, 187 .{ .user_index = 7, .name = "mongodb", .description = "The MongoDB Database" }, 188 .{ .user_index = 8, .name = "elasticsearch", .description = "Free and open, distributed, RESTful search engine" }, 189 .{ .user_index = 9, .name = "kafka", .description = "Distributed event streaming platform" }, 190 .{ .user_index = 10, .name = "terraform", .description = "Infrastructure as code tool" }, 191 .{ .user_index = 11, .name = "svelte", .description = "Cybernetically enhanced web apps" }, 192 .{ .user_index = 12, .name = "vue", .description = "The progressive JavaScript framework" }, 193 .{ .user_index = 13, .name = "flask", .description = "The Python micro framework for building web applications" }, 194 .{ .user_index = 14, .name = "django", .description = "The web framework for perfectionists with deadlines" }, 195 .{ .user_index = 15, .name = "rails", .description = "Ruby on Rails web framework" }, 196 .{ .user_index = 16, .name = "phoenix", .description = "Peace of mind from prototype to production for Elixir web apps" }, 197 .{ .user_index = 17, .name = "laravel", .description = "The PHP framework for web artisans" }, 198 .{ .user_index = 18, .name = "prometheus", .description = "The Prometheus monitoring system and time series database" }, 199 .{ .user_index = 19, .name = "grafana", .description = "The open and composable observability and data visualization platform" }, 200 .{ .user_index = 20, .name = "ansible", .description = "Simple, agentless IT automation" }, 201 // public and owned by admin. the last repo is the one given patches 202 .{ .user_index = 0, .name = "toolbox", .description = "Command line tools for inspecting and building workspaces" }, 203 }; 204 205 var user_ids: [user_data.len][evt.event_id_size]u8 = undefined; 206 for (&user_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 207 // admin is the first entry of user_data above 208 admin_user_id = user_ids[0]; 209 210 var repo_event_ids: [repo_data.len][evt.event_id_size]u8 = undefined; 211 for (&repo_event_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 212 213 var password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 214 const password_hash = try evt.User.hashPassword("password", &password_hash_buf, io); 215 216 // public key matching temp-try/key, given to admin so we can push as admin 217 const admin_ssh_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKeIs8mJqigBZ5y84J4COgnAJJ5bHPKy+lM2SliMXbYm radar@roark"; 218 219 var events_to_consume: [user_data.len]evt.EventWithId = undefined; 220 for (user_data, 0..) |u, i| { 221 events_to_consume[i] = .{ 222 .id = std.fmt.bytesToHex(user_ids[i], .lower), 223 // spread the accounts over the months before the repo activity 224 .timestamp = seedTimestamp(now, i, user_data.len, 120 * std.time.s_per_day, 100 * std.time.s_per_day), 225 .author = .{ .name = "admin", .email = "admin@example.test" }, 226 .event = .{ 227 .user = .{ 228 .name = u.name, 229 .email = u.email, 230 .password_hash = password_hash, 231 .ssh_keys = if (std.mem.eql(u8, "admin", u.name)) admin_ssh_key else "", 232 }, 233 }, 234 }; 235 } 236 // commit the seed events and consume them into the database 237 const users_dir = try std.Io.Dir.path.join(arena.allocator(), &.{ server_path, "users" }); 238 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .admin, .xit, evt.admin_repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume); 239 240 // admin is granted ownership of every repo, so it has full privileges 241 // everywhere (including undo) while the other users only own their own 242 var repo_events: [repo_data.len]evt.EventWithId = undefined; 243 var grant_events: [repo_data.len]evt.EventWithId = undefined; 244 for (repo_data, 0..) |r, i| { 245 const author: evt.CommitAuthor = .{ .name = user_data[r.user_index].name, .email = user_data[r.user_index].email }; 246 repo_events[i] = .{ 247 .id = std.fmt.bytesToHex(repo_event_ids[i], .lower), 248 .timestamp = seedTimestamp(now, i, repo_data.len, 90 * std.time.s_per_day, 70 * std.time.s_per_day), 249 .author = author, 250 .event = .{ 251 .repo = .{ 252 .user_id = &user_ids[r.user_index], 253 .name = r.name, 254 .description = r.description, 255 .read_access = r.read_access, 256 .discuss_role = .read, 257 .issue_role = .read, 258 .patch_role = .read, 259 }, 260 }, 261 }; 262 grant_events[i] = .{ 263 .id = std.fmt.bytesToHex(evt.Grant.idOf(&repo_event_ids[i], &admin_user_id), .lower), 264 .timestamp = seedTimestamp(now, i, repo_data.len, 90 * std.time.s_per_day, 70 * std.time.s_per_day), 265 .author = author, 266 .event = .{ .grant = .{ .target_id = &repo_event_ids[i], .user_id = &admin_user_id, .role = .owner } }, 267 }; 268 } 269 270 // each user's repo holds the repos they own and the grants on them 271 for (user_ids, 0..) |user_id, user_index| { 272 const user_repo_path = try evt.userRepoPath(arena.allocator(), users_dir, &user_id); 273 var user_repo = try evt.initUserRepo(io, allocator, user_repo_path); 274 defer user_repo.deinit(io, allocator); 275 var owned: std.ArrayList(evt.EventWithId) = .empty; 276 for (repo_data, repo_events, grant_events) |r, repo_event, grant_event| { 277 if (r.user_index != user_index) continue; 278 try owned.append(arena.allocator(), repo_event); 279 // admin already owns its own repos 280 if (r.user_index != 0) try owned.append(arena.allocator(), grant_event); 281 } 282 if (owned.items.len > 0) try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .user, .xit, evt.user_repo_opts, io, allocator, &user_repo, evt.events_ref, owned.items); 283 } 284 285 // the creation timestamps order the repos tabs 286 for (repo_data, &repo_event_ids, repo_events) |r, *id_bytes, event| { 287 try evt.bumpRepoActivity(io, allocator, users_dir, &user_ids[r.user_index], id_bytes, event.timestamp); 288 } 289 290 // every repo gets the same generated history, so build it once into a 291 // template repo and copy that to each repo's location below rather than 292 // redoing the expensive commit work for every repo. 293 const template_path = try std.Io.Dir.path.join(arena.allocator(), &.{ cwd_path, temp_dir_name, "template" }); 294 { 295 var template_repo = try rp.Repo(.xit, .{}).init(io, allocator, .{ .path = template_path }); 296 defer template_repo.deinit(io, allocator); 297 298 try template_repo.setMergeAlgorithm(io, allocator, .patch); 299 try template_repo.addConfig(io, allocator, .{ .name = "user.name", .value = "haxy" }); 300 try template_repo.addConfig(io, allocator, .{ .name = "user.email", .value = "admin@example.test" }); 301 302 // a README plus a nested doc so the file tree has a directory to 303 // descend into 304 { 305 var repo_dir = try cwd.openDir(io, template_path, .{}); 306 defer repo_dir.close(io); 307 308 const readme = try repo_dir.createFile(io, "README.md", .{}); 309 defer readme.close(io); 310 try readme.writeStreamingAll(io, 311 \\# Sample Repo 312 \\ 313 \\A repository seeded with test data for exercising the UI. Text can be 314 \\*italic*, **bold**, ***both*** or ~~struck through~~, and links go to 315 \\[the contributing guide](docs/dev/contribute.md) or to 316 \\[an external site](https://github.com/xit-vcs/haxy). 317 \\ 318 \\## Features 319 \\ 320 \\- Fast and lightweight 321 \\- Zero configuration required 322 \\ - Sensible defaults 323 \\ - Nothing to edit 324 \\- Works out of the box 325 \\ 326 \\## Roadmap 327 \\ 328 \\- [x] Render markdown 329 \\- [ ] Render tables 330 \\ 331 \\> Simplicity is prerequisite for reliability. A quote this long wraps 332 \\> onto more than one line in a narrow pane. 333 \\ 334 \\--- 335 \\ 336 \\## Installation 337 \\ 338 \\Clone the repository and build it from source: 339 \\ 340 \\```sh 341 \\git clone https://example.test/sample-repo.git 342 \\cd sample-repo 343 \\make install 344 \\``` 345 \\ 346 \\## Usage 347 \\ 348 \\Run the program with the `--help` flag to see all available 349 \\options: 350 \\ 351 \\```sh 352 \\sample-repo --help 353 \\``` 354 \\ 355 \\| Option | Description | 356 \\| ----------- | ------------------------------ | 357 \\| `--verbose` | Print extra diagnostic output | 358 \\| `--quiet` | Suppress all non-error output | 359 \\| `--version` | Print the version and exit | 360 \\ 361 \\## Contributing 362 \\ 363 \\Contributions are welcome! Please open an issue to discuss 364 \\any significant changes before submitting a pull request. 365 \\See the docs under `docs/dev` for more details. 366 \\ 367 \\## A Heading Too Long To Fit On One Line In The Subtitle Font 368 \\ 369 \\Long headings wrap onto more lines in their font. 370 \\ 371 \\## License 372 \\ 373 \\Released under the MIT License. See `LICENSE` for the full 374 \\text. 375 \\ 376 ); 377 378 try repo_dir.createDirPath(io, "docs/dev"); 379 const doc = try repo_dir.createFile(io, "docs/dev/contribute.md", .{}); 380 defer doc.close(io); 381 try doc.writeStreamingAll(io, "To contribute, please make a pull request"); 382 } 383 384 try template_repo.add(io, allocator, &.{ "README.md", "docs/dev/contribute.md" }); 385 _ = try template_repo.commit(io, allocator, .{ .message = "let there be light", .timestamp = now - 61 * std.time.s_per_day }); 386 387 // tag every commit in creation order, three versions at a time 388 var tag_num: usize = 1; 389 try addNextTags(&template_repo, io, allocator, &tag_num); 390 391 // a batch of commits so the commits tab has more than one page to 392 // paginate through. each rewrites a few files with scattered line 393 // edits, so every commit is a multi-file diff with several separate 394 // hunks to look at. stepped timestamps vary the date column. 395 const base_ts = now - 60 * std.time.s_per_day; 396 const edit_files = [_][]const u8{ "src/alpha.txt", "src/beta.txt", "src/gamma.txt" }; 397 // commit subjects, cycled then padded to a varying length. 398 const subjects = [_][]const u8{ 399 "fix off-by-one in scatter loop", 400 "encode and decode buffers", 401 "tune kernel socket timeouts", 402 "render matrix vector product", 403 "gather falcon and otter stats", 404 "refactor badger walrus module", 405 "drop dead ferret marmot branch", 406 }; 407 const commit_count = 30; 408 var c: usize = 0; 409 while (c < commit_count) : (c += 1) { 410 { 411 var repo_dir = try cwd.openDir(io, template_path, .{}); 412 defer repo_dir.close(io); 413 try repo_dir.createDirPath(io, "src"); 414 for (edit_files, 0..) |path, fi| { 415 try writeScatterFile(io, allocator, repo_dir, path, fi, c); 416 } 417 } 418 try template_repo.add(io, allocator, &edit_files); 419 // a cycling subject padded to a c-varying length so the commit 420 // list shows messages of different widths, capped at 120. 421 var msg_writer = std.Io.Writer.Allocating.init(allocator); 422 defer msg_writer.deinit(); 423 try msg_writer.writer.print("{s}", .{subjects[c % subjects.len]}); 424 const msg_target = 16 + (c * 41) % 96; 425 var mw = c; 426 while (true) : (mw += 1) { 427 const word = scatter_words[mw % scatter_words.len]; 428 const len = msg_writer.written().len; 429 if (len >= msg_target or len + 1 + word.len > 120) break; 430 try msg_writer.writer.print(" {s}", .{word}); 431 } 432 // the newest commit's message links to the project and runs 433 // past what the detail pane reads, so it shows the truncated 434 // message and its link. 435 if (c == commit_count - 1) { 436 try msg_writer.writer.writeAll("\n\nsee https://github.com/xit-vcs/haxy"); 437 for (0..ui.detail_preview_lines) |line| { 438 try msg_writer.writer.print("\n{d} {s}", .{ line, scatter_words[line % scatter_words.len] }); 439 } 440 } 441 const message = try arena.allocator().dupe(u8, msg_writer.written()); 442 // the newest commit was committed by someone other than its 443 // author, so the detail pane shows a committer box. 444 const committer: ?[]const u8 = if (c == commit_count - 1) "alice <alice@example.test>" else null; 445 _ = try template_repo.commit(io, allocator, .{ .message = message, .committer = committer, .timestamp = base_ts + c * std.time.s_per_day }); 446 try addNextTags(&template_repo, io, allocator, &tag_num); 447 } 448 449 // two more branches forked off master, each adding a single commit 450 // that makes a scattered edit to one file. tag each new commit too, 451 // and switch back to master after each so the next branch also 452 // forks from master and the template ends up back on master. 453 const branch_data = [_]struct { 454 name: []const u8, 455 file: []const u8, 456 fi: usize, 457 message: []const u8, 458 rev: usize, 459 }{ 460 .{ .name = "extra", .file = "src/alpha.txt", .fi = 0, .message = "scatter alpha on the extra branch", .rev = 30 }, 461 .{ .name = "feature", .file = "src/beta.txt", .fi = 1, .message = "scatter beta on the feature branch", .rev = 31 }, 462 }; 463 for (branch_data) |b| { 464 try template_repo.addBranch(io, .{ .name = b.name }); 465 { 466 var to_branch = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = b.name } } }); 467 defer to_branch.deinit(); 468 } 469 470 { 471 var repo_dir = try cwd.openDir(io, template_path, .{}); 472 defer repo_dir.close(io); 473 try writeScatterFile(io, allocator, repo_dir, b.file, b.fi, b.rev); 474 } 475 try template_repo.add(io, allocator, &.{b.file}); 476 _ = try template_repo.commit(io, allocator, .{ .message = b.message, .timestamp = base_ts + b.rev * std.time.s_per_day }); 477 try addNextTags(&template_repo, io, allocator, &tag_num); 478 479 { 480 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 481 defer to_master.deinit(); 482 } 483 } 484 485 // the timestamps issue's description, in paragraphs, so the 486 // seeded conflict below can insert and delete among them 487 const tz_p1 = "All timestamps in the activity feed render in `UTC` regardless of the system timezone."; 488 const tz_p2 = "The tooltip on each entry repeats the same UTC value, so there is *no way* to see the local time without converting by hand."; 489 const tz_p3 = "A quick survey of [other clients](https://github.com/xit-vcs/haxy) shows every one of them rendering local time by default."; 490 const tz_p4 = "Convert to local time and include the **offset** in tooltips."; 491 492 // the sync issue's description, in paragraphs, so each side of its 493 // seeded conflict can rework a different one 494 const sync_p1 = "The sync engine only has *unit tests* with mocked transports."; 495 const sync_p2 = "Failures seen in production involve reconnects and **partial writes**, which the mocks can't reproduce."; 496 const sync_p3 = "Add end-to-end tests that run two instances against a real local server."; 497 498 // seed issues so every repo's issue tracker has content 499 const issue_data = [_]struct { 500 title: []const u8, 501 description: []const u8, 502 labels: []const u8, 503 status: evt.Issue.Status = .open, 504 }{ 505 .{ 506 .title = "Crash on startup when config file is missing", 507 .description = "Running the program without a `config.toml` present dereferences a null pointer and segfaults. Fall back to the built-in defaults and log a **warning** instead.", 508 .labels = "bug priority-high crash", 509 }, 510 .{ 511 .title = "Support dark mode in the settings panel", 512 .description = "The settings panel is *always* rendered with the light palette even when the rest of the app is in dark mode. Read the active theme and pick colors accordingly.", 513 .labels = "enhancement ui theme", 514 }, 515 .{ 516 .title = "Memory leak when reconnecting after network failure", 517 .description = "Each reconnect allocates a new connection state without freeing the previous one.\n\n> After a flaky network session, memory usage grows by several megabytes per hour.", 518 .labels = "bug memory networking", 519 .status = .closed, 520 }, 521 .{ 522 .title = "Document the plugin API", 523 .description = "The plugin interface has no documentation beyond the header comments. Add a guide covering:\n\n- the lifecycle hooks\n- the event callbacks\n- a minimal working plugin", 524 .labels = "documentation plugins", 525 }, 526 .{ 527 .title = "Slow file indexing on large directories", 528 .description = "Indexing a directory with more than 100k files takes several minutes because every entry is `stat`'d twice. Cache the first stat result and batch the reads.", 529 .labels = "performance indexing", 530 }, 531 .{ 532 .title = "Add keyboard shortcut for quick search", 533 .description = "Opening the search box currently requires clicking the toolbar icon. Bind a shortcut and show it in the tooltip so **keyboard users** can search without the mouse.", 534 .labels = "enhancement ux keyboard", 535 }, 536 .{ 537 .title = "Unicode filenames are garbled in the export dialog", 538 .description = "Filenames containing non-ASCII characters display as replacement characters in the export dialog. The dialog decodes the path as *Latin-1* instead of *UTF-8*.", 539 .labels = "bug unicode i18n", 540 }, 541 .{ 542 .title = "Flaky test: integration suite times out on CI", 543 .description = "The integration suite intermittently exceeds the CI time limit because the server fixture waits for a fixed 30 seconds:\n\n```\nstd.Io.sleep(io, .fromSeconds(30), .awake);\n```\n\nPoll for readiness instead of sleeping.", 544 .labels = "bug testing ci", 545 }, 546 .{ 547 .title = "Upgrade bundled zlib to the latest release", 548 .description = "The vendored zlib is two major releases behind and misses several upstream fixes.\n\n- [ ] update the bundled copy\n- [ ] re-run the compression benchmarks", 549 .labels = "dependencies maintenance", 550 }, 551 .{ 552 .title = "Progress bar overshoots 100% during resumed downloads", 553 .description = "Resuming a partial download counts the already-downloaded bytes twice, so the progress bar reads up to **150%**. Subtract the resume offset from the total.", 554 .labels = "bug ui downloads", 555 .status = .closed, 556 }, 557 .{ 558 .title = "Config parser rejects trailing commas", 559 .description = "A trailing comma after the last entry in a config block is reported as a syntax error:\n\n```\n[server]\nport = 8080,\n```\n\nMost editors add one automatically, so accept it.", 560 .labels = "bug config parser", 561 }, 562 .{ 563 .title = "Add a --json flag to the status command", 564 .description = "Scripts currently scrape the human-readable `status` output, which breaks whenever the format changes. Emit a stable machine-readable JSON form behind a `--json` flag.", 565 .labels = "enhancement cli", 566 }, 567 .{ 568 .title = "Race condition between autosave and manual save", 569 .description = "Saving manually while an autosave is in flight can interleave the two writes and **corrupt the file**. Serialize saves through a single queue.", 570 .labels = "bug priority-high data-loss", 571 }, 572 .{ 573 .title = "Reduce binary size of release builds", 574 .description = "The release binary has grown past 40 MB, mostly from:\n\n1. debug info\n2. an unused bundled font\n\nStrip symbols and drop the font from the default build.", 575 .labels = "performance build", 576 }, 577 .{ 578 .title = "Tooltips flicker when the cursor moves between adjacent buttons", 579 .description = "Moving the cursor across a toolbar hides and re-shows the tooltip for *every* button. Keep the tooltip open with a short grace period between neighbors.", 580 .labels = "bug ui polish", 581 }, 582 .{ 583 .title = "Support environment variable expansion in config paths", 584 .description = "Paths in the config file are taken literally, so shared configs can't refer to `$HOME` portably. Expand environment variables when loading.", 585 .labels = "enhancement config", 586 }, 587 .{ 588 .title = "Log rotation deletes the newest file instead of the oldest", 589 .description = "When the log directory hits its size cap, the rotation logic sorts by name rather than `mtime` and removes the **most recent** log. Sort by modification time.", 590 .labels = "bug logging", 591 .status = .closed, 592 }, 593 .{ 594 .title = "Add man pages for all subcommands", 595 .description = "Only the top-level command has a man page. Generate one per subcommand from the existing `--help` text as part of the release build.", 596 .labels = "documentation cli", 597 }, 598 .{ 599 .title = "High CPU usage while idle in the background", 600 .description = "The main loop polls for file changes every 10 ms even when no window is visible.\n\n> Switch to native file watching and idle at zero CPU.", 601 .labels = "performance priority-high", 602 }, 603 .{ 604 .title = "Paste from clipboard drops the final newline", 605 .description = "Pasting text that ends with a newline silently trims it, which breaks pasted shell snippets. Preserve the clipboard content *exactly*.", 606 .labels = "bug editor clipboard", 607 }, 608 .{ 609 .title = "Improve error message for expired credentials", 610 .description = "## Problem\n\nAn expired token currently surfaces as a bare `401` with no guidance.\n\n## Fix\n\nDetect the expiry case and tell the user how to re-authenticate, linking to the [setup guide](https://github.com/xit-vcs/haxy).", 611 .labels = "enhancement ux auth", 612 }, 613 .{ 614 .title = "Crash when window is resized during startup animation", 615 .description = "Resizing the window while the splash animation is running dereferences a **freed** layout node. Cancel the animation before rebuilding the layout.", 616 .labels = "bug crash ui", 617 }, 618 .{ 619 .title = "Add integration tests for the sync engine", 620 .description = sync_p1 ++ "\n\n" ++ sync_p2 ++ "\n\n" ++ sync_p3, 621 .labels = "testing sync", 622 }, 623 .{ 624 .title = "Timestamps display in UTC instead of local time", 625 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p3 ++ "\n\n" ++ tz_p4, 626 .labels = "bug i18n time", 627 }, 628 .{ 629 .title = "Deprecate the legacy plugin format", 630 .description = "Both plugin formats are currently loaded, doubling the maintenance surface.\n\n- [ ] warn on legacy plugins this release\n- [ ] drop support in the next", 631 .labels = "maintenance plugins", 632 }, 633 }; 634 635 var issue_events: [issue_data.len]evt.EventWithId = undefined; 636 for (issue_data, 0..) |issue, i| { 637 // the fifth-newest issue's description runs past what the detail 638 // pane shows, so it shows the truncated description and its 639 // link. 640 const description = if (i == issue_data.len - 5) 641 try longDescription(arena.allocator(), issue.description) 642 else 643 issue.description; 644 issue_events[i] = .{ 645 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 646 // threads span several weeks, with time for replies on the newest one 647 .timestamp = seedTimestamp(now, i, issue_data.len, 25 * std.time.s_per_day, 3 * std.time.s_per_hour), 648 .author = .{ .name = user_data[i % user_data.len].name, .email = user_data[i % user_data.len].email }, 649 .event = .{ 650 .issue = .{ 651 .title = issue.title, 652 .description = description, 653 .labels = issue.labels, 654 .status = issue.status, 655 }, 656 }, 657 }; 658 } 659 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &issue_events); 660 // the tip is the branch point for the conflicting edits below 661 const seed_tip = (try template_repo.readRef(io, evt.events_ref)) orelse unreachable; 662 663 // two divergent edits per conflicted issue: ours on the events 664 // branch, theirs on a temp branch rooted at the seed tip, then a 665 // merge. the 4th-newest issue conflicts on title and labels; the 666 // 3rd-newest on its description with every hunk auto-resolving 667 // (each side reworks a different paragraph); the 2nd-newest on 668 // its description with a removal conflict (ours removes a 669 // paragraph theirs rewords) and an insertion conflict (each side 670 // appends a different closing paragraph). 671 { 672 const other_ref: xit.ref.Ref = .{ .kind = .head, .name = "haxy/other" }; 673 const title_issue = issue_data[issue_data.len - 4]; 674 const sync_issue = issue_data[issue_data.len - 3]; 675 const desc_issue = issue_data[issue_data.len - 2]; 676 677 const ours = [_]evt.EventWithId{ .{ 678 .id = issue_events[issue_data.len - 4].id, 679 .timestamp = now - 120 * std.time.s_per_min, 680 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 681 .event = .{ .issue = .{ 682 .title = "Crash when resizing the window during the splash animation", 683 .description = title_issue.description, 684 .labels = "bug crash ui priority-high", 685 } }, 686 }, .{ 687 .id = issue_events[issue_data.len - 3].id, 688 .timestamp = now - 115 * std.time.s_per_min, 689 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 690 .event = .{ .issue = .{ 691 .title = sync_issue.title, 692 .description = "The sync engine's coverage is *unit tests* only, with every transport mocked out." ++ "\n\n" ++ 693 sync_p2 ++ "\n\n" ++ sync_p3, 694 .labels = sync_issue.labels, 695 } }, 696 }, .{ 697 .id = issue_events[issue_data.len - 2].id, 698 .timestamp = now - 110 * std.time.s_per_min, 699 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 700 .event = .{ .issue = .{ 701 .title = desc_issue.title, 702 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p4 ++ "\n\n" ++ 703 "> The confusion is worst for teams spread across timezones, who each read a different wall-clock time from the same feed.", 704 .labels = desc_issue.labels, 705 } }, 706 } }; 707 const theirs = [_]evt.EventWithId{ .{ 708 .id = issue_events[issue_data.len - 4].id, 709 .timestamp = now - 105 * std.time.s_per_min, 710 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 711 .event = .{ .issue = .{ 712 .title = "Segfault on early window resize", 713 .description = title_issue.description, 714 .labels = "bug crash rendering", 715 } }, 716 }, .{ 717 .id = issue_events[issue_data.len - 3].id, 718 .timestamp = now - 100 * std.time.s_per_min, 719 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 720 .event = .{ .issue = .{ 721 .title = sync_issue.title, 722 .description = sync_p1 ++ "\n\n" ++ sync_p2 ++ "\n\n" ++ 723 "Add end-to-end tests that drive two live instances against a local server on **every** CI run.", 724 .labels = sync_issue.labels, 725 } }, 726 }, .{ 727 .id = issue_events[issue_data.len - 2].id, 728 .timestamp = now - 95 * std.time.s_per_min, 729 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 730 .event = .{ .issue = .{ 731 .title = desc_issue.title, 732 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ 733 "Most other clients already render local time by default, which makes our `UTC` output stand out as a bug." ++ "\n\n" ++ tz_p4 ++ "\n\n" ++ 734 "Log exports inherit the same UTC rendering, so downstream tooling has to *guess* the source timezone.", 735 .labels = desc_issue.labels, 736 } }, 737 } }; 738 739 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &ours); 740 741 try commitEventsAtRef(io, allocator, &template_repo, other_ref, &theirs, seed_tip); 742 743 { 744 var to_events = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 745 defer to_events.deinit(); 746 } 747 { 748 var merge = try template_repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, null); 749 defer merge.deinit(); 750 if (merge.result != .success) return error.MergeFailed; 751 } 752 { 753 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 754 defer to_master.deinit(); 755 } 756 try template_repo.removeBranch(io, .{ .name = other_ref.name }); 757 } 758 759 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &.{}); 760 761 // seed a small comment tree on the newest issue 762 var comment_ids: [5][evt.event_id_size]u8 = undefined; 763 for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 764 765 const comment_bodies = [_][]const u8{ 766 "I can take this. The compatibility scanner already reports which repositories still use the **legacy** format.", 767 "Can we keep loading legacy plugins for *one release* after the warning lands? That would give downstream maintainers time to migrate.", 768 "That sounds good. I'll include the scanner output in the warning so each affected plugin is named:\n\n```\nwarning: plugins/foo.so uses the legacy format\n```", 769 "Please include the replacement manifest path too. That should make the warning actionable without opening the [migration guide](https://github.com/xit-vcs/haxy).", 770 "I'll add release-note text once the warning format is settled:\n\n- [x] draft the summary\n- [ ] list the affected plugins", 771 }; 772 773 var comment_events: [comment_ids.len]evt.EventWithId = undefined; 774 for (&comment_events, 0..) |*event, i| { 775 event.* = .{ 776 .id = std.fmt.bytesToHex(comment_ids[i], .lower), 777 .timestamp = seedTimestamp(now, i, comment_events.len, 2 * std.time.s_per_hour, 45), 778 .author = .{ .name = user_data[(i + 1) % user_data.len].name, .email = user_data[(i + 1) % user_data.len].email }, 779 .event = .{ .comment = .{ 780 .thread_id = issue_events[issue_events.len - 1].id, 781 .parent_id = switch (i) { 782 0, 1 => issue_events[issue_events.len - 1].id, 783 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), 784 3 => std.fmt.bytesToHex(comment_ids[2], .lower), 785 else => unreachable, 786 }, 787 .body = comment_bodies[i], 788 } }, 789 }; 790 } 791 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &comment_events); 792 793 // assign a few issues so the assignee filter has something to show: 794 // alice and bobby share the newest issue, alice also holds the one 795 // before it, and admin an older one 796 const assignment_data = [_]struct { issue: usize, user: usize }{ 797 .{ .issue = issue_data.len - 1, .user = 1 }, 798 .{ .issue = issue_data.len - 1, .user = 2 }, 799 .{ .issue = issue_data.len - 2, .user = 1 }, 800 .{ .issue = issue_data.len - 5, .user = 0 }, 801 }; 802 var assignment_events: [assignment_data.len]evt.EventWithId = undefined; 803 for (&assignment_events, assignment_data, 0..) |*event, assignment, i| { 804 const thread_id = issue_events[assignment.issue].id; 805 const email = user_data[assignment.user].email; 806 event.* = .{ 807 .id = std.fmt.bytesToHex(evt.Assignment.idOf(&try evt.parseEventId(&thread_id), email), .lower), 808 .timestamp = seedTimestamp(now, i, assignment_events.len, 90 * std.time.s_per_min, 20 * std.time.s_per_min), 809 .author = .{ .name = user_data[0].name, .email = user_data[0].email }, 810 .event = .{ .assign = .{ .thread_id = thread_id, .email = email } }, 811 }; 812 } 813 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &assignment_events); 814 815 const discussion_data = [_]struct { 816 title: []const u8, 817 description: []const u8, 818 labels: []const u8, 819 }{ 820 .{ 821 .title = "How should plugins declare capabilities?", 822 .description = "I'd like the manifest to make **privileged** capabilities explicit without making simple plugins verbose.", 823 .labels = "plugins design", 824 }, 825 .{ 826 .title = "Ideas for making large repositories faster", 827 .description = "This is a place to collect *profiling results* and discuss which indexing work is worth pursuing first.", 828 .labels = "performance indexing", 829 }, 830 .{ 831 .title = "What should the next release focus on?", 832 .description = "Let's compare the most important reliability fixes with the larger features already in progress, starting from https://github.com/xit-vcs/haxy.", 833 .labels = "release planning", 834 }, 835 .{ 836 .title = "Improving keyboard navigation", 837 .description = "Share workflows that still **require a mouse** and suggestions for making their focus behavior predictable.", 838 .labels = "ui keyboard accessibility", 839 }, 840 .{ 841 .title = "Configuration format discussion", 842 .description = "Should the next configuration format favor *strict validation* or accept common conveniences such as trailing commas?", 843 .labels = "config design", 844 }, 845 }; 846 847 var discussion_events: [discussion_data.len]evt.EventWithId = undefined; 848 for (discussion_data, 0..) |discussion, i| { 849 discussion_events[i] = .{ 850 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 851 .timestamp = seedTimestamp(now, i, discussion_data.len, 21 * std.time.s_per_day, 2 * std.time.s_per_hour), 852 .author = .{ .name = user_data[i % user_data.len].name, .email = user_data[i % user_data.len].email }, 853 .event = .{ .discuss = .{ 854 .title = discussion.title, 855 .description = discussion.description, 856 .labels = discussion.labels, 857 } }, 858 }; 859 } 860 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &discussion_events); 861 862 var discussion_comment_ids: [discussion_data.len][3][evt.event_id_size]u8 = undefined; 863 for (&discussion_comment_ids) |*ids| { 864 for (ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 865 } 866 var discussion_comment_events: [discussion_data.len * 3]evt.EventWithId = undefined; 867 for (&discussion_comment_events, 0..) |*event, i| { 868 const discussion_index = i / 3; 869 const comment_index = i % 3; 870 event.* = .{ 871 .id = std.fmt.bytesToHex(discussion_comment_ids[discussion_index][comment_index], .lower), 872 .timestamp = seedTimestamp(now, i, discussion_comment_events.len, 90 * std.time.s_per_min, 30), 873 .author = .{ .name = user_data[(i + 1) % user_data.len].name, .email = user_data[(i + 1) % user_data.len].email }, 874 .event = .{ .comment = .{ 875 .thread_id = discussion_events[discussion_index].id, 876 .parent_id = if (comment_index < 2) 877 discussion_events[discussion_index].id 878 else 879 std.fmt.bytesToHex(discussion_comment_ids[discussion_index][0], .lower), 880 .body = switch (comment_index) { 881 0 => "My preference is to start with the **smallest useful version** and expand it after we have real usage to learn from.", 882 1 => "It would help to *write down* the tradeoffs we are accepting so the decision is easy to revisit later.", 883 2 => "> A narrow first version\n\nAgreed. It should also make it easier to keep the implementation understandable.", 884 else => unreachable, 885 }, 886 } }, 887 }; 888 } 889 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &discussion_comment_events); 890 try template_repo.patchAll(io, allocator, null); 891 try template_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "true" }); 892 try transaction(io, &template_repo, Reindex{ .io = io, .allocator = allocator, .core = &template_repo.core, .commits = true }); 893 // clear history only after indexing has written its transactions. 894 { 895 const moment = try evt.currentMoment(.{}, &template_repo); 896 const roots = try evt.PatchRev.gcRoots(rp.Repo(.xit, .{}).DB, .sha1, allocator, moment); 897 defer allocator.free(roots); 898 _ = try template_repo.garbageCollect(io, allocator, .{ .extra_roots = roots }); 899 } 900 } 901 902 // copy the template to each repo's on-disk location, named by its 903 // hex-encoded event id. the template repo is deinitialized above, so its 904 // db file is fully written before we copy it. 905 { 906 var template_dir = try cwd.openDir(io, template_path, .{}); 907 defer template_dir.close(io); 908 var template_repo_dir = try template_dir.openDir(io, ".xit", .{ .iterate = true }); 909 defer template_repo_dir.close(io); 910 911 for (repo_event_ids, 0..) |id_bytes, repo_index| { 912 const repo_path = try evt.repoPath(arena.allocator(), users_dir, &user_ids[repo_data[repo_index].user_index], &id_bytes); 913 { 914 var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); 915 defer dest_dir.close(io); 916 var dest_repo_dir = try dest_dir.createDirPathOpen(io, ".xit", .{}); 917 defer dest_repo_dir.close(io); 918 try copyDir(io, template_repo_dir, dest_repo_dir); 919 } 920 921 if (repo_index + 1 == repo_data.len) { 922 var target_repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = repo_path, .require_repo_root = true }); 923 defer target_repo.deinit(io, allocator); 924 try seedPatches(io, allocator, server_path, &repo, &target_repo, &repo_event_ids[repo_index], &user_ids[repo_data[repo_index].user_index], &admin_user_id, prng.random(), now); 925 } 926 } 927 } 928 929 break :blk try ui.Session.init(&session_arena, &repo, .{ .user_id = &admin_user_id }); 930 }; 931 session.is_terminal = true; 932 session.color = std.mem.eql(u8, init.environ_map.get("NO_COLOR") orelse "", ""); 933 session.data.git_ssh_prefix = git_ssh_prefix; 934 935 // start the server 936 937 // let the native TUI's page builders open the on-disk repos for the file tree 938 session.io = io; 939 session.users_dir = try std.Io.Dir.path.join(session_arena.allocator(), &.{ server_path, "users" }); 940 941 // leave a one-shot session for the first browser to hit the web ui, so it 942 // starts logged in as admin 943 { 944 var data_dir = try cwd.createDirPathOpen(io, server_path, .{}); 945 defer data_dir.close(io); 946 const store = try hx.web.SessionStore.init(io, data_dir); 947 defer store.deinit(); 948 const token = try store.create(&admin_user_id); 949 try store.offerAutoLogin(&token); 950 } 951 952 if (cli) { 953 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 954 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 955 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 956 957 const Runnable = struct { 958 io: std.Io, 959 key_path: []const u8, 960 961 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port_maybe: ?u16) !void { 962 _ = web_port; 963 _ = http_port; 964 const ssh_port = ssh_port_maybe orelse return error.SshDisabled; 965 std.debug.print( 966 \\ 967 \\connect to the TUI with: 968 \\ ssh -p {d} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 969 \\ 970 \\create a git repo and push over SSH: 971 \\ mkdir -p temp-try/client/test 972 \\ cd temp-try/client/test 973 \\ git init 974 \\ echo "hello" > hello.txt 975 \\ git add hello.txt 976 \\ git commit -m "let there be light" 977 \\ GIT_SSH_COMMAND='ssh -p {d} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -i {s}' git push localhost:admin:test HEAD:master 978 \\ 979 \\to quit, press enter. 980 \\ 981 , .{ ssh_port, ssh_port, self.key_path }); 982 // portable stdin read via std.Io — equivalent to the 983 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 984 // on windows. 985 var buf: [1]u8 = undefined; 986 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 987 _ = stdin_reader.interface.takeByte() catch {}; 988 } 989 }; 990 991 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 992 } else { 993 var null_writer = std.Io.Writer.Discarding.init(&.{}); 994 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 995 996 const Runnable = struct { 997 io: std.Io, 998 allocator: std.mem.Allocator, 999 session: *ui.Session, 1000 repo: *Repo, 1001 1002 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port: ?u16) !void { 1003 // launch the TUI; the footer points at whatever port was bound 1004 self.session.web_port = web_port; 1005 self.session.data.git_http_port = http_port; 1006 self.session.data.git_ssh_port = ssh_port; 1007 try hx.ui.run(self.io, self.allocator, self.session, self.repo); 1008 } 1009 }; 1010 1011 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session, .repo = &repo }); 1012 } 1013 } 1014 1015 // words cycled through to pad generated lines into prose-like content 1016 const scatter_words = [_][]const u8{ 1017 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", 1018 "quantum", "vector", "matrix", "buffer", "kernel", "socket", 1019 "falcon", "otter", "badger", "walrus", "ferret", "marmot", 1020 "scatter", "gather", "encode", "decode", "render", "commit", 1021 }; 1022 1023 fn longDescription(allocator: std.mem.Allocator, initial: []const u8) ![]u8 { 1024 var writer = std.Io.Writer.Allocating.init(allocator); 1025 defer writer.deinit(); 1026 try writer.writer.print("{s}\n", .{initial}); 1027 for (0..ui.detail_preview_lines) |line| 1028 try writer.writer.print("\n- {d} {s}", .{ line, scatter_words[line % scatter_words.len] }); 1029 return writer.toOwnedSlice(); 1030 } 1031 1032 // write `path` as 40 lines of mostly-stable filler. revision `c` and file 1033 // index `fi` shift which lines change and the words used, so each (c, fi) 1034 // yields a distinct multi-hunk diff against the previous revision. untouched 1035 // lines stay byte-identical across revisions, so a bump in `c` is a scatter of 1036 // small hunks rather than a full rewrite. 1037 fn writeScatterFile(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir, path: []const u8, fi: usize, c: usize) !void { 1038 const content = try scatterContent(allocator, fi, c, null); 1039 defer allocator.free(content); 1040 const file = try repo_dir.createFile(io, path, .{}); 1041 defer file.close(io); 1042 try file.writeStreamingAll(io, content); 1043 } 1044 1045 const ScatterEdit = struct { line: usize, text: []const u8 }; 1046 1047 fn scatterContent(allocator: std.mem.Allocator, fi: usize, c: usize, replacement: ?ScatterEdit) ![]u8 { 1048 var writer = std.Io.Writer.Allocating.init(allocator); 1049 defer writer.deinit(); 1050 for (0..40) |line| { 1051 if (replacement) |edit| if (line == edit.line) { 1052 try writer.writer.print("{s}\n", .{edit.text}); 1053 continue; 1054 }; 1055 const start = writer.written().len; 1056 if ((line + fi + c) % 8 == 0) { 1057 const tag = scatter_words[(line + fi + c * 3) % scatter_words.len]; 1058 try writer.writer.print("rev {d} {s}", .{ c, tag }); 1059 } 1060 const target = 40 + (line * 17 + fi * 23) % 70; 1061 var w = line + fi; 1062 while (true) : (w += 1) { 1063 const word = scatter_words[w % scatter_words.len]; 1064 const len = writer.written().len - start; 1065 if (len >= target or len + 1 + word.len > 120) break; 1066 if (len == 0) 1067 try writer.writer.print("{s}", .{word}) 1068 else 1069 try writer.writer.print(" {s}", .{word}); 1070 } 1071 try writer.writer.writeByte('\n'); 1072 } 1073 return writer.toOwnedSlice(); 1074 } 1075 1076 // tag the current HEAD with the next three sequential versions, so the refs 1077 // tab has enough tags to page through 1078 fn addNextTags(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { 1079 for (0..3) |_| { 1080 var buf: [16]u8 = undefined; 1081 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); 1082 _ = try repo.addTag(io, allocator, .{ .name = name }); 1083 n.* += 1; 1084 } 1085 } 1086 1087 fn commitTree( 1088 io: std.Io, 1089 allocator: std.mem.Allocator, 1090 repo: *rp.Repo(.xit, .{}), 1091 oid: *const [hash.hexLen(.sha1)]u8, 1092 ) ![hash.hexLen(.sha1)]u8 { 1093 var moment = try repo.core.latestMoment(); 1094 const state = rp.Repo(.xit, .{}).State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; 1095 var object = try obj.Object(.xit, .{}).init(state, io, allocator, oid); 1096 defer object.deinit(); 1097 return switch (object.content) { 1098 .commit => |commit| commit.tree, 1099 else => error.InvalidObject, 1100 }; 1101 } 1102 1103 // distribute a seed batch from oldest to newest relative to this run. 1104 fn seedTimestamp(now: u64, index: usize, count: usize, oldest_age: u64, newest_age: u64) u64 { 1105 const age = if (count > 1) oldest_age - (oldest_age - newest_age) * index / (count - 1) else newest_age; 1106 return now - age; 1107 } 1108 1109 fn seedPatchRevision( 1110 io: std.Io, 1111 allocator: std.mem.Allocator, 1112 users_dir: []const u8, 1113 forker_id: *const [evt.event_id_size]u8, 1114 patch_id: *const [evt.event_id_size]u8, 1115 title: []const u8, 1116 author: evt.CommitAuthor, 1117 timestamp: u64, 1118 random: std.Random, 1119 base_oid: [hash.hexLen(.sha1)]u8, 1120 path: []const u8, 1121 contents: []const []const u8, 1122 // a base file the last commit removes 1123 removed_path: ?[]const u8, 1124 ) !void { 1125 const patch_hex = std.fmt.bytesToHex(patch_id.*, .lower); 1126 const fork_path = try fork.forkPath(allocator, users_dir, forker_id, patch_id); 1127 defer allocator.free(fork_path); 1128 var fork_repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }); 1129 defer fork_repo.deinit(io, allocator); 1130 1131 try fork_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "false" }); 1132 { 1133 var reset = try fork_repo.reset(io, allocator, .{ .target = .{ .oid = &base_oid } }); 1134 defer reset.deinit(); 1135 } 1136 const base_tree_oid = try commitTree(io, allocator, &fork_repo, &base_oid); 1137 var source_oid = base_oid; 1138 var fork_dir = try std.Io.Dir.cwd().openDir(io, fork_path, .{}); 1139 defer fork_dir.close(io); 1140 var writer = std.Io.Writer.Allocating.init(allocator); 1141 defer writer.deinit(); 1142 if (std.Io.Dir.path.dirname(path)) |dir| try fork_dir.createDirPath(io, dir); 1143 const commit_count = contents.len + @intFromBool(removed_path != null); 1144 for (contents, 0..) |content, i| { 1145 { 1146 const file = try fork_dir.createFile(io, path, .{}); 1147 defer file.close(io); 1148 try file.writeStreamingAll(io, content); 1149 } 1150 try fork_repo.add(io, allocator, &.{path}); 1151 writer.clearRetainingCapacity(); 1152 try writer.writer.print("{s} ({d}/{d})", .{ title, i + 1, commit_count }); 1153 source_oid = try fork_repo.commit(io, allocator, .{ .message = writer.written(), .timestamp = timestamp + i }); 1154 } 1155 if (removed_path) |removed| { 1156 // the bare fork never checked the base files out, so only the index drops it 1157 try fork_repo.remove(io, allocator, &.{removed}, .{ .update_work_dir = false }); 1158 writer.clearRetainingCapacity(); 1159 try writer.writer.print("{s} ({d}/{d})", .{ title, commit_count, commit_count }); 1160 source_oid = try fork_repo.commit(io, allocator, .{ .message = writer.written(), .timestamp = timestamp + contents.len }); 1161 } 1162 try fork_repo.patchAll(io, allocator, null); 1163 try fork_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "true" }); 1164 try transaction(io, &fork_repo, Reindex{ .io = io, .allocator = allocator, .core = &fork_repo.core, .commits = false }); 1165 _ = try fork_repo.garbageCollect(io, allocator, .{}); 1166 try fork_dir.deleteFile(io, path); 1167 const revision_id = evt.EventWithId.randomId(random); 1168 const head_tree_oid = try commitTree(io, allocator, &fork_repo, &source_oid); 1169 const revision: evt.PatchRev = .{ 1170 .base_oid = &base_oid, 1171 .source_oid = &source_oid, 1172 .message = title, 1173 }; 1174 const revision_timestamp = timestamp + 3; 1175 const tree_entries = [_]evt.EventTreeEntry{ 1176 .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, 1177 .{ .tree = .{ .name = "head", .oid = &head_tree_oid } }, 1178 }; 1179 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .fork, .xit, .{}, io, allocator, &fork_repo, evt.events_ref, &.{.{ 1180 .id = std.fmt.bytesToHex(revision_id, .lower), 1181 .timestamp = revision_timestamp, 1182 .author = author, 1183 .tree_entries = &tree_entries, 1184 .event = .{ .patchrev = revision }, 1185 }}); 1186 1187 var arena = std.heap.ArenaAllocator.init(allocator); 1188 defer arena.deinit(); 1189 const moment = try evt.currentMoment(.{}, &fork_repo); 1190 const revision_record = (try evt.PatchRev.readById(evt.EventDB(.sha1), .sha1, moment, &arena, &revision_id)) orelse return error.NotFound; 1191 const patch_record = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &arena, patch_id)) orelse return error.NotFound; 1192 var patch = patch_record.event; 1193 patch.revision = evt.Patch.Revision.fromRecord(revision_id, revision_record); 1194 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .fork, .xit, .{}, io, allocator, &fork_repo, evt.events_ref, &.{.{ 1195 .id = patch_hex, 1196 .timestamp = revision_timestamp + 1, 1197 .author = author, 1198 .event = .{ .patch = patch }, 1199 }}); 1200 } 1201 1202 fn seedPatches( 1203 io: std.Io, 1204 allocator: std.mem.Allocator, 1205 server_path: []const u8, 1206 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), 1207 target_repo: *rp.Repo(.xit, .{}), 1208 repo_id: *const [evt.event_id_size]u8, 1209 repo_user_id: *const [evt.event_id_size]u8, 1210 user_id: *const [evt.event_id_size]u8, 1211 random: std.Random, 1212 now: u64, 1213 ) !void { 1214 const patch_data = [_]struct { 1215 title: []const u8, 1216 description: []const u8, 1217 labels: []const u8, 1218 status: ?evt.Patch.StatusKind, 1219 alpha_edit: ?ScatterEdit = null, 1220 // based behind master, so merging it makes a merge commit 1221 behind_master: bool = false, 1222 // a base file the patch removes 1223 removed_path: ?[]const u8 = null, 1224 }{ 1225 .{ 1226 .title = "Draft a faster dependency scanner", 1227 .description = "Rework dependency discovery so a **large workspace** can be scanned without repeatedly opening the same manifests.", 1228 .labels = "performance build", 1229 .status = null, 1230 }, 1231 .{ 1232 .title = "Remove the legacy configuration loader", 1233 .description = "Delete the compatibility loader now that the replacement format has shipped and the migration warning has been available for a *full release*.", 1234 .labels = "cleanup config", 1235 .status = .merged, 1236 .behind_master = true, 1237 }, 1238 .{ 1239 .title = "Cache parsed manifests between commands", 1240 .description = "Keep parsed manifests in the command `Context` so consecutive operations do not repeat identical filesystem and parsing work.", 1241 .labels = "performance cache", 1242 .status = .closed, 1243 }, 1244 .{ 1245 .title = "Add structured output to the inspect command", 1246 .description = "Add a stable JSON representation of `inspect` results for scripts and editor integrations.", 1247 .labels = "enhancement cli", 1248 .status = .open, 1249 .removed_path = "docs/dev/contribute.md", 1250 }, 1251 .{ 1252 .title = "Edit an adjacent line in alpha.txt", 1253 .description = "This patch can be cleanly merged by haxy, while git throws a **merge conflict**!", 1254 .labels = "merge", 1255 .status = .open, 1256 .alpha_edit = .{ .line = 2, .text = "adjust alpha beside the latest scatter edit" }, 1257 }, 1258 .{ 1259 .title = "Edit a conflicting line in alpha.txt", 1260 .description = "Both source and squash merges conflict with a different edit to the *same line* on master.", 1261 .labels = "merge", 1262 .status = .open, 1263 .alpha_edit = .{ .line = 3, .text = "replace alpha with a conflicting edit" }, 1264 }, 1265 }; 1266 1267 const users_dir = try std.Io.Dir.path.join(allocator, &.{ server_path, "users" }); 1268 defer allocator.free(users_dir); 1269 const patch_author = evt.CommitAuthor{ .name = "admin", .email = "admin@example.test" }; 1270 var patch_ids: [patch_data.len][evt.event_id_size]u8 = undefined; 1271 for (patch_data, 0..) |patch, i| { 1272 const timestamp = seedTimestamp(now, i, patch_data.len, 25 * std.time.s_per_day, 3 * std.time.s_per_hour); 1273 patch_ids[i] = evt.EventWithId.randomId(random); 1274 const patch_hex = std.fmt.bytesToHex(patch_ids[i], .lower); 1275 const path = try fork.create(.{}, io, allocator, users_dir, .{ 1276 .id = patch_hex, 1277 .user_id = user_id.*, 1278 .repo_id = repo_id.*, 1279 .repo_user_id = repo_user_id.*, 1280 .title = patch.title, 1281 .description = patch.description, 1282 .labels = patch.labels, 1283 .target_branch = "master", 1284 .author = patch_author, 1285 .timestamp = timestamp, 1286 }); 1287 allocator.free(path); 1288 1289 // prepare the patch's base and file contents, then seed its commits and revision 1290 { 1291 var arena = std.heap.ArenaAllocator.init(allocator); 1292 defer arena.deinit(); 1293 const aa = arena.allocator(); 1294 var base_oid = (try target_repo.readRef(io, .{ .kind = .head, .name = "master" })) orelse return error.NotFound; 1295 if (patch.alpha_edit != null or patch.behind_master) { 1296 // v30 precedes the last scatter commit, even after the sample merge 1297 const tag_oid = (try target_repo.readRef(io, .{ .kind = .tag, .name = "v30" })) orelse return error.NotFound; 1298 var moment = try target_repo.core.latestMoment(); 1299 const state = rp.Repo(.xit, .{}).State(.read_only){ .core = &target_repo.core, .extra = .{ .moment = &moment } }; 1300 var tag = try obj.Object(.xit, .{}).init(state, io, allocator, &tag_oid); 1301 defer tag.deinit(); 1302 base_oid = switch (tag.content) { 1303 .tag => |value| value.target, 1304 else => return error.InvalidObject, 1305 }; 1306 } 1307 var contents: [3][]const u8 = undefined; 1308 const file_path: []const u8, const count: usize = if (patch.alpha_edit) |edit| blk: { 1309 // rev 29 first changes line 4; edit that line or the one above it 1310 contents[0] = try scatterContent(aa, 0, 28, edit); 1311 break :blk .{ "src/alpha.txt", 1 }; 1312 } else blk: { 1313 var writer = std.Io.Writer.Allocating.init(allocator); 1314 defer writer.deinit(); 1315 for (&contents, 0..) |*content, part| { 1316 try writer.writer.print("{s}: part {d}\n", .{ patch.title, part + 1 }); 1317 content.* = try aa.dupe(u8, writer.written()); 1318 } 1319 break :blk .{ "patch.txt", contents.len }; 1320 }; 1321 try seedPatchRevision(io, allocator, users_dir, user_id, &patch_ids[i], patch.title, patch_author, timestamp + 1, random, base_oid, file_path, contents[0..count], patch.removed_path); 1322 } 1323 const status = patch.status orelse continue; 1324 try pch.publish(.{}, io, allocator, users_dir, admin_repo, target_repo, .{ 1325 .id = patch_hex, 1326 .user_id = user_id.*, 1327 .repo_id = repo_id.*, 1328 .author = patch_author, 1329 .timestamp = timestamp + 6, 1330 }); 1331 1332 if (status == .merged) { 1333 try pch.mergeAndRemoveFork(.{}, io, allocator, users_dir, target_repo, .{ 1334 .id = patch_hex, 1335 .revision = .source, 1336 .author = patch_author, 1337 .timestamp = timestamp + 7, 1338 }); 1339 } else if (status != .open) { 1340 try evt.Patch.update(.{ .server = .{ .users_dir = users_dir } }, .xit, .{}, io, allocator, target_repo, &patch_ids[i], .{ .status = status }, patch_author); 1341 } 1342 } 1343 1344 // seed a small comment tree on the newest patch 1345 var comment_ids: [5][evt.event_id_size]u8 = undefined; 1346 for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(random); 1347 const comment_bodies = [_][]const u8{ 1348 "This also needs to preserve the **executable bit** for files nested inside generated directories.", 1349 "I tested both paths:\n\n- the archive path\n- the direct-copy path\n\nBoth currently lose the mode.", 1350 "The `ArchiveWriter` is the right place to centralize it, since every exporter already goes through that layer.", 1351 "Please include a regression fixture with a *mix* of executable and ordinary files.", 1352 "I'll add that fixture and cover both supported archive formats:\n\n1. tar\n2. zip", 1353 }; 1354 var comments: [comment_ids.len]evt.EventWithId = undefined; 1355 for (&comments, 0..) |*comment, i| { 1356 comment.* = .{ 1357 .id = std.fmt.bytesToHex(comment_ids[i], .lower), 1358 .timestamp = seedTimestamp(now, i, comments.len, 2 * std.time.s_per_hour, 45), 1359 .author = .{ .name = if (i % 2 == 0) "alice" else "bobby", .email = if (i % 2 == 0) "alice@example.test" else "bobby@example.test" }, 1360 .event = .{ .comment = .{ 1361 .thread_id = std.fmt.bytesToHex(patch_ids[patch_ids.len - 1], .lower), 1362 .parent_id = switch (i) { 1363 0, 1 => std.fmt.bytesToHex(patch_ids[patch_ids.len - 1], .lower), 1364 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), 1365 3 => std.fmt.bytesToHex(comment_ids[2], .lower), 1366 else => unreachable, 1367 }, 1368 .body = comment_bodies[i], 1369 } }, 1370 }; 1371 } 1372 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &comments); 1373 1374 // create divergent metadata edits on the next three patches 1375 var patch_arena = std.heap.ArenaAllocator.init(allocator); 1376 defer patch_arena.deinit(); 1377 const moment = try evt.currentMoment(.{}, target_repo); 1378 var values: [3]evt.Patch = undefined; 1379 for (1..4) |i| { 1380 const record = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &patch_arena, &patch_ids[i])) orelse return error.NotFound; 1381 values[i - 1] = record.event; 1382 } 1383 var ours_values = values; 1384 ours_values[0].title = "Drop the legacy configuration loader"; 1385 ours_values[0].labels = "cleanup config breaking"; 1386 const closed_description = try longDescription( 1387 patch_arena.allocator(), 1388 "## Summary\n\nCache parsed manifests for the lifetime of a command invocation and invalidate entries when their files change.", 1389 ); 1390 ours_values[1].description = closed_description; 1391 ours_values[2].description = "Add JSON output to `inspect` with versioned field names and deterministic object ordering."; 1392 var theirs_values = values; 1393 theirs_values[0].title = "Delete compatibility configuration support"; 1394 theirs_values[0].labels = "config maintenance"; 1395 theirs_values[1].description = "Keep a process-wide manifest cache shared by **every** command and refresh it after writes."; 1396 theirs_values[2].description = "Expose `inspect` results as newline-delimited JSON so callers can stream large repositories."; 1397 1398 const seed_tip = (try target_repo.readRef(io, evt.events_ref)) orelse return error.NotFound; 1399 const other_ref = xit.ref.Ref{ .kind = .head, .name = "haxy/patch-other" }; 1400 var ours = [_]evt.EventWithId{ 1401 .{ 1402 .id = std.fmt.bytesToHex(patch_ids[1], .lower), 1403 .timestamp = now - 30 * std.time.s_per_min, 1404 .author = .{ .name = "alice", .email = "alice@example.test" }, 1405 .event = .{ .patch = ours_values[0] }, 1406 }, 1407 .{ 1408 .id = std.fmt.bytesToHex(patch_ids[2], .lower), 1409 .timestamp = now - 29 * std.time.s_per_min, 1410 .author = .{ .name = "alice", .email = "alice@example.test" }, 1411 .event = .{ .patch = ours_values[1] }, 1412 }, 1413 .{ 1414 .id = std.fmt.bytesToHex(patch_ids[3], .lower), 1415 .timestamp = now - 28 * std.time.s_per_min, 1416 .author = .{ .name = "alice", .email = "alice@example.test" }, 1417 .event = .{ .patch = ours_values[2] }, 1418 }, 1419 }; 1420 1421 var theirs = ours; 1422 theirs[0].timestamp = now - 25 * std.time.s_per_min; 1423 theirs[0].author = .{ .name = "bobby", .email = "bobby@example.test" }; 1424 theirs[0].event.patch = theirs_values[0]; 1425 theirs[1].timestamp = now - 24 * std.time.s_per_min; 1426 theirs[1].author = .{ .name = "bobby", .email = "bobby@example.test" }; 1427 theirs[1].event.patch = theirs_values[1]; 1428 theirs[2].timestamp = now - 23 * std.time.s_per_min; 1429 theirs[2].author = .{ .name = "bobby", .email = "bobby@example.test" }; 1430 theirs[2].event.patch = theirs_values[2]; 1431 1432 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &ours); 1433 try commitEventsAtRef(io, allocator, target_repo, other_ref, &theirs, seed_tip); 1434 { 1435 var merge = try target_repo.mergeAtRef(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, evt.events_ref, null); 1436 defer merge.deinit(); 1437 if (merge.result != .success) return error.MergeFailed; 1438 } 1439 try target_repo.removeBranch(io, .{ .name = other_ref.name }); 1440 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &.{}); 1441 try pch.writeBranchPatch(.{ .server = .{ .users_dir = users_dir } }, .xit, .{}, io, allocator, target_repo, std.fmt.bytesToHex(evt.EventWithId.randomId(random), .lower), .{ 1442 .title = "Merge the existing feature branch", 1443 .description = "This patch tracks the `feature` branch in this repo without creating a fork.", 1444 .labels = "feature", 1445 .source_branch = "feature", 1446 .target_branch = "master", 1447 }, null, patch_author); 1448 1449 // a push onto the branch that patch tracks, so the undo tab shows the 1450 // action and the patch refresh it sets off 1451 try seedPush(io, allocator, target_repo, users_dir, patch_author, now - 15); 1452 try pch.refreshBranches(.{ .server = .{ .users_dir = users_dir } }, .xit, .{}, io, allocator, target_repo, null, null); 1453 } 1454 1455 // stand in for a client push, which the fixture has no server to receive: one 1456 // transaction moves the branch, refreshes the patches tracking it and records 1457 // the action, the way receive-pack does 1458 fn seedPush(io: std.Io, allocator: std.mem.Allocator, repo: *rp.Repo(.xit, .{}), users_dir: []const u8, author: evt.CommitAuthor, timestamp: u64) !void { 1459 const Repo = rp.Repo(.xit, .{}); 1460 const DB = Repo.DB; 1461 const Ctx = struct { 1462 core: *Repo.Core, 1463 io: std.Io, 1464 allocator: std.mem.Allocator, 1465 users_dir: []const u8, 1466 author: evt.CommitAuthor, 1467 timestamp: u64, 1468 1469 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 1470 const opts: rp.RepoOpts(.xit) = .{}; 1471 var moment = try DB.HashMap(.read_write).init(cursor.*); 1472 const state = Repo.State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 1473 var arena = std.heap.ArenaAllocator.init(ctx.allocator); 1474 defer arena.deinit(); 1475 const aa = arena.allocator(); 1476 1477 // a null tree keeps the parent's content, so the branch advances 1478 // without the fixture having to build one 1479 const author_line = try aa.print("{s} <{s}>", .{ ctx.author.name, ctx.author.email }); 1480 const feature_old = (try rf.readRecur(.xit, opts, state.readOnly(), ctx.io, .{ .ref = .{ .kind = .head, .name = "feature" } })) orelse return error.NotFound; 1481 const feature_new = try obj.writeCommit(.xit, opts, state, ctx.io, ctx.allocator, .{ 1482 .message = "Revise the feature branch", 1483 .author = author_line, 1484 .timestamp = ctx.timestamp, 1485 }, null, .{ .kind = .head, .name = "feature" }); 1486 1487 // a branch the push creates has no old tip, so its range is open 1488 try bch.add(.xit, opts, state, ctx.io, .{ .name = "docs" }); 1489 const docs_new = (try rf.readRecur(.xit, opts, state.readOnly(), ctx.io, .{ .ref = .{ .kind = .head, .name = "docs" } })) orelse return error.NotFound; 1490 1491 const updates = [_]xit.net_server_receive_pack.AppliedRefUpdate{ 1492 .{ 1493 .ref_name = try aa.dupe(u8, "refs/heads/feature"), 1494 .old_oid = try aa.dupe(u8, &feature_old), 1495 .new_oid = try aa.dupe(u8, &feature_new), 1496 }, 1497 .{ 1498 .ref_name = try aa.dupe(u8, "refs/heads/docs"), 1499 .old_oid = try aa.dupe(u8, &@as([hash.hexLen(opts.hash)]u8, @splat('0'))), 1500 .new_oid = try aa.dupe(u8, &docs_new), 1501 }, 1502 }; 1503 1504 _ = try pch.refreshBranchesInTransaction(.{ .server = .{ .users_dir = ctx.users_dir } }, opts, state, &moment, ctx.io, ctx.allocator, &updates, null, null); 1505 try push.writeUndo(opts, state, ctx.io, ctx.allocator, ctx.author, &updates); 1506 } 1507 }; 1508 1509 try transaction(io, repo, Ctx{ .core = &repo.core, .io = io, .allocator = allocator, .users_dir = users_dir, .author = author, .timestamp = timestamp }); 1510 } 1511 1512 // commit `events` onto `ref`, rooted at `parent`, as one transaction. 1513 // consume always roots a new branch at nothing, so the divergent fixtures 1514 // write their side branch here instead. nothing is consumed, so it records 1515 // xit's own commit action rather than one of haxy's event actions 1516 fn commitEventsAtRef( 1517 io: std.Io, 1518 allocator: std.mem.Allocator, 1519 repo: *rp.Repo(.xit, .{}), 1520 ref: xit.ref.Ref, 1521 events: []const evt.EventWithId, 1522 parent: [hash.hexLen(.sha1)]u8, 1523 ) !void { 1524 const Repo = rp.Repo(.xit, .{}); 1525 const DB = Repo.DB; 1526 const Ctx = struct { 1527 core: *Repo.Core, 1528 io: std.Io, 1529 allocator: std.mem.Allocator, 1530 ref: xit.ref.Ref, 1531 events: []const evt.EventWithId, 1532 parent: [hash.hexLen(.sha1)]u8, 1533 1534 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 1535 const opts: rp.RepoOpts(.xit) = .{}; 1536 var moment = try DB.HashMap(.read_write).init(cursor.*); 1537 const state = Repo.State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 1538 try evt.commitEvents(.xit, opts, state, ctx.io, ctx.allocator, ctx.ref, ctx.events, .{ctx.parent}); 1539 const tip = (try xit.ref.readRecur(.xit, opts, state.readOnly(), ctx.io, .{ .ref = ctx.ref })) orelse return error.NotFound; 1540 try xit.undo.write(opts, state, std.Io.Timestamp.now(ctx.io, .real).toSeconds(), .{ 1541 .commit = try .init("seed conflicting events", tip, opts.max_read_size), 1542 }); 1543 } 1544 }; 1545 1546 try transaction(io, repo, Ctx{ 1547 .core = &repo.core, 1548 .io = io, 1549 .allocator = allocator, 1550 .ref = ref, 1551 .events = events, 1552 .parent = parent, 1553 }); 1554 } 1555 1556 // append `ctx` to the repo's history as one transaction. the fixtures write a 1557 // few states the normal paths have no way to produce 1558 // fixtures commit directly, so the ref tips are indexed by hand. a fork gets 1559 // no commit index, as in production. 1560 const Reindex = struct { 1561 io: std.Io, 1562 allocator: std.mem.Allocator, 1563 core: *rp.Repo(.xit, .{}).Core, 1564 commits: bool, 1565 1566 pub fn run(ctx: @This(), cursor: *rp.Repo(.xit, .{}).DB.Cursor(.read_write)) !void { 1567 var moment = try rp.Repo(.xit, .{}).DB.HashMap(.read_write).init(cursor.*); 1568 const state = rp.Repo(.xit, .{}).State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 1569 _ = try find.refreshInTransaction(.{}, state, &moment, ctx.io, ctx.allocator, null); 1570 if (ctx.commits) _ = try srch_cmmt.refreshInTransaction(.{}, state, &moment, ctx.io, ctx.allocator, &.{}, null); 1571 } 1572 }; 1573 1574 fn transaction(io: std.Io, repo: *rp.Repo(.xit, .{}), ctx: anytype) !void { 1575 const DB = rp.Repo(.xit, .{}).DB; 1576 try repo.core.db_file.lock(io, .exclusive); 1577 defer repo.core.db_file.unlock(io); 1578 1579 const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); 1580 try history.appendContext(.{ .slot = try history.getSlot(-1) }, ctx); 1581 } 1582 1583 // recursively copy the contents of src_dir into dest_dir 1584 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 1585 var iter = src_dir.iterate(); 1586 while (try iter.next(io)) |entry| { 1587 switch (entry.kind) { 1588 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 1589 .directory => { 1590 try dest_dir.createDirPath(io, entry.name); 1591 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1592 defer dest_entry_dir.close(io); 1593 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1594 defer src_entry_dir.close(io); 1595 try copyDir(io, src_entry_dir, dest_entry_dir); 1596 }, 1597 else => {}, 1598 } 1599 } 1600 } 1601