╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1502 var segments = std.mem.splitScalar(u8, sub, '/'); │ │1503 const tab = segments.next() orelse return null; │ │1504 var params = Params{}; │ │1484 - if (std.mem.eql(u8, tab, "repo")) return if (segments.peek() == null) .{ .repo_repo = Array(repo_route_max_len).from(pair) orelse return null } else if (isNewTail(&segments)) .{ .repo_repo_new = Array(repo_route_max_len).from(pair) orelse return null } else null;│ │1505 + if (std.mem.eql(u8, tab, "repo")) { │ │1506 + const word = segments.peek() orelse return null; │ │1507 + if (std.mem.eql(u8, word, "settings")) { │ │1508 + _ = segments.next(); │ │1509 + return if (segments.next() == null) repoSettingsRoute(pair) else null; │ │1510 + } │ │1511 + if (std.mem.eql(u8, word, "roles")) { │ │1512 + _ = segments.next(); │ │1513 + const roles_params = listParams(&segments, &.{ .user, .start }) orelse return null; │ │1514 + var route = repoRolesRoute(pair, roles_params.start() orelse return null) orelse return null; │ │1515 + if (roles_params.values.get(.user)) |user| route.repo_roles.user = Array(evt.User.name_max_len).from(user) orelse return null; │ │1516 + return route; │ │1517 + } │ │1518 + return if (isNewTail(&segments)) .{ .repo_repo_new = Array(repo_route_max_len).from(pair) orelse return null } else null; │ │1519 + } │ │1520 if (std.mem.eql(u8, tab, "user")) return if (segments.peek() == null) .{ .repo_user = Array(repo_route_max_len).from(pair) orelse return null } else if (isNewTail(&segments)) .{ .repo_user_new = Array(repo_route_max_len).from(pair) orelse return null } else null;│ │1521 if (std.mem.eql(u8, tab, "refs")) { │ │1522 const word = params.scanTail(&segments) catch return null; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1829 std.mem.eql(u8, a_e.selected.slice(), b.repo_events.selected.slice()), │ │1830 .repo_repo_new => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_repo_new.slice()), │ │1831 .repo_user_new => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_user_new.slice()), │ │1797 - .repo_repo => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_repo.slice()), │ │1832 + .repo_settings => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_settings.slice()), │ │1833 + .repo_roles => |a_r| std.mem.eql(u8, a_r.name.slice(), b.repo_roles.name.slice()) and │ │1834 + a_r.start == b.repo_roles.start and │ │1835 + std.mem.eql(u8, a_r.user.slice(), b.repo_roles.user.slice()), │ │1836 .repo_user => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_user.slice()), │ │1837 .fork_patch => |a_f| std.mem.eql(u8, a_f.name.slice(), b.fork_patch.name.slice()) and std.mem.eql(u8, a_f.id.slice(), b.fork_patch.id.slice()),│ │1838 .fork_diff => |a_d| std.mem.eql(u8, a_d.fork.name.slice(), b.fork_diff.fork.name.slice()) and │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────╮ │2248 }; │ │2249 } │ │2250 }; │ │2251 + │ │2252 + pub const RolesFailure = enum { not_found, already_added };│ │2253 │ │2254 pub const UserFailure = enum { │ │2255 required_name, │ ╰─────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────╮ │2312 issue_role: ?evt.Repo.Role,│ │2313 patch_role: ?evt.Repo.Role,│ │2314 }, │ │2315 + }, │ │2316 + repo_roles: struct { │ │2317 + failure: RolesFailure, │ │2318 + name: []const u8, │ │2319 }, │ │2320 issue: struct { │ │2321 failure: ThreadFailure, │ ╰─────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭────────────────────────────────────────────────────────╮ │25 pub const Undo = @import("./Repo/Undo.zig"); │ │26 pub const Events = @import("./Repo/Events.zig"); │ │27 pub const Settings = @import("./Repo/Settings.zig");│ │28 +pub const Roles = @import("./Repo/Roles.zig"); │ │29 pub const Quit = @import("./Quit.zig"); │ │30 │ │31 header: Header, │ ╰────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │221 .repo_events => |*e| e.selected.slice(), │ │222 else => "", │ │223 }; │ │224 + const roles_start: usize = switch (route) { │ │225 + .repo_roles => |r| r.start, │ │226 + else => 0, │ │227 + }; │ │228 │ │229 // where the on-disk repo lives (null keeps the views' empty fallback), plus│ │230 // the repo and owner-name metadata the header shows. local mode already │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │312 try evt.consume(.local, .repo, repo_kind, opened.self_repo_opts, io, gpa, opened, evt.events_ref, &.{}); │ │313 try pch.refreshBranches(.local, repo_kind, opened.self_repo_opts, io, gpa, opened, null, null); │ │314 } │ │310 - if (session.data.host_kind == .server and handle.canUndo()) settings = .{ │ │311 - .identity = try arena.allocator().dupe(u8, repo_identity.identity), │ │312 - .name = repo.event.name, │ │313 - .description = repo.event.description, │ │314 - .access = repo.event.read_access, │ │315 - .discuss_role = repo.event.discuss_role, │ │316 - .issue_role = repo.event.issue_role, │ │317 - .patch_role = repo.event.patch_role, │ │318 - .hash_kind = opened.self_repo_opts.hash, │ │319 - }; │ │315 + if (session.data.host_kind == .server and handle.canUndo()) { │ │316 + // a server page always resolved its repo id │ │317 + const repo_id = repo_id_maybe orelse unreachable; │ │318 + settings = .{ │ │319 + .identity = try arena.allocator().dupe(u8, repo_identity.identity), │ │320 + .name = repo.event.name, │ │321 + .description = repo.event.description, │ │322 + .access = repo.event.read_access, │ │323 + .discuss_role = repo.event.discuss_role, │ │324 + .issue_role = repo.event.issue_role, │ │325 + .patch_role = repo.event.patch_role, │ │326 + .hash_kind = opened.self_repo_opts.hash, │ │327 + .view = if (route == .repo_roles) .roles else .settings, │ │328 + .roles = try Roles.init(io, gpa, arena, session.haxy_moment orelse return error.NoMoment, session.users_dir orelse return error.NoMoment, repo_identity.identity, repo.event.user_id[0..evt.event_id_size], &repo_id, roles_start),│ │329 + }; │ │330 + } │ │331 // tabs switch in-page, so every tab's data is read here │ │332 if (repo_kind == .xit and handle.canWrite()) { │ │333 undo_data = Undo.init(opened.self_repo_opts, arena, opened, session.haxy_moment, repo_identity.identity, undo_index, handle) catch |err| switch (err) { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │379 }; │ │380 }; │ │381 if (route == .repo_undo and undo_data == null) return error.NotFound; │ │371 - if (route == .repo_repo and settings == null) return error.NotFound; │ │382 + if ((route == .repo_settings or route == .repo_roles) and settings == null) return error.NotFound;│ │383 if (undo_data) |*undo| undo.clear = undo_clear; │ │384 discussions.repo_source = source; │ │385 issues.repo_source = source; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │591 .repo_refs => |*v| if (v.focusHeader(root_focus)) return, │ │592 .repo_events => |*v| if (v.focusHeader(root_focus)) return, │ │593 .repo_undo => |*v| if (v.focusHeader(root_focus)) return, │ │594 + .repo_settings => |*v| if (v.focusHeader(root_focus)) return,│ │595 else => {}, │ │596 }; │ │597 index = stack_index; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Header.zig│ ╰──────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │301 } │ │302 │ │303 if (page.settings != null) { │ │304 - const route = ui.RoutablePage.repoRepoRoute(identity) orelse return error.RouteTooLong; │ │304 + const route = ui.RoutablePage.repoSettingsRoute(identity) orelse return error.RouteTooLong; │ │305 var text_box = try wgt.TextBox.init(allocator, ui.Repo.Settings.tab_label, .{ .border = .single, .round_corners = true, .wrap_kind = .none });│ │306 errdefer text_box.deinit(allocator); │ │307 text_box.getFocus().mode = .all; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯