╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ───╮ │src/fork.zig│ ╰────────────╯ ╭────────────────────────────────────────╮ │145 id: [evt.event_id_size * 2]u8, │ │146 + user_id: [evt.event_id_size]u8,│ │147 + repo_id: [evt.event_id_size]u8,│ │148 title: []const u8, │ │149 tags: []const u8, │ │150 description: []const u8, │ ╰────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────╮ │156 comptime repo_opts: rp.RepoOpts(.xit), │ │157 io: std.Io, │ │158 allocator: std.mem.Allocator, │ │159 + admin_repo: *rp.Repo(.xit, evt.admin_repo_opts),│ │160 target_repo: *rp.Repo(.xit, repo_opts), │ │161 draft_path: []const u8, │ │162 input: PublishInput, │ ╰─────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │166 │ │167 var arena = std.heap.ArenaAllocator.init(allocator); │ │168 defer arena.deinit(); │ │169 + const admin_moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); │ │170 + const fork_record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &patch_id)) orelse return error.InvalidPatchDraft; │ │171 + if (fork_record.removed or │ │172 + !std.mem.eql(u8, fork_record.event.user_id, &input.user_id) or │ │173 + !std.mem.eql(u8, fork_record.event.repo_id, &input.repo_id)) return error.InvalidPatchDraft; │ │174 + const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &input.user_id)) orelse return error.InvalidPatchDraft; │ │175 + if (user.removed or !std.mem.eql(u8, user.event.name, input.author.name) or !std.mem.eql(u8, user.event.email, input.author.email)) return error.InvalidPatchDraft;│ │176 + const target_name = fork_record.event.target; │ │177 + const recorded_source_oid = fork_record.event.source_oid orelse return error.PatchNotPushed; │ │178 + │ │179 const moment_maybe = evt.currentMoment(repo_opts, target_repo) catch |err| switch (err) { │ │180 error.NotFound => null, │ │181 else => |other| return other, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │187 │ │188 var stage = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = draft_path }); │ │189 defer stage.deinit(io, allocator); │ │143 - const stored_author_maybe = try draftAuthor(repo_opts, &stage, io, allocator); │ │144 - defer if (stored_author_maybe) |value| allocator.free(value); │ │145 - const stored_author = stored_author_maybe orelse return error.InvalidPatchDraft; │ │146 - if (!std.mem.eql(u8, stored_author, input.author.name)) return error.InvalidPatchDraft; │ │147 - const target_name = (try draftTarget(repo_opts, &stage, io, allocator)) orelse return error.PatchNotPushed; │ │148 - defer allocator.free(target_name); │ │149 - if (!xit.ref.validateName(target_name)) return error.InvalidPatchDraft; │ │190 const source_oid = (try stage.readRef(io, ref)) orelse return error.PatchNotPushed; │ │191 + if (!std.mem.eql(u8, recorded_source_oid, &source_oid)) return error.InvalidPatchDraft; │ │192 │ │193 const target_oid = (try target_repo.readRef(io, .{ .kind = .head, .name = target_name })) orelse return error.TargetNotFound;│ │194 var stage_moment = try stage.core.latestMoment(); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │258 } else { │ │259 try evt.consume(.xit, repo_opts, io, allocator, target_repo, evt.events_ref, &.{patch_event}); │ │260 } │ │261 +} │ │262 + │ │263 +// delete first so a failed tombstone can be retried safely │ │264 +pub fn remove( │ │265 + io: std.Io, │ │266 + allocator: std.mem.Allocator, │ │267 + repo_root_path: []const u8, │ │268 + admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), │ │269 + id: *const [evt.event_id_size * 2]u8, │ │270 + user_id: *const [evt.event_id_size]u8, │ │271 + author: evt.CommitAuthor, │ │272 +) !void { │ │273 + const fork_id = try evt.parseEventId(id); │ │274 + var arena = std.heap.ArenaAllocator.init(allocator); │ │275 + defer arena.deinit(); │ │276 + const moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); │ │277 + const record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, &fork_id)) orelse return error.InvalidPatchDraft;│ │278 + if (!std.mem.eql(u8, record.event.user_id, user_id)) return error.InvalidPatchDraft; │ │279 + │ │280 + const draft_path = try forkPath(allocator, repo_root_path, id); │ │281 + defer allocator.free(draft_path); │ │282 + try std.Io.Dir.cwd().deleteTree(io, draft_path); │ │283 + if (!record.removed) try evt.remove(.xit, evt.admin_repo_opts, io, allocator, admin_repo, &fork_id, .fork, author); │ │284 } │ │285 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/lib.zig│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────╮ │2 pub const command = @import("./command.zig"); │ │3 pub const main = @import("./main.zig"); │ │4 pub const event = @import("./event.zig"); │ │5 +pub const fork = @import("./fork.zig"); │ │6 pub const serve = @import("./serve.zig"); │ │7 pub const serve_ssh = @import("./serve_ssh.zig"); │ │8 pub const serve_ssh_protocol = @import("./serve_ssh_protocol.zig");│ ╰──────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯