╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
151 .{ .user_index = 13, .name = "flask", .description = "The Python micro framework for building web applications" }, 152 .{ .user_index = 14, .name = "django", .description = "The web framework for perfectionists with deadlines" }, 153 .{ .user_index = 15, .name = "rails", .description = "Ruby on Rails web framework" }, 154 .{ .user_index = 16, .name = "phoenix", .description = "Peace of mind from prototype to production for Elixir web apps" }, 155 .{ .user_index = 17, .name = "laravel", .description = "The PHP framework for web artisans" }, 156 .{ .user_index = 18, .name = "prometheus", .description = "The Prometheus monitoring system and time series database" }, 157 .{ .user_index = 19, .name = "grafana", .description = "The open and composable observability and data visualization platform" }, 158 .{ .user_index = 20, .name = "ansible", .description = "Simple, agentless IT automation" }, 159 }; 160 161 var user_ids: [user_data.len][evt.event_id_size]u8 = undefined; 162 for (&user_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 163 164 var repo_event_ids: [repo_data.len][evt.event_id_size]u8 = undefined; 165 for (&repo_event_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 166 167 var password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 168 const password_hash = try evt.User.hashPassword("password", &password_hash_buf, io); 169 170 // public key matching temp-try/key, given to admin so we can push as admin 171 const admin_ssh_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKeIs8mJqigBZ5y84J4COgnAJJ5bHPKy+lM2SliMXbYm radar@roark"; 172 173 var events_to_consume: [user_data.len + repo_data.len]evt.EventWithId = undefined; 174 for (user_data, 0..) |u, i| { 175 events_to_consume[i] = .{ 176 .id = std.fmt.bytesToHex(user_ids[i], .lower), 177 // stepped timestamps so the seeded users/repos list in a stable order 178 .timestamp = @intCast(i + 1), 179 .event = .{ 180 .user = .{ 181 .name = u.name, 182 .display_name = u.display_name, 183 .email = u.email, 184 .password_hash = password_hash, 185 .ssh_keys = if (i == 0) admin_ssh_key else "", 186 }, 187 }, 188 }; 189 } 190 for (repo_data, 0..) |r, i| { 191 events_to_consume[user_data.len + i] = .{ 192 .id = std.fmt.bytesToHex(repo_event_ids[i], .lower), 193 .timestamp = @intCast(user_data.len + i + 1), 194 .event = .{ 195 .repo = .{ 196 .user_id = &user_ids[r.user_index], 197 .name = r.name, 198 .description = r.description, 199 .enable_issue = true, 200 }, 201 }, 202 }; 203 } 204 205 // commit the seed events and consume them into the database 206 try evt.commitAndConsume(evt.admin_repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume); 207 208 // every repo gets the same generated history, so build it once into a 209 // template repo and copy that to each repo's location below rather than 210 // redoing the expensive commit work for every repo. 211 const template_path = try std.fs.path.join(arena.allocator(), &.{ cwd_path, temp_dir_name, "template" }); 212 { 213 var template_repo = try rp.Repo(.xit, .{}).init(io, allocator, .{ .path = template_path }); 214 defer template_repo.deinit(io, allocator); 215 216 try template_repo.addConfig(io, allocator, .{ .name = "user.name", .value = "haxy" }); 217 try template_repo.addConfig(io, allocator, .{ .name = "user.email", .value = "admin@haxy" }); 218 219 // a README plus a nested doc so the file tree has a directory to 220 // descend into 221 { 222 var repo_dir = try cwd.openDir(io, template_path, .{}); 223 defer repo_dir.close(io); 224 225 const readme = try repo_dir.createFile(io, "README.md", .{}); 226 defer readme.close(io); 227 try readme.writeStreamingAll(io, 228 \\# Sample Repo 229 \\ 230 \\A repository seeded with test data for exercising the UI. 231 \\ 232 \\## Features 233 \\ 234 \\- Fast and lightweight 235 \\- Zero configuration required 236 \\- Works out of the box 237 \\ 238 \\## Installation 239 \\ 240 \\Clone the repository and build it from source: 241 \\ 242 \\```sh 243 \\git clone https://example.test/sample-repo.git 244 \\cd sample-repo 245 \\make install 246 \\``` 247 \\ 248 \\## Usage 249 \\ 250 \\Run the program with the `--help` flag to see all available 251 \\options: 252 \\ 253 \\```sh 254 \\sample-repo --help 255 \\``` 256 \\ 257 \\| Option | Description | 258 \\| ----------- | ------------------------------ | 259 \\| `--verbose` | Print extra diagnostic output | 260 \\| `--quiet` | Suppress all non-error output | 261 \\| `--version` | Print the version and exit | 262 \\ 263 \\## Contributing 264 \\ 265 \\Contributions are welcome! Please open an issue to discuss 266 \\any significant changes before submitting a pull request. 267 \\See the docs under `docs/dev` for more details. 268 \\ 269 \\## License 270 \\ 271 \\Released under the MIT License. See `LICENSE` for the full 272 \\text. 273 \\ 274 ); 275 276 try repo_dir.createDirPath(io, "docs/dev"); 277 const doc = try repo_dir.createFile(io, "docs/dev/contribute.md", .{}); 278 defer doc.close(io); 279 try doc.writeStreamingAll(io, "To contribute, please make a pull request"); 280 } 281 282 try template_repo.add(io, allocator, &.{ "README.md", "docs/dev/contribute.md" }); 283 _ = try template_repo.commit(io, allocator, .{ .message = "let there be light" }); 284 285 // tag every commit in creation order as v1, v2, and so on 286 var tag_num: usize = 1; 287 try addNextTag(&template_repo, io, allocator, &tag_num); 288 289 // a batch of commits so the commits tab has more than one page to 290 // paginate through. each rewrites a few files with scattered line 291 // edits, so every commit is a multi-file diff with several separate 292 // hunks to look at. stepped timestamps vary the date column. 293 const base_ts: u64 = 1_700_000_000; // 2023-11-14 294 const edit_files = [_][]const u8{ "src/alpha.txt", "src/beta.txt", "src/gamma.txt" }; 295 // commit subjects, cycled then padded to a varying length. 296 const subjects = [_][]const u8{ 297 "fix off-by-one in scatter loop", 298 "encode and decode buffers", 299 "tune kernel socket timeouts", 300 "render matrix vector product", 301 "gather falcon and otter stats", 302 "refactor badger walrus module", 303 "drop dead ferret marmot branch", 304 }; 305 var c: usize = 0; 306 while (c < 30) : (c += 1) { 307 { 308 var repo_dir = try cwd.openDir(io, template_path, .{}); 309 defer repo_dir.close(io); 310 try repo_dir.createDirPath(io, "src"); 311 for (edit_files, 0..) |path, fi| { 312 try writeScatterFile(io, allocator, repo_dir, path, fi, c); 313 } 314 } 315 try template_repo.add(io, allocator, &edit_files); 316 // a cycling subject padded to a c-varying length so the commit 317 // list shows messages of different widths, capped at 120. 318 var msg_writer = std.Io.Writer.Allocating.init(allocator); 319 defer msg_writer.deinit(); 320 try msg_writer.writer.print("{s}", .{subjects[c % subjects.len]}); 321 const msg_target = 16 + (c * 41) % 96; 322 var mw = c; 323 while (true) : (mw += 1) { 324 const word = scatter_words[mw % scatter_words.len]; 325 const len = msg_writer.written().len; 326 if (len >= msg_target or len + 1 + word.len > 120) break; 327 try msg_writer.writer.print(" {s}", .{word}); 328 } 329 const message = try arena.allocator().dupe(u8, msg_writer.written()); 330 _ = try template_repo.commit(io, allocator, .{ .message = message, .timestamp = base_ts + c * std.time.s_per_day }); 331 try addNextTag(&template_repo, io, allocator, &tag_num); 332 } 333 334 // two more branches forked off master, each adding a single commit 335 // that makes a scattered edit to one file. tag each new commit too, 336 // and switch back to master after each so the next branch also 337 // forks from master and the template ends up back on master. 338 const branch_data = [_]struct { 339 name: []const u8, 340 file: []const u8, 341 fi: usize, 342 message: []const u8, 343 rev: usize, 344 }{ 345 .{ .name = "extra", .file = "src/alpha.txt", .fi = 0, .message = "scatter alpha on the extra branch", .rev = 30 }, 346 .{ .name = "feature", .file = "src/beta.txt", .fi = 1, .message = "scatter beta on the feature branch", .rev = 31 }, 347 }; 348 for (branch_data) |b| { 349 try template_repo.addBranch(io, .{ .name = b.name }); 350 { 351 var to_branch = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = b.name } } }); 352 defer to_branch.deinit(); 353 } 354 355 { 356 var repo_dir = try cwd.openDir(io, template_path, .{}); 357 defer repo_dir.close(io); 358 try writeScatterFile(io, allocator, repo_dir, b.file, b.fi, b.rev); 359 } 360 try template_repo.add(io, allocator, &.{b.file}); 361 _ = try template_repo.commit(io, allocator, .{ .message = b.message, .timestamp = base_ts + b.rev * std.time.s_per_day }); 362 try addNextTag(&template_repo, io, allocator, &tag_num); 363 364 { 365 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 366 defer to_master.deinit(); 367 } 368 } 369 } 370 371 // copy the template to each repo's on-disk location, named by its 372 // hex-encoded event id. the template repo is deinitialized above, so its 373 // db file is fully written before we copy it. 374 { 375 var template_dir = try cwd.openDir(io, template_path, .{ .iterate = true }); 376 defer template_dir.close(io); 377 378 for (repo_event_ids) |id_bytes| { 379 const repo_id = std.fmt.bytesToHex(id_bytes, .lower); 380 const repo_path = try std.fs.path.join(arena.allocator(), &.{ cwd_path, temp_dir_name, "server", "repos", &repo_id }); 381 var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); 382 defer dest_dir.close(io); 383 try copyDir(io, template_dir, dest_dir); 384 } 385 } 386 387 break :blk try ui.Session.init(&session_arena, &repo, .{}); 388 }; 389 session.is_terminal = true; 390 // try uses the default serve options below, so the footer's url points at 391 // the default web UI port. 392 session.web_port = try (srv.Options{}).wuiPort(); 393 394 // start the server 395 396 var cli = false; 397 398 var arg_it = try init.minimal.args.iterateAllocator(allocator); 399 defer arg_it.deinit(); 400 _ = arg_it.skip(); 401 while (arg_it.next()) |arg| { 402 if (std.mem.eql(u8, "--cli", arg)) { 403 cli = true; 404 } 405 } 406 407 const server_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server" }); 408 defer allocator.free(server_path); 409 410 // let the native TUI's page builders open the on-disk repos for the file tree 411 session.io = io; 412 session.repos_dir = try std.fs.path.join(session_arena.allocator(), &.{ server_path, "repos" }); 413 414 if (cli) { 415 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 416 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 417 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 418 419 const key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 420 defer allocator.free(key_path); 421 422 const Runnable = struct { 423 io: std.Io, 424 key_path: []const u8, 425 426 pub fn run(self: @This()) !void { 427 std.debug.print( 428 \\ 429 \\connect to the TUI with: 430 \\ ssh -p 8022 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 431 \\ 432 \\create a git repo and push over SSH: 433 \\ mkdir -p temp-try/client/test 434 \\ cd temp-try/client/test 435 \\ git init 436 \\ echo "hello" > hello.txt 437 \\ git add hello.txt 438 \\ git commit -m "let there be light" 439 \\ 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 440 \\ 441 \\to quit, press enter. 442 \\ 443 , .{self.key_path}); 444 // portable stdin read via std.Io — equivalent to the 445 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 446 // on windows. 447 var buf: [1]u8 = undefined; 448 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 449 _ = stdin_reader.interface.takeByte() catch {}; 450 } 451 }; 452 453 try srv.run(.xit, .{}, io, allocator, cwd_path, .{ 454 .data_dir = server_path, 455 }, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 456 } else { 457 var null_writer = std.Io.Writer.Discarding.init(&.{}); 458 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 459 460 const Runnable = struct { 461 io: std.Io, 462 allocator: std.mem.Allocator, 463 session: *ui.Session, 464 465 pub fn run(self: @This()) !void { 466 // launch the TUI 467 try hx.ui.run(self.io, self.allocator, self.session); 468 } 469 }; 470 471 try srv.run(.xit, .{}, io, allocator, cwd_path, .{ 472 .data_dir = server_path, 473 }, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session }); 474 } 475 } 476 477 // words cycled through to pad generated lines into prose-like content 478 const scatter_words = [_][]const u8{ 479 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", 480 "quantum", "vector", "matrix", "buffer", "kernel", "socket", 481 "falcon", "otter", "badger", "walrus", "ferret", "marmot", 482 "scatter", "gather", "encode", "decode", "render", "commit", 483 }; 484 485 // write `path` as 40 lines of mostly-stable filler. revision `c` and file 486 // index `fi` shift which lines change and the words used, so each (c, fi) 487 // yields a distinct multi-hunk diff against the previous revision. untouched 488 // lines stay byte-identical across revisions, so a bump in `c` is a scatter of 489 // small hunks rather than a full rewrite. 490 fn writeScatterFile(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir, path: []const u8, fi: usize, c: usize) !void { 491 const file = try repo_dir.createFile(io, path, .{}); 492 defer file.close(io); 493 var writer = std.Io.Writer.Allocating.init(allocator); 494 defer writer.deinit(); 495 for (0..40) |line| { 496 const start = writer.written().len; 497 if ((line + fi + c) % 8 == 0) { 498 const tag = scatter_words[(line + fi + c * 3) % scatter_words.len]; 499 try writer.writer.print("rev {d} {s}", .{ c, tag }); 500 } 501 const target = 40 + (line * 17 + fi * 23) % 70; 502 var w = line + fi; 503 while (true) : (w += 1) { 504 const word = scatter_words[w % scatter_words.len]; 505 const len = writer.written().len - start; 506 if (len >= target or len + 1 + word.len > 120) break; 507 if (len == 0) 508 try writer.writer.print("{s}", .{word}) 509 else 510 try writer.writer.print(" {s}", .{word}); 511 } 512 try writer.writer.writeByte('\n'); 513 } 514 try file.writeStreamingAll(io, writer.written()); 515 } 516 517 // tag the current HEAD as the next sequential version: v1, v2, and so on 518 fn addNextTag(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { 519 var buf: [16]u8 = undefined; 520 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); 521 _ = try repo.addTag(io, allocator, .{ .name = name }); 522 n.* += 1; 523 } 524 525 // recursively copy the contents of src_dir into dest_dir 526 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 527 var iter = src_dir.iterate(); 528 while (try iter.next(io)) |entry| { 529 switch (entry.kind) { 530 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 531 .directory => { 532 try dest_dir.createDirPath(io, entry.name); 533 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 534 defer dest_entry_dir.close(io); 535 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 536 defer src_entry_dir.close(io); 537 try copyDir(io, src_entry_dir, dest_entry_dir); 538 }, 539 else => {}, 540 } 541 } 542 } 543