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