╭ patch.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
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 const obj = xit.object; 7 const rf = xit.ref; 8 9 pub const PostInput = struct { 10 id: [evt.event_id_size * 2]u8, 11 user_id: [evt.event_id_size]u8, 12 repo_id: [evt.event_id_size]u8, 13 author: evt.CommitAuthor, 14 timestamp: u64, 15 }; 16 17 pub fn post( 18 comptime repo_opts: rp.RepoOpts(.xit), 19 io: std.Io, 20 allocator: std.mem.Allocator, 21 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), 22 target_repo: *rp.Repo(.xit, repo_opts), 23 path: []const u8, 24 input: PostInput, 25 ) !void { 26 const patch_id = try evt.parseEventId(&input.id); 27 var arena = std.heap.ArenaAllocator.init(allocator); 28 defer arena.deinit(); 29 30 const admin_moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); 31 const fork_record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &patch_id)) orelse return error.InvalidPatchDraft; 32 if (fork_record.removed or 33 !std.mem.eql(u8, fork_record.event.user_id, &input.user_id) or 34 !std.mem.eql(u8, fork_record.event.repo_id, &input.repo_id)) return error.InvalidPatchDraft; 35 const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &input.user_id)) orelse return error.InvalidPatchDraft; 36 if (user.removed or 37 !std.mem.eql(u8, user.event.name, input.author.name) or 38 !std.mem.eql(u8, user.event.email, input.author.email)) return error.InvalidPatchDraft; 39 40 var fork_repo = rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = path }) catch return error.PatchDataUnavailable; 41 defer fork_repo.deinit(io, allocator); 42 const fork_moment = evt.currentMoment(repo_opts, &fork_repo) catch return error.PatchDataUnavailable; 43 const local_patch = (try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena, &patch_id)) orelse return error.PatchDataUnavailable; 44 45 const target_moment = evt.currentMoment(repo_opts, target_repo) catch |err| switch (err) { 46 error.NotFound => null, 47 else => |other| return other, 48 }; 49 const existing = if (target_moment) |moment| 50 try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, moment, &arena, &patch_id) 51 else 52 null; 53 if (existing) |record| { 54 if (record.removed or !std.mem.eql(u8, record.author_email orelse "", input.author.email)) return error.InvalidPatch; 55 } 56 if (local_patch.removed) { 57 if (existing == null) return error.PatchDataUnavailable; 58 } else { 59 const selected = local_patch.event.revision orelse return error.PatchNotPushed; 60 const revision_id = try evt.parseEventId(&selected.id); 61 const revision = (try evt.PatchRev.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena, &revision_id)) orelse return error.PatchNotPushed; 62 if (!selected.matches(revision)) return error.PatchNotPushed; 63 var patch = (existing orelse local_patch).event; 64 patch.revision = selected; 65 if (existing == null) patch.status = .open; 66 67 try evt.consume(.repo, .xit, repo_opts, io, allocator, target_repo, evt.events_ref, &.{.{ 68 .id = input.id, 69 .timestamp = input.timestamp, 70 .author = input.author, 71 .event = .{ .patch = patch }, 72 }}); 73 74 try evt.consume(.fork, .xit, repo_opts, io, allocator, &fork_repo, evt.events_ref, &.{.{ 75 .id = input.id, 76 .timestamp = input.timestamp, 77 .author = input.author, 78 .event = .{ .patch = null }, 79 }}); 80 } 81 82 if (fork_record.event.stage == .draft) { 83 var posted = fork_record.event; 84 posted.stage = .posted; 85 try evt.consume(.admin, .xit, evt.admin_repo_opts, io, allocator, admin_repo, evt.events_ref, &.{.{ 86 .id = input.id, 87 .timestamp = input.timestamp, 88 .author = input.author, 89 .event = .{ .fork = posted }, 90 }}); 91 } 92 } 93 94 fn commitAuthor(line: []const u8) !evt.CommitAuthor { 95 const open = std.mem.lastIndexOfScalar(u8, line, '<') orelse return error.AuthorNotFound; 96 const close = std.mem.indexOfScalarPos(u8, line, open + 1, '>') orelse return error.AuthorNotFound; 97 const name = std.mem.trimEnd(u8, line[0..open], " "); 98 if (name.len == 0 or close == open + 1) return error.AuthorNotFound; 99 return .{ .name = name, .email = line[open + 1 .. close] }; 100 } 101 102 fn importMerged( 103 comptime repo_opts: rp.RepoOpts(.xit), 104 state: rp.Repo(.xit, repo_opts).State(.read_write), 105 db: *evt.EventDB(repo_opts.hash), 106 moment: *evt.EventDB(repo_opts.hash).HashMap(.read_write), 107 io: std.Io, 108 allocator: std.mem.Allocator, 109 repo_root_path: []const u8, 110 patch_id: *const [evt.event_id_size]u8, 111 ) !void { 112 const DB = evt.EventDB(repo_opts.hash); 113 var arena = std.heap.ArenaAllocator.init(allocator); 114 defer arena.deinit(); 115 116 const haxy_moment = try evt.currentMomentFromRepoMoment(repo_opts.hash, moment.readOnly()); 117 const patch_record = (try evt.Patch.readById(DB, repo_opts.hash, haxy_moment, &arena, patch_id)) orelse return; 118 const selected = patch_record.event.revision orelse return; 119 if (patch_record.removed or patch_record.event.status == .merged) return; 120 const revision_id = try evt.parseEventId(&selected.id); 121 122 const patch_hex = std.fmt.bytesToHex(patch_id.*, .lower); 123 const data_path = std.fs.path.dirname(repo_root_path) orelse "."; 124 const fork_path = try std.fs.path.join(arena.allocator(), &.{ data_path, "forks", &patch_hex }); 125 var fork_repo = rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }) catch return; 126 defer fork_repo.deinit(io, allocator); 127 128 const fork_moment = evt.currentMoment(repo_opts, &fork_repo) catch return; 129 const revision = (evt.PatchRev.readById(DB, repo_opts.hash, fork_moment, &arena, &revision_id) catch return) orelse return; 130 if (!selected.matches(revision)) return; 131 132 var fork_repo_moment = try fork_repo.core.latestMoment(); 133 const fork_state = rp.Repo(.xit, repo_opts).State(.read_only){ .core = &fork_repo.core, .extra = .{ .moment = &fork_repo_moment } }; 134 var event_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 135 if (revision.event_oid.len != event_oid.len) return; 136 @memcpy(&event_oid, revision.event_oid); 137 var event_object = obj.Object(.xit, repo_opts).initCommit(fork_state, io, allocator, &event_oid) catch return; 138 defer event_object.deinit(); 139 const original_author = commitAuthor(event_object.content.commit.metadata.author orelse return) catch return; 140 141 var events: [2]evt.EventWithId = undefined; 142 var event_count: usize = 0; 143 const current_revision = try evt.PatchRev.readById(DB, repo_opts.hash, haxy_moment, &arena, &revision_id); 144 var tree_entries: [2]evt.EventTreeEntry = undefined; 145 const needs_import = if (current_revision) |existing| existing.removed else true; 146 if (needs_import) { 147 tree_entries = .{ 148 .{ .tree = .{ .name = "base", .oid = revision.base_tree_oid } }, 149 .{ .tree = .{ .name = "head", .oid = revision.head_tree_oid } }, 150 }; 151 events[event_count] = .{ 152 .id = selected.id, 153 .timestamp = event_object.content.commit.metadata.timestamp, 154 .author = original_author, 155 .tree_entries = &tree_entries, 156 .event = .{ .patchrev = revision.event }, 157 }; 158 event_count += 1; 159 } else { 160 const existing = current_revision orelse unreachable; 161 if (!selected.matches(existing)) return error.InvalidPatch; 162 } 163 164 var patch = patch_record.event; 165 patch.status = .merged; 166 events[event_count] = .{ 167 .id = std.fmt.bytesToHex(patch_id.*, .lower), 168 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), 169 .author = original_author, 170 .event = .{ .patch = patch }, 171 }; 172 event_count += 1; 173 174 try evt.commitEvents(.xit, repo_opts, state, io, allocator, evt.events_ref, events[0..event_count], null); 175 if (!try evt.consumeInTransaction(.repo, .xit, repo_opts, state, db, moment, io, allocator, evt.events_ref)) return error.CancelTransaction; 176 } 177 178 pub fn detectMerged( 179 comptime repo_opts: rp.RepoOpts(.xit), 180 state: rp.Repo(.xit, repo_opts).State(.read_write), 181 db: *evt.EventDB(repo_opts.hash), 182 moment: *evt.EventDB(repo_opts.hash).HashMap(.read_write), 183 io: std.Io, 184 allocator: std.mem.Allocator, 185 repo_root_path: []const u8, 186 updates: []const xit.net_server_receive_pack.AppliedRefUpdate, 187 ) !void { 188 const DB = evt.EventDB(repo_opts.hash); 189 var arena = std.heap.ArenaAllocator.init(allocator); 190 defer arena.deinit(); 191 192 const haxy_moment = try evt.currentMomentFromRepoMoment(repo_opts.hash, moment.readOnly()); 193 const revisions_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.revision_to_id_set_key)) orelse return; 194 const revisions = try DB.HashMap(.read_only).init(revisions_cursor); 195 var events_ref_buffer: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; 196 const events_ref_path = try evt.events_ref.toPath(&events_ref_buffer); 197 for (updates) |update| { 198 if (!std.mem.startsWith(u8, update.ref_name, "refs/heads/") or 199 std.mem.eql(u8, update.ref_name, events_ref_path) or 200 std.mem.eql(u8, update.old_oid, update.new_oid)) continue; 201 if (update.old_oid.len != hash.hexLen(repo_opts.hash) or 202 update.new_oid.len != hash.hexLen(repo_opts.hash)) return error.InvalidOid; 203 var old_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 204 var new_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 205 @memcpy(&old_oid, update.old_oid); 206 @memcpy(&new_oid, update.new_oid); 207 if (std.mem.allEqual(u8, &new_oid, '0')) continue; 208 209 var commits = try obj.ObjectIterator(.xit, repo_opts).init(state.readOnly(), io, arena.allocator(), .{ .kind = .commit }); 210 defer commits.deinit(); 211 if (!std.mem.allEqual(u8, &old_oid, '0')) try commits.exclude(&old_oid); 212 try commits.include(&new_oid); 213 var revision_key_buffer: [rf.MAX_REF_CONTENT_SIZE + 1 + hash.hexLen(repo_opts.hash)]u8 = undefined; 214 while (try commits.next(arena.allocator())) |commit| { 215 defer commit.deinit(); 216 const key = try std.fmt.bufPrint(&revision_key_buffer, "{s}\x00{s}", .{ update.ref_name, &commit.oid }); 217 const ids_cursor = try revisions.getCursor(hash.hashInt(repo_opts.hash, key)) orelse continue; 218 const ids = try DB.CountedHashSet(.read_only).init(ids_cursor); 219 var ids_iter = try ids.iterator(); 220 while (try ids_iter.next()) |cursor| { 221 const pair = try cursor.readKeyValuePair(); 222 var patch_id: [evt.event_id_size]u8 = undefined; 223 if ((try pair.key_cursor.readBytes(&patch_id)).len != patch_id.len) return error.InvalidPatch; 224 try importMerged(repo_opts, state, db, moment, io, allocator, repo_root_path, &patch_id); 225 } 226 } 227 } 228 } 229