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