╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
374 for (0..ui.detail_preview_lines) |line| { 375 try msg_writer.writer.print("\n{d} {s}", .{ line, scatter_words[line % scatter_words.len] }); 376 } 377 } 378 const message = try arena.allocator().dupe(u8, msg_writer.written()); 379 _ = try template_repo.commit(io, allocator, .{ .message = message, .timestamp = base_ts + c * std.time.s_per_day }); 380 try addNextTag(&template_repo, io, allocator, &tag_num); 381 } 382 383 // two more branches forked off master, each adding a single commit 384 // that makes a scattered edit to one file. tag each new commit too, 385 // and switch back to master after each so the next branch also 386 // forks from master and the template ends up back on master. 387 const branch_data = [_]struct { 388 name: []const u8, 389 file: []const u8, 390 fi: usize, 391 message: []const u8, 392 rev: usize, 393 }{ 394 .{ .name = "extra", .file = "src/alpha.txt", .fi = 0, .message = "scatter alpha on the extra branch", .rev = 30 }, 395 .{ .name = "feature", .file = "src/beta.txt", .fi = 1, .message = "scatter beta on the feature branch", .rev = 31 }, 396 }; 397 for (branch_data) |b| { 398 try template_repo.addBranch(io, .{ .name = b.name }); 399 { 400 var to_branch = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = b.name } } }); 401 defer to_branch.deinit(); 402 } 403 404 { 405 var repo_dir = try cwd.openDir(io, template_path, .{}); 406 defer repo_dir.close(io); 407 try writeScatterFile(io, allocator, repo_dir, b.file, b.fi, b.rev); 408 } 409 try template_repo.add(io, allocator, &.{b.file}); 410 _ = try template_repo.commit(io, allocator, .{ .message = b.message, .timestamp = base_ts + b.rev * std.time.s_per_day }); 411 try addNextTag(&template_repo, io, allocator, &tag_num); 412 413 { 414 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 415 defer to_master.deinit(); 416 } 417 } 418 419 // the timestamps issue's description, in paragraphs, so the 420 // seeded conflict below can insert and delete among them 421 const tz_p1 = "All timestamps in the activity feed render in UTC regardless of the system timezone."; 422 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."; 423 const tz_p3 = "A quick survey of other clients shows every one of them rendering local time by default."; 424 const tz_p4 = "Convert to local time and include the offset in tooltips."; 425 426 // the sync issue's description, in paragraphs, so each side of its 427 // seeded conflict can rework a different one 428 const sync_p1 = "The sync engine only has unit tests with mocked transports."; 429 const sync_p2 = "Failures seen in production involve reconnects and partial writes, which the mocks can't reproduce."; 430 const sync_p3 = "Add end-to-end tests that run two instances against a real local server."; 431 432 // seed issues so every repo's issue tracker has content 433 const issue_data = [_]struct { 434 title: []const u8, 435 description: []const u8, 436 tags: []const u8, 437 status: evt.Issue.Status = .open, 438 }{ 439 .{ 440 .title = "Crash on startup when config file is missing", 441 .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.", 442 .tags = "bug priority-high crash", 443 }, 444 .{ 445 .title = "Support dark mode in the settings panel", 446 .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.", 447 .tags = "enhancement ui theme", 448 }, 449 .{ 450 .title = "Memory leak when reconnecting after network failure", 451 .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.", 452 .tags = "bug memory networking", 453 .status = .closed, 454 }, 455 .{ 456 .title = "Document the plugin API", 457 .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.", 458 .tags = "documentation plugins", 459 }, 460 .{ 461 .title = "Slow file indexing on large directories", 462 .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.", 463 .tags = "performance indexing", 464 }, 465 .{ 466 .title = "Add keyboard shortcut for quick search", 467 .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.", 468 .tags = "enhancement ux keyboard", 469 }, 470 .{ 471 .title = "Unicode filenames are garbled in the export dialog", 472 .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.", 473 .tags = "bug unicode i18n", 474 }, 475 .{ 476 .title = "Flaky test: integration suite times out on CI", 477 .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.", 478 .tags = "bug testing ci", 479 }, 480 .{ 481 .title = "Upgrade bundled zlib to the latest release", 482 .description = "The vendored zlib is two major releases behind and misses several upstream fixes. Update the bundled copy and re-run the compression benchmarks.", 483 .tags = "dependencies maintenance", 484 }, 485 .{ 486 .title = "Progress bar overshoots 100% during resumed downloads", 487 .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.", 488 .tags = "bug ui downloads", 489 .status = .closed, 490 }, 491 .{ 492 .title = "Config parser rejects trailing commas", 493 .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.", 494 .tags = "bug config parser", 495 }, 496 .{ 497 .title = "Add a --json flag to the status command", 498 .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.", 499 .tags = "enhancement cli", 500 }, 501 .{ 502 .title = "Race condition between autosave and manual save", 503 .description = "Saving manually while an autosave is in flight can interleave the two writes and corrupt the file. Serialize saves through a single queue.", 504 .tags = "bug priority-high data-loss", 505 }, 506 .{ 507 .title = "Reduce binary size of release builds", 508 .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.", 509 .tags = "performance build", 510 }, 511 .{ 512 .title = "Tooltips flicker when the cursor moves between adjacent buttons", 513 .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.", 514 .tags = "bug ui polish", 515 }, 516 .{ 517 .title = "Support environment variable expansion in config paths", 518 .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.", 519 .tags = "enhancement config", 520 }, 521 .{ 522 .title = "Log rotation deletes the newest file instead of the oldest", 523 .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.", 524 .tags = "bug logging", 525 .status = .closed, 526 }, 527 .{ 528 .title = "Add man pages for all subcommands", 529 .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.", 530 .tags = "documentation cli", 531 }, 532 .{ 533 .title = "High CPU usage while idle in the background", 534 .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.", 535 .tags = "performance priority-high", 536 }, 537 .{ 538 .title = "Paste from clipboard drops the final newline", 539 .description = "Pasting text that ends with a newline silently trims it, which breaks pasted shell snippets. Preserve the clipboard content exactly.", 540 .tags = "bug editor clipboard", 541 }, 542 .{ 543 .title = "Improve error message for expired credentials", 544 .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.", 545 .tags = "enhancement ux auth", 546 }, 547 .{ 548 .title = "Crash when window is resized during startup animation", 549 .description = "Resizing the window while the splash animation is running dereferences a freed layout node. Cancel the animation before rebuilding the layout.", 550 .tags = "bug crash ui", 551 }, 552 .{ 553 .title = "Add integration tests for the sync engine", 554 .description = sync_p1 ++ "\n\n" ++ sync_p2 ++ "\n\n" ++ sync_p3, 555 .tags = "testing sync", 556 }, 557 .{ 558 .title = "Timestamps display in UTC instead of local time", 559 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p3 ++ "\n\n" ++ tz_p4, 560 .tags = "bug i18n time", 561 }, 562 .{ 563 .title = "Deprecate the legacy plugin format", 564 .description = "Both plugin formats are currently loaded, doubling the maintenance surface. Warn on legacy plugins this release and drop support in the next.", 565 .tags = "maintenance plugins", 566 }, 567 }; 568 569 var issue_events: [issue_data.len]evt.EventWithId = undefined; 570 for (issue_data, 0..) |issue, i| { 571 // the fifth-newest issue's description runs past what the detail 572 // pane shows, so it shows the truncated description and its 573 // link. 574 const description = if (i == issue_data.len - 5) 575 try longDescription(arena.allocator(), issue.description) 576 else 577 issue.description; 578 issue_events[i] = .{ 579 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 580 // stepped timestamps so the issues list in a stable order 581 .timestamp = @intCast(i + 1), 582 .author = .{ .name = user_data[i % user_data.len].name, .email = user_data[i % user_data.len].email }, 583 .event = .{ 584 .issue = .{ 585 .title = issue.title, 586 .description = description, 587 .tags = issue.tags, 588 .status = issue.status, 589 }, 590 }, 591 }; 592 } 593 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &issue_events); 594 // the tip is the branch point for the conflicting edits below 595 const seed_tip = (try template_repo.readRef(io, evt.events_ref)) orelse unreachable; 596 597 // two divergent edits per conflicted issue: ours on the events 598 // branch, theirs on a temp branch rooted at the seed tip, then a 599 // merge. the 4th-newest issue conflicts on title and tags; the 600 // 3rd-newest on its description with every hunk auto-resolving 601 // (each side reworks a different paragraph); the 2nd-newest on 602 // its description with a removal conflict (ours removes a 603 // paragraph theirs rewords) and an insertion conflict (each side 604 // appends a different closing paragraph). 605 { 606 const other_ref: xit.ref.Ref = .{ .kind = .head, .name = "haxy/other" }; 607 const title_issue = issue_data[issue_data.len - 4]; 608 const sync_issue = issue_data[issue_data.len - 3]; 609 const desc_issue = issue_data[issue_data.len - 2]; 610 611 const ours = [_]evt.EventWithId{ .{ 612 .id = issue_events[issue_data.len - 4].id, 613 .timestamp = 100, 614 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 615 .event = .{ .issue = .{ 616 .title = "Crash when resizing the window during the splash animation", 617 .description = title_issue.description, 618 .tags = "bug crash ui priority-high", 619 } }, 620 }, .{ 621 .id = issue_events[issue_data.len - 3].id, 622 .timestamp = 101, 623 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 624 .event = .{ .issue = .{ 625 .title = sync_issue.title, 626 .description = "The sync engine's coverage is unit tests only, with every transport mocked out." ++ "\n\n" ++ 627 sync_p2 ++ "\n\n" ++ sync_p3, 628 .tags = sync_issue.tags, 629 } }, 630 }, .{ 631 .id = issue_events[issue_data.len - 2].id, 632 .timestamp = 102, 633 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 634 .event = .{ .issue = .{ 635 .title = desc_issue.title, 636 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p4 ++ "\n\n" ++ 637 "The confusion is worst for teams spread across timezones, who each read a different wall-clock time from the same feed.", 638 .tags = desc_issue.tags, 639 } }, 640 } }; 641 const theirs = [_]evt.EventWithId{ .{ 642 .id = issue_events[issue_data.len - 4].id, 643 .timestamp = 103, 644 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 645 .event = .{ .issue = .{ 646 .title = "Segfault on early window resize", 647 .description = title_issue.description, 648 .tags = "bug crash rendering", 649 } }, 650 }, .{ 651 .id = issue_events[issue_data.len - 3].id, 652 .timestamp = 104, 653 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 654 .event = .{ .issue = .{ 655 .title = sync_issue.title, 656 .description = sync_p1 ++ "\n\n" ++ sync_p2 ++ "\n\n" ++ 657 "Add end-to-end tests that drive two live instances against a local server on every CI run.", 658 .tags = sync_issue.tags, 659 } }, 660 }, .{ 661 .id = issue_events[issue_data.len - 2].id, 662 .timestamp = 105, 663 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 664 .event = .{ .issue = .{ 665 .title = desc_issue.title, 666 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ 667 "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" ++ 668 "Log exports inherit the same UTC rendering, so downstream tooling has to guess the source timezone.", 669 .tags = desc_issue.tags, 670 } }, 671 } }; 672 673 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &ours); 674 675 // consume can't root a new branch, so theirs commits by hand 676 var json: std.Io.Writer.Allocating = .init(allocator); 677 defer json.deinit(); 678 for (theirs, 0..) |event, i| { 679 json.clearRetainingCapacity(); 680 try std.json.Stringify.value(event, .{}, &json.writer); 681 const author = try std.fmt.allocPrint(allocator, "{s} <{s}>", .{ event.author.name, event.author.email }); 682 defer allocator.free(author); 683 _ = try template_repo.commitAtRef(io, allocator, .{ 684 .author = author, 685 .message = json.written(), 686 .timestamp = event.timestamp, 687 // root the branch at the shared seed tip 688 .parent_oids = if (i == 0) &.{seed_tip} else null, 689 }, null, other_ref); 690 } 691 692 { 693 var to_events = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 694 defer to_events.deinit(); 695 } 696 { 697 var merge = try template_repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, null); 698 defer merge.deinit(); 699 if (merge.result != .success) return error.MergeFailed; 700 } 701 { 702 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 703 defer to_master.deinit(); 704 } 705 try template_repo.removeBranch(io, .{ .name = other_ref.name }); 706 } 707 708 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &.{}); 709 710 // seed a small comment tree on the newest issue 711 var comment_ids: [5][evt.event_id_size]u8 = undefined; 712 for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 713 714 const comment_bodies = [_][]const u8{ 715 "I can take this. The compatibility scanner already reports which repositories still use the legacy format.", 716 "Can we keep loading legacy plugins for one release after the warning lands? That would give downstream maintainers time to migrate.", 717 "That sounds good. I'll include the scanner output in the warning so each affected plugin is named.", 718 "Please include the replacement manifest path too. That should make the warning actionable without opening the migration guide.", 719 "I'll add release-note text once the warning format is settled.", 720 }; 721 722 var comment_events: [comment_ids.len]evt.EventWithId = undefined; 723 for (&comment_events, 0..) |*event, i| { 724 event.* = .{ 725 .id = std.fmt.bytesToHex(comment_ids[i], .lower), 726 .timestamp = @intCast(200 + i), 727 .author = .{ .name = user_data[(i + 1) % user_data.len].name, .email = user_data[(i + 1) % user_data.len].email }, 728 .event = .{ .comment = .{ 729 .thread_id = issue_events[issue_events.len - 1].id, 730 .parent_id = switch (i) { 731 0, 1 => issue_events[issue_events.len - 1].id, 732 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), 733 3 => std.fmt.bytesToHex(comment_ids[2], .lower), 734 else => unreachable, 735 }, 736 .body = comment_bodies[i], 737 } }, 738 }; 739 } 740 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &comment_events); 741 742 const discussion_data = [_]struct { 743 title: []const u8, 744 description: []const u8, 745 tags: []const u8, 746 }{ 747 .{ 748 .title = "How should plugins declare capabilities?", 749 .description = "I'd like the manifest to make privileged capabilities explicit without making simple plugins verbose.", 750 .tags = "plugins design", 751 }, 752 .{ 753 .title = "Ideas for making large repositories faster", 754 .description = "This is a place to collect profiling results and discuss which indexing work is worth pursuing first.", 755 .tags = "performance indexing", 756 }, 757 .{ 758 .title = "What should the next release focus on?", 759 .description = "Let's compare the most important reliability fixes with the larger features already in progress.", 760 .tags = "release planning", 761 }, 762 .{ 763 .title = "Improving keyboard navigation", 764 .description = "Share workflows that still require a mouse and suggestions for making their focus behavior predictable.", 765 .tags = "ui keyboard accessibility", 766 }, 767 .{ 768 .title = "Configuration format discussion", 769 .description = "Should the next configuration format favor strict validation or accept common conveniences such as trailing commas?", 770 .tags = "config design", 771 }, 772 }; 773 774 var discussion_events: [discussion_data.len]evt.EventWithId = undefined; 775 for (discussion_data, 0..) |discussion, i| { 776 discussion_events[i] = .{ 777 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 778 .timestamp = @intCast(300 + i), 779 .author = .{ .name = user_data[i % user_data.len].name, .email = user_data[i % user_data.len].email }, 780 .event = .{ .discuss = .{ 781 .title = discussion.title, 782 .description = discussion.description, 783 .tags = discussion.tags, 784 } }, 785 }; 786 } 787 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &discussion_events); 788 789 var discussion_comment_ids: [discussion_data.len][3][evt.event_id_size]u8 = undefined; 790 for (&discussion_comment_ids) |*ids| { 791 for (ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 792 } 793 var discussion_comment_events: [discussion_data.len * 3]evt.EventWithId = undefined; 794 for (&discussion_comment_events, 0..) |*event, i| { 795 const discussion_index = i / 3; 796 const comment_index = i % 3; 797 event.* = .{ 798 .id = std.fmt.bytesToHex(discussion_comment_ids[discussion_index][comment_index], .lower), 799 .timestamp = @intCast(400 + i), 800 .author = .{ .name = user_data[(i + 1) % user_data.len].name, .email = user_data[(i + 1) % user_data.len].email }, 801 .event = .{ .comment = .{ 802 .thread_id = discussion_events[discussion_index].id, 803 .parent_id = if (comment_index < 2) 804 discussion_events[discussion_index].id 805 else 806 std.fmt.bytesToHex(discussion_comment_ids[discussion_index][0], .lower), 807 .body = switch (comment_index) { 808 0 => "My preference is to start with the smallest useful version and expand it after we have real usage to learn from.", 809 1 => "It would help to write down the tradeoffs we are accepting so the decision is easy to revisit later.", 810 2 => "Agreed. A narrow first version should also make it easier to keep the implementation understandable.", 811 else => unreachable, 812 }, 813 } }, 814 }; 815 } 816 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &discussion_comment_events); 817 try template_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "true" }); 818 } 819 820 // copy the template to each repo's on-disk location, named by its 821 // hex-encoded event id. the template repo is deinitialized above, so its 822 // db file is fully written before we copy it. 823 { 824 var template_dir = try cwd.openDir(io, template_path, .{}); 825 defer template_dir.close(io); 826 var template_repo_dir = try template_dir.openDir(io, ".xit", .{ .iterate = true }); 827 defer template_repo_dir.close(io); 828 829 for (repo_event_ids, 0..) |id_bytes, repo_index| { 830 const repo_id = std.fmt.bytesToHex(id_bytes, .lower); 831 const repo_path = try std.fs.path.join(arena.allocator(), &.{ server_path, "repos", &repo_id }); 832 { 833 var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); 834 defer dest_dir.close(io); 835 var dest_repo_dir = try dest_dir.createDirPathOpen(io, ".xit", .{}); 836 defer dest_repo_dir.close(io); 837 try copyDir(io, template_repo_dir, dest_repo_dir); 838 } 839 840 if (repo_index + 1 == repo_data.len) { 841 var target_repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = repo_path, .require_repo_root = true }); 842 defer target_repo.deinit(io, allocator); 843 try seedPatches(io, allocator, server_path, &repo, &target_repo, &repo_event_ids[repo_index], &admin_user_id, prng.random()); 844 } 845 } 846 } 847 848 break :blk try ui.Session.init(&session_arena, &repo, .{ .user_id = &admin_user_id }); 849 }; 850 session.is_terminal = true; 851 session.data.git_ssh_prefix = git_ssh_prefix; 852 853 // start the server 854 855 // let the native TUI's page builders open the on-disk repos for the file tree 856 session.io = io; 857 session.repos_dir = try std.fs.path.join(session_arena.allocator(), &.{ server_path, "repos" }); 858 859 // leave a one-shot session for the first browser to hit the web ui, so it 860 // starts logged in as admin 861 { 862 var data_dir = try cwd.createDirPathOpen(io, server_path, .{}); 863 defer data_dir.close(io); 864 const store = try hx.web.SessionStore.init(io, data_dir); 865 defer store.deinit(); 866 const token = try store.create(&admin_user_id); 867 try store.offerAutoLogin(&token); 868 } 869 870 if (cli) { 871 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 872 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 873 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 874 875 const Runnable = struct { 876 io: std.Io, 877 key_path: []const u8, 878 879 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port_maybe: ?u16) !void { 880 _ = web_port; 881 _ = http_port; 882 const ssh_port = ssh_port_maybe orelse return error.SshDisabled; 883 std.debug.print( 884 \\ 885 \\connect to the TUI with: 886 \\ ssh -p {d} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 887 \\ 888 \\create a git repo and push over SSH: 889 \\ mkdir -p temp-try/client/test 890 \\ cd temp-try/client/test 891 \\ git init 892 \\ echo "hello" > hello.txt 893 \\ git add hello.txt 894 \\ git commit -m "let there be light" 895 \\ 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 896 \\ 897 \\to quit, press enter. 898 \\ 899 , .{ ssh_port, ssh_port, self.key_path }); 900 // portable stdin read via std.Io — equivalent to the 901 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 902 // on windows. 903 var buf: [1]u8 = undefined; 904 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 905 _ = stdin_reader.interface.takeByte() catch {}; 906 } 907 }; 908 909 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 910 } else { 911 var null_writer = std.Io.Writer.Discarding.init(&.{}); 912 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 913 914 const Runnable = struct { 915 io: std.Io, 916 allocator: std.mem.Allocator, 917 session: *ui.Session, 918 repo: *Repo, 919 920 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port: ?u16) !void { 921 // launch the TUI; the footer points at whatever port was bound 922 self.session.web_port = web_port; 923 self.session.data.git_http_port = http_port; 924 self.session.data.git_ssh_port = ssh_port; 925 try hx.ui.run(self.io, self.allocator, self.session, self.repo); 926 } 927 }; 928 929 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session, .repo = &repo }); 930 } 931 } 932 933 // words cycled through to pad generated lines into prose-like content 934 const scatter_words = [_][]const u8{ 935 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", 936 "quantum", "vector", "matrix", "buffer", "kernel", "socket", 937 "falcon", "otter", "badger", "walrus", "ferret", "marmot", 938 "scatter", "gather", "encode", "decode", "render", "commit", 939 }; 940 941 fn longDescription(allocator: std.mem.Allocator, initial: []const u8) ![]u8 { 942 var writer = std.Io.Writer.Allocating.init(allocator); 943 defer writer.deinit(); 944 try writer.writer.print("{s}", .{initial}); 945 for (0..ui.detail_preview_lines) |line| 946 try writer.writer.print("\n{d} {s}", .{ line, scatter_words[line % scatter_words.len] }); 947 return writer.toOwnedSlice(); 948 } 949 950 // write `path` as 40 lines of mostly-stable filler. revision `c` and file 951 // index `fi` shift which lines change and the words used, so each (c, fi) 952 // yields a distinct multi-hunk diff against the previous revision. untouched 953 // lines stay byte-identical across revisions, so a bump in `c` is a scatter of 954 // small hunks rather than a full rewrite. 955 fn writeScatterFile(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir, path: []const u8, fi: usize, c: usize) !void { 956 const file = try repo_dir.createFile(io, path, .{}); 957 defer file.close(io); 958 var writer = std.Io.Writer.Allocating.init(allocator); 959 defer writer.deinit(); 960 for (0..40) |line| { 961 const start = writer.written().len; 962 if ((line + fi + c) % 8 == 0) { 963 const tag = scatter_words[(line + fi + c * 3) % scatter_words.len]; 964 try writer.writer.print("rev {d} {s}", .{ c, tag }); 965 } 966 const target = 40 + (line * 17 + fi * 23) % 70; 967 var w = line + fi; 968 while (true) : (w += 1) { 969 const word = scatter_words[w % scatter_words.len]; 970 const len = writer.written().len - start; 971 if (len >= target or len + 1 + word.len > 120) break; 972 if (len == 0) 973 try writer.writer.print("{s}", .{word}) 974 else 975 try writer.writer.print(" {s}", .{word}); 976 } 977 try writer.writer.writeByte('\n'); 978 } 979 try file.writeStreamingAll(io, writer.written()); 980 } 981 982 // tag the current HEAD as the next sequential version: v1, v2, and so on 983 fn addNextTag(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { 984 var buf: [16]u8 = undefined; 985 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); 986 _ = try repo.addTag(io, allocator, .{ .name = name }); 987 n.* += 1; 988 } 989 990 fn commitTree( 991 io: std.Io, 992 allocator: std.mem.Allocator, 993 repo: *rp.Repo(.xit, .{}), 994 oid: *const [hash.hexLen(.sha1)]u8, 995 ) ![hash.hexLen(.sha1)]u8 { 996 var moment = try repo.core.latestMoment(); 997 const state = rp.Repo(.xit, .{}).State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; 998 var object = try obj.Object(.xit, .{}).init(state, io, allocator, oid); 999 defer object.deinit(); 1000 return switch (object.content) { 1001 .commit => |commit| commit.tree, 1002 else => error.InvalidObject, 1003 }; 1004 } 1005 1006 fn seedPatchRevision( 1007 io: std.Io, 1008 allocator: std.mem.Allocator, 1009 repos_path: []const u8, 1010 patch_id: *const [evt.event_id_size]u8, 1011 title: []const u8, 1012 author: evt.CommitAuthor, 1013 timestamp: u64, 1014 random: std.Random, 1015 ) !void { 1016 const patch_hex = std.fmt.bytesToHex(patch_id.*, .lower); 1017 const fork_path = try fork.forkPath(allocator, repos_path, &patch_hex); 1018 defer allocator.free(fork_path); 1019 var fork_repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }); 1020 defer fork_repo.deinit(io, allocator); 1021 1022 const base_oid = (try fork_repo.readRef(io, fork.ref)) orelse return error.NotFound; 1023 const base_tree_oid = try commitTree(io, allocator, &fork_repo, &base_oid); 1024 try fork_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "false" }); 1025 var source_oid = base_oid; 1026 var fork_dir = try std.Io.Dir.cwd().openDir(io, fork_path, .{}); 1027 defer fork_dir.close(io); 1028 var writer = std.Io.Writer.Allocating.init(allocator); 1029 defer writer.deinit(); 1030 for (0..3) |i| { 1031 writer.clearRetainingCapacity(); 1032 for (0..i + 1) |line| try writer.writer.print("{s}: part {d}\n", .{ title, line + 1 }); 1033 { 1034 const file = try fork_dir.createFile(io, "patch.txt", .{}); 1035 defer file.close(io); 1036 try file.writeStreamingAll(io, writer.written()); 1037 } 1038 try fork_repo.add(io, allocator, &.{"patch.txt"}); 1039 writer.clearRetainingCapacity(); 1040 try writer.writer.print("{s} ({d}/3)", .{ title, i + 1 }); 1041 source_oid = try fork_repo.commit(io, allocator, .{ .message = writer.written(), .timestamp = timestamp + i }); 1042 } 1043 try fork_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "true" }); 1044 try fork_dir.deleteFile(io, "patch.txt"); 1045 const revision_id = evt.EventWithId.randomId(random); 1046 const head_tree_oid = try commitTree(io, allocator, &fork_repo, &source_oid); 1047 const revision: evt.PatchRev = .{ 1048 .base_oid = &base_oid, 1049 .source_oid = &source_oid, 1050 .message = title, 1051 }; 1052 const revision_timestamp = timestamp + 3; 1053 const tree_entries = [_]evt.EventTreeEntry{ 1054 .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, 1055 .{ .tree = .{ .name = "head", .oid = &head_tree_oid } }, 1056 }; 1057 try evt.consume(.fork, .xit, .{}, io, allocator, &fork_repo, evt.events_ref, &.{.{ 1058 .id = std.fmt.bytesToHex(revision_id, .lower), 1059 .timestamp = revision_timestamp, 1060 .author = author, 1061 .tree_entries = &tree_entries, 1062 .event = .{ .patchrev = revision }, 1063 }}); 1064 1065 var arena = std.heap.ArenaAllocator.init(allocator); 1066 defer arena.deinit(); 1067 const moment = try evt.currentMoment(.{}, &fork_repo); 1068 const revision_record = (try evt.PatchRev.readById(evt.EventDB(.sha1), .sha1, moment, &arena, &revision_id)) orelse return error.NotFound; 1069 const patch_record = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &arena, patch_id)) orelse return error.NotFound; 1070 var patch = patch_record.event; 1071 patch.revision = evt.Patch.Revision.fromRecord(revision_id, revision_record); 1072 try evt.consume(.fork, .xit, .{}, io, allocator, &fork_repo, evt.events_ref, &.{.{ 1073 .id = patch_hex, 1074 .timestamp = revision_timestamp + 1, 1075 .author = author, 1076 .event = .{ .patch = patch }, 1077 }}); 1078 } 1079 1080 fn seedPatches( 1081 io: std.Io, 1082 allocator: std.mem.Allocator, 1083 server_path: []const u8, 1084 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), 1085 target_repo: *rp.Repo(.xit, .{}), 1086 repo_id: *const [evt.event_id_size]u8, 1087 user_id: *const [evt.event_id_size]u8, 1088 random: std.Random, 1089 ) !void { 1090 const patch_data = [_]struct { 1091 title: []const u8, 1092 description: []const u8, 1093 tags: []const u8, 1094 status: ?evt.Patch.StatusKind, 1095 }{ 1096 .{ 1097 .title = "Draft a faster dependency scanner", 1098 .description = "Rework dependency discovery so a large workspace can be scanned without repeatedly opening the same manifests.", 1099 .tags = "performance build", 1100 .status = null, 1101 }, 1102 .{ 1103 .title = "Remove the legacy configuration loader", 1104 .description = "Delete the compatibility loader now that the replacement format has shipped and the migration warning has been available for a full release.", 1105 .tags = "cleanup config", 1106 .status = .merged, 1107 }, 1108 .{ 1109 .title = "Cache parsed manifests between commands", 1110 .description = "Keep parsed manifests in the command context so consecutive operations do not repeat identical filesystem and parsing work.", 1111 .tags = "performance cache", 1112 .status = .closed, 1113 }, 1114 .{ 1115 .title = "Add structured output to the inspect command", 1116 .description = "Add a stable JSON representation of inspect results for scripts and editor integrations.", 1117 .tags = "enhancement cli", 1118 .status = .open, 1119 }, 1120 .{ 1121 .title = "Preserve file permissions during export", 1122 .description = "Carry executable bits through archive exports so unpacked command-line tools remain runnable without a manual chmod step.", 1123 .tags = "bug export permissions", 1124 .status = .open, 1125 }, 1126 }; 1127 1128 const repos_path = try std.fs.path.join(allocator, &.{ server_path, "repos" }); 1129 defer allocator.free(repos_path); 1130 const patch_author = evt.CommitAuthor{ .name = "admin", .email = "admin@example.test" }; 1131 var patch_ids: [patch_data.len][evt.event_id_size]u8 = undefined; 1132 for (patch_data, 0..) |patch, i| { 1133 const timestamp: u64 = @intCast(500 + i * 10); 1134 patch_ids[i] = evt.EventWithId.randomId(random); 1135 const patch_hex = std.fmt.bytesToHex(patch_ids[i], .lower); 1136 const path = try fork.create(.{}, io, allocator, repos_path, admin_repo, .{ 1137 .id = patch_hex, 1138 .user_id = user_id.*, 1139 .repo_id = repo_id.*, 1140 .title = patch.title, 1141 .description = patch.description, 1142 .tags = patch.tags, 1143 .target_branch = "master", 1144 .author = patch_author, 1145 .timestamp = timestamp, 1146 }); 1147 allocator.free(path); 1148 1149 try seedPatchRevision(io, allocator, repos_path, &patch_ids[i], patch.title, patch_author, timestamp + 1, random); 1150 const status = patch.status orelse continue; 1151 const fork_path = try fork.forkPath(allocator, repos_path, &patch_hex); 1152 defer allocator.free(fork_path); 1153 try pch.publish(.{}, io, allocator, admin_repo, target_repo, fork_path, .{ 1154 .id = patch_hex, 1155 .user_id = user_id.*, 1156 .repo_id = repo_id.*, 1157 .author = patch_author, 1158 .timestamp = timestamp + 6, 1159 }); 1160 1161 if (status == .merged) { 1162 try pch.mergeAndRemoveFork(.{}, io, allocator, repos_path, admin_repo, target_repo, .{ 1163 .id = patch_hex, 1164 .revision = .source, 1165 .author = patch_author, 1166 .timestamp = timestamp + 7, 1167 }); 1168 } else if (status != .open) { 1169 try evt.Patch.update(.xit, .{}, io, allocator, target_repo, &patch_ids[i], .{ .status = status }, patch_author); 1170 } 1171 } 1172 1173 // seed a small comment tree on the newest patch 1174 var comment_ids: [5][evt.event_id_size]u8 = undefined; 1175 for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(random); 1176 const comment_bodies = [_][]const u8{ 1177 "This also needs to preserve the executable bit for files nested inside generated directories.", 1178 "I tested the archive path and the direct-copy path; both currently lose the mode.", 1179 "The archive writer is the right place to centralize it, since every exporter already goes through that layer.", 1180 "Please include a regression fixture with a mix of executable and ordinary files.", 1181 "I'll add that fixture and cover both supported archive formats.", 1182 }; 1183 var comments: [comment_ids.len]evt.EventWithId = undefined; 1184 for (&comments, 0..) |*comment, i| { 1185 comment.* = .{ 1186 .id = std.fmt.bytesToHex(comment_ids[i], .lower), 1187 .timestamp = @intCast(700 + i), 1188 .author = .{ .name = if (i % 2 == 0) "alice" else "bob", .email = if (i % 2 == 0) "alice@example.test" else "bob@example.test" }, 1189 .event = .{ .comment = .{ 1190 .thread_id = std.fmt.bytesToHex(patch_ids[patch_ids.len - 1], .lower), 1191 .parent_id = switch (i) { 1192 0, 1 => std.fmt.bytesToHex(patch_ids[patch_ids.len - 1], .lower), 1193 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), 1194 3 => std.fmt.bytesToHex(comment_ids[2], .lower), 1195 else => unreachable, 1196 }, 1197 .body = comment_bodies[i], 1198 } }, 1199 }; 1200 } 1201 try evt.consume(.repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &comments); 1202 1203 // create divergent metadata edits on the next three patches 1204 var patch_arena = std.heap.ArenaAllocator.init(allocator); 1205 defer patch_arena.deinit(); 1206 const moment = try evt.currentMoment(.{}, target_repo); 1207 var values: [3]evt.Patch = undefined; 1208 for (1..4) |i| { 1209 const record = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &patch_arena, &patch_ids[i])) orelse return error.NotFound; 1210 values[i - 1] = record.event; 1211 } 1212 var ours_values = values; 1213 ours_values[0].title = "Drop the legacy configuration loader"; 1214 ours_values[0].tags = "cleanup config breaking"; 1215 const closed_description = try longDescription( 1216 patch_arena.allocator(), 1217 "Cache parsed manifests for the lifetime of a command invocation and invalidate entries when their files change.", 1218 ); 1219 ours_values[1].description = closed_description; 1220 ours_values[2].description = "Add JSON output to inspect with versioned field names and deterministic object ordering."; 1221 var theirs_values = values; 1222 theirs_values[0].title = "Delete compatibility configuration support"; 1223 theirs_values[0].tags = "config maintenance"; 1224 theirs_values[1].description = "Keep a process-wide manifest cache shared by every command and refresh it after writes."; 1225 theirs_values[2].description = "Expose inspect results as newline-delimited JSON so callers can stream large repositories."; 1226 1227 const seed_tip = (try target_repo.readRef(io, evt.events_ref)) orelse return error.NotFound; 1228 const other_ref = xit.ref.Ref{ .kind = .head, .name = "haxy/patch-other" }; 1229 var ours = [_]evt.EventWithId{ 1230 .{ 1231 .id = std.fmt.bytesToHex(patch_ids[1], .lower), 1232 .timestamp = 800, 1233 .author = .{ .name = "alice", .email = "alice@example.test" }, 1234 .event = .{ .patch = ours_values[0] }, 1235 }, 1236 .{ 1237 .id = std.fmt.bytesToHex(patch_ids[2], .lower), 1238 .timestamp = 801, 1239 .author = .{ .name = "alice", .email = "alice@example.test" }, 1240 .event = .{ .patch = ours_values[1] }, 1241 }, 1242 .{ 1243 .id = std.fmt.bytesToHex(patch_ids[3], .lower), 1244 .timestamp = 802, 1245 .author = .{ .name = "alice", .email = "alice@example.test" }, 1246 .event = .{ .patch = ours_values[2] }, 1247 }, 1248 }; 1249 1250 var theirs = ours; 1251 theirs[0].timestamp = 810; 1252 theirs[0].author = .{ .name = "bob", .email = "bob@example.test" }; 1253 theirs[0].event.patch = theirs_values[0]; 1254 theirs[1].timestamp = 811; 1255 theirs[1].author = .{ .name = "bob", .email = "bob@example.test" }; 1256 theirs[1].event.patch = theirs_values[1]; 1257 theirs[2].timestamp = 812; 1258 theirs[2].author = .{ .name = "bob", .email = "bob@example.test" }; 1259 theirs[2].event.patch = theirs_values[2]; 1260 1261 try evt.consume(.repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &ours); 1262 var json: std.Io.Writer.Allocating = .init(allocator); 1263 defer json.deinit(); 1264 for (theirs, 0..) |event, i| { 1265 json.clearRetainingCapacity(); 1266 try std.json.Stringify.value(event, .{}, &json.writer); 1267 const author = try std.fmt.allocPrint(allocator, "{s} <{s}>", .{ event.author.name, event.author.email }); 1268 defer allocator.free(author); 1269 _ = try target_repo.commitAtRef(io, allocator, .{ 1270 .author = author, 1271 .message = json.written(), 1272 .timestamp = event.timestamp, 1273 .parent_oids = if (i == 0) &.{seed_tip} else null, 1274 }, null, other_ref); 1275 } 1276 { 1277 var merge = try target_repo.mergeAtRef(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, evt.events_ref, null); 1278 defer merge.deinit(); 1279 if (merge.result != .success) return error.MergeFailed; 1280 } 1281 try target_repo.removeBranch(io, .{ .name = other_ref.name }); 1282 try evt.consume(.repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &.{}); 1283 } 1284 1285 // recursively copy the contents of src_dir into dest_dir 1286 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 1287 var iter = src_dir.iterate(); 1288 while (try iter.next(io)) |entry| { 1289 switch (entry.kind) { 1290 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 1291 .directory => { 1292 try dest_dir.createDirPath(io, entry.name); 1293 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1294 defer dest_entry_dir.close(io); 1295 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1296 defer src_entry_dir.close(io); 1297 try copyDir(io, src_entry_dir, dest_entry_dir); 1298 }, 1299 else => {}, 1300 } 1301 } 1302 } 1303