╭ changed ──────────╮ │src/embed/script.js│ ╰───────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │380 return; │ │381 } else if (tag === "INPUT" && event.key === "Enter") { │ │382 // enter in a form's text input never submits it; only the │ │383 - // submit button does. an input outside a form has no submit │ │384 - // button, so the TUI handles its enter instead. │ │383 + // submit button does. a form with no button submits on enter. │ │384 + // an input outside a form has no submit button, so the TUI │ │385 + // handles its enter instead. │ │386 + const form = document.activeElement.form; │ │387 + if (form && ![...form.elements].some((element) => element.tagName === "BUTTON")) return;│ │388 event.preventDefault(); │ │389 if (!document.activeElement.form) { │ │390 wasmInstance.exports._onKeyDown(event.keyCode); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2071 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │2072 .author = author, │ │2073 .event = .{ .repo = event }, │ │2074 + }}); │ │2075 +} │ │2076 + │ │2077 +// grant `user_id` `role` on the repo `repo_id` in its owner's user repo, │ │2078 +// replacing any role they held │ │2079 +pub fn grantRole( │ │2080 + io: std.Io, │ │2081 + allocator: std.mem.Allocator, │ │2082 + users_dir: []const u8, │ │2083 + owner_id: *const [event_id_size]u8, │ │2084 + repo_id: *const [event_id_size]u8, │ │2085 + user_id: *const [event_id_size]u8, │ │2086 + role: Repo.Role, │ │2087 + author: CommitAuthor, │ │2088 +) !void { │ │2089 + var user_repo = (try openUserRepo(io, allocator, users_dir, owner_id)) orelse return error.NotFound; │ │2090 + defer user_repo.deinit(io, allocator); │ │2091 + try consume(.{ .server = .{ .users_dir = users_dir } }, .user, .xit, user_repo_opts, io, allocator, &user_repo, events_ref, &[_]EventWithId{.{ │ │2092 + .id = std.fmt.bytesToHex(Grant.idOf(repo_id, user_id), .lower), │ │2093 + .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │2094 + .author = author, │ │2095 + .event = .{ .grant = .{ .target_id = repo_id, .user_id = user_id, .role = role } }, │ │2096 }}); │ │2097 +} │ │2098 + │ │2099 +// revoke `user_id`'s grant on the repo `repo_id` in its owner's user repo │ │2100 +pub fn revokeRole( │ │2101 + io: std.Io, │ │2102 + allocator: std.mem.Allocator, │ │2103 + users_dir: []const u8, │ │2104 + owner_id: *const [event_id_size]u8, │ │2105 + repo_id: *const [event_id_size]u8, │ │2106 + user_id: *const [event_id_size]u8, │ │2107 + author: CommitAuthor, │ │2108 +) !void { │ │2109 + var user_repo = (try openUserRepo(io, allocator, users_dir, owner_id)) orelse return error.NotFound; │ │2110 + defer user_repo.deinit(io, allocator); │ │2111 + // a revoked grant stays in the kind index, which is all `remove` checks │ │2112 + { │ │2113 + var arena = std.heap.ArenaAllocator.init(allocator); │ │2114 + defer arena.deinit(); │ │2115 + const moment = (try userMoment(&user_repo)) orelse return error.EventNotFound; │ │2116 + if (try Grant.readRole(UserDB, user_repo_opts.hash, moment, &arena, repo_id, user_id) == null) return error.EventNotFound; │ │2117 + } │ │2118 + try remove(.{ .server = .{ .users_dir = users_dir } }, .user, .xit, user_repo_opts, io, allocator, &user_repo, &Grant.idOf(repo_id, user_id), .grant, author);│ │2119 } │ │2120 │ │2121 // create a user and their user repo, returning the new user's id │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 96 }; │ │ 97 }, │ │ 98 .repo => switch (route) { │ │ 99 - .repo_files, .repo_commits, .repo_diff, .repo_refs, .repo_issues, .repo_patches, .repo_discussions, .repo_events, .repo_undo, .repo_repo, .repo_repo_new, .repo_user_new, .repo_user => .{ .repo = try Repo.init(arena, session, route) }, │ │ 99 + .repo_files, .repo_commits, .repo_diff, .repo_refs, .repo_issues, .repo_patches, .repo_discussions, .repo_events, .repo_undo, .repo_settings, .repo_roles, .repo_repo_new, .repo_user_new, .repo_user => .{ .repo = try Repo.init(arena, session, route) },│ │100 else => return error.UnexpectedRoute, │ │101 }, │ │102 .fork => switch (route) { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────╮ │190 repo_undo: RepoUndoRoute, │ │191 repo_repo_new: Array(repo_route_max_len),│ │192 repo_user_new: Array(repo_route_max_len),│ │193 - repo_repo: Array(repo_route_max_len), │ │193 + repo_settings: Array(repo_route_max_len),│ │194 + repo_roles: RepoRolesRoute, │ │195 repo_user: Array(repo_route_max_len), │ │196 fork_patch: ForkRoute, │ │197 fork_diff: ForkDiffRoute, │ ╰──────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │222 │ │223 // the "key:value" segments of a url tail, collected in any order │ │224 const Params = struct { │ │224 - const ParamKey = enum { start, line, branch, tag, label, object, base, patchrev, theirs, find, search, from, moment, merge }; │ │225 + const ParamKey = enum { start, line, branch, tag, label, object, base, patchrev, theirs, find, search, from, moment, merge, user };│ │226 │ │227 // a branch:/tag:/object: param; the value stays url-encoded │ │228 const RefParam = struct { kind: RefOrOid, value: []const u8 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────╮ │316 const base_seg = @tagName(Params.ParamKey.base) ++ ":"; │ │317 const merge_seg = @tagName(Params.ParamKey.merge) ++ ":"; │ │318 const theirs_seg = @tagName(Params.ParamKey.theirs) ++ ":";│ │319 + const user_seg = @tagName(Params.ParamKey.user) ++ ":"; │ │320 const issue_seg = "issue:"; │ │321 const patch_seg = "patch:"; │ │322 const discuss_seg = "discuss:"; │ ╰────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │423 clear: bool = false, │ │424 }; │ │425 │ │424 - pub fn repoRepoRoute(identity: []const u8) ?RoutablePage { │ │425 - return .{ .repo_repo = Array(repo_route_max_len).from(identity) orelse return null }; │ │426 + pub const RepoRolesRoute = struct { │ │427 + name: Array(repo_identity_max_len), │ │428 + start: usize = 0, │ │429 + // the user a row's form acts on ("" = none) │ │430 + user: Array(evt.User.name_max_len) = .{}, │ │431 + }; │ │432 + │ │433 + pub fn repoSettingsRoute(identity: []const u8) ?RoutablePage { │ │434 + return .{ .repo_settings = Array(repo_route_max_len).from(identity) orelse return null }; │ │435 + } │ │436 + │ │437 + pub fn repoRolesRoute(identity: []const u8, start: usize) ?RoutablePage { │ │438 + return .{ .repo_roles = .{ .name = Array(repo_identity_max_len).from(identity) orelse return null, .start = start } };│ │439 } │ │440 │ │441 pub fn repoUndoRoute(identity: []const u8, index: ?u64) ?RoutablePage { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1251 }, │ │1252 .repo_repo_new => |name| try arena.allocator().print("{s}/repo/new", .{try repoUrlPrefix(arena, name.slice())}), │ │1253 .repo_user_new => |name| try arena.allocator().print("{s}/user/new", .{try repoUrlPrefix(arena, name.slice())}), │ │1241 - .repo_repo => |name| try arena.allocator().print("{s}/repo", .{try repoUrlPrefix(arena, name.slice())}), │ │1254 + .repo_settings => |name| try arena.allocator().print("{s}/repo/settings", .{try repoUrlPrefix(arena, name.slice())}),│ │1255 + .repo_roles => |r| blk: { │ │1256 + var out: std.Io.Writer.Allocating = .init(arena.allocator()); │ │1257 + try out.writer.print("{s}/repo/roles", .{try repoUrlPrefix(arena, r.name.slice())}); │ │1258 + if (r.user.len != 0) try out.writer.print("/" ++ user_seg ++ "{s}", .{r.user.slice()}); │ │1259 + if (r.start != 0) try out.writer.print("/" ++ start_seg ++ "{d}", .{r.start}); │ │1260 + break :blk out.written(); │ │1261 + }, │ │1262 .repo_user => |name| try arena.allocator().print("{s}/user", .{try repoUrlPrefix(arena, name.slice())}), │ │1263 .fork_patch => |f| try arena.allocator().print("/{s}+{s}", .{ f.name.slice(), f.id.slice() }), │ │1264 .fork_diff => |d| blk: { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1410 .repo_discussions => |*t| t.name.slice(), │ │1411 .repo_events => |*e| e.name.slice(), │ │1412 .repo_undo => |*u| u.name.slice(), │ │1393 - .repo_repo, .repo_repo_new, .repo_user_new, .repo_user => |*name| name.slice(), │ │1413 + .repo_roles => |*r| r.name.slice(), │ │1414 + .repo_settings, .repo_repo_new, .repo_user_new, .repo_user => |*name| name.slice(),│ │1415 else => null, │ │1416 }; │ │1417 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1454 return switch (self) { │ │1455 .home_about, .home_users, .home_repo_new, .home_user_new, .home_user => .home, │ │1456 .user_repos, .user_forks, .user_repo_new, .user_user_new, .user_user => .user, │ │1436 - .repo_files, .repo_commits, .repo_diff, .repo_refs, .repo_issues, .repo_patches, .repo_discussions, .repo_events, .repo_undo, .repo_repo, .repo_repo_new, .repo_user_new, .repo_user => .repo, │ │1457 + .repo_files, .repo_commits, .repo_diff, .repo_refs, .repo_issues, .repo_patches, .repo_discussions, .repo_events, .repo_undo, .repo_settings, .repo_roles, .repo_repo_new, .repo_user_new, .repo_user => .repo,│ │1458 .fork_patch, .fork_diff, .fork_files, .fork_commits, .fork_repo_new, .fork_user_new, .fork_user => .fork, │ │1459 .not_found => .not_found, │ │1460 }; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯