╭ search_commit.zig ────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("event.zig"); 3 const srch = @import("search.zig"); 4 const xit = @import("xit"); 5 const rp = xit.repo; 6 const hash = xit.hash; 7 const rf = xit.ref; 8 const obj = xit.object; 9 const mrg = xit.merge; 10 11 // the repo moment key holding one commit-message index per ref tip. it sits 12 // outside the haxy moment because pushes change it and events never do. each 13 // entry is keyed by its commit oid's hash: its value is the index as of that 14 // commit, and its key slot holds the raw commit oid. 15 pub const index_key = "haxy/commit-oid->word->time+oid"; 16 17 // a commit a version covers. 18 pub fn Commit(comptime hash_kind: hash.HashKind) type { 19 return struct { 20 oid: [hash.hexLen(hash_kind)]u8, 21 timestamp: u64, 22 }; 23 } 24 25 // `commit_oid`'s index, or null when it has none. a version is correct for its 26 // commit by construction, so there is nothing to check for staleness. 27 pub fn lookup( 28 comptime repo_opts: rp.RepoOpts(.xit), 29 moment: rp.Repo(.xit, repo_opts).DB.HashMap(.read_only), 30 commit_oid: *const [hash.hexLen(repo_opts.hash)]u8, 31 ) !?rp.Repo(.xit, repo_opts).DB.SortedMap(.read_only) { 32 const DB = rp.Repo(.xit, repo_opts).DB; 33 const index_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, index_key)) orelse return null; 34 const index = try DB.HashMap(.read_only).init(index_cursor); 35 const cursor = try index.getCursor(try hash.hexToInt(repo_opts.hash, commit_oid)) orelse return null; 36 return try DB.SortedMap(.read_only).init(cursor); 37 } 38 39 // a commit's doc key: its committer timestamp counting down, so a posting set 40 // iterates newest first and a new commit inserts at the front, then its raw 41 // oid, which makes the key unique. 42 pub fn docKey( 43 comptime hash_kind: hash.HashKind, 44 timestamp: u64, 45 oid: *const [hash.hexLen(hash_kind)]u8, 46 ) ![docKeyLen(hash_kind)]u8 { 47 var key: [docKeyLen(hash_kind)]u8 = undefined; 48 std.mem.writeInt(u64, key[0..@sizeOf(u64)], std.math.maxInt(u64) - timestamp, .big); 49 _ = try std.fmt.hexToBytes(key[@sizeOf(u64)..], oid); 50 return key; 51 } 52 53 // the commit a doc key names. 54 pub fn keyOid(comptime hash_kind: hash.HashKind, key: []const u8) ![hash.hexLen(hash_kind)]u8 { 55 if (key.len != docKeyLen(hash_kind)) return error.InvalidCommitIndex; 56 var oid: [hash.byteLen(hash_kind)]u8 = undefined; 57 @memcpy(&oid, key[@sizeOf(u64)..]); 58 return std.fmt.bytesToHex(oid, .lower); 59 } 60 61 fn docKeyLen(comptime hash_kind: hash.HashKind) usize { 62 return @sizeOf(u64) + hash.byteLen(hash_kind); 63 } 64 65 // reconcile the index versions with the repo's ref tips, in one transaction. 66 // `updates` is the push that prompted this, whose old oids are the cheapest 67 // bases to derive the new tips from. 68 pub fn refresh( 69 comptime repo_opts: rp.RepoOpts(.xit), 70 io: std.Io, 71 allocator: std.mem.Allocator, 72 repo: *rp.Repo(.xit, repo_opts), 73 updates: ?[]const xit.net_server_receive_pack.AppliedRefUpdate, 74 ) !void { 75 const Repo = rp.Repo(.xit, repo_opts); 76 const DB = Repo.DB; 77 const Oid = [hash.hexLen(repo_opts.hash)]u8; 78 const Tip = Commit(repo_opts.hash); 79 80 // one version to build: the commit it covers, and the commit whose version 81 // it derives from (null = walk the whole history). 82 const Work = struct { 83 oid: Oid, 84 timestamp: u64, 85 base: ?Oid, 86 }; 87 88 var arena = std.heap.ArenaAllocator.init(allocator); 89 defer arena.deinit(); 90 const aa = arena.allocator(); 91 92 var work: std.ArrayList(Work) = .empty; 93 var gone: std.ArrayList(Oid) = .empty; 94 var default_base: ?Oid = null; 95 var newest: ?Tip = null; 96 97 // held across the reads and the writes, so a concurrent refresh can't 98 // change the entries these decisions were made from. 99 try repo.core.db_file.lock(io, .exclusive); 100 defer repo.core.db_file.unlock(io); 101 102 { 103 var moment = try repo.core.latestMoment(); 104 const state = Repo.State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; 105 106 // the commits that already have a version 107 var existing: std.AutoArrayHashMapUnmanaged(Oid, void) = .empty; 108 if (try moment.getCursor(hash.hashInt(repo_opts.hash, index_key))) |index_cursor| { 109 const index = try DB.HashMap(.read_only).init(index_cursor); 110 var iter = try index.iterator(); 111 while (try iter.next()) |cursor| { 112 const pair = try cursor.readKeyValuePair(); 113 var oid_bytes: [hash.byteLen(repo_opts.hash)]u8 = undefined; 114 if ((try pair.key_cursor.readBytes(&oid_bytes)).len != oid_bytes.len) return error.InvalidCommitIndex; 115 try existing.put(aa, std.fmt.bytesToHex(oid_bytes, .lower), {}); 116 } 117 } 118 119 // what every head except the events branch and every tag points at 120 var tips: std.ArrayList(Oid) = .empty; 121 { 122 var branches = try repo.listBranches(io, allocator, .beginning); 123 defer branches.deinit(); 124 while (try branches.next()) |ref| { 125 if (std.mem.eql(u8, ref.name, evt.events_ref.name)) continue; 126 try tips.append(aa, (try repo.readRef(io, .{ .kind = .head, .name = ref.name })) orelse continue); 127 } 128 } 129 { 130 var tags = try repo.listTags(io, allocator, .beginning); 131 defer tags.deinit(); 132 while (try tags.next()) |ref| { 133 try tips.append(aa, (try repo.readRef(io, .{ .kind = .tag, .name = ref.name })) orelse continue); 134 } 135 } 136 137 // the commits a version is wanted for, annotated tags peeled. refs 138 // sharing a commit share its version. 139 var wanted: std.AutoArrayHashMapUnmanaged(Oid, void) = .empty; 140 var missing: std.AutoArrayHashMapUnmanaged(Oid, Work) = .empty; 141 for (tips.items) |tip| { 142 // a tip that already has a version needs no reading 143 if (existing.contains(tip)) { 144 try wanted.put(aa, tip, {}); 145 continue; 146 } 147 var commit = obj.Object(.xit, repo_opts).initCommit(state, io, allocator, &tip) catch continue; 148 defer commit.deinit(); 149 try wanted.put(aa, commit.oid, {}); 150 if (existing.contains(commit.oid)) continue; 151 const entry = try missing.getOrPut(aa, commit.oid); 152 if (!entry.found_existing) entry.value_ptr.* = .{ 153 .oid = commit.oid, 154 .timestamp = commit.content.commit.metadata.timestamp, 155 .base = null, 156 }; 157 if (entry.value_ptr.base == null) entry.value_ptr.base = pushedFrom(repo_opts, updates, &tip, existing); 158 } 159 160 // the base a brand-new tip falls back to, preferring the default 161 // branch so most of its map is shared rather than rewritten. 162 var head_buffer: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; 163 if (repo.head(io, &head_buffer)) |head| switch (head) { 164 .ref => |ref| if (try repo.readRef(io, .{ .kind = .head, .name = ref.name })) |oid| { 165 if (existing.contains(oid)) default_base = oid; 166 }, 167 .oid => {}, 168 } else |_| {} 169 170 // ... else the newest version there is, which only matters when a 171 // version has to be built without the default branch's. an entry whose 172 // commit is gone can't be walked from, so it isn't a candidate. 173 if (missing.count() > 0 and default_base == null) for (existing.keys()) |oid| { 174 var commit = obj.Object(.xit, repo_opts).initCommit(state, io, allocator, &oid) catch continue; 175 defer commit.deinit(); 176 const timestamp = commit.content.commit.metadata.timestamp; 177 if (newest) |tip| if (timestamp <= tip.timestamp) continue; 178 newest = .{ .oid = oid, .timestamp = timestamp }; 179 }; 180 181 try work.appendSlice(aa, missing.values()); 182 // oldest first, so a later tip derives from an earlier one 183 std.mem.sort(Work, work.items, {}, struct { 184 fn lessThan(_: void, a: Work, b: Work) bool { 185 return a.timestamp < b.timestamp; 186 } 187 }.lessThan); 188 189 for (existing.keys()) |oid| { 190 if (!wanted.contains(oid)) try gone.append(aa, oid); 191 } 192 } 193 194 if (work.items.len == 0 and gone.items.len == 0) return; 195 196 const Save = struct { 197 core: *Repo.Core, 198 io: std.Io, 199 allocator: std.mem.Allocator, 200 work: []const Work, 201 gone: []const Oid, 202 default_base: ?Oid, 203 newest: ?Tip, 204 205 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 206 var moment = try DB.HashMap(.read_write).init(cursor.*); 207 const state = Repo.State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 208 const index_hash = hash.hashInt(repo_opts.hash, index_key); 209 var newest_built = ctx.newest; 210 211 var message: std.ArrayList(u8) = .empty; 212 defer message.deinit(ctx.allocator); 213 214 for (ctx.work) |item| { 215 const index = try DB.HashMap(.read_write).init(try moment.putCursor(index_hash)); 216 const fallback: ?Oid = if (newest_built) |tip| tip.oid else null; 217 const base = item.base orelse ctx.default_base orelse fallback; 218 219 var oid_bytes: [hash.byteLen(repo_opts.hash)]u8 = undefined; 220 _ = try std.fmt.hexToBytes(&oid_bytes, &item.oid); 221 const oid_int = hash.bytesToInt(repo_opts.hash, &oid_bytes); 222 try index.putKey(oid_int, .{ .bytes = &oid_bytes }); 223 var version_cursor = try index.putCursor(oid_int); 224 if (base) |base_oid| { 225 const base_cursor = (try index.getCursor(try hash.hexToInt(repo_opts.hash, &base_oid))) orelse return error.InvalidCommitIndex; 226 try version_cursor.write(.{ .slot = base_cursor.slot() }); 227 } 228 const version = try DB.SortedMap(.read_write).init(version_cursor); 229 230 var diff_arena = std.heap.ArenaAllocator.init(ctx.allocator); 231 defer diff_arena.deinit(); 232 const diff = try commitDiff(repo_opts, state.readOnly(), ctx.io, ctx.allocator, diff_arena.allocator(), if (base) |*base_oid| base_oid else null, &item.oid); 233 for (diff.removed) |commit| { 234 const key = try readPosting(repo_opts, state.readOnly(), ctx.io, ctx.allocator, &message, commit); 235 try srch.remove(DB, version, ctx.allocator, &key, message.items); 236 } 237 for (diff.added) |commit| { 238 const key = try readPosting(repo_opts, state.readOnly(), ctx.io, ctx.allocator, &message, commit); 239 try srch.add(DB, version, ctx.allocator, &key, message.items); 240 } 241 242 const supersedes = if (newest_built) |tip| item.timestamp > tip.timestamp else true; 243 if (supersedes) newest_built = .{ .oid = item.oid, .timestamp = item.timestamp }; 244 // the next version copies this one's slot, so everything 245 // written so far has to be shared rather than mutated 246 try ctx.core.db.freeze(); 247 } 248 249 // removing last keeps a tip that is going away available as a base 250 const index = try DB.HashMap(.read_write).init(try moment.putCursor(index_hash)); 251 for (ctx.gone) |oid| _ = try index.remove(try hash.hexToInt(repo_opts.hash, &oid)); 252 } 253 }; 254 255 const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); 256 try history.appendContext(.{ .slot = try history.getSlot(-1) }, Save{ 257 .core = &repo.core, 258 .io = io, 259 .allocator = allocator, 260 .work = work.items, 261 .gone = gone.items, 262 .default_base = default_base, 263 .newest = newest, 264 }); 265 } 266 267 // the commit this push moved a ref to `tip` from, when it has a version to 268 // derive from. 269 fn pushedFrom( 270 comptime repo_opts: rp.RepoOpts(.xit), 271 updates: ?[]const xit.net_server_receive_pack.AppliedRefUpdate, 272 tip: *const [hash.hexLen(repo_opts.hash)]u8, 273 existing: std.AutoArrayHashMapUnmanaged([hash.hexLen(repo_opts.hash)]u8, void), 274 ) ?[hash.hexLen(repo_opts.hash)]u8 { 275 for (updates orelse &.{}) |update| { 276 if (!std.mem.eql(u8, update.new_oid, tip)) continue; 277 if (update.old_oid.len != hash.hexLen(repo_opts.hash)) continue; 278 var oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 279 @memcpy(&oid, update.old_oid); 280 if (existing.contains(oid)) return oid; 281 } 282 return null; 283 } 284 285 // read `commit`'s message into `message` and return the doc key it indexes 286 // under. 287 fn readPosting( 288 comptime repo_opts: rp.RepoOpts(.xit), 289 state: rp.Repo(.xit, repo_opts).State(.read_only), 290 io: std.Io, 291 allocator: std.mem.Allocator, 292 message: *std.ArrayList(u8), 293 commit: Commit(repo_opts.hash), 294 ) ![docKeyLen(repo_opts.hash)]u8 { 295 message.clearRetainingCapacity(); 296 var object = try obj.Object(.xit, repo_opts).init(state, io, allocator, &commit.oid); 297 defer object.deinit(); 298 object.readMessage(allocator, message, .limited(srch.max_indexed_bytes)) catch |err| switch (err) { 299 // only the indexed prefix matters 300 error.StreamTooLong => {}, 301 else => |e| return e, 302 }; 303 return docKey(repo_opts.hash, commit.timestamp, &commit.oid); 304 } 305 306 // what `tip` reaches that `base_tip` doesn't, and the other way round, built 307 // into `aa`. with no base the whole history is added. 308 fn commitDiff( 309 comptime repo_opts: rp.RepoOpts(.xit), 310 state: rp.Repo(.xit, repo_opts).State(.read_only), 311 io: std.Io, 312 allocator: std.mem.Allocator, 313 aa: std.mem.Allocator, 314 base_tip: ?*const [hash.hexLen(repo_opts.hash)]u8, 315 tip: *const [hash.hexLen(repo_opts.hash)]u8, 316 ) !struct { removed: []const Commit(repo_opts.hash), added: []const Commit(repo_opts.hash) } { 317 var removed: std.ArrayList(Commit(repo_opts.hash)) = .empty; 318 var added: std.ArrayList(Commit(repo_opts.hash)) = .empty; 319 320 if (base_tip) |base| { 321 const Ancestry = mrg.Ancestry(.xit, repo_opts); 322 var ancestry = try Ancestry.init(state, io, allocator, base, tip); 323 defer ancestry.deinit(); 324 try ancestry.finish(); 325 326 // timestamps only choose the walk's order, so a commit the base reached 327 // can be passed before the tip turns out to reach it too. that can only 328 // make a removal wrong, never an addition. a fast-forward removes 329 // nothing, which the base tip picking up the tip's flag proves. anything 330 // else is settled by walking until nothing is left to carry down. 331 const base_node = ancestry.nodes.get(ancestry.tips[0]) orelse unreachable; 332 const fast_forward = base_node.flags & Ancestry.two != 0; 333 if (!fast_forward) { 334 var candidates = false; 335 var flagged = ancestry.nodes.valueIterator(); 336 while (flagged.next()) |node| { 337 if (node.flags & Ancestry.both == Ancestry.one) candidates = true; 338 } 339 if (candidates) while (try ancestry.step()) {}; 340 } 341 342 var iter = ancestry.nodes.iterator(); 343 while (iter.next()) |entry| { 344 // tag objects are peeled through, not walked 345 if (entry.value_ptr.tag_target != null) continue; 346 const commit = Commit(repo_opts.hash){ .oid = entry.key_ptr.*, .timestamp = entry.value_ptr.timestamp }; 347 switch (entry.value_ptr.flags & Ancestry.both) { 348 Ancestry.one => if (!fast_forward) try removed.append(aa, commit), 349 Ancestry.two => try added.append(aa, commit), 350 else => {}, 351 } 352 } 353 } else { 354 var iter = try obj.ObjectIterator(.xit, repo_opts).init(state, io, allocator, .{ .kind = .commit }); 355 defer iter.deinit(); 356 try iter.include(tip); 357 while (try iter.next(allocator)) |object| { 358 defer object.deinit(); 359 try added.append(aa, .{ .oid = object.oid, .timestamp = object.content.commit.metadata.timestamp }); 360 } 361 } 362 363 return .{ .removed = removed.items, .added = added.items }; 364 } 365