╭ changed ───────────╮ │src/ui/Repo/Undo.zig│ ╰────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────╮ │ 5 const inp = @import("../input.zig");│ │ 6 const xit = @import("xit"); │ │ 7 const rp = xit.repo; │ │ 8 +const hash = xit.hash; │ │ 9 const wgt = xit.xitui.widget; │ │10 const layout = xit.xitui.layout; │ │11 const Key = xit.xitui.input.Key; │ ╰────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────╮ │14 const Self = @This(); │ │15 │ │16 pub const page_size = 20; │ │17 + │ │18 +// replaces the description for a transaction that wrote events│ │19 +pub const EventsLink = struct { │ │20 + label: []const u8, │ │21 + link: []const u8, │ │22 +}; │ │23 + │ │24 pub const Item = struct { │ │25 index: u64, │ │26 action: []const u8, │ ╰───────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────╮ │27 description: []const u8, │ │28 + timestamp: []const u8, │ │29 link: []const u8, │ │30 form: []const u8, │ │31 + events: ?EventsLink = null,│ │32 }; │ │33 │ │34 identity: []const u8, │ ╰───────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │25 -count: u64, │ │26 -items: []const Item, │ │27 -next: ?u64, │ │35 +count: u64 = 0, │ │36 +items: []const Item = &.{}, │ │37 +next: ?u64 = null, │ │38 +failure: ?[]const u8 = null, │ │39 +can_undo: bool = false, │ │40 │ │41 pub fn init(comptime opts: rp.RepoOpts(.xit), arena: *std.heap.ArenaAllocator, repo: *rp.Repo(.xit, opts), identity: []const u8, selected: ?u64) !Self {│ │42 const aa = arena.allocator(); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │51 remaining -= 1; │ │52 const cursor = try history.getCursor(remaining) orelse return error.TransactionNotFound; │ │53 const moment = try DB.HashMap(.read_only).init(cursor); │ │42 - var record = try xit.undo.read(opts, moment, &buffer); │ │43 - if (record) |*value| { │ │44 - if (std.mem.eql(u8, value.action, "update_branch_patch")) value.action = "update_patch"; │ │45 - } │ │54 + const record = try xit.undo.read(opts, moment, &buffer); │ │55 var description: std.Io.Writer.Allocating = .init(aa); │ │47 - if (record) |value| { │ │56 + const detail = if (record) |value| try eventDetail(opts, arena, identity, value, moment) else null;│ │57 + if (detail) |value| { │ │58 + try description.writer.writeAll(value.description); │ │59 + } else if (record) |value| { │ │60 try xit.undo.format(opts, &repo.core.db, arena.child_allocator, value, &description.writer); │ │49 - try description.writer.print("\n\n{s}", .{try formatTimestamp(aa, value.timestamp)}); │ │61 } else { │ │51 - try description.writer.writeAll("(empty description)\n\ntimestamp unavailable"); │ │62 + try description.writer.writeAll("no undo record"); │ │63 } │ │64 const route = ui.RoutablePage.repoUndoRoute(identity, remaining) orelse return error.RouteTooLong; │ │65 const url = try route.toUrl(arena); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │66 try items.append(aa, .{ │ │67 .index = remaining, │ │57 - .action = if (record) |value| try aa.dupe(u8, value.action) else "(empty description)", │ │68 + .action = if (detail) |value| value.action else if (record) |value| try aa.dupe(u8, value.action) else "unknown",│ │69 .description = description.written(), │ │70 + .events = if (detail) |value| value.events else null, │ │71 + .timestamp = if (record) |value| try formatTimestamp(aa, value.timestamp) else "timestamp unavailable", │ │72 .link = try std.fmt.allocPrint(aa, "ai:{s}", .{url}), │ │73 .form = try std.fmt.allocPrint(aa, "form:{s}/undo", .{url}), │ │74 }); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 75 } │ │ 76 return .{ .identity = try aa.dupe(u8, identity), .count = count, .items = items.items, .next = if (remaining > 0) remaining - 1 else null }; │ │ 77 +} │ │ 78 + │ │ 79 +const Detail = struct { │ │ 80 + action: []const u8, │ │ 81 + description: []const u8, │ │ 82 + events: ?EventsLink = null, │ │ 83 +}; │ │ 84 + │ │ 85 +// every haxy event transaction shares one action, and the events it wrote are │ │ 86 +// named by the moment's index rather than by the record │ │ 87 +fn eventDetail( │ │ 88 + comptime opts: rp.RepoOpts(.xit), │ │ 89 + arena: *std.heap.ArenaAllocator, │ │ 90 + identity: []const u8, │ │ 91 + record: xit.undo.UndoRecord, │ │ 92 + moment: rp.Repo(.xit, opts).DB.HashMap(.read_only), │ │ 93 +) !?Detail { │ │ 94 + if (std.mem.eql(u8, record.action, evt.merge_undo_action)) return .{ .action = "merge events", .description = "merged the events ref from a remote" };│ │ 95 + if (!std.mem.eql(u8, record.action, evt.undo_action)) return null; │ │ 96 + │ │ 97 + const description = "updated the event database"; │ │ 98 + const batch = try momentBatch(opts, moment) orelse return .{ .action = "events", .description = description }; │ │ 99 + │ │100 + const aa = arena.allocator(); │ │101 + const route = ui.RoutablePage.repoEventsRoute(identity, .active, null, "", batch.index) orelse return error.RouteTooLong; │ │102 + return .{ │ │103 + .action = try std.fmt.allocPrint(aa, "events ({d})", .{batch.count}), │ │104 + .description = description, │ │105 + .events = .{ │ │106 + .label = try std.fmt.allocPrint(aa, "view events ({d})", .{batch.count}), │ │107 + .link = try std.fmt.allocPrint(aa, "a:{s}", .{try route.toUrl(arena)}), │ │108 + }, │ │109 + }; │ │110 +} │ │111 + │ │112 +const Batch = struct { index: u64, count: u64 }; │ │113 + │ │114 +// the events the transaction's moment wrote, or null when it wrote none │ │115 +fn momentBatch(comptime opts: rp.RepoOpts(.xit), moment: rp.Repo(.xit, opts).DB.HashMap(.read_only)) !?Batch { │ │116 + const DB = rp.Repo(.xit, opts).DB; │ │117 + const haxy_moment = evt.currentMomentFromRepoMoment(opts.hash, moment) catch return null; │ │118 + const index_cursor = try haxy_moment.getCursor(hash.hashInt(opts.hash, evt.moment_index_key)) orelse return null; │ │119 + const moment_index = try index_cursor.readUint(); │ │120 + const ids = try evt.momentEventIds(DB, opts.hash, haxy_moment, moment_index) orelse return null; │ │121 + return .{ .index = moment_index, .count = try ids.count() }; │ │122 } │ │123 │ │124 pub fn formatTimestamp(aa: std.mem.Allocator, timestamp: i64) ![]const u8 { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │140 } │ │141 } │ │142 │ │143 +// keep action failures on the current tab without rebuilding a stale route. │ │144 +pub fn handleRequest(allocator: std.mem.Allocator, session: *ui.Session, target: ui.RoutablePage.RepoUndoRoute) void {│ │145 + perform(allocator, session, target) catch |err| { │ │146 + session.data.undo_failure = @errorName(err); │ │147 + return; │ │148 + }; │ │149 + session.data.undo_failure = null; │ │150 +} │ │151 + │ │152 // both terminal hosts resolve and authorize the request afresh before opening │ │153 // the repository; the web handler uses its matching http authorization path. │ │154 pub fn perform(allocator: std.mem.Allocator, session: *ui.Session, target: ui.RoutablePage.RepoUndoRoute) !void { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │155 const index = target.index orelse return error.InvalidHistoryIndex; │ │156 const identity = target.name.slice(); │ │ 90 - if (try session.authorize(identity, .write) == null) return error.Forbidden;│ │157 + if (try session.authorize(identity, .owner) == null) return error.Forbidden;│ │158 const io = session.io orelse return error.NotFound; │ │159 const source = session.local orelse blk: { │ │160 const admin_repo = session.admin_repo orelse return error.NotFound; │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────╮ │173 data: *const Self, │ │174 session: *ui.Session, │ │175 detailed_index: ?usize = null, │ │176 + failure_id: ?usize = null, │ │177 + shown_failure: ?[]const u8 = null,│ │178 │ │179 const list_max_width = 20; │ │180 const detail_min_width = 40; │ ╰───────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯