╭ changed ──────────╮ │src/event/Issue.zig│ ╰───────────────────╯ ╭───────────────────────────────────────╮ │1 const std = @import("std"); │ │2 const evt = @import("../event.zig");│ │3 const xit = @import("xit"); │ │4 +const rp = xit.repo; │ │5 const hash = xit.hash; │ │6 │ │7 title: []const u8, │ ╰───────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │35 return std.mem.splitScalar(u8, tags, ' '); │ │36 } │ │37 │ │38 +// a title is required, and a too-long tag would fail consumption after the│ │39 +// event is already committed │ │40 +pub fn fieldsValid(title: []const u8, tags: []const u8) bool { │ │41 + if (title.len == 0) return false; │ │42 + var tag_iter = tagIterator(tags); │ │43 + while (tag_iter.next()) |tag| { │ │44 + if (tag.len > tag_max_len) return false; │ │45 + } │ │46 + return true; │ │47 +} │ │48 + │ │49 // `created_ts` comes from the commit timestamp, not the event │ │50 // payload, so it must not appear in the JSON we output │ │51 pub fn jsonStringify(self: @This(), jw: anytype) !void { │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │143 ) !DB.SortedSet(.read_write) { │ │144 const cursor = try statuses.putCursor(@tagName(status)); │ │145 return DB.SortedSet(.read_write).init(cursor); │ │146 +} │ │147 + │ │148 +// what an update replaces on an issue: its status, or its content │ │149 +pub const Update = union(enum) { │ │150 + status: Status, │ │151 + fields: struct { title: []const u8, tags: []const u8, description: []const u8 }, │ │152 +}; │ │153 + │ │154 +// re-emit the issue `id_bytes` names with `change` applied, preserving the │ │155 +// rest of its current state. error.NotFound when the id names no issue. │ │156 +// `repo` must be writable. │ │157 +pub fn update( │ │158 + comptime repo_kind: rp.RepoKind, │ │159 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │160 + io: std.Io, │ │161 + allocator: std.mem.Allocator, │ │162 + repo: *rp.Repo(repo_kind, repo_opts), │ │163 + id_bytes: *const [evt.event_id_size]u8, │ │164 + change: Update, │ │165 +) !void { │ │166 + var arena = std.heap.ArenaAllocator.init(allocator); │ │167 + defer arena.deinit(); │ │168 + │ │169 + const issue = (try readById(repo_kind, repo_opts, io, allocator, &arena, repo, id_bytes)) orelse return error.NotFound; │ │170 + │ │171 + var updated = issue; │ │172 + switch (change) { │ │173 + .status => |status| { │ │174 + if (issue.status == status) return; │ │175 + updated.status = status; │ │176 + }, │ │177 + .fields => |fields| { │ │178 + updated.title = fields.title; │ │179 + updated.tags = fields.tags; │ │180 + updated.description = fields.description; │ │181 + }, │ │182 + } │ │183 + │ │184 + try evt.commitAndConsume(repo_kind, repo_opts, io, allocator, repo, evt.events_ref, &[_]evt.EventWithId{.{ │ │185 + .id = std.fmt.bytesToHex(id_bytes.*, .lower), │ │186 + .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │187 + .event = .{ .issue = updated }, │ │188 + }}); │ │189 } │ │190 │ │191 +// the issue `id_bytes` names in the repo's consumed event db (the repo's own │ │192 +// db for xit; the side db next to a git repo), or null │ │193 +fn readById( │ │194 + comptime repo_kind: rp.RepoKind, │ │195 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │196 + io: std.Io, │ │197 + allocator: std.mem.Allocator, │ │198 + arena: *std.heap.ArenaAllocator, │ │199 + repo: *rp.Repo(repo_kind, repo_opts), │ │200 + id_bytes: *const [evt.event_id_size]u8, │ │201 +) !?@This() { │ │202 + const DB = evt.EventDB(repo_opts.hash); │ │203 + 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;│ │204 + defer if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); │ │205 + const moment = (if (event_db_maybe) |*event_db| │ │206 + evt.currentMomentFromDb(repo_opts.hash, event_db.db) │ │207 + else if (repo_kind == .git) │ │208 + return null │ │209 + else │ │210 + evt.currentMoment(repo_opts, repo)) catch return null; │ │211 + const event_id_to_issue_cursor = (try moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue"))) orelse return null; │ │212 + const event_id_to_issue = try DB.HashMap(.read_only).init(event_id_to_issue_cursor); │ │213 + const issue_cursor = (try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, id_bytes))) orelse return null; │ │214 + const issue_map = try DB.HashMap(.read_only).init(issue_cursor); │ │215 + return try evt.read(@This(), DB, repo_opts.hash, arena, issue_map); │ │216 +} │ │217 + │ │218 // remove an issue's order key from its tags' `status` sets, pruning entries │ │219 // whose set becomes empty │ │220 fn removeFromTagSets( │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │1087 const description = try description_input.text(allocator); │ │1088 defer allocator.free(description); │ │1089 │ │1090 - // a title is required, and a too-long tag would fail consumption│ │1091 - // after the event is already committed │ │1092 - if (title.len == 0) return; │ │1093 - var tag_iter = evt.Issue.tagIterator(tags); │ │1094 - while (tag_iter.next()) |tag| { │ │1095 - if (tag.len > evt.Issue.tag_max_len) return; │ │1096 - } │ │1090 + if (!evt.Issue.fieldsValid(title, tags)) return; │ │1091 │ │1092 var id_bytes: [evt.event_id_size]u8 = undefined; │ │1093 io.random(&id_bytes); │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │1143 const description = try description_input.text(allocator); │ │1144 defer allocator.free(description); │ │1145 │ │1152 - // a title is required, and a too-long tag would fail consumption │ │1153 - // after the event is already committed │ │1154 - if (title.len == 0) return; │ │1155 - var tag_iter = evt.Issue.tagIterator(tags); │ │1156 - while (tag_iter.next()) |tag| { │ │1157 - if (tag.len > evt.Issue.tag_max_len) return; │ │1158 - } │ │1159 - │ │1160 - var updated = entry.issue; │ │1161 - updated.title = title; │ │1162 - updated.tags = tags; │ │1163 - updated.description = description; │ │1146 + if (!evt.Issue.fieldsValid(title, tags)) return; │ │1147 │ │1165 - const event = evt.EventWithId{ │ │1166 - .id = entry.id[0 .. evt.event_id_size * 2].*, │ │1167 - .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │1168 - .event = .{ .issue = updated }, │ │1169 - }; │ │1148 + var id_bytes: [evt.event_id_size]u8 = undefined; │ │1149 + _ = try std.fmt.hexToBytes(&id_bytes, entry.id[0 .. evt.event_id_size * 2]);│ │1150 │ │1151 switch (src.repo_kind) { │ │1152 inline else => |repo_kind| { │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1153 var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, .{ .path = src.path }); │ │1154 defer any_repo.deinit(io, allocator); │ │1155 switch (any_repo) { │ │1176 - inline else => |*repo| try evt.commitAndConsume(repo_kind, repo.self_repo_opts, io, allocator, repo, evt.events_ref, &.{event}),│ │1156 + inline else => |*repo| try evt.Issue.update(repo_kind, repo.self_repo_opts, io, allocator, repo, &id_bytes, .{ .fields = .{ │ │1157 + .title = title, │ │1158 + .tags = tags, │ │1159 + .description = description, │ │1160 + } }), │ │1161 } │ │1162 }, │ │1163 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1164 │ │1181 - const route = ui.RoutablePage.repoIssuesRoute(self.data.identity, updated.status, "", entry.id) orelse return; │ │1165 + const route = ui.RoutablePage.repoIssuesRoute(self.data.identity, entry.issue.status, "", entry.id) orelse return;│ │1166 try self.session.navigate(route); │ │1167 } │ │1168 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────╮ │1176 const sel = self.detailed_index[index] orelse return; │ │1177 const entry = self.window(index).issues[sel]; │ │1178 │ │1195 - var updated = entry.issue; │ │1196 - updated.status = switch (entry.issue.status) { │ │1179 + const status: evt.Issue.Status = switch (entry.issue.status) {│ │1180 .open => .closed, │ │1181 .closed => .open, │ │1182 }; │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │1183 │ │1201 - const event = evt.EventWithId{ │ │1202 - .id = entry.id[0 .. evt.event_id_size * 2].*, │ │1203 - .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │1204 - .event = .{ .issue = updated }, │ │1205 - }; │ │1184 + var id_bytes: [evt.event_id_size]u8 = undefined; │ │1185 + _ = try std.fmt.hexToBytes(&id_bytes, entry.id[0 .. evt.event_id_size * 2]);│ │1186 │ │1187 switch (src.repo_kind) { │ │1188 inline else => |repo_kind| { │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1189 var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, .{ .path = src.path }); │ │1190 defer any_repo.deinit(io, allocator); │ │1191 switch (any_repo) { │ │1212 - inline else => |*repo| try evt.commitAndConsume(repo_kind, repo.self_repo_opts, io, allocator, repo, evt.events_ref, &.{event}), │ │1192 + inline else => |*repo| try evt.Issue.update(repo_kind, repo.self_repo_opts, io, allocator, repo, &id_bytes, .{ .status = status }),│ │1193 } │ │1194 }, │ │1195 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯