╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
99 defer allocator.free(work_path); 100 101 const Repo = rp.Repo(.xit, evt.admin_repo_opts); 102 var repo = try Repo.init(io, allocator, .{ .path = work_path, .bare = true }); 103 defer repo.deinit(io, allocator); 104 105 var session_arena = std.heap.ArenaAllocator.init(allocator); 106 defer session_arena.deinit(); 107 108 // the seeded admin's event id, so both uis start logged in as them 109 var admin_user_id: [evt.event_id_size]u8 = undefined; 110 111 var session: ui.Session = blk: { 112 var arena = std.heap.ArenaAllocator.init(allocator); 113 defer arena.deinit(); 114 115 // define test events 116 117 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 118 119 const user_data = [_]struct { 120 name: []const u8, 121 email: []const u8, 122 }{ 123 .{ .name = "admin", .email = "admin@example.test" }, 124 .{ .name = "alice", .email = "alice@example.test" }, 125 .{ .name = "bob", .email = "bob@example.test" }, 126 .{ .name = "carol", .email = "carol@example.test" }, 127 .{ .name = "dave", .email = "dave@example.test" }, 128 .{ .name = "eve", .email = "eve@example.test" }, 129 .{ .name = "frank", .email = "frank@example.test" }, 130 .{ .name = "grace", .email = "grace@example.test" }, 131 .{ .name = "henry", .email = "henry@example.test" }, 132 .{ .name = "ivy", .email = "ivy@example.test" }, 133 .{ .name = "jack", .email = "jack@example.test" }, 134 .{ .name = "kate", .email = "kate@example.test" }, 135 .{ .name = "liam", .email = "liam@example.test" }, 136 .{ .name = "mona", .email = "mona@example.test" }, 137 .{ .name = "noah", .email = "noah@example.test" }, 138 .{ .name = "olivia", .email = "olivia@example.test" }, 139 .{ .name = "peter", .email = "peter@example.test" }, 140 .{ .name = "quinn", .email = "quinn@example.test" }, 141 .{ .name = "rachel", .email = "rachel@example.test" }, 142 .{ .name = "sam", .email = "sam@example.test" }, 143 .{ .name = "tina", .email = "tina@example.test" }, 144 }; 145 146 const repo_data = [_]struct { 147 user_index: usize, 148 name: []const u8, 149 description: []const u8, 150 }{ 151 .{ .user_index = 1, .name = "ziglings", .description = "Learn the Zig programming language by fixing tiny broken programs" }, 152 .{ .user_index = 2, .name = "linux", .description = "Linux kernel source tree" }, 153 .{ .user_index = 3, .name = "kubernetes", .description = "Production-grade container orchestration" }, 154 .{ .user_index = 4, .name = "react", .description = "A declarative, efficient, and flexible JavaScript library for building user interfaces" }, 155 .{ .user_index = 5, .name = "typescript", .description = "TypeScript is a superset of JavaScript that compiles to clean JavaScript output" }, 156 .{ .user_index = 6, .name = "rust", .description = "Empowering everyone to build reliable and efficient software" }, 157 .{ .user_index = 7, .name = "go", .description = "The Go programming language" }, 158 .{ .user_index = 8, .name = "nodejs", .description = "Node.js JavaScript runtime" }, 159 .{ .user_index = 9, .name = "cpython", .description = "The Python programming language" }, 160 .{ .user_index = 10, .name = "docker", .description = "Container platform for developing, shipping, and running applications" }, 161 .{ .user_index = 1, .name = "vim", .description = "The ubiquitous text editor" }, 162 .{ .user_index = 2, .name = "neovim", .description = "Hyperextensible Vim-based text editor" }, 163 .{ .user_index = 3, .name = "emacs", .description = "GNU Emacs source code mirror" }, 164 .{ .user_index = 4, .name = "tmux", .description = "Terminal multiplexer" }, 165 .{ .user_index = 5, .name = "zsh", .description = "Mirror of the Z shell source code repository" }, 166 .{ .user_index = 6, .name = "git", .description = "Distributed version control system" }, 167 .{ .user_index = 7, .name = "mercurial", .description = "Source-control management tool" }, 168 .{ .user_index = 8, .name = "tensorflow", .description = "An end-to-end open source machine learning platform" }, 169 .{ .user_index = 9, .name = "pytorch", .description = "Tensors and dynamic neural networks in Python with strong GPU acceleration" }, 170 .{ .user_index = 10, .name = "numpy", .description = "The fundamental package for scientific computing with Python" }, 171 .{ .user_index = 1, .name = "pandas", .description = "Flexible and powerful data analysis and manipulation library for Python" }, 172 .{ .user_index = 2, .name = "scikit-learn", .description = "Machine learning in Python" }, 173 .{ .user_index = 3, .name = "nginx", .description = "High performance HTTP server and reverse proxy" }, 174 .{ .user_index = 4, .name = "redis", .description = "In-memory data structure store, used as a database, cache, and message broker" }, 175 .{ .user_index = 5, .name = "postgres", .description = "The world's most advanced open source relational database" }, 176 .{ .user_index = 6, .name = "sqlite", .description = "Self-contained, serverless, zero-configuration SQL database engine" }, 177 .{ .user_index = 7, .name = "mongodb", .description = "The MongoDB Database" }, 178 .{ .user_index = 8, .name = "elasticsearch", .description = "Free and open, distributed, RESTful search engine" }, 179 .{ .user_index = 9, .name = "kafka", .description = "Distributed event streaming platform" }, 180 .{ .user_index = 10, .name = "terraform", .description = "Infrastructure as code tool" }, 181 .{ .user_index = 11, .name = "svelte", .description = "Cybernetically enhanced web apps" }, 182 .{ .user_index = 12, .name = "vue", .description = "The progressive JavaScript framework" }, 183 .{ .user_index = 13, .name = "flask", .description = "The Python micro framework for building web applications" }, 184 .{ .user_index = 14, .name = "django", .description = "The web framework for perfectionists with deadlines" }, 185 .{ .user_index = 15, .name = "rails", .description = "Ruby on Rails web framework" }, 186 .{ .user_index = 16, .name = "phoenix", .description = "Peace of mind from prototype to production for Elixir web apps" }, 187 .{ .user_index = 17, .name = "laravel", .description = "The PHP framework for web artisans" }, 188 .{ .user_index = 18, .name = "prometheus", .description = "The Prometheus monitoring system and time series database" }, 189 .{ .user_index = 19, .name = "grafana", .description = "The open and composable observability and data visualization platform" }, 190 .{ .user_index = 20, .name = "ansible", .description = "Simple, agentless IT automation" }, 191 }; 192 193 var user_ids: [user_data.len][evt.event_id_size]u8 = undefined; 194 for (&user_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 195 // admin is the first entry of user_data above 196 admin_user_id = user_ids[0]; 197 198 var repo_event_ids: [repo_data.len][evt.event_id_size]u8 = undefined; 199 for (&repo_event_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 200 201 var password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 202 const password_hash = try evt.User.hashPassword("password", &password_hash_buf, io); 203 204 // public key matching temp-try/key, given to admin so we can push as admin 205 const admin_ssh_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKeIs8mJqigBZ5y84J4COgnAJJ5bHPKy+lM2SliMXbYm radar@roark"; 206 207 var events_to_consume: [user_data.len + repo_data.len]evt.EventWithId = undefined; 208 // inserted back to front, so the newest-first listing shows them in the 209 // order they're written above 210 for (0..user_data.len) |slot| { 211 const i = user_data.len - 1 - slot; 212 const u = user_data[i]; 213 events_to_consume[slot] = .{ 214 .id = std.fmt.bytesToHex(user_ids[i], .lower), 215 // stepped timestamps so the seeded users/repos list in a stable order 216 .timestamp = @intCast(slot + 1), 217 .author = .{ .name = "admin", .email = "admin@example.test" }, 218 .event = .{ 219 .user = .{ 220 .name = u.name, 221 .email = u.email, 222 .password_hash = password_hash, 223 .ssh_keys = if (std.mem.eql(u8, "admin", u.name)) admin_ssh_key else "", 224 }, 225 }, 226 }; 227 } 228 for (repo_data, 0..) |r, i| { 229 events_to_consume[user_data.len + i] = .{ 230 .id = std.fmt.bytesToHex(repo_event_ids[i], .lower), 231 .timestamp = @intCast(user_data.len + i + 1), 232 .author = .{ .name = user_data[r.user_index].name, .email = user_data[r.user_index].email }, 233 .event = .{ 234 .repo = .{ 235 .user_id = &user_ids[r.user_index], 236 .name = r.name, 237 .description = r.description, 238 .read_access = .public, 239 .write_access = .public, 240 }, 241 }, 242 }; 243 } 244 245 // commit the seed events and consume them into the database 246 try evt.consume(.admin, .xit, evt.admin_repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume); 247 248 // every repo gets the same generated history, so build it once into a 249 // template repo and copy that to each repo's location below rather than 250 // redoing the expensive commit work for every repo. 251 const template_path = try std.fs.path.join(arena.allocator(), &.{ cwd_path, temp_dir_name, "template" }); 252 { 253 var template_repo = try rp.Repo(.xit, .{}).init(io, allocator, .{ .path = template_path }); 254 defer template_repo.deinit(io, allocator); 255 256 try template_repo.setMergeAlgorithm(io, allocator, .patch); 257 try template_repo.addConfig(io, allocator, .{ .name = "user.name", .value = "haxy" }); 258 try template_repo.addConfig(io, allocator, .{ .name = "user.email", .value = "admin@example.test" }); 259 260 // a README plus a nested doc so the file tree has a directory to 261 // descend into 262 { 263 var repo_dir = try cwd.openDir(io, template_path, .{}); 264 defer repo_dir.close(io); 265 266 const readme = try repo_dir.createFile(io, "README.md", .{}); 267 defer readme.close(io); 268 try readme.writeStreamingAll(io, 269 \\# Sample Repo 270 \\ 271 \\A repository seeded with test data for exercising the UI. 272 \\ 273 \\## Features 274 \\ 275 \\- Fast and lightweight 276 \\- Zero configuration required 277 \\- Works out of the box 278 \\ 279 \\## Installation 280 \\ 281 \\Clone the repository and build it from source: 282 \\ 283 \\```sh 284 \\git clone https://example.test/sample-repo.git 285 \\cd sample-repo 286 \\make install 287 \\``` 288 \\ 289 \\## Usage 290 \\ 291 \\Run the program with the `--help` flag to see all available 292 \\options: 293 \\ 294 \\```sh 295 \\sample-repo --help 296 \\``` 297 \\ 298 \\| Option | Description | 299 \\| ----------- | ------------------------------ | 300 \\| `--verbose` | Print extra diagnostic output | 301 \\| `--quiet` | Suppress all non-error output | 302 \\| `--version` | Print the version and exit | 303 \\ 304 \\## Contributing 305 \\ 306 \\Contributions are welcome! Please open an issue to discuss 307 \\any significant changes before submitting a pull request. 308 \\See the docs under `docs/dev` for more details. 309 \\ 310 \\## License 311 \\ 312 \\Released under the MIT License. See `LICENSE` for the full 313 \\text. 314 \\ 315 ); 316 317 try repo_dir.createDirPath(io, "docs/dev"); 318 const doc = try repo_dir.createFile(io, "docs/dev/contribute.md", .{}); 319 defer doc.close(io); 320 try doc.writeStreamingAll(io, "To contribute, please make a pull request"); 321 } 322 323 try template_repo.add(io, allocator, &.{ "README.md", "docs/dev/contribute.md" }); 324 _ = try template_repo.commit(io, allocator, .{ .message = "let there be light" }); 325 326 // tag every commit in creation order as v1, v2, and so on 327 var tag_num: usize = 1; 328 try addNextTag(&template_repo, io, allocator, &tag_num); 329 330 // a batch of commits so the commits tab has more than one page to 331 // paginate through. each rewrites a few files with scattered line 332 // edits, so every commit is a multi-file diff with several separate 333 // hunks to look at. stepped timestamps vary the date column. 334 const base_ts: u64 = 1_700_000_000; // 2023-11-14 335 const edit_files = [_][]const u8{ "src/alpha.txt", "src/beta.txt", "src/gamma.txt" }; 336 // commit subjects, cycled then padded to a varying length. 337 const subjects = [_][]const u8{ 338 "fix off-by-one in scatter loop", 339 "encode and decode buffers", 340 "tune kernel socket timeouts", 341 "render matrix vector product", 342 "gather falcon and otter stats", 343 "refactor badger walrus module", 344 "drop dead ferret marmot branch", 345 }; 346 const commit_count = 30; 347 var c: usize = 0; 348 while (c < commit_count) : (c += 1) { 349 { 350 var repo_dir = try cwd.openDir(io, template_path, .{}); 351 defer repo_dir.close(io); 352 try repo_dir.createDirPath(io, "src"); 353 for (edit_files, 0..) |path, fi| { 354 try writeScatterFile(io, allocator, repo_dir, path, fi, c); 355 } 356 } 357 try template_repo.add(io, allocator, &edit_files); 358 // a cycling subject padded to a c-varying length so the commit 359 // list shows messages of different widths, capped at 120. 360 var msg_writer = std.Io.Writer.Allocating.init(allocator); 361 defer msg_writer.deinit(); 362 try msg_writer.writer.print("{s}", .{subjects[c % subjects.len]}); 363 const msg_target = 16 + (c * 41) % 96; 364 var mw = c; 365 while (true) : (mw += 1) { 366 const word = scatter_words[mw % scatter_words.len]; 367 const len = msg_writer.written().len; 368 if (len >= msg_target or len + 1 + word.len > 120) break; 369 try msg_writer.writer.print(" {s}", .{word}); 370 } 371 // the newest commit's message runs past what the detail pane 372 // reads, so it shows the truncated message and its link. 373 if (c == commit_count - 1) { 374 for (0..ui.detail_preview_lines) |line| { 375 try msg_writer.writer.print("\n{d} {s}", .{ line, scatter_words[line % scatter_words.len] }); 376 } 377 } 378 const message = try arena.allocator().dupe(u8, msg_writer.written()); 379 _ = try template_repo.commit(io, allocator, .{ .message = message, .timestamp = base_ts + c * std.time.s_per_day }); 380 try addNextTag(&template_repo, io, allocator, &tag_num); 381 } 382 383 // two more branches forked off master, each adding a single commit 384 // that makes a scattered edit to one file. tag each new commit too, 385 // and switch back to master after each so the next branch also 386 // forks from master and the template ends up back on master. 387 const branch_data = [_]struct { 388 name: []const u8, 389 file: []const u8, 390 fi: usize, 391 message: []const u8, 392 rev: usize, 393 }{ 394 .{ .name = "extra", .file = "src/alpha.txt", .fi = 0, .message = "scatter alpha on the extra branch", .rev = 30 }, 395 .{ .name = "feature", .file = "src/beta.txt", .fi = 1, .message = "scatter beta on the feature branch", .rev = 31 }, 396 }; 397 for (branch_data) |b| { 398 try template_repo.addBranch(io, .{ .name = b.name }); 399 { 400 var to_branch = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = b.name } } }); 401 defer to_branch.deinit(); 402 } 403 404 { 405 var repo_dir = try cwd.openDir(io, template_path, .{}); 406 defer repo_dir.close(io); 407 try writeScatterFile(io, allocator, repo_dir, b.file, b.fi, b.rev); 408 } 409 try template_repo.add(io, allocator, &.{b.file}); 410 _ = try template_repo.commit(io, allocator, .{ .message = b.message, .timestamp = base_ts + b.rev * std.time.s_per_day }); 411 try addNextTag(&template_repo, io, allocator, &tag_num); 412 413 { 414 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 415 defer to_master.deinit(); 416 } 417 } 418 419 // the timestamps issue's description, in paragraphs, so the 420 // seeded conflict below can insert and delete among them 421 const tz_p1 = "All timestamps in the activity feed render in UTC regardless of the system timezone."; 422 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."; 423 const tz_p3 = "A quick survey of other clients shows every one of them rendering local time by default."; 424 const tz_p4 = "Convert to local time and include the offset in tooltips."; 425 426 // the sync issue's description, in paragraphs, so each side of its 427 // seeded conflict can rework a different one 428 const sync_p1 = "The sync engine only has unit tests with mocked transports."; 429 const sync_p2 = "Failures seen in production involve reconnects and partial writes, which the mocks can't reproduce."; 430 const sync_p3 = "Add end-to-end tests that run two instances against a real local server."; 431 432 // seed issues so every repo's issue tracker has content 433 const issue_data = [_]struct { 434 title: []const u8, 435 description: []const u8, 436 tags: []const u8, 437 status: evt.Issue.Status = .open, 438 }{ 439 .{ 440 .title = "Crash on startup when config file is missing", 441 .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.", 442 .tags = "bug priority-high crash", 443 }, 444 .{ 445 .title = "Support dark mode in the settings panel", 446 .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.", 447 .tags = "enhancement ui theme", 448 }, 449 .{ 450 .title = "Memory leak when reconnecting after network failure", 451 .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.", 452 .tags = "bug memory networking", 453 .status = .closed, 454 }, 455 .{ 456 .title = "Document the plugin API", 457 .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.", 458 .tags = "documentation plugins", 459 }, 460 .{ 461 .title = "Slow file indexing on large directories", 462 .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.", 463 .tags = "performance indexing", 464 }, 465 .{ 466 .title = "Add keyboard shortcut for quick search", 467 .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.", 468 .tags = "enhancement ux keyboard", 469 }, 470 .{ 471 .title = "Unicode filenames are garbled in the export dialog", 472 .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.", 473 .tags = "bug unicode i18n", 474 }, 475 .{ 476 .title = "Flaky test: integration suite times out on CI", 477 .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.", 478 .tags = "bug testing ci", 479 }, 480 .{ 481 .title = "Upgrade bundled zlib to the latest release", 482 .description = "The vendored zlib is two major releases behind and misses several upstream fixes. Update the bundled copy and re-run the compression benchmarks.", 483 .tags = "dependencies maintenance", 484 }, 485 .{ 486 .title = "Progress bar overshoots 100% during resumed downloads", 487 .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.", 488 .tags = "bug ui downloads", 489 .status = .closed, 490 }, 491 .{ 492 .title = "Config parser rejects trailing commas", 493 .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.", 494 .tags = "bug config parser", 495 }, 496 .{ 497 .title = "Add a --json flag to the status command", 498 .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.", 499 .tags = "enhancement cli", 500 }, 501 .{ 502 .title = "Race condition between autosave and manual save", 503 .description = "Saving manually while an autosave is in flight can interleave the two writes and corrupt the file. Serialize saves through a single queue.", 504 .tags = "bug priority-high data-loss", 505 }, 506 .{ 507 .title = "Reduce binary size of release builds", 508 .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.", 509 .tags = "performance build", 510 }, 511 .{ 512 .title = "Tooltips flicker when the cursor moves between adjacent buttons", 513 .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.", 514 .tags = "bug ui polish", 515 }, 516 .{ 517 .title = "Support environment variable expansion in config paths", 518 .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.", 519 .tags = "enhancement config", 520 }, 521 .{ 522 .title = "Log rotation deletes the newest file instead of the oldest", 523 .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.", 524 .tags = "bug logging", 525 .status = .closed, 526 }, 527 .{ 528 .title = "Add man pages for all subcommands", 529 .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.", 530 .tags = "documentation cli", 531 }, 532 .{ 533 .title = "High CPU usage while idle in the background", 534 .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.", 535 .tags = "performance priority-high", 536 }, 537 .{ 538 .title = "Paste from clipboard drops the final newline", 539 .description = "Pasting text that ends with a newline silently trims it, which breaks pasted shell snippets. Preserve the clipboard content exactly.", 540 .tags = "bug editor clipboard", 541 }, 542 .{ 543 .title = "Improve error message for expired credentials", 544 .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.", 545 .tags = "enhancement ux auth", 546 }, 547 .{ 548 .title = "Crash when window is resized during startup animation", 549 .description = "Resizing the window while the splash animation is running dereferences a freed layout node. Cancel the animation before rebuilding the layout.", 550 .tags = "bug crash ui", 551 }, 552 .{ 553 .title = "Add integration tests for the sync engine", 554 .description = sync_p1 ++ "\n\n" ++ sync_p2 ++ "\n\n" ++ sync_p3, 555 .tags = "testing sync", 556 }, 557 .{ 558 .title = "Timestamps display in UTC instead of local time", 559 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p3 ++ "\n\n" ++ tz_p4, 560 .tags = "bug i18n time", 561 }, 562 .{ 563 .title = "Deprecate the legacy plugin format", 564 .description = "Both plugin formats are currently loaded, doubling the maintenance surface. Warn on legacy plugins this release and drop support in the next.", 565 .tags = "maintenance plugins", 566 }, 567 }; 568 569 var issue_events: [issue_data.len]evt.EventWithId = undefined; 570 for (issue_data, 0..) |issue, i| { 571 // the fifth-newest issue's description runs past what the detail 572 // pane shows, so it shows the truncated description and its 573 // link. 574 const description = if (i == issue_data.len - 5) 575 try longDescription(arena.allocator(), issue.description) 576 else 577 issue.description; 578 issue_events[i] = .{ 579 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 580 // stepped timestamps so the issues list in a stable order 581 .timestamp = @intCast(i + 1), 582 .author = .{ .name = user_data[i % user_data.len].name, .email = user_data[i % user_data.len].email }, 583 .event = .{ 584 .issue = .{ 585 .title = issue.title, 586 .description = description, 587 .tags = issue.tags, 588 .status = issue.status, 589 }, 590 }, 591 }; 592 } 593 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &issue_events); 594 // the tip is the branch point for the conflicting edits below 595 const seed_tip = (try template_repo.readRef(io, evt.events_ref)) orelse unreachable; 596 597 // two divergent edits per conflicted issue: ours on the events 598 // branch, theirs on a temp branch rooted at the seed tip, then a 599 // merge. the 4th-newest issue conflicts on title and tags; the 600 // 3rd-newest on its description with every hunk auto-resolving 601 // (each side reworks a different paragraph); the 2nd-newest on 602 // its description with a removal conflict (ours removes a 603 // paragraph theirs rewords) and an insertion conflict (each side 604 // appends a different closing paragraph). 605 { 606 const other_ref: xit.ref.Ref = .{ .kind = .head, .name = "haxy/other" }; 607 const title_issue = issue_data[issue_data.len - 4]; 608 const sync_issue = issue_data[issue_data.len - 3]; 609 const desc_issue = issue_data[issue_data.len - 2]; 610 611 const ours = [_]evt.EventWithId{ .{ 612 .id = issue_events[issue_data.len - 4].id, 613 .timestamp = 100, 614 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 615 .event = .{ .issue = .{ 616 .title = "Crash when resizing the window during the splash animation", 617 .description = title_issue.description, 618 .tags = "bug crash ui priority-high", 619 } }, 620 }, .{ 621 .id = issue_events[issue_data.len - 3].id, 622 .timestamp = 101, 623 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 624 .event = .{ .issue = .{ 625 .title = sync_issue.title, 626 .description = "The sync engine's coverage is unit tests only, with every transport mocked out." ++ "\n\n" ++ 627 sync_p2 ++ "\n\n" ++ sync_p3, 628 .tags = sync_issue.tags, 629 } }, 630 }, .{ 631 .id = issue_events[issue_data.len - 2].id, 632 .timestamp = 102, 633 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 634 .event = .{ .issue = .{ 635 .title = desc_issue.title, 636 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p4 ++ "\n\n" ++ 637 "The confusion is worst for teams spread across timezones, who each read a different wall-clock time from the same feed.", 638 .tags = desc_issue.tags, 639 } }, 640 } }; 641 const theirs = [_]evt.EventWithId{ .{ 642 .id = issue_events[issue_data.len - 4].id, 643 .timestamp = 103, 644 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 645 .event = .{ .issue = .{ 646 .title = "Segfault on early window resize", 647 .description = title_issue.description, 648 .tags = "bug crash rendering", 649 } }, 650 }, .{ 651 .id = issue_events[issue_data.len - 3].id, 652 .timestamp = 104, 653 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 654 .event = .{ .issue = .{ 655 .title = sync_issue.title, 656 .description = sync_p1 ++ "\n\n" ++ sync_p2 ++ "\n\n" ++ 657 "Add end-to-end tests that drive two live instances against a local server on every CI run.", 658 .tags = sync_issue.tags, 659 } }, 660 }, .{ 661 .id = issue_events[issue_data.len - 2].id, 662 .timestamp = 105, 663 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 664 .event = .{ .issue = .{ 665 .title = desc_issue.title, 666 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ 667 "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" ++ 668 "Log exports inherit the same UTC rendering, so downstream tooling has to guess the source timezone.", 669 .tags = desc_issue.tags, 670 } }, 671 } }; 672 673 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &ours); 674 675 // consume can't root a new branch, so theirs commits by hand 676 var json: std.Io.Writer.Allocating = .init(allocator); 677 defer json.deinit(); 678 for (theirs, 0..) |event, i| { 679 json.clearRetainingCapacity(); 680 try std.json.Stringify.value(event, .{}, &json.writer); 681 const author = try std.fmt.allocPrint(allocator, "{s} <{s}>", .{ event.author.name, event.author.email }); 682 defer allocator.free(author); 683 _ = try template_repo.commitAtRef(io, allocator, .{ 684 .author = author, 685 .message = json.written(), 686 .timestamp = event.timestamp, 687 // root the branch at the shared seed tip 688 .parent_oids = if (i == 0) &.{seed_tip} else null, 689 }, null, other_ref); 690 } 691 692 { 693 var to_events = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 694 defer to_events.deinit(); 695 } 696 { 697 var merge = try template_repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, null); 698 defer merge.deinit(); 699 if (merge.result != .success) return error.MergeFailed; 700 } 701 { 702 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 703 defer to_master.deinit(); 704 } 705 try template_repo.removeBranch(io, .{ .name = other_ref.name }); 706 } 707 708 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &.{}); 709 710 // seed a small comment tree on the newest issue 711 var comment_ids: [5][evt.event_id_size]u8 = undefined; 712 for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 713 714 const comment_bodies = [_][]const u8{ 715 "I can take this. The compatibility scanner already reports which repositories still use the legacy format.", 716 "Can we keep loading legacy plugins for one release after the warning lands? That would give downstream maintainers time to migrate.", 717 "That sounds good. I'll include the scanner output in the warning so each affected plugin is named.", 718 "Please include the replacement manifest path too. That should make the warning actionable without opening the migration guide.", 719 "I'll add release-note text once the warning format is settled.", 720 }; 721 722 var comment_events: [comment_ids.len]evt.EventWithId = undefined; 723 for (&comment_events, 0..) |*event, i| { 724 event.* = .{ 725 .id = std.fmt.bytesToHex(comment_ids[i], .lower), 726 .timestamp = @intCast(200 + i), 727 .author = .{ .name = user_data[(i + 1) % user_data.len].name, .email = user_data[(i + 1) % user_data.len].email }, 728 .event = .{ .comment = .{ 729 .thread_id = issue_events[issue_events.len - 1].id, 730 .parent_id = switch (i) { 731 0, 1 => issue_events[issue_events.len - 1].id, 732 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), 733 3 => std.fmt.bytesToHex(comment_ids[2], .lower), 734 else => unreachable, 735 }, 736 .body = comment_bodies[i], 737 } }, 738 }; 739 } 740 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &comment_events); 741 742 const discussion_data = [_]struct { 743 title: []const u8, 744 description: []const u8, 745 tags: []const u8, 746 }{ 747 .{ 748 .title = "How should plugins declare capabilities?", 749 .description = "I'd like the manifest to make privileged capabilities explicit without making simple plugins verbose.", 750 .tags = "plugins design", 751 }, 752 .{ 753 .title = "Ideas for making large repositories faster", 754 .description = "This is a place to collect profiling results and discuss which indexing work is worth pursuing first.", 755 .tags = "performance indexing", 756 }, 757 .{ 758 .title = "What should the next release focus on?", 759 .description = "Let's compare the most important reliability fixes with the larger features already in progress.", 760 .tags = "release planning", 761 }, 762 .{ 763 .title = "Improving keyboard navigation", 764 .description = "Share workflows that still require a mouse and suggestions for making their focus behavior predictable.", 765 .tags = "ui keyboard accessibility", 766 }, 767 .{ 768 .title = "Configuration format discussion", 769 .description = "Should the next configuration format favor strict validation or accept common conveniences such as trailing commas?", 770 .tags = "config design", 771 }, 772 }; 773 774 var discussion_events: [discussion_data.len]evt.EventWithId = undefined; 775 for (discussion_data, 0..) |discussion, i| { 776 discussion_events[i] = .{ 777 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 778 .timestamp = @intCast(300 + i), 779 .author = .{ .name = user_data[i % user_data.len].name, .email = user_data[i % user_data.len].email }, 780 .event = .{ .discuss = .{ 781 .title = discussion.title, 782 .description = discussion.description, 783 .tags = discussion.tags, 784 } }, 785 }; 786 } 787 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &discussion_events); 788 789 var discussion_comment_ids: [discussion_data.len][3][evt.event_id_size]u8 = undefined; 790 for (&discussion_comment_ids) |*ids| { 791 for (ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 792 } 793 var discussion_comment_events: [discussion_data.len * 3]evt.EventWithId = undefined; 794 for (&discussion_comment_events, 0..) |*event, i| { 795 const discussion_index = i / 3; 796 const comment_index = i % 3; 797 event.* = .{ 798 .id = std.fmt.bytesToHex(discussion_comment_ids[discussion_index][comment_index], .lower), 799 .timestamp = @intCast(400 + i), 800 .author = .{ .name = user_data[(i + 1) % user_data.len].name, .email = user_data[(i + 1) % user_data.len].email }, 801 .event = .{ .comment = .{ 802 .thread_id = discussion_events[discussion_index].id, 803 .parent_id = if (comment_index < 2) 804 discussion_events[discussion_index].id 805 else 806 std.fmt.bytesToHex(discussion_comment_ids[discussion_index][0], .lower), 807 .body = switch (comment_index) { 808 0 => "My preference is to start with the smallest useful version and expand it after we have real usage to learn from.", 809 1 => "It would help to write down the tradeoffs we are accepting so the decision is easy to revisit later.", 810 2 => "Agreed. A narrow first version should also make it easier to keep the implementation understandable.", 811 else => unreachable, 812 }, 813 } }, 814 }; 815 } 816 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &discussion_comment_events); 817 try template_repo.patchAll(io, allocator, null); 818 try template_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "true" }); 819 } 820 821 // copy the template to each repo's on-disk location, named by its 822 // hex-encoded event id. the template repo is deinitialized above, so its 823 // db file is fully written before we copy it. 824 { 825 var template_dir = try cwd.openDir(io, template_path, .{}); 826 defer template_dir.close(io); 827 var template_repo_dir = try template_dir.openDir(io, ".xit", .{ .iterate = true }); 828 defer template_repo_dir.close(io); 829 830 for (repo_event_ids, 0..) |id_bytes, repo_index| { 831 const repo_id = std.fmt.bytesToHex(id_bytes, .lower); 832 const repo_path = try std.fs.path.join(arena.allocator(), &.{ server_path, "repos", &repo_id }); 833 { 834 var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); 835 defer dest_dir.close(io); 836 var dest_repo_dir = try dest_dir.createDirPathOpen(io, ".xit", .{}); 837 defer dest_repo_dir.close(io); 838 try copyDir(io, template_repo_dir, dest_repo_dir); 839 } 840 841 if (repo_index + 1 == repo_data.len) { 842 var target_repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = repo_path, .require_repo_root = true }); 843 defer target_repo.deinit(io, allocator); 844 try seedPatches(io, allocator, server_path, &repo, &target_repo, &repo_event_ids[repo_index], &admin_user_id, prng.random()); 845 } 846 } 847 } 848 849 break :blk try ui.Session.init(&session_arena, &repo, .{ .user_id = &admin_user_id }); 850 }; 851 session.is_terminal = true; 852 session.data.git_ssh_prefix = git_ssh_prefix; 853 854 // start the server 855 856 // let the native TUI's page builders open the on-disk repos for the file tree 857 session.io = io; 858 session.repos_dir = try std.fs.path.join(session_arena.allocator(), &.{ server_path, "repos" }); 859 860 // leave a one-shot session for the first browser to hit the web ui, so it 861 // starts logged in as admin 862 { 863 var data_dir = try cwd.createDirPathOpen(io, server_path, .{}); 864 defer data_dir.close(io); 865 const store = try hx.web.SessionStore.init(io, data_dir); 866 defer store.deinit(); 867 const token = try store.create(&admin_user_id); 868 try store.offerAutoLogin(&token); 869 } 870 871 if (cli) { 872 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 873 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 874 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 875 876 const Runnable = struct { 877 io: std.Io, 878 key_path: []const u8, 879 880 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port_maybe: ?u16) !void { 881 _ = web_port; 882 _ = http_port; 883 const ssh_port = ssh_port_maybe orelse return error.SshDisabled; 884 std.debug.print( 885 \\ 886 \\connect to the TUI with: 887 \\ ssh -p {d} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 888 \\ 889 \\create a git repo and push over SSH: 890 \\ mkdir -p temp-try/client/test 891 \\ cd temp-try/client/test 892 \\ git init 893 \\ echo "hello" > hello.txt 894 \\ git add hello.txt 895 \\ git commit -m "let there be light" 896 \\ 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 897 \\ 898 \\to quit, press enter. 899 \\ 900 , .{ ssh_port, ssh_port, self.key_path }); 901 // portable stdin read via std.Io — equivalent to the 902 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 903 // on windows. 904 var buf: [1]u8 = undefined; 905 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 906 _ = stdin_reader.interface.takeByte() catch {}; 907 } 908 }; 909 910 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 911 } else { 912 var null_writer = std.Io.Writer.Discarding.init(&.{}); 913 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 914 915 const Runnable = struct { 916 io: std.Io, 917 allocator: std.mem.Allocator, 918 session: *ui.Session, 919 repo: *Repo, 920 921 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port: ?u16) !void { 922 // launch the TUI; the footer points at whatever port was bound 923 self.session.web_port = web_port; 924 self.session.data.git_http_port = http_port; 925 self.session.data.git_ssh_port = ssh_port; 926 try hx.ui.run(self.io, self.allocator, self.session, self.repo); 927 } 928 }; 929 930 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session, .repo = &repo }); 931 } 932 } 933 934 // words cycled through to pad generated lines into prose-like content 935 const scatter_words = [_][]const u8{ 936 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", 937 "quantum", "vector", "matrix", "buffer", "kernel", "socket", 938 "falcon", "otter", "badger", "walrus", "ferret", "marmot", 939 "scatter", "gather", "encode", "decode", "render", "commit", 940 }; 941 942 fn longDescription(allocator: std.mem.Allocator, initial: []const u8) ![]u8 { 943 var writer = std.Io.Writer.Allocating.init(allocator); 944 defer writer.deinit(); 945 try writer.writer.print("{s}", .{initial}); 946 for (0..ui.detail_preview_lines) |line| 947 try writer.writer.print("\n{d} {s}", .{ line, scatter_words[line % scatter_words.len] }); 948 return writer.toOwnedSlice(); 949 } 950 951 // write `path` as 40 lines of mostly-stable filler. revision `c` and file 952 // index `fi` shift which lines change and the words used, so each (c, fi) 953 // yields a distinct multi-hunk diff against the previous revision. untouched 954 // lines stay byte-identical across revisions, so a bump in `c` is a scatter of 955 // small hunks rather than a full rewrite. 956 fn writeScatterFile(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir, path: []const u8, fi: usize, c: usize) !void { 957 const content = try scatterContent(allocator, fi, c, null); 958 defer allocator.free(content); 959 const file = try repo_dir.createFile(io, path, .{}); 960 defer file.close(io); 961 try file.writeStreamingAll(io, content); 962 } 963 964 fn scatterContent(allocator: std.mem.Allocator, fi: usize, c: usize, replacement: ?struct { line: usize, text: []const u8 }) ![]u8 { 965 var writer = std.Io.Writer.Allocating.init(allocator); 966 defer writer.deinit(); 967 for (0..40) |line| { 968 if (replacement) |edit| if (line == edit.line) { 969 try writer.writer.print("{s}\n", .{edit.text}); 970 continue; 971 }; 972 const start = writer.written().len; 973 if ((line + fi + c) % 8 == 0) { 974 const tag = scatter_words[(line + fi + c * 3) % scatter_words.len]; 975 try writer.writer.print("rev {d} {s}", .{ c, tag }); 976 } 977 const target = 40 + (line * 17 + fi * 23) % 70; 978 var w = line + fi; 979 while (true) : (w += 1) { 980 const word = scatter_words[w % scatter_words.len]; 981 const len = writer.written().len - start; 982 if (len >= target or len + 1 + word.len > 120) break; 983 if (len == 0) 984 try writer.writer.print("{s}", .{word}) 985 else 986 try writer.writer.print(" {s}", .{word}); 987 } 988 try writer.writer.writeByte('\n'); 989 } 990 return writer.toOwnedSlice(); 991 } 992 993 // tag the current HEAD as the next sequential version: v1, v2, and so on 994 fn addNextTag(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { 995 var buf: [16]u8 = undefined; 996 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); 997 _ = try repo.addTag(io, allocator, .{ .name = name }); 998 n.* += 1; 999 } 1000 1001 fn commitTree( 1002 io: std.Io, 1003 allocator: std.mem.Allocator, 1004 repo: *rp.Repo(.xit, .{}), 1005 oid: *const [hash.hexLen(.sha1)]u8, 1006 ) ![hash.hexLen(.sha1)]u8 { 1007 var moment = try repo.core.latestMoment(); 1008 const state = rp.Repo(.xit, .{}).State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; 1009 var object = try obj.Object(.xit, .{}).init(state, io, allocator, oid); 1010 defer object.deinit(); 1011 return switch (object.content) { 1012 .commit => |commit| commit.tree, 1013 else => error.InvalidObject, 1014 }; 1015 } 1016 1017 fn seedPatchRevision( 1018 io: std.Io, 1019 allocator: std.mem.Allocator, 1020 repos_path: []const u8, 1021 patch_id: *const [evt.event_id_size]u8, 1022 title: []const u8, 1023 author: evt.CommitAuthor, 1024 timestamp: u64, 1025 random: std.Random, 1026 base_oid: [hash.hexLen(.sha1)]u8, 1027 path: []const u8, 1028 contents: []const []const u8, 1029 ) !void { 1030 const patch_hex = std.fmt.bytesToHex(patch_id.*, .lower); 1031 const fork_path = try fork.forkPath(allocator, repos_path, &patch_hex); 1032 defer allocator.free(fork_path); 1033 var fork_repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }); 1034 defer fork_repo.deinit(io, allocator); 1035 1036 try fork_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "false" }); 1037 { 1038 var reset = try fork_repo.reset(io, allocator, .{ .target = .{ .oid = &base_oid } }); 1039 defer reset.deinit(); 1040 } 1041 const base_tree_oid = try commitTree(io, allocator, &fork_repo, &base_oid); 1042 var source_oid = base_oid; 1043 var fork_dir = try std.Io.Dir.cwd().openDir(io, fork_path, .{}); 1044 defer fork_dir.close(io); 1045 var writer = std.Io.Writer.Allocating.init(allocator); 1046 defer writer.deinit(); 1047 if (std.fs.path.dirname(path)) |dir| try fork_dir.createDirPath(io, dir); 1048 for (contents, 0..) |content, i| { 1049 { 1050 const file = try fork_dir.createFile(io, path, .{}); 1051 defer file.close(io); 1052 try file.writeStreamingAll(io, content); 1053 } 1054 try fork_repo.add(io, allocator, &.{path}); 1055 writer.clearRetainingCapacity(); 1056 try writer.writer.print("{s} ({d}/{d})", .{ title, i + 1, contents.len }); 1057 source_oid = try fork_repo.commit(io, allocator, .{ .message = writer.written(), .timestamp = timestamp + i }); 1058 } 1059 try fork_repo.patchAll(io, allocator, null); 1060 try fork_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "true" }); 1061 try fork_dir.deleteFile(io, path); 1062 const revision_id = evt.EventWithId.randomId(random); 1063 const head_tree_oid = try commitTree(io, allocator, &fork_repo, &source_oid); 1064 const revision: evt.PatchRev = .{ 1065 .base_oid = &base_oid, 1066 .source_oid = &source_oid, 1067 .message = title, 1068 }; 1069 const revision_timestamp = timestamp + 3; 1070 const tree_entries = [_]evt.EventTreeEntry{ 1071 .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, 1072 .{ .tree = .{ .name = "head", .oid = &head_tree_oid } }, 1073 }; 1074 try evt.consume(.fork, .xit, .{}, io, allocator, &fork_repo, evt.events_ref, &.{.{ 1075 .id = std.fmt.bytesToHex(revision_id, .lower), 1076 .timestamp = revision_timestamp, 1077 .author = author, 1078 .tree_entries = &tree_entries, 1079 .event = .{ .patchrev = revision }, 1080 }}); 1081 1082 var arena = std.heap.ArenaAllocator.init(allocator); 1083 defer arena.deinit(); 1084 const moment = try evt.currentMoment(.{}, &fork_repo); 1085 const revision_record = (try evt.PatchRev.readById(evt.EventDB(.sha1), .sha1, moment, &arena, &revision_id)) orelse return error.NotFound; 1086 const patch_record = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &arena, patch_id)) orelse return error.NotFound; 1087 var patch = patch_record.event; 1088 patch.revision = evt.Patch.Revision.fromRecord(revision_id, revision_record); 1089 try evt.consume(.fork, .xit, .{}, io, allocator, &fork_repo, evt.events_ref, &.{.{ 1090 .id = patch_hex, 1091 .timestamp = revision_timestamp + 1, 1092 .author = author, 1093 .event = .{ .patch = patch }, 1094 }}); 1095 } 1096 1097 fn seedPatches( 1098 io: std.Io, 1099 allocator: std.mem.Allocator, 1100 server_path: []const u8, 1101 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), 1102 target_repo: *rp.Repo(.xit, .{}), 1103 repo_id: *const [evt.event_id_size]u8, 1104 user_id: *const [evt.event_id_size]u8, 1105 random: std.Random, 1106 ) !void { 1107 const patch_data = [_]struct { 1108 title: []const u8, 1109 description: []const u8, 1110 tags: []const u8, 1111 status: ?evt.Patch.StatusKind, 1112 adjacent_edit: bool = false, 1113 }{ 1114 .{ 1115 .title = "Draft a faster dependency scanner", 1116 .description = "Rework dependency discovery so a large workspace can be scanned without repeatedly opening the same manifests.", 1117 .tags = "performance build", 1118 .status = null, 1119 }, 1120 .{ 1121 .title = "Remove the legacy configuration loader", 1122 .description = "Delete the compatibility loader now that the replacement format has shipped and the migration warning has been available for a full release.", 1123 .tags = "cleanup config", 1124 .status = .merged, 1125 }, 1126 .{ 1127 .title = "Cache parsed manifests between commands", 1128 .description = "Keep parsed manifests in the command context so consecutive operations do not repeat identical filesystem and parsing work.", 1129 .tags = "performance cache", 1130 .status = .closed, 1131 }, 1132 .{ 1133 .title = "Add structured output to the inspect command", 1134 .description = "Add a stable JSON representation of inspect results for scripts and editor integrations.", 1135 .tags = "enhancement cli", 1136 .status = .open, 1137 }, 1138 .{ 1139 .title = "Edit an adjacent line in alpha.txt", 1140 .description = "This patch can be cleanly merged by haxy, while git throws a merge conflict!", 1141 .tags = "merge", 1142 .status = .open, 1143 .adjacent_edit = true, 1144 }, 1145 .{ 1146 .title = "Preserve file permissions during export", 1147 .description = "Carry executable bits through archive exports so unpacked command-line tools remain runnable without a manual chmod step.", 1148 .tags = "bug export permissions", 1149 .status = .open, 1150 }, 1151 }; 1152 1153 const repos_path = try std.fs.path.join(allocator, &.{ server_path, "repos" }); 1154 defer allocator.free(repos_path); 1155 const patch_author = evt.CommitAuthor{ .name = "admin", .email = "admin@example.test" }; 1156 var patch_ids: [patch_data.len][evt.event_id_size]u8 = undefined; 1157 for (patch_data, 0..) |patch, i| { 1158 const timestamp: u64 = @intCast(500 + i * 10); 1159 patch_ids[i] = evt.EventWithId.randomId(random); 1160 const patch_hex = std.fmt.bytesToHex(patch_ids[i], .lower); 1161 const path = try fork.create(.{}, io, allocator, repos_path, admin_repo, .{ 1162 .id = patch_hex, 1163 .user_id = user_id.*, 1164 .repo_id = repo_id.*, 1165 .title = patch.title, 1166 .description = patch.description, 1167 .tags = patch.tags, 1168 .target_branch = "master", 1169 .author = patch_author, 1170 .timestamp = timestamp, 1171 }); 1172 allocator.free(path); 1173 1174 // prepare the patch's base and file contents, then seed its commits and revision 1175 { 1176 var arena = std.heap.ArenaAllocator.init(allocator); 1177 defer arena.deinit(); 1178 const aa = arena.allocator(); 1179 var base_oid = (try target_repo.readRef(io, .{ .kind = .head, .name = "master" })) orelse return error.NotFound; 1180 var contents: [3][]const u8 = undefined; 1181 const file_path: []const u8, const count: usize = if (patch.adjacent_edit) blk: { 1182 // v30 precedes the last scatter commit, even after the sample merge 1183 const tag_oid = (try target_repo.readRef(io, .{ .kind = .tag, .name = "v30" })) orelse return error.NotFound; 1184 var moment = try target_repo.core.latestMoment(); 1185 const state = rp.Repo(.xit, .{}).State(.read_only){ .core = &target_repo.core, .extra = .{ .moment = &moment } }; 1186 var tag = try obj.Object(.xit, .{}).init(state, io, allocator, &tag_oid); 1187 defer tag.deinit(); 1188 base_oid = switch (tag.content) { 1189 .tag => |value| value.target, 1190 else => return error.InvalidObject, 1191 }; 1192 // rev 29 first changes line 4; edit the line above it in rev 28 1193 contents[0] = try scatterContent(aa, 0, 28, .{ .line = 2, .text = "adjust alpha beside the latest scatter edit" }); 1194 break :blk .{ "src/alpha.txt", 1 }; 1195 } else blk: { 1196 var writer = std.Io.Writer.Allocating.init(allocator); 1197 defer writer.deinit(); 1198 for (&contents, 0..) |*content, part| { 1199 try writer.writer.print("{s}: part {d}\n", .{ patch.title, part + 1 }); 1200 content.* = try aa.dupe(u8, writer.written()); 1201 } 1202 break :blk .{ "patch.txt", contents.len }; 1203 }; 1204 try seedPatchRevision(io, allocator, repos_path, &patch_ids[i], patch.title, patch_author, timestamp + 1, random, base_oid, file_path, contents[0..count]); 1205 } 1206 const status = patch.status orelse continue; 1207 const fork_path = try fork.forkPath(allocator, repos_path, &patch_hex); 1208 defer allocator.free(fork_path); 1209 try pch.publish(.{}, io, allocator, admin_repo, target_repo, fork_path, .{ 1210 .id = patch_hex, 1211 .user_id = user_id.*, 1212 .repo_id = repo_id.*, 1213 .author = patch_author, 1214 .timestamp = timestamp + 6, 1215 }); 1216 1217 if (status == .merged) { 1218 try pch.mergeAndRemoveFork(.{}, io, allocator, repos_path, admin_repo, target_repo, .{ 1219 .id = patch_hex, 1220 .revision = .source, 1221 .author = patch_author, 1222 .timestamp = timestamp + 7, 1223 }); 1224 } else if (status != .open) { 1225 try evt.Patch.update(.xit, .{}, io, allocator, target_repo, &patch_ids[i], .{ .status = status }, patch_author); 1226 } 1227 } 1228 1229 // seed a small comment tree on the newest patch 1230 var comment_ids: [5][evt.event_id_size]u8 = undefined; 1231 for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(random); 1232 const comment_bodies = [_][]const u8{ 1233 "This also needs to preserve the executable bit for files nested inside generated directories.", 1234 "I tested the archive path and the direct-copy path; both currently lose the mode.", 1235 "The archive writer is the right place to centralize it, since every exporter already goes through that layer.", 1236 "Please include a regression fixture with a mix of executable and ordinary files.", 1237 "I'll add that fixture and cover both supported archive formats.", 1238 }; 1239 var comments: [comment_ids.len]evt.EventWithId = undefined; 1240 for (&comments, 0..) |*comment, i| { 1241 comment.* = .{ 1242 .id = std.fmt.bytesToHex(comment_ids[i], .lower), 1243 .timestamp = @intCast(700 + i), 1244 .author = .{ .name = if (i % 2 == 0) "alice" else "bob", .email = if (i % 2 == 0) "alice@example.test" else "bob@example.test" }, 1245 .event = .{ .comment = .{ 1246 .thread_id = std.fmt.bytesToHex(patch_ids[patch_ids.len - 1], .lower), 1247 .parent_id = switch (i) { 1248 0, 1 => std.fmt.bytesToHex(patch_ids[patch_ids.len - 1], .lower), 1249 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), 1250 3 => std.fmt.bytesToHex(comment_ids[2], .lower), 1251 else => unreachable, 1252 }, 1253 .body = comment_bodies[i], 1254 } }, 1255 }; 1256 } 1257 try evt.consume(.repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &comments); 1258 1259 // create divergent metadata edits on the next three patches 1260 var patch_arena = std.heap.ArenaAllocator.init(allocator); 1261 defer patch_arena.deinit(); 1262 const moment = try evt.currentMoment(.{}, target_repo); 1263 var values: [3]evt.Patch = undefined; 1264 for (1..4) |i| { 1265 const record = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &patch_arena, &patch_ids[i])) orelse return error.NotFound; 1266 values[i - 1] = record.event; 1267 } 1268 var ours_values = values; 1269 ours_values[0].title = "Drop the legacy configuration loader"; 1270 ours_values[0].tags = "cleanup config breaking"; 1271 const closed_description = try longDescription( 1272 patch_arena.allocator(), 1273 "Cache parsed manifests for the lifetime of a command invocation and invalidate entries when their files change.", 1274 ); 1275 ours_values[1].description = closed_description; 1276 ours_values[2].description = "Add JSON output to inspect with versioned field names and deterministic object ordering."; 1277 var theirs_values = values; 1278 theirs_values[0].title = "Delete compatibility configuration support"; 1279 theirs_values[0].tags = "config maintenance"; 1280 theirs_values[1].description = "Keep a process-wide manifest cache shared by every command and refresh it after writes."; 1281 theirs_values[2].description = "Expose inspect results as newline-delimited JSON so callers can stream large repositories."; 1282 1283 const seed_tip = (try target_repo.readRef(io, evt.events_ref)) orelse return error.NotFound; 1284 const other_ref = xit.ref.Ref{ .kind = .head, .name = "haxy/patch-other" }; 1285 var ours = [_]evt.EventWithId{ 1286 .{ 1287 .id = std.fmt.bytesToHex(patch_ids[1], .lower), 1288 .timestamp = 800, 1289 .author = .{ .name = "alice", .email = "alice@example.test" }, 1290 .event = .{ .patch = ours_values[0] }, 1291 }, 1292 .{ 1293 .id = std.fmt.bytesToHex(patch_ids[2], .lower), 1294 .timestamp = 801, 1295 .author = .{ .name = "alice", .email = "alice@example.test" }, 1296 .event = .{ .patch = ours_values[1] }, 1297 }, 1298 .{ 1299 .id = std.fmt.bytesToHex(patch_ids[3], .lower), 1300 .timestamp = 802, 1301 .author = .{ .name = "alice", .email = "alice@example.test" }, 1302 .event = .{ .patch = ours_values[2] }, 1303 }, 1304 }; 1305 1306 var theirs = ours; 1307 theirs[0].timestamp = 810; 1308 theirs[0].author = .{ .name = "bob", .email = "bob@example.test" }; 1309 theirs[0].event.patch = theirs_values[0]; 1310 theirs[1].timestamp = 811; 1311 theirs[1].author = .{ .name = "bob", .email = "bob@example.test" }; 1312 theirs[1].event.patch = theirs_values[1]; 1313 theirs[2].timestamp = 812; 1314 theirs[2].author = .{ .name = "bob", .email = "bob@example.test" }; 1315 theirs[2].event.patch = theirs_values[2]; 1316 1317 try evt.consume(.repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &ours); 1318 var json: std.Io.Writer.Allocating = .init(allocator); 1319 defer json.deinit(); 1320 for (theirs, 0..) |event, i| { 1321 json.clearRetainingCapacity(); 1322 try std.json.Stringify.value(event, .{}, &json.writer); 1323 const author = try std.fmt.allocPrint(allocator, "{s} <{s}>", .{ event.author.name, event.author.email }); 1324 defer allocator.free(author); 1325 _ = try target_repo.commitAtRef(io, allocator, .{ 1326 .author = author, 1327 .message = json.written(), 1328 .timestamp = event.timestamp, 1329 .parent_oids = if (i == 0) &.{seed_tip} else null, 1330 }, null, other_ref); 1331 } 1332 { 1333 var merge = try target_repo.mergeAtRef(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, evt.events_ref, null); 1334 defer merge.deinit(); 1335 if (merge.result != .success) return error.MergeFailed; 1336 } 1337 try target_repo.removeBranch(io, .{ .name = other_ref.name }); 1338 try evt.consume(.repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &.{}); 1339 } 1340 1341 // recursively copy the contents of src_dir into dest_dir 1342 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 1343 var iter = src_dir.iterate(); 1344 while (try iter.next(io)) |entry| { 1345 switch (entry.kind) { 1346 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 1347 .directory => { 1348 try dest_dir.createDirPath(io, entry.name); 1349 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1350 defer dest_entry_dir.close(io); 1351 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1352 defer src_entry_dir.close(io); 1353 try copyDir(io, src_entry_dir, dest_entry_dir); 1354 }, 1355 else => {}, 1356 } 1357 } 1358 } 1359