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