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