╭ 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 commit_count: ?u64 = null, 23 }; 24 25 const Self = @This(); 26 27 pub const merge_policy: evt.MergePolicy = .target_wins; 28 pub const record_map_key = "event-id->patchrev"; 29 pub const all_id_set_key = "patchrev-id-set"; 30 31 const commit_count_limit = 256; 32 33 // prepare a revision event and its patch reference in the caller's arena 34 pub fn prepare( 35 comptime repo_kind: rp.RepoKind, 36 comptime repo_opts: rp.RepoOpts(repo_kind), 37 state: rp.Repo(repo_kind, repo_opts).State(.read_write), 38 io: std.Io, 39 arena: *std.heap.ArenaAllocator, 40 base_oid: *const [hash.hexLen(repo_opts.hash)]u8, 41 source_oid: *const [hash.hexLen(repo_opts.hash)]u8, 42 message: []const u8, 43 author: evt.CommitAuthor, 44 timestamp: u64, 45 ) !struct { event: evt.EventWithId, revision: evt.Patch.Revision } { 46 const allocator = arena.allocator(); 47 const base_tree = if (std.mem.allEqual(u8, base_oid, '0')) blk: { 48 // a merge into a newly created branch starts from an empty tree 49 var reader = std.Io.Reader.fixed(""); 50 var oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 51 try obj.writeObject(repo_kind, repo_opts, state, io, allocator, &reader, .{ .kind = .tree, .size = 0 }, &oid); 52 break :blk std.fmt.bytesToHex(oid, .lower); 53 } else blk: { 54 var base_object = try obj.Object(repo_kind, repo_opts).initCommit(state.readOnly(), io, allocator, base_oid); 55 defer base_object.deinit(); 56 break :blk base_object.content.commit.tree; 57 }; 58 var source_object = try obj.Object(repo_kind, repo_opts).initCommit(state.readOnly(), io, allocator, source_oid); 59 defer source_object.deinit(); 60 const revision = Self{ .base_oid = try allocator.dupe(u8, base_oid), .source_oid = try allocator.dupe(u8, source_oid), .message = message }; 61 const identity = try std.fmt.allocPrint(allocator, "{s} <{s}>", .{ author.name, author.email }); 62 defer allocator.free(identity); 63 const squash = try writeSquashCommit(repo_kind, repo_opts, state, io, allocator, revision, &source_object.content.commit.tree, identity, identity, timestamp); 64 var id: [evt.event_id_size]u8 = undefined; 65 io.random(&id); 66 const id_hex = std.fmt.bytesToHex(id, .lower); 67 const entries = try allocator.alloc(evt.EventTreeEntry, 2); 68 entries[0] = .{ .tree = .{ .name = "base", .oid = try allocator.dupe(u8, &base_tree) } }; 69 entries[1] = .{ .tree = .{ .name = "head", .oid = try allocator.dupe(u8, &source_object.content.commit.tree) } }; 70 return .{ 71 .event = .{ .id = id_hex, .author = author, .timestamp = timestamp, .tree_entries = entries, .event = .{ .patchrev = revision } }, 72 .revision = .{ 73 .id = id_hex, 74 .source_oid = revision.source_oid, 75 .squash_oid = try allocator.dupe(u8, &squash), 76 .commit_count = commitCount(repo_kind, repo_opts, state.readOnly(), io, allocator, base_oid, source_oid) catch null, 77 }, 78 }; 79 } 80 81 // only show the depth difference if a short first-parent walk reaches the base 82 pub fn commitCount( 83 comptime repo_kind: rp.RepoKind, 84 comptime repo_opts: rp.RepoOpts(repo_kind), 85 state: rp.Repo(repo_kind, repo_opts).State(.read_only), 86 io: std.Io, 87 allocator: std.mem.Allocator, 88 base_oid: []const u8, 89 source_oid: []const u8, 90 ) !?u64 { 91 switch (repo_kind) { 92 .git => return null, 93 .xit => { 94 const oid_len = comptime hash.hexLen(repo_opts.hash); 95 if (base_oid.len != oid_len or source_oid.len != oid_len) return error.InvalidOid; 96 const base_count = try state.commitCount(io, allocator, .{ .oid = base_oid[0..oid_len] }); 97 const source_count = try state.commitCount(io, allocator, .{ .oid = source_oid[0..oid_len] }); 98 const count = std.math.sub(u64, source_count, base_count) catch return null; 99 if (count > commit_count_limit) return null; 100 101 // depth subtraction is only valid if the base is on the source's first-parent chain 102 var oid = source_oid[0..oid_len].*; 103 var remaining = count; 104 while (remaining > 0) : (remaining -= 1) { 105 var object = try obj.Object(.xit, repo_opts).initCommit(state, io, allocator, &oid); 106 defer object.deinit(); 107 const parent = object.content.commit.metadata.firstParent() orelse return null; 108 oid = parent.*; 109 } 110 return if (std.mem.eql(u8, &oid, base_oid)) count else null; 111 }, 112 } 113 } 114 115 // create the mergeable squash commit represented by this patch revision 116 pub fn writeSquashCommit( 117 comptime repo_kind: rp.RepoKind, 118 comptime repo_opts: rp.RepoOpts(repo_kind), 119 state: rp.Repo(repo_kind, repo_opts).State(.read_write), 120 io: std.Io, 121 allocator: std.mem.Allocator, 122 event: Self, 123 head_tree_oid: *const [hash.hexLen(repo_opts.hash)]u8, 124 author: []const u8, 125 committer: []const u8, 126 timestamp: u64, 127 ) ![hash.hexLen(repo_opts.hash)]u8 { 128 var base_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 129 if (event.base_oid.len != base_oid.len) return error.InvalidPatch; 130 @memcpy(&base_oid, event.base_oid); 131 try validateOid(repo_opts.hash, &base_oid); 132 const parent_oids = [_][hash.hexLen(repo_opts.hash)]u8{base_oid}; 133 return try obj.writeCommitWithoutRef(repo_kind, repo_opts, state, io, allocator, .{ 134 .author = author, 135 .committer = committer, 136 .message = event.message, 137 .parent_oids = if (std.mem.allEqual(u8, &base_oid, '0')) &.{} else &parent_oids, 138 .timestamp = timestamp, 139 }, head_tree_oid); 140 } 141 142 pub fn readTrees( 143 comptime repo_kind: rp.RepoKind, 144 comptime repo_opts: rp.RepoOpts(repo_kind), 145 state: rp.Repo(repo_kind, repo_opts).State(.read_only), 146 io: std.Io, 147 allocator: std.mem.Allocator, 148 tree_oid: *const [hash.hexLen(repo_opts.hash)]u8, 149 ) !struct { 150 base: [hash.hexLen(repo_opts.hash)]u8, 151 head: [hash.hexLen(repo_opts.hash)]u8, 152 } { 153 var tree_object = try obj.Object(repo_kind, repo_opts).init(state, io, allocator, tree_oid); 154 defer tree_object.deinit(); 155 156 const entries = switch (tree_object.content) { 157 .tree => |tree| tree.entries, 158 else => return error.InvalidPatch, 159 }; 160 if (entries.count() != 2) return error.InvalidPatch; 161 const base = entries.get("base") orelse return error.InvalidPatch; 162 const head = entries.get("head") orelse return error.InvalidPatch; 163 if (!base.isTree() or !head.isTree()) return error.InvalidPatch; 164 165 const base_oid = std.fmt.bytesToHex(base.oid, .lower); 166 const head_oid = std.fmt.bytesToHex(head.oid, .lower); 167 var base_reader = try obj.ObjectReader(repo_kind, repo_opts).init(state, io, allocator, &base_oid); 168 defer base_reader.deinit(); 169 var head_reader = try obj.ObjectReader(repo_kind, repo_opts).init(state, io, allocator, &head_oid); 170 defer head_reader.deinit(); 171 if (base_reader.header().kind != .tree or head_reader.header().kind != .tree) return error.InvalidPatch; 172 173 return .{ .base = base_oid, .head = head_oid }; 174 } 175 176 pub fn consume( 177 comptime DB: type, 178 comptime hash_kind: hash.HashKind, 179 haxy_moment: DB.HashMap(.read_write), 180 event_id: *const [evt.event_id_size]u8, 181 record_maybe: ?Record, 182 arena: *std.heap.ArenaAllocator, 183 _: ?[]const u8, 184 ) !void { 185 const record_key = hash.hashInt(hash_kind, event_id); 186 const records = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, record_map_key))); 187 188 var existing_maybe: ?Record = null; 189 const existing_cursor_maybe = try records.getCursor(record_key); 190 if (existing_cursor_maybe) |cursor| { 191 existing_maybe = try evt.read(Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(cursor)); 192 } 193 194 var record = record_maybe orelse try evt.removedRecord(Record, DB, hash_kind, haxy_moment.readOnly(), existing_maybe); 195 196 try validateOid(hash_kind, record.event.base_oid); 197 try validateOid(hash_kind, record.event.source_oid); 198 try validateOid(hash_kind, record.event_oid); 199 try validateOid(hash_kind, record.base_tree_oid); 200 try validateOid(hash_kind, record.head_tree_oid); 201 try validateOid(hash_kind, record.patch_oid); 202 203 if (existing_maybe) |existing| { 204 if (!std.mem.eql(u8, existing.event.base_oid, record.event.base_oid) or 205 !std.mem.eql(u8, existing.event.source_oid, record.event.source_oid) or 206 !std.mem.eql(u8, existing.event.message, record.event.message) or 207 !std.mem.eql(u8, existing.base_tree_oid, record.base_tree_oid) or 208 !std.mem.eql(u8, existing.head_tree_oid, record.head_tree_oid) or 209 !std.mem.eql(u8, existing.patch_oid, record.patch_oid)) 210 { 211 return error.PatchRevChanged; 212 } 213 record.created_order = existing.created_order; 214 record.author_email = existing.author_email; 215 record.event_oid = existing.event_oid; 216 } 217 218 const cursor = try records.putCursor(record_key); 219 try evt.upsert(Record, DB, hash_kind, try DB.HashMap(.read_write).init(cursor), record); 220 try evt.indexEvent(DB, hash_kind, haxy_moment, event_id, .patchrev, existing_maybe, record); 221 222 if (existing_cursor_maybe == null) { 223 const ids = try DB.SortedSet(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, all_id_set_key))); 224 try ids.put(&evt.orderKeyDesc(record.created_order, event_id)); 225 } 226 } 227 228 pub fn validateOid(comptime hash_kind: hash.HashKind, oid: []const u8) !void { 229 if (oid.len != hash.hexLen(hash_kind)) return error.InvalidOid; 230 var bytes: [hash.byteLen(hash_kind)]u8 = undefined; 231 _ = try std.fmt.hexToBytes(&bytes, oid); 232 if (!std.mem.eql(u8, oid, &std.fmt.bytesToHex(bytes, .lower))) return error.InvalidOid; 233 } 234 235 pub fn readById( 236 comptime DB: type, 237 comptime hash_kind: hash.HashKind, 238 haxy_moment: DB.HashMap(.read_only), 239 arena: *std.heap.ArenaAllocator, 240 id: *const [evt.event_id_size]u8, 241 ) !?Record { 242 const records_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, record_map_key)) orelse return null; 243 const records = try DB.HashMap(.read_only).init(records_cursor); 244 const record_cursor = try records.getCursor(hash.hashInt(hash_kind, id)) orelse return null; 245 return try evt.read(Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(record_cursor)); 246 } 247 248 pub const WithId = struct { 249 id: [evt.event_id_size]u8, 250 record: Record, 251 }; 252 253 pub fn readNewest( 254 comptime DB: type, 255 comptime hash_kind: hash.HashKind, 256 haxy_moment: DB.HashMap(.read_only), 257 arena: *std.heap.ArenaAllocator, 258 ) !?WithId { 259 const ids_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, all_id_set_key)) orelse return null; 260 const ids = try DB.SortedSet(.read_only).init(ids_cursor); 261 var iter = try ids.iteratorFromIndex(0); 262 while (try iter.next()) |cursor| { 263 const id = try evt.readOrderKeyId(DB, cursor); 264 const record = (try readById(DB, hash_kind, haxy_moment, arena, &id)) orelse continue; 265 if (!record.removed) return .{ .id = id, .record = record }; 266 } 267 return null; 268 } 269 270 // squash commits referenced only by patch revision records 271 pub fn gcRoots( 272 comptime DB: type, 273 comptime hash_kind: hash.HashKind, 274 allocator: std.mem.Allocator, 275 haxy_moment: DB.HashMap(.read_only), 276 ) ![][hash.hexLen(hash_kind)]u8 { 277 var roots: std.ArrayList([hash.hexLen(hash_kind)]u8) = .empty; 278 errdefer roots.deinit(allocator); 279 var arena = std.heap.ArenaAllocator.init(allocator); 280 defer arena.deinit(); 281 282 const ids_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, all_id_set_key)) orelse return try roots.toOwnedSlice(allocator); 283 const ids = try DB.SortedSet(.read_only).init(ids_cursor); 284 var iter = try ids.iteratorFromIndex(0); 285 while (try iter.next()) |cursor| { 286 _ = arena.reset(.retain_capacity); 287 const id = try evt.readOrderKeyId(DB, cursor); 288 const record = (try readById(DB, hash_kind, haxy_moment, &arena, &id)) orelse continue; 289 var oid: [hash.hexLen(hash_kind)]u8 = undefined; 290 if (record.patch_oid.len != oid.len) return error.InvalidOid; 291 @memcpy(&oid, record.patch_oid); 292 try roots.append(allocator, oid); 293 } 294 295 return try roots.toOwnedSlice(allocator); 296 } 297