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