╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
430 .tags = "bug priority-high crash", 431 }, 432 .{ 433 .title = "Support dark mode in the settings panel", 434 .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.", 435 .tags = "enhancement ui theme", 436 }, 437 .{ 438 .title = "Memory leak when reconnecting after network failure", 439 .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.", 440 .tags = "bug memory networking", 441 .status = .closed, 442 }, 443 .{ 444 .title = "Document the plugin API", 445 .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.", 446 .tags = "documentation plugins", 447 }, 448 .{ 449 .title = "Slow file indexing on large directories", 450 .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.", 451 .tags = "performance indexing", 452 }, 453 .{ 454 .title = "Add keyboard shortcut for quick search", 455 .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.", 456 .tags = "enhancement ux keyboard", 457 }, 458 .{ 459 .title = "Unicode filenames are garbled in the export dialog", 460 .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.", 461 .tags = "bug unicode i18n", 462 }, 463 .{ 464 .title = "Flaky test: integration suite times out on CI", 465 .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.", 466 .tags = "bug testing ci", 467 }, 468 .{ 469 .title = "Upgrade bundled zlib to the latest release", 470 .description = "The vendored zlib is two major releases behind and misses several upstream fixes. Update the bundled copy and re-run the compression benchmarks.", 471 .tags = "dependencies maintenance", 472 }, 473 .{ 474 .title = "Progress bar overshoots 100% during resumed downloads", 475 .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.", 476 .tags = "bug ui downloads", 477 .status = .closed, 478 }, 479 .{ 480 .title = "Config parser rejects trailing commas", 481 .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.", 482 .tags = "bug config parser", 483 }, 484 .{ 485 .title = "Add a --json flag to the status command", 486 .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.", 487 .tags = "enhancement cli", 488 }, 489 .{ 490 .title = "Race condition between autosave and manual save", 491 .description = "Saving manually while an autosave is in flight can interleave the two writes and corrupt the file. Serialize saves through a single queue.", 492 .tags = "bug priority-high data-loss", 493 }, 494 .{ 495 .title = "Reduce binary size of release builds", 496 .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.", 497 .tags = "performance build", 498 }, 499 .{ 500 .title = "Tooltips flicker when the cursor moves between adjacent buttons", 501 .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.", 502 .tags = "bug ui polish", 503 }, 504 .{ 505 .title = "Support environment variable expansion in config paths", 506 .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.", 507 .tags = "enhancement config", 508 }, 509 .{ 510 .title = "Log rotation deletes the newest file instead of the oldest", 511 .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.", 512 .tags = "bug logging", 513 .status = .closed, 514 }, 515 .{ 516 .title = "Add man pages for all subcommands", 517 .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.", 518 .tags = "documentation cli", 519 }, 520 .{ 521 .title = "High CPU usage while idle in the background", 522 .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.", 523 .tags = "performance priority-high", 524 }, 525 .{ 526 .title = "Paste from clipboard drops the final newline", 527 .description = "Pasting text that ends with a newline silently trims it, which breaks pasted shell snippets. Preserve the clipboard content exactly.", 528 .tags = "bug editor clipboard", 529 }, 530 .{ 531 .title = "Improve error message for expired credentials", 532 .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.", 533 .tags = "enhancement ux auth", 534 }, 535 .{ 536 .title = "Crash when window is resized during startup animation", 537 .description = "Resizing the window while the splash animation is running dereferences a freed layout node. Cancel the animation before rebuilding the layout.", 538 .tags = "bug crash ui", 539 }, 540 .{ 541 .title = "Add integration tests for the sync engine", 542 .description = sync_p1 ++ "\n\n" ++ sync_p2 ++ "\n\n" ++ sync_p3, 543 .tags = "testing sync", 544 }, 545 .{ 546 .title = "Timestamps display in UTC instead of local time", 547 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p3 ++ "\n\n" ++ tz_p4, 548 .tags = "bug i18n time", 549 }, 550 .{ 551 .title = "Deprecate the legacy plugin format", 552 .description = "Both plugin formats are currently loaded, doubling the maintenance surface. Warn on legacy plugins this release and drop support in the next.", 553 .tags = "maintenance plugins", 554 }, 555 }; 556 557 var issue_events: [issue_data.len]evt.EventWithId = undefined; 558 for (issue_data, 0..) |issue, i| { 559 // the fifth-newest issue's description runs past what the detail 560 // pane shows, so it shows the truncated description and its 561 // link. 562 const description = if (i == issue_data.len - 5) desc: { 563 var desc_writer = std.Io.Writer.Allocating.init(allocator); 564 defer desc_writer.deinit(); 565 try desc_writer.writer.print("{s}", .{issue.description}); 566 var line: usize = 0; 567 while (desc_writer.written().len <= ui.Repo.Issues.max_description_size) : (line += 1) { 568 try desc_writer.writer.print("\n{d} {s}", .{ line, scatter_words[line % scatter_words.len] }); 569 } 570 break :desc try arena.allocator().dupe(u8, desc_writer.written()); 571 } else issue.description; 572 issue_events[i] = .{ 573 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 574 // stepped timestamps so the issues list in a stable order 575 .timestamp = @intCast(i + 1), 576 .author = .{ .name = user_data[i % user_data.len].name, .email = user_data[i % user_data.len].email }, 577 .event = .{ 578 .issue = .{ 579 .title = issue.title, 580 .description = description, 581 .tags = issue.tags, 582 .status = issue.status, 583 }, 584 }, 585 }; 586 } 587 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &issue_events); 588 // the tip is the branch point for the conflicting edits below 589 const seed_tip = (try template_repo.readRef(io, evt.events_ref)) orelse unreachable; 590 591 // two divergent edits per conflicted issue: ours on the events 592 // branch, theirs on a temp branch rooted at the seed tip, then a 593 // merge. the 4th-newest issue conflicts on title and tags; the 594 // 3rd-newest on its description with every hunk auto-resolving 595 // (each side reworks a different paragraph); the 2nd-newest on 596 // its description with a removal conflict (ours removes a 597 // paragraph theirs rewords) and an insertion conflict (each side 598 // appends a different closing paragraph). 599 { 600 const other_ref: xit.ref.Ref = .{ .kind = .head, .name = "haxy/other" }; 601 const title_issue = issue_data[issue_data.len - 4]; 602 const sync_issue = issue_data[issue_data.len - 3]; 603 const desc_issue = issue_data[issue_data.len - 2]; 604 605 const ours = [_]evt.EventWithId{ .{ 606 .id = issue_events[issue_data.len - 4].id, 607 .timestamp = 100, 608 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 609 .event = .{ .issue = .{ 610 .title = "Crash when resizing the window during the splash animation", 611 .description = title_issue.description, 612 .tags = "bug crash ui priority-high", 613 } }, 614 }, .{ 615 .id = issue_events[issue_data.len - 3].id, 616 .timestamp = 101, 617 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 618 .event = .{ .issue = .{ 619 .title = sync_issue.title, 620 .description = "The sync engine's coverage is unit tests only, with every transport mocked out." ++ "\n\n" ++ 621 sync_p2 ++ "\n\n" ++ sync_p3, 622 .tags = sync_issue.tags, 623 } }, 624 }, .{ 625 .id = issue_events[issue_data.len - 2].id, 626 .timestamp = 102, 627 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 628 .event = .{ .issue = .{ 629 .title = desc_issue.title, 630 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p4 ++ "\n\n" ++ 631 "The confusion is worst for teams spread across timezones, who each read a different wall-clock time from the same feed.", 632 .tags = desc_issue.tags, 633 } }, 634 } }; 635 const theirs = [_]evt.EventWithId{ .{ 636 .id = issue_events[issue_data.len - 4].id, 637 .timestamp = 103, 638 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 639 .event = .{ .issue = .{ 640 .title = "Segfault on early window resize", 641 .description = title_issue.description, 642 .tags = "bug crash rendering", 643 } }, 644 }, .{ 645 .id = issue_events[issue_data.len - 3].id, 646 .timestamp = 104, 647 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 648 .event = .{ .issue = .{ 649 .title = sync_issue.title, 650 .description = sync_p1 ++ "\n\n" ++ sync_p2 ++ "\n\n" ++ 651 "Add end-to-end tests that drive two live instances against a local server on every CI run.", 652 .tags = sync_issue.tags, 653 } }, 654 }, .{ 655 .id = issue_events[issue_data.len - 2].id, 656 .timestamp = 105, 657 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 658 .event = .{ .issue = .{ 659 .title = desc_issue.title, 660 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ 661 "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" ++ 662 "Log exports inherit the same UTC rendering, so downstream tooling has to guess the source timezone.", 663 .tags = desc_issue.tags, 664 } }, 665 } }; 666 667 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &ours); 668 669 // consume can't root a new branch, so theirs commits by hand 670 var json: std.Io.Writer.Allocating = .init(allocator); 671 defer json.deinit(); 672 for (theirs, 0..) |event, i| { 673 json.clearRetainingCapacity(); 674 try std.json.Stringify.value(event, .{}, &json.writer); 675 const author = try std.fmt.allocPrint(allocator, "{s} <{s}>", .{ event.author.name, event.author.email }); 676 defer allocator.free(author); 677 _ = try template_repo.commitAtRef(io, allocator, .{ 678 .author = author, 679 .message = json.written(), 680 .timestamp = event.timestamp, 681 // root the branch at the shared seed tip 682 .parent_oids = if (i == 0) &.{seed_tip} else null, 683 }, null, other_ref); 684 } 685 686 { 687 var to_events = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 688 defer to_events.deinit(); 689 } 690 { 691 var merge = try template_repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, null); 692 defer merge.deinit(); 693 if (merge.result != .success) return error.MergeFailed; 694 } 695 { 696 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 697 defer to_master.deinit(); 698 } 699 try template_repo.removeBranch(io, .{ .name = other_ref.name }); 700 } 701 702 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &.{}); 703 704 // seed a small comment tree on the newest issue 705 var comment_ids: [5][evt.event_id_size]u8 = undefined; 706 for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 707 708 const comment_bodies = [_][]const u8{ 709 "I can take this. The compatibility scanner already reports which repositories still use the legacy format.", 710 "Can we keep loading legacy plugins for one release after the warning lands? That would give downstream maintainers time to migrate.", 711 "That sounds good. I'll include the scanner output in the warning so each affected plugin is named.", 712 "Please include the replacement manifest path too. That should make the warning actionable without opening the migration guide.", 713 "I'll add release-note text once the warning format is settled.", 714 }; 715 716 var comment_events: [comment_ids.len]evt.EventWithId = undefined; 717 for (&comment_events, 0..) |*event, i| { 718 event.* = .{ 719 .id = std.fmt.bytesToHex(comment_ids[i], .lower), 720 .timestamp = @intCast(200 + i), 721 .author = .{ .name = user_data[(i + 1) % user_data.len].name, .email = user_data[(i + 1) % user_data.len].email }, 722 .event = .{ .comment = .{ 723 .thread_id = issue_events[issue_events.len - 1].id, 724 .parent_id = switch (i) { 725 0, 1 => issue_events[issue_events.len - 1].id, 726 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), 727 3 => std.fmt.bytesToHex(comment_ids[2], .lower), 728 else => unreachable, 729 }, 730 .body = comment_bodies[i], 731 } }, 732 }; 733 } 734 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &comment_events); 735 736 const discussion_data = [_]struct { 737 title: []const u8, 738 description: []const u8, 739 tags: []const u8, 740 }{ 741 .{ 742 .title = "How should plugins declare capabilities?", 743 .description = "I'd like the manifest to make privileged capabilities explicit without making simple plugins verbose.", 744 .tags = "plugins design", 745 }, 746 .{ 747 .title = "Ideas for making large repositories faster", 748 .description = "This is a place to collect profiling results and discuss which indexing work is worth pursuing first.", 749 .tags = "performance indexing", 750 }, 751 .{ 752 .title = "What should the next release focus on?", 753 .description = "Let's compare the most important reliability fixes with the larger features already in progress.", 754 .tags = "release planning", 755 }, 756 .{ 757 .title = "Improving keyboard navigation", 758 .description = "Share workflows that still require a mouse and suggestions for making their focus behavior predictable.", 759 .tags = "ui keyboard accessibility", 760 }, 761 .{ 762 .title = "Configuration format discussion", 763 .description = "Should the next configuration format favor strict validation or accept common conveniences such as trailing commas?", 764 .tags = "config design", 765 }, 766 }; 767 768 var discussion_events: [discussion_data.len]evt.EventWithId = undefined; 769 for (discussion_data, 0..) |discussion, i| { 770 discussion_events[i] = .{ 771 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 772 .timestamp = @intCast(300 + i), 773 .author = .{ .name = user_data[i % user_data.len].name, .email = user_data[i % user_data.len].email }, 774 .event = .{ .discuss = .{ 775 .title = discussion.title, 776 .description = discussion.description, 777 .tags = discussion.tags, 778 } }, 779 }; 780 } 781 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &discussion_events); 782 783 var discussion_comment_ids: [discussion_data.len][3][evt.event_id_size]u8 = undefined; 784 for (&discussion_comment_ids) |*ids| { 785 for (ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 786 } 787 var discussion_comment_events: [discussion_data.len * 3]evt.EventWithId = undefined; 788 for (&discussion_comment_events, 0..) |*event, i| { 789 const discussion_index = i / 3; 790 const comment_index = i % 3; 791 event.* = .{ 792 .id = std.fmt.bytesToHex(discussion_comment_ids[discussion_index][comment_index], .lower), 793 .timestamp = @intCast(400 + i), 794 .author = .{ .name = user_data[(i + 1) % user_data.len].name, .email = user_data[(i + 1) % user_data.len].email }, 795 .event = .{ .comment = .{ 796 .thread_id = discussion_events[discussion_index].id, 797 .parent_id = if (comment_index < 2) 798 discussion_events[discussion_index].id 799 else 800 std.fmt.bytesToHex(discussion_comment_ids[discussion_index][0], .lower), 801 .body = switch (comment_index) { 802 0 => "My preference is to start with the smallest useful version and expand it after we have real usage to learn from.", 803 1 => "It would help to write down the tradeoffs we are accepting so the decision is easy to revisit later.", 804 2 => "Agreed. A narrow first version should also make it easier to keep the implementation understandable.", 805 else => unreachable, 806 }, 807 } }, 808 }; 809 } 810 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &discussion_comment_events); 811 } 812 813 // copy the template to each repo's on-disk location, named by its 814 // hex-encoded event id. the template repo is deinitialized above, so its 815 // db file is fully written before we copy it. 816 { 817 var template_dir = try cwd.openDir(io, template_path, .{ .iterate = true }); 818 defer template_dir.close(io); 819 820 for (repo_event_ids) |id_bytes| { 821 const repo_id = std.fmt.bytesToHex(id_bytes, .lower); 822 const repo_path = try std.fs.path.join(arena.allocator(), &.{ server_path, "repos", &repo_id }); 823 var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); 824 defer dest_dir.close(io); 825 try copyDir(io, template_dir, dest_dir); 826 } 827 } 828 829 break :blk try ui.Session.init(&session_arena, &repo, .{ .user_id = &admin_user_id }); 830 }; 831 session.is_terminal = true; 832 833 // start the server 834 835 // let the native TUI's page builders open the on-disk repos for the file tree 836 session.io = io; 837 session.repos_dir = try std.fs.path.join(session_arena.allocator(), &.{ server_path, "repos" }); 838 839 // leave a one-shot session for the first browser to hit the web ui, so it 840 // starts logged in as admin 841 { 842 var data_dir = try cwd.createDirPathOpen(io, server_path, .{}); 843 defer data_dir.close(io); 844 const store = try hx.web.SessionStore.init(io, data_dir); 845 defer store.deinit(); 846 const token = try store.create(&admin_user_id); 847 try store.offerAutoLogin(&token); 848 } 849 850 if (cli) { 851 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 852 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 853 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 854 855 const key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 856 defer allocator.free(key_path); 857 858 const Runnable = struct { 859 io: std.Io, 860 key_path: []const u8, 861 862 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port_maybe: ?u16) !void { 863 _ = web_port; 864 _ = http_port; 865 const ssh_port = ssh_port_maybe orelse return error.SshDisabled; 866 std.debug.print( 867 \\ 868 \\connect to the TUI with: 869 \\ ssh -p {d} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 870 \\ 871 \\create a git repo and push over SSH: 872 \\ mkdir -p temp-try/client/test 873 \\ cd temp-try/client/test 874 \\ git init 875 \\ echo "hello" > hello.txt 876 \\ git add hello.txt 877 \\ git commit -m "let there be light" 878 \\ 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 879 \\ 880 \\to quit, press enter. 881 \\ 882 , .{ ssh_port, ssh_port, self.key_path }); 883 // portable stdin read via std.Io — equivalent to the 884 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 885 // on windows. 886 var buf: [1]u8 = undefined; 887 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 888 _ = stdin_reader.interface.takeByte() catch {}; 889 } 890 }; 891 892 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 893 } else { 894 var null_writer = std.Io.Writer.Discarding.init(&.{}); 895 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 896 897 const Runnable = struct { 898 io: std.Io, 899 allocator: std.mem.Allocator, 900 session: *ui.Session, 901 repo: *Repo, 902 903 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port: ?u16) !void { 904 // launch the TUI; the footer points at whatever port was bound 905 self.session.web_port = web_port; 906 self.session.data.clone_http_port = http_port; 907 self.session.data.clone_ssh_port = ssh_port; 908 try hx.ui.run(self.io, self.allocator, self.session, self.repo); 909 } 910 }; 911 912 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session, .repo = &repo }); 913 } 914 } 915 916 // words cycled through to pad generated lines into prose-like content 917 const scatter_words = [_][]const u8{ 918 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", 919 "quantum", "vector", "matrix", "buffer", "kernel", "socket", 920 "falcon", "otter", "badger", "walrus", "ferret", "marmot", 921 "scatter", "gather", "encode", "decode", "render", "commit", 922 }; 923 924 // write `path` as 40 lines of mostly-stable filler. revision `c` and file 925 // index `fi` shift which lines change and the words used, so each (c, fi) 926 // yields a distinct multi-hunk diff against the previous revision. untouched 927 // lines stay byte-identical across revisions, so a bump in `c` is a scatter of 928 // small hunks rather than a full rewrite. 929 fn writeScatterFile(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir, path: []const u8, fi: usize, c: usize) !void { 930 const file = try repo_dir.createFile(io, path, .{}); 931 defer file.close(io); 932 var writer = std.Io.Writer.Allocating.init(allocator); 933 defer writer.deinit(); 934 for (0..40) |line| { 935 const start = writer.written().len; 936 if ((line + fi + c) % 8 == 0) { 937 const tag = scatter_words[(line + fi + c * 3) % scatter_words.len]; 938 try writer.writer.print("rev {d} {s}", .{ c, tag }); 939 } 940 const target = 40 + (line * 17 + fi * 23) % 70; 941 var w = line + fi; 942 while (true) : (w += 1) { 943 const word = scatter_words[w % scatter_words.len]; 944 const len = writer.written().len - start; 945 if (len >= target or len + 1 + word.len > 120) break; 946 if (len == 0) 947 try writer.writer.print("{s}", .{word}) 948 else 949 try writer.writer.print(" {s}", .{word}); 950 } 951 try writer.writer.writeByte('\n'); 952 } 953 try file.writeStreamingAll(io, writer.written()); 954 } 955 956 // tag the current HEAD as the next sequential version: v1, v2, and so on 957 fn addNextTag(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { 958 var buf: [16]u8 = undefined; 959 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); 960 _ = try repo.addTag(io, allocator, .{ .name = name }); 961 n.* += 1; 962 } 963 964 // recursively copy the contents of src_dir into dest_dir 965 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 966 var iter = src_dir.iterate(); 967 while (try iter.next(io)) |entry| { 968 switch (entry.kind) { 969 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 970 .directory => { 971 try dest_dir.createDirPath(io, entry.name); 972 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 973 defer dest_entry_dir.close(io); 974 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 975 defer src_entry_dir.close(io); 976 try copyDir(io, src_entry_dir, dest_entry_dir); 977 }, 978 else => {}, 979 } 980 } 981 } 982