╭ changed ────╮ │build.zig.zon│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 3 .version = "0.0.0", │ │ 4 .dependencies = .{ │ │ 5 .xit = .{ │ │ 6 - .url = "git+https://github.com/xit-vcs/xit#97a26b03ff4d6b08627ca6281b63b917dc78258c",│ │ 7 - .hash = "xit-0.0.0-Ub3nDB1RHQB9m6S3qseYL1FAtLIqgbxXLiRdidwHht-U", │ │ 6 + .url = "git+https://github.com/xit-vcs/xit#380d7e4fbea97f0c50c58836d9f62e68be95fc10",│ │ 7 + .hash = "xit-0.0.0-Ub3nDBpTHQCRGhTyvfdmQmnVURvN5qIE1QKC_oaMWT6m", │ │ 8 }, │ │ 9 }, │ │10 .paths = .{ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │414 var sideband = progress.Sideband{ .writer = &writer.interface, .sess = sess }; │ │415 try repo.uploadPack(io, allocator, &reader.interface, &writer.interface, .{ .protocol_version = protocol_version }, &sideband); │ │416 }, │ │417 - .push => |pusher| try push.receivePackAndConsume(repo.self_repo_opts, io, allocator, repo, &reader.interface, &writer.interface, .{ .protocol_version = protocol_version, .protected_branches = pusher.protected_branches }, pusher.author, handler.server, handler.err, sess), │ │417 + .push => |pusher| try push.receivePackAndConsume(repo.self_repo_opts, io, allocator, repo, &reader.interface, &writer.interface, .{ .protocol_version = protocol_version, .protected_branches = pusher.protected_branches, .locked_branches = pusher.locked_branches }, pusher.author, handler.server, handler.err, sess),│ │418 }, │ │419 } │ │420 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────╮ │559 const Pusher = struct { │ │560 author: evt.CommitAuthor, │ │561 protected_branches: []const []const u8,│ │562 + locked_branches: []const []const u8, │ │563 }; │ │564 │ │565 // the owner of a repo a push may create │ ╰────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │601 // any key may fetch a public repo; a push always needs a key holder │ │602 if (service == .upload_pack and repo.role != null) return .{ .allowed = .fetch }; │ │603 │ │603 - // a key can be shared, so a holder whose role falls short doesn't end the search │ │604 + // a key can be shared, so the push acts as the holder with the highest role │ │605 const owner_id = try evt.parseEventId(&repo.repo.event.user_id); │ │606 + var best: ?struct { id: [evt.event_id_size]u8, role: evt.Repo.Role } = null; │ │607 for (try evt.User.readIdsByFingerprint(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, fingerprint)) |holder_id| { │ │608 const seen = (try evt.readRepoById(io, allocator, arena, users_dir, &owner_id, &repo.event_id, holder_id)) orelse return .not_found;│ │609 const role = seen.role orelse continue; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 - } }; │ │610 + if (best) |held| if (held.role.atLeast(role)) continue; │ │611 + best = .{ .id = holder_id, .role = role }; │ │612 } │ │617 - return .denied; │ │613 + const holder = best orelse return .denied; │ │614 + if (!holder.role.atLeast(min_role)) return .denied; │ │615 + return .{ │ │616 + .allowed = switch (service) { │ │617 + .upload_pack => .fetch, │ │618 + .receive_pack => blk: { │ │619 + const names = try repo.repo.event.protectedBranchNames(arena.allocator()); │ │620 + // members change protected branches only by merging a patch │ │621 + const owner = holder.role == .owner; │ │622 + break :blk .{ .push = .{ │ │623 + .author = try commitAuthor(moment, arena, &holder.id), │ │624 + .protected_branches = if (owner) names else &.{}, │ │625 + .locked_branches = if (owner) &.{} else names, │ │626 + } }; │ │627 + }, │ │628 + }, │ │629 + }; │ │630 } │ │631 │ │632 // an active user as a commit author │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/ui/Repo/Branches.zig│ ╰────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │74 data: *const Self, │ │75 session: *ui.Session, │ │76 │ │77 - const add_index = 0; │ │77 + const about_index = 0; │ │78 + const add_index = 1; │ │79 │ │80 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !View {│ │81 const aa = session.page_arena.allocator(); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 85 const url = try route.toUrl(session.page_arena); │ │ 86 │ │ 87 { │ │ 87 - // the gap leaves a blank line between the text and the input │ │ 88 - var add = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .vert, .gap = 1 }); │ │ 88 + var about = try ui.widget.initAbout(allocator, &.{ │ │ 89 + .{ .text = "protected branches can't be deleted or force pushed.\n" }, │ │ 90 + .{ .text = "owners", .style = .{ .bold = true } }, │ │ 91 + .{ .text = " can push directly to them.\n" }, │ │ 92 + .{ .text = "members", .style = .{ .bold = true } }, │ │ 93 + .{ .text = " can only change them by merging a patch." }, │ │ 94 + }); │ │ 95 + errdefer about.deinit(allocator); │ │ 96 + try box.children.put(allocator, about.getFocus().id, .{ .widget = .{ .box = about }, .rect = null, .min_size = null }); │ │ 97 + } │ │ 98 + { │ │ 99 + var add = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .horiz }); │ │100 errdefer add.deinit(allocator); │ │101 add.getFocus().kind = .{ .custom = try aa.print("form:{s}/protect", .{url}) }; │ │ 91 - { │ │ 92 - var about = try ui.widget.RichText.init(allocator, &.{ │ │ 93 - .{ .text = "protected branches can't be deleted or force pushed." }, │ │ 94 - }); │ │ 95 - errdefer about.deinit(allocator); │ │ 96 - try add.children.put(allocator, about.getFocus().id, .{ .widget = .{ .rich_text = about }, .rect = null, .min_size = null }); │ │ 97 - } │ │102 var name = try wgt.TextInput.init(allocator, .{ .top_label = .{ .text = " add a protected branch " }, .name = "name", .visible_width = 30, .round_corners = true, .render_content = session.is_terminal });│ │103 errdefer name.deinit(allocator); │ │104 name.getFocus().mode = .all; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │105 if (session.formFeedback(.repo_branches)) |saved| try name.setContent(allocator, saved.name); │ │106 add.getFocus().child_id = name.getFocus().id; │ │107 try add.children.put(allocator, name.getFocus().id, .{ .widget = .{ .text_input = name }, .rect = null, .min_size = .{ .width = null, .height = 3 } });│ │104 - try box.children.put(allocator, add.getFocus().id, .{ .widget = .{ .box = add }, .rect = null, .min_size = .{ .width = null, .height = 5 } }); │ │108 + try box.children.put(allocator, add.getFocus().id, .{ .widget = .{ .box = add }, .rect = null, .min_size = .{ .width = null, .height = 3 } }); │ │109 } │ │110 │ │111 // the branch rows follow the add row in the same box │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │135 try box.children.put(allocator, row_box.getFocus().id, .{ .widget = .{ .box = row_box }, .rect = null, .min_size = null });│ │136 } │ │137 │ │134 - box.getFocus().child_id = box.children.keys()[add_index]; │ │138 + box.getFocus().child_id = box.children.keys()[about_index]; │ │139 var center = try ui.widget.Center.init(allocator, .{ .box = box }); │ │140 errdefer center.deinit(allocator); │ │141 return .{ │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────╮ │158 } │ │159 │ │160 fn nameInput(self: *View) *wgt.TextInput { │ │157 - return &self.addBox().children.values()[1].widget.text_input;│ │161 + return &self.addBox().children.values()[0].widget.text_input;│ │162 } │ │163 │ │164 fn addActive(self: *View) bool { │ ╰──────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯