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