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