╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
224 225 const readme = try repo_dir.createFile(io, "README.md", .{}); 226 defer readme.close(io); 227 try readme.writeStreamingAll(io, 228 \\# Sample Repo 229 \\ 230 \\A repository seeded with test data for exercising the UI. 231 \\ 232 \\## Features 233 \\ 234 \\- Fast and lightweight 235 \\- Zero configuration required 236 \\- Works out of the box 237 \\ 238 \\## Installation 239 \\ 240 \\Clone the repository and build it from source: 241 \\ 242 \\```sh 243 \\git clone https://example.test/sample-repo.git 244 \\cd sample-repo 245 \\make install 246 \\``` 247 \\ 248 \\## Usage 249 \\ 250 \\Run the program with the `--help` flag to see all available 251 \\options: 252 \\ 253 \\```sh 254 \\sample-repo --help 255 \\``` 256 \\ 257 \\| Option | Description | 258 \\| ----------- | ------------------------------ | 259 \\| `--verbose` | Print extra diagnostic output | 260 \\| `--quiet` | Suppress all non-error output | 261 \\| `--version` | Print the version and exit | 262 \\ 263 \\## Contributing 264 \\ 265 \\Contributions are welcome! Please open an issue to discuss 266 \\any significant changes before submitting a pull request. 267 \\See the docs under `docs/dev` for more details. 268 \\ 269 \\## License 270 \\ 271 \\Released under the MIT License. See `LICENSE` for the full 272 \\text. 273 \\ 274 ); 275 276 try repo_dir.createDirPath(io, "docs/dev"); 277 const doc = try repo_dir.createFile(io, "docs/dev/contribute.md", .{}); 278 defer doc.close(io); 279 try doc.writeStreamingAll(io, "To contribute, please make a pull request"); 280 } 281 282 try template_repo.add(io, allocator, &.{ "README.md", "docs/dev/contribute.md" }); 283 _ = try template_repo.commit(io, allocator, .{ .message = "let there be light" }); 284 285 // tag every commit in creation order as v1, v2, and so on 286 var tag_num: usize = 1; 287 try addNextTag(&template_repo, io, allocator, &tag_num); 288 289 // a batch of commits so the commits tab has more than one page to 290 // paginate through. each rewrites a few files with scattered line 291 // edits, so every commit is a multi-file diff with several separate 292 // hunks to look at. stepped timestamps vary the date column. 293 const base_ts: u64 = 1_700_000_000; // 2023-11-14 294 const edit_files = [_][]const u8{ "src/alpha.txt", "src/beta.txt", "src/gamma.txt" }; 295 // commit subjects, cycled then padded to a varying length. 296 const subjects = [_][]const u8{ 297 "fix off-by-one in scatter loop", 298 "encode and decode buffers", 299 "tune kernel socket timeouts", 300 "render matrix vector product", 301 "gather falcon and otter stats", 302 "refactor badger walrus module", 303 "drop dead ferret marmot branch", 304 }; 305 const commit_count = 30; 306 var c: usize = 0; 307 while (c < commit_count) : (c += 1) { 308 { 309 var repo_dir = try cwd.openDir(io, template_path, .{}); 310 defer repo_dir.close(io); 311 try repo_dir.createDirPath(io, "src"); 312 for (edit_files, 0..) |path, fi| { 313 try writeScatterFile(io, allocator, repo_dir, path, fi, c); 314 } 315 } 316 try template_repo.add(io, allocator, &edit_files); 317 // a cycling subject padded to a c-varying length so the commit 318 // list shows messages of different widths, capped at 120. 319 var msg_writer = std.Io.Writer.Allocating.init(allocator); 320 defer msg_writer.deinit(); 321 try msg_writer.writer.print("{s}", .{subjects[c % subjects.len]}); 322 const msg_target = 16 + (c * 41) % 96; 323 var mw = c; 324 while (true) : (mw += 1) { 325 const word = scatter_words[mw % scatter_words.len]; 326 const len = msg_writer.written().len; 327 if (len >= msg_target or len + 1 + word.len > 120) break; 328 try msg_writer.writer.print(" {s}", .{word}); 329 } 330 // the newest commit's message runs past what the detail pane 331 // reads, so it shows the truncated message and its link. 332 if (c == commit_count - 1) { 333 var line: usize = 0; 334 while (msg_writer.written().len <= ui.Repo.Commits.max_message_size) : (line += 1) { 335 try msg_writer.writer.print("\n{d} {s}", .{ line, scatter_words[line % scatter_words.len] }); 336 } 337 } 338 const message = try arena.allocator().dupe(u8, msg_writer.written()); 339 _ = try template_repo.commit(io, allocator, .{ .message = message, .timestamp = base_ts + c * std.time.s_per_day }); 340 try addNextTag(&template_repo, io, allocator, &tag_num); 341 } 342 343 // two more branches forked off master, each adding a single commit 344 // that makes a scattered edit to one file. tag each new commit too, 345 // and switch back to master after each so the next branch also 346 // forks from master and the template ends up back on master. 347 const branch_data = [_]struct { 348 name: []const u8, 349 file: []const u8, 350 fi: usize, 351 message: []const u8, 352 rev: usize, 353 }{ 354 .{ .name = "extra", .file = "src/alpha.txt", .fi = 0, .message = "scatter alpha on the extra branch", .rev = 30 }, 355 .{ .name = "feature", .file = "src/beta.txt", .fi = 1, .message = "scatter beta on the feature branch", .rev = 31 }, 356 }; 357 for (branch_data) |b| { 358 try template_repo.addBranch(io, .{ .name = b.name }); 359 { 360 var to_branch = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = b.name } } }); 361 defer to_branch.deinit(); 362 } 363 364 { 365 var repo_dir = try cwd.openDir(io, template_path, .{}); 366 defer repo_dir.close(io); 367 try writeScatterFile(io, allocator, repo_dir, b.file, b.fi, b.rev); 368 } 369 try template_repo.add(io, allocator, &.{b.file}); 370 _ = try template_repo.commit(io, allocator, .{ .message = b.message, .timestamp = base_ts + b.rev * std.time.s_per_day }); 371 try addNextTag(&template_repo, io, allocator, &tag_num); 372 373 { 374 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 375 defer to_master.deinit(); 376 } 377 } 378 379 // seed issues so every repo's issue tracker has content 380 const issue_data = [_]struct { 381 title: []const u8, 382 description: []const u8, 383 tags: []const u8, 384 status: evt.Issue.Status = .open, 385 }{ 386 .{ 387 .title = "Crash on startup when config file is missing", 388 .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.", 389 .tags = "bug priority-high crash", 390 }, 391 .{ 392 .title = "Support dark mode in the settings panel", 393 .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.", 394 .tags = "enhancement ui theme", 395 }, 396 .{ 397 .title = "Memory leak when reconnecting after network failure", 398 .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.", 399 .tags = "bug memory networking", 400 .status = .closed, 401 }, 402 .{ 403 .title = "Document the plugin API", 404 .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.", 405 .tags = "documentation plugins", 406 }, 407 .{ 408 .title = "Slow file indexing on large directories", 409 .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.", 410 .tags = "performance indexing", 411 }, 412 .{ 413 .title = "Add keyboard shortcut for quick search", 414 .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.", 415 .tags = "enhancement ux keyboard", 416 }, 417 .{ 418 .title = "Unicode filenames are garbled in the export dialog", 419 .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.", 420 .tags = "bug unicode i18n", 421 }, 422 .{ 423 .title = "Flaky test: integration suite times out on CI", 424 .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.", 425 .tags = "bug testing ci", 426 }, 427 .{ 428 .title = "Upgrade bundled zlib to the latest release", 429 .description = "The vendored zlib is two major releases behind and misses several upstream fixes. Update the bundled copy and re-run the compression benchmarks.", 430 .tags = "dependencies maintenance", 431 }, 432 .{ 433 .title = "Progress bar overshoots 100% during resumed downloads", 434 .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.", 435 .tags = "bug ui downloads", 436 .status = .closed, 437 }, 438 .{ 439 .title = "Config parser rejects trailing commas", 440 .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.", 441 .tags = "bug config parser", 442 }, 443 .{ 444 .title = "Add a --json flag to the status command", 445 .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.", 446 .tags = "enhancement cli", 447 }, 448 .{ 449 .title = "Race condition between autosave and manual save", 450 .description = "Saving manually while an autosave is in flight can interleave the two writes and corrupt the file. Serialize saves through a single queue.", 451 .tags = "bug priority-high data-loss", 452 }, 453 .{ 454 .title = "Reduce binary size of release builds", 455 .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.", 456 .tags = "performance build", 457 }, 458 .{ 459 .title = "Tooltips flicker when the cursor moves between adjacent buttons", 460 .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.", 461 .tags = "bug ui polish", 462 }, 463 .{ 464 .title = "Support environment variable expansion in config paths", 465 .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.", 466 .tags = "enhancement config", 467 }, 468 .{ 469 .title = "Log rotation deletes the newest file instead of the oldest", 470 .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.", 471 .tags = "bug logging", 472 .status = .closed, 473 }, 474 .{ 475 .title = "Add man pages for all subcommands", 476 .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.", 477 .tags = "documentation cli", 478 }, 479 .{ 480 .title = "High CPU usage while idle in the background", 481 .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.", 482 .tags = "performance priority-high", 483 }, 484 .{ 485 .title = "Paste from clipboard drops the final newline", 486 .description = "Pasting text that ends with a newline silently trims it, which breaks pasted shell snippets. Preserve the clipboard content exactly.", 487 .tags = "bug editor clipboard", 488 }, 489 .{ 490 .title = "Improve error message for expired credentials", 491 .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.", 492 .tags = "enhancement ux auth", 493 }, 494 .{ 495 .title = "Crash when window is resized during startup animation", 496 .description = "Resizing the window while the splash animation is running dereferences a freed layout node. Cancel the animation before rebuilding the layout.", 497 .tags = "bug crash ui", 498 }, 499 .{ 500 .title = "Add integration tests for the sync engine", 501 .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.", 502 .tags = "testing sync", 503 }, 504 .{ 505 .title = "Timestamps display in UTC instead of local time", 506 .description = "All timestamps in the activity feed render in UTC regardless of the system timezone. Convert to local time and include the offset in tooltips.", 507 .tags = "bug i18n time", 508 }, 509 .{ 510 .title = "Deprecate the legacy plugin format", 511 .description = "Both plugin formats are currently loaded, doubling the maintenance surface. Warn on legacy plugins this release and drop support in the next.", 512 .tags = "maintenance plugins", 513 }, 514 }; 515 516 var issue_events: [issue_data.len]evt.EventWithId = undefined; 517 for (issue_data, 0..) |issue, i| { 518 // the newest issue's description runs past what the detail 519 // pane shows, so it shows the truncated description and its 520 // link. 521 const description = if (i == issue_data.len - 1) desc: { 522 var desc_writer = std.Io.Writer.Allocating.init(allocator); 523 defer desc_writer.deinit(); 524 try desc_writer.writer.print("{s}", .{issue.description}); 525 var line: usize = 0; 526 while (desc_writer.written().len <= ui.Repo.Issues.max_description_size) : (line += 1) { 527 try desc_writer.writer.print("\n{d} {s}", .{ line, scatter_words[line % scatter_words.len] }); 528 } 529 break :desc try arena.allocator().dupe(u8, desc_writer.written()); 530 } else issue.description; 531 issue_events[i] = .{ 532 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 533 // stepped timestamps so the issues list in a stable order 534 .timestamp = @intCast(i + 1), 535 .author_email = user_data[i % user_data.len].email, 536 .event = .{ 537 .issue = .{ 538 .title = issue.title, 539 .description = description, 540 .tags = issue.tags, 541 .status = issue.status, 542 }, 543 }, 544 }; 545 } 546 try evt.consume(.xit, .{}, io, allocator, &template_repo, evt.events_ref, &issue_events); 547 } 548 549 // copy the template to each repo's on-disk location, named by its 550 // hex-encoded event id. the template repo is deinitialized above, so its 551 // db file is fully written before we copy it. 552 { 553 var template_dir = try cwd.openDir(io, template_path, .{ .iterate = true }); 554 defer template_dir.close(io); 555 556 for (repo_event_ids) |id_bytes| { 557 const repo_id = std.fmt.bytesToHex(id_bytes, .lower); 558 const repo_path = try std.fs.path.join(arena.allocator(), &.{ cwd_path, temp_dir_name, "server", "repos", &repo_id }); 559 var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); 560 defer dest_dir.close(io); 561 try copyDir(io, template_dir, dest_dir); 562 } 563 } 564 565 break :blk try ui.Session.init(&session_arena, &repo, .{}); 566 }; 567 session.is_terminal = true; 568 // try uses the default serve options below, so the footer's url points at 569 // the default web UI port. 570 session.web_port = try (srv.Options{}).wuiPort(); 571 572 // start the server 573 574 var cli = false; 575 576 var arg_it = try init.minimal.args.iterateAllocator(allocator); 577 defer arg_it.deinit(); 578 _ = arg_it.skip(); 579 while (arg_it.next()) |arg| { 580 if (std.mem.eql(u8, "--cli", arg)) { 581 cli = true; 582 } 583 } 584 585 const server_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server" }); 586 defer allocator.free(server_path); 587 588 // let the native TUI's page builders open the on-disk repos for the file tree 589 session.io = io; 590 session.repos_dir = try std.fs.path.join(session_arena.allocator(), &.{ server_path, "repos" }); 591 592 if (cli) { 593 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 594 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 595 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 596 597 const key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 598 defer allocator.free(key_path); 599 600 const Runnable = struct { 601 io: std.Io, 602 key_path: []const u8, 603 604 pub fn run(self: @This()) !void { 605 std.debug.print( 606 \\ 607 \\connect to the TUI with: 608 \\ ssh -p 8022 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 609 \\ 610 \\create a git repo and push over SSH: 611 \\ mkdir -p temp-try/client/test 612 \\ cd temp-try/client/test 613 \\ git init 614 \\ echo "hello" > hello.txt 615 \\ git add hello.txt 616 \\ git commit -m "let there be light" 617 \\ GIT_SSH_COMMAND='ssh -p 8022 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -i {s}' git push localhost:admin/test HEAD:master 618 \\ 619 \\to quit, press enter. 620 \\ 621 , .{self.key_path}); 622 // portable stdin read via std.Io — equivalent to the 623 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 624 // on windows. 625 var buf: [1]u8 = undefined; 626 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 627 _ = stdin_reader.interface.takeByte() catch {}; 628 } 629 }; 630 631 try srv.run(.xit, .{}, io, allocator, cwd_path, .{ 632 .data_dir = server_path, 633 }, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 634 } else { 635 var null_writer = std.Io.Writer.Discarding.init(&.{}); 636 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 637 638 const Runnable = struct { 639 io: std.Io, 640 allocator: std.mem.Allocator, 641 session: *ui.Session, 642 repo: *Repo, 643 644 pub fn run(self: @This()) !void { 645 // launch the TUI 646 try hx.ui.run(self.io, self.allocator, self.session, self.repo); 647 } 648 }; 649 650 try srv.run(.xit, .{}, io, allocator, cwd_path, .{ 651 .data_dir = server_path, 652 }, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session, .repo = &repo }); 653 } 654 } 655 656 // words cycled through to pad generated lines into prose-like content 657 const scatter_words = [_][]const u8{ 658 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", 659 "quantum", "vector", "matrix", "buffer", "kernel", "socket", 660 "falcon", "otter", "badger", "walrus", "ferret", "marmot", 661 "scatter", "gather", "encode", "decode", "render", "commit", 662 }; 663 664 // write `path` as 40 lines of mostly-stable filler. revision `c` and file 665 // index `fi` shift which lines change and the words used, so each (c, fi) 666 // yields a distinct multi-hunk diff against the previous revision. untouched 667 // lines stay byte-identical across revisions, so a bump in `c` is a scatter of 668 // small hunks rather than a full rewrite. 669 fn writeScatterFile(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir, path: []const u8, fi: usize, c: usize) !void { 670 const file = try repo_dir.createFile(io, path, .{}); 671 defer file.close(io); 672 var writer = std.Io.Writer.Allocating.init(allocator); 673 defer writer.deinit(); 674 for (0..40) |line| { 675 const start = writer.written().len; 676 if ((line + fi + c) % 8 == 0) { 677 const tag = scatter_words[(line + fi + c * 3) % scatter_words.len]; 678 try writer.writer.print("rev {d} {s}", .{ c, tag }); 679 } 680 const target = 40 + (line * 17 + fi * 23) % 70; 681 var w = line + fi; 682 while (true) : (w += 1) { 683 const word = scatter_words[w % scatter_words.len]; 684 const len = writer.written().len - start; 685 if (len >= target or len + 1 + word.len > 120) break; 686 if (len == 0) 687 try writer.writer.print("{s}", .{word}) 688 else 689 try writer.writer.print(" {s}", .{word}); 690 } 691 try writer.writer.writeByte('\n'); 692 } 693 try file.writeStreamingAll(io, writer.written()); 694 } 695 696 // tag the current HEAD as the next sequential version: v1, v2, and so on 697 fn addNextTag(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { 698 var buf: [16]u8 = undefined; 699 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); 700 _ = try repo.addTag(io, allocator, .{ .name = name }); 701 n.* += 1; 702 } 703 704 // recursively copy the contents of src_dir into dest_dir 705 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 706 var iter = src_dir.iterate(); 707 while (try iter.next(io)) |entry| { 708 switch (entry.kind) { 709 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 710 .directory => { 711 try dest_dir.createDirPath(io, entry.name); 712 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 713 defer dest_entry_dir.close(io); 714 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 715 defer src_entry_dir.close(io); 716 try copyDir(io, src_entry_dir, dest_entry_dir); 717 }, 718 else => {}, 719 } 720 } 721 } 722