╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │140 if (method == .POST) { │ │141 switch (host) { │ │142 .server => |server| { │ │143 - const PostRoute = enum { login, logout, ansi, new, edit, remove, open, close, resolve, publish, merge, squash, attach, undo, clear }; │ │143 + const PostRoute = enum { login, logout, ansi, @"new-repo", new, edit, remove, open, close, resolve, publish, merge, squash, attach, undo, clear };│ │144 inline for (@typeInfo(PostRoute).@"enum".fields) |field| { │ │145 const suffix = "/" ++ field.name; │ │146 if (std.mem.endsWith(u8, path, suffix)) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │149 .login => handleLogin(io, request, allocator, base, server.admin_repo_path, server.session_store), │ │150 .logout => handleLogout(request, base, server.session_store), │ │151 .ansi => handleAnsi(io, request, allocator, base, server.admin_repo_path, server.users_dir, server.session_store), │ │152 + .@"new-repo" => handleRepoNew(io, request, allocator, path, server.admin_repo_path, server.users_dir, server.session_store),│ │153 .new => handleNew(io, request, allocator, base, host), │ │154 .edit => handleEdit(io, request, allocator, base, host), │ │155 .remove => handleRemove(io, request, allocator, base, host), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │633 .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │634 }); │ │635 } │ │636 + │ │637 +// create a repo for the logged-in user and redirect to it. a failure goes │ │638 +// back to the new-repo page that posted the form. │ │639 +fn handleRepoNew( │ │640 + io: std.Io, │ │641 + request: *std.http.Server.Request, │ │642 + allocator: std.mem.Allocator, │ │643 + form_location: []const u8, │ │644 + admin_repo_path: []const u8, │ │645 + users_dir: []const u8, │ │646 + session_store: SessionStore, │ │647 +) !void { │ │648 + const user_id = requestUserId(request, session_store) orelse return respondLoginRequired(request); │ │649 + var arena = std.heap.ArenaAllocator.init(allocator); │ │650 + defer arena.deinit(); │ │651 + const user = blk: { │ │652 + var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_repo_path }); │ │653 + defer admin_repo.deinit(io, allocator); │ │654 + const moment = try evt.currentMoment(evt.admin_repo_opts, &admin_repo); │ │655 + break :blk (try ui.activeUser(moment, &arena, user_id)) orelse return respondLoginRequired(request); │ │656 + }; │ │657 │ │658 + const body = try readFormBody(request, allocator); │ │659 + defer allocator.free(body); │ │660 + const name = (try parseFormField(allocator, body, "name")) orelse try allocator.dupe(u8, ""); │ │661 + defer allocator.free(name); │ │662 + const description = (try parseFormField(allocator, body, "description")) orelse try allocator.dupe(u8, ""); │ │663 + defer allocator.free(description); │ │664 + const hash_value = (try parseFormField(allocator, body, "hash")) orelse try allocator.dupe(u8, ""); │ │665 + defer allocator.free(hash_value); │ │666 + const access_value = (try parseFormField(allocator, body, "access")) orelse try allocator.dupe(u8, ""); │ │667 + defer allocator.free(access_value); │ │668 + const bad_request: std.http.Server.Request.RespondOptions = .{ .status = .bad_request, .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }} }; │ │669 + const hash_kind = std.meta.stringToEnum(hash.HashKind, hash_value) orelse return request.respond("invalid hash", bad_request); │ │670 + const access = std.meta.stringToEnum(evt.Repo.Access, access_value) orelse return request.respond("invalid access", bad_request); │ │671 + │ │672 + _ = evt.createRepo(io, allocator, users_dir, &user_id, .{ .name = user.event.name, .email = user.event.email }, name, description, access, hash_kind) catch |err| {│ │673 + const failure = ui.Session.FormFeedback.RepoFailure.fromError(err) orelse return err; │ │674 + return respondFormFailure(request, allocator, session_store, form_location, .{ .repo = .{ .failure = failure, .fields = .{ │ │675 + .name = name, │ │676 + .description = description, │ │677 + .hash = hash_kind, │ │678 + .access = access, │ │679 + } } }); │ │680 + }; │ │681 + │ │682 + const location = try std.fmt.allocPrint(allocator, "/repo/{s}/{s}", .{ user.event.name, name }); │ │683 + defer allocator.free(location); │ │684 + try request.respond("", .{ │ │685 + .status = .see_other, │ │686 + .extra_headers = &.{.{ .name = "location", .value = location }}, │ │687 + }); │ │688 +} │ │689 + │ │690 // create a thread in the repo the form's page names and redirect to it. │ │691 fn handleThreadNew( │ │692 io: std.Io, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │872 const request_repo = (try requestRepoSource(io, allocator, host, parts.repo_base)) orelse return respondRemoveNotFound(request);│ │873 defer request_repo.deinit(allocator); │ │874 const repo_id = evt.parseEventId(std.fs.path.basename(request_repo.source.path)) catch return respondRemoveNotFound(request); │ │821 - var target_repo = try rp.Repo(.xit, .{}).open(io, allocator, request_repo.source.localInitOpts()); │ │875 + var target_repo = try rp.AnyRepo(.xit, .{}).open(io, allocator, request_repo.source.localInitOpts()); │ │876 defer target_repo.deinit(io, allocator); │ │877 var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = server.admin_repo_path }); │ │878 defer admin_repo.deinit(io, allocator); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │879 const id = std.fmt.bytesToHex(parts.thread_id, .lower); │ │826 - try pch.publish(.{}, io, allocator, server.users_dir, &admin_repo, &target_repo, .{ │ │827 - .id = id, │ │828 - .user_id = user_id, │ │829 - .repo_id = repo_id, │ │830 - .author = author, │ │831 - .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │832 - }); │ │880 + switch (target_repo) { │ │881 + inline else => |*repo| try pch.publish(repo.self_repo_opts, io, allocator, server.users_dir, &admin_repo, repo, .{│ │882 + .id = id, │ │883 + .user_id = user_id, │ │884 + .repo_id = repo_id, │ │885 + .author = author, │ │886 + .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │887 + }), │ │888 + } │ │889 try request.respond("", .{ │ │890 .status = .see_other, │ │891 .extra_headers = &.{.{ .name = "location", .value = base }}, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │912 │ │913 const request_repo = (try requestRepoSource(io, allocator, host, parts.repo_base)) orelse return respondRemoveNotFound(request);│ │914 defer request_repo.deinit(allocator); │ │859 - var target_repo = try rp.Repo(.xit, .{}).open(io, allocator, request_repo.source.localInitOpts()); │ │915 + var target_repo = try rp.AnyRepo(.xit, .{}).open(io, allocator, request_repo.source.localInitOpts()); │ │916 defer target_repo.deinit(io, allocator); │ │917 │ │918 const id = std.fmt.bytesToHex(parts.thread_id, .lower); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │863 - pch.mergeAndRemoveFork(.{}, io, allocator, server.users_dir, &target_repo, .{ │ │864 - .id = id, │ │865 - .revision = revision, │ │866 - .author = author, │ │867 - .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │868 - }) catch |err| switch (err) { │ │919 + const merged = switch (target_repo) { │ │920 + inline else => |*repo| pch.mergeAndRemoveFork(repo.self_repo_opts, io, allocator, server.users_dir, repo, .{│ │921 + .id = id, │ │922 + .revision = revision, │ │923 + .author = author, │ │924 + .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │925 + }), │ │926 + }; │ │927 + merged catch |err| switch (err) { │ │928 error.MergeConflict => return request.respond("merge conflict", .{ │ │929 .status = .conflict, │ │930 .keep_alive = false, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯