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