╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Issue.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯
141 const issue_id_set = try DB.SortedSet(.read_write).init(issue_id_set_cursor); 142 try issue_id_set.put(&order_key); 143 } 144 145 // the search index is keyed by the same order key as the sets above 146 try srch_thrd.update(DB, hash_kind, .issue, haxy_moment, arena.child_allocator, &order_key, existing_record_maybe, record_to_write); 147 148 if (!record_to_write.removed) { 149 const status_set = try statusSet(DB, status_to_issues, record_to_write.event.status); 150 try status_set.put(&order_key); 151 152 var label_iter = labelIterator(record_to_write.event.labels); 153 while (label_iter.next()) |label| { 154 // an over-long label goes unindexed rather than failing the consume, 155 // which would wedge the branch it arrived on 156 if (label.len == 0 or label.len > label_max_len) continue; 157 var key_buffer: LabelStatusKey = undefined; 158 const label_set_cursor = try label_to_issues.putCursor(try labelStatusKey(&key_buffer, label, record_to_write.event.status)); 159 const label_set = try DB.SortedSet(.read_write).init(label_set_cursor); 160 try label_set.put(&order_key); 161 } 162 } 163 164 // the assignee sets hold the issue while it exists, whatever its status 165 for (try evt.Assignment.load(hash_kind, arena, haxy_moment.readOnly(), event_id)) |email| { 166 try evt.Assignment.indexIssue(DB, hash_kind, haxy_moment, email, &order_key, !record_to_write.removed); 167 } 168 } 169 170 // `status`'s sorted set within `statuses`, keyed by status name 171 fn statusSet( 172 comptime DB: type, 173 statuses: DB.SortedMap(.read_write), 174 status: Status, 175 ) !DB.SortedSet(.read_write) { 176 const cursor = try statuses.putCursor(@tagName(status)); 177 return DB.SortedSet(.read_write).init(cursor); 178 } 179 180 // the resolve form's inputs: null fields keep the live values, `hunks` holds 181 // the description conflict hunks' resolutions in chunk order (absent 182 // trailing ones keep our side) 183 pub const Resolve = struct { 184 title: ?[]const u8 = null, 185 labels: ?[]const u8 = null, 186 hunks: []const []const u8 = &.{}, 187 }; 188 189 // what an update replaces on an issue: its status, its content, or — for a 190 // conflicted issue — the resolution of its conflict 191 pub const Update = union(enum) { 192 status: Status, 193 fields: struct { title: []const u8, labels: []const u8, description: []const u8 }, 194 resolve: Resolve, 195 }; 196 197 // re-emit the issue `id_bytes` names with `change` applied, preserving the 198 // rest of its current state. error.NotFound when the id names no issue, 199 // error.InvalidFields when a resolve composes invalid fields. `repo` must be 200 // writable. 201 pub fn update( 202 host: evt.Host, 203 comptime repo_kind: rp.RepoKind, 204 comptime repo_opts: rp.RepoOpts(repo_kind), 205 io: std.Io, 206 allocator: std.mem.Allocator, 207 repo: *rp.Repo(repo_kind, repo_opts), 208 id_bytes: *const [evt.event_id_size]u8, 209 change: Update, 210 author: evt.CommitAuthor, 211 ) !void { 212 var arena = std.heap.ArenaAllocator.init(allocator); 213 defer arena.deinit(); 214 215 const issue = (try readById(repo_kind, repo_opts, io, allocator, &arena, repo, id_bytes)) orelse return error.NotFound; 216 if (issue.removed) return error.NotFound; 217 218 var updated = issue.event; 219 switch (change) { 220 .status => |status| { 221 if (updated.status == status) return; 222 updated.status = status; 223 }, 224 .fields => |fields| { 225 updated.title = fields.title; 226 updated.labels = fields.labels; 227 updated.description = fields.description; 228 }, 229 .resolve => |resolve| { 230 updated = try resolveFields(repo_kind, repo_opts, io, allocator, &arena, repo, id_bytes, issue, resolve); 231 if (!fieldsValid(updated.title, updated.labels)) return error.InvalidFields; 232 }, 233 } 234 235 try evt.consume(host, .repo, repo_kind, repo_opts, io, allocator, repo, evt.events_ref, &[_]evt.EventWithId{.{ 236 .id = std.fmt.bytesToHex(id_bytes.*, .lower), 237 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), 238 .author = author, 239 .event = .{ .issue = updated }, 240 }}); 241 } 242 243 // the live fields with `resolve` applied: submitted title/labels stand in for the 244 // live ones, and a conflicted description is reassembled from the hunk 245 // resolutions against the conflict entry. the chunking is deterministic: the 246 // entry holds immutable slots and any event since would have removed it, so 247 // the hunks match what the resolve form was built from. a settled or 248 // unreadable entry keeps the live description. 249 fn resolveFields( 250 comptime repo_kind: rp.RepoKind, 251 comptime repo_opts: rp.RepoOpts(repo_kind), 252 io: std.Io, 253 allocator: std.mem.Allocator, 254 arena: *std.heap.ArenaAllocator, 255 repo: *rp.Repo(repo_kind, repo_opts), 256 id_bytes: *const [evt.event_id_size]u8, 257 live: Record, 258 resolve: Resolve, 259 ) !Self { 260 const DB = evt.EventDB(repo_opts.hash); 261 const aa = arena.allocator(); 262 263 var updated = live.event; 264 if (resolve.title) |title| updated.title = title; 265 if (resolve.labels) |labels| updated.labels = labels; 266 267 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; 268 defer if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); 269 const moment = (if (event_db_maybe) |*event_db| 270 evt.currentMomentFromDb(repo_opts.hash, event_db.db) 271 else if (repo_kind == .git) 272 return updated 273 else 274 evt.currentMoment(repo_opts, repo)) catch return updated; 275 276 const conflicts_cursor = (try moment.getCursor(hash.hashInt(repo_opts.hash, conflicts_key))) orelse return updated; 277 const conflicts = try DB.SortedMap(.read_only).init(conflicts_cursor); 278 const order_key = evt.orderKeyDesc(live.created_order, id_bytes); 279 const conflict_cursor = (try conflicts.getCursor(&order_key)) orelse return updated; 280 const entry = try DB.HashMap(.read_only).init(conflict_cursor); 281 282 const fields_cursor = (try entry.getCursor(hash.hashInt(repo_opts.hash, evt.conflicted_fields_key))) orelse return updated; 283 const conflicted_fields = try fields_cursor.readBytesAlloc(aa, null); 284 if (std.mem.indexOf(u8, conflicted_fields, "description") == null) return updated; 285 286 const their_cursor = (try entry.getCursor(hash.hashInt(repo_opts.hash, evt.their_record_key))) orelse return updated; 287 const theirs = try evt.read(Record, DB, repo_opts.hash, arena, try DB.HashMap(.read_only).init(their_cursor)); 288 289 // absent when both sides created the issue independently 290 var base_description: []const u8 = ""; 291 if (try entry.getCursor(hash.hashInt(repo_opts.hash, evt.base_record_key))) |base_cursor| { 292 const base = try evt.read(Record, DB, repo_opts.hash, arena, try DB.HashMap(.read_only).init(base_cursor)); 293 base_description = base.event.description; 294 } 295 296 const chunks = try diff3.chunks(allocator, arena, base_description, live.event.description, theirs.event.description); 297 var resolutions: std.ArrayList([]const u8) = .empty; 298 var hunk_index: usize = 0; 299 for (chunks) |chunk| { 300 if (chunk != .conflict) continue; 301 try resolutions.append(aa, if (hunk_index < resolve.hunks.len) resolve.hunks[hunk_index] else (chunk.conflict.ours orelse "")); 302 hunk_index += 1; 303 } 304 updated.description = try diff3.assemble(aa, chunks, resolutions.items); 305 return updated; 306 } 307 308 // the issue `id_bytes` names in the repo's consumed event db (the repo's own 309 // db for xit; the side db next to a git repo), or null 310 fn readById( 311 comptime repo_kind: rp.RepoKind, 312 comptime repo_opts: rp.RepoOpts(repo_kind), 313 io: std.Io, 314 allocator: std.mem.Allocator, 315 arena: *std.heap.ArenaAllocator, 316 repo: *rp.Repo(repo_kind, repo_opts), 317 id_bytes: *const [evt.event_id_size]u8, 318 ) !?Record { 319 const DB = evt.EventDB(repo_opts.hash); 320 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; 321 defer if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); 322 const moment = (if (event_db_maybe) |*event_db| 323 evt.currentMomentFromDb(repo_opts.hash, event_db.db) 324 else if (repo_kind == .git) 325 return null 326 else 327 evt.currentMoment(repo_opts, repo)) catch return null; 328 const event_id_to_issue_cursor = (try moment.getCursor(hash.hashInt(repo_opts.hash, record_map_key))) orelse return null; 329 const event_id_to_issue = try DB.HashMap(.read_only).init(event_id_to_issue_cursor); 330 const issue_cursor = (try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, id_bytes))) orelse return null; 331 const issue_map = try DB.HashMap(.read_only).init(issue_cursor); 332 return try evt.read(Record, DB, repo_opts.hash, arena, issue_map); 333 } 334 335 // remove an issue's order key from its labels' `status` sets, pruning entries 336 // whose set becomes empty 337 fn removeFromLabelSets( 338 comptime DB: type, 339 label_to_issues: DB.SortedMap(.read_write), 340 labels: []const u8, 341 status: Status, 342 order_key: []const u8, 343 ) !void { 344 var label_iter = labelIterator(labels); 345 while (label_iter.next()) |label| { 346 // skipped the same way consume skipped it, so it was never indexed 347 if (label.len == 0 or label.len > label_max_len) continue; 348 var key_buffer: LabelStatusKey = undefined; 349 const key = try labelStatusKey(&key_buffer, label, status); 350 const label_set_cursor = try label_to_issues.putCursor(key); 351 const label_set = try DB.SortedSet(.read_write).init(label_set_cursor); 352 _ = try label_set.remove(order_key); 353 if (0 == try label_set.count()) _ = try label_to_issues.remove(key); 354 } 355 } 356