╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 //! start a server with some fake data and then launch 2 //! the TUI. this provides a nice way to test things 3 //! out safely. 4 5 const std = @import("std"); 6 const builtin = @import("builtin"); 7 const hx = @import("haxy"); 8 const srv = hx.serve; 9 const evt = hx.event; 10 const xit = hx.xit; 11 const rp = xit.repo; 12 const ui = hx.ui; 13 14 // cook the terminal before a panic/segfault trace is printed, so the trace 15 // isn't mangled by raw mode and the alternate buffer 16 pub const std_options_debug_io = xit.xitui.terminal.crash_debug_io; 17 18 pub fn main(init: std.process.Init) !void { 19 var gpa: std.heap.DebugAllocator(.{}) = .init; 20 defer _ = gpa.deinit(); 21 const allocator = gpa.allocator(); 22 23 var threaded = std.Io.Threaded.init(allocator, .{ 24 .environ = init.minimal.environ, 25 }); 26 defer threaded.deinit(); 27 const io = threaded.io(); 28 29 const temp_dir_name = "temp-try"; 30 31 // create the temp dir 32 const cwd = std.Io.Dir.cwd(); 33 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 34 if (temp_dir_or_err) |*temp_dir| { 35 temp_dir.close(io); 36 try cwd.deleteTree(io, temp_dir_name); 37 } else |_| {} 38 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 39 defer cwd.deleteTree(io, temp_dir_name) catch {}; 40 defer temp_dir.close(io); 41 42 // write the dev SSH private key so pushes can authenticate against the 43 // matching public key seeded on the admin account below 44 { 45 const priv_key_file = try temp_dir.createFile(io, "key", .{}); 46 defer priv_key_file.close(io); 47 try priv_key_file.writeStreamingAll(io, 48 \\-----BEGIN OPENSSH PRIVATE KEY----- 49 \\b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 50 \\QyNTUxOQAAACCniLPJiaooAWecvOCeAjoJwCSeWxzysvpTNkpYjF22JgAAAJA+7hikPu4Y 51 \\pAAAAAtzc2gtZWQyNTUxOQAAACCniLPJiaooAWecvOCeAjoJwCSeWxzysvpTNkpYjF22Jg 52 \\AAAEDVlopOMnKt/7by/IA8VZvQXUS/O6VLkixOqnnahUdPCKeIs8mJqigBZ5y84J4COgnA 53 \\JJ5bHPKy+lM2SliMXbYmAAAAC3JhZGFyQHJvYXJrAQI= 54 \\-----END OPENSSH PRIVATE KEY----- 55 \\ 56 ); 57 if (.windows != builtin.os.tag) { 58 try priv_key_file.setPermissions(io, @enumFromInt(0o600)); 59 } 60 } 61 62 const cwd_path = try std.process.currentPathAlloc(io, allocator); 63 defer allocator.free(cwd_path); 64 65 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server", "admin" }); 66 defer allocator.free(work_path); 67 68 const Repo = rp.Repo(.xit, evt.admin_repo_opts); 69 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 70 defer repo.deinit(io, allocator); 71 72 try repo.addConfig(io, allocator, .{ .name = "user.name", .value = "haxy" }); 73 try repo.addConfig(io, allocator, .{ .name = "user.email", .value = "admin@haxy" }); 74 75 var session_arena = std.heap.ArenaAllocator.init(allocator); 76 defer session_arena.deinit(); 77 78 var session: ui.Session = blk: { 79 var arena = std.heap.ArenaAllocator.init(allocator); 80 defer arena.deinit(); 81 82 // define test events 83 84 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 85 86 const user_data = [_]struct { 87 name: []const u8, 88 display_name: []const u8, 89 email: []const u8, 90 }{ 91 .{ .name = "admin", .display_name = "Admin", .email = "admin@example.test" }, 92 .{ .name = "alice", .display_name = "Alice Tulley", .email = "alice@example.test" }, 93 .{ .name = "bob", .display_name = "Bob Smith", .email = "bob@example.test" }, 94 .{ .name = "carol", .display_name = "Carol Johnson", .email = "carol@example.test" }, 95 .{ .name = "dave", .display_name = "Dave Wilson", .email = "dave@example.test" }, 96 .{ .name = "eve", .display_name = "Eve Anderson", .email = "eve@example.test" }, 97 .{ .name = "frank", .display_name = "Frank Miller", .email = "frank@example.test" }, 98 .{ .name = "grace", .display_name = "Grace Lee", .email = "grace@example.test" }, 99 .{ .name = "henry", .display_name = "Henry Davis", .email = "henry@example.test" }, 100 .{ .name = "ivy", .display_name = "Ivy Martinez", .email = "ivy@example.test" }, 101 .{ .name = "jack", .display_name = "Jack Thompson", .email = "jack@example.test" }, 102 .{ .name = "kate", .display_name = "Kate Robinson", .email = "kate@example.test" }, 103 .{ .name = "liam", .display_name = "Liam Walker", .email = "liam@example.test" }, 104 .{ .name = "mona", .display_name = "Mona Patel", .email = "mona@example.test" }, 105 .{ .name = "noah", .display_name = "Noah Garcia", .email = "noah@example.test" }, 106 .{ .name = "olivia", .display_name = "Olivia Hernandez", .email = "olivia@example.test" }, 107 .{ .name = "peter", .display_name = "Peter Wright", .email = "peter@example.test" }, 108 .{ .name = "quinn", .display_name = "Quinn Foster", .email = "quinn@example.test" }, 109 .{ .name = "rachel", .display_name = "Rachel Bennett", .email = "rachel@example.test" }, 110 .{ .name = "sam", .display_name = "Sam Brooks", .email = "sam@example.test" }, 111 .{ .name = "tina", .display_name = "Tina Cooper", .email = "tina@example.test" }, 112 }; 113 114 const repo_data = [_]struct { 115 user_index: usize, 116 name: []const u8, 117 description: []const u8, 118 }{ 119 .{ .user_index = 1, .name = "ziglings", .description = "Learn the Zig programming language by fixing tiny broken programs" }, 120 .{ .user_index = 2, .name = "linux", .description = "Linux kernel source tree" }, 121 .{ .user_index = 3, .name = "kubernetes", .description = "Production-grade container orchestration" }, 122 .{ .user_index = 4, .name = "react", .description = "A declarative, efficient, and flexible JavaScript library for building user interfaces" }, 123 .{ .user_index = 5, .name = "typescript", .description = "TypeScript is a superset of JavaScript that compiles to clean JavaScript output" }, 124 .{ .user_index = 6, .name = "rust", .description = "Empowering everyone to build reliable and efficient software" }, 125 .{ .user_index = 7, .name = "go", .description = "The Go programming language" }, 126 .{ .user_index = 8, .name = "nodejs", .description = "Node.js JavaScript runtime" }, 127 .{ .user_index = 9, .name = "cpython", .description = "The Python programming language" }, 128 .{ .user_index = 10, .name = "docker", .description = "Container platform for developing, shipping, and running applications" }, 129 .{ .user_index = 1, .name = "vim", .description = "The ubiquitous text editor" }, 130 .{ .user_index = 2, .name = "neovim", .description = "Hyperextensible Vim-based text editor" }, 131 .{ .user_index = 3, .name = "emacs", .description = "GNU Emacs source code mirror" }, 132 .{ .user_index = 4, .name = "tmux", .description = "Terminal multiplexer" }, 133 .{ .user_index = 5, .name = "zsh", .description = "Mirror of the Z shell source code repository" }, 134 .{ .user_index = 6, .name = "git", .description = "Distributed version control system" }, 135 .{ .user_index = 7, .name = "mercurial", .description = "Source-control management tool" }, 136 .{ .user_index = 8, .name = "tensorflow", .description = "An end-to-end open source machine learning platform" }, 137 .{ .user_index = 9, .name = "pytorch", .description = "Tensors and dynamic neural networks in Python with strong GPU acceleration" }, 138 .{ .user_index = 10, .name = "numpy", .description = "The fundamental package for scientific computing with Python" }, 139 .{ .user_index = 1, .name = "pandas", .description = "Flexible and powerful data analysis and manipulation library for Python" }, 140 .{ .user_index = 2, .name = "scikit-learn", .description = "Machine learning in Python" }, 141 .{ .user_index = 3, .name = "nginx", .description = "High performance HTTP server and reverse proxy" }, 142 .{ .user_index = 4, .name = "redis", .description = "In-memory data structure store, used as a database, cache, and message broker" }, 143 .{ .user_index = 5, .name = "postgres", .description = "The world's most advanced open source relational database" }, 144 .{ .user_index = 6, .name = "sqlite", .description = "Self-contained, serverless, zero-configuration SQL database engine" }, 145 .{ .user_index = 7, .name = "mongodb", .description = "The MongoDB Database" }, 146 .{ .user_index = 8, .name = "elasticsearch", .description = "Free and open, distributed, RESTful search engine" }, 147 .{ .user_index = 9, .name = "kafka", .description = "Distributed event streaming platform" }, 148 .{ .user_index = 10, .name = "terraform", .description = "Infrastructure as code tool" }, 149 .{ .user_index = 11, .name = "svelte", .description = "Cybernetically enhanced web apps" }, 150 .{ .user_index = 12, .name = "vue", .description = "The progressive JavaScript framework" }, 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 .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 .event = .{ 192 .repo = .{ 193 .user_id = &user_ids[r.user_index], 194 .name = r.name, 195 .description = r.description, 196 .enable_issue = true, 197 }, 198 }, 199 }; 200 } 201 202 // commit the seed events and consume them into the database 203 try evt.commitAndConsume(evt.admin_repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume); 204 205 // every repo gets the same generated history, so build it once into a 206 // template repo and copy that to each repo's location below rather than 207 // redoing the expensive commit work for every repo. 208 const template_path = try std.fs.path.join(arena.allocator(), &.{ cwd_path, temp_dir_name, "template" }); 209 { 210 var template_repo = try rp.Repo(.xit, .{}).init(io, allocator, .{ .path = template_path }); 211 defer template_repo.deinit(io, allocator); 212 213 try template_repo.addConfig(io, allocator, .{ .name = "user.name", .value = "haxy" }); 214 try template_repo.addConfig(io, allocator, .{ .name = "user.email", .value = "admin@haxy" }); 215 216 // a README plus a nested doc so the file tree has a directory to 217 // descend into 218 { 219 var repo_dir = try cwd.openDir(io, template_path, .{}); 220 defer repo_dir.close(io); 221 222 const readme = try repo_dir.createFile(io, "README.md", .{}); 223 defer readme.close(io); 224 try readme.writeStreamingAll(io, "# Sample Repo\n\nA repository seeded with test data.\n"); 225 226 try repo_dir.createDirPath(io, "docs/dev"); 227 const doc = try repo_dir.createFile(io, "docs/dev/contribute.md", .{}); 228 defer doc.close(io); 229 try doc.writeStreamingAll(io, "To contribute, please make a pull request"); 230 } 231 232 try template_repo.add(io, allocator, &.{ "README.md", "docs/dev/contribute.md" }); 233 _ = try template_repo.commit(io, allocator, .{ .message = "let there be light" }); 234 235 // tag every commit in creation order as v1, v2, and so on 236 var tag_num: usize = 1; 237 try addNextTag(&template_repo, io, allocator, &tag_num); 238 239 // a batch of commits so the commits tab has more than one page to 240 // paginate through. each rewrites a few files with scattered line 241 // edits, so every commit is a multi-file diff with several separate 242 // hunks to look at. stepped timestamps vary the date column. 243 const base_ts: u64 = 1_700_000_000; // 2023-11-14 244 const edit_files = [_][]const u8{ "src/alpha.txt", "src/beta.txt", "src/gamma.txt" }; 245 // commit subjects, cycled then padded to a varying length. 246 const subjects = [_][]const u8{ 247 "fix off-by-one in scatter loop", 248 "encode and decode buffers", 249 "tune kernel socket timeouts", 250 "render matrix vector product", 251 "gather falcon and otter stats", 252 "refactor badger walrus module", 253 "drop dead ferret marmot branch", 254 }; 255 var c: usize = 0; 256 while (c < 30) : (c += 1) { 257 { 258 var repo_dir = try cwd.openDir(io, template_path, .{}); 259 defer repo_dir.close(io); 260 try repo_dir.createDirPath(io, "src"); 261 for (edit_files, 0..) |path, fi| { 262 try writeScatterFile(io, allocator, repo_dir, path, fi, c); 263 } 264 } 265 try template_repo.add(io, allocator, &edit_files); 266 // a cycling subject padded to a c-varying length so the commit 267 // list shows messages of different widths, capped at 120. 268 var msg_writer = std.Io.Writer.Allocating.init(allocator); 269 defer msg_writer.deinit(); 270 try msg_writer.writer.print("{s}", .{subjects[c % subjects.len]}); 271 const msg_target = 16 + (c * 41) % 96; 272 var mw = c; 273 while (true) : (mw += 1) { 274 const word = scatter_words[mw % scatter_words.len]; 275 const len = msg_writer.written().len; 276 if (len >= msg_target or len + 1 + word.len > 120) break; 277 try msg_writer.writer.print(" {s}", .{word}); 278 } 279 const message = try arena.allocator().dupe(u8, msg_writer.written()); 280 _ = try template_repo.commit(io, allocator, .{ .message = message, .timestamp = base_ts + c * std.time.s_per_day }); 281 try addNextTag(&template_repo, io, allocator, &tag_num); 282 } 283 284 // two more branches forked off master, each adding a single commit 285 // that makes a scattered edit to one file. tag each new commit too, 286 // and switch back to master after each so the next branch also 287 // forks from master and the template ends up back on master. 288 const branch_data = [_]struct { 289 name: []const u8, 290 file: []const u8, 291 fi: usize, 292 message: []const u8, 293 rev: usize, 294 }{ 295 .{ .name = "extra", .file = "src/alpha.txt", .fi = 0, .message = "scatter alpha on the extra branch", .rev = 30 }, 296 .{ .name = "feature", .file = "src/beta.txt", .fi = 1, .message = "scatter beta on the feature branch", .rev = 31 }, 297 }; 298 for (branch_data) |b| { 299 try template_repo.addBranch(io, .{ .name = b.name }); 300 { 301 var to_branch = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = b.name } } }); 302 defer to_branch.deinit(); 303 } 304 305 { 306 var repo_dir = try cwd.openDir(io, template_path, .{}); 307 defer repo_dir.close(io); 308 try writeScatterFile(io, allocator, repo_dir, b.file, b.fi, b.rev); 309 } 310 try template_repo.add(io, allocator, &.{b.file}); 311 _ = try template_repo.commit(io, allocator, .{ .message = b.message, .timestamp = base_ts + b.rev * std.time.s_per_day }); 312 try addNextTag(&template_repo, io, allocator, &tag_num); 313 314 { 315 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 316 defer to_master.deinit(); 317 } 318 } 319 } 320 321 // copy the template to each repo's on-disk location, named by its 322 // hex-encoded event id. the template repo is deinitialized above, so its 323 // db file is fully written before we copy it. 324 { 325 var template_dir = try cwd.openDir(io, template_path, .{ .iterate = true }); 326 defer template_dir.close(io); 327 328 for (repo_event_ids) |id_bytes| { 329 const repo_id = std.fmt.bytesToHex(id_bytes, .lower); 330 const repo_path = try std.fs.path.join(arena.allocator(), &.{ cwd_path, temp_dir_name, "server", "repos", &repo_id }); 331 var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); 332 defer dest_dir.close(io); 333 try copyDir(io, template_dir, dest_dir); 334 } 335 } 336 337 break :blk try ui.Session.init(&session_arena, &repo, .{}); 338 }; 339 session.is_terminal = true; 340 // try uses the default serve options below, so the footer's url points at 341 // the default web UI port. 342 session.web_port = try (srv.Options{}).wuiPort(); 343 344 // start the server 345 346 var cli = false; 347 348 var arg_it = try init.minimal.args.iterateAllocator(allocator); 349 defer arg_it.deinit(); 350 _ = arg_it.skip(); 351 while (arg_it.next()) |arg| { 352 if (std.mem.eql(u8, "--cli", arg)) { 353 cli = true; 354 } 355 } 356 357 const server_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server" }); 358 defer allocator.free(server_path); 359 360 // let the native TUI's page builders open the on-disk repos for the file tree 361 session.io = io; 362 session.repos_dir = try std.fs.path.join(session_arena.allocator(), &.{ server_path, "repos" }); 363 364 if (cli) { 365 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 366 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 367 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 368 369 const key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 370 defer allocator.free(key_path); 371 372 const Runnable = struct { 373 io: std.Io, 374 key_path: []const u8, 375 376 pub fn run(self: @This()) !void { 377 std.debug.print( 378 \\ 379 \\connect to the TUI with: 380 \\ ssh -p 8022 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 381 \\ 382 \\create a git repo and push over SSH: 383 \\ mkdir -p temp-try/client/test 384 \\ cd temp-try/client/test 385 \\ git init 386 \\ echo "hello" > hello.txt 387 \\ git add hello.txt 388 \\ git commit -m "let there be light" 389 \\ 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 390 \\ 391 \\to quit, press enter. 392 \\ 393 , .{self.key_path}); 394 // portable stdin read via std.Io — equivalent to the 395 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 396 // on windows. 397 var buf: [1]u8 = undefined; 398 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 399 _ = stdin_reader.interface.takeByte() catch {}; 400 } 401 }; 402 403 try srv.run(.xit, .{}, io, allocator, cwd_path, .{ 404 .data_dir = server_path, 405 }, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 406 } else { 407 var null_writer = std.Io.Writer.Discarding.init(&.{}); 408 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 409 410 const Runnable = struct { 411 io: std.Io, 412 allocator: std.mem.Allocator, 413 session: *ui.Session, 414 415 pub fn run(self: @This()) !void { 416 // launch the TUI 417 try hx.ui.run(self.io, self.allocator, self.session); 418 } 419 }; 420 421 try srv.run(.xit, .{}, io, allocator, cwd_path, .{ 422 .data_dir = server_path, 423 }, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session }); 424 } 425 } 426 427 // words cycled through to pad generated lines into prose-like content 428 const scatter_words = [_][]const u8{ 429 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", 430 "quantum", "vector", "matrix", "buffer", "kernel", "socket", 431 "falcon", "otter", "badger", "walrus", "ferret", "marmot", 432 "scatter", "gather", "encode", "decode", "render", "commit", 433 }; 434 435 // write `path` as 40 lines of mostly-stable filler. revision `c` and file 436 // index `fi` shift which lines change and the words used, so each (c, fi) 437 // yields a distinct multi-hunk diff against the previous revision. untouched 438 // lines stay byte-identical across revisions, so a bump in `c` is a scatter of 439 // small hunks rather than a full rewrite. 440 fn writeScatterFile(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir, path: []const u8, fi: usize, c: usize) !void { 441 const file = try repo_dir.createFile(io, path, .{}); 442 defer file.close(io); 443 var writer = std.Io.Writer.Allocating.init(allocator); 444 defer writer.deinit(); 445 for (0..40) |line| { 446 const start = writer.written().len; 447 if ((line + fi + c) % 8 == 0) { 448 const tag = scatter_words[(line + fi + c * 3) % scatter_words.len]; 449 try writer.writer.print("rev {d} {s}", .{ c, tag }); 450 } 451 const target = 40 + (line * 17 + fi * 23) % 70; 452 var w = line + fi; 453 while (true) : (w += 1) { 454 const word = scatter_words[w % scatter_words.len]; 455 const len = writer.written().len - start; 456 if (len >= target or len + 1 + word.len > 120) break; 457 if (len == 0) 458 try writer.writer.print("{s}", .{word}) 459 else 460 try writer.writer.print(" {s}", .{word}); 461 } 462 try writer.writer.writeByte('\n'); 463 } 464 try file.writeStreamingAll(io, writer.written()); 465 } 466 467 // tag the current HEAD as the next sequential version: v1, v2, and so on 468 fn addNextTag(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { 469 var buf: [16]u8 = undefined; 470 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); 471 _ = try repo.addTag(io, allocator, .{ .name = name }); 472 n.* += 1; 473 } 474 475 // recursively copy the contents of src_dir into dest_dir 476 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 477 var iter = src_dir.iterate(); 478 while (try iter.next(io)) |entry| { 479 switch (entry.kind) { 480 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 481 .directory => { 482 try dest_dir.createDirPath(io, entry.name); 483 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 484 defer dest_entry_dir.close(io); 485 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 486 defer src_entry_dir.close(io); 487 try copyDir(io, src_entry_dir, dest_entry_dir); 488 }, 489 else => {}, 490 } 491 } 492 } 493