╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────────────╮ │src/event/Comment.zig│ ╰─────────────────────╯ ╭─────────────────────────────────────────────────────╮ │144 io: std.Io, │ │145 allocator: std.mem.Allocator, │ │146 repo: *rp.Repo(repo_kind, repo_opts), │ │147 + thread_kind: evt.EventKind, │ │148 thread_id: *const [evt.event_id_size * 2]u8,│ │149 comment_id: *const [evt.event_id_size]u8, │ │150 body: []const u8, │ ╰─────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │151 author: evt.CommitAuthor, │ │152 ) !void { │ │153 if (!fieldsValid(body)) return error.InvalidFields; │ │154 + │ │155 + const thread_id_bytes = evt.parseEventId(thread_id) catch return error.NotFound; │ │156 + if (!try evt.threadHolds(repo_kind, repo_opts, io, allocator, repo, thread_kind, &thread_id_bytes, .{ .comment = comment_id })) return error.NotFound;│ │157 │ │158 var arena = std.heap.ArenaAllocator.init(allocator); │ │159 defer arena.deinit(); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │170 evt.currentMoment(repo_opts, repo)) catch return error.NotFound; │ │171 break :blk (try readById(DB, repo_opts.hash, moment, &arena, comment_id)) orelse return error.NotFound;│ │172 }; │ │162 - if (!std.mem.eql(u8, &comment.event.thread_id, thread_id)) return error.NotFound; │ │173 if (comment.removed) return error.NotFound; │ │174 │ │175 var updated = comment.event; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/event/Repo.zig│ ╰──────────────────╯ ╭──────────────────────────────────────────────────────────────────────────╮ │ 8 description: []const u8, │ │ 9 read_access: Access = .private, │ │10 write_access: Access = .private, │ │11 +// the least role that may write in each tab, null turns the tab off │ │12 +discuss_role: ?Role = .read, │ │13 +issue_role: ?Role = .read, │ │14 +patch_role: ?Role = null, │ │15 │ │16 // what the db stores: the event's data plus the commit-derived fields│ │17 pub const Record = struct { │ ╰──────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │50 │ │51 pub const name_max_len = 32; │ │52 │ │53 +// the role a kind's tab needs for a write, or null when the tab is off or the │ │54 +// kind has no tab │ │55 +pub fn threadRole(self: Self, kind: evt.EventKind) ?Role { │ │56 + return switch (kind) { │ │57 + .discuss => self.discuss_role, │ │58 + .issue => self.issue_role, │ │59 + .patch => self.patch_role, │ │60 + else => null, │ │61 + }; │ │62 +} │ │63 + │ │64 // the role `user_id_maybe` holds in the repo, or null when they hold none: its│ │65 // creator owns it, and anyone else gets the higher of the base role and their │ │66 // grant. `user_moment` is the owner's, which holds the grants. a null user is │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │487 if (service == .receive_pack and !isKeyInAuthorizedKeys(user.event.ssh_keys, &sess.fingerprint)) │ │488 return writeError(sess, "unauthorized: this SSH key is not registered to the patch author"); │ │489 │ │490 + const target = (try evt.readRepoById(io, allocator, &admin_arena, handler.users_dir, fork_record.event.repo_user_id[0..evt.event_id_size], fork_record.event.repo_id[0..evt.event_id_size], forker_id)) orelse│ │491 + return writeError(sess, "repo not found"); │ │492 + // patches that are off take their drafts with them │ │493 + const patch_role = target.repo.event.patch_role orelse return writeError(sess, "patch draft not found"); │ │494 + if (service == .receive_pack) { │ │495 + const role = target.role orelse return writeError(sess, "repo not found"); │ │496 + if (!role.atLeast(patch_role)) return writeError(sess, "unauthorized: your role in this repo may not write patches"); │ │497 + } │ │498 + │ │499 const draft_path = try fork.forkPath(allocator, handler.users_dir, &forker_id, &fork_id); │ │500 defer allocator.free(draft_path); │ │501 var draft = rp.AnyRepo(.xit, any_repo_opts).open(io, allocator, .{ .path = draft_path }) catch │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │507 inline else => |*repo| try repo.uploadPack(io, allocator, reader, writer, .{ .protocol_version = protocol_version }, &sideband), │ │508 } │ │509 } else { │ │501 - const target = (try evt.readRepoById(io, allocator, &admin_arena, handler.users_dir, fork_record.event.repo_user_id[0..evt.event_id_size], fork_record.event.repo_id[0..evt.event_id_size], null)) orelse│ │502 - return writeError(sess, "repo not found"); │ │510 if (!std.mem.eql(u8, target.repo.event.name, forker_repo.name)) return writeError(sess, "patch draft belongs to another repo"); │ │511 const target_path = try evt.repoPath(allocator, handler.users_dir, fork_record.event.repo_user_id, fork_record.event.repo_id); │ │512 defer allocator.free(target_path); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/test/event/patch.zig│ ╰────────────────────────╯ ╭─────────────────────────────────────────╮ │856 .user_id = &user_id,│ │857 .name = "repo", │ │858 .description = "", │ │859 + .patch_role = .read,│ │860 } }, │ │861 }, │ │862 }); │ ╰─────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1233 } }; │ │1234 try evt.Patch.update(.local, .xit, repo_opts, io, allocator, target, id, edit, author); │ │1235 try std.testing.expectEqualDeep(pch.Mergeability{}, try readMergeability(target, io, allocator, id, patch)); │ │1235 - _ = try evt.Comment.create(.{ .server = .{ .users_dir = users_dir } }, .xit, repo_opts, io, allocator, target, &input.id, &input.id, "reviewing the answer", author); │ │1236 + _ = try evt.Comment.create(.{ .server = .{ .users_dir = users_dir } }, .xit, repo_opts, io, allocator, target, .patch, &input.id, &input.id, "reviewing the answer", author);│ │1237 try std.testing.expectEqualDeep(pch.Mergeability{}, try readMergeability(target, io, allocator, id, patch)); │ │1238 │ │1239 // a server patch edit refreshes its mergeability │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────╮ │src/test/net.zig│ ╰────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │546 const target = (try evt.readRepoByOwnerAndName(io, allocator, &arena, moment, users_dir, "admin", "target", null)) orelse return error.NotFound; │ │547 repo_id = target.event_id; │ │548 @memcpy(&user_id, target.repo.event.user_id); │ │549 + // patches are off on a new repo │ │550 + try evt.updateRepo(io, allocator, users_dir, &user_id, .{ .name = "admin", .email = "admin@example.test" }, "target", "target", target.repo.event.description, target.repo.event.read_access, .read, .read, .read);│ │551 var user_repo = (try evt.openUserRepo(io, allocator, users_dir, &user_id)) orelse return error.NotFound; │ │552 defer user_repo.deinit(io, allocator); │ │553 break :blk try fork.create(.{}, io, allocator, users_dir, .{ │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯