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