╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1651 } │ │1652 } │ │1653 } │ │1654 + } │ │1655 +} │ │1656 + │ │1657 +// a comment or attachment a write names under a thread │ │1658 +pub const ThreadChild = union(enum) { │ │1659 + comment: *const [event_id_size]u8, │ │1660 + attach: *const [event_id_size]u8, │ │1661 +}; │ │1662 + │ │1663 +// whether `thread_id` names a thread of `thread_kind` in the repo's events, and │ │1664 +// `child`, when given, belongs to it. a caller's url can claim any kind for any │ │1665 +// id, so every write under a thread checks its claim here first. │ │1666 +pub fn threadHolds( │ │1667 + comptime repo_kind: rp.RepoKind, │ │1668 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │1669 + io: std.Io, │ │1670 + allocator: std.mem.Allocator, │ │1671 + repo: *rp.Repo(repo_kind, repo_opts), │ │1672 + thread_kind: EventKind, │ │1673 + thread_id: *const [event_id_size]u8, │ │1674 + child_maybe: ?ThreadChild, │ │1675 +) !bool { │ │1676 + const DB = EventDB(repo_opts.hash); │ │1677 + var event_db_maybe: ?LocalEventDB(repo_opts.hash) = if (repo_kind == .git) try LocalEventDB(repo_opts.hash).openReadOnly(io, allocator, repo.core.repo_dir) else null; │ │1678 + defer if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); │ │1679 + const moment = (if (event_db_maybe) |*event_db| │ │1680 + currentMomentFromDb(repo_opts.hash, event_db.db) │ │1681 + else if (repo_kind == .git) │ │1682 + return false │ │1683 + else │ │1684 + currentMoment(repo_opts, repo)) catch return false; │ │1685 + │ │1686 + // only these kinds are threads │ │1687 + switch (thread_kind) { │ │1688 + .discuss, .issue, .patch => {}, │ │1689 + else => return false, │ │1690 + } │ │1691 + const kinds_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, event_index_key)) orelse return false; │ │1692 + const kinds = try DB.HashMap(.read_only).init(kinds_cursor); │ │1693 + if ((try readEventKind(repo_opts.hash, kinds, thread_id)) != thread_kind) return false; │ │1694 + │ │1695 + const child = child_maybe orelse return true; │ │1696 + var arena = std.heap.ArenaAllocator.init(allocator); │ │1697 + defer arena.deinit(); │ │1698 + const thread_id_hex = std.fmt.bytesToHex(thread_id.*, .lower); │ │1699 + switch (child) { │ │1700 + .comment => |id| { │ │1701 + const comment = (try readRecordSubset(Comment, struct { event: struct { thread_id: [event_id_size * 2]u8 } }, DB, repo_opts.hash, moment, &arena, id)) orelse return false; │ │1702 + return std.mem.eql(u8, &comment.event.thread_id, &thread_id_hex); │ │1703 + }, │ │1704 + .attach => |id| { │ │1705 + const attachment = (try readRecordSubset(Attachment, struct { event: struct { parent_id: [event_id_size * 2]u8 } }, DB, repo_opts.hash, moment, &arena, id)) orelse return false;│ │1706 + return std.mem.eql(u8, &attachment.event.parent_id, &thread_id_hex); │ │1707 + }, │ │1708 } │ │1709 } │ │1710 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1711 +// remove an event a thread's page names: the thread itself, or a comment or │ │1712 +// attachment, which must belong to it │ │1713 +pub fn removeInThread( │ │1714 + host: Host, │ │1715 + comptime repo_kind: rp.RepoKind, │ │1716 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │1717 + io: std.Io, │ │1718 + allocator: std.mem.Allocator, │ │1719 + repo: *rp.Repo(repo_kind, repo_opts), │ │1720 + thread_kind: EventKind, │ │1721 + thread_id: *const [event_id_size]u8, │ │1722 + kind: EventKind, │ │1723 + id: *const [event_id_size]u8, │ │1724 + author: CommitAuthor, │ │1725 +) !void { │ │1726 + const child_maybe: ?ThreadChild = switch (kind) { │ │1727 + .comment => .{ .comment = id }, │ │1728 + .attach => .{ .attach = id }, │ │1729 + else => null, │ │1730 + }; │ │1731 + if (child_maybe) |child| { │ │1732 + if (!try threadHolds(repo_kind, repo_opts, io, allocator, repo, thread_kind, thread_id, child)) return error.EventNotFound;│ │1733 + } │ │1734 + try remove(host, .repo, repo_kind, repo_opts, io, allocator, repo, id, kind, author); │ │1735 +} │ │1736 + │ │1737 pub fn readFromRepo( │ │1738 comptime T: type, │ │1739 comptime repo_kind: rp.RepoKind, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────╮ │1890 id: *const [event_id_size]u8, │ │1891 ) !?[]const u8 { │ │1892 const record_map_key = switch (kind) { │ │1813 - .issue => Issue.record_map_key, │ │1893 .discuss => Discussion.record_map_key,│ │1894 + .issue => Issue.record_map_key, │ │1895 .comment => Comment.record_map_key, │ │1896 .attach => Attachment.record_map_key, │ │1897 .patchrev => PatchRev.record_map_key, │ ╰────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │2032 return .{ .owner_id = owner_id.*, .repo_id = id_bytes }; │ │2033 } │ │2034 │ │1955 -// change the name, description, and read access of the repo named │ │1956 -// `old_name` in its owner's user repo │ │2035 +// change the name, description, read access, and tab roles of the repo│ │2036 +// named `old_name` in its owner's user repo │ │2037 pub fn updateRepo( │ │2038 io: std.Io, │ │2039 allocator: std.mem.Allocator, │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2044 name: []const u8, │ │2045 description: []const u8, │ │2046 read_access: Repo.Access, │ │2047 + discuss_role: ?Repo.Role, │ │2048 + issue_role: ?Repo.Role, │ │2049 + patch_role: ?Repo.Role, │ │2050 ) !void { │ │2051 try Repo.validateName(name); │ │2052 var user_repo = (try openUserRepo(io, allocator, users_dir, owner_id)) orelse return error.NotFound;│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2063 event.name = name; │ │2064 event.description = description; │ │2065 event.read_access = read_access; │ │2066 + event.discuss_role = discuss_role; │ │2067 + event.issue_role = issue_role; │ │2068 + event.patch_role = patch_role; │ │2069 try consume(.{ .server = .{ .users_dir = users_dir } }, .user, .xit, user_repo_opts, io, allocator, &user_repo, events_ref, &[_]EventWithId{.{│ │2070 .id = std.fmt.bytesToHex(found.event_id, .lower), │ │2071 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/event/Attachment.zig│ ╰────────────────────────╯ ╭─────────────────────────────────────────────────────╮ │121 io: std.Io, │ │122 allocator: std.mem.Allocator, │ │123 repo: *rp.Repo(repo_kind, repo_opts), │ │124 + parent_kind: evt.EventKind, │ │125 parent_id: *const [evt.event_id_size * 2]u8,│ │126 blob: evt.Blob, │ │127 author: evt.CommitAuthor, │ ╰─────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │130 if (blob.size == 0 or blob.size > max_size) return error.InvalidFields; │ │131 │ │132 const parent_id_bytes = evt.parseEventId(parent_id) catch return error.InvalidFields; │ │133 + if (!try evt.threadHolds(repo_kind, repo_opts, io, allocator, repo, parent_kind, &parent_id_bytes, null)) return error.ParentNotFound; │ │134 │ │133 - { │ │134 - const DB = evt.EventDB(repo_opts.hash); │ │135 - var event_db_maybe: ?evt.LocalEventDB(repo_opts.hash) = if (repo_kind == .git) try evt.LocalEventDB(repo_opts.hash).openReadOnly(io, allocator, repo.core.repo_dir) else null;│ │136 - defer if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); │ │137 - const haxy_moment = (if (event_db_maybe) |*event_db| │ │138 - evt.currentMomentFromDb(repo_opts.hash, event_db.db) │ │139 - else if (repo_kind == .git) │ │140 - return error.ParentNotFound │ │141 - else │ │142 - evt.currentMoment(repo_opts, repo)) catch return error.ParentNotFound; │ │143 - if (!try parentExists(DB, repo_opts.hash, haxy_moment, &parent_id_bytes)) return error.ParentNotFound; │ │144 - } │ │145 - │ │135 var id_bytes: [evt.event_id_size]u8 = undefined; │ │136 io.random(&id_bytes); │ │137 const event_id = std.fmt.bytesToHex(id_bytes, .lower); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────────╮ │src/event/Comment.zig│ ╰─────────────────────╯ ╭─────────────────────────────────────────────────────╮ │105 io: std.Io, │ │106 allocator: std.mem.Allocator, │ │107 repo: *rp.Repo(repo_kind, repo_opts), │ │108 + thread_kind: evt.EventKind, │ │109 thread_id: *const [evt.event_id_size * 2]u8,│ │110 parent_id: *const [evt.event_id_size * 2]u8,│ │111 body: []const u8, │ ╰─────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │112 author: evt.CommitAuthor, │ │113 ) ![evt.event_id_size * 2]u8 { │ │114 if (!fieldsValid(body)) return error.InvalidFields; │ │115 + │ │116 + // a reply's parent is the thread itself or one of its comments │ │117 + const thread_id_bytes = evt.parseEventId(thread_id) catch return error.NotFound; │ │118 + const parent_id_bytes = evt.parseEventId(parent_id) catch return error.NotFound; │ │119 + const parent: ?evt.ThreadChild = if (std.mem.eql(u8, thread_id, parent_id)) null else .{ .comment = &parent_id_bytes }; │ │120 + if (!try evt.threadHolds(repo_kind, repo_opts, io, allocator, repo, thread_kind, &thread_id_bytes, parent)) return error.NotFound;│ │121 │ │122 var id_bytes: [evt.event_id_size]u8 = undefined; │ │123 io.random(&id_bytes); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯