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