╭ changed ───────────╮ │src/ui/Repo/Undo.zig│ ╰────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │58 const cursor = try history.getCursor(remaining) orelse return error.TransactionNotFound; │ │59 const moment = try DB.HashMap(.read_only).init(cursor); │ │60 const record = try xit.undo.read(opts, moment, &buffer); │ │61 - var description: std.Io.Writer.Allocating = .init(aa); │ │62 - const detail = if (record) |value| try eventDetail(opts, arena, haxy_moment, identity, value, moment, remaining) else null; │ │63 - if (detail) |value| { │ │64 - try description.writer.writeAll(value.description); │ │65 - } else if (record) |value| { │ │66 - try xit.undo.format(opts, &repo.core.db, arena.child_allocator, value, &description.writer); │ │67 - } else { │ │68 - try description.writer.writeAll("no undo record"); │ │69 - } │ │61 + const detail = if (record) |value| try eventDetail(opts, arena, &repo.core.db, haxy_moment, identity, value, moment, remaining) else null;│ │62 + 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 + }; │ │66 const route = ui.RoutablePage.repoUndoRoute(identity, remaining) orelse return error.RouteTooLong; │ │67 const url = try route.toUrl(arena); │ │68 try items.append(aa, .{ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │69 .index = remaining, │ │74 - .action = if (detail) |value| value.action else if (record) |value| try aa.dupe(u8, value.action) else "unknown",│ │75 - .description = description.written(), │ │76 - .buttons = if (detail) |value| value.buttons else &.{}, │ │70 + .action = shown.action, │ │71 + .description = shown.description, │ │72 + .buttons = shown.buttons, │ │73 .timestamp = if (record) |value| try formatTimestamp(aa, value.timestamp) else "timestamp unavailable", │ │74 .link = try std.fmt.allocPrint(aa, "ai:{s}", .{url}), │ │75 .form = try std.fmt.allocPrint(aa, "form:{s}/undo", .{url}), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────╮ │89 fn eventDetail( │ │90 comptime opts: rp.RepoOpts(.xit), │ │91 arena: *std.heap.ArenaAllocator, │ │92 + db: *rp.Repo(.xit, opts).DB, │ │93 haxy_moment: ?evt.AdminDB.HashMap(.read_only),│ │94 identity: []const u8, │ │95 record: xit.undo.UndoRecord, │ ╰──────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 97 history_index: u64, │ │ 98 ) !?Detail { │ │ 99 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) {│ │105 + error.OutOfMemory => return err, │ │106 + else => return null, │ │107 + }; │ │108 + 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}) }; │ │111 + } │ │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" }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │131 .description = "updated the event database", │ │132 .buttons = buttons, │ │133 }; │ │134 +} │ │135 + │ │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 + │ │145 +// 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 {│ │147 + const value = record orelse return "no undo record"; │ │148 + var description: std.Io.Writer.Allocating = .init(arena.allocator()); │ │149 + try xit.undo.format(opts, db, arena.child_allocator, value, &description.writer); │ │150 + return description.written(); │ │151 +} │ │152 + │ │153 +// the transaction an undo restored, as a button that reads like its own row. │ │154 +// the error set is explicit because this and `eventDetail` call each other │ │155 +fn undoneButton( │ │156 + comptime opts: rp.RepoOpts(.xit), │ │157 + arena: *std.heap.ArenaAllocator, │ │158 + db: *rp.Repo(.xit, opts).DB, │ │159 + haxy_moment: ?evt.AdminDB.HashMap(.read_only), │ │160 + identity: []const u8, │ │161 + index: u64, │ │162 +) anyerror!DetailButton { │ │163 + const DB = rp.Repo(.xit, opts).DB; │ │164 + const history = try DB.ArrayList(.read_only).init(db.rootCursor().readOnly()); │ │165 + 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 + } │ │180 + │ │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; │ │185 + 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), │ │188 + .link = try std.fmt.allocPrint(arena.allocator(), "a:{s}", .{try route.toUrl(arena)}), │ │189 + }; │ │190 +} │ │191 + │ │192 // the page reads the transaction's own moment, so the url names the │ │193 // transaction rather than the state it produced │ │194 fn eventsButton(arena: *std.heap.ArenaAllocator, identity: []const u8, history_index: u64, count: u64) !DetailButton { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯