╭ changed ──────────────╮ │src/ui/Repo/Patches.zig│ ╰───────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 15 const pch = @import("../../patch.zig"); │ │ 16 │ │ 17 const wasm = builtin.target.cpu.arch == .wasm32; │ │ 18 + │ │ 19 +pub const Source = struct { │ │ 20 + box: wgt.Box(ui.Widget), │ │ 21 + session: *ui.Session, │ │ 22 + existing: bool, │ │ 23 + input_index: usize, │ │ 24 + │ │ 25 + pub fn init(allocator: std.mem.Allocator, session: *ui.Session, initial: []const u8, existing: bool) !Source { │ │ 26 + var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │ 27 + errdefer box.deinit(allocator); │ │ 28 + const labels = [_][]const u8{ "from a new fork", "from an existing branch" }; │ │ 29 + for (labels, 0..) |label, i| { │ │ 30 + if (session.data.host_kind == .local) { │ │ 31 + if (i == 0) continue; │ │ 32 + } │ │ 33 + var selector = try wgt.TextBox.init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │ 34 + errdefer selector.deinit(allocator); │ │ 35 + selector.getFocus().mode = .all; │ │ 36 + try box.children.put(allocator, selector.getFocus().id, .{ .widget = .{ .text_box = selector }, .rect = null, .min_size = .{ .width = label.len + 2, .height = 3 } }); │ │ 37 + } │ │ 38 + const input_index = box.children.count(); │ │ 39 + var input_box = try wgt.TextInput.init(allocator, .{ .label = " source branch ", .name = "source_branch", .rounded_corners = true, .visible_width = null, .render_content = session.is_terminal });│ │ 40 + errdefer input_box.deinit(allocator); │ │ 41 + input_box.getFocus().mode = .all; │ │ 42 + try input_box.setContent(allocator, initial); │ │ 43 + try box.children.put(allocator, input_box.getFocus().id, .{ .widget = .{ .text_input = input_box }, .rect = null, .min_size = .{ .width = 15, .height = 3 } }); │ │ 44 + box.getFocus().child_id = box.children.keys()[if (existing) input_index - 1 else 0]; │ │ 45 + return .{ .box = box, .session = session, .existing = existing or input_index == 1, .input_index = input_index }; │ │ 46 + } │ │ 47 + │ │ 48 + pub fn field(self: *Source) *wgt.TextInput { │ │ 49 + return &self.box.children.values()[self.input_index].widget.text_input; │ │ 50 + } │ │ 51 + pub fn deinit(self: *Source, allocator: std.mem.Allocator) void { │ │ 52 + self.box.deinit(allocator); │ │ 53 + } │ │ 54 + pub fn clearGrid(self: *Source) void { │ │ 55 + self.box.clearGrid(); │ │ 56 + } │ │ 57 + pub fn getGrid(self: Source) ?xitui.grid.Grid { │ │ 58 + return self.box.getGrid(); │ │ 59 + } │ │ 60 + pub fn getFocus(self: *Source) *xitui.focus.Focus { │ │ 61 + return self.box.getFocus(); │ │ 62 + } │ │ 63 + │ │ 64 + pub fn build(self: *Source, allocator: std.mem.Allocator, constraint: xitui.layout.Constraint, root_focus: *xitui.focus.Focus) !void { │ │ 65 + self.clearGrid(); │ │ 66 + self.box.children.values()[self.input_index].hidden = !self.existing; │ │ 67 + for (self.box.children.values()[0..self.input_index], 0..) |*child, i| { │ │ 68 + child.widget.text_box.options.border_style = if ((i == self.input_index - 1) == self.existing) .single else .hidden; │ │ 69 + } │ │ 70 + try self.box.build(allocator, constraint, root_focus); │ │ 71 + if (!self.session.is_terminal) { │ │ 72 + try self.session.text_inputs.put(self.session.arena.allocator(), self.field().getFocus().id, self.field()); │ │ 73 + } │ │ 74 + } │ │ 75 │ │ 76 + pub fn input(self: *Source, allocator: std.mem.Allocator, key: xitui.input.Key, root_focus: *xitui.focus.Focus) !void { │ │ 77 + const current = self.box.getFocus().child_id orelse return; │ │ 78 + const index = self.box.children.getIndex(current) orelse return; │ │ 79 + if (index == self.input_index and key != .arrow_left) return self.field().input(allocator, key, root_focus); │ │ 80 + switch (key) { │ │ 81 + .arrow_left => if (index > 0) { │ │ 82 + self.existing = index == self.input_index; │ │ 83 + root_focus.setFocus(self.box.children.keys()[index - 1]); │ │ 84 + }, │ │ 85 + .arrow_right => { │ │ 86 + self.existing = true; │ │ 87 + root_focus.setFocus(self.box.children.keys()[index + 1]); │ │ 88 + }, │ │ 89 + .enter => { │ │ 90 + self.existing = index == self.input_index - 1; │ │ 91 + if (self.existing) root_focus.setFocus(self.field().getFocus().id); │ │ 92 + }, │ │ 93 + .mouse => |mouse| { │ │ 94 + for (self.box.children.keys()[0..self.input_index], 0..) |id, i| { │ │ 95 + if (@import("../input.zig").leftClickOn(root_focus, id, mouse)) self.existing = i == self.input_index - 1; │ │ 96 + } │ │ 97 + }, │ │ 98 + else => {}, │ │ 99 + } │ │100 + } │ │101 +}; │ │102 + │ │103 // how many patches one window shows before a "next" link appears. │ │104 pub const page_size = 20; │ │105 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────╮ │118 comments: Comment.Window = .empty, │ │119 attachments: []const Attachment.WithId = &.{},│ │120 draft: bool = false, │ │ 37 - fork_oid: []const u8 = "", │ │121 + revision_oid: []const u8 = "", │ │122 no_changes: bool = false, │ │123 fork_exists: bool = false, │ │124 mergeability: pch.Mergeability = .{}, │ ╰───────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │246 │ │247 pub fn canMerge(entry: Entry, session: *const ui.Session) bool { │ │248 return !entry.draft and │ │165 - entry.fork_exists and │ │249 + (entry.fork_exists or entry.record.event.source_branch != null) and│ │250 !entry.conflicted and │ │251 entry.record.event.revision != null and │ │252 entry.record.event.status.kind() == .open and │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────╮ │254 session.data.user_id != null;│ │255 } │ │256 │ │173 -pub fn createDraft( │ │257 +pub fn create( │ │258 data: *const Self, │ │259 session: *ui.Session, │ │260 allocator: std.mem.Allocator, │ ╰──────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │262 tags: []const u8, │ │263 description: []const u8, │ │264 target_branch: []const u8, │ │265 + source_branch: ?[]const u8, │ │266 ) ![evt.event_id_size * 2]u8 { │ │267 if (!evt.Patch.fieldsValid(title, tags)) return error.InvalidFields;│ │268 const io = session.io orelse return error.NotFound; │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │184 - const repos_dir = session.repos_dir orelse return error.NotFound; │ │185 - const admin_repo = session.admin_repo orelse return error.NotFound; │ │186 - const user_id = session.userId() orelse return error.NotFound; │ │187 - const repo_id = data.repo_id orelse return error.NotFound; │ │269 const repo_source = data.repo_source orelse return error.NotFound; │ │270 const author = (try session.eventAuthor()) orelse return error.NotFound; │ │190 - if (!try repo_source.hasBranch(io, allocator, target_branch)) return error.InvalidFields;│ │271 var id_bytes: [evt.event_id_size]u8 = undefined; │ │272 io.random(&id_bytes); │ │273 const id = std.fmt.bytesToHex(id_bytes, .lower); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │274 + if (source_branch) |branch| { │ │275 + switch (repo_source.repo_kind) { │ │276 + inline else => |repo_kind| { │ │277 + var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, repo_source.localInitOpts()); │ │278 + defer any_repo.deinit(io, allocator); │ │279 + switch (any_repo) { │ │280 + inline else => |*repo| try pch.writeBranchPatch(session.data.host_kind, repo_kind, repo.self_repo_opts, io, allocator, repo, id, .{│ │281 + .title = title, │ │282 + .tags = tags, │ │283 + .description = description, │ │284 + .target_branch = target_branch, │ │285 + .source_branch = branch, │ │286 + }, null, author), │ │287 + } │ │288 + }, │ │289 + } │ │290 + return id; │ │291 + } │ │292 + if (session.data.host_kind == .local) return error.InvalidSourceBranch; │ │293 + const repos_dir = session.repos_dir orelse return error.NotFound; │ │294 + const admin_repo = session.admin_repo orelse return error.NotFound; │ │295 + const user_id = session.userId() orelse return error.NotFound; │ │296 + const repo_id = data.repo_id orelse return error.NotFound; │ │297 + if (!try repo_source.hasBranch(io, allocator, target_branch)) return error.InvalidTargetBranch; │ │298 const fork_path = try fork.create(.{}, io, allocator, repos_dir, admin_repo, .{ │ │299 .id = id, │ │300 .user_id = user_id, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │373 const user_id = session.userId() orelse return error.NotFound; │ │374 const patch_id = evt.parseEventId(id) catch return error.NotFound; │ │375 const author = (try session.eventAuthor()) orelse return error.NotFound; │ │272 - if (!try repo_source.hasBranch(io, allocator, target_branch)) return error.InvalidFields; │ │376 + if (!try repo_source.hasBranch(io, allocator, target_branch)) return error.InvalidTargetBranch;│ │377 │ │378 const id_hex = std.fmt.bytesToHex(patch_id, .lower); │ │379 const fork_path = try fork.forkPath(allocator, repos_dir, &id_hex); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │505 theirs_picks: []const u8, │ │506 view: ui.RoutablePage.PatchesView, │ │507 ) !Self { │ │404 - const allowed_view: ui.RoutablePage.PatchesView = if (session.local != null and (view == .new or view == .drafts)) .open else view;│ │508 + const allowed_view: ui.RoutablePage.PatchesView = if (session.local != null and view == .drafts) .open else view; │ │509 var empty = try emptyResult(arena.allocator(), identity, tag, selected_id, comment_id, comments_start, theirs_picks, allowed_view);│ │510 empty.default_target_branch = try arena.allocator().dupe(u8, target_branch); │ │511 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │650 var closed_window = try thread.loadWindow(Self, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_patch, closed_set, closed_root, conflict_set, empty.selected_id, thread_comments_start); │ │651 var merged_window = try thread.loadWindow(Self, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_patch, merged_set, merged_root, conflict_set, empty.selected_id, thread_comments_start); │ │652 var conflicts_window = try thread.loadWindow(Self, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_patch, conflict_set, conflicts_root, conflict_set, empty.selected_id, thread_comments_start);│ │549 - try setForkDetails(repo_kind, repo_opts, io, arena, admin_moment, repo, &open_window); │ │550 - try setForkDetails(repo_kind, repo_opts, io, arena, admin_moment, repo, &closed_window); │ │551 - try setForkDetails(repo_kind, repo_opts, io, arena, admin_moment, repo, &merged_window); │ │552 - try setForkDetails(repo_kind, repo_opts, io, arena, admin_moment, repo, &conflicts_window); │ │653 + try setPatchDetails(repo_kind, repo_opts, io, arena, admin_moment, repo, &open_window); │ │654 + try setPatchDetails(repo_kind, repo_opts, io, arena, admin_moment, repo, &closed_window); │ │655 + try setPatchDetails(repo_kind, repo_opts, io, arena, admin_moment, repo, &merged_window); │ │656 + try setPatchDetails(repo_kind, repo_opts, io, arena, admin_moment, repo, &conflicts_window); │ │657 if (view == .conflicts and conflicts_window.count > 0) resolved_view = .conflicts; │ │658 │ │659 const comment_page = if (empty.comment_id.len == 0) │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯