╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ───────────────╮ │src/test/event/patch.zig│ ╰────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │63 const event_tip = (try repo.readRef(io, evt.events_ref)) orelse return error.NotFound; │ │64 try pch.refreshBranches(.local, kind, opts, io, allocator, &repo, null); │ │65 + try std.testing.expectEqualStrings(&event_tip, &(try repo.readRef(io, evt.events_ref) orelse return error.NotFound)); │ │66 + │ │67 + // retargeting to the source leaves the patch and revision unchanged │ │68 + try std.testing.expectError(error.SameBranch, evt.Patch.update(.local, kind, opts, io, allocator, &repo, &id, .{ .fields = .{│ │69 + .title = patch.title, │ │70 + .tags = patch.tags, │ │71 + .description = patch.description, │ │72 + .target_branch = "feature", │ │73 + } }, author)); │ │74 try std.testing.expectEqualStrings(&event_tip, &(try repo.readRef(io, evt.events_ref) orelse return error.NotFound)); │ │75 │ │76 // a source update replaces the revision, not the patch metadata │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │323 .{ .tree = .{ .name = "head", .oid = &head_tree_oid } }, │ │324 }; │ │325 │ │326 + // reject invalid branches even when the event bypasses patch creation │ │327 + { │ │328 + const cases = [_]struct { source: ?[]const u8, target: []const u8, expected_error: anyerror }{ │ │329 + .{ .source = "master", .target = "master", .expected_error = error.SameBranch }, │ │330 + .{ .source = null, .target = evt.events_ref.name, .expected_error = error.InvalidTargetBranch }, │ │331 + }; │ │332 + for (cases) |case| { │ │333 + var invalid = patch; │ │334 + invalid.source_branch = case.source; │ │335 + invalid.target_branch = case.target; │ │336 + try std.testing.expectError(case.expected_error, evt.consume(.local, .repo, .xit, repo_opts, io, allocator, &target, evt.events_ref, &.{.{│ │337 + .id = std.fmt.bytesToHex(patch_id, .lower), │ │338 + .timestamp = 3, │ │339 + .author = author, │ │340 + .event = .{ .patch = invalid }, │ │341 + }})); │ │342 + try std.testing.expectEqual(null, try target.readRef(io, evt.events_ref)); │ │343 + } │ │344 + } │ │345 + │ │346 // │ │347 // consume the patch revision and patch │ │348 // │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────╮ │1713 required_title, │ │1714 invalid_target_branch, │ │1715 invalid_source_branch, │ │1716 + same_branch, │ │1717 unrelated_branches, │ │1718 │ │1719 pub fn fromError(err: anyerror) ?PatchFailure {│ ╰─────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │1720 return switch (err) { │ │1721 error.InvalidTargetBranch => .invalid_target_branch,│ │1722 error.InvalidSourceBranch => .invalid_source_branch,│ │1723 + error.SameBranch => .same_branch, │ │1724 error.UnrelatedBranches => .unrelated_branches, │ │1725 else => null, │ │1726 }; │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Patches.zig│ ╰───────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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; │ │297 + if (!evt.Patch.branchValid(target_branch) or !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; │ │376 - if (!try repo_source.hasBranch(io, allocator, target_branch)) return error.InvalidTargetBranch; │ │376 + if (!evt.Patch.branchValid(target_branch) or !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); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2139 (try formField(form, "title")).options.label = if (failure == .required_title) " title (required) " else " title "; │ │2140 if (comptime supports_drafts) { │ │2141 if (formWidget(form, .patch_source)) |source| { │ │2142 - source.field().options.label = if (failure == .invalid_source_branch) " source branch (not found) " else if (failure == .unrelated_branches) " source branch (unrelated) " else " source branch "; │ │2142 + source.field().options.label = if (failure == .invalid_source_branch) " source branch (not found) " else if (failure == .same_branch) " source branch (same as target) " else if (failure == .unrelated_branches) " source branch (unrelated) " else " source branch ";│ │2143 const submit = formWidget(form, .submit_button) orelse return error.MissingFormField; │ │2144 try submit.setLabel(allocator, if (source.existing) "submit patch" else "submit draft"); │ │2145 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2146 (try formField(form, "target_branch")).options.label = │ │2147 - if (failure == .invalid_target_branch) " target branch (not found) " else if (self.data.view == .edit and failure == .unrelated_branches) " target branch (unrelated to source) " else if (self.data.view == .edit and failure == .invalid_source_branch) " target branch (source branch not found) " else " target branch "; │ │2147 + if (failure == .invalid_target_branch) " target branch (invalid) " else if (self.data.view == .edit and failure == .same_branch) " target branch (same as source) " else if (self.data.view == .edit and failure == .unrelated_branches) " target branch (unrelated to source) " else if (self.data.view == .edit and failure == .invalid_source_branch) " target branch (source branch not found) " else " target branch ";│ │2148 } │ │2149 } │ │2150 if (self.formBox()) |form| { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │637 .local => return respondRemoveNotFound(request), │ │638 }; │ │639 const user_id = request_author.user_id orelse return respondLoginRequired(request); │ │640 - if (!try request_repo.source.hasBranch(io, allocator, target_branch)) { │ │640 + if (!evt.Patch.branchValid(target_branch) or !try request_repo.source.hasBranch(io, allocator, target_branch)) { │ │641 const form_location = try std.fmt.allocPrint(allocator, "{s}/patches/new", .{base}); │ │642 defer allocator.free(form_location); │ │643 return respondThreadFormFailure(request, allocator, host, form_location, patchFeedback(.invalid_target_branch, title, tags, description, target_branch, null));│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1445 }; │ │1446 const request_repo = (try requestRepoSource(io, allocator, host, parts.repo_base)) orelse return respondRemoveNotFound(request); │ │1447 defer request_repo.deinit(allocator); │ │1448 - if (!try request_repo.source.hasBranch(io, allocator, target_branch)) { │ │1448 + if (!evt.Patch.branchValid(target_branch) or !try request_repo.source.hasBranch(io, allocator, target_branch)) { │ │1449 const form_location = try std.fmt.allocPrint(allocator, "{s}/edit", .{base}); │ │1450 defer allocator.free(form_location); │ │1451 return respondThreadFormFailure(request, allocator, host, form_location, patchFeedback(.invalid_target_branch, title, tags, description, target_branch, null));│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯