╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Comment.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
51 52 const parent_id_to_comment_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, parent_id_to_comment_id_set_key)); 53 const parent_id_to_comment_id_set = try DB.HashMap(.read_write).init(parent_id_to_comment_id_set_cursor); 54 55 var existing_record_maybe: ?Record = null; 56 const existing_cursor_maybe = try event_id_to_comment.getCursor(comment_key); 57 if (existing_cursor_maybe) |existing_cursor| { 58 const existing_comment = try DB.HashMap(.read_only).init(existing_cursor); 59 existing_record_maybe = try evt.read(Record, DB, hash_kind, arena, existing_comment); 60 } 61 62 var record_to_write = record_maybe orelse try evt.removedRecord(Record, DB, hash_kind, haxy_moment.readOnly(), existing_record_maybe); 63 64 const thread_id = try evt.parseEventId(&record_to_write.event.thread_id); 65 _ = try evt.parseEventId(&record_to_write.event.parent_id); 66 67 if (existing_record_maybe) |existing_record| { 68 // updates preserve the original creation order and author 69 record_to_write.created_order = existing_record.created_order; 70 record_to_write.author_email = existing_record.author_email; 71 72 // a comment cannot move between threads or positions in the thread 73 if (!std.mem.eql(u8, &existing_record.event.thread_id, &record_to_write.event.thread_id)) return error.ThreadChanged; 74 if (!std.mem.eql(u8, &existing_record.event.parent_id, &record_to_write.event.parent_id)) return error.ParentChanged; 75 } 76 77 const comment_cursor = try event_id_to_comment.putCursor(comment_key); 78 const comment = try DB.HashMap(.read_write).init(comment_cursor); 79 try evt.upsert(Record, DB, hash_kind, comment, record_to_write); 80 try evt.indexEvent(DB, hash_kind, haxy_moment, event_id, .comment, existing_record_maybe, record_to_write); 81 82 if (existing_cursor_maybe == null) { 83 const comment_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, all_id_set_key)); 84 const comment_id_set = try DB.SortedSet(.read_write).init(comment_id_set_cursor); 85 try comment_id_set.put(&evt.orderKeyDesc(record_to_write.created_order, event_id)); 86 try evt.touchThread(DB, hash_kind, haxy_moment, &thread_id, record_to_write.created_order, arena); 87 } 88 89 // removed comments stay in the thread so their replies remain connected 90 const thread_order_key = evt.orderKey(record_to_write.created_order, event_id); 91 const thread_comments_cursor = try thread_id_to_comment_id_set.putCursor(hash.hashInt(hash_kind, &record_to_write.event.thread_id)); 92 const thread_comments = try DB.SortedSet(.read_write).init(thread_comments_cursor); 93 try thread_comments.put(&thread_order_key); 94 95 const replies_cursor = try parent_id_to_comment_id_set.putCursor(hash.hashInt(hash_kind, &record_to_write.event.parent_id)); 96 const replies = try DB.SortedSet(.read_write).init(replies_cursor); 97 try replies.put(&thread_order_key); 98 } 99 100 // create a comment and return its event id. `repo` must be writable. 101 pub fn create( 102 host: evt.Host, 103 comptime repo_kind: rp.RepoKind, 104 comptime repo_opts: rp.RepoOpts(repo_kind), 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); 124 const event_id = std.fmt.bytesToHex(id_bytes, .lower); 125 try evt.consume(host, .repo, repo_kind, repo_opts, io, allocator, repo, evt.events_ref, &.{.{ 126 .id = event_id, 127 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), 128 .author = author, 129 .event = .{ .comment = .{ 130 .thread_id = thread_id.*, 131 .parent_id = parent_id.*, 132 .body = body, 133 } }, 134 }}); 135 return event_id; 136 } 137 138 // replace a comment's body while preserving its thread and parent. `repo` must 139 // be writable. 140 pub fn update( 141 host: evt.Host, 142 comptime repo_kind: rp.RepoKind, 143 comptime repo_opts: rp.RepoOpts(repo_kind), 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(); 160 161 const comment = blk: { 162 const DB = evt.EventDB(repo_opts.hash); 163 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; 164 defer if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); 165 const moment = (if (event_db_maybe) |*event_db| 166 evt.currentMomentFromDb(repo_opts.hash, event_db.db) 167 else if (repo_kind == .git) 168 return error.NotFound 169 else 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 }; 173 if (comment.removed) return error.NotFound; 174 175 var updated = comment.event; 176 updated.body = body; 177 try evt.consume(host, .repo, repo_kind, repo_opts, io, allocator, repo, evt.events_ref, &.{.{ 178 .id = std.fmt.bytesToHex(comment_id.*, .lower), 179 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), 180 .author = author, 181 .event = .{ .comment = updated }, 182 }}); 183 } 184 185 // read a comment by event id, or null if the id isn't a known comment. field 186 // byte slices are allocated in `arena`. 187 pub fn readById( 188 comptime DB: type, 189 comptime hash_kind: hash.HashKind, 190 haxy_moment: DB.HashMap(.read_only), 191 arena: *std.heap.ArenaAllocator, 192 comment_id: []const u8, 193 ) !?Record { 194 const records_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, record_map_key)) orelse return null; 195 const records = try DB.HashMap(.read_only).init(records_cursor); 196 const record_cursor = try records.getCursor(hash.hashInt(hash_kind, comment_id)) orelse return null; 197 const record_map = try DB.HashMap(.read_only).init(record_cursor); 198 return try evt.read(Record, DB, hash_kind, arena, record_map); 199 } 200