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