╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │16 pub const SessionHandler = struct { │ │17 admin_repo_path: []const u8, │ │18 repo_root_path: []const u8, │ │19 + // test mode serves repos directly by path instead of resolving them as │ │20 + // <owner>/<repo> through the event store │ │21 + is_test: bool, │ │22 │ │23 pub fn handleSession(self: *const SessionHandler, sess: *ssh.SessionCtx, request: ssh.Request) !void {│ │24 std.debug.print("ssh session: kind={s} key={s}\n", .{ @tagName(request), sess.fingerprint }); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │172 }; │ │173 defer parsed.deinit(allocator); │ │174 │ │172 - const repo_path = try resolveRepoPath(allocator, handler.repo_root_path, parsed.dir); │ │175 + const create_if_missing = parsed.service == .receive_pack; │ │176 + const any_repo_opts: rp.AnyRepoOpts(.xit) = .{}; │ │177 + │ │178 + // derive the on-disk repo path. normally resolve <owner>/<repo> through the │ │179 + // event store (minting a repo event for a fresh push) and serve from its │ │180 + // event-id directory; in test mode serve the path directly │ │181 + const repo_path = if (handler.is_test) blk: { │ │182 + const direct_path = try resolveRepoPath(allocator, handler.repo_root_path, parsed.dir); │ │183 + if (!isSubPath(handler.repo_root_path, direct_path)) { │ │184 + allocator.free(direct_path); │ │185 + try writeError(sess, "forbidden path"); │ │186 + try sess.exit(1); │ │187 + return; │ │188 + } │ │189 + break :blk direct_path; │ │190 + } else blk: { │ │191 + const owner_repo = evt.parseOwnerRepoPath(parsed.dir) orelse { │ │192 + try writeError(sess, "repo path must be <owner>/<repo>"); │ │193 + try sess.exit(1); │ │194 + return; │ │195 + }; │ │196 + const event_id_hex = (try evt.resolveOrCreateRepo(io, allocator, handler.admin_repo_path, owner_repo.owner, owner_repo.name, create_if_missing)) orelse {│ │197 + try writeError(sess, "repo not found"); │ │198 + try sess.exit(1); │ │199 + return; │ │200 + }; │ │201 + break :blk try std.fs.path.join(allocator, &.{ handler.repo_root_path, &event_id_hex }); │ │202 + }; │ │203 defer allocator.free(repo_path); │ │204 │ │175 - if (!isSubPath(handler.repo_root_path, repo_path)) { │ │176 - try writeError(sess, "forbidden path"); │ │205 + if (try serveIfExists(repo_path, sess, io, allocator, parsed.service)) { │ │206 + try sess.exit(0); │ │207 + return; │ │208 + } │ │209 + │ │210 + if (!create_if_missing) { │ │211 + try writeError(sess, "repo not found"); │ │212 try sess.exit(1); │ │213 return; │ │214 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │215 │ │181 - const create_if_missing = parsed.service == .receive_pack; │ │182 - const any_repo_opts: rp.AnyRepoOpts(.xit) = .{}; │ │216 + // create the on-disk repo for the just-minted event and serve the push │ │217 + var repo = try createRepo(any_repo_opts.toRepoOpts(), io, allocator, repo_path); │ │218 + defer repo.deinit(io, allocator); │ │219 + try servePack(repo.self_repo_opts, &repo, sess, io, allocator, parsed.service); │ │220 + try sess.exit(0); │ │221 +} │ │222 + │ │223 +// serve an existing repo at `repo_path`, or return false if none is there │ │224 +fn serveIfExists( │ │225 + repo_path: []const u8, │ │226 + sess: *ssh.SessionCtx, │ │227 + io: std.Io, │ │228 + allocator: std.mem.Allocator, │ │229 + service: GitService, │ │230 +) !bool { │ │231 + // a bare open() creates the directory while probing for the repo, so only │ │232 + // attempt it when the path already exists │ │233 + std.Io.Dir.accessAbsolute(io, repo_path, .{}) catch return false; │ │234 │ │235 + const any_repo_opts: rp.AnyRepoOpts(.xit) = .{}; │ │236 var any_repo = rp.AnyRepo(.xit, any_repo_opts).open(io, allocator, .{ .path = repo_path }) catch |err| switch (err) {│ │185 - error.RepoNotFound => { │ │186 - if (!create_if_missing) { │ │187 - try writeError(sess, "repo not found"); │ │188 - try sess.exit(1); │ │189 - return; │ │190 - } │ │191 - // nothing on disk to detect a hash from, so create with the │ │192 - // default concrete options and serve that. │ │193 - var repo = try createRepo(any_repo_opts.toRepoOpts(), io, allocator, repo_path); │ │194 - defer repo.deinit(io, allocator); │ │195 - try servePack(repo.self_repo_opts, &repo, sess, io, allocator, parsed.service); │ │196 - try sess.exit(0); │ │197 - return; │ │198 - }, │ │237 + error.RepoNotFound => return false, │ │238 else => |e| return e, │ │239 }; │ │240 defer any_repo.deinit(io, allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │241 │ │242 switch (any_repo) { │ │204 - inline else => |*repo| try servePack(repo.self_repo_opts, repo, sess, io, allocator, parsed.service),│ │243 + inline else => |*repo| try servePack(repo.self_repo_opts, repo, sess, io, allocator, service), │ │244 } │ │206 - │ │207 - try sess.exit(0); │ │245 + return true; │ │246 } │ │247 │ │248 fn createRepo( │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯