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