╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ───────────╮ │src/ui/Repo/Undo.zig│ ╰────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │58 var buffer: [opts.max_read_size]u8 = undefined; │ │59 while (remaining > 0 and items.items.len < page_size) { │ │60 remaining -= 1; │ │58 - const cursor = try history.getCursor(remaining) orelse return error.TransactionNotFound; │ │59 - const moment = try DB.HashMap(.read_only).init(cursor); │ │61 + const moment = try repo.core.momentAt(remaining); │ │62 const record = try xit.undo.read(opts, moment, &buffer); │ │61 - const detail = if (record) |value| try eventDetail(opts, arena, &repo.core.db, haxy_moment, identity, value, moment, remaining) else null;│ │63 + const detail = if (record) |value| try eventDetail(opts, arena, &repo.core, haxy_moment, identity, value, moment, remaining) else null; │ │64 const shown = detail orelse Detail{ │ │63 - .action = if (record) |value| try aa.dupe(u8, value.action) else "unknown", │ │64 - .description = try formatRecord(opts, arena, &repo.core.db, record), │ │65 + .action = if (record) |value| try aa.dupe(u8, value.action_kind) else "unknown", │ │66 + .description = try formatRecord(opts, arena, &repo.core, record), │ │67 }; │ │68 const route = ui.RoutablePage.repoUndoRoute(identity, remaining) orelse return error.RouteTooLong; │ │69 const url = try route.toUrl(arena); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────╮ │ 91 fn eventDetail( │ │ 92 comptime opts: rp.RepoOpts(.xit), │ │ 93 arena: *std.heap.ArenaAllocator, │ │ 92 - db: *rp.Repo(.xit, opts).DB, │ │ 94 + core: *rp.Repo(.xit, opts).Core, │ │ 95 haxy_moment: ?evt.AdminDB.HashMap(.read_only), │ │ 96 identity: []const u8, │ │ 95 - record: xit.undo.UndoRecord, │ │ 97 + record: xit.undo.Record, │ │ 98 moment: rp.Repo(.xit, opts).DB.HashMap(.read_only),│ │ 99 history_index: u64, │ │100 ) !?Detail { │ ╰────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │101 const aa = arena.allocator(); │ │100 - if (std.mem.eql(u8, record.action, undoTag(opts))) { │ │101 - // parsing here is what bounds the recursion below: a record that does │ │102 - // not parse is left to xit, so `undoneButton` always starts the chain │ │103 - // at a transaction other than this one │ │104 - const parsed = std.json.parseFromSlice(UndoPayload, arena.child_allocator, record.payload, .{ .ignore_unknown_fields = true }) catch |err| switch (err) { │ │102 + if (std.mem.eql(u8, record.action_kind, undo_action_kind)) { │ │103 + // an undo names a transaction earlier than itself, so the chain │ │104 + // below always moves down the history and ends │ │105 + const parsed = std.json.parseFromSlice(xit.undo.Undo, arena.child_allocator, record.payload, .{ .ignore_unknown_fields = true }) catch |err| switch (err) {│ │106 error.OutOfMemory => return err, │ │107 else => return null, │ │108 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │109 defer parsed.deinit(); │ │109 - const button = try undoneButton(opts, arena, db, haxy_moment, identity, parsed.value.index); │ │110 - return .{ .action = undoTag(opts), .description = button.text, .buttons = try aa.dupe(DetailButton, &.{button}) }; │ │110 + const button = try undoneButton(opts, arena, core, haxy_moment, identity, parsed.value); │ │111 + return .{ .action = undo_action_kind, .description = button.text, .buttons = try aa.dupe(DetailButton, &.{button}) }; │ │112 } │ │112 - if (std.mem.eql(u8, record.action, evt.merge_undo_action)) return .{ .action = "merge events", .description = "merged the events ref from a remote" }; │ │113 - if (std.mem.eql(u8, record.action, pch.merge_undo_action)) return .{ .action = "merge patch", .description = "merged a patch into its target branch" }; │ │114 - if (std.mem.eql(u8, record.action, fork.undo_action)) return .{ .action = "fork", .description = "created a fork to draft a patch in" }; │ │115 - if (std.mem.eql(u8, record.action, push.undo_action)) { │ │113 + if (std.mem.eql(u8, record.action_kind, evt.merge_undo_action)) return .{ .action = "merge events", .description = "merged the events ref from a remote" }; │ │114 + if (std.mem.eql(u8, record.action_kind, pch.merge_undo_action)) return .{ .action = "merge patch", .description = "merged a patch into its target branch" };│ │115 + if (std.mem.eql(u8, record.action_kind, fork.undo_action)) return .{ .action = "fork", .description = "created a fork to draft a patch in" }; │ │116 + if (std.mem.eql(u8, record.action_kind, push.undo_action)) { │ │117 // a push consumes the events it carried in its own transaction │ │118 var buttons: std.ArrayList(DetailButton) = .empty; │ │119 if (try pushUser(arena, haxy_moment, record.payload)) |button| try buttons.append(aa, button); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │121 if (count > 0) try buttons.append(aa, try eventsButton(arena, identity, history_index, count)); │ │122 return .{ .action = "push", .description = "received a push", .buttons = buttons.items }; │ │123 } │ │123 - if (!std.mem.eql(u8, record.action, evt.undo_action)) return null; │ │124 + if (!std.mem.eql(u8, record.action_kind, evt.undo_action)) return null; │ │125 │ │126 const count = try momentEventCount(opts, moment) orelse return .{ .action = "events", .description = "updated the event database" };│ │127 // a transaction that consumed only merge commits indexed no events of its own │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │134 }; │ │135 } │ │136 │ │136 -// xit names its own commands after their tag │ │137 -fn undoTag(comptime opts: rp.RepoOpts(.xit)) []const u8 { │ │138 - return @tagName(std.meta.Tag(xit.undo.UndoCommand(opts.hash)).undo); │ │139 -} │ │140 - │ │141 -// what an undo record carries: the transaction it restored, and the last one │ │142 -// it discarded │ │143 -const UndoPayload = struct { index: u64, last_index: u64 }; │ │144 - │ │137 // xit formats the transactions haxy has no name of its own for │ │146 -fn formatRecord(comptime opts: rp.RepoOpts(.xit), arena: *std.heap.ArenaAllocator, db: *rp.Repo(.xit, opts).DB, record: ?xit.undo.UndoRecord) ![]const u8 {│ │138 +fn formatRecord(comptime opts: rp.RepoOpts(.xit), arena: *std.heap.ArenaAllocator, core: *rp.Repo(.xit, opts).Core, record: ?xit.undo.Record) ![]const u8 {│ │139 const value = record orelse return "no undo record"; │ │140 var description: std.Io.Writer.Allocating = .init(arena.allocator()); │ │149 - try xit.undo.format(opts, db, arena.child_allocator, value, &description.writer); │ │141 + try xit.undo.format(opts, core, arena.child_allocator, value, &description.writer); │ │142 return description.written(); │ │143 } │ │144 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │147 fn undoneButton( │ │148 comptime opts: rp.RepoOpts(.xit), │ │149 arena: *std.heap.ArenaAllocator, │ │158 - db: *rp.Repo(.xit, opts).DB, │ │150 + core: *rp.Repo(.xit, opts).Core, │ │151 haxy_moment: ?evt.AdminDB.HashMap(.read_only), │ │152 identity: []const u8, │ │161 - index: u64, │ │153 + undo: xit.undo.Undo, │ │154 ) anyerror!DetailButton { │ │163 - const DB = rp.Repo(.xit, opts).DB; │ │164 - const history = try DB.ArrayList(.read_only).init(db.rootCursor().readOnly()); │ │155 var buffer: [opts.max_read_size]u8 = undefined; │ │166 - │ │167 - // an undo of an undo is a redo, so the chain is followed the way xit's own │ │168 - // formatting does, until it reaches the transaction that was restored │ │169 - var target = index; │ │170 - var redo = false; │ │171 - while (true) { │ │172 - const moment = try DB.HashMap(.read_only).init(try history.getCursor(target) orelse return error.TransactionNotFound); │ │173 - const record = try xit.undo.read(opts, moment, &buffer) orelse break; │ │174 - if (!std.mem.eql(u8, record.action, undoTag(opts))) break; │ │175 - const parsed = std.json.parseFromSlice(UndoPayload, arena.child_allocator, record.payload, .{ .ignore_unknown_fields = true }) catch break;│ │176 - defer parsed.deinit(); │ │177 - redo = !redo; │ │178 - target = if (redo) parsed.value.last_index else parsed.value.index; │ │179 - } │ │156 + const target = try xit.undo.undoneTarget(opts, core, arena.child_allocator, undo, &buffer); │ │157 │ │181 - const moment = try DB.HashMap(.read_only).init(try history.getCursor(target) orelse return error.TransactionNotFound); │ │182 - const record = try xit.undo.read(opts, moment, &buffer); │ │183 - const detail = if (record) |value| try eventDetail(opts, arena, db, haxy_moment, identity, value, moment, target) else null; │ │184 - const route = ui.RoutablePage.repoUndoRoute(identity, target) orelse return error.RouteTooLong; │ │158 + // the detail reads the event count out of the transaction's own moment │ │159 + const moment = try core.momentAt(target.index); │ │160 + const detail = if (target.record) |value| try eventDetail(opts, arena, core, haxy_moment, identity, value, moment, target.index) else null; │ │161 + const route = ui.RoutablePage.repoUndoRoute(identity, target.index) orelse return error.RouteTooLong; │ │162 return .{ │ │186 - .label = if (redo) " what was redone " else " what was undone ", │ │187 - .text = if (detail) |value| value.action else try formatRecord(opts, arena, db, record), │ │163 + .label = if (target.redo) " what was redone " else " what was undone ", │ │164 + .text = if (detail) |value| value.action else try formatRecord(opts, arena, core, target.record), │ │165 .link = try std.fmt.allocPrint(arena.allocator(), "a:{s}", .{try route.toUrl(arena)}), │ │166 }; │ │167 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯