╭ changed ────╮ │build.zig.zon│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 3 .version = "0.0.0", │ │ 4 .dependencies = .{ │ │ 5 .xit = .{ │ │ 6 - .url = "git+https://github.com/xit-vcs/xit#fca8669ae328db5394dd925cbb4c4b50b5747c91",│ │ 7 - .hash = "xit-0.0.0-Ub3nDHHvUAFfxhL66LWF35khCJYDE1XXAqTzi2JRC1Sz", │ │ 6 + .url = "git+https://github.com/xit-vcs/xit#203ceedd05e2c021f3830f76da5af3217e1dfc2e",│ │ 7 + .hash = "xit-0.0.0-Ub3nDLnuUAFV-ZvaJ_ou1MsEbbssaqY_2iZEhptlWSI3", │ │ 8 }, │ │ 9 }, │ │10 .paths = .{ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────╮ │src/event/Issue.zig│ ╰───────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │39 pub const id_set_key = "issue-id-set"; │ │40 pub const conflicts_key = "conflicted-issue-id->conflict"; │ │41 pub const id_to_field_to_oid_key = "issue-id->field->oid"; │ │42 +pub const status_to_id_set_key = "status->issue-id-set"; │ │43 +pub const tag_status_to_id_set_key = "tag+status->issue-id-set"; │ │44 │ │45 // a "tag status" key names a tag's per-status issue set (tags can't contain│ │46 // spaces, so the pair is unambiguous) │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │77 ) !void { │ │78 const issue_key = hash.hashInt(hash_kind, event_id); │ │79 │ │78 - const event_id_to_issue_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "event-id->issue"));│ │80 + const event_id_to_issue_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, record_map_key)); │ │81 const event_id_to_issue = try DB.HashMap(.read_write).init(event_id_to_issue_cursor); │ │82 │ │83 // the per-status sets the issues views list, ordered by creation, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │84 // keyed by status name │ │83 - const status_to_issues_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "status->issue-id-set"));│ │85 + const status_to_issues_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, status_to_id_set_key)); │ │86 const status_to_issues = try DB.SortedMap(.read_write).init(status_to_issues_cursor); │ │87 │ │88 // the per-tag, per-status sets, keyed "tag,status" │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │87 - const tag_to_issues_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "tag+status->issue-id-set"));│ │89 + const tag_to_issues_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, tag_status_to_id_set_key)); │ │90 const tag_to_issues = try DB.SortedMap(.read_write).init(tag_to_issues_cursor); │ │91 │ │92 const conflicts_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, conflicts_key)); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │318 return null │ │319 else │ │320 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;│ │321 + const event_id_to_issue_cursor = (try moment.getCursor(hash.hashInt(repo_opts.hash, record_map_key))) orelse return null; │ │322 const event_id_to_issue = try DB.HashMap(.read_only).init(event_id_to_issue_cursor); │ │323 const issue_cursor = (try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, id_bytes))) orelse return null; │ │324 const issue_map = try DB.HashMap(.read_only).init(issue_cursor); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────╮ │src/event/Patch.zig│ ╰───────────────────╯ ╭─────────────────────────────────────────╮ │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; │ │7 │ │8 title: []const u8, │ ╰─────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────╮ │54 for (@typeInfo(Status).@"enum".fields) |field| len = @max(len, field.name.len);│ │55 break :blk len; │ │56 }; │ │57 +}; │ │58 + │ │59 +pub const Resolve = struct { │ │60 + title: ?[]const u8 = null, │ │61 + tags: ?[]const u8 = null, │ │62 + hunks: []const []const u8 = &.{}, │ │63 + theirs: []const u8 = "", │ │64 }; │ │65 │ │66 +pub const Update = union(enum) { │ │67 + status: Status, │ │68 + fields: struct { title: []const u8, tags: []const u8, description: []const u8 }, │ │69 + resolve: Resolve, │ │70 +}; │ │71 + │ │72 pub const tag_max_len = 64; │ │73 pub const merge_policy: evt.MergePolicy = .field_conflicts; │ │74 pub const record_map_key = "event-id->patch"; │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │239 } else if (parent.status == .merged and evt.fieldEqual(?Revision, parent.revision, merged.revision)) { │ │240 merged.status = .merged; │ │241 outcome[status_index] = .parent; │ │242 + } │ │243 +} │ │244 + │ │245 +pub fn update( │ │246 + comptime repo_kind: rp.RepoKind, │ │247 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │248 + io: std.Io, │ │249 + allocator: std.mem.Allocator, │ │250 + repo: *rp.Repo(repo_kind, repo_opts), │ │251 + id: *const [evt.event_id_size]u8, │ │252 + change: Update, │ │253 + author: evt.CommitAuthor, │ │254 +) !void { │ │255 + var arena = std.heap.ArenaAllocator.init(allocator); │ │256 + defer arena.deinit(); │ │257 + const record = (try readFromRepo(repo_kind, repo_opts, io, allocator, &arena, repo, id)) orelse return error.NotFound; │ │258 + if (record.removed) return error.NotFound; │ │259 + │ │260 + var updated = record.event; │ │261 + switch (change) { │ │262 + .status => |status| { │ │263 + if (updated.status == status) return; │ │264 + updated.status = status; │ │265 + }, │ │266 + .fields => |fields| { │ │267 + updated.title = fields.title; │ │268 + updated.tags = fields.tags; │ │269 + updated.description = fields.description; │ │270 + }, │ │271 + .resolve => |resolve| { │ │272 + updated = try resolveFields(repo_kind, repo_opts, io, allocator, &arena, repo, id, record, resolve); │ │273 + }, │ │274 + } │ │275 + if (!fieldsValid(updated.title, updated.tags)) return error.InvalidFields; │ │276 + │ │277 + try evt.consume(.repo, repo_kind, repo_opts, io, allocator, repo, evt.events_ref, &.{.{ │ │278 + .id = std.fmt.bytesToHex(id.*, .lower), │ │279 + .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │280 + .author = author, │ │281 + .event = .{ .patch = updated }, │ │282 + }}); │ │283 +} │ │284 + │ │285 +fn resolveFields( │ │286 + comptime repo_kind: rp.RepoKind, │ │287 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │288 + io: std.Io, │ │289 + allocator: std.mem.Allocator, │ │290 + arena: *std.heap.ArenaAllocator, │ │291 + repo: *rp.Repo(repo_kind, repo_opts), │ │292 + id: *const [evt.event_id_size]u8, │ │293 + live: Record, │ │294 + resolve: Resolve, │ │295 +) !Self { │ │296 + const DB = evt.EventDB(repo_opts.hash); │ │297 + var updated = live.event; │ │298 + if (resolve.title) |title| updated.title = title; │ │299 + if (resolve.tags) |tags| updated.tags = tags; │ │300 + │ │301 + 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;│ │302 + defer if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); │ │303 + const moment = (if (event_db_maybe) |*event_db| │ │304 + evt.currentMomentFromDb(repo_opts.hash, event_db.db) │ │305 + else if (repo_kind == .git) │ │306 + return updated │ │307 + else │ │308 + evt.currentMoment(repo_opts, repo)) catch return updated; │ │309 + const conflicts_cursor = (try moment.getCursor(hash.hashInt(repo_opts.hash, conflicts_key))) orelse return updated; │ │310 + const conflicts = try DB.SortedMap(.read_only).init(conflicts_cursor); │ │311 + const conflict_cursor = (try conflicts.getCursor(&evt.orderKeyDesc(live.created_order, id))) orelse return updated; │ │312 + const entry = try DB.HashMap(.read_only).init(conflict_cursor); │ │313 + const fields_cursor = (try entry.getCursor(hash.hashInt(repo_opts.hash, evt.conflicted_fields_key))) orelse return updated; │ │314 + const fields = try fields_cursor.readBytesAlloc(arena.allocator(), null); │ │315 + const their_cursor = (try entry.getCursor(hash.hashInt(repo_opts.hash, evt.their_record_key))) orelse return updated; │ │316 + const theirs = try evt.read(Record, DB, repo_opts.hash, arena, try DB.HashMap(.read_only).init(their_cursor)); │ │317 + │ │318 + var field_iter = std.mem.splitScalar(u8, fields, ' '); │ │319 + while (field_iter.next()) |field| { │ │320 + if (std.mem.eql(u8, field, "status") and fieldListed(resolve.theirs, ',', field)) updated.status = theirs.event.status; │ │321 + if (std.mem.eql(u8, field, "revision") and fieldListed(resolve.theirs, ',', field)) updated.revision = theirs.event.revision; │ │322 + } │ │323 + │ │324 + if (!fieldListed(fields, ' ', "description")) return updated; │ │325 + var base_description: []const u8 = ""; │ │326 + if (try entry.getCursor(hash.hashInt(repo_opts.hash, evt.base_record_key))) |base_cursor| { │ │327 + const base = try evt.read(Record, DB, repo_opts.hash, arena, try DB.HashMap(.read_only).init(base_cursor)); │ │328 + base_description = base.event.description; │ │329 + } │ │330 + const chunks = try diff3.chunks(io, allocator, arena, base_description, live.event.description, theirs.event.description); │ │331 + var resolutions: std.ArrayList([]const u8) = .empty; │ │332 + var hunk_index: usize = 0; │ │333 + for (chunks) |chunk| { │ │334 + if (chunk != .conflict) continue; │ │335 + try resolutions.append(arena.allocator(), if (hunk_index < resolve.hunks.len) resolve.hunks[hunk_index] else (chunk.conflict.ours orelse "")); │ │336 + hunk_index += 1; │ │337 + } │ │338 + updated.description = try diff3.assemble(arena.allocator(), chunks, resolutions.items); │ │339 + return updated; │ │340 +} │ │341 + │ │342 +fn fieldListed(fields: []const u8, delimiter: u8, name: []const u8) bool { │ │343 + var iter = std.mem.splitScalar(u8, fields, delimiter); │ │344 + while (iter.next()) |field| { │ │345 + if (std.mem.eql(u8, field, name)) return true; │ │346 } │ │347 + return false; │ │348 } │ │349 │ │350 +fn readFromRepo( │ │351 + comptime repo_kind: rp.RepoKind, │ │352 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │353 + io: std.Io, │ │354 + allocator: std.mem.Allocator, │ │355 + arena: *std.heap.ArenaAllocator, │ │356 + repo: *rp.Repo(repo_kind, repo_opts), │ │357 + id: *const [evt.event_id_size]u8, │ │358 +) !?Record { │ │359 + 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;│ │360 + defer if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); │ │361 + const moment = (if (event_db_maybe) |*event_db| │ │362 + evt.currentMomentFromDb(repo_opts.hash, event_db.db) │ │363 + else if (repo_kind == .git) │ │364 + return null │ │365 + else │ │366 + evt.currentMoment(repo_opts, repo)) catch return null; │ │367 + return readById(evt.EventDB(repo_opts.hash), repo_opts.hash, moment, arena, id); │ │368 +} │ │369 + │ │370 fn revisionKey(allocator: std.mem.Allocator, target_ref: []const u8, oid: []const u8) ![]u8 { │ │371 return std.mem.concat(allocator, u8, &.{ target_ref, "\x00", oid }); │ │372 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/try.zig│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │ 9 const evt = hx.event; │ │10 const xit = hx.xit; │ │11 const rp = xit.repo; │ │12 +const hash = xit.hash; │ │13 +const obj = xit.object; │ │14 const ui = hx.ui; │ │15 +const fork = hx.fork; │ │16 +const pch = hx.pch; │ │17 │ │18 // cook the terminal before a panic/segfault trace is printed, so the trace│ │19 // isn't mangled by raw mode and the alternate buffer │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯