╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────╮ │src/serve.zig│ ╰─────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │383 - var any_repo = rp.AnyRepo(repo_kind, any_repo_opts).open(io, allocator, .{ .path = repo_path }) catch |open_err| switch (open_err) { │ │384 - error.RepoNotFound => { │ │385 - if (!create_if_missing) return open_err; │ │418 + var repo = try openRepo(repo_kind, any_repo_opts.toRepoOpts(), io, allocator, repo_path, true); │ │419 + defer repo.deinit(io, allocator); │ │420 + try service.serve(repo_kind, any_repo_opts.toRepoOpts(), &repo, io, allocator); │ │421 + } │ │422 +} │ │423 │ │387 - var repo = try openRepo(repo_kind, any_repo_opts.toRepoOpts(), io, allocator, repo_path, true); │ │388 - defer repo.deinit(io, allocator); │ │389 - try service.serve(repo_kind, any_repo_opts.toRepoOpts(), &repo, io, allocator); │ │390 - return; │ │391 - }, │ │424 +// serve an existing repo at `repo_path`, or return false if none is there │ │425 +fn serveIfExists( │ │426 + comptime repo_kind: rp.RepoKind, │ │427 + comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), │ │428 + io: std.Io, │ │429 + allocator: std.mem.Allocator, │ │430 + repo_path: []const u8, │ │431 + service: anytype, │ │432 +) !bool { │ │433 + // a bare open() creates the directory while probing for the repo, so only │ │434 + // attempt it when the path already exists │ │435 + std.Io.Dir.accessAbsolute(io, repo_path, .{}) catch return false; │ │436 + │ │437 + if (any_repo_opts.hash) |hash_kind| { │ │438 + var repo = rp.Repo(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind)).open(io, allocator, .{ .path = repo_path }) catch |open_err| switch (open_err) {│ │439 + error.RepoNotFound => return false, │ │440 else => |e| return e, │ │441 }; │ │442 + defer repo.deinit(io, allocator); │ │443 + try service.serve(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind), &repo, io, allocator); │ │444 + } else { │ │445 + var any_repo = rp.AnyRepo(repo_kind, any_repo_opts).open(io, allocator, .{ .path = repo_path }) catch |open_err| switch (open_err) { │ │446 + error.RepoNotFound => return false, │ │447 + else => |e| return e, │ │448 + }; │ │449 defer any_repo.deinit(io, allocator); │ │395 - │ │450 switch (any_repo) { │ │397 - inline else => |*repo| { │ │398 - try service.serve(repo.self_repo_kind, repo.self_repo_opts, repo, io, allocator); │ │399 - }, │ │451 + inline else => |*repo| try service.serve(repo.self_repo_kind, repo.self_repo_opts, repo, io, allocator), │ │452 } │ │453 } │ │454 + return true; │ │455 } │ │456 │ │457 fn openRepo( │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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( │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────╮ │src/test/net.zig│ ╰────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │106 const ssh_listen_arg = std.fmt.comptimePrint("127.0.0.1:{}", .{ssh_port}); │ │107 │ │108 const process = try std.process.spawn(io, .{ │ │109 - .argv = &.{ haxy_path, "serve", "--http-listen", http_listen_arg, "--ssh-listen", ssh_listen_arg, "--data-dir", temp_dir_name }, │ │109 + .argv = &.{ haxy_path, "serve", "--http-listen", http_listen_arg, "--ssh-listen", ssh_listen_arg, "--data-dir", temp_dir_name, "--test" },│ │110 .stdin = .ignore, │ │111 .stdout = .ignore, │ │112 .stderr = .ignore, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/try.zig│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │63 display_name: []const u8, │ │64 email: []const u8, │ │65 }{ │ │66 + .{ .name = "admin", .display_name = "Admin", .email = "admin@example.test" }, │ │67 .{ .name = "alice", .display_name = "Alice Tulley", .email = "alice@example.test" }, │ │68 .{ .name = "bob", .display_name = "Bob Smith", .email = "bob@example.test" }, │ │69 .{ .name = "carol", .display_name = "Carol Johnson", .email = "carol@example.test" },│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │259 \\ echo "hello" > hello.txt │ │260 \\ git add hello.txt │ │261 \\ git commit -m "let there be light" │ │261 - \\ GIT_SSH_COMMAND='ssh -p 8022 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' git push localhost:test HEAD:master │ │262 + \\ GIT_SSH_COMMAND='ssh -p 8022 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' git push localhost:admin/test HEAD:master│ │263 \\ │ │264 \\to quit, press enter. │ │265 \\ │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯