╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │330 const repo_identity = if (std.mem.startsWith(u8, parsed.dir, repo_prefix)) parsed.dir[repo_prefix.len..] else parsed.dir; │ │331 │ │332 if (fork.parseRoute(repo_identity)) |route| { │ │333 - if (evt.parseOwnerRepoPath(route.identity) == null) return writeError(sess, "repo path must be <owner>/<repo>"); │ │333 + const owner_repo = evt.parseOwnerRepoPath(route.identity) orelse return writeError(sess, "repo path must be <owner>/<repo>"); │ │334 if (parsed.service == .receive_pack and !xit.ref.validateName(route.target)) return writeError(sess, "invalid target branch"); │ │335 + │ │336 + var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = handler.admin_repo_path }); │ │337 + defer admin.deinit(io, allocator); │ │338 + var admin_arena = std.heap.ArenaAllocator.init(allocator); │ │339 + defer admin_arena.deinit(); │ │340 + const admin_moment = try evt.currentMoment(evt.admin_repo_opts, &admin); │ │341 + const fork_id = try evt.parseEventId(&route.id); │ │342 + const fork_record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &admin_arena, &fork_id)) orelse │ │343 + return writeError(sess, "patch draft not found"); │ │344 + if (fork_record.removed) return writeError(sess, "patch draft not found"); │ │345 + const target_repo = (try evt.Repo.readByOwnerAndName(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &admin_arena, owner_repo.owner, owner_repo.name)) orelse│ │346 + return writeError(sess, "repo not found"); │ │347 + if (!std.mem.eql(u8, fork_record.event.repo_id, &target_repo.event_id)) return writeError(sess, "patch draft belongs to another repo"); │ │348 + const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &admin_arena, fork_record.event.user_id)) orelse │ │349 + return writeError(sess, "invalid patch draft"); │ │350 + if (user.removed) return writeError(sess, "invalid patch draft"); │ │351 + if (builtin.mode != .Debug and !isKeyInAuthorizedKeys(user.event.ssh_keys, &sess.fingerprint)) │ │352 + return writeError(sess, "unauthorized: this SSH key is not registered to the patch author"); │ │353 │ │354 const draft_path = try fork.forkPath(allocator, handler.repo_root_path, &route.id); │ │355 defer allocator.free(draft_path); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │358 defer draft.deinit(io, allocator); │ │359 switch (draft) { │ │360 inline else => |*repo| { │ │343 - const author_maybe = try fork.draftAuthor(repo.self_repo_opts, repo, io, allocator); │ │344 - defer if (author_maybe) |author| allocator.free(author); │ │345 - const author = author_maybe orelse return writeError(sess, "invalid patch draft"); │ │346 - if (builtin.mode != .Debug and !try isKeyAuthorized(io, allocator, handler.admin_repo_path, author, &sess.fingerprint))│ │347 - return writeError(sess, "unauthorized: this SSH key is not registered to the patch author"); │ │361 var reader_buf: [4096]u8 = undefined; │ │362 var writer_buf: [4096]u8 = undefined; │ │363 var reader = ssh.SessionReader.init(sess, &reader_buf); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │368 } │ │369 writer.interface.flush() catch {}; │ │370 if (parsed.service == .receive_pack) { │ │358 - if (try repo.readRef(io, fork.ref) == null) return writeError(sess, "push HEAD to refs/heads/patch"); │ │359 - try fork.setTarget(repo.self_repo_opts, repo, io, allocator, route.target); │ │371 + const source_oid = (try repo.readRef(io, fork.ref)) orelse return writeError(sess, "push HEAD to refs/heads/patch");│ │372 + try fork.recordPush(io, allocator, &admin, &route.id, route.target, &source_oid, .{ │ │373 + .name = user.event.name, │ │374 + .email = user.event.email, │ │375 + }, @intCast(std.Io.Timestamp.now(io, .real).toSeconds())); │ │376 } │ │377 }, │ │378 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │506 │ │507 const user = (try evt.User.readByName(io, allocator, admin_repo_path, &arena, owner_name)) orelse return false;│ │508 │ │493 - var it = std.mem.splitScalar(u8, user.event.ssh_keys, '\n'); │ │509 + return isKeyInAuthorizedKeys(user.event.ssh_keys, fingerprint); │ │510 +} │ │511 + │ │512 +fn isKeyInAuthorizedKeys(ssh_keys: []const u8, fingerprint: *const [ssh.fingerprint_len]u8) bool { │ │513 + var it = std.mem.splitScalar(u8, ssh_keys, '\n'); │ │514 while (it.next()) |line| { │ │515 const fp = fingerprintOfAuthorizedKey(line) orelse continue; │ │516 if (std.mem.eql(u8, &fp, fingerprint)) return true; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯