╭ changed ──────────╮ │src/event/Issue.zig│ ╰───────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────╮ │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( │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯