╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │423 }; │ │424 } │ │425 │ │431 -fn invalidTargetBranchFeedback(title: []const u8, tags: []const u8, description: []const u8, target_branch: []const u8) ui.Session.FormFeedback { │ │432 - return .{ .patch = .{ .failure = .invalid_target_branch, .fields = .{ │ │426 +fn patchFeedback(failure: ui.Session.FormFeedback.PatchFailure, title: []const u8, tags: []const u8, description: []const u8, target_branch: []const u8, source_branch: ?[]const u8) ui.Session.FormFeedback {│ │427 + return .{ .patch = .{ .failure = failure, .fields = .{ │ │428 .title = title, │ │429 .tags = tags, │ │430 .description = description, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────╮ │431 .target_branch = target_branch, │ │432 + .source_branch = source_branch orelse "",│ │433 + .branch_source = source_branch != null, │ │434 } } }; │ │435 } │ │436 │ ╰──────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │588 defer allocator.free(description); │ │589 const target_branch = (try parseFormField(allocator, body, "target_branch")) orelse try allocator.dupe(u8, "");│ │590 defer allocator.free(target_branch); │ │591 + │ │592 + const source_branch = if (kind == .patch) try parseFormField(allocator, body, "source_branch") else null; │ │593 + defer if (source_branch) |branch| allocator.free(branch); │ │594 │ │595 const valid = switch (kind) { │ │596 .issue => evt.Issue.fieldsValid(title, tags), │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │608 const form_location = try std.fmt.allocPrint(allocator, "{s}/{s}/new", .{ base, list_name }); │ │609 defer allocator.free(form_location); │ │610 if (!evt.titleValid(title)) { │ │611 - return respondThreadFormFailure(request, allocator, host, form_location, requiredTitleFeedback(kind, title, tags, description, target_branch)); │ │611 + return respondThreadFormFailure(request, allocator, host, form_location, requiredTitleFeedback(kind, title, tags, description, target_branch, source_branch));│ │612 } │ │613 try request.respond("", .{ │ │614 .status = .see_other, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │621 io.random(&id_bytes); │ │622 const event_id_hex = std.fmt.bytesToHex(id_bytes, .lower); │ │623 │ │624 - if (kind == .patch) { │ │624 + const not_found = "repo not found"; │ │625 + const request_repo = (try requestRepoSource(io, allocator, host, base)) orelse {│ │626 + try request.respond(not_found, .{ │ │627 + .status = .not_found, │ │628 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }},│ │629 + }); │ │630 + return; │ │631 + }; │ │632 + defer request_repo.deinit(allocator); │ │633 + const source = request_repo.source; │ │634 + if (kind == .patch and source_branch == null) { │ │635 const server = switch (host) { │ │636 .server => |server| server, │ │637 .local => return respondRemoveNotFound(request), │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │638 }; │ │639 const user_id = request_author.user_id orelse return respondLoginRequired(request); │ │630 - const request_repo = (try requestRepoSource(io, allocator, host, base)) orelse return respondRemoveNotFound(request);│ │631 - defer request_repo.deinit(allocator); │ │640 if (!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); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │635 - return respondThreadFormFailure(request, allocator, host, form_location, invalidTargetBranchFeedback(title, tags, description, target_branch)); │ │643 + return respondThreadFormFailure(request, allocator, host, form_location, patchFeedback(.invalid_target_branch, title, tags, description, target_branch, null));│ │644 } │ │645 const repo_id = evt.parseEventId(std.fs.path.basename(request_repo.source.path)) catch return respondRemoveNotFound(request); │ │646 var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = server.admin_repo_path }); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │659 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │660 }); │ │661 defer allocator.free(fork_path); │ │654 - const location = try std.fmt.allocPrint(allocator, "{s}/patch:{s}", .{ base, &event_id_hex }); │ │655 - defer allocator.free(location); │ │656 - try request.respond("", .{ .status = .see_other, .extra_headers = &.{.{ .name = "location", .value = location }} }); │ │657 - return; │ │658 - } │ │659 - │ │660 - const event = evt.EventWithId{ │ │661 - .id = event_id_hex, │ │662 - .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │663 - .author = author, │ │664 - .event = switch (kind) { │ │665 - .issue => .{ .issue = .{ .title = title, .description = description, .tags = tags } }, │ │666 - .patch => unreachable, │ │667 - .discuss => .{ .discuss = .{ .title = title, .description = description, .tags = tags } }, │ │668 - else => unreachable, │ │669 - }, │ │670 - }; │ │662 + } else { │ │663 + const event = evt.EventWithId{ │ │664 + .id = event_id_hex, │ │665 + .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │666 + .author = author, │ │667 + .event = switch (kind) { │ │668 + .issue => .{ .issue = .{ .title = title, .description = description, .tags = tags } }, │ │669 + .patch => .{ .patch = .{ .title = title, .description = description, .tags = tags, .target_branch = target_branch, .source_branch = source_branch } }, │ │670 + .discuss => .{ .discuss = .{ .title = title, .description = description, .tags = tags } }, │ │671 + else => unreachable, │ │672 + }, │ │673 + }; │ │674 │ │672 - const not_found = "repo not found"; │ │673 - const request_repo = (try requestRepoSource(io, allocator, host, base)) orelse { │ │674 - try request.respond(not_found, .{ │ │675 - .status = .not_found, │ │676 - .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │677 - }); │ │678 - return; │ │679 - }; │ │680 - defer request_repo.deinit(allocator); │ │681 - const source = request_repo.source; │ │682 - switch (source.repo_kind) { │ │683 - inline else => |repo_kind| { │ │684 - var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, source.localInitOpts()); │ │685 - defer any_repo.deinit(io, allocator); │ │686 - switch (any_repo) { │ │687 - inline else => |*repo| try evt.consume(std.meta.activeTag(host), .repo, repo_kind, repo.self_repo_opts, io, allocator, repo, evt.events_ref, &[_]evt.EventWithId{event}), │ │688 - } │ │689 - }, │ │675 + switch (source.repo_kind) { │ │676 + inline else => |repo_kind| { │ │677 + var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, source.localInitOpts()); │ │678 + defer any_repo.deinit(io, allocator); │ │679 + switch (any_repo) { │ │680 + inline else => |*repo| { │ │681 + const result = if (kind == .patch) │ │682 + pch.writeBranchPatch(std.meta.activeTag(host), repo_kind, repo.self_repo_opts, io, allocator, repo, event_id_hex, event.event.patch orelse unreachable, null, author)│ │683 + else │ │684 + evt.consume(std.meta.activeTag(host), .repo, repo_kind, repo.self_repo_opts, io, allocator, repo, evt.events_ref, &.{event}); │ │685 + result catch |err| { │ │686 + if (kind != .patch) return err; │ │687 + const failure = ui.Session.FormFeedback.PatchFailure.fromError(err) orelse return err; │ │688 + const form_location = try std.fmt.allocPrint(allocator, "{s}/patches/new", .{base}); │ │689 + defer allocator.free(form_location); │ │690 + return respondThreadFormFailure(request, allocator, host, form_location, patchFeedback(failure, title, tags, description, target_branch, source_branch)); │ │691 + }; │ │692 + }, │ │693 + } │ │694 + }, │ │695 + } │ │696 } │ │697 │ │698 const location = try std.fmt.allocPrint(allocator, "{s}/{s}:{s}", .{ base, @tagName(kind), &event_id_hex }); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1428 const form_location = try std.fmt.allocPrint(allocator, "{s}/edit", .{base}); │ │1429 defer allocator.free(form_location); │ │1430 if (!evt.titleValid(title)) { │ │1425 - return respondThreadFormFailure(request, allocator, host, form_location, requiredTitleFeedback(parts.thread_kind, title, tags, description, target_branch)); │ │1431 + return respondThreadFormFailure(request, allocator, host, form_location, requiredTitleFeedback(parts.thread_kind, title, tags, description, target_branch, null));│ │1432 } │ │1433 try request.respond("", .{ │ │1434 .status = .see_other, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1448 if (!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); │ │1445 - return respondThreadFormFailure(request, allocator, host, form_location, invalidTargetBranchFeedback(title, tags, description, target_branch)); │ │1451 + return respondThreadFormFailure(request, allocator, host, form_location, patchFeedback(.invalid_target_branch, title, tags, description, target_branch, null));│ │1452 } │ │1453 const repo_id = evt.parseEventId(std.fs.path.basename(request_repo.source.path)) catch return respondRemoveNotFound(request); │ │1454 var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = server.admin_repo_path }); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯