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