╭ 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 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server", "admin" }); 64 defer allocator.free(work_path); 65 66 const Repo = rp.Repo(.xit, evt.admin_repo_opts); 67 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 68 defer repo.deinit(io, allocator); 69 70 var session_arena = std.heap.ArenaAllocator.init(allocator); 71 defer session_arena.deinit(); 72 73 var session: ui.Session = blk: { 74 var arena = std.heap.ArenaAllocator.init(allocator); 75 defer arena.deinit(); 76 77 // define test events 78 79 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 80 81 const user_data = [_]struct { 82 name: []const u8, 83 display_name: []const u8, 84 email: []const u8, 85 }{ 86 .{ .name = "admin", .display_name = "Admin", .email = "admin@example.test" }, 87 .{ .name = "alice", .display_name = "Alice Tulley", .email = "alice@example.test" }, 88 .{ .name = "bob", .display_name = "Bob Smith", .email = "bob@example.test" }, 89 .{ .name = "carol", .display_name = "Carol Johnson", .email = "carol@example.test" }, 90 .{ .name = "dave", .display_name = "Dave Wilson", .email = "dave@example.test" }, 91 .{ .name = "eve", .display_name = "Eve Anderson", .email = "eve@example.test" }, 92 .{ .name = "frank", .display_name = "Frank Miller", .email = "frank@example.test" }, 93 .{ .name = "grace", .display_name = "Grace Lee", .email = "grace@example.test" }, 94 .{ .name = "henry", .display_name = "Henry Davis", .email = "henry@example.test" }, 95 .{ .name = "ivy", .display_name = "Ivy Martinez", .email = "ivy@example.test" }, 96 .{ .name = "jack", .display_name = "Jack Thompson", .email = "jack@example.test" }, 97 .{ .name = "kate", .display_name = "Kate Robinson", .email = "kate@example.test" }, 98 .{ .name = "liam", .display_name = "Liam Walker", .email = "liam@example.test" }, 99 .{ .name = "mona", .display_name = "Mona Patel", .email = "mona@example.test" }, 100 .{ .name = "noah", .display_name = "Noah Garcia", .email = "noah@example.test" }, 101 .{ .name = "olivia", .display_name = "Olivia Hernandez", .email = "olivia@example.test" }, 102 .{ .name = "peter", .display_name = "Peter Wright", .email = "peter@example.test" }, 103 .{ .name = "quinn", .display_name = "Quinn Foster", .email = "quinn@example.test" }, 104 .{ .name = "rachel", .display_name = "Rachel Bennett", .email = "rachel@example.test" }, 105 .{ .name = "sam", .display_name = "Sam Brooks", .email = "sam@example.test" }, 106 .{ .name = "tina", .display_name = "Tina Cooper", .email = "tina@example.test" }, 107 }; 108 109 const repo_data = [_]struct { 110 user_index: usize, 111 name: []const u8, 112 description: []const u8, 113 }{ 114 .{ .user_index = 1, .name = "ziglings", .description = "Learn the Zig programming language by fixing tiny broken programs" }, 115 .{ .user_index = 2, .name = "linux", .description = "Linux kernel source tree" }, 116 .{ .user_index = 3, .name = "kubernetes", .description = "Production-grade container orchestration" }, 117 .{ .user_index = 4, .name = "react", .description = "A declarative, efficient, and flexible JavaScript library for building user interfaces" }, 118 .{ .user_index = 5, .name = "typescript", .description = "TypeScript is a superset of JavaScript that compiles to clean JavaScript output" }, 119 .{ .user_index = 6, .name = "rust", .description = "Empowering everyone to build reliable and efficient software" }, 120 .{ .user_index = 7, .name = "go", .description = "The Go programming language" }, 121 .{ .user_index = 8, .name = "nodejs", .description = "Node.js JavaScript runtime" }, 122 .{ .user_index = 9, .name = "cpython", .description = "The Python programming language" }, 123 .{ .user_index = 10, .name = "docker", .description = "Container platform for developing, shipping, and running applications" }, 124 .{ .user_index = 1, .name = "vim", .description = "The ubiquitous text editor" }, 125 .{ .user_index = 2, .name = "neovim", .description = "Hyperextensible Vim-based text editor" }, 126 .{ .user_index = 3, .name = "emacs", .description = "GNU Emacs source code mirror" }, 127 .{ .user_index = 4, .name = "tmux", .description = "Terminal multiplexer" }, 128 .{ .user_index = 5, .name = "zsh", .description = "Mirror of the Z shell source code repository" }, 129 .{ .user_index = 6, .name = "git", .description = "Distributed version control system" }, 130 .{ .user_index = 7, .name = "mercurial", .description = "Source-control management tool" }, 131 .{ .user_index = 8, .name = "tensorflow", .description = "An end-to-end open source machine learning platform" }, 132 .{ .user_index = 9, .name = "pytorch", .description = "Tensors and dynamic neural networks in Python with strong GPU acceleration" }, 133 .{ .user_index = 10, .name = "numpy", .description = "The fundamental package for scientific computing with Python" }, 134 .{ .user_index = 1, .name = "pandas", .description = "Flexible and powerful data analysis and manipulation library for Python" }, 135 .{ .user_index = 2, .name = "scikit-learn", .description = "Machine learning in Python" }, 136 .{ .user_index = 3, .name = "nginx", .description = "High performance HTTP server and reverse proxy" }, 137 .{ .user_index = 4, .name = "redis", .description = "In-memory data structure store, used as a database, cache, and message broker" }, 138 .{ .user_index = 5, .name = "postgres", .description = "The world's most advanced open source relational database" }, 139 .{ .user_index = 6, .name = "sqlite", .description = "Self-contained, serverless, zero-configuration SQL database engine" }, 140 .{ .user_index = 7, .name = "mongodb", .description = "The MongoDB Database" }, 141 .{ .user_index = 8, .name = "elasticsearch", .description = "Free and open, distributed, RESTful search engine" }, 142 .{ .user_index = 9, .name = "kafka", .description = "Distributed event streaming platform" }, 143 .{ .user_index = 10, .name = "terraform", .description = "Infrastructure as code tool" }, 144 .{ .user_index = 11, .name = "svelte", .description = "Cybernetically enhanced web apps" }, 145 .{ .user_index = 12, .name = "vue", .description = "The progressive JavaScript framework" }, 146 .{ .user_index = 13, .name = "flask", .description = "The Python micro framework for building web applications" }, 147 .{ .user_index = 14, .name = "django", .description = "The web framework for perfectionists with deadlines" }, 148 .{ .user_index = 15, .name = "rails", .description = "Ruby on Rails web framework" }, 149 .{ .user_index = 16, .name = "phoenix", .description = "Peace of mind from prototype to production for Elixir web apps" }, 150 .{ .user_index = 17, .name = "laravel", .description = "The PHP framework for web artisans" }, 151 .{ .user_index = 18, .name = "prometheus", .description = "The Prometheus monitoring system and time series database" }, 152 .{ .user_index = 19, .name = "grafana", .description = "The open and composable observability and data visualization platform" }, 153 .{ .user_index = 20, .name = "ansible", .description = "Simple, agentless IT automation" }, 154 }; 155 156 var user_ids: [user_data.len][evt.event_id_size]u8 = undefined; 157 for (&user_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 158 159 var repo_event_ids: [repo_data.len][evt.event_id_size]u8 = undefined; 160 for (&repo_event_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 161 162 var password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 163 const password_hash = try evt.User.hashPassword("password", &password_hash_buf, io); 164 165 // public key matching temp-try/key, given to admin so we can push as admin 166 const admin_ssh_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKeIs8mJqigBZ5y84J4COgnAJJ5bHPKy+lM2SliMXbYm radar@roark"; 167 168 var events_to_consume: [user_data.len + repo_data.len]evt.EventWithId = undefined; 169 // inserted back to front, so the newest-first listing shows them in the 170 // order they're written above 171 for (0..user_data.len) |slot| { 172 const i = user_data.len - 1 - slot; 173 const u = user_data[i]; 174 events_to_consume[slot] = .{ 175 .id = std.fmt.bytesToHex(user_ids[i], .lower), 176 // stepped timestamps so the seeded users/repos list in a stable order 177 .timestamp = @intCast(slot + 1), 178 .event = .{ 179 .user = .{ 180 .name = u.name, 181 .display_name = u.display_name, 182 .email = u.email, 183 .password_hash = password_hash, 184 .ssh_keys = if (std.mem.eql(u8, "admin", u.name)) admin_ssh_key else "", 185 }, 186 }, 187 }; 188 } 189 for (repo_data, 0..) |r, i| { 190 events_to_consume[user_data.len + i] = .{ 191 .id = std.fmt.bytesToHex(repo_event_ids[i], .lower), 192 .timestamp = @intCast(user_data.len + i + 1), 193 .event = .{ 194 .repo = .{ 195 .user_id = &user_ids[r.user_index], 196 .name = r.name, 197 .description = r.description, 198 }, 199 }, 200 }; 201 } 202 203 // commit the seed events and consume them into the database 204 try evt.consume(.xit, evt.admin_repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume); 205 206 // every repo gets the same generated history, so build it once into a 207 // template repo and copy that to each repo's location below rather than 208 // redoing the expensive commit work for every repo. 209 const template_path = try std.fs.path.join(arena.allocator(), &.{ cwd_path, temp_dir_name, "template" }); 210 { 211 var template_repo = try rp.Repo(.xit, .{}).init(io, allocator, .{ .path = template_path }); 212 defer template_repo.deinit(io, allocator); 213 214 try template_repo.addConfig(io, allocator, .{ .name = "user.name", .value = "haxy" }); 215 try template_repo.addConfig(io, allocator, .{ .name = "user.email", .value = "admin@haxy" }); 216 217 // a README plus a nested doc so the file tree has a directory to 218 // descend into 219 { 220 var repo_dir = try cwd.openDir(io, template_path, .{}); 221 defer repo_dir.close(io); 222 223 const readme = try repo_dir.createFile(io, "README.md", .{}); 224 defer readme.close(io); 225 try readme.writeStreamingAll(io, 226 \\# Sample Repo 227 \\ 228 \\A repository seeded with test data for exercising the UI. 229 \\ 230 \\## Features 231 \\ 232 \\- Fast and lightweight 233 \\- Zero configuration required 234 \\- Works out of the box 235 \\ 236 \\## Installation 237 \\ 238 \\Clone the repository and build it from source: 239 \\ 240 \\```sh 241 \\git clone https://example.test/sample-repo.git 242 \\cd sample-repo 243 \\make install 244 \\``` 245 \\ 246 \\## Usage 247 \\ 248 \\Run the program with the `--help` flag to see all available 249 \\options: 250 \\ 251 \\```sh 252 \\sample-repo --help 253 \\``` 254 \\ 255 \\| Option | Description | 256 \\| ----------- | ------------------------------ | 257 \\| `--verbose` | Print extra diagnostic output | 258 \\| `--quiet` | Suppress all non-error output | 259 \\| `--version` | Print the version and exit | 260 \\ 261 \\## Contributing 262 \\ 263 \\Contributions are welcome! Please open an issue to discuss 264 \\any significant changes before submitting a pull request. 265 \\See the docs under `docs/dev` for more details. 266 \\ 267 \\## License 268 \\ 269 \\Released under the MIT License. See `LICENSE` for the full 270 \\text. 271 \\ 272 ); 273 274 try repo_dir.createDirPath(io, "docs/dev"); 275 const doc = try repo_dir.createFile(io, "docs/dev/contribute.md", .{}); 276 defer doc.close(io); 277 try doc.writeStreamingAll(io, "To contribute, please make a pull request"); 278 } 279 280 try template_repo.add(io, allocator, &.{ "README.md", "docs/dev/contribute.md" }); 281 _ = try template_repo.commit(io, allocator, .{ .message = "let there be light" }); 282 283 // tag every commit in creation order as v1, v2, and so on 284 var tag_num: usize = 1; 285 try addNextTag(&template_repo, io, allocator, &tag_num); 286 287 // a batch of commits so the commits tab has more than one page to 288 // paginate through. each rewrites a few files with scattered line 289 // edits, so every commit is a multi-file diff with several separate 290 // hunks to look at. stepped timestamps vary the date column. 291 const base_ts: u64 = 1_700_000_000; // 2023-11-14 292 const edit_files = [_][]const u8{ "src/alpha.txt", "src/beta.txt", "src/gamma.txt" }; 293 // commit subjects, cycled then padded to a varying length. 294 const subjects = [_][]const u8{ 295 "fix off-by-one in scatter loop", 296 "encode and decode buffers", 297 "tune kernel socket timeouts", 298 "render matrix vector product", 299 "gather falcon and otter stats", 300 "refactor badger walrus module", 301 "drop dead ferret marmot branch", 302 }; 303 const commit_count = 30; 304 var c: usize = 0; 305 while (c < commit_count) : (c += 1) { 306 { 307 var repo_dir = try cwd.openDir(io, template_path, .{}); 308 defer repo_dir.close(io); 309 try repo_dir.createDirPath(io, "src"); 310 for (edit_files, 0..) |path, fi| { 311 try writeScatterFile(io, allocator, repo_dir, path, fi, c); 312 } 313 } 314 try template_repo.add(io, allocator, &edit_files); 315 // a cycling subject padded to a c-varying length so the commit 316 // list shows messages of different widths, capped at 120. 317 var msg_writer = std.Io.Writer.Allocating.init(allocator); 318 defer msg_writer.deinit(); 319 try msg_writer.writer.print("{s}", .{subjects[c % subjects.len]}); 320 const msg_target = 16 + (c * 41) % 96; 321 var mw = c; 322 while (true) : (mw += 1) { 323 const word = scatter_words[mw % scatter_words.len]; 324 const len = msg_writer.written().len; 325 if (len >= msg_target or len + 1 + word.len > 120) break; 326 try msg_writer.writer.print(" {s}", .{word}); 327 } 328 // the newest commit's message runs past what the detail pane 329 // reads, so it shows the truncated message and its link. 330 if (c == commit_count - 1) { 331 var line: usize = 0; 332 while (msg_writer.written().len <= ui.Repo.Commits.max_message_size) : (line += 1) { 333 try msg_writer.writer.print("\n{d} {s}", .{ line, scatter_words[line % scatter_words.len] }); 334 } 335 } 336 const message = try arena.allocator().dupe(u8, msg_writer.written()); 337 _ = try template_repo.commit(io, allocator, .{ .message = message, .timestamp = base_ts + c * std.time.s_per_day }); 338 try addNextTag(&template_repo, io, allocator, &tag_num); 339 } 340 341 // two more branches forked off master, each adding a single commit 342 // that makes a scattered edit to one file. tag each new commit too, 343 // and switch back to master after each so the next branch also 344 // forks from master and the template ends up back on master. 345 const branch_data = [_]struct { 346 name: []const u8, 347 file: []const u8, 348 fi: usize, 349 message: []const u8, 350 rev: usize, 351 }{ 352 .{ .name = "extra", .file = "src/alpha.txt", .fi = 0, .message = "scatter alpha on the extra branch", .rev = 30 }, 353 .{ .name = "feature", .file = "src/beta.txt", .fi = 1, .message = "scatter beta on the feature branch", .rev = 31 }, 354 }; 355 for (branch_data) |b| { 356 try template_repo.addBranch(io, .{ .name = b.name }); 357 { 358 var to_branch = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = b.name } } }); 359 defer to_branch.deinit(); 360 } 361 362 { 363 var repo_dir = try cwd.openDir(io, template_path, .{}); 364 defer repo_dir.close(io); 365 try writeScatterFile(io, allocator, repo_dir, b.file, b.fi, b.rev); 366 } 367 try template_repo.add(io, allocator, &.{b.file}); 368 _ = try template_repo.commit(io, allocator, .{ .message = b.message, .timestamp = base_ts + b.rev * std.time.s_per_day }); 369 try addNextTag(&template_repo, io, allocator, &tag_num); 370 371 { 372 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 373 defer to_master.deinit(); 374 } 375 } 376 377 // seed issues so every repo's issue tracker has content 378 const issue_data = [_]struct { 379 title: []const u8, 380 description: []const u8, 381 tags: []const u8, 382 status: evt.Issue.Status = .open, 383 }{ 384 .{ 385 .title = "Crash on startup when config file is missing", 386 .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.", 387 .tags = "bug priority-high crash", 388 }, 389 .{ 390 .title = "Support dark mode in the settings panel", 391 .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.", 392 .tags = "enhancement ui theme", 393 }, 394 .{ 395 .title = "Memory leak when reconnecting after network failure", 396 .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.", 397 .tags = "bug memory networking", 398 .status = .closed, 399 }, 400 .{ 401 .title = "Document the plugin API", 402 .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.", 403 .tags = "documentation plugins", 404 }, 405 .{ 406 .title = "Slow file indexing on large directories", 407 .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.", 408 .tags = "performance indexing", 409 }, 410 .{ 411 .title = "Add keyboard shortcut for quick search", 412 .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.", 413 .tags = "enhancement ux keyboard", 414 }, 415 .{ 416 .title = "Unicode filenames are garbled in the export dialog", 417 .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.", 418 .tags = "bug unicode i18n", 419 }, 420 .{ 421 .title = "Flaky test: integration suite times out on CI", 422 .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.", 423 .tags = "bug testing ci", 424 }, 425 .{ 426 .title = "Upgrade bundled zlib to the latest release", 427 .description = "The vendored zlib is two major releases behind and misses several upstream fixes. Update the bundled copy and re-run the compression benchmarks.", 428 .tags = "dependencies maintenance", 429 }, 430 .{ 431 .title = "Progress bar overshoots 100% during resumed downloads", 432 .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.", 433 .tags = "bug ui downloads", 434 .status = .closed, 435 }, 436 .{ 437 .title = "Config parser rejects trailing commas", 438 .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.", 439 .tags = "bug config parser", 440 }, 441 .{ 442 .title = "Add a --json flag to the status command", 443 .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.", 444 .tags = "enhancement cli", 445 }, 446 .{ 447 .title = "Race condition between autosave and manual save", 448 .description = "Saving manually while an autosave is in flight can interleave the two writes and corrupt the file. Serialize saves through a single queue.", 449 .tags = "bug priority-high data-loss", 450 }, 451 .{ 452 .title = "Reduce binary size of release builds", 453 .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.", 454 .tags = "performance build", 455 }, 456 .{ 457 .title = "Tooltips flicker when the cursor moves between adjacent buttons", 458 .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.", 459 .tags = "bug ui polish", 460 }, 461 .{ 462 .title = "Support environment variable expansion in config paths", 463 .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.", 464 .tags = "enhancement config", 465 }, 466 .{ 467 .title = "Log rotation deletes the newest file instead of the oldest", 468 .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.", 469 .tags = "bug logging", 470 .status = .closed, 471 }, 472 .{ 473 .title = "Add man pages for all subcommands", 474 .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.", 475 .tags = "documentation cli", 476 }, 477 .{ 478 .title = "High CPU usage while idle in the background", 479 .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.", 480 .tags = "performance priority-high", 481 }, 482 .{ 483 .title = "Paste from clipboard drops the final newline", 484 .description = "Pasting text that ends with a newline silently trims it, which breaks pasted shell snippets. Preserve the clipboard content exactly.", 485 .tags = "bug editor clipboard", 486 }, 487 .{ 488 .title = "Improve error message for expired credentials", 489 .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.", 490 .tags = "enhancement ux auth", 491 }, 492 .{ 493 .title = "Crash when window is resized during startup animation", 494 .description = "Resizing the window while the splash animation is running dereferences a freed layout node. Cancel the animation before rebuilding the layout.", 495 .tags = "bug crash ui", 496 }, 497 .{ 498 .title = "Add integration tests for the sync engine", 499 .description = "The sync engine only has unit tests with mocked transports. Add end-to-end tests that run two instances against a real local server.", 500 .tags = "testing sync", 501 }, 502 .{ 503 .title = "Timestamps display in UTC instead of local time", 504 .description = "All timestamps in the activity feed render in UTC regardless of the system timezone. Convert to local time and include the offset in tooltips.", 505 .tags = "bug i18n time", 506 }, 507 .{ 508 .title = "Deprecate the legacy plugin format", 509 .description = "Both plugin formats are currently loaded, doubling the maintenance surface. Warn on legacy plugins this release and drop support in the next.", 510 .tags = "maintenance plugins", 511 }, 512 }; 513 514 var issue_events: [issue_data.len]evt.EventWithId = undefined; 515 for (issue_data, 0..) |issue, i| { 516 // the newest issue's description runs past what the detail 517 // pane shows, so it shows the truncated description and its 518 // link. 519 const description = if (i == issue_data.len - 1) desc: { 520 var desc_writer = std.Io.Writer.Allocating.init(allocator); 521 defer desc_writer.deinit(); 522 try desc_writer.writer.print("{s}", .{issue.description}); 523 var line: usize = 0; 524 while (desc_writer.written().len <= ui.Repo.Issues.max_description_size) : (line += 1) { 525 try desc_writer.writer.print("\n{d} {s}", .{ line, scatter_words[line % scatter_words.len] }); 526 } 527 break :desc try arena.allocator().dupe(u8, desc_writer.written()); 528 } else issue.description; 529 issue_events[i] = .{ 530 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 531 // stepped timestamps so the issues list in a stable order 532 .timestamp = @intCast(i + 1), 533 .event = .{ 534 .issue = .{ 535 .title = issue.title, 536 .description = description, 537 .tags = issue.tags, 538 .status = issue.status, 539 }, 540 }, 541 }; 542 } 543 try evt.consume(.xit, .{}, io, allocator, &template_repo, evt.events_ref, &issue_events); 544 } 545 546 // copy the template to each repo's on-disk location, named by its 547 // hex-encoded event id. the template repo is deinitialized above, so its 548 // db file is fully written before we copy it. 549 { 550 var template_dir = try cwd.openDir(io, template_path, .{ .iterate = true }); 551 defer template_dir.close(io); 552 553 for (repo_event_ids) |id_bytes| { 554 const repo_id = std.fmt.bytesToHex(id_bytes, .lower); 555 const repo_path = try std.fs.path.join(arena.allocator(), &.{ cwd_path, temp_dir_name, "server", "repos", &repo_id }); 556 var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); 557 defer dest_dir.close(io); 558 try copyDir(io, template_dir, dest_dir); 559 } 560 } 561 562 break :blk try ui.Session.init(&session_arena, &repo, .{}); 563 }; 564 session.is_terminal = true; 565 // try uses the default serve options below, so the footer's url points at 566 // the default web UI port. 567 session.web_port = try (srv.Options{}).wuiPort(); 568 569 // start the server 570 571 var cli = false; 572 573 var arg_it = try init.minimal.args.iterateAllocator(allocator); 574 defer arg_it.deinit(); 575 _ = arg_it.skip(); 576 while (arg_it.next()) |arg| { 577 if (std.mem.eql(u8, "--cli", arg)) { 578 cli = true; 579 } 580 } 581 582 const server_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server" }); 583 defer allocator.free(server_path); 584 585 // let the native TUI's page builders open the on-disk repos for the file tree 586 session.io = io; 587 session.repos_dir = try std.fs.path.join(session_arena.allocator(), &.{ server_path, "repos" }); 588 589 if (cli) { 590 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 591 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 592 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 593 594 const key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 595 defer allocator.free(key_path); 596 597 const Runnable = struct { 598 io: std.Io, 599 key_path: []const u8, 600 601 pub fn run(self: @This()) !void { 602 std.debug.print( 603 \\ 604 \\connect to the TUI with: 605 \\ ssh -p 8022 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 606 \\ 607 \\create a git repo and push over SSH: 608 \\ mkdir -p temp-try/client/test 609 \\ cd temp-try/client/test 610 \\ git init 611 \\ echo "hello" > hello.txt 612 \\ git add hello.txt 613 \\ git commit -m "let there be light" 614 \\ GIT_SSH_COMMAND='ssh -p 8022 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -i {s}' git push localhost:admin/test HEAD:master 615 \\ 616 \\to quit, press enter. 617 \\ 618 , .{self.key_path}); 619 // portable stdin read via std.Io — equivalent to the 620 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 621 // on windows. 622 var buf: [1]u8 = undefined; 623 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 624 _ = stdin_reader.interface.takeByte() catch {}; 625 } 626 }; 627 628 try srv.run(.xit, .{}, io, allocator, cwd_path, .{ 629 .data_dir = server_path, 630 }, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 631 } else { 632 var null_writer = std.Io.Writer.Discarding.init(&.{}); 633 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 634 635 const Runnable = struct { 636 io: std.Io, 637 allocator: std.mem.Allocator, 638 session: *ui.Session, 639 repo: *Repo, 640 641 pub fn run(self: @This()) !void { 642 // launch the TUI 643 try hx.ui.run(self.io, self.allocator, self.session, self.repo); 644 } 645 }; 646 647 try srv.run(.xit, .{}, io, allocator, cwd_path, .{ 648 .data_dir = server_path, 649 }, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session, .repo = &repo }); 650 } 651 } 652 653 // words cycled through to pad generated lines into prose-like content 654 const scatter_words = [_][]const u8{ 655 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", 656 "quantum", "vector", "matrix", "buffer", "kernel", "socket", 657 "falcon", "otter", "badger", "walrus", "ferret", "marmot", 658 "scatter", "gather", "encode", "decode", "render", "commit", 659 }; 660 661 // write `path` as 40 lines of mostly-stable filler. revision `c` and file 662 // index `fi` shift which lines change and the words used, so each (c, fi) 663 // yields a distinct multi-hunk diff against the previous revision. untouched 664 // lines stay byte-identical across revisions, so a bump in `c` is a scatter of 665 // small hunks rather than a full rewrite. 666 fn writeScatterFile(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir, path: []const u8, fi: usize, c: usize) !void { 667 const file = try repo_dir.createFile(io, path, .{}); 668 defer file.close(io); 669 var writer = std.Io.Writer.Allocating.init(allocator); 670 defer writer.deinit(); 671 for (0..40) |line| { 672 const start = writer.written().len; 673 if ((line + fi + c) % 8 == 0) { 674 const tag = scatter_words[(line + fi + c * 3) % scatter_words.len]; 675 try writer.writer.print("rev {d} {s}", .{ c, tag }); 676 } 677 const target = 40 + (line * 17 + fi * 23) % 70; 678 var w = line + fi; 679 while (true) : (w += 1) { 680 const word = scatter_words[w % scatter_words.len]; 681 const len = writer.written().len - start; 682 if (len >= target or len + 1 + word.len > 120) break; 683 if (len == 0) 684 try writer.writer.print("{s}", .{word}) 685 else 686 try writer.writer.print(" {s}", .{word}); 687 } 688 try writer.writer.writeByte('\n'); 689 } 690 try file.writeStreamingAll(io, writer.written()); 691 } 692 693 // tag the current HEAD as the next sequential version: v1, v2, and so on 694 fn addNextTag(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { 695 var buf: [16]u8 = undefined; 696 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); 697 _ = try repo.addTag(io, allocator, .{ .name = name }); 698 n.* += 1; 699 } 700 701 // recursively copy the contents of src_dir into dest_dir 702 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 703 var iter = src_dir.iterate(); 704 while (try iter.next(io)) |entry| { 705 switch (entry.kind) { 706 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 707 .directory => { 708 try dest_dir.createDirPath(io, entry.name); 709 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 710 defer dest_entry_dir.close(io); 711 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 712 defer src_entry_dir.close(io); 713 try copyDir(io, src_entry_dir, dest_entry_dir); 714 }, 715 else => {}, 716 } 717 } 718 } 719