╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
369 { 370 var repo_dir = try cwd.openDir(io, template_path, .{}); 371 defer repo_dir.close(io); 372 try writeScatterFile(io, allocator, repo_dir, b.file, b.fi, b.rev); 373 } 374 try template_repo.add(io, allocator, &.{b.file}); 375 _ = try template_repo.commit(io, allocator, .{ .message = b.message, .timestamp = base_ts + b.rev * std.time.s_per_day }); 376 try addNextTag(&template_repo, io, allocator, &tag_num); 377 378 { 379 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 380 defer to_master.deinit(); 381 } 382 } 383 384 // the timestamps issue's description, in paragraphs, so the 385 // seeded conflict below can insert and delete among them 386 const tz_p1 = "All timestamps in the activity feed render in UTC regardless of the system timezone."; 387 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."; 388 const tz_p3 = "A quick survey of other clients shows every one of them rendering local time by default."; 389 const tz_p4 = "Convert to local time and include the offset in tooltips."; 390 391 // seed issues so every repo's issue tracker has content 392 const issue_data = [_]struct { 393 title: []const u8, 394 description: []const u8, 395 tags: []const u8, 396 status: evt.Issue.Status = .open, 397 }{ 398 .{ 399 .title = "Crash on startup when config file is missing", 400 .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.", 401 .tags = "bug priority-high crash", 402 }, 403 .{ 404 .title = "Support dark mode in the settings panel", 405 .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.", 406 .tags = "enhancement ui theme", 407 }, 408 .{ 409 .title = "Memory leak when reconnecting after network failure", 410 .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.", 411 .tags = "bug memory networking", 412 .status = .closed, 413 }, 414 .{ 415 .title = "Document the plugin API", 416 .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.", 417 .tags = "documentation plugins", 418 }, 419 .{ 420 .title = "Slow file indexing on large directories", 421 .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.", 422 .tags = "performance indexing", 423 }, 424 .{ 425 .title = "Add keyboard shortcut for quick search", 426 .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.", 427 .tags = "enhancement ux keyboard", 428 }, 429 .{ 430 .title = "Unicode filenames are garbled in the export dialog", 431 .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.", 432 .tags = "bug unicode i18n", 433 }, 434 .{ 435 .title = "Flaky test: integration suite times out on CI", 436 .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.", 437 .tags = "bug testing ci", 438 }, 439 .{ 440 .title = "Upgrade bundled zlib to the latest release", 441 .description = "The vendored zlib is two major releases behind and misses several upstream fixes. Update the bundled copy and re-run the compression benchmarks.", 442 .tags = "dependencies maintenance", 443 }, 444 .{ 445 .title = "Progress bar overshoots 100% during resumed downloads", 446 .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.", 447 .tags = "bug ui downloads", 448 .status = .closed, 449 }, 450 .{ 451 .title = "Config parser rejects trailing commas", 452 .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.", 453 .tags = "bug config parser", 454 }, 455 .{ 456 .title = "Add a --json flag to the status command", 457 .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.", 458 .tags = "enhancement cli", 459 }, 460 .{ 461 .title = "Race condition between autosave and manual save", 462 .description = "Saving manually while an autosave is in flight can interleave the two writes and corrupt the file. Serialize saves through a single queue.", 463 .tags = "bug priority-high data-loss", 464 }, 465 .{ 466 .title = "Reduce binary size of release builds", 467 .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.", 468 .tags = "performance build", 469 }, 470 .{ 471 .title = "Tooltips flicker when the cursor moves between adjacent buttons", 472 .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.", 473 .tags = "bug ui polish", 474 }, 475 .{ 476 .title = "Support environment variable expansion in config paths", 477 .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.", 478 .tags = "enhancement config", 479 }, 480 .{ 481 .title = "Log rotation deletes the newest file instead of the oldest", 482 .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.", 483 .tags = "bug logging", 484 .status = .closed, 485 }, 486 .{ 487 .title = "Add man pages for all subcommands", 488 .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.", 489 .tags = "documentation cli", 490 }, 491 .{ 492 .title = "High CPU usage while idle in the background", 493 .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.", 494 .tags = "performance priority-high", 495 }, 496 .{ 497 .title = "Paste from clipboard drops the final newline", 498 .description = "Pasting text that ends with a newline silently trims it, which breaks pasted shell snippets. Preserve the clipboard content exactly.", 499 .tags = "bug editor clipboard", 500 }, 501 .{ 502 .title = "Improve error message for expired credentials", 503 .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.", 504 .tags = "enhancement ux auth", 505 }, 506 .{ 507 .title = "Crash when window is resized during startup animation", 508 .description = "Resizing the window while the splash animation is running dereferences a freed layout node. Cancel the animation before rebuilding the layout.", 509 .tags = "bug crash ui", 510 }, 511 .{ 512 .title = "Add integration tests for the sync engine", 513 .description = "The sync engine only has unit tests with mocked transports. Add end-to-end tests that run two instances against a real local server.", 514 .tags = "testing sync", 515 }, 516 .{ 517 .title = "Timestamps display in UTC instead of local time", 518 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p3 ++ "\n\n" ++ tz_p4, 519 .tags = "bug i18n time", 520 }, 521 .{ 522 .title = "Deprecate the legacy plugin format", 523 .description = "Both plugin formats are currently loaded, doubling the maintenance surface. Warn on legacy plugins this release and drop support in the next.", 524 .tags = "maintenance plugins", 525 }, 526 }; 527 528 var issue_events: [issue_data.len]evt.EventWithId = undefined; 529 for (issue_data, 0..) |issue, i| { 530 // the newest issue's description runs past what the detail 531 // pane shows, so it shows the truncated description and its 532 // link. 533 const description = if (i == issue_data.len - 1) desc: { 534 var desc_writer = std.Io.Writer.Allocating.init(allocator); 535 defer desc_writer.deinit(); 536 try desc_writer.writer.print("{s}", .{issue.description}); 537 var line: usize = 0; 538 while (desc_writer.written().len <= ui.Repo.Issues.max_description_size) : (line += 1) { 539 try desc_writer.writer.print("\n{d} {s}", .{ line, scatter_words[line % scatter_words.len] }); 540 } 541 break :desc try arena.allocator().dupe(u8, desc_writer.written()); 542 } else issue.description; 543 issue_events[i] = .{ 544 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 545 // stepped timestamps so the issues list in a stable order 546 .timestamp = @intCast(i + 1), 547 .author_email = user_data[i % user_data.len].email, 548 .event = .{ 549 .issue = .{ 550 .title = issue.title, 551 .description = description, 552 .tags = issue.tags, 553 .status = issue.status, 554 }, 555 }, 556 }; 557 } 558 try evt.consume(.xit, .{}, io, allocator, &template_repo, evt.events_ref, &issue_events); 559 // the tip is the branch point for the conflicting edits below 560 const seed_tip = (try template_repo.readRef(io, evt.events_ref)) orelse unreachable; 561 562 // two divergent edits per conflicted issue: ours on the events 563 // branch, theirs on a temp branch rooted at the seed tip, then a 564 // merge. the 4th-newest issue conflicts on title and tags; the 565 // 2nd-newest on its description, with a deletion conflict (ours 566 // removes a paragraph theirs rewords) and an insertion conflict 567 // (each side appends a different closing paragraph). 568 { 569 const other_ref: xit.ref.Ref = .{ .kind = .head, .name = "haxy/other" }; 570 const title_issue = issue_data[issue_data.len - 4]; 571 const desc_issue = issue_data[issue_data.len - 2]; 572 573 const ours = [_]evt.EventWithId{ .{ 574 .id = issue_events[issue_data.len - 4].id, 575 .timestamp = 100, 576 .author_email = user_data[1].email, 577 .event = .{ .issue = .{ 578 .title = "Crash when resizing the window during the splash animation", 579 .description = title_issue.description, 580 .tags = "bug crash ui priority-high", 581 } }, 582 }, .{ 583 .id = issue_events[issue_data.len - 2].id, 584 .timestamp = 101, 585 .author_email = user_data[1].email, 586 .event = .{ .issue = .{ 587 .title = desc_issue.title, 588 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p4 ++ "\n\n" ++ 589 "The confusion is worst for teams spread across timezones, who each read a different wall-clock time from the same feed.", 590 .tags = desc_issue.tags, 591 } }, 592 } }; 593 const theirs = [_]evt.EventWithId{ .{ 594 .id = issue_events[issue_data.len - 4].id, 595 .timestamp = 102, 596 .author_email = user_data[2].email, 597 .event = .{ .issue = .{ 598 .title = "Segfault on early window resize", 599 .description = title_issue.description, 600 .tags = "bug crash rendering", 601 } }, 602 }, .{ 603 .id = issue_events[issue_data.len - 2].id, 604 .timestamp = 103, 605 .author_email = user_data[2].email, 606 .event = .{ .issue = .{ 607 .title = desc_issue.title, 608 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ 609 "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" ++ 610 "Log exports inherit the same UTC rendering, so downstream tooling has to guess the source timezone.", 611 .tags = desc_issue.tags, 612 } }, 613 } }; 614 615 try evt.consume(.xit, .{}, io, allocator, &template_repo, evt.events_ref, &ours); 616 617 // consume can't root a new branch, so theirs commits by hand 618 var json: std.Io.Writer.Allocating = .init(allocator); 619 defer json.deinit(); 620 for (theirs, 0..) |event, i| { 621 json.clearRetainingCapacity(); 622 try std.json.Stringify.value(event, .{}, &json.writer); 623 const author = try std.fmt.allocPrint(allocator, "haxy <{s}>", .{event.author_email}); 624 defer allocator.free(author); 625 _ = try template_repo.commitAtRef(io, allocator, .{ 626 .author = author, 627 .message = json.written(), 628 .timestamp = event.timestamp, 629 // root the branch at the shared seed tip 630 .parent_oids = if (i == 0) &.{seed_tip} else null, 631 }, null, other_ref); 632 } 633 634 { 635 var to_events = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 636 defer to_events.deinit(); 637 } 638 { 639 var merge = try template_repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, null); 640 defer merge.deinit(); 641 if (merge.result != .success) return error.MergeFailed; 642 } 643 { 644 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 645 defer to_master.deinit(); 646 } 647 try template_repo.removeBranch(io, .{ .name = other_ref.name }); 648 } 649 650 try evt.consume(.xit, .{}, io, allocator, &template_repo, evt.events_ref, &.{}); 651 } 652 653 // copy the template to each repo's on-disk location, named by its 654 // hex-encoded event id. the template repo is deinitialized above, so its 655 // db file is fully written before we copy it. 656 { 657 var template_dir = try cwd.openDir(io, template_path, .{ .iterate = true }); 658 defer template_dir.close(io); 659 660 for (repo_event_ids) |id_bytes| { 661 const repo_id = std.fmt.bytesToHex(id_bytes, .lower); 662 const repo_path = try std.fs.path.join(arena.allocator(), &.{ cwd_path, temp_dir_name, "server", "repos", &repo_id }); 663 var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); 664 defer dest_dir.close(io); 665 try copyDir(io, template_dir, dest_dir); 666 } 667 } 668 669 break :blk try ui.Session.init(&session_arena, &repo, .{ .user_id = &admin_user_id }); 670 }; 671 session.is_terminal = true; 672 673 // start the server 674 675 var cli = false; 676 677 var arg_it = try init.minimal.args.iterateAllocator(allocator); 678 defer arg_it.deinit(); 679 _ = arg_it.skip(); 680 while (arg_it.next()) |arg| { 681 if (std.mem.eql(u8, "--cli", arg)) { 682 cli = true; 683 } 684 } 685 686 const server_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server" }); 687 defer allocator.free(server_path); 688 689 // let the native TUI's page builders open the on-disk repos for the file tree 690 session.io = io; 691 session.repos_dir = try std.fs.path.join(session_arena.allocator(), &.{ server_path, "repos" }); 692 693 // leave a one-shot session for the first browser to hit the web ui, so it 694 // starts logged in as admin 695 { 696 var data_dir = try cwd.createDirPathOpen(io, server_path, .{}); 697 defer data_dir.close(io); 698 const store = try hx.web.SessionStore.init(io, data_dir); 699 defer store.deinit(); 700 const token = try store.create(&admin_user_id); 701 try store.offerAutoLogin(&token); 702 } 703 704 if (cli) { 705 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 706 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 707 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 708 709 const key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 710 defer allocator.free(key_path); 711 712 const Runnable = struct { 713 io: std.Io, 714 key_path: []const u8, 715 716 pub fn run(self: @This(), web_port: u16, ssh_port: u16) !void { 717 _ = web_port; 718 std.debug.print( 719 \\ 720 \\connect to the TUI with: 721 \\ ssh -p {d} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 722 \\ 723 \\create a git repo and push over SSH: 724 \\ mkdir -p temp-try/client/test 725 \\ cd temp-try/client/test 726 \\ git init 727 \\ echo "hello" > hello.txt 728 \\ git add hello.txt 729 \\ git commit -m "let there be light" 730 \\ 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 731 \\ 732 \\to quit, press enter. 733 \\ 734 , .{ ssh_port, ssh_port, self.key_path }); 735 // portable stdin read via std.Io — equivalent to the 736 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 737 // on windows. 738 var buf: [1]u8 = undefined; 739 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 740 _ = stdin_reader.interface.takeByte() catch {}; 741 } 742 }; 743 744 try srv.run(.xit, .{}, io, allocator, cwd_path, .{ 745 .data_dir = server_path, 746 }, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 747 } else { 748 var null_writer = std.Io.Writer.Discarding.init(&.{}); 749 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 750 751 const Runnable = struct { 752 io: std.Io, 753 allocator: std.mem.Allocator, 754 session: *ui.Session, 755 repo: *Repo, 756 757 pub fn run(self: @This(), web_port: u16, ssh_port: u16) !void { 758 _ = ssh_port; 759 // launch the TUI; the footer points at whatever port was bound 760 self.session.web_port = web_port; 761 try hx.ui.run(self.io, self.allocator, self.session, self.repo); 762 } 763 }; 764 765 try srv.run(.xit, .{}, io, allocator, cwd_path, .{ 766 .data_dir = server_path, 767 }, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session, .repo = &repo }); 768 } 769 } 770 771 // words cycled through to pad generated lines into prose-like content 772 const scatter_words = [_][]const u8{ 773 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", 774 "quantum", "vector", "matrix", "buffer", "kernel", "socket", 775 "falcon", "otter", "badger", "walrus", "ferret", "marmot", 776 "scatter", "gather", "encode", "decode", "render", "commit", 777 }; 778 779 // write `path` as 40 lines of mostly-stable filler. revision `c` and file 780 // index `fi` shift which lines change and the words used, so each (c, fi) 781 // yields a distinct multi-hunk diff against the previous revision. untouched 782 // lines stay byte-identical across revisions, so a bump in `c` is a scatter of 783 // small hunks rather than a full rewrite. 784 fn writeScatterFile(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir, path: []const u8, fi: usize, c: usize) !void { 785 const file = try repo_dir.createFile(io, path, .{}); 786 defer file.close(io); 787 var writer = std.Io.Writer.Allocating.init(allocator); 788 defer writer.deinit(); 789 for (0..40) |line| { 790 const start = writer.written().len; 791 if ((line + fi + c) % 8 == 0) { 792 const tag = scatter_words[(line + fi + c * 3) % scatter_words.len]; 793 try writer.writer.print("rev {d} {s}", .{ c, tag }); 794 } 795 const target = 40 + (line * 17 + fi * 23) % 70; 796 var w = line + fi; 797 while (true) : (w += 1) { 798 const word = scatter_words[w % scatter_words.len]; 799 const len = writer.written().len - start; 800 if (len >= target or len + 1 + word.len > 120) break; 801 if (len == 0) 802 try writer.writer.print("{s}", .{word}) 803 else 804 try writer.writer.print(" {s}", .{word}); 805 } 806 try writer.writer.writeByte('\n'); 807 } 808 try file.writeStreamingAll(io, writer.written()); 809 } 810 811 // tag the current HEAD as the next sequential version: v1, v2, and so on 812 fn addNextTag(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { 813 var buf: [16]u8 = undefined; 814 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); 815 _ = try repo.addTag(io, allocator, .{ .name = name }); 816 n.* += 1; 817 } 818 819 // recursively copy the contents of src_dir into dest_dir 820 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 821 var iter = src_dir.iterate(); 822 while (try iter.next(io)) |entry| { 823 switch (entry.kind) { 824 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 825 .directory => { 826 try dest_dir.createDirPath(io, entry.name); 827 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 828 defer dest_entry_dir.close(io); 829 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 830 defer src_entry_dir.close(io); 831 try copyDir(io, src_entry_dir, dest_entry_dir); 832 }, 833 else => {}, 834 } 835 } 836 } 837