╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2188 .author = author, │ │2189 .event = .{ .repo = event }, │ │2190 }}); │ │2191 +} │ │2192 + │ │2193 +// remove the repo `repo_id` from its owner's user repo, then its dir. tombstone │ │2194 +// first so a missing repo never remains visible │ │2195 +pub fn deleteRepo( │ │2196 + io: std.Io, │ │2197 + allocator: std.mem.Allocator, │ │2198 + users_dir: []const u8, │ │2199 + owner_id: *const [event_id_size]u8, │ │2200 + repo_id: *const [event_id_size]u8, │ │2201 + author: CommitAuthor, │ │2202 +) !void { │ │2203 + { │ │2204 + var user_repo = (try openUserRepo(io, allocator, users_dir, owner_id)) orelse return error.NotFound; │ │2205 + defer user_repo.deinit(io, allocator); │ │2206 + try remove(.{ .server = .{ .users_dir = users_dir } }, .user, .xit, user_repo_opts, io, allocator, &user_repo, repo_id, .repo, author);│ │2207 + } │ │2208 + const path = try repoPath(allocator, users_dir, owner_id, repo_id); │ │2209 + defer allocator.free(path); │ │2210 + try std.Io.Dir.cwd().deleteTree(io, path); │ │2211 } │ │2212 │ │2213 // grant `user_id` `role` on the repo `repo_id` in its owner's user repo, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2391 .author = .{ .name = event.name, .email = email }, │ │2392 .event = .{ .user = event }, │ │2393 }}); │ │2394 +} │ │2395 + │ │2396 +// remove a user from the admin repo, then their dir with every repo and fork │ │2397 +// in it. tombstone first so a missing user never remains visible │ │2398 +pub fn deleteUser( │ │2399 + io: std.Io, │ │2400 + allocator: std.mem.Allocator, │ │2401 + users_dir: []const u8, │ │2402 + admin_repo: *rp.Repo(.xit, admin_repo_opts), │ │2403 + user_id: *const [event_id_size]u8, │ │2404 + author: CommitAuthor, │ │2405 +) !void { │ │2406 + try remove(.{ .server = .{ .users_dir = users_dir } }, .admin, .xit, admin_repo_opts, io, allocator, admin_repo, user_id, .user, author);│ │2407 + const path = try std.Io.Dir.path.join(allocator, &.{ users_dir, &std.fmt.bytesToHex(user_id.*, .lower) }); │ │2408 + defer allocator.free(path); │ │2409 + try std.Io.Dir.cwd().deleteTree(io, path); │ │2410 } │ │2411 │ │2412 // add an authorized_keys line to the user's ssh keys │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/event/Fork.zig│ ╰──────────────────╯ ╭──────────────────────────────────────────────────────────────────────────╮ │ 6 repo_id: [evt.event_id_size * 2]u8, │ │ 7 // the target repo's owner, whose user repo holds the target │ │ 8 repo_user_id: [evt.event_id_size * 2]u8, │ │ 9 +// the target repo's name when forked, which outlives a deleted target│ │10 +repo_name: []const u8, │ │11 stage: Stage = .draft, │ │12 │ │13 // what the db stores: the event's data plus the commit-derived fields│ ╰──────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────╮ │34 fn validate(record: Record) !void { │ │35 _ = try evt.parseEventId(&record.event.repo_id); │ │36 _ = try evt.parseEventId(&record.event.repo_user_id);│ │37 + try evt.Repo.validateName(record.event.repo_name); │ │38 } │ │39 │ │40 pub fn consume( │ ╰─────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/event/Repo.zig│ ╰──────────────────╯ ╭───────────────────────────────────────────────────────╮ │204 return .{ .repo = repo, .event_id = repo_id };│ │205 } │ │206 │ │207 +// a deleted repo reads as missing │ │208 pub fn readById( │ │209 comptime DB: type, │ │210 comptime hash_kind: hash.HashKind, │ ╰───────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │215 const records_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, record_map_key)) orelse return null;│ │216 const records = try DB.HashMap(.read_only).init(records_cursor); │ │217 const record_cursor = try records.getCursor(hash.hashInt(hash_kind, id)) orelse return null; │ │217 - return try evt.read(Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(record_cursor)); │ │218 + const record = try evt.read(Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(record_cursor)); │ │219 + return if (record.removed) null else record; │ │220 } │ │221 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───╮ │src/fork.zig│ ╰────────────╯ ╭────────────────────────────────────────────╮ │53 user_id: [evt.event_id_size]u8, │ │54 repo_id: [evt.event_id_size]u8, │ │55 repo_user_id: [evt.event_id_size]u8,│ │56 + repo_name: []const u8, │ │57 title: []const u8, │ │58 description: []const u8, │ │59 labels: []const u8, │ ╰────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │242 .event = .{ .fork = .{ │ │243 .repo_id = std.fmt.bytesToHex(input.repo_id, .lower), │ │244 .repo_user_id = std.fmt.bytesToHex(input.repo_user_id, .lower),│ │245 + .repo_name = input.repo_name, │ │246 } }, │ │247 }}); │ │248 } │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │489 const fork_record = (try evt.readForkById(io, allocator, &admin_arena, handler.server.users_dir, &forker_id, &fork_id)) orelse│ │490 return writeError(sess, "patch draft not found"); │ │491 if (fork_record.removed) return writeError(sess, "patch draft not found"); │ │492 + // the url keeps the name the fork was made under, even after a rename │ │493 + if (!std.mem.eql(u8, fork_record.event.repo_name, forker_repo.name)) return writeError(sess, "patch draft not found"); │ │494 const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &admin_arena, &forker_id)) orelse │ │495 return writeError(sess, "invalid patch draft"); │ │496 if (user.removed) return writeError(sess, "invalid patch draft"); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │499 │ │500 const target_owner_id = try evt.parseEventId(&fork_record.event.repo_user_id); │ │501 const target_id = try evt.parseEventId(&fork_record.event.repo_id); │ │500 - const target = (try evt.readRepoById(io, allocator, &admin_arena, handler.server.users_dir, &target_owner_id, &target_id, forker_id)) orelse│ │501 - return writeError(sess, "repo not found"); │ │502 - // patches that are off take their drafts with them │ │503 - const patch_role = target.repo.event.patch_role orelse return writeError(sess, "patch draft not found"); │ │504 - if (service == .receive_pack) { │ │505 - const role = target.role orelse return writeError(sess, "repo not found"); │ │506 - if (!role.atLeast(patch_role)) return writeError(sess, "unauthorized: your role in this repo may not write patches"); │ │502 + // a deleted target, or one whose owner was deleted, leaves the fork fetchable but not pushable │ │503 + const target = try evt.readRepoById(io, allocator, &admin_arena, handler.server.users_dir, &target_owner_id, &target_id, forker_id); │ │504 + if (target) |value| { │ │505 + // patches that are off take their drafts with them │ │506 + const patch_role = value.repo.event.patch_role orelse return writeError(sess, "patch draft not found"); │ │507 + if (service == .receive_pack) { │ │508 + const role = value.role orelse return writeError(sess, "repo not found"); │ │509 + if (!role.atLeast(patch_role)) return writeError(sess, "unauthorized: your role in this repo may not write patches"); │ │510 + } │ │511 } │ │512 │ │513 const draft_path = try fork.forkPath(allocator, handler.server.users_dir, &forker_id, &fork_id); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯