╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
260 } 261 262 // each user's repo holds the repos they own and the grants on them 263 for (user_ids, 0..) |user_id, user_index| { 264 const user_repo_path = try evt.userRepoPath(arena.allocator(), users_dir, &user_id); 265 var user_repo = try evt.initUserRepo(io, allocator, user_repo_path); 266 defer user_repo.deinit(io, allocator); 267 var owned: std.ArrayList(evt.EventWithId) = .empty; 268 for (repo_data, repo_events, grant_events) |r, repo_event, grant_event| { 269 if (r.user_index != user_index) continue; 270 try owned.append(arena.allocator(), repo_event); 271 try owned.append(arena.allocator(), grant_event); 272 } 273 if (owned.items.len > 0) try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .user, .xit, evt.user_repo_opts, io, allocator, &user_repo, evt.events_ref, owned.items); 274 } 275 276 // the creation timestamps order the repos tabs 277 for (repo_data, &repo_event_ids, repo_events) |r, *id_bytes, event| { 278 try evt.bumpRepoActivity(io, allocator, users_dir, &user_ids[r.user_index], id_bytes, event.timestamp); 279 } 280 281 // every repo gets the same generated history, so build it once into a 282 // template repo and copy that to each repo's location below rather than 283 // redoing the expensive commit work for every repo. 284 const template_path = try std.Io.Dir.path.join(arena.allocator(), &.{ cwd_path, temp_dir_name, "template" }); 285 { 286 var template_repo = try rp.Repo(.xit, .{}).init(io, allocator, .{ .path = template_path }); 287 defer template_repo.deinit(io, allocator); 288 289 try template_repo.setMergeAlgorithm(io, allocator, .patch); 290 try template_repo.addConfig(io, allocator, .{ .name = "user.name", .value = "haxy" }); 291 try template_repo.addConfig(io, allocator, .{ .name = "user.email", .value = "admin@example.test" }); 292 293 // a README plus a nested doc so the file tree has a directory to 294 // descend into 295 { 296 var repo_dir = try cwd.openDir(io, template_path, .{}); 297 defer repo_dir.close(io); 298 299 const readme = try repo_dir.createFile(io, "README.md", .{}); 300 defer readme.close(io); 301 try readme.writeStreamingAll(io, 302 \\# Sample Repo 303 \\ 304 \\A repository seeded with test data for exercising the UI. Text can be 305 \\*italic*, **bold**, ***both*** or ~~struck through~~, and links go to 306 \\[the contributing guide](docs/dev/contribute.md) or to 307 \\[an external site](https://github.com/xit-vcs/haxy). 308 \\ 309 \\## Features 310 \\ 311 \\- Fast and lightweight 312 \\- Zero configuration required 313 \\ - Sensible defaults 314 \\ - Nothing to edit 315 \\- Works out of the box 316 \\ 317 \\## Roadmap 318 \\ 319 \\- [x] Render markdown 320 \\- [ ] Render tables 321 \\ 322 \\> Simplicity is prerequisite for reliability. A quote this long wraps 323 \\> onto more than one line in a narrow pane. 324 \\ 325 \\--- 326 \\ 327 \\## Installation 328 \\ 329 \\Clone the repository and build it from source: 330 \\ 331 \\```sh 332 \\git clone https://example.test/sample-repo.git 333 \\cd sample-repo 334 \\make install 335 \\``` 336 \\ 337 \\## Usage 338 \\ 339 \\Run the program with the `--help` flag to see all available 340 \\options: 341 \\ 342 \\```sh 343 \\sample-repo --help 344 \\``` 345 \\ 346 \\| Option | Description | 347 \\| ----------- | ------------------------------ | 348 \\| `--verbose` | Print extra diagnostic output | 349 \\| `--quiet` | Suppress all non-error output | 350 \\| `--version` | Print the version and exit | 351 \\ 352 \\## Contributing 353 \\ 354 \\Contributions are welcome! Please open an issue to discuss 355 \\any significant changes before submitting a pull request. 356 \\See the docs under `docs/dev` for more details. 357 \\ 358 \\## A Heading Too Long To Fit On One Line In The Subtitle Font 359 \\ 360 \\Long headings wrap onto more lines in their font. 361 \\ 362 \\## License 363 \\ 364 \\Released under the MIT License. See `LICENSE` for the full 365 \\text. 366 \\ 367 ); 368 369 try repo_dir.createDirPath(io, "docs/dev"); 370 const doc = try repo_dir.createFile(io, "docs/dev/contribute.md", .{}); 371 defer doc.close(io); 372 try doc.writeStreamingAll(io, "To contribute, please make a pull request"); 373 } 374 375 try template_repo.add(io, allocator, &.{ "README.md", "docs/dev/contribute.md" }); 376 _ = try template_repo.commit(io, allocator, .{ .message = "let there be light" }); 377 378 // tag every commit in creation order, three versions at a time 379 var tag_num: usize = 1; 380 try addNextTags(&template_repo, io, allocator, &tag_num); 381 382 // a batch of commits so the commits tab has more than one page to 383 // paginate through. each rewrites a few files with scattered line 384 // edits, so every commit is a multi-file diff with several separate 385 // hunks to look at. stepped timestamps vary the date column. 386 const base_ts: u64 = 1_700_000_000; // 2023-11-14 387 const edit_files = [_][]const u8{ "src/alpha.txt", "src/beta.txt", "src/gamma.txt" }; 388 // commit subjects, cycled then padded to a varying length. 389 const subjects = [_][]const u8{ 390 "fix off-by-one in scatter loop", 391 "encode and decode buffers", 392 "tune kernel socket timeouts", 393 "render matrix vector product", 394 "gather falcon and otter stats", 395 "refactor badger walrus module", 396 "drop dead ferret marmot branch", 397 }; 398 const commit_count = 30; 399 var c: usize = 0; 400 while (c < commit_count) : (c += 1) { 401 { 402 var repo_dir = try cwd.openDir(io, template_path, .{}); 403 defer repo_dir.close(io); 404 try repo_dir.createDirPath(io, "src"); 405 for (edit_files, 0..) |path, fi| { 406 try writeScatterFile(io, allocator, repo_dir, path, fi, c); 407 } 408 } 409 try template_repo.add(io, allocator, &edit_files); 410 // a cycling subject padded to a c-varying length so the commit 411 // list shows messages of different widths, capped at 120. 412 var msg_writer = std.Io.Writer.Allocating.init(allocator); 413 defer msg_writer.deinit(); 414 try msg_writer.writer.print("{s}", .{subjects[c % subjects.len]}); 415 const msg_target = 16 + (c * 41) % 96; 416 var mw = c; 417 while (true) : (mw += 1) { 418 const word = scatter_words[mw % scatter_words.len]; 419 const len = msg_writer.written().len; 420 if (len >= msg_target or len + 1 + word.len > 120) break; 421 try msg_writer.writer.print(" {s}", .{word}); 422 } 423 // the newest commit's message links to the project and runs 424 // past what the detail pane reads, so it shows the truncated 425 // message and its link. 426 if (c == commit_count - 1) { 427 try msg_writer.writer.writeAll("\n\nsee https://github.com/xit-vcs/haxy"); 428 for (0..ui.detail_preview_lines) |line| { 429 try msg_writer.writer.print("\n{d} {s}", .{ line, scatter_words[line % scatter_words.len] }); 430 } 431 } 432 const message = try arena.allocator().dupe(u8, msg_writer.written()); 433 // the newest commit was committed by someone other than its 434 // author, so the detail pane shows a committer box. 435 const committer: ?[]const u8 = if (c == commit_count - 1) "alice <alice@example.test>" else null; 436 _ = try template_repo.commit(io, allocator, .{ .message = message, .committer = committer, .timestamp = base_ts + c * std.time.s_per_day }); 437 try addNextTags(&template_repo, io, allocator, &tag_num); 438 } 439 440 // two more branches forked off master, each adding a single commit 441 // that makes a scattered edit to one file. tag each new commit too, 442 // and switch back to master after each so the next branch also 443 // forks from master and the template ends up back on master. 444 const branch_data = [_]struct { 445 name: []const u8, 446 file: []const u8, 447 fi: usize, 448 message: []const u8, 449 rev: usize, 450 }{ 451 .{ .name = "extra", .file = "src/alpha.txt", .fi = 0, .message = "scatter alpha on the extra branch", .rev = 30 }, 452 .{ .name = "feature", .file = "src/beta.txt", .fi = 1, .message = "scatter beta on the feature branch", .rev = 31 }, 453 }; 454 for (branch_data) |b| { 455 try template_repo.addBranch(io, .{ .name = b.name }); 456 { 457 var to_branch = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = b.name } } }); 458 defer to_branch.deinit(); 459 } 460 461 { 462 var repo_dir = try cwd.openDir(io, template_path, .{}); 463 defer repo_dir.close(io); 464 try writeScatterFile(io, allocator, repo_dir, b.file, b.fi, b.rev); 465 } 466 try template_repo.add(io, allocator, &.{b.file}); 467 _ = try template_repo.commit(io, allocator, .{ .message = b.message, .timestamp = base_ts + b.rev * std.time.s_per_day }); 468 try addNextTags(&template_repo, io, allocator, &tag_num); 469 470 { 471 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 472 defer to_master.deinit(); 473 } 474 } 475 476 // the timestamps issue's description, in paragraphs, so the 477 // seeded conflict below can insert and delete among them 478 const tz_p1 = "All timestamps in the activity feed render in `UTC` regardless of the system timezone."; 479 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."; 480 const tz_p3 = "A quick survey of [other clients](https://github.com/xit-vcs/haxy) shows every one of them rendering local time by default."; 481 const tz_p4 = "Convert to local time and include the **offset** in tooltips."; 482 483 // the sync issue's description, in paragraphs, so each side of its 484 // seeded conflict can rework a different one 485 const sync_p1 = "The sync engine only has *unit tests* with mocked transports."; 486 const sync_p2 = "Failures seen in production involve reconnects and **partial writes**, which the mocks can't reproduce."; 487 const sync_p3 = "Add end-to-end tests that run two instances against a real local server."; 488 489 // seed issues so every repo's issue tracker has content 490 const issue_data = [_]struct { 491 title: []const u8, 492 description: []const u8, 493 labels: []const u8, 494 status: evt.Issue.Status = .open, 495 }{ 496 .{ 497 .title = "Crash on startup when config file is missing", 498 .description = "Running the program without a `config.toml` present dereferences a null pointer and segfaults. Fall back to the built-in defaults and log a **warning** instead.", 499 .labels = "bug priority-high crash", 500 }, 501 .{ 502 .title = "Support dark mode in the settings panel", 503 .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.", 504 .labels = "enhancement ui theme", 505 }, 506 .{ 507 .title = "Memory leak when reconnecting after network failure", 508 .description = "Each reconnect allocates a new connection state without freeing the previous one.\n\n> After a flaky network session, memory usage grows by several megabytes per hour.", 509 .labels = "bug memory networking", 510 .status = .closed, 511 }, 512 .{ 513 .title = "Document the plugin API", 514 .description = "The plugin interface has no documentation beyond the header comments. Add a guide covering:\n\n- the lifecycle hooks\n- the event callbacks\n- a minimal working plugin", 515 .labels = "documentation plugins", 516 }, 517 .{ 518 .title = "Slow file indexing on large directories", 519 .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.", 520 .labels = "performance indexing", 521 }, 522 .{ 523 .title = "Add keyboard shortcut for quick search", 524 .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.", 525 .labels = "enhancement ux keyboard", 526 }, 527 .{ 528 .title = "Unicode filenames are garbled in the export dialog", 529 .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*.", 530 .labels = "bug unicode i18n", 531 }, 532 .{ 533 .title = "Flaky test: integration suite times out on CI", 534 .description = "The integration suite intermittently exceeds the CI time limit because the server fixture waits for a fixed 30 seconds:\n\n```\nstd.Io.sleep(io, .fromSeconds(30), .awake);\n```\n\nPoll for readiness instead of sleeping.", 535 .labels = "bug testing ci", 536 }, 537 .{ 538 .title = "Upgrade bundled zlib to the latest release", 539 .description = "The vendored zlib is two major releases behind and misses several upstream fixes.\n\n- [ ] update the bundled copy\n- [ ] re-run the compression benchmarks", 540 .labels = "dependencies maintenance", 541 }, 542 .{ 543 .title = "Progress bar overshoots 100% during resumed downloads", 544 .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.", 545 .labels = "bug ui downloads", 546 .status = .closed, 547 }, 548 .{ 549 .title = "Config parser rejects trailing commas", 550 .description = "A trailing comma after the last entry in a config block is reported as a syntax error:\n\n```\n[server]\nport = 8080,\n```\n\nMost editors add one automatically, so accept it.", 551 .labels = "bug config parser", 552 }, 553 .{ 554 .title = "Add a --json flag to the status command", 555 .description = "Scripts currently scrape the human-readable `status` output, which breaks whenever the format changes. Emit a stable machine-readable JSON form behind a `--json` flag.", 556 .labels = "enhancement cli", 557 }, 558 .{ 559 .title = "Race condition between autosave and manual save", 560 .description = "Saving manually while an autosave is in flight can interleave the two writes and **corrupt the file**. Serialize saves through a single queue.", 561 .labels = "bug priority-high data-loss", 562 }, 563 .{ 564 .title = "Reduce binary size of release builds", 565 .description = "The release binary has grown past 40 MB, mostly from:\n\n1. debug info\n2. an unused bundled font\n\nStrip symbols and drop the font from the default build.", 566 .labels = "performance build", 567 }, 568 .{ 569 .title = "Tooltips flicker when the cursor moves between adjacent buttons", 570 .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.", 571 .labels = "bug ui polish", 572 }, 573 .{ 574 .title = "Support environment variable expansion in config paths", 575 .description = "Paths in the config file are taken literally, so shared configs can't refer to `$HOME` portably. Expand environment variables when loading.", 576 .labels = "enhancement config", 577 }, 578 .{ 579 .title = "Log rotation deletes the newest file instead of the oldest", 580 .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.", 581 .labels = "bug logging", 582 .status = .closed, 583 }, 584 .{ 585 .title = "Add man pages for all subcommands", 586 .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.", 587 .labels = "documentation cli", 588 }, 589 .{ 590 .title = "High CPU usage while idle in the background", 591 .description = "The main loop polls for file changes every 10 ms even when no window is visible.\n\n> Switch to native file watching and idle at zero CPU.", 592 .labels = "performance priority-high", 593 }, 594 .{ 595 .title = "Paste from clipboard drops the final newline", 596 .description = "Pasting text that ends with a newline silently trims it, which breaks pasted shell snippets. Preserve the clipboard content *exactly*.", 597 .labels = "bug editor clipboard", 598 }, 599 .{ 600 .title = "Improve error message for expired credentials", 601 .description = "## Problem\n\nAn expired token currently surfaces as a bare `401` with no guidance.\n\n## Fix\n\nDetect the expiry case and tell the user how to re-authenticate, linking to the [setup guide](https://github.com/xit-vcs/haxy).", 602 .labels = "enhancement ux auth", 603 }, 604 .{ 605 .title = "Crash when window is resized during startup animation", 606 .description = "Resizing the window while the splash animation is running dereferences a **freed** layout node. Cancel the animation before rebuilding the layout.", 607 .labels = "bug crash ui", 608 }, 609 .{ 610 .title = "Add integration tests for the sync engine", 611 .description = sync_p1 ++ "\n\n" ++ sync_p2 ++ "\n\n" ++ sync_p3, 612 .labels = "testing sync", 613 }, 614 .{ 615 .title = "Timestamps display in UTC instead of local time", 616 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p3 ++ "\n\n" ++ tz_p4, 617 .labels = "bug i18n time", 618 }, 619 .{ 620 .title = "Deprecate the legacy plugin format", 621 .description = "Both plugin formats are currently loaded, doubling the maintenance surface.\n\n- [ ] warn on legacy plugins this release\n- [ ] drop support in the next", 622 .labels = "maintenance plugins", 623 }, 624 }; 625 626 var issue_events: [issue_data.len]evt.EventWithId = undefined; 627 for (issue_data, 0..) |issue, i| { 628 // the fifth-newest issue's description runs past what the detail 629 // pane shows, so it shows the truncated description and its 630 // link. 631 const description = if (i == issue_data.len - 5) 632 try longDescription(arena.allocator(), issue.description) 633 else 634 issue.description; 635 issue_events[i] = .{ 636 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 637 // stepped timestamps so the issues list in a stable order 638 .timestamp = @intCast(i + 1), 639 .author = .{ .name = user_data[i % user_data.len].name, .email = user_data[i % user_data.len].email }, 640 .event = .{ 641 .issue = .{ 642 .title = issue.title, 643 .description = description, 644 .labels = issue.labels, 645 .status = issue.status, 646 }, 647 }, 648 }; 649 } 650 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &issue_events); 651 // the tip is the branch point for the conflicting edits below 652 const seed_tip = (try template_repo.readRef(io, evt.events_ref)) orelse unreachable; 653 654 // two divergent edits per conflicted issue: ours on the events 655 // branch, theirs on a temp branch rooted at the seed tip, then a 656 // merge. the 4th-newest issue conflicts on title and labels; the 657 // 3rd-newest on its description with every hunk auto-resolving 658 // (each side reworks a different paragraph); the 2nd-newest on 659 // its description with a removal conflict (ours removes a 660 // paragraph theirs rewords) and an insertion conflict (each side 661 // appends a different closing paragraph). 662 { 663 const other_ref: xit.ref.Ref = .{ .kind = .head, .name = "haxy/other" }; 664 const title_issue = issue_data[issue_data.len - 4]; 665 const sync_issue = issue_data[issue_data.len - 3]; 666 const desc_issue = issue_data[issue_data.len - 2]; 667 668 const ours = [_]evt.EventWithId{ .{ 669 .id = issue_events[issue_data.len - 4].id, 670 .timestamp = 100, 671 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 672 .event = .{ .issue = .{ 673 .title = "Crash when resizing the window during the splash animation", 674 .description = title_issue.description, 675 .labels = "bug crash ui priority-high", 676 } }, 677 }, .{ 678 .id = issue_events[issue_data.len - 3].id, 679 .timestamp = 101, 680 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 681 .event = .{ .issue = .{ 682 .title = sync_issue.title, 683 .description = "The sync engine's coverage is *unit tests* only, with every transport mocked out." ++ "\n\n" ++ 684 sync_p2 ++ "\n\n" ++ sync_p3, 685 .labels = sync_issue.labels, 686 } }, 687 }, .{ 688 .id = issue_events[issue_data.len - 2].id, 689 .timestamp = 102, 690 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 691 .event = .{ .issue = .{ 692 .title = desc_issue.title, 693 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p4 ++ "\n\n" ++ 694 "> The confusion is worst for teams spread across timezones, who each read a different wall-clock time from the same feed.", 695 .labels = desc_issue.labels, 696 } }, 697 } }; 698 const theirs = [_]evt.EventWithId{ .{ 699 .id = issue_events[issue_data.len - 4].id, 700 .timestamp = 103, 701 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 702 .event = .{ .issue = .{ 703 .title = "Segfault on early window resize", 704 .description = title_issue.description, 705 .labels = "bug crash rendering", 706 } }, 707 }, .{ 708 .id = issue_events[issue_data.len - 3].id, 709 .timestamp = 104, 710 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 711 .event = .{ .issue = .{ 712 .title = sync_issue.title, 713 .description = sync_p1 ++ "\n\n" ++ sync_p2 ++ "\n\n" ++ 714 "Add end-to-end tests that drive two live instances against a local server on **every** CI run.", 715 .labels = sync_issue.labels, 716 } }, 717 }, .{ 718 .id = issue_events[issue_data.len - 2].id, 719 .timestamp = 105, 720 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 721 .event = .{ .issue = .{ 722 .title = desc_issue.title, 723 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ 724 "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" ++ 725 "Log exports inherit the same UTC rendering, so downstream tooling has to *guess* the source timezone.", 726 .labels = desc_issue.labels, 727 } }, 728 } }; 729 730 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &ours); 731 732 try commitEventsAtRef(io, allocator, &template_repo, other_ref, &theirs, seed_tip); 733 734 { 735 var to_events = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 736 defer to_events.deinit(); 737 } 738 { 739 var merge = try template_repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, null); 740 defer merge.deinit(); 741 if (merge.result != .success) return error.MergeFailed; 742 } 743 { 744 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 745 defer to_master.deinit(); 746 } 747 try template_repo.removeBranch(io, .{ .name = other_ref.name }); 748 } 749 750 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &.{}); 751 752 // seed a small comment tree on the newest issue 753 var comment_ids: [5][evt.event_id_size]u8 = undefined; 754 for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 755 756 const comment_bodies = [_][]const u8{ 757 "I can take this. The compatibility scanner already reports which repositories still use the **legacy** format.", 758 "Can we keep loading legacy plugins for *one release* after the warning lands? That would give downstream maintainers time to migrate.", 759 "That sounds good. I'll include the scanner output in the warning so each affected plugin is named:\n\n```\nwarning: plugins/foo.so uses the legacy format\n```", 760 "Please include the replacement manifest path too. That should make the warning actionable without opening the [migration guide](https://github.com/xit-vcs/haxy).", 761 "I'll add release-note text once the warning format is settled:\n\n- [x] draft the summary\n- [ ] list the affected plugins", 762 }; 763 764 var comment_events: [comment_ids.len]evt.EventWithId = undefined; 765 for (&comment_events, 0..) |*event, i| { 766 event.* = .{ 767 .id = std.fmt.bytesToHex(comment_ids[i], .lower), 768 .timestamp = @intCast(200 + i), 769 .author = .{ .name = user_data[(i + 1) % user_data.len].name, .email = user_data[(i + 1) % user_data.len].email }, 770 .event = .{ .comment = .{ 771 .thread_id = issue_events[issue_events.len - 1].id, 772 .parent_id = switch (i) { 773 0, 1 => issue_events[issue_events.len - 1].id, 774 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), 775 3 => std.fmt.bytesToHex(comment_ids[2], .lower), 776 else => unreachable, 777 }, 778 .body = comment_bodies[i], 779 } }, 780 }; 781 } 782 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &comment_events); 783 784 const discussion_data = [_]struct { 785 title: []const u8, 786 description: []const u8, 787 labels: []const u8, 788 }{ 789 .{ 790 .title = "How should plugins declare capabilities?", 791 .description = "I'd like the manifest to make **privileged** capabilities explicit without making simple plugins verbose.", 792 .labels = "plugins design", 793 }, 794 .{ 795 .title = "Ideas for making large repositories faster", 796 .description = "This is a place to collect *profiling results* and discuss which indexing work is worth pursuing first.", 797 .labels = "performance indexing", 798 }, 799 .{ 800 .title = "What should the next release focus on?", 801 .description = "Let's compare the most important reliability fixes with the larger features already in progress, starting from https://github.com/xit-vcs/haxy.", 802 .labels = "release planning", 803 }, 804 .{ 805 .title = "Improving keyboard navigation", 806 .description = "Share workflows that still **require a mouse** and suggestions for making their focus behavior predictable.", 807 .labels = "ui keyboard accessibility", 808 }, 809 .{ 810 .title = "Configuration format discussion", 811 .description = "Should the next configuration format favor *strict validation* or accept common conveniences such as trailing commas?", 812 .labels = "config design", 813 }, 814 }; 815 816 var discussion_events: [discussion_data.len]evt.EventWithId = undefined; 817 for (discussion_data, 0..) |discussion, i| { 818 discussion_events[i] = .{ 819 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 820 .timestamp = @intCast(300 + i), 821 .author = .{ .name = user_data[i % user_data.len].name, .email = user_data[i % user_data.len].email }, 822 .event = .{ .discuss = .{ 823 .title = discussion.title, 824 .description = discussion.description, 825 .labels = discussion.labels, 826 } }, 827 }; 828 } 829 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &discussion_events); 830 831 var discussion_comment_ids: [discussion_data.len][3][evt.event_id_size]u8 = undefined; 832 for (&discussion_comment_ids) |*ids| { 833 for (ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 834 } 835 var discussion_comment_events: [discussion_data.len * 3]evt.EventWithId = undefined; 836 for (&discussion_comment_events, 0..) |*event, i| { 837 const discussion_index = i / 3; 838 const comment_index = i % 3; 839 event.* = .{ 840 .id = std.fmt.bytesToHex(discussion_comment_ids[discussion_index][comment_index], .lower), 841 .timestamp = @intCast(400 + i), 842 .author = .{ .name = user_data[(i + 1) % user_data.len].name, .email = user_data[(i + 1) % user_data.len].email }, 843 .event = .{ .comment = .{ 844 .thread_id = discussion_events[discussion_index].id, 845 .parent_id = if (comment_index < 2) 846 discussion_events[discussion_index].id 847 else 848 std.fmt.bytesToHex(discussion_comment_ids[discussion_index][0], .lower), 849 .body = switch (comment_index) { 850 0 => "My preference is to start with the **smallest useful version** and expand it after we have real usage to learn from.", 851 1 => "It would help to *write down* the tradeoffs we are accepting so the decision is easy to revisit later.", 852 2 => "> A narrow first version\n\nAgreed. It should also make it easier to keep the implementation understandable.", 853 else => unreachable, 854 }, 855 } }, 856 }; 857 } 858 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &discussion_comment_events); 859 try template_repo.patchAll(io, allocator, null); 860 try template_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "true" }); 861 try transaction(io, &template_repo, Reindex{ .io = io, .allocator = allocator, .core = &template_repo.core, .commits = true }); 862 // clear history only after indexing has written its transactions. 863 { 864 const moment = try evt.currentMoment(.{}, &template_repo); 865 const roots = try evt.PatchRev.gcRoots(rp.Repo(.xit, .{}).DB, .sha1, allocator, moment); 866 defer allocator.free(roots); 867 _ = try template_repo.garbageCollect(io, allocator, .{ .extra_roots = roots }); 868 } 869 } 870 871 // copy the template to each repo's on-disk location, named by its 872 // hex-encoded event id. the template repo is deinitialized above, so its 873 // db file is fully written before we copy it. 874 { 875 var template_dir = try cwd.openDir(io, template_path, .{}); 876 defer template_dir.close(io); 877 var template_repo_dir = try template_dir.openDir(io, ".xit", .{ .iterate = true }); 878 defer template_repo_dir.close(io); 879 880 for (repo_event_ids, 0..) |id_bytes, repo_index| { 881 const repo_path = try evt.repoPath(arena.allocator(), users_dir, &user_ids[repo_data[repo_index].user_index], &id_bytes); 882 { 883 var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); 884 defer dest_dir.close(io); 885 var dest_repo_dir = try dest_dir.createDirPathOpen(io, ".xit", .{}); 886 defer dest_repo_dir.close(io); 887 try copyDir(io, template_repo_dir, dest_repo_dir); 888 } 889 890 if (repo_index + 1 == repo_data.len) { 891 var target_repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = repo_path, .require_repo_root = true }); 892 defer target_repo.deinit(io, allocator); 893 try seedPatches(io, allocator, server_path, &repo, &target_repo, &repo_event_ids[repo_index], &user_ids[repo_data[repo_index].user_index], &admin_user_id, prng.random()); 894 } 895 } 896 } 897 898 break :blk try ui.Session.init(&session_arena, &repo, .{ .user_id = &admin_user_id }); 899 }; 900 session.is_terminal = true; 901 session.color = std.mem.eql(u8, init.environ_map.get("NO_COLOR") orelse "", ""); 902 session.data.git_ssh_prefix = git_ssh_prefix; 903 904 // start the server 905 906 // let the native TUI's page builders open the on-disk repos for the file tree 907 session.io = io; 908 session.users_dir = try std.Io.Dir.path.join(session_arena.allocator(), &.{ server_path, "users" }); 909 910 // leave a one-shot session for the first browser to hit the web ui, so it 911 // starts logged in as admin 912 { 913 var data_dir = try cwd.createDirPathOpen(io, server_path, .{}); 914 defer data_dir.close(io); 915 const store = try hx.web.SessionStore.init(io, data_dir); 916 defer store.deinit(); 917 const token = try store.create(&admin_user_id); 918 try store.offerAutoLogin(&token); 919 } 920 921 if (cli) { 922 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 923 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 924 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 925 926 const Runnable = struct { 927 io: std.Io, 928 key_path: []const u8, 929 930 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port_maybe: ?u16) !void { 931 _ = web_port; 932 _ = http_port; 933 const ssh_port = ssh_port_maybe orelse return error.SshDisabled; 934 std.debug.print( 935 \\ 936 \\connect to the TUI with: 937 \\ ssh -p {d} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 938 \\ 939 \\create a git repo and push over SSH: 940 \\ mkdir -p temp-try/client/test 941 \\ cd temp-try/client/test 942 \\ git init 943 \\ echo "hello" > hello.txt 944 \\ git add hello.txt 945 \\ git commit -m "let there be light" 946 \\ 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 947 \\ 948 \\to quit, press enter. 949 \\ 950 , .{ ssh_port, ssh_port, self.key_path }); 951 // portable stdin read via std.Io — equivalent to the 952 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 953 // on windows. 954 var buf: [1]u8 = undefined; 955 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 956 _ = stdin_reader.interface.takeByte() catch {}; 957 } 958 }; 959 960 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 961 } else { 962 var null_writer = std.Io.Writer.Discarding.init(&.{}); 963 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 964 965 const Runnable = struct { 966 io: std.Io, 967 allocator: std.mem.Allocator, 968 session: *ui.Session, 969 repo: *Repo, 970 971 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port: ?u16) !void { 972 // launch the TUI; the footer points at whatever port was bound 973 self.session.web_port = web_port; 974 self.session.data.git_http_port = http_port; 975 self.session.data.git_ssh_port = ssh_port; 976 try hx.ui.run(self.io, self.allocator, self.session, self.repo); 977 } 978 }; 979 980 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session, .repo = &repo }); 981 } 982 } 983 984 // words cycled through to pad generated lines into prose-like content 985 const scatter_words = [_][]const u8{ 986 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", 987 "quantum", "vector", "matrix", "buffer", "kernel", "socket", 988 "falcon", "otter", "badger", "walrus", "ferret", "marmot", 989 "scatter", "gather", "encode", "decode", "render", "commit", 990 }; 991 992 fn longDescription(allocator: std.mem.Allocator, initial: []const u8) ![]u8 { 993 var writer = std.Io.Writer.Allocating.init(allocator); 994 defer writer.deinit(); 995 try writer.writer.print("{s}\n", .{initial}); 996 for (0..ui.detail_preview_lines) |line| 997 try writer.writer.print("\n- {d} {s}", .{ line, scatter_words[line % scatter_words.len] }); 998 return writer.toOwnedSlice(); 999 } 1000 1001 // write `path` as 40 lines of mostly-stable filler. revision `c` and file 1002 // index `fi` shift which lines change and the words used, so each (c, fi) 1003 // yields a distinct multi-hunk diff against the previous revision. untouched 1004 // lines stay byte-identical across revisions, so a bump in `c` is a scatter of 1005 // small hunks rather than a full rewrite. 1006 fn writeScatterFile(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir, path: []const u8, fi: usize, c: usize) !void { 1007 const content = try scatterContent(allocator, fi, c, null); 1008 defer allocator.free(content); 1009 const file = try repo_dir.createFile(io, path, .{}); 1010 defer file.close(io); 1011 try file.writeStreamingAll(io, content); 1012 } 1013 1014 const ScatterEdit = struct { line: usize, text: []const u8 }; 1015 1016 fn scatterContent(allocator: std.mem.Allocator, fi: usize, c: usize, replacement: ?ScatterEdit) ![]u8 { 1017 var writer = std.Io.Writer.Allocating.init(allocator); 1018 defer writer.deinit(); 1019 for (0..40) |line| { 1020 if (replacement) |edit| if (line == edit.line) { 1021 try writer.writer.print("{s}\n", .{edit.text}); 1022 continue; 1023 }; 1024 const start = writer.written().len; 1025 if ((line + fi + c) % 8 == 0) { 1026 const tag = scatter_words[(line + fi + c * 3) % scatter_words.len]; 1027 try writer.writer.print("rev {d} {s}", .{ c, tag }); 1028 } 1029 const target = 40 + (line * 17 + fi * 23) % 70; 1030 var w = line + fi; 1031 while (true) : (w += 1) { 1032 const word = scatter_words[w % scatter_words.len]; 1033 const len = writer.written().len - start; 1034 if (len >= target or len + 1 + word.len > 120) break; 1035 if (len == 0) 1036 try writer.writer.print("{s}", .{word}) 1037 else 1038 try writer.writer.print(" {s}", .{word}); 1039 } 1040 try writer.writer.writeByte('\n'); 1041 } 1042 return writer.toOwnedSlice(); 1043 } 1044 1045 // tag the current HEAD with the next three sequential versions, so the refs 1046 // tab has enough tags to page through 1047 fn addNextTags(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { 1048 for (0..3) |_| { 1049 var buf: [16]u8 = undefined; 1050 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); 1051 _ = try repo.addTag(io, allocator, .{ .name = name }); 1052 n.* += 1; 1053 } 1054 } 1055 1056 fn commitTree( 1057 io: std.Io, 1058 allocator: std.mem.Allocator, 1059 repo: *rp.Repo(.xit, .{}), 1060 oid: *const [hash.hexLen(.sha1)]u8, 1061 ) ![hash.hexLen(.sha1)]u8 { 1062 var moment = try repo.core.latestMoment(); 1063 const state = rp.Repo(.xit, .{}).State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; 1064 var object = try obj.Object(.xit, .{}).init(state, io, allocator, oid); 1065 defer object.deinit(); 1066 return switch (object.content) { 1067 .commit => |commit| commit.tree, 1068 else => error.InvalidObject, 1069 }; 1070 } 1071 1072 fn seedPatchRevision( 1073 io: std.Io, 1074 allocator: std.mem.Allocator, 1075 users_dir: []const u8, 1076 forker_id: *const [evt.event_id_size]u8, 1077 patch_id: *const [evt.event_id_size]u8, 1078 title: []const u8, 1079 author: evt.CommitAuthor, 1080 timestamp: u64, 1081 random: std.Random, 1082 base_oid: [hash.hexLen(.sha1)]u8, 1083 path: []const u8, 1084 contents: []const []const u8, 1085 // a base file the last commit removes 1086 removed_path: ?[]const u8, 1087 ) !void { 1088 const patch_hex = std.fmt.bytesToHex(patch_id.*, .lower); 1089 const fork_path = try fork.forkPath(allocator, users_dir, forker_id, patch_id); 1090 defer allocator.free(fork_path); 1091 var fork_repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }); 1092 defer fork_repo.deinit(io, allocator); 1093 1094 try fork_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "false" }); 1095 { 1096 var reset = try fork_repo.reset(io, allocator, .{ .target = .{ .oid = &base_oid } }); 1097 defer reset.deinit(); 1098 } 1099 const base_tree_oid = try commitTree(io, allocator, &fork_repo, &base_oid); 1100 var source_oid = base_oid; 1101 var fork_dir = try std.Io.Dir.cwd().openDir(io, fork_path, .{}); 1102 defer fork_dir.close(io); 1103 var writer = std.Io.Writer.Allocating.init(allocator); 1104 defer writer.deinit(); 1105 if (std.Io.Dir.path.dirname(path)) |dir| try fork_dir.createDirPath(io, dir); 1106 const commit_count = contents.len + @intFromBool(removed_path != null); 1107 for (contents, 0..) |content, i| { 1108 { 1109 const file = try fork_dir.createFile(io, path, .{}); 1110 defer file.close(io); 1111 try file.writeStreamingAll(io, content); 1112 } 1113 try fork_repo.add(io, allocator, &.{path}); 1114 writer.clearRetainingCapacity(); 1115 try writer.writer.print("{s} ({d}/{d})", .{ title, i + 1, commit_count }); 1116 source_oid = try fork_repo.commit(io, allocator, .{ .message = writer.written(), .timestamp = timestamp + i }); 1117 } 1118 if (removed_path) |removed| { 1119 // the bare fork never checked the base files out, so only the index drops it 1120 try fork_repo.remove(io, allocator, &.{removed}, .{ .update_work_dir = false }); 1121 writer.clearRetainingCapacity(); 1122 try writer.writer.print("{s} ({d}/{d})", .{ title, commit_count, commit_count }); 1123 source_oid = try fork_repo.commit(io, allocator, .{ .message = writer.written(), .timestamp = timestamp + contents.len }); 1124 } 1125 try fork_repo.patchAll(io, allocator, null); 1126 try fork_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "true" }); 1127 try transaction(io, &fork_repo, Reindex{ .io = io, .allocator = allocator, .core = &fork_repo.core, .commits = false }); 1128 _ = try fork_repo.garbageCollect(io, allocator, .{}); 1129 try fork_dir.deleteFile(io, path); 1130 const revision_id = evt.EventWithId.randomId(random); 1131 const head_tree_oid = try commitTree(io, allocator, &fork_repo, &source_oid); 1132 const revision: evt.PatchRev = .{ 1133 .base_oid = &base_oid, 1134 .source_oid = &source_oid, 1135 .message = title, 1136 }; 1137 const revision_timestamp = timestamp + 3; 1138 const tree_entries = [_]evt.EventTreeEntry{ 1139 .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, 1140 .{ .tree = .{ .name = "head", .oid = &head_tree_oid } }, 1141 }; 1142 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .fork, .xit, .{}, io, allocator, &fork_repo, evt.events_ref, &.{.{ 1143 .id = std.fmt.bytesToHex(revision_id, .lower), 1144 .timestamp = revision_timestamp, 1145 .author = author, 1146 .tree_entries = &tree_entries, 1147 .event = .{ .patchrev = revision }, 1148 }}); 1149 1150 var arena = std.heap.ArenaAllocator.init(allocator); 1151 defer arena.deinit(); 1152 const moment = try evt.currentMoment(.{}, &fork_repo); 1153 const revision_record = (try evt.PatchRev.readById(evt.EventDB(.sha1), .sha1, moment, &arena, &revision_id)) orelse return error.NotFound; 1154 const patch_record = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &arena, patch_id)) orelse return error.NotFound; 1155 var patch = patch_record.event; 1156 patch.revision = evt.Patch.Revision.fromRecord(revision_id, revision_record); 1157 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .fork, .xit, .{}, io, allocator, &fork_repo, evt.events_ref, &.{.{ 1158 .id = patch_hex, 1159 .timestamp = revision_timestamp + 1, 1160 .author = author, 1161 .event = .{ .patch = patch }, 1162 }}); 1163 } 1164 1165 fn seedPatches( 1166 io: std.Io, 1167 allocator: std.mem.Allocator, 1168 server_path: []const u8, 1169 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), 1170 target_repo: *rp.Repo(.xit, .{}), 1171 repo_id: *const [evt.event_id_size]u8, 1172 repo_user_id: *const [evt.event_id_size]u8, 1173 user_id: *const [evt.event_id_size]u8, 1174 random: std.Random, 1175 ) !void { 1176 const patch_data = [_]struct { 1177 title: []const u8, 1178 description: []const u8, 1179 labels: []const u8, 1180 status: ?evt.Patch.StatusKind, 1181 alpha_edit: ?ScatterEdit = null, 1182 // based behind master, so merging it makes a merge commit 1183 behind_master: bool = false, 1184 // a base file the patch removes 1185 removed_path: ?[]const u8 = null, 1186 }{ 1187 .{ 1188 .title = "Draft a faster dependency scanner", 1189 .description = "Rework dependency discovery so a **large workspace** can be scanned without repeatedly opening the same manifests.", 1190 .labels = "performance build", 1191 .status = null, 1192 }, 1193 .{ 1194 .title = "Remove the legacy configuration loader", 1195 .description = "Delete the compatibility loader now that the replacement format has shipped and the migration warning has been available for a *full release*.", 1196 .labels = "cleanup config", 1197 .status = .merged, 1198 .behind_master = true, 1199 }, 1200 .{ 1201 .title = "Cache parsed manifests between commands", 1202 .description = "Keep parsed manifests in the command `Context` so consecutive operations do not repeat identical filesystem and parsing work.", 1203 .labels = "performance cache", 1204 .status = .closed, 1205 }, 1206 .{ 1207 .title = "Add structured output to the inspect command", 1208 .description = "Add a stable JSON representation of `inspect` results for scripts and editor integrations.", 1209 .labels = "enhancement cli", 1210 .status = .open, 1211 .removed_path = "docs/dev/contribute.md", 1212 }, 1213 .{ 1214 .title = "Edit an adjacent line in alpha.txt", 1215 .description = "This patch can be cleanly merged by haxy, while git throws a **merge conflict**!", 1216 .labels = "merge", 1217 .status = .open, 1218 .alpha_edit = .{ .line = 2, .text = "adjust alpha beside the latest scatter edit" }, 1219 }, 1220 .{ 1221 .title = "Edit a conflicting line in alpha.txt", 1222 .description = "Both source and squash merges conflict with a different edit to the *same line* on master.", 1223 .labels = "merge", 1224 .status = .open, 1225 .alpha_edit = .{ .line = 3, .text = "replace alpha with a conflicting edit" }, 1226 }, 1227 }; 1228 1229 const users_dir = try std.Io.Dir.path.join(allocator, &.{ server_path, "users" }); 1230 defer allocator.free(users_dir); 1231 const patch_author = evt.CommitAuthor{ .name = "admin", .email = "admin@example.test" }; 1232 var patch_ids: [patch_data.len][evt.event_id_size]u8 = undefined; 1233 for (patch_data, 0..) |patch, i| { 1234 const timestamp: u64 = @intCast(500 + i * 10); 1235 patch_ids[i] = evt.EventWithId.randomId(random); 1236 const patch_hex = std.fmt.bytesToHex(patch_ids[i], .lower); 1237 const path = try fork.create(.{}, io, allocator, users_dir, .{ 1238 .id = patch_hex, 1239 .user_id = user_id.*, 1240 .repo_id = repo_id.*, 1241 .repo_user_id = repo_user_id.*, 1242 .title = patch.title, 1243 .description = patch.description, 1244 .labels = patch.labels, 1245 .target_branch = "master", 1246 .author = patch_author, 1247 .timestamp = timestamp, 1248 }); 1249 allocator.free(path); 1250 1251 // prepare the patch's base and file contents, then seed its commits and revision 1252 { 1253 var arena = std.heap.ArenaAllocator.init(allocator); 1254 defer arena.deinit(); 1255 const aa = arena.allocator(); 1256 var base_oid = (try target_repo.readRef(io, .{ .kind = .head, .name = "master" })) orelse return error.NotFound; 1257 if (patch.alpha_edit != null or patch.behind_master) { 1258 // v30 precedes the last scatter commit, even after the sample merge 1259 const tag_oid = (try target_repo.readRef(io, .{ .kind = .tag, .name = "v30" })) orelse return error.NotFound; 1260 var moment = try target_repo.core.latestMoment(); 1261 const state = rp.Repo(.xit, .{}).State(.read_only){ .core = &target_repo.core, .extra = .{ .moment = &moment } }; 1262 var tag = try obj.Object(.xit, .{}).init(state, io, allocator, &tag_oid); 1263 defer tag.deinit(); 1264 base_oid = switch (tag.content) { 1265 .tag => |value| value.target, 1266 else => return error.InvalidObject, 1267 }; 1268 } 1269 var contents: [3][]const u8 = undefined; 1270 const file_path: []const u8, const count: usize = if (patch.alpha_edit) |edit| blk: { 1271 // rev 29 first changes line 4; edit that line or the one above it 1272 contents[0] = try scatterContent(aa, 0, 28, edit); 1273 break :blk .{ "src/alpha.txt", 1 }; 1274 } else blk: { 1275 var writer = std.Io.Writer.Allocating.init(allocator); 1276 defer writer.deinit(); 1277 for (&contents, 0..) |*content, part| { 1278 try writer.writer.print("{s}: part {d}\n", .{ patch.title, part + 1 }); 1279 content.* = try aa.dupe(u8, writer.written()); 1280 } 1281 break :blk .{ "patch.txt", contents.len }; 1282 }; 1283 try seedPatchRevision(io, allocator, users_dir, user_id, &patch_ids[i], patch.title, patch_author, timestamp + 1, random, base_oid, file_path, contents[0..count], patch.removed_path); 1284 } 1285 const status = patch.status orelse continue; 1286 try pch.publish(.{}, io, allocator, users_dir, admin_repo, target_repo, .{ 1287 .id = patch_hex, 1288 .user_id = user_id.*, 1289 .repo_id = repo_id.*, 1290 .author = patch_author, 1291 .timestamp = timestamp + 6, 1292 }); 1293 1294 if (status == .merged) { 1295 try pch.mergeAndRemoveFork(.{}, io, allocator, users_dir, target_repo, .{ 1296 .id = patch_hex, 1297 .revision = .source, 1298 .author = patch_author, 1299 .timestamp = timestamp + 7, 1300 }); 1301 } else if (status != .open) { 1302 try evt.Patch.update(.{ .server = .{ .users_dir = users_dir } }, .xit, .{}, io, allocator, target_repo, &patch_ids[i], .{ .status = status }, patch_author); 1303 } 1304 } 1305 1306 // seed a small comment tree on the newest patch 1307 var comment_ids: [5][evt.event_id_size]u8 = undefined; 1308 for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(random); 1309 const comment_bodies = [_][]const u8{ 1310 "This also needs to preserve the **executable bit** for files nested inside generated directories.", 1311 "I tested both paths:\n\n- the archive path\n- the direct-copy path\n\nBoth currently lose the mode.", 1312 "The `ArchiveWriter` is the right place to centralize it, since every exporter already goes through that layer.", 1313 "Please include a regression fixture with a *mix* of executable and ordinary files.", 1314 "I'll add that fixture and cover both supported archive formats:\n\n1. tar\n2. zip", 1315 }; 1316 var comments: [comment_ids.len]evt.EventWithId = undefined; 1317 for (&comments, 0..) |*comment, i| { 1318 comment.* = .{ 1319 .id = std.fmt.bytesToHex(comment_ids[i], .lower), 1320 .timestamp = @intCast(700 + i), 1321 .author = .{ .name = if (i % 2 == 0) "alice" else "bobby", .email = if (i % 2 == 0) "alice@example.test" else "bobby@example.test" }, 1322 .event = .{ .comment = .{ 1323 .thread_id = std.fmt.bytesToHex(patch_ids[patch_ids.len - 1], .lower), 1324 .parent_id = switch (i) { 1325 0, 1 => std.fmt.bytesToHex(patch_ids[patch_ids.len - 1], .lower), 1326 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), 1327 3 => std.fmt.bytesToHex(comment_ids[2], .lower), 1328 else => unreachable, 1329 }, 1330 .body = comment_bodies[i], 1331 } }, 1332 }; 1333 } 1334 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &comments); 1335 1336 // create divergent metadata edits on the next three patches 1337 var patch_arena = std.heap.ArenaAllocator.init(allocator); 1338 defer patch_arena.deinit(); 1339 const moment = try evt.currentMoment(.{}, target_repo); 1340 var values: [3]evt.Patch = undefined; 1341 for (1..4) |i| { 1342 const record = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &patch_arena, &patch_ids[i])) orelse return error.NotFound; 1343 values[i - 1] = record.event; 1344 } 1345 var ours_values = values; 1346 ours_values[0].title = "Drop the legacy configuration loader"; 1347 ours_values[0].labels = "cleanup config breaking"; 1348 const closed_description = try longDescription( 1349 patch_arena.allocator(), 1350 "## Summary\n\nCache parsed manifests for the lifetime of a command invocation and invalidate entries when their files change.", 1351 ); 1352 ours_values[1].description = closed_description; 1353 ours_values[2].description = "Add JSON output to `inspect` with versioned field names and deterministic object ordering."; 1354 var theirs_values = values; 1355 theirs_values[0].title = "Delete compatibility configuration support"; 1356 theirs_values[0].labels = "config maintenance"; 1357 theirs_values[1].description = "Keep a process-wide manifest cache shared by **every** command and refresh it after writes."; 1358 theirs_values[2].description = "Expose `inspect` results as newline-delimited JSON so callers can stream large repositories."; 1359 1360 const seed_tip = (try target_repo.readRef(io, evt.events_ref)) orelse return error.NotFound; 1361 const other_ref = xit.ref.Ref{ .kind = .head, .name = "haxy/patch-other" }; 1362 var ours = [_]evt.EventWithId{ 1363 .{ 1364 .id = std.fmt.bytesToHex(patch_ids[1], .lower), 1365 .timestamp = 800, 1366 .author = .{ .name = "alice", .email = "alice@example.test" }, 1367 .event = .{ .patch = ours_values[0] }, 1368 }, 1369 .{ 1370 .id = std.fmt.bytesToHex(patch_ids[2], .lower), 1371 .timestamp = 801, 1372 .author = .{ .name = "alice", .email = "alice@example.test" }, 1373 .event = .{ .patch = ours_values[1] }, 1374 }, 1375 .{ 1376 .id = std.fmt.bytesToHex(patch_ids[3], .lower), 1377 .timestamp = 802, 1378 .author = .{ .name = "alice", .email = "alice@example.test" }, 1379 .event = .{ .patch = ours_values[2] }, 1380 }, 1381 }; 1382 1383 var theirs = ours; 1384 theirs[0].timestamp = 810; 1385 theirs[0].author = .{ .name = "bobby", .email = "bobby@example.test" }; 1386 theirs[0].event.patch = theirs_values[0]; 1387 theirs[1].timestamp = 811; 1388 theirs[1].author = .{ .name = "bobby", .email = "bobby@example.test" }; 1389 theirs[1].event.patch = theirs_values[1]; 1390 theirs[2].timestamp = 812; 1391 theirs[2].author = .{ .name = "bobby", .email = "bobby@example.test" }; 1392 theirs[2].event.patch = theirs_values[2]; 1393 1394 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &ours); 1395 try commitEventsAtRef(io, allocator, target_repo, other_ref, &theirs, seed_tip); 1396 { 1397 var merge = try target_repo.mergeAtRef(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, evt.events_ref, null); 1398 defer merge.deinit(); 1399 if (merge.result != .success) return error.MergeFailed; 1400 } 1401 try target_repo.removeBranch(io, .{ .name = other_ref.name }); 1402 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &.{}); 1403 try pch.writeBranchPatch(.{ .server = .{ .users_dir = users_dir } }, .xit, .{}, io, allocator, target_repo, std.fmt.bytesToHex(evt.EventWithId.randomId(random), .lower), .{ 1404 .title = "Merge the existing feature branch", 1405 .description = "This patch tracks the `feature` branch in this repo without creating a fork.", 1406 .labels = "feature", 1407 .source_branch = "feature", 1408 .target_branch = "master", 1409 }, null, patch_author); 1410 1411 // a push onto the branch that patch tracks, so the undo tab shows the 1412 // action and the patch refresh it sets off 1413 try seedPush(io, allocator, target_repo, users_dir, patch_author); 1414 try pch.refreshBranches(.{ .server = .{ .users_dir = users_dir } }, .xit, .{}, io, allocator, target_repo, null, null); 1415 } 1416 1417 // stand in for a client push, which the fixture has no server to receive: one 1418 // transaction moves the branch, refreshes the patches tracking it and records 1419 // the action, the way receive-pack does 1420 fn seedPush(io: std.Io, allocator: std.mem.Allocator, repo: *rp.Repo(.xit, .{}), users_dir: []const u8, author: evt.CommitAuthor) !void { 1421 const Repo = rp.Repo(.xit, .{}); 1422 const DB = Repo.DB; 1423 const Ctx = struct { 1424 core: *Repo.Core, 1425 io: std.Io, 1426 allocator: std.mem.Allocator, 1427 users_dir: []const u8, 1428 author: evt.CommitAuthor, 1429 1430 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 1431 const opts: rp.RepoOpts(.xit) = .{}; 1432 var moment = try DB.HashMap(.read_write).init(cursor.*); 1433 const state = Repo.State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 1434 var arena = std.heap.ArenaAllocator.init(ctx.allocator); 1435 defer arena.deinit(); 1436 const aa = arena.allocator(); 1437 1438 // a null tree keeps the parent's content, so the branch advances 1439 // without the fixture having to build one 1440 const author_line = try aa.print("{s} <{s}>", .{ ctx.author.name, ctx.author.email }); 1441 const feature_old = (try rf.readRecur(.xit, opts, state.readOnly(), ctx.io, .{ .ref = .{ .kind = .head, .name = "feature" } })) orelse return error.NotFound; 1442 const feature_new = try obj.writeCommit(.xit, opts, state, ctx.io, ctx.allocator, .{ 1443 .message = "Revise the feature branch", 1444 .author = author_line, 1445 .timestamp = 820, 1446 }, null, .{ .kind = .head, .name = "feature" }); 1447 1448 // a branch the push creates has no old tip, so its range is open 1449 try bch.add(.xit, opts, state, ctx.io, .{ .name = "docs" }); 1450 const docs_new = (try rf.readRecur(.xit, opts, state.readOnly(), ctx.io, .{ .ref = .{ .kind = .head, .name = "docs" } })) orelse return error.NotFound; 1451 1452 const updates = [_]xit.net_server_receive_pack.AppliedRefUpdate{ 1453 .{ 1454 .ref_name = try aa.dupe(u8, "refs/heads/feature"), 1455 .old_oid = try aa.dupe(u8, &feature_old), 1456 .new_oid = try aa.dupe(u8, &feature_new), 1457 }, 1458 .{ 1459 .ref_name = try aa.dupe(u8, "refs/heads/docs"), 1460 .old_oid = try aa.dupe(u8, &@as([hash.hexLen(opts.hash)]u8, @splat('0'))), 1461 .new_oid = try aa.dupe(u8, &docs_new), 1462 }, 1463 }; 1464 1465 _ = try pch.refreshBranchesInTransaction(.{ .server = .{ .users_dir = ctx.users_dir } }, opts, state, &moment, ctx.io, ctx.allocator, &updates, null, null); 1466 try push.writeUndo(opts, state, ctx.io, ctx.allocator, ctx.author, &updates); 1467 } 1468 }; 1469 1470 try transaction(io, repo, Ctx{ .core = &repo.core, .io = io, .allocator = allocator, .users_dir = users_dir, .author = author }); 1471 } 1472 1473 // commit `events` onto `ref`, rooted at `parent`, as one transaction. 1474 // consume always roots a new branch at nothing, so the divergent fixtures 1475 // write their side branch here instead. nothing is consumed, so it records 1476 // xit's own commit action rather than one of haxy's event actions 1477 fn commitEventsAtRef( 1478 io: std.Io, 1479 allocator: std.mem.Allocator, 1480 repo: *rp.Repo(.xit, .{}), 1481 ref: xit.ref.Ref, 1482 events: []const evt.EventWithId, 1483 parent: [hash.hexLen(.sha1)]u8, 1484 ) !void { 1485 const Repo = rp.Repo(.xit, .{}); 1486 const DB = Repo.DB; 1487 const Ctx = struct { 1488 core: *Repo.Core, 1489 io: std.Io, 1490 allocator: std.mem.Allocator, 1491 ref: xit.ref.Ref, 1492 events: []const evt.EventWithId, 1493 parent: [hash.hexLen(.sha1)]u8, 1494 1495 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 1496 const opts: rp.RepoOpts(.xit) = .{}; 1497 var moment = try DB.HashMap(.read_write).init(cursor.*); 1498 const state = Repo.State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 1499 try evt.commitEvents(.xit, opts, state, ctx.io, ctx.allocator, ctx.ref, ctx.events, .{ctx.parent}); 1500 const tip = (try xit.ref.readRecur(.xit, opts, state.readOnly(), ctx.io, .{ .ref = ctx.ref })) orelse return error.NotFound; 1501 try xit.undo.write(opts, state, std.Io.Timestamp.now(ctx.io, .real).toSeconds(), .{ 1502 .commit = try .init("seed conflicting events", tip, opts.max_read_size), 1503 }); 1504 } 1505 }; 1506 1507 try transaction(io, repo, Ctx{ 1508 .core = &repo.core, 1509 .io = io, 1510 .allocator = allocator, 1511 .ref = ref, 1512 .events = events, 1513 .parent = parent, 1514 }); 1515 } 1516 1517 // append `ctx` to the repo's history as one transaction. the fixtures write a 1518 // few states the normal paths have no way to produce 1519 // fixtures commit directly, so the ref tips are indexed by hand. a fork gets 1520 // no commit index, as in production. 1521 const Reindex = struct { 1522 io: std.Io, 1523 allocator: std.mem.Allocator, 1524 core: *rp.Repo(.xit, .{}).Core, 1525 commits: bool, 1526 1527 pub fn run(ctx: @This(), cursor: *rp.Repo(.xit, .{}).DB.Cursor(.read_write)) !void { 1528 var moment = try rp.Repo(.xit, .{}).DB.HashMap(.read_write).init(cursor.*); 1529 const state = rp.Repo(.xit, .{}).State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 1530 _ = try find.refreshInTransaction(.{}, state, &moment, ctx.io, ctx.allocator, null); 1531 if (ctx.commits) _ = try srch_cmmt.refreshInTransaction(.{}, state, &moment, ctx.io, ctx.allocator, &.{}, null); 1532 } 1533 }; 1534 1535 fn transaction(io: std.Io, repo: *rp.Repo(.xit, .{}), ctx: anytype) !void { 1536 const DB = rp.Repo(.xit, .{}).DB; 1537 try repo.core.db_file.lock(io, .exclusive); 1538 defer repo.core.db_file.unlock(io); 1539 1540 const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); 1541 try history.appendContext(.{ .slot = try history.getSlot(-1) }, ctx); 1542 } 1543 1544 // recursively copy the contents of src_dir into dest_dir 1545 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 1546 var iter = src_dir.iterate(); 1547 while (try iter.next(io)) |entry| { 1548 switch (entry.kind) { 1549 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 1550 .directory => { 1551 try dest_dir.createDirPath(io, entry.name); 1552 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1553 defer dest_entry_dir.close(io); 1554 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1555 defer src_entry_dir.close(io); 1556 try copyDir(io, src_entry_dir, dest_entry_dir); 1557 }, 1558 else => {}, 1559 } 1560 } 1561 } 1562