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