╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1196 │ │1217 - const route = ui.RoutablePage.repoIssuesRoute(self.data.identity, updated.status, "", entry.id) orelse return;│ │1197 + const route = ui.RoutablePage.repoIssuesRoute(self.data.identity, status, "", entry.id) orelse return; │ │1198 try self.session.navigate(route); │ │1199 } │ │1200 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭──────────────────────────────────────────────────────╮ │1 const std = @import("std"); │ │2 const xit = @import("xit"); │ │3 const rp = xit.repo; │ │4 -const hash = xit.hash; │ │4 const evt = @import("./event.zig"); │ │5 const serve_common = @import("./serve_common.zig");│ │6 const ui = @import("./ui.zig"); │ ╰──────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │371 const description = try std.mem.replaceOwned(u8, allocator, description_crlf, "\r\n", "\n");│ │372 defer allocator.free(description); │ │373 │ │375 - if (!issueFieldsValid(title, tags)) { │ │374 + if (!evt.Issue.fieldsValid(title, tags)) { │ │375 const form_location = try std.fmt.allocPrint(allocator, "{s}/issues/new", .{base}); │ │376 defer allocator.free(form_location); │ │377 try request.respond("", .{ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │454 }); │ │455 } │ │456 │ │458 -// a title is required, and a too-long tag would fail consumption after the │ │459 -// event is already committed │ │460 -fn issueFieldsValid(title: []const u8, tags: []const u8) bool { │ │461 - if (title.len == 0) return false; │ │462 - var tag_iter = evt.Issue.tagIterator(tags); │ │463 - while (tag_iter.next()) |tag| { │ │464 - if (tag.len > evt.Issue.tag_max_len) return false; │ │465 - } │ │466 - return true; │ │467 -} │ │468 - │ │457 // split an issue url ("/repo/<owner>/<name>/issues/<id>", identity elided in│ │458 // local mode) into what precedes "/issues/" and the decoded id, null when it│ │459 // isn't one. the repo base is validated per host in updateIssue. │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │469 return .{ .repo_base = head[0 .. head.len - issues_infix.len], .id_bytes = id_bytes };│ │470 } │ │471 │ │484 -// what an update route replaces on an issue: its status, or its content. │ │485 -const IssueUpdate = union(enum) { │ │486 - status: evt.Issue.Status, │ │487 - fields: struct { title: []const u8, tags: []const u8, description: []const u8 }, │ │488 -}; │ │489 - │ │472 // re-emit the issue `parts` names with `update` applied, in the repo the │ │473 // host resolves the base to. error.NotFound when the base names no repo or │ │474 // the id no issue. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────╮ │477 allocator: std.mem.Allocator,│ │478 host: Host, │ │479 parts: IssueBaseParts, │ │498 - update: IssueUpdate, │ │480 + update: evt.Issue.Update, │ │481 ) !void { │ │482 switch (host) { │ │483 .remote => |remote| { │ ╰──────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │493 │ │494 var repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = repo_path }); │ │495 defer repo.deinit(io, allocator); │ │514 - try updateIssueInRepo(.xit, .{}, io, allocator, &repo, &parts.id_bytes, update);│ │496 + try evt.Issue.update(.xit, .{}, io, allocator, &repo, &parts.id_bytes, update); │ │497 }, │ │498 .local => |src| { │ │499 // the local forms post to "/issues/<id>/...", so the repo base │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │504 var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, .{ .path = src.path }); │ │505 defer any_repo.deinit(io, allocator); │ │506 switch (any_repo) { │ │525 - inline else => |*repo| try updateIssueInRepo(repo_kind, repo.self_repo_opts, io, allocator, repo, &parts.id_bytes, update),│ │507 + inline else => |*repo| try evt.Issue.update(repo_kind, repo.self_repo_opts, io, allocator, repo, &parts.id_bytes, update), │ │508 } │ │509 }, │ │510 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │511 }, │ │512 } │ │513 } │ │532 - │ │533 -fn updateIssueInRepo( │ │534 - comptime repo_kind: rp.RepoKind, │ │535 - comptime repo_opts: rp.RepoOpts(repo_kind), │ │536 - io: std.Io, │ │537 - allocator: std.mem.Allocator, │ │538 - repo: *rp.Repo(repo_kind, repo_opts), │ │539 - id_bytes: *const [evt.event_id_size]u8, │ │540 - update: IssueUpdate, │ │541 -) !void { │ │542 - var arena = std.heap.ArenaAllocator.init(allocator); │ │543 - defer arena.deinit(); │ │514 │ │545 - const issue = (try readRepoIssue(repo_kind, repo_opts, io, allocator, &arena, repo, id_bytes)) orelse return error.NotFound; │ │546 - │ │547 - var updated = issue; │ │548 - switch (update) { │ │549 - .status => |status| { │ │550 - if (issue.status == status) return; │ │551 - updated.status = status; │ │552 - }, │ │553 - .fields => |fields| { │ │554 - updated.title = fields.title; │ │555 - updated.tags = fields.tags; │ │556 - updated.description = fields.description; │ │557 - }, │ │558 - } │ │559 - │ │560 - try evt.commitAndConsume(repo_kind, repo_opts, io, allocator, repo, evt.events_ref, &[_]evt.EventWithId{.{ │ │561 - .id = std.fmt.bytesToHex(id_bytes.*, .lower), │ │562 - .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │563 - .event = .{ .issue = updated }, │ │564 - }}); │ │565 -} │ │566 - │ │567 -// the issue `id_bytes` names in the repo's consumed event db (the repo's own │ │568 -// db for xit; the side db next to a git repo), or null │ │569 -fn readRepoIssue( │ │570 - comptime repo_kind: rp.RepoKind, │ │571 - comptime repo_opts: rp.RepoOpts(repo_kind), │ │572 - io: std.Io, │ │573 - allocator: std.mem.Allocator, │ │574 - arena: *std.heap.ArenaAllocator, │ │575 - repo: *rp.Repo(repo_kind, repo_opts), │ │576 - id_bytes: *const [evt.event_id_size]u8, │ │577 -) !?evt.Issue { │ │578 - const DB = evt.EventDB(repo_opts.hash); │ │579 - var event_db_maybe: ?evt.LocalEventDB(repo_opts.hash) = if (repo_kind == .git) try evt.LocalEventDB(repo_opts.hash).openReadOnly(io, allocator, repo.core.repo_dir) else null;│ │580 - defer if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); │ │581 - const moment = (if (event_db_maybe) |*event_db| │ │582 - evt.currentMomentFromDb(repo_opts.hash, event_db.db) │ │583 - else if (repo_kind == .git) │ │584 - return null │ │585 - else │ │586 - evt.currentMoment(repo_opts, repo)) catch return null; │ │587 - const event_id_to_issue_cursor = (try moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue"))) orelse return null; │ │588 - const event_id_to_issue = try DB.HashMap(.read_only).init(event_id_to_issue_cursor); │ │589 - const issue_cursor = (try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, id_bytes))) orelse return null; │ │590 - const issue_map = try DB.HashMap(.read_only).init(issue_cursor); │ │591 - return try evt.read(evt.Issue, DB, repo_opts.hash, arena, issue_map); │ │592 -} │ │593 - │ │515 // set the status of the issue the url names (base is │ │516 // "/repo/<owner>/<name>/issues/<id>", identity elided in local mode) by │ │517 // re-emitting its event, then redirect back to it │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │580 defer allocator.free(description); │ │581 │ │582 // invalid fields send the user back to the edit form │ │662 - if (!issueFieldsValid(title, tags)) { │ │583 + if (!evt.Issue.fieldsValid(title, tags)) { │ │584 const form_location = try std.fmt.allocPrint(allocator, "{s}/edit", .{base});│ │585 defer allocator.free(form_location); │ │586 try request.respond("", .{ │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯