╭──────────╮ │← previous│ ╰──────────╯ ╭ added ─────────────────╮ │src/ui/Repo/Branches.zig│ ╰────────────────────────╯ ╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭──────────────────────────────────────────────────╮ │67 repo_settings: ui.Repo.Settings.View, │ │68 repo_settings_form: ui.Repo.Settings.Form,│ │69 repo_members: ui.Repo.Members.View, │ │70 + repo_branches: ui.Repo.Branches.View, │ │71 repo_comment: ui.Repo.Comment.Item, │ │72 copyable_text: CopyableText, │ │73 patch_source: ui.Repo.Patches.Source, │ ╰──────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │120 // whether moving up should return to the page header │ │121 pub fn atTop(self: *Widget, root_focus: *Focus) bool { │ │122 return switch (self.*) { │ │122 - inline .home_about, .home_users, .user_repos, .flow_box_scroll, .new_repo, .new_user, .user_login, .user_notifications, .quit, .repo_files, .repo_commits, .diff_view, .repo_refs, .repo_issues, .repo_patches, .repo_discussions, .repo_events, .repo_undo, .repo_settings, .repo_settings_form, .repo_members, .user_keys => |*view| view.atTop(), │ │123 + inline .home_about, .home_users, .user_repos, .flow_box_scroll, .new_repo, .new_user, .user_login, .user_notifications, .quit, .repo_files, .repo_commits, .diff_view, .repo_refs, .repo_issues, .repo_patches, .repo_discussions, .repo_events, .repo_undo, .repo_settings, .repo_settings_form, .repo_members, .repo_branches, .user_keys => |*view| view.atTop(),│ │124 inline .repo_patch_detail, .user_settings => |*view| view.atTop(root_focus), │ │125 else => false, │ │126 }; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │159 switch (host) { │ │160 .server => |server| { │ │161 // "repo/new" and "user/new" come before "new", which would claim them │ │162 - const PostRoute = enum { login, logout, @"repo/new", @"user/new", user, addkey, removekey, settings, grant, revoke, promote, demote, new, edit, remove, open, close, resolve, publish, merge, squash, attach, assign, unassign, subscribe, mute, undo, clear }; │ │162 + const PostRoute = enum { login, logout, @"repo/new", @"user/new", user, addkey, removekey, settings, grant, revoke, promote, demote, protect, unprotect, new, edit, remove, open, close, resolve, publish, merge, squash, attach, assign, unassign, subscribe, mute, undo, clear };│ │163 inline for (@typeInfo(PostRoute).@"enum".field_names) |name| { │ │164 const suffix = "/" ++ name; │ │165 if (std.mem.endsWith(u8, path, suffix)) { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │177 .revoke => handleMembers(io, request, allocator, base, host, .revoke), │ │178 .promote => handleMembers(io, request, allocator, base, host, .promote), │ │179 .demote => handleMembers(io, request, allocator, base, host, .demote), │ │180 + .protect => handleBranches(io, request, allocator, base, host, .protect), │ │181 + .unprotect => handleBranches(io, request, allocator, base, host, .unprotect),│ │182 .new => handleNew(io, request, allocator, base, host), │ │183 .edit => handleEdit(io, request, allocator, base, host), │ │184 .remove => handleRemove(io, request, allocator, base, host), │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │801 return respondFormFailure(request, allocator, server.session_store, location, .{ .repo_members = .{ .failure = value, .name = name } }); │ │802 } │ │803 const location = try (ui.Repo.Members.landingRoute(identity, target.start, actor, action, name) orelse return error.RouteTooLong).toUrl(&arena); │ │804 + try request.respond("", .{ .status = .see_other, .keep_alive = false, .extra_headers = &.{.{ .name = "location", .value = location }} }); │ │805 +} │ │806 + │ │807 +// protect or unprotect a branch of the repo the branches url `base` names, as │ │808 +// one of its owners, then redirect back to the list. the add form names the │ │809 +// branch in its body and the row forms in the url. │ │810 +fn handleBranches(io: std.Io, request: *std.http.Server.Request, allocator: std.mem.Allocator, base: []const u8, host: Host, action: ui.Repo.Branches.Action) !void { │ │811 + const server = switch (host) { │ │812 + .server => |server| server, │ │813 + .local => return respondRepoNotFound(request), │ │814 + }; │ │815 + const route = routeFromUrl(host, base) orelse return respondRepoNotFound(request); │ │816 + if (route != .repo_branches) return respondRepoNotFound(request); │ │817 + const target = route.repo_branches; │ │818 + var arena = std.heap.ArenaAllocator.init(allocator); │ │819 + defer arena.deinit(); │ │820 + const identity = target.name.slice(); │ │821 + const actor = (try authorizeWrite(io, allocator, &arena, request, host, try arena.allocator().print("/{s}", .{identity}), .owner, null)) orelse return; │ │822 + │ │823 + const branch = switch (action) { │ │824 + .protect => blk: { │ │825 + const body = try readFormBody(request, allocator); │ │826 + defer allocator.free(body); │ │827 + break :blk (try parseFormField(arena.allocator(), body, "name")) orelse ""; │ │828 + }, │ │829 + .unprotect => if (target.branch.len != 0) std.Uri.percentDecodeInPlace(try arena.allocator().dupe(u8, target.branch.slice())) else return respondRepoNotFound(request),│ │830 + }; │ │831 + │ │832 + const failure = ui.Repo.Branches.perform(io, allocator, server.users_dir, actor, identity, action, branch) catch |err| switch (err) { │ │833 + error.NotFound => return respondRepoNotFound(request), │ │834 + else => return err, │ │835 + }; │ │836 + const location = try (ui.RoutablePage.repoBranchesRoute(identity) orelse return error.RouteTooLong).toUrl(&arena); │ │837 + if (failure) |value| return respondFormFailure(request, allocator, server.session_store, location, .{ .repo_branches = .{ .failure = value, .name = branch } }); │ │838 try request.respond("", .{ .status = .see_other, .keep_alive = false, .extra_headers = &.{.{ .name = "location", .value = location }} }); │ │839 } │ │840 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯