╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │597 .upload_pack => .read, │ │598 .receive_pack => .write, │ │599 }; │ │595 - const protected_branches = try repo.repo.event.protectedBranchNames(arena.allocator()); │ │596 - // skip the key lookups when the base role is enough │ │597 - if (repo.role) |base| { │ │598 - if (base.atLeast(min_role)) return .{ .allowed = .{ .author = null, .protected_branches = protected_branches } };│ │599 - } │ │600 + // any key may fetch a public repo; a push always needs a key holder │ │601 + if (service == .upload_pack and repo.role != null) return .{ .allowed = .fetch }; │ │602 │ │603 // a key can be shared, so a holder whose role falls short doesn't end the search │ │604 const owner_id = try evt.parseEventId(&repo.repo.event.user_id); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │605 for (try evt.User.readIdsByFingerprint(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, fingerprint)) |holder_id| { │ │606 const seen = (try evt.readRepoById(io, allocator, arena, users_dir, &owner_id, &repo.event_id, holder_id)) orelse return .not_found; │ │607 const role = seen.role orelse continue; │ │606 - if (role.atLeast(min_role)) return .{ .allowed = .{ .author = try commitAuthor(moment, arena, &holder_id), .protected_branches = protected_branches } };│ │608 + if (!role.atLeast(min_role)) continue; │ │609 + return .{ .allowed = switch (service) { │ │610 + .upload_pack => .fetch, │ │611 + .receive_pack => .{ .push = .{ │ │612 + .author = try commitAuthor(moment, arena, &holder_id), │ │613 + .protected_branches = try repo.repo.event.protectedBranchNames(arena.allocator()), │ │614 + } }, │ │615 + } }; │ │616 } │ │617 return .denied; │ │618 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/test/undo.zig│ ╰─────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │135 Undo.handleRequest(allocator, &session, ui.RoutablePage.repoUndoRoute("alice:demo", 1).?.repo_undo); │ │136 try std.testing.expectEqualStrings("Forbidden", session.data.undo_failure.?); │ │137 try std.testing.expectEqual(null, session.next_page); │ │138 - │ │139 - // public-write repos still require login for history and undo. │ │140 - repo_event.user_id = std.fmt.bytesToHex(owner, .lower); │ │141 - repo_event.write_access = .public; │ │142 - try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .user, .xit, evt.user_repo_opts, io, allocator, &user_repo, evt.events_ref, &.{.{ .id = std.fmt.bytesToHex(repo_id, .lower), .author = author, .event = .{ .repo = repo_event } }});│ │143 - var anonymous = try ui.Session.init(&arena, &admin, .{}); │ │144 - anonymous.io = io; │ │145 - anonymous.users_dir = users_dir; │ │146 - try std.testing.expectError(error.NotFound, ui.Repo.init(&arena, &anonymous, ui.RoutablePage.repoUndoRoute("alice:demo", null).?)); │ │138 } │ │139 │ │140 test "local git has no undo and local xit can undo and refresh" { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────╮ │src/ui/Repo/Undo.zig│ ╰────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │257 // 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 │ │259 fn pushUser(arena: *std.heap.ArenaAllocator, haxy_moment: ?evt.AdminDB.HashMap(.read_only), payload: []const u8) !?DetailButton { │ │260 - const Payload = struct { author: ?struct { email: []const u8 } = null }; │ │260 + const Payload = struct { author: struct { email: []const u8 } }; │ │261 const parsed = std.json.parseFromSlice(Payload, arena.child_allocator, payload, .{ .ignore_unknown_fields = true }) catch |err| switch (err) {│ │262 error.OutOfMemory => return err, │ │263 else => return null, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │264 }; │ │265 defer parsed.deinit(); │ │266 - const author = parsed.value.author orelse return null; │ │267 - return switch (try ui.Author.initFromEmail(haxy_moment, arena, author.email)) { │ │266 + return switch (try ui.Author.initFromEmail(haxy_moment, arena, parsed.value.author.email)) { │ │267 .unknown => null, │ │268 .email => |email| .{ .label = " user ", .text = email }, │ │269 .user_name => |name| .{ .label = " user ", .text = name, .link = try ui.userLink(arena, name) },│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯