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