╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Issue.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
211 .status => |status| { 212 if (updated.status == status) return; 213 updated.status = status; 214 }, 215 .fields => |fields| { 216 updated.title = fields.title; 217 updated.tags = fields.tags; 218 updated.description = fields.description; 219 }, 220 .resolve => |resolve| { 221 updated = try resolveFields(repo_kind, repo_opts, io, allocator, &arena, repo, id_bytes, issue, resolve); 222 if (!fieldsValid(updated.title, updated.tags)) return error.InvalidFields; 223 }, 224 } 225 226 try evt.consume(repo_kind, repo_opts, io, allocator, repo, evt.events_ref, &[_]evt.EventWithId{.{ 227 .id = std.fmt.bytesToHex(id_bytes.*, .lower), 228 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), 229 .author = author, 230 .event = .{ .issue = updated }, 231 }}); 232 } 233 234 // the live fields with `resolve` applied: posted title/tags stand in for the 235 // live ones, and a conflicted description is reassembled from the hunk 236 // resolutions against the conflict entry. the chunking is deterministic: the 237 // entry holds immutable slots and any event since would have removed it, so 238 // the hunks match what the resolve form was built from. a settled or 239 // unreadable entry keeps the live description. 240 fn resolveFields( 241 comptime repo_kind: rp.RepoKind, 242 comptime repo_opts: rp.RepoOpts(repo_kind), 243 io: std.Io, 244 allocator: std.mem.Allocator, 245 arena: *std.heap.ArenaAllocator, 246 repo: *rp.Repo(repo_kind, repo_opts), 247 id_bytes: *const [evt.event_id_size]u8, 248 live: Record, 249 resolve: Resolve, 250 ) !Self { 251 const DB = evt.EventDB(repo_opts.hash); 252 const aa = arena.allocator(); 253 254 var updated = live.event; 255 if (resolve.title) |title| updated.title = title; 256 if (resolve.tags) |tags| updated.tags = tags; 257 258 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; 259 defer if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); 260 const moment = (if (event_db_maybe) |*event_db| 261 evt.currentMomentFromDb(repo_opts.hash, event_db.db) 262 else if (repo_kind == .git) 263 return updated 264 else 265 evt.currentMoment(repo_opts, repo)) catch return updated; 266 267 const conflicts_cursor = (try moment.getCursor(hash.hashInt(repo_opts.hash, conflicts_key))) orelse return updated; 268 const conflicts = try DB.SortedMap(.read_only).init(conflicts_cursor); 269 const order_key = evt.orderKeyDesc(live.created_order, id_bytes); 270 const conflict_cursor = (try conflicts.getCursor(&order_key)) orelse return updated; 271 const entry = try DB.HashMap(.read_only).init(conflict_cursor); 272 273 const fields_cursor = (try entry.getCursor(hash.hashInt(repo_opts.hash, evt.conflicted_fields_key))) orelse return updated; 274 const conflicted_fields = try fields_cursor.readBytesAlloc(aa, null); 275 if (std.mem.indexOf(u8, conflicted_fields, "description") == null) return updated; 276 277 const their_cursor = (try entry.getCursor(hash.hashInt(repo_opts.hash, evt.their_record_key))) orelse return updated; 278 const theirs = try evt.read(Record, DB, repo_opts.hash, arena, try DB.HashMap(.read_only).init(their_cursor)); 279 280 // absent when both sides created the issue independently 281 var base_description: []const u8 = ""; 282 if (try entry.getCursor(hash.hashInt(repo_opts.hash, evt.base_record_key))) |base_cursor| { 283 const base = try evt.read(Record, DB, repo_opts.hash, arena, try DB.HashMap(.read_only).init(base_cursor)); 284 base_description = base.event.description; 285 } 286 287 const chunks = try diff3.chunks(io, allocator, arena, base_description, live.event.description, theirs.event.description); 288 var resolutions: std.ArrayList([]const u8) = .empty; 289 var hunk_index: usize = 0; 290 for (chunks) |chunk| { 291 if (chunk != .conflict) continue; 292 try resolutions.append(aa, if (hunk_index < resolve.hunks.len) resolve.hunks[hunk_index] else (chunk.conflict.ours orelse "")); 293 hunk_index += 1; 294 } 295 updated.description = try diff3.assemble(aa, chunks, resolutions.items); 296 return updated; 297 } 298 299 // the issue `id_bytes` names in the repo's consumed event db (the repo's own 300 // db for xit; the side db next to a git repo), or null 301 fn readById( 302 comptime repo_kind: rp.RepoKind, 303 comptime repo_opts: rp.RepoOpts(repo_kind), 304 io: std.Io, 305 allocator: std.mem.Allocator, 306 arena: *std.heap.ArenaAllocator, 307 repo: *rp.Repo(repo_kind, repo_opts), 308 id_bytes: *const [evt.event_id_size]u8, 309 ) !?Record { 310 const DB = evt.EventDB(repo_opts.hash); 311 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; 312 defer if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); 313 const moment = (if (event_db_maybe) |*event_db| 314 evt.currentMomentFromDb(repo_opts.hash, event_db.db) 315 else if (repo_kind == .git) 316 return null 317 else 318 evt.currentMoment(repo_opts, repo)) catch return null; 319 const event_id_to_issue_cursor = (try moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue"))) orelse return null; 320 const event_id_to_issue = try DB.HashMap(.read_only).init(event_id_to_issue_cursor); 321 const issue_cursor = (try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, id_bytes))) orelse return null; 322 const issue_map = try DB.HashMap(.read_only).init(issue_cursor); 323 return try evt.read(Record, DB, repo_opts.hash, arena, issue_map); 324 } 325 326 // remove an issue's order key from its tags' `status` sets, pruning entries 327 // whose set becomes empty 328 fn removeFromTagSets( 329 comptime DB: type, 330 tag_to_issues: DB.SortedMap(.read_write), 331 tags: []const u8, 332 status: Status, 333 order_key: []const u8, 334 ) !void { 335 var tag_iter = tagIterator(tags); 336 while (tag_iter.next()) |tag| { 337 // skipped the same way consume skipped it, so it was never indexed 338 if (tag.len == 0 or tag.len > tag_max_len) continue; 339 var key_buffer: TagStatusKey = undefined; 340 const key = try tagStatusKey(&key_buffer, tag, status); 341 const tag_set_cursor = try tag_to_issues.putCursor(key); 342 const tag_set = try DB.SortedSet(.read_write).init(tag_set_cursor); 343 _ = try tag_set.remove(order_key); 344 if (0 == try tag_set.count()) _ = try tag_to_issues.remove(key); 345 } 346 } 347