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