╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │367 const fork_prefix = "fork/"; │ │368 │ │369 if (std.mem.startsWith(u8, path, fork_prefix)) { │ │372 - const route = fork.parseRoute(path[fork_prefix.len..]) orelse return writeError(sess, "invalid fork path"); │ │373 - const owner_repo = evt.parseOwnerRepoPath(route.identity) orelse return writeError(sess, "repo path must be <owner>/<repo>"); │ │374 - │ │375 - var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = handler.admin_repo_path }); │ │376 - defer admin.deinit(io, allocator); │ │377 - var admin_arena = std.heap.ArenaAllocator.init(allocator); │ │378 - defer admin_arena.deinit(); │ │379 - const admin_moment = try evt.currentMoment(evt.admin_repo_opts, &admin); │ │380 - const fork_id = try evt.parseEventId(&route.id); │ │381 - const fork_record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &admin_arena, &fork_id)) orelse │ │382 - return writeError(sess, "patch draft not found"); │ │383 - if (fork_record.removed) return writeError(sess, "patch draft not found"); │ │384 - const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &admin_arena, fork_record.event.user_id)) orelse │ │385 - return writeError(sess, "invalid patch draft"); │ │386 - if (user.removed) return writeError(sess, "invalid patch draft"); │ │387 - if (parsed.service == .receive_pack and !isKeyInAuthorizedKeys(user.event.ssh_keys, &sess.fingerprint)) │ │388 - return writeError(sess, "unauthorized: this SSH key is not registered to the patch author"); │ │389 - │ │390 - const draft_path = try fork.forkPath(allocator, handler.repo_root_path, &route.id); │ │391 - defer allocator.free(draft_path); │ │392 - var draft = rp.AnyRepo(.xit, .{ .ProgressCtx = *push.PushProgress }).open(io, allocator, .{ .path = draft_path }) catch │ │393 - return writeError(sess, "patch draft not found"); │ │394 - defer draft.deinit(io, allocator); │ │395 - var reader_buf: [4096]u8 = undefined; │ │396 - var writer_buf: [4096]u8 = undefined; │ │397 - var reader = ssh.SessionReader.init(sess, &reader_buf); │ │398 - var writer = ssh.SessionWriter.init(sess, &writer_buf); │ │399 - if (parsed.service == .upload_pack) { │ │400 - switch (draft) { │ │401 - inline else => |*repo| try repo.uploadPack(io, allocator, &reader.interface, &writer.interface, .{ .protocol_version = protocol_version }), │ │402 - } │ │403 - } else { │ │404 - const target = (try evt.Repo.readByOwnerAndName(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &admin_arena, owner_repo.owner, owner_repo.name)) orelse │ │405 - return writeError(sess, "repo not found"); │ │406 - if (!std.mem.eql(u8, fork_record.event.repo_id, &target.event_id)) return writeError(sess, "patch draft belongs to another repo"); │ │407 - const target_id = std.fmt.bytesToHex(target.event_id, .lower); │ │408 - const target_path = try std.fs.path.join(allocator, &.{ handler.repo_root_path, &target_id }); │ │409 - defer allocator.free(target_path); │ │410 - const author: evt.CommitAuthor = .{ .name = user.event.name, .email = user.event.email }; │ │411 - const timestamp: u64 = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()); │ │412 - switch (draft) { │ │413 - inline else => |*repo| { │ │414 - var target_repo = rp.Repo(.xit, repo.self_repo_opts).open(io, allocator, .{ .path = target_path }) catch │ │415 - return writeError(sess, "repo not found or has the wrong hash"); │ │416 - defer target_repo.deinit(io, allocator); │ │417 - try push.receiveFork(repo.self_repo_opts, io, allocator, repo, &target_repo, &route.id, author, timestamp, &reader.interface, &writer.interface, handler.err);│ │418 - }, │ │419 - } │ │420 - } │ │421 - writer.interface.flush() catch {}; │ │422 - try sess.exit(0); │ │423 - return; │ │370 + return runForkSession(handler, sess, path[fork_prefix.len..], parsed.service, protocol_version, &reader.interface, &writer.interface); │ │371 } │ │372 │ │373 const repo_prefix = "repo/"; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │374 const repo_identity = if (std.mem.startsWith(u8, path, repo_prefix)) path[repo_prefix.len..] else path; │ │375 │ │376 const create_if_missing = parsed.service == .receive_pack; │ │430 - const any_repo_opts: rp.AnyRepoOpts(.xit) = .{ .ProgressCtx = *push.PushProgress }; │ │377 │ │378 const owner_repo = evt.parseOwnerRepoPath(repo_identity) orelse return writeError(sess, "repo path must be <owner>/<repo>"); │ │379 switch (try authorizeRepoKey(io, allocator, handler.admin_repo_path, owner_repo.owner, owner_repo.name, parsed.service, &sess.fingerprint)) {│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │387 │ │388 const repo_path = switch (try serve_common.resolveRepoPath(io, allocator, handler.repo_root_path, handler.admin_repo_path, repo_identity, create_if_missing)) {│ │389 .ok => |p| p, │ │444 - .invalid => return writeError(sess, "repo path must be <owner>/<repo>"), │ │390 + .invalid => unreachable, // parsed above │ │391 .not_found => return writeError(sess, "repo not found"), │ │392 }; │ │393 defer allocator.free(repo_path); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │394 │ │449 - if (try serveIfExists(repo_path, handler.repo_root_path, sess, io, allocator, parsed.service, protocol_version, handler.err)) { │ │450 - try sess.exit(0); │ │451 - return; │ │452 - } │ │395 + if (try serveIfExists(repo_path, handler.repo_root_path, &reader.interface, &writer.interface, io, allocator, parsed.service, protocol_version, handler.err)) return;│ │396 │ │397 if (!create_if_missing) return writeError(sess, "repo not found"); │ │398 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │399 // create the on-disk repo for the just-minted event and serve the push │ │400 var repo = try createRepo(any_repo_opts.toRepoOpts(), io, allocator, repo_path); │ │401 defer repo.deinit(io, allocator); │ │459 - try servePack(repo.self_repo_opts, &repo, handler.repo_root_path, sess, io, allocator, parsed.service, protocol_version, handler.err); │ │460 - try sess.exit(0); │ │402 + try servePack(repo.self_repo_opts, &repo, handler.repo_root_path, &reader.interface, &writer.interface, io, allocator, parsed.service, protocol_version, handler.err);│ │403 +} │ │404 + │ │405 +// serve a patch draft: anyone may fetch it, only its author may push to it │ │406 +fn runForkSession( │ │407 + handler: *const SessionHandler, │ │408 + sess: *ssh.SessionCtx, │ │409 + fork_path: []const u8, │ │410 + service: GitService, │ │411 + protocol_version: xit.net_server_common.ProtocolVersion, │ │412 + reader: *std.Io.Reader, │ │413 + writer: *std.Io.Writer, │ │414 +) !void { │ │415 + const allocator = sess.conn.allocator; │ │416 + const io = sess.conn.io; │ │417 + │ │418 + const route = fork.parseRoute(fork_path) orelse return writeError(sess, "invalid fork path"); │ │419 + const owner_repo = evt.parseOwnerRepoPath(route.identity) orelse return writeError(sess, "repo path must be <owner>/<repo>"); │ │420 + │ │421 + var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = handler.admin_repo_path }); │ │422 + defer admin.deinit(io, allocator); │ │423 + var admin_arena = std.heap.ArenaAllocator.init(allocator); │ │424 + defer admin_arena.deinit(); │ │425 + const admin_moment = try evt.currentMoment(evt.admin_repo_opts, &admin); │ │426 + const fork_id = try evt.parseEventId(&route.id); │ │427 + const fork_record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &admin_arena, &fork_id)) orelse │ │428 + return writeError(sess, "patch draft not found"); │ │429 + if (fork_record.removed) return writeError(sess, "patch draft not found"); │ │430 + const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &admin_arena, fork_record.event.user_id)) orelse │ │431 + return writeError(sess, "invalid patch draft"); │ │432 + if (user.removed) return writeError(sess, "invalid patch draft"); │ │433 + if (service == .receive_pack and !isKeyInAuthorizedKeys(user.event.ssh_keys, &sess.fingerprint)) │ │434 + return writeError(sess, "unauthorized: this SSH key is not registered to the patch author"); │ │435 + │ │436 + const draft_path = try fork.forkPath(allocator, handler.repo_root_path, &route.id); │ │437 + defer allocator.free(draft_path); │ │438 + var draft = rp.AnyRepo(.xit, any_repo_opts).open(io, allocator, .{ .path = draft_path }) catch │ │439 + return writeError(sess, "patch draft not found"); │ │440 + defer draft.deinit(io, allocator); │ │441 + if (service == .upload_pack) { │ │442 + switch (draft) { │ │443 + inline else => |*repo| try repo.uploadPack(io, allocator, reader, writer, .{ .protocol_version = protocol_version }), │ │444 + } │ │445 + } else { │ │446 + const target = (try evt.Repo.readByOwnerAndName(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &admin_arena, owner_repo.owner, owner_repo.name)) orelse │ │447 + return writeError(sess, "repo not found"); │ │448 + if (!std.mem.eql(u8, fork_record.event.repo_id, &target.event_id)) return writeError(sess, "patch draft belongs to another repo"); │ │449 + const target_id = std.fmt.bytesToHex(target.event_id, .lower); │ │450 + const target_path = try std.fs.path.join(allocator, &.{ handler.repo_root_path, &target_id }); │ │451 + defer allocator.free(target_path); │ │452 + const author: evt.CommitAuthor = .{ .name = user.event.name, .email = user.event.email }; │ │453 + const timestamp: u64 = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()); │ │454 + switch (draft) { │ │455 + inline else => |*repo| { │ │456 + var target_repo = rp.Repo(.xit, repo.self_repo_opts).open(io, allocator, .{ .path = target_path }) catch │ │457 + return writeError(sess, "repo not found or has the wrong hash"); │ │458 + defer target_repo.deinit(io, allocator); │ │459 + try push.receiveFork(repo.self_repo_opts, io, allocator, repo, &target_repo, &route.id, author, timestamp, reader, writer, handler.err); │ │460 + }, │ │461 + } │ │462 + } │ │463 } │ │464 │ │465 // serve an existing repo at `repo_path`, or return false if none is there │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────╮ │466 fn serveIfExists( │ │467 repo_path: []const u8, │ │468 repo_root_path: []const u8, │ │467 - sess: *ssh.SessionCtx, │ │469 + reader: *std.Io.Reader, │ │470 + writer: *std.Io.Writer, │ │471 io: std.Io, │ │472 allocator: std.mem.Allocator,│ │473 service: GitService, │ ╰──────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │478 // attempt it when the path already exists │ │479 std.Io.Dir.accessAbsolute(io, repo_path, .{}) catch return false; │ │480 │ │478 - const any_repo_opts: rp.AnyRepoOpts(.xit) = .{ .ProgressCtx = *push.PushProgress }; │ │481 var any_repo = rp.AnyRepo(.xit, any_repo_opts).open(io, allocator, .{ .path = repo_path }) catch |err| switch (err) {│ │482 error.RepoNotFound => return false, │ │483 else => |e| return e, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │485 defer any_repo.deinit(io, allocator); │ │486 │ │487 switch (any_repo) { │ │486 - inline else => |*repo| try servePack(repo.self_repo_opts, repo, repo_root_path, sess, io, allocator, service, protocol_version, error_writer), │ │488 + inline else => |*repo| try servePack(repo.self_repo_opts, repo, repo_root_path, reader, writer, io, allocator, service, protocol_version, error_writer),│ │489 } │ │490 return true; │ │491 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │503 return repo; │ │504 } │ │505 │ │504 -// wrap the channel I/O in std.Io.Reader/Writer adapters so the repo's pack│ │505 -// functions can consume them like any other stream, then run the requested│ │506 -// service │ │506 fn servePack( │ │507 comptime repo_opts: rp.RepoOpts(.xit), │ │508 repo: *rp.Repo(.xit, repo_opts), │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────╮ │509 repo_root_path: []const u8, │ │511 - sess: *ssh.SessionCtx, │ │510 + reader: *std.Io.Reader, │ │511 + writer: *std.Io.Writer, │ │512 io: std.Io, │ │513 allocator: std.mem.Allocator,│ │514 service: GitService, │ ╰──────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯