╭ changed ──────────╮ │src/event/Issue.zig│ ╰───────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────╮ │1 const std = @import("std"); │ │2 const evt = @import("../event.zig"); │ │3 +const diff3 = @import("../diff3.zig");│ │4 const xit = @import("xit"); │ │5 const rp = xit.repo; │ │6 const hash = xit.hash; │ ╰─────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │177 return DB.SortedSet(.read_write).init(cursor); │ │178 } │ │179 │ │179 -// what an update replaces on an issue: its status, or its content │ │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 + tags: ?[]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, tags: []const u8, description: []const u8 },│ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │194 + resolve: Resolve, │ │195 }; │ │196 │ │197 // re-emit the issue `id_bytes` names with `change` applied, preserving the│ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │186 -// rest of its current state. error.NotFound when the id names no issue. │ │187 -// `repo` must be writable. │ │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 comptime repo_kind: rp.RepoKind, │ │203 comptime repo_opts: rp.RepoOpts(repo_kind), │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │223 updated.title = fields.title; │ │224 updated.tags = fields.tags; │ │225 updated.description = fields.description; │ │226 + }, │ │227 + .resolve => |resolve| { │ │228 + updated = try resolveFields(repo_kind, repo_opts, io, allocator, &arena, repo, id_bytes, issue, resolve);│ │229 + if (!fieldsValid(updated.title, updated.tags)) return error.InvalidFields; │ │230 }, │ │231 } │ │232 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │236 .author_email = author_email, │ │237 .event = .{ .issue = updated }, │ │238 }}); │ │239 +} │ │240 + │ │241 +// the live fields with `resolve` applied: posted title/tags stand in for the │ │242 +// live ones, and a conflicted description is reassembled from the hunk │ │243 +// resolutions against the conflict entry. the chunking is deterministic: the │ │244 +// entry holds immutable slots and any event since would have removed it, so │ │245 +// the hunks match what the resolve form was built from. a settled or │ │246 +// unreadable entry keeps the live description. │ │247 +fn resolveFields( │ │248 + comptime repo_kind: rp.RepoKind, │ │249 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │250 + io: std.Io, │ │251 + allocator: std.mem.Allocator, │ │252 + arena: *std.heap.ArenaAllocator, │ │253 + repo: *rp.Repo(repo_kind, repo_opts), │ │254 + id_bytes: *const [evt.event_id_size]u8, │ │255 + live: Record, │ │256 + resolve: Resolve, │ │257 +) !Self { │ │258 + const DB = evt.EventDB(repo_opts.hash); │ │259 + const aa = arena.allocator(); │ │260 + │ │261 + var updated = live.event; │ │262 + if (resolve.title) |title| updated.title = title; │ │263 + if (resolve.tags) |tags| updated.tags = tags; │ │264 + │ │265 + 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;│ │266 + defer if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); │ │267 + const moment = (if (event_db_maybe) |*event_db| │ │268 + evt.currentMomentFromDb(repo_opts.hash, event_db.db) │ │269 + else if (repo_kind == .git) │ │270 + return updated │ │271 + else │ │272 + evt.currentMoment(repo_opts, repo)) catch return updated; │ │273 + │ │274 + const conflicts_cursor = (try moment.getCursor(hash.hashInt(repo_opts.hash, conflicts_key))) orelse return updated; │ │275 + const conflicts = try DB.SortedMap(.read_only).init(conflicts_cursor); │ │276 + const order_key = evt.orderKeyDesc(live.created_ts, id_bytes); │ │277 + const conflict_cursor = (try conflicts.getCursor(&order_key)) orelse return updated; │ │278 + const entry = try DB.HashMap(.read_only).init(conflict_cursor); │ │279 + │ │280 + const fields_cursor = (try entry.getCursor(hash.hashInt(repo_opts.hash, evt.conflicted_fields_key))) orelse return updated; │ │281 + const conflicted_fields = try fields_cursor.readBytesAlloc(aa, null); │ │282 + if (std.mem.indexOf(u8, conflicted_fields, "description") == null) return updated; │ │283 + │ │284 + const their_cursor = (try entry.getCursor(hash.hashInt(repo_opts.hash, evt.their_record_key))) orelse return updated; │ │285 + const theirs = try evt.read(Record, DB, repo_opts.hash, arena, try DB.HashMap(.read_only).init(their_cursor)); │ │286 + │ │287 + // absent when both sides created the issue independently │ │288 + var base_description: []const u8 = ""; │ │289 + if (try entry.getCursor(hash.hashInt(repo_opts.hash, evt.base_record_key))) |base_cursor| { │ │290 + const base = try evt.read(Record, DB, repo_opts.hash, arena, try DB.HashMap(.read_only).init(base_cursor)); │ │291 + base_description = base.event.description; │ │292 + } │ │293 + │ │294 + const chunks = try diff3.chunks(io, allocator, arena, base_description, live.event.description, theirs.event.description); │ │295 + var resolutions: std.ArrayList([]const u8) = .empty; │ │296 + var hunk_index: usize = 0; │ │297 + for (chunks) |chunk| { │ │298 + if (chunk != .conflict) continue; │ │299 + try resolutions.append(aa, if (hunk_index < resolve.hunks.len) resolve.hunks[hunk_index] else (chunk.conflict.ours orelse "")); │ │300 + hunk_index += 1; │ │301 + } │ │302 + updated.description = try diff3.assemble(aa, chunks, resolutions.items); │ │303 + return updated; │ │304 } │ │305 │ │306 // the issue `id_bytes` names in the repo's consumed event db (the repo's own │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯