╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │26 admin_repo_path: []const u8, │ │27 repo_root_path: []const u8, │ │28 wui_port: u16, // port the web UI is served on, shown in the TUI footer's url │ │29 + clone_http_port: u16, │ │30 + clone_ssh_port: u16, │ │31 err: *std.Io.Writer, │ │32 │ │33 pub fn handleSession(self: *const SessionHandler, sess: *ssh.SessionCtx, request: ssh.Request) !void {│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │196 var ui_session = try ui.Session.init(&session_arena, &repo, .{}); │ │197 ui_session.is_terminal = true; │ │198 ui_session.web_port = handler.wui_port; │ │199 + ui_session.data.clone_http_port = handler.clone_http_port; │ │200 + ui_session.data.clone_ssh_port = handler.clone_ssh_port; │ │201 // let page builders open on-disk repos (sibling "repos" dir of the admin repo) │ │202 ui_session.io = io; │ │203 ui_session.repos_dir = try std.fs.path.join(session_arena.allocator(), &.{ std.fs.path.dirname(handler.admin_repo_path) orelse ".", "repos" });│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │212 // flushes, restoring the client's screen. as a function-scoped defer it runs │ │213 // before runTui returns — on the normal path and on any error — so the │ │214 // caller can write to the client afterward (and sess.exit can close cleanly). │ │211 - var terminal = try StreamTerminal.init(allocator, &session_writer.interface, .{ │ │215 + var terminal_size = Size{ │ │216 .width = effective_pty.width_cells, │ │217 .height = effective_pty.height_cells, │ │214 - }); │ │215 - defer terminal.deinit(); │ │218 + }; │ │219 + var terminal_maybe: ?StreamTerminal = try StreamTerminal.init(allocator, &session_writer.interface, terminal_size);│ │220 + defer if (terminal_maybe) |*terminal| terminal.deinit(); │ │221 │ │222 var last_size = Size{ .width = 0, .height = 0 }; │ │223 var last_grid = try Grid.init(allocator, last_size); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────╮ │228 .min_size = .{ .width = null, .height = null }, │ │229 .max_size = .{ .width = last_size.width, .height = last_size.height }, │ │230 }, nav.root.getFocus()); │ │226 - _ = try terminal.render(&nav.root, &last_grid, &last_size); │ │231 + if (terminal_maybe) |*terminal| _ = try terminal.render(&nav.root, &last_grid, &last_size);│ │232 │ │233 // event loop. nextEvent blocks until something interesting arrives; │ │234 // for each event, rebuild the widget tree and re-render so the user │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │235 // sees the effect of their input on the same iteration. │ │231 - while (!terminal.shouldQuit()) { │ │236 + event_loop: while (true) { │ │237 const event = try sess.nextEvent(); │ │233 - switch (event) { │ │234 - .data => |payload| { │ │235 - defer allocator.free(payload); │ │236 - try terminal.writeBytes(payload); │ │237 - while (terminal.popKey()) |key| { │ │238 - try ui.inputKey(allocator, &nav.root, key, &ui_session); │ │239 - } │ │240 - }, │ │241 - .resize => |sz| { │ │242 - terminal.pushResize(.{ .width = sz.width_cells, .height = sz.height_cells }); │ │243 - while (terminal.popKey()) |key| { │ │244 - try ui.inputKey(allocator, &nav.root, key, &ui_session); │ │245 - } │ │246 - }, │ │247 - // either way no more input can arrive, so nothing is left to │ │248 - // drive the ui — but on .eof the channel is still open, so the │ │249 - // teardown below still reaches the client. │ │250 - .eof, .close => terminal.requestQuit(), │ │238 + if (terminal_maybe) |*terminal| { │ │239 + switch (event) { │ │240 + .data => |payload| { │ │241 + defer allocator.free(payload); │ │242 + try terminal.writeBytes(payload); │ │243 + while (terminal.popKey()) |key| { │ │244 + try ui.inputKey(allocator, &nav.root, key, &ui_session); │ │245 + } │ │246 + }, │ │247 + .resize => |sz| { │ │248 + terminal_size = .{ .width = sz.width_cells, .height = sz.height_cells }; │ │249 + terminal.pushResize(terminal_size); │ │250 + while (terminal.popKey()) |key| { │ │251 + try ui.inputKey(allocator, &nav.root, key, &ui_session); │ │252 + } │ │253 + }, │ │254 + .eof, .close => break :event_loop, │ │255 + } │ │256 + } else { │ │257 + switch (event) { │ │258 + .data => |payload| { │ │259 + defer allocator.free(payload); │ │260 + if (std.mem.indexOfAny(u8, payload, "\r\n") == null) continue; │ │261 + terminal_maybe = try StreamTerminal.init(allocator, &session_writer.interface, terminal_size); │ │262 + last_size = .{ .width = 0, .height = 0 }; │ │263 + }, │ │264 + .resize => |sz| { │ │265 + terminal_size = .{ .width = sz.width_cells, .height = sz.height_cells }; │ │266 + continue; │ │267 + }, │ │268 + .eof, .close => break :event_loop, │ │269 + } │ │270 + } │ │271 + │ │272 + if (ui_session.host_request) |request| { │ │273 + ui_session.host_request = null; │ │274 + switch (request) { │ │275 + .clone_url => |url| { │ │276 + if (terminal_maybe) |*terminal| terminal.deinit(); │ │277 + terminal_maybe = null; │ │278 + try session_writer.interface.print("\x1b[2J\x1b[H{s}\r\n\r\npress enter to go back\r\n", .{url});│ │279 + try session_writer.interface.flush(); │ │280 + continue; │ │281 + }, │ │282 + } │ │283 } │ │284 │ │285 try ui_session.applyAndWritePending(io, allocator, &repo); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────╮ │292 try nav.sync(allocator, &ui_session); │ │293 │ │294 // the quit button (on the quit tab) asks the host to tear down│ │263 - if (ui_session.quit_requested) terminal.requestQuit(); │ │295 + if (ui_session.quit_requested) break :event_loop; │ │296 │ │297 try nav.root.build(allocator, .{ │ │298 .min_size = .{ .width = null, .height = null }, │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │299 .max_size = .{ .width = last_size.width, .height = last_size.height }, │ │300 }, nav.root.getFocus()); │ │269 - _ = try terminal.render(&nav.root, &last_grid, &last_size); │ │301 + if (terminal_maybe) |*terminal| _ = try terminal.render(&nav.root, &last_grid, &last_size);│ │302 } │ │303 } │ │304 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │323 │ │324 const parsed = parseGitCommand(allocator, command) catch return writeError(sess, "unsupported command (expected git-upload-pack or git-receive-pack)");│ │325 defer parsed.deinit(allocator); │ │326 + const repo_prefix = "/repo/"; │ │327 + const repo_identity = if (std.mem.startsWith(u8, parsed.dir, repo_prefix)) parsed.dir[repo_prefix.len..] else parsed.dir; │ │328 │ │329 const create_if_missing = parsed.service == .receive_pack; │ │330 const any_repo_opts: rp.AnyRepoOpts(.xit) = .{}; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │332 // authenticate pushes: the authenticated key must be registered to the │ │333 // repo's owner. │ │334 if (create_if_missing) { │ │301 - const owner_repo = evt.parseOwnerRepoPath(parsed.dir) orelse return writeError(sess, "repo path must be <owner>/<repo>"); │ │335 + const owner_repo = evt.parseOwnerRepoPath(repo_identity) orelse return writeError(sess, "repo path must be <owner>/<repo>");│ │336 if (!try isKeyAuthorized(io, allocator, handler.admin_repo_path, owner_repo.owner, &sess.fingerprint)) │ │337 return writeError(sess, "unauthorized: this SSH key is not registered to the repo owner"); │ │338 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │339 │ │306 - const repo_path = switch (try serve_common.resolveRepoPath(io, allocator, handler.repo_root_path, handler.admin_repo_path, parsed.dir, create_if_missing)) { │ │340 + const repo_path = switch (try serve_common.resolveRepoPath(io, allocator, handler.repo_root_path, handler.admin_repo_path, repo_identity, create_if_missing)) {│ │341 .ok => |p| p, │ │342 .invalid => return writeError(sess, "repo path must be <owner>/<repo>"), │ │343 .not_found => return writeError(sess, "repo not found"), │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯