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