╭ PatchRev.zig ─────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("../event.zig"); 3 const xit = @import("xit"); 4 const hash = xit.hash; 5 const obj = xit.object; 6 const rp = xit.repo; 7 8 base_oid: []const u8, 9 source_oid: []const u8, 10 message: []const u8, 11 12 pub const Record = struct { 13 event: Self, 14 removed: bool = false, 15 author_email: ?[]const u8 = null, 16 created_order: u64 = 0, 17 updated_order: u64 = 0, 18 event_oid: []const u8, 19 base_tree_oid: []const u8, 20 head_tree_oid: []const u8, 21 patch_oid: []const u8, 22 }; 23 24 const Self = @This(); 25 26 pub const merge_policy: evt.MergePolicy = .target_wins; 27 pub const record_map_key = "event-id->patchrev"; 28 pub const all_id_set_key = "patchrev-id-set"; 29 30 // prepare a revision event and its patch reference in the caller's arena 31 pub fn prepare( 32 comptime repo_kind: rp.RepoKind, 33 comptime repo_opts: rp.RepoOpts(repo_kind), 34 state: rp.Repo(repo_kind, repo_opts).State(.read_write), 35 io: std.Io, 36 arena: *std.heap.ArenaAllocator, 37 base_oid: *const [hash.hexLen(repo_opts.hash)]u8, 38 source_oid: *const [hash.hexLen(repo_opts.hash)]u8, 39 message: []const u8, 40 author: evt.CommitAuthor, 41 timestamp: u64, 42 ) !struct { event: evt.EventWithId, revision: evt.Patch.Revision } { 43 const allocator = arena.allocator(); 44 var base_object = try obj.Object(repo_kind, repo_opts).initCommit(state.readOnly(), io, allocator, base_oid); 45 defer base_object.deinit(); 46 var source_object = try obj.Object(repo_kind, repo_opts).initCommit(state.readOnly(), io, allocator, source_oid); 47 defer source_object.deinit(); 48 const revision = Self{ .base_oid = try allocator.dupe(u8, base_oid), .source_oid = try allocator.dupe(u8, source_oid), .message = message }; 49 const identity = try std.fmt.allocPrint(allocator, "{s} <{s}>", .{ author.name, author.email }); 50 defer allocator.free(identity); 51 const squash = try writeSquashCommit(repo_kind, repo_opts, state, io, allocator, revision, &source_object.content.commit.tree, identity, identity, timestamp); 52 var id: [evt.event_id_size]u8 = undefined; 53 io.random(&id); 54 const id_hex = std.fmt.bytesToHex(id, .lower); 55 const entries = try allocator.alloc(evt.EventTreeEntry, 2); 56 entries[0] = .{ .tree = .{ .name = "base", .oid = try allocator.dupe(u8, &base_object.content.commit.tree) } }; 57 entries[1] = .{ .tree = .{ .name = "head", .oid = try allocator.dupe(u8, &source_object.content.commit.tree) } }; 58 return .{ 59 .event = .{ .id = id_hex, .author = author, .timestamp = timestamp, .tree_entries = entries, .event = .{ .patchrev = revision } }, 60 .revision = .{ .id = id_hex, .source_oid = revision.source_oid, .squash_oid = try allocator.dupe(u8, &squash) }, 61 }; 62 } 63 64 // create the mergeable squash commit represented by this patch revision 65 pub fn writeSquashCommit( 66 comptime repo_kind: rp.RepoKind, 67 comptime repo_opts: rp.RepoOpts(repo_kind), 68 state: rp.Repo(repo_kind, repo_opts).State(.read_write), 69 io: std.Io, 70 allocator: std.mem.Allocator, 71 event: Self, 72 head_tree_oid: *const [hash.hexLen(repo_opts.hash)]u8, 73 author: []const u8, 74 committer: []const u8, 75 timestamp: u64, 76 ) ![hash.hexLen(repo_opts.hash)]u8 { 77 var base_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 78 if (event.base_oid.len != base_oid.len) return error.InvalidPatch; 79 @memcpy(&base_oid, event.base_oid); 80 try validateOid(repo_opts.hash, &base_oid); 81 const parent_oids = [_][hash.hexLen(repo_opts.hash)]u8{base_oid}; 82 return try obj.writeCommitWithoutRef(repo_kind, repo_opts, state, io, allocator, .{ 83 .author = author, 84 .committer = committer, 85 .message = event.message, 86 .parent_oids = &parent_oids, 87 .timestamp = timestamp, 88 }, head_tree_oid); 89 } 90 91 pub fn readTrees( 92 comptime repo_kind: rp.RepoKind, 93 comptime repo_opts: rp.RepoOpts(repo_kind), 94 state: rp.Repo(repo_kind, repo_opts).State(.read_only), 95 io: std.Io, 96 allocator: std.mem.Allocator, 97 tree_oid: *const [hash.hexLen(repo_opts.hash)]u8, 98 ) !struct { 99 base: [hash.hexLen(repo_opts.hash)]u8, 100 head: [hash.hexLen(repo_opts.hash)]u8, 101 } { 102 var tree_object = try obj.Object(repo_kind, repo_opts).init(state, io, allocator, tree_oid); 103 defer tree_object.deinit(); 104 105 const entries = switch (tree_object.content) { 106 .tree => |tree| tree.entries, 107 else => return error.InvalidPatch, 108 }; 109 if (entries.count() != 2) return error.InvalidPatch; 110 const base = entries.get("base") orelse return error.InvalidPatch; 111 const head = entries.get("head") orelse return error.InvalidPatch; 112 if (!base.isTree() or !head.isTree()) return error.InvalidPatch; 113 114 const base_oid = std.fmt.bytesToHex(base.oid, .lower); 115 const head_oid = std.fmt.bytesToHex(head.oid, .lower); 116 var base_reader = try obj.ObjectReader(repo_kind, repo_opts).init(state, io, allocator, &base_oid); 117 defer base_reader.deinit(); 118 var head_reader = try obj.ObjectReader(repo_kind, repo_opts).init(state, io, allocator, &head_oid); 119 defer head_reader.deinit(); 120 if (base_reader.header().kind != .tree or head_reader.header().kind != .tree) return error.InvalidPatch; 121 122 return .{ .base = base_oid, .head = head_oid }; 123 } 124 125 pub fn consume( 126 comptime DB: type, 127 comptime hash_kind: hash.HashKind, 128 haxy_moment: DB.HashMap(.read_write), 129 event_id: *const [evt.event_id_size]u8, 130 record_maybe: ?Record, 131 arena: *std.heap.ArenaAllocator, 132 _: ?[]const u8, 133 ) !void { 134 const record_key = hash.hashInt(hash_kind, event_id); 135 const records = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, record_map_key))); 136 137 var existing_maybe: ?Record = null; 138 const existing_cursor_maybe = try records.getCursor(record_key); 139 if (existing_cursor_maybe) |cursor| { 140 existing_maybe = try evt.read(Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(cursor)); 141 } 142 143 var record = record_maybe orelse try evt.removedRecord(Record, DB, hash_kind, haxy_moment.readOnly(), existing_maybe); 144 145 try validateOid(hash_kind, record.event.base_oid); 146 try validateOid(hash_kind, record.event.source_oid); 147 try validateOid(hash_kind, record.event_oid); 148 try validateOid(hash_kind, record.base_tree_oid); 149 try validateOid(hash_kind, record.head_tree_oid); 150 try validateOid(hash_kind, record.patch_oid); 151 152 if (existing_maybe) |existing| { 153 if (!std.mem.eql(u8, existing.event.base_oid, record.event.base_oid) or 154 !std.mem.eql(u8, existing.event.source_oid, record.event.source_oid) or 155 !std.mem.eql(u8, existing.event.message, record.event.message) or 156 !std.mem.eql(u8, existing.base_tree_oid, record.base_tree_oid) or 157 !std.mem.eql(u8, existing.head_tree_oid, record.head_tree_oid) or 158 !std.mem.eql(u8, existing.patch_oid, record.patch_oid)) 159 { 160 return error.PatchRevChanged; 161 } 162 record.created_order = existing.created_order; 163 record.author_email = existing.author_email; 164 record.event_oid = existing.event_oid; 165 } 166 167 const cursor = try records.putCursor(record_key); 168 try evt.upsert(Record, DB, hash_kind, try DB.HashMap(.read_write).init(cursor), record); 169 try evt.indexEvent(DB, hash_kind, haxy_moment, event_id, .patchrev, existing_maybe, record); 170 171 if (existing_cursor_maybe == null) { 172 const ids = try DB.SortedSet(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, all_id_set_key))); 173 try ids.put(&evt.orderKeyDesc(record.created_order, event_id)); 174 } 175 } 176 177 pub fn validateOid(comptime hash_kind: hash.HashKind, oid: []const u8) !void { 178 var bytes: [hash.byteLen(hash_kind)]u8 = undefined; 179 _ = try std.fmt.hexToBytes(&bytes, oid); 180 if (!std.mem.eql(u8, oid, &std.fmt.bytesToHex(bytes, .lower))) return error.InvalidOid; 181 } 182 183 pub fn readById( 184 comptime DB: type, 185 comptime hash_kind: hash.HashKind, 186 haxy_moment: DB.HashMap(.read_only), 187 arena: *std.heap.ArenaAllocator, 188 id: *const [evt.event_id_size]u8, 189 ) !?Record { 190 const records_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, record_map_key)) orelse return null; 191 const records = try DB.HashMap(.read_only).init(records_cursor); 192 const record_cursor = try records.getCursor(hash.hashInt(hash_kind, id)) orelse return null; 193 return try evt.read(Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(record_cursor)); 194 } 195 196 pub const WithId = struct { 197 id: [evt.event_id_size]u8, 198 record: Record, 199 }; 200 201 pub fn readNewest( 202 comptime DB: type, 203 comptime hash_kind: hash.HashKind, 204 haxy_moment: DB.HashMap(.read_only), 205 arena: *std.heap.ArenaAllocator, 206 ) !?WithId { 207 const ids_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, all_id_set_key)) orelse return null; 208 const ids = try DB.SortedSet(.read_only).init(ids_cursor); 209 var iter = try ids.iteratorFromIndex(0); 210 while (try iter.next()) |cursor| { 211 const id = try evt.readOrderKeyId(DB, cursor); 212 const record = (try readById(DB, hash_kind, haxy_moment, arena, &id)) orelse continue; 213 if (!record.removed) return .{ .id = id, .record = record }; 214 } 215 return null; 216 } 217 218 // squash commits referenced only by patch revision records 219 pub fn gcRoots( 220 comptime DB: type, 221 comptime hash_kind: hash.HashKind, 222 allocator: std.mem.Allocator, 223 haxy_moment: DB.HashMap(.read_only), 224 ) ![][hash.hexLen(hash_kind)]u8 { 225 var roots: std.ArrayList([hash.hexLen(hash_kind)]u8) = .empty; 226 errdefer roots.deinit(allocator); 227 var arena = std.heap.ArenaAllocator.init(allocator); 228 defer arena.deinit(); 229 230 const ids_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, all_id_set_key)) orelse return try roots.toOwnedSlice(allocator); 231 const ids = try DB.SortedSet(.read_only).init(ids_cursor); 232 var iter = try ids.iteratorFromIndex(0); 233 while (try iter.next()) |cursor| { 234 _ = arena.reset(.retain_capacity); 235 const id = try evt.readOrderKeyId(DB, cursor); 236 const record = (try readById(DB, hash_kind, haxy_moment, &arena, &id)) orelse continue; 237 var oid: [hash.hexLen(hash_kind)]u8 = undefined; 238 if (record.patch_oid.len != oid.len) return error.InvalidOid; 239 @memcpy(&oid, record.patch_oid); 240 try roots.append(allocator, oid); 241 } 242 243 return try roots.toOwnedSlice(allocator); 244 } 245