╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ undo.zig ─────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
88 try repo.addConfig(io, allocator, .{ .name = "undo.value", .value = "second" }); 89 } 90 for ([_]?[]const u8{ &owner, &writer, &reader, null }, [_]bool{ true, true, false, false }) |user, allowed| { 91 var session = try ui.Session.init(&arena, &admin, .{ .user_id = user }); 92 session.io = io; 93 session.repos_dir = repos_dir; 94 const files = ui.RoutablePage.repoFilesRoute("alice/demo", null, "", "", 0).?; 95 session.data.current_page = files; 96 const page = try ui.Repo.init(&arena, &session, files); 97 try std.testing.expectEqual(allowed, page.undo != null); 98 if (page.undo) |undo| try std.testing.expectEqual(user != null and std.mem.eql(u8, user.?, &owner), undo.can_undo); 99 var root = ui.Widget{ .repo = try ui.Repo.View.init(allocator, &page, &session) }; 100 defer root.deinit(allocator); 101 try root.build(allocator, constraint, root.getFocus()); 102 const html = try @import("../web.zig").generateHtml(allocator, &root, &session); 103 defer allocator.free(html); 104 try std.testing.expectEqual(allowed, std.mem.indexOf(u8, html, "/repo/alice/demo/undo") != null); 105 const route = ui.RoutablePage.repoUndoRoute("alice/demo", null).?; 106 if (allowed) { 107 _ = try ui.Repo.init(&arena, &session, route); 108 } else { 109 try std.testing.expectError(error.NotFound, ui.Repo.init(&arena, &session, route)); 110 } 111 if (user == null or !std.mem.eql(u8, user.?, &owner)) { 112 try std.testing.expectError(error.Forbidden, Undo.perform(allocator, &session, ui.RoutablePage.repoUndoRoute("alice/demo", 2).?.repo_undo)); 113 } 114 } 115 var session = try ui.Session.init(&arena, &admin, .{ .user_id = &owner }); 116 session.io = io; 117 session.repos_dir = repos_dir; 118 try Undo.perform(allocator, &session, ui.RoutablePage.repoUndoRoute("alice/demo", 2).?.repo_undo); 119 try std.testing.expectEqual(null, session.next_page.?.repo_undo.index); 120 { 121 var repo = try xit.repo.Repo(.xit, .{}).open(io, allocator, .{ .path = path }); 122 defer repo.deinit(io, allocator); 123 try expectConfig(&repo, "first"); 124 const data = try Undo.init(.{}, &arena, &repo, null, "alice/demo", null); 125 try std.testing.expectEqualStrings("undo", data.items[0].action); 126 } 127 // keep the old session/page moment, but transfer ownership to the writer. 128 repo_event.user_id = &writer; 129 try evt.consume(.server, .admin, .xit, evt.admin_repo_opts, io, allocator, &admin, evt.events_ref, &.{.{ .id = std.fmt.bytesToHex(repo_id, .lower), .author = author, .event = .{ .repo = repo_event } }}); 130 session.next_page = null; 131 Undo.handleRequest(allocator, &session, ui.RoutablePage.repoUndoRoute("alice/demo", 1).?.repo_undo); 132 try std.testing.expectEqualStrings("Forbidden", session.data.undo_failure.?); 133 try std.testing.expectEqual(null, session.next_page); 134 135 // public-write repos still require login for history and undo. 136 repo_event.user_id = &owner; 137 repo_event.write_access = .public; 138 try evt.consume(.server, .admin, .xit, evt.admin_repo_opts, io, allocator, &admin, evt.events_ref, &.{.{ .id = std.fmt.bytesToHex(repo_id, .lower), .author = author, .event = .{ .repo = repo_event } }}); 139 var anonymous = try ui.Session.init(&arena, &admin, .{}); 140 anonymous.io = io; 141 anonymous.repos_dir = repos_dir; 142 try std.testing.expectError(error.NotFound, ui.Repo.init(&arena, &anonymous, ui.RoutablePage.repoUndoRoute("alice/demo", null).?)); 143 } 144 145 test "local git has no undo and local xit can undo and refresh" { 146 inline for ([_]xit.repo.RepoKind{ .git, .xit }) |kind| { 147 var temp = std.testing.tmpDir(.{}); 148 defer temp.cleanup(); 149 var arena = std.heap.ArenaAllocator.init(allocator); 150 defer arena.deinit(); 151 const path = try temp.dir.realPathFileAlloc(io, ".", arena.allocator()); 152 { 153 var repo = try xit.repo.Repo(kind, .{}).init(io, allocator, .{ .path = path }); 154 defer repo.deinit(io, allocator); 155 try repo.addConfig(io, allocator, .{ .name = "undo.value", .value = "first" }); 156 } 157 var session = ui.Session{ .arena = &arena, .page_arena = &arena, .io = io, .local = .{ .path = path, .repo_kind = kind }, .data = .{ .host_kind = .local } }; 158 const files = ui.RoutablePage.repoFilesRoute("", null, "", "", 0).?; 159 const data = try ui.Repo.init(&arena, &session, files); 160 try std.testing.expectEqual(kind == .xit, data.undo != null); 161 if (kind == .git) { 162 try std.testing.expectError(error.NotFound, ui.Repo.init(&arena, &session, ui.RoutablePage.repoUndoRoute("", null).?)); 163 try std.testing.expectError(error.NotFound, Undo.execute(io, allocator, session.local.?, 1)); 164 } else { 165 Undo.handleRequest(allocator, &session, ui.RoutablePage.repoUndoRoute("", 999).?.repo_undo); 166 try std.testing.expectEqualStrings("InvalidHistoryIndex", session.data.undo_failure.?); 167 try std.testing.expectEqual(null, session.next_page); 168 Undo.handleRequest(allocator, &session, ui.RoutablePage.repoUndoRoute("", 1).?.repo_undo); 169 try std.testing.expectEqual(null, session.data.undo_failure); 170 const refreshed = try ui.Repo.init(&arena, &session, session.next_page.?); 171 try std.testing.expectEqualStrings("undo", refreshed.undo.?.items[0].action); 172 } 173 } 174 } 175 176 test "commit undo records use the formatted description" { 177 var temp = std.testing.tmpDir(.{}); 178 defer temp.cleanup(); 179 var arena = std.heap.ArenaAllocator.init(allocator); 180 defer arena.deinit(); 181 const path = try temp.dir.realPathFileAlloc(io, ".", arena.allocator()); 182 const opts: xit.repo.RepoOpts(.xit) = .{ .is_test = true }; 183 var repo = try xit.repo.Repo(.xit, opts).init(io, allocator, .{ .path = path }); 184 defer repo.deinit(io, allocator); 185 _ = try repo.commitAtRef(io, allocator, .{ .message = "example commit" }, null, .{ .kind = .head, .name = "master" }); 186 const data = try Undo.init(opts, &arena, &repo, null, "", null); 187 try std.testing.expect(std.mem.startsWith(u8, data.items[0].description, "commit -m \"example commit\"")); 188 } 189 190 test "malformed undo history is shown as an error" { 191 var temp = std.testing.tmpDir(.{}); 192 defer temp.cleanup(); 193 var arena = std.heap.ArenaAllocator.init(allocator); 194 defer arena.deinit(); 195 const path = try temp.dir.realPathFileAlloc(io, ".", arena.allocator()); 196 const Repo = xit.repo.Repo(.xit, .{}); 197 var repo = try Repo.init(io, allocator, .{ .path = path }); 198 defer repo.deinit(io, allocator); 199 200 // append an invalid undo record without damaging the repository data. 201 const Corrupt = struct { 202 pub fn run(_: @This(), cursor: *Repo.DB.Cursor(.read_write)) !void { 203 const moment = try Repo.DB.HashMap(.read_write).init(cursor.*); 204 try moment.put(xit.hash.hashInt(.sha1, "undo"), .{ .bytes = "bad" }); 205 } 206 }; 207 const history = try Repo.DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); 208 try history.appendContext(.{ .slot = try history.getSlot(-1) }, Corrupt{}); 209 210 var session = ui.Session{ .arena = &arena, .page_arena = &arena, .io = io, .local = .{ .path = path, .repo_kind = .xit }, .data = .{ .host_kind = .local } }; 211 // tabs switch in-page, so every tab carries the undo data and its failure 212 const files = try ui.Repo.init(&arena, &session, ui.RoutablePage.repoFilesRoute("", null, "", "", 0).?); 213 try std.testing.expectEqual(0, files.undo.?.items.len); 214 try std.testing.expectEqualStrings("InvalidUndoRecord", files.undo.?.failure.?); 215 const undo = try ui.Repo.init(&arena, &session, ui.RoutablePage.repoUndoRoute("", null).?); 216 try std.testing.expectEqualStrings("InvalidUndoRecord", undo.undo.?.failure.?); 217 218 var root = ui.Widget{ .repo_undo = try Undo.View.init(allocator, &undo.undo.?, &session) }; 219 defer root.deinit(allocator); 220 try root.build(allocator, constraint, root.getFocus()); 221 const html = try @import("../web.zig").generateHtml(allocator, &root, &session); 222 defer allocator.free(html); 223 try std.testing.expect(std.mem.indexOf(u8, html, "InvalidUndoRecord") != null); 224 } 225