╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────╮ │10 pub const Repo = @import("event/Repo.zig"); │ │11 pub const Issue = @import("event/Issue.zig"); │ │12 pub const Comment = @import("event/Comment.zig"); │ │13 +pub const Attachment = @import("event/Attachment.zig");│ │14 │ │15 pub const event_id_size: usize = 16; │ │16 │ ╰───────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────╮ │54 return result; │ │55 } │ │56 │ │57 +// kept to eight bytes or fewer, so the db stores them as short bytes│ │58 pub const EventKind = enum { │ │59 user, │ │60 repo, │ ╰─────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────╮ │61 issue, │ │62 comment, │ │63 + attach, │ │64 }; │ │65 │ │66 // every logical event's kind and creation-ordered id set│ ╰─────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │73 repo: ?Repo, │ │74 issue: ?Issue, │ │75 comment: ?Comment, │ │76 + attach: ?Attachment, │ │77 +}; │ │78 + │ │79 +// a file committed alongside its event, as the one entry in that commit's tree.│ │80 +// it isn't serialized, so nothing about it reaches the commit message. │ │81 +pub const Blob = struct { │ │82 + name: []const u8, │ │83 + size: u64, │ │84 + reader: *std.Io.Reader, │ │85 }; │ │86 │ │87 // who a commit is attributed to │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────╮ │ 95 event: Event, │ │ 96 timestamp: u64 = 0, // not serialized, because it comes from the commit timestamp │ │ 97 author: CommitAuthor, // not serialized, because it goes in the commit author line│ │ 98 + blob: ?Blob = null, // not serialized, because it goes in the commit tree │ │ 99 │ │100 pub fn jsonStringify(self: EventWithId, jw: anytype) !void { │ │101 try jw.beginObject(); │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │143 .comment => .{ │ │144 .comment = if (json_event.data) |value| │ │145 try std.json.parseFromValueLeaky(Comment, arena.allocator(), value, .{ .ignore_unknown_fields = true }) │ │146 + else │ │147 + null, │ │148 + }, │ │149 + .attach => .{ │ │150 + .attach = if (json_event.data) |value| │ │151 + try std.json.parseFromValueLeaky(Attachment, arena.allocator(), value, .{ .ignore_unknown_fields = true })│ │152 else │ │153 null, │ │154 }, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │334 return true; │ │335 } │ │336 } │ │318 - │ │319 - var local_commit = try obj.Object(repo_kind, repo_opts).initCommit(state.readOnly(), io, allocator, &local_oid); │ │320 - defer local_commit.deinit(); │ │321 - var remote_commit = try obj.Object(repo_kind, repo_opts).initCommit(state.readOnly(), io, allocator, &remote_oid); │ │322 - defer remote_commit.deinit(); │ │323 - if (!std.mem.eql(u8, &local_commit.content.commit.tree, &remote_commit.content.commit.tree)) return error.EventTreeConflict;│ │337 │ │338 const parent_oids = [_][hash.hexLen(repo_opts.hash)]u8{ local_oid, remote_oid }; │ │339 + var tree = try obj.Tree.init(allocator); │ │340 + defer tree.deinit(); │ │341 _ = try obj.writeCommit(repo_kind, repo_opts, state, io, allocator, .{ │ │342 .message = "merge events", │ │343 .parent_oids = &parent_oids, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │329 - }, null, events_ref); │ │344 + }, &tree, events_ref); │ │345 return true; │ │346 +} │ │347 + │ │348 +// the file an attach event carries: the one entry in its commit's tree. the │ │349 +// name and oid are allocated in `arena`. │ │350 +fn attachedFile( │ │351 + comptime repo_kind: rp.RepoKind, │ │352 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │353 + state: rp.Repo(repo_kind, repo_opts).State(.read_only), │ │354 + io: std.Io, │ │355 + allocator: std.mem.Allocator, │ │356 + arena: *std.heap.ArenaAllocator, │ │357 + tree_oid: *const [hash.hexLen(repo_opts.hash)]u8, │ │358 +) !struct { name: []const u8, oid: []const u8 } { │ │359 + var tree_object = try obj.Object(repo_kind, repo_opts).init(state, io, allocator, tree_oid); │ │360 + defer tree_object.deinit(); │ │361 + │ │362 + const entries = switch (tree_object.content) { │ │363 + .tree => |tree| tree.entries, │ │364 + else => return error.InvalidAttachment, │ │365 + }; │ │366 + if (entries.count() != 1) return error.InvalidAttachment; │ │367 + const entry = entries.values()[0]; │ │368 + if (entry.isTree()) return error.InvalidAttachment; │ │369 + const name = entries.keys()[0]; │ │370 + if (!Attachment.nameValid(name)) return error.InvalidAttachment; │ │371 + │ │372 + const oid = std.fmt.bytesToHex(entry.oid, .lower); │ │373 + var object_reader = try obj.ObjectReader(repo_kind, repo_opts).init(state, io, allocator, &oid);│ │374 + defer object_reader.deinit(); │ │375 + if (object_reader.header().kind != .blob) return error.InvalidAttachment; │ │376 + │ │377 + return .{ │ │378 + .name = try arena.allocator().dupe(u8, name), │ │379 + .oid = try arena.allocator().dupe(u8, &oid), │ │380 + }; │ │381 } │ │382 │ │383 // commit each event as a JSON commit message on `ref` through `state`, so a │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │403 if (json.written().len > max_event_size) return error.EventTooLarge; │ │404 const author = try std.fmt.allocPrint(allocator, "{s} <{s}>", .{ event.author.name, event.author.email }); │ │405 defer allocator.free(author); │ │356 - _ = try obj.writeCommit(repo_kind, repo_opts, state, io, allocator, .{ .author = author, .message = json.written(), .timestamp = event.timestamp, .parent_oids = parent_oids }, null, ref);│ │406 + │ │407 + // every event gets its own tree, with one entry when it carries a file │ │408 + // and no entries otherwise │ │409 + var tree = try obj.Tree.init(allocator); │ │410 + defer tree.deinit(); │ │411 + if (event.blob) |blob| { │ │412 + var oid_bytes = [_]u8{0} ** hash.byteLen(repo_opts.hash); │ │413 + try obj.writeObject(repo_kind, repo_opts, state, io, blob.reader, .{ .kind = .blob, .size = blob.size }, &oid_bytes); │ │414 + try tree.addBlobEntry(.{ .content = .{ .unix_permission = 0o644, .object_type = .regular_file } }, blob.name, &oid_bytes); │ │415 + } │ │416 + │ │417 + _ = try obj.writeCommit(repo_kind, repo_opts, state, io, allocator, .{ │ │418 + .author = author, │ │419 + .message = json.written(), │ │420 + .timestamp = event.timestamp, │ │421 + .parent_oids = parent_oids, │ │422 + .allow_empty = true, │ │423 + }, &tree, ref); │ │424 // later events parent on the ref's new tip │ │425 parent_oids = null; │ │426 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │818 .created_ts = created_ts, │ │819 } else null; │ │820 try Comment.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, record_maybe, &arena, &repo_event_oid); │ │821 + }, │ │822 + .attach => |event_maybe| { │ │823 + const record_maybe: ?Attachment.Record = if (event_maybe) |event| blk: { │ │824 + const entry = try attachedFile(repo_kind, repo_opts, read_state, io, allocator, &arena, &commit_object.content.commit.tree);│ │825 + break :blk .{ │ │826 + .event = event, │ │827 + .author_email = authorEmail(commit_object.content.commit.metadata.author orelse ""), │ │828 + .created_ts = created_ts, │ │829 + .name = entry.name, │ │830 + .blob_oid = entry.oid, │ │831 + }; │ │832 + } else null; │ │833 + try Attachment.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, record_maybe, &arena, &repo_event_oid); │ │834 }, │ │835 } │ │836 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯