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