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