╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ───────────╮ │src/ui/Repo/Undo.zig│ ╰────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │253 │ │254 // the push record stores the email of the user whose key the server accepted. │ │255 // resolving it names them as they are now, and leaves the email when no user │ │258 -// holds it, which is also what local mode sees with no admin db to read │ │256 +// holds it │ │257 fn pushUser(arena: *std.heap.ArenaAllocator, haxy_moment: ?evt.AdminDB.HashMap(.read_only), payload: []const u8) !?DetailButton { │ │258 const Payload = struct { author: struct { email: []const u8 } }; │ │259 const parsed = std.json.parseFromSlice(Payload, arena.child_allocator, payload, .{ .ignore_unknown_fields = true }) catch |err| switch (err) {│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │311 } │ │312 │ │313 pub fn execute(io: std.Io, allocator: std.mem.Allocator, source: ui.RepoSource, index: u64) !void {│ │316 - if (source.repo_kind != .xit) return error.NotFound; │ │314 var any_repo = try rp.AnyRepo(.xit, .{}).open(io, allocator, source.localInitOpts()); │ │315 defer any_repo.deinit(io, allocator); │ │316 switch (any_repo) { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │317 inline else => |*repo| try repo.undo(io, index), │ │321 - } │ │322 -} │ │323 - │ │324 -// discard every earlier transaction by compacting the database. the patch │ │325 -// revisions events point at are unreachable from the refs, so they are named │ │326 -// as extra roots to survive the collection. │ │327 -pub fn clearHistory(io: std.Io, allocator: std.mem.Allocator, source: ui.RepoSource) !void { │ │328 - if (source.repo_kind != .xit) return error.NotFound; │ │329 - var any_repo = try rp.AnyRepo(.xit, .{}).open(io, allocator, source.localInitOpts()); │ │330 - defer any_repo.deinit(io, allocator); │ │331 - switch (any_repo) { │ │332 - inline else => |*repo| { │ │333 - const opts = repo.self_repo_opts; │ │334 - // a repo that has consumed no events has no revisions to protect │ │335 - const roots = if (evt.currentMoment(opts, repo)) |moment| │ │336 - try evt.PatchRev.gcRoots(rp.Repo(.xit, opts).DB, opts.hash, allocator, moment)│ │337 - else |_| │ │338 - &.{}; │ │339 - defer allocator.free(roots); │ │340 - _ = try repo.garbageCollect(io, allocator, .{ .extra_roots = roots }); │ │341 - }, │ │318 } │ │319 } │ │320 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │331 // the repository; the web handler uses its matching http authorization path. │ │332 pub fn perform(allocator: std.mem.Allocator, session: *ui.Session, target: ui.RoutablePage.RepoUndoRoute) !void {│ │333 const identity = target.name.slice(); │ │358 - const index = if (target.clear) null else target.index orelse return error.InvalidHistoryIndex; │ │334 + const index = target.index orelse return error.InvalidHistoryIndex; │ │335 const source = try authorizedSource(session, identity) orelse return error.Forbidden; │ │336 const io = session.io orelse return error.NotFound; │ │361 - if (index) |value| try execute(io, allocator, source, value) else try clearHistory(io, allocator, source); │ │337 + try execute(io, allocator, source, index); │ │338 try session.navigate(ui.RoutablePage.repoUndoRoute(identity, null) orelse return error.RouteTooLong); │ │339 } │ │340 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │341 // the repo an owner may act on, or null when they may not │ │342 fn authorizedSource(session: *ui.Session, identity: []const u8) !?ui.RepoSource { │ │343 if (try session.authorize(identity, .owner, null) == null) return null; │ │368 - if (session.local) |local| return local; │ │344 const admin_repo = session.admin_repo orelse return error.NotFound; │ │345 const moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); │ │346 const pair = ui.RoutablePage.RepoIdentity.parse(identity) orelse return error.NotFound;│ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │359 │ │360 const list_max_width = 20; │ │361 const detail_min_width = 40; │ │362 + // the sub header leads the box where there is one │ │363 const header_index = 0; │ │388 - const content_index = 1; │ │364 // the undo button and timestamp stay; everything below them is rebuilt│ │365 const fixed_rows = 2; │ │366 │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │368 var outer = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .vert }); │ │369 errdefer outer.deinit(allocator); │ │370 │ │396 - // the sub header carries the tab's only action │ │397 - { │ │371 + // the sub header holds the command that clears the history. a server │ │372 + // without ssh has no way to. │ │373 + if (session.data.git_ssh_port) |port| { │ │374 var header_scroll = try ui.widget.HeaderScroll.init(allocator, session); │ │375 errdefer header_scroll.deinit(allocator); │ │400 - const header = header_scroll.row(); │ │401 - const route = ui.RoutablePage.repoUndoClearRoute(data.identity) orelse return error.RouteTooLong; │ │402 - const link = try session.page_arena.allocator().print("a:{s}", .{try route.toUrl(session.page_arena)}); │ │403 - const clear_label = "clear undo history"; │ │404 - try addText(allocator, header, clear_label, link, .single); │ │405 - // one line wide, so a narrow window scrolls the row instead of wrapping it │ │406 - header.children.values()[0].min_size = .{ .width = clear_label.len + 2, .height = null }; │ │407 - header.getFocus().child_id = header.children.keys()[0]; │ │376 + { │ │377 + const aa = session.page_arena.allocator(); │ │378 + const command = try aa.print("git push ssh://localhost:{d}/{s} :" ++ push.clear_undo_ref, .{ port, data.identity }); │ │379 + var copyable_text = try ui.widget.CopyableText.init(allocator, session, &.{.{ │ │380 + .text = command, │ │381 + .copyable_text = try aa.print("{s}{s}", .{ session.data.git_ssh_prefix, command }), │ │382 + .label = " clear undo history ", │ │383 + }}, 0); │ │384 + errdefer copyable_text.deinit(allocator); │ │385 + const min_width = copyable_text.minWidth(); │ │386 + const row = header_scroll.row(); │ │387 + row.getFocus().child_id = copyable_text.getFocus().id; │ │388 + try row.children.put(allocator, copyable_text.getFocus().id, .{ .widget = .{ .copyable_text = copyable_text }, .rect = null, .min_size = .{ .width = min_width, .height = 3 }, .max_size = .{ .width = min_width, .height = 3 } });│ │389 + } │ │390 // a row taller than the header, leaving a blank line beneath it │ │391 try outer.children.put(allocator, header_scroll.getFocus().id, .{ .widget = .{ .header_scroll = header_scroll }, .rect = null, .min_size = .{ .width = null, .height = 4 } }); │ │392 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │393 │ │412 - // the clear form, or the empty message when there is no history or failure to show │ │413 - if (data.clear or data.isEmpty(session)) { │ │414 - var center = if (data.clear) try initClearForm(allocator, data, session) else try ui.widget.initEmpty(allocator); │ │394 + // the empty message when there is no history or failure to show │ │395 + if (data.isEmpty(session)) { │ │396 + var center = try ui.widget.initEmpty(allocator); │ │397 errdefer center.deinit(allocator); │ │398 try outer.children.put(allocator, center.getFocus().id, .{ .widget = .{ .center = center }, .rect = null, .min_size = null });│ │417 - // the empty message has nothing to focus, so focus rests on the sub-header │ │418 - outer.getFocus().child_id = outer.children.keys()[if (data.clear) content_index else header_index]; │ │399 + // the empty message has nothing to focus, so focus rests on the sub header where there is one │ │400 + outer.getFocus().child_id = outer.children.keys()[header_index]; │ │401 return .{ .box = outer, .data = data, .session = session }; │ │402 } │ │403 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │447 } │ │448 box.getFocus().child_id = box.children.keys()[if (data.items.len > 0) 1 else 0]; │ │449 try outer.children.put(allocator, box.getFocus().id, .{ .widget = .{ .box = box }, .rect = null, .min_size = null }); │ │468 - outer.getFocus().child_id = outer.children.keys()[content_index]; │ │450 + outer.getFocus().child_id = outer.children.keys()[outer.children.count() - 1]; │ │451 return .{ .box = outer, .data = data, .session = session }; │ │470 - } │ │471 - │ │472 - // the same shape the thread views use to confirm a removal │ │473 - fn initClearForm(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !ui.widget.Center { │ │474 - var box = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .round_corners = true, .direction = .vert }); │ │475 - errdefer box.deinit(allocator); │ │476 - const route = ui.RoutablePage.repoUndoClearRoute(data.identity) orelse return error.RouteTooLong; │ │477 - box.getFocus().kind = .{ .custom = try session.page_arena.allocator().print("form:{s}", .{try route.toUrl(session.page_arena)}) }; │ │478 - │ │479 - var prompt = try wgt.Text.init(allocator, "are you sure?"); │ │480 - errdefer prompt.deinit(allocator); │ │481 - try box.children.put(allocator, prompt.getFocus().id, .{ .widget = .{ .text = prompt }, .rect = null, .min_size = null }); │ │482 - │ │483 - var button = try wgt.TextBox.init(allocator, "clear undo history", .{ .border = .single, .round_corners = true, .wrap_kind = .none });│ │484 - errdefer button.deinit(allocator); │ │485 - button.getFocus().mode = .all; │ │486 - button.getFocus().kind = if (data.handle.canUndo()) .{ .custom = "submit" } else .text_box; │ │487 - try box.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = null }); │ │488 - box.getFocus().child_id = button.getFocus().id; │ │489 - │ │490 - return ui.widget.Center.init(allocator, .{ .box = box }); │ │452 } │ │453 │ │454 fn addText(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), text: []const u8, link: ?[]const u8, border: wgt.Border) !void { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │473 self.session.host_request = .{ .undo = route.repo_undo }; │ │474 } │ │475 │ │515 - fn headerBox(self: *View) *wgt.Box(ui.Widget) { │ │516 - return self.box.children.values()[header_index].widget.header_scroll.row(); │ │476 + fn header(self: *View) ?*ui.widget.CopyableText { │ │477 + return switch (self.box.children.values()[header_index].widget) { │ │478 + .header_scroll => |*header_scroll| &header_scroll.row().children.values()[0].widget.copyable_text,│ │479 + else => null, │ │480 + }; │ │481 } │ │482 fn headerActive(self: *View) bool { │ │483 + if (self.header() == null) return false; │ │484 return self.box.getFocus().child_id == self.box.children.keys()[header_index]; │ │485 } │ │486 + // the content is last, below the sub header where there is one │ │487 fn contentBox(self: *View) *wgt.Box(ui.Widget) { │ │522 - return &self.box.children.values()[content_index].widget.box; │ │488 + return &self.box.children.values()[self.box.children.count() - 1].widget.box; │ │489 } │ │490 fn listScroll(self: *View) *wgt.Scroll(ui.Widget) { │ │491 return &self.contentBox().children.values()[0].widget.scroll; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯