╭ changed ────╮ │build.zig.zon│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 3 .version = "0.0.0", │ │ 4 .dependencies = .{ │ │ 5 .xit = .{ │ │ 6 - .url = "git+https://github.com/xit-vcs/xit#f45f785b423dfa88288974ca405dd8822529c640",│ │ 7 - .hash = "xit-0.0.0-Ub3nDLwxVAHK56fiI9PPem70Ut4k6xdWHvszjrVCw1jg", │ │ 6 + .url = "git+https://github.com/xit-vcs/xit#7184fdcfb10fc817a2e9cff01200abfa0d3642e6",│ │ 7 + .hash = "xit-0.0.0-Ub3nDMM-VAHpJIpy2AGPyfJrnAo-jq9vvJJGhDTmGa4E", │ │ 8 }, │ │ 9 }, │ │10 .paths = .{ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────╮ │src/serve_common.zig│ ╰────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │ 7 pub const RepoPath = union(enum) { │ │ 8 ok: []const u8, // resolved on-disk path; the caller owns and frees it │ │ 9 invalid, // a <owner>:<repo> path was expected but not given │ │10 - not_found, // unknown owner, or the repo doesn't exist and we aren't creating│ │10 + not_found, // unknown owner, or the repo doesn't exist │ │11 }; │ │12 │ │13 // resolve a requested repo path to its on-disk directory, parsed as │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │14 -// <owner>:<repo> through the event store, minting a repo event for a fresh push.│ │14 +// <owner>:<repo> through the event store. │ │15 pub fn resolveRepoPath( │ │16 io: std.Io, │ │17 allocator: std.mem.Allocator, │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │18 users_dir: []const u8, │ │19 admin_repo_path: []const u8, │ │20 requested: []const u8, │ │21 - create_if_missing: bool, │ │21 ) !RepoPath { │ │22 const owner_repo = evt.parseOwnerRepoPath(requested) orelse return .invalid;│ │23 const location = (try evt.resolveOrCreateRepo( │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │27 admin_repo_path, │ │28 owner_repo.owner, │ │29 owner_repo.name, │ │31 - if (create_if_missing) .{} else null, │ │30 + null, │ │31 )) orelse return .not_found; │ │32 return .{ .ok = try evt.repoPath(allocator, users_dir, &location.owner_id, &location.repo_id) };│ │33 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │380 if (std.mem.indexOfScalar(u8, repo_identity, '+') != null) { │ │381 return runForkSession(handler, sess, repo_identity, parsed.service, protocol_version, &reader.interface, &writer.interface);│ │382 } │ │383 - │ │384 - const create_if_missing = parsed.service == .receive_pack; │ │383 │ │384 const owner_repo = evt.parseOwnerRepoPath(repo_identity) orelse return writeError(sess, "repo path must be <owner>:<repo>"); │ │385 var author_arena = std.heap.ArenaAllocator.init(allocator); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │386 defer author_arena.deinit(); │ │387 const author = switch (try authorizeRepoKey(io, &author_arena, handler.admin_repo_path, handler.users_dir, owner_repo.owner, owner_repo.name, parsed.service, &sess.fingerprint)) {│ │388 .allowed => |user| user, │ │389 + .create => |creator| return runCreatingPush(handler, sess, &creator, owner_repo.name, protocol_version, &reader.interface, &writer.interface), │ │390 .denied => return switch (parsed.service) { │ │391 .upload_pack => writeError(sess, "unauthorized: this SSH key cannot read this repo"), │ │392 .receive_pack => writeError(sess, "unauthorized: this SSH key cannot push to this repo"), │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │394 .not_found => return writeError(sess, "repo not found"), │ │395 }; │ │396 │ │398 - const repo_path = switch (try serve_common.resolveRepoPath(io, allocator, handler.users_dir, handler.admin_repo_path, repo_identity, create_if_missing)) {│ │397 + const repo_path = switch (try serve_common.resolveRepoPath(io, allocator, handler.users_dir, handler.admin_repo_path, repo_identity)) { │ │398 .ok => |p| p, │ │399 .invalid => unreachable, // parsed above │ │400 .not_found => return writeError(sess, "repo not found"), │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │401 }; │ │402 defer allocator.free(repo_path); │ │403 + │ │404 + var any_repo = try rp.AnyRepo(.xit, any_repo_opts).open(io, allocator, .{ .path = repo_path }); │ │405 + defer any_repo.deinit(io, allocator); │ │406 + │ │407 + switch (any_repo) { │ │408 + inline else => |*repo| switch (parsed.service) { │ │409 + .upload_pack => { │ │410 + var sideband = progress.Sideband{ .writer = &writer.interface, .sess = sess }; │ │411 + try repo.uploadPack(io, allocator, &reader.interface, &writer.interface, .{ .protocol_version = protocol_version }, &sideband); │ │412 + }, │ │413 + .receive_pack => try push.receivePackAndConsume(repo.self_repo_opts, io, allocator, repo, &reader.interface, &writer.interface, .{ .protocol_version = protocol_version }, author, handler.users_dir, handler.err, sess),│ │414 + }, │ │415 + } │ │416 +} │ │417 │ │405 - if (try serveIfExists(repo_path, handler.users_dir, &reader.interface, &writer.interface, io, allocator, parsed.service, protocol_version, author, handler.err, sess)) return; │ │418 +// push to a repo that doesn't exist yet, creating it in the hash format the │ │419 +// client's first command names │ │420 +fn runCreatingPush( │ │421 + handler: *const SessionHandler, │ │422 + sess: *ssh.SessionCtx, │ │423 + creator: *const RepoCreator, │ │424 + name: []const u8, │ │425 + protocol_version: xit.net_server_common.ProtocolVersion, │ │426 + reader: *std.Io.Reader, │ │427 + writer: *std.Io.Writer, │ │428 +) !void { │ │429 + const allocator = sess.conn.allocator; │ │430 + const io = sess.conn.io; │ │431 │ │407 - if (!create_if_missing) return writeError(sess, "repo not found"); │ │432 + evt.Repo.validateName(name) catch return writeError(sess, "invalid repo name"); │ │433 + │ │434 + try xit.net_server_receive_pack.advertiseUncreated(.xit, writer, .{ .protocol_version = protocol_version }); │ │435 + try writer.flush(); │ │436 + // a client with nothing to push creates nothing │ │437 + const hash_kind = (try xit.net_server_receive_pack.peekObjectFormat(reader)) orelse return; │ │438 + │ │439 + const location = evt.createRepo(io, allocator, handler.users_dir, &creator.owner_id, creator.owner, name, "", .private, hash_kind) catch |err| switch (err) { │ │440 + error.NameTaken => return writeError(sess, "repo was just created by another push, try again"), │ │441 + else => |e| return e, │ │442 + }; │ │443 + const repo_path = try evt.repoPath(allocator, handler.users_dir, &location.owner_id, &location.repo_id); │ │444 + defer allocator.free(repo_path); │ │445 │ │409 - // create the on-disk repo for the just-minted event and serve the push │ │410 - var repo = try rp.Repo(.xit, any_repo_opts.toRepoOpts()).init(io, allocator, .{ .path = repo_path, .bare = true }); │ │411 - defer repo.deinit(io, allocator); │ │412 - try servePack(repo.self_repo_opts, &repo, handler.users_dir, &reader.interface, &writer.interface, io, allocator, parsed.service, protocol_version, author, handler.err, sess); │ │446 + switch (hash_kind) { │ │447 + inline else => |kind| { │ │448 + var repo = try rp.Repo(.xit, any_repo_opts.toRepoOptsWithHash(kind)).open(io, allocator, .{ .path = repo_path }); │ │449 + defer repo.deinit(io, allocator); │ │450 + // stateless since the advertisement already went out │ │451 + try push.receivePackAndConsume(repo.self_repo_opts, io, allocator, &repo, reader, writer, .{ .protocol_version = protocol_version, .is_stateless = true }, creator.owner, handler.users_dir, handler.err, sess); │ │452 + }, │ │453 + } │ │454 } │ │455 │ │456 // serve a patch draft: anyone may fetch it, only its author may push to it │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │516 } │ │517 } │ │518 │ │478 -// serve an existing repo at `repo_path`, or return false if none is there │ │479 -fn serveIfExists( │ │480 - repo_path: []const u8, │ │481 - users_dir: []const u8, │ │482 - reader: *std.Io.Reader, │ │483 - writer: *std.Io.Writer, │ │484 - io: std.Io, │ │485 - allocator: std.mem.Allocator, │ │486 - service: GitService, │ │487 - protocol_version: xit.net_server_common.ProtocolVersion, │ │488 - author: ?evt.CommitAuthor, │ │489 - error_writer: *std.Io.Writer, │ │490 - sess: ?*ssh.SessionCtx, │ │491 -) !bool { │ │492 - // a bare open() creates the directory while probing for the repo, so only │ │493 - // attempt it when the path already exists │ │494 - std.Io.Dir.accessAbsolute(io, repo_path, .{}) catch return false; │ │495 - │ │496 - var any_repo = rp.AnyRepo(.xit, any_repo_opts).open(io, allocator, .{ .path = repo_path }) catch |err| switch (err) { │ │497 - error.RepoNotFound => return false, │ │498 - else => |e| return e, │ │499 - }; │ │500 - defer any_repo.deinit(io, allocator); │ │501 - │ │502 - switch (any_repo) { │ │503 - inline else => |*repo| try servePack(repo.self_repo_opts, repo, users_dir, reader, writer, io, allocator, service, protocol_version, author, error_writer, sess), │ │504 - } │ │505 - return true; │ │506 -} │ │507 - │ │508 -fn servePack( │ │509 - comptime repo_opts: rp.RepoOpts(.xit), │ │510 - repo: *rp.Repo(.xit, repo_opts), │ │511 - users_dir: []const u8, │ │512 - reader: *std.Io.Reader, │ │513 - writer: *std.Io.Writer, │ │514 - io: std.Io, │ │515 - allocator: std.mem.Allocator, │ │516 - service: GitService, │ │517 - protocol_version: xit.net_server_common.ProtocolVersion, │ │518 - author: ?evt.CommitAuthor, │ │519 - error_writer: *std.Io.Writer, │ │520 - sess: ?*ssh.SessionCtx, │ │521 -) !void { │ │522 - switch (service) { │ │523 - .upload_pack => { │ │524 - var sideband = progress.Sideband{ .writer = writer, .sess = sess }; │ │525 - try repo.uploadPack(io, allocator, reader, writer, .{ .protocol_version = protocol_version }, &sideband); │ │526 - }, │ │527 - .receive_pack => try push.receivePackAndConsume(repo_opts, io, allocator, repo, reader, writer, .{ .protocol_version = protocol_version }, author, users_dir, error_writer, sess),│ │528 - } │ │529 -} │ │530 - │ │519 fn parseGitCommand(allocator: std.mem.Allocator, command: []const u8) !ParsedGitCommand { │ │520 var tokens = try std.process.Args.IteratorGeneral(.{ .single_quotes = true }).init(allocator, command); │ │521 defer tokens.deinit(); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯