╭ 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 const progress = @import("./progress.zig"); 11 12 // the repo moment key holding one commit-message index per ref tip. it sits 13 // outside the haxy moment because pushes change it and events never do. each 14 // entry is keyed by its commit oid's hash: its value is the index as of that 15 // commit, and its key slot holds the raw commit oid followed by its timestamp. 16 pub const index_key = "haxy/commit-oid->word->time+oid"; 17 18 // a ref move: the old tip's version, when it has one, is the base the new 19 // tip's derives from 20 pub const Move = struct { 21 old_oid: []const u8, 22 new_oid: []const u8, 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 // the action a commit index refresh records when it runs on its own 66 pub const undo_action = "haxy/commit-index"; 67 68 // reconcile the index versions with the repo's ref tips, in a transaction of 69 // its own 70 pub fn refresh( 71 comptime repo_opts: rp.RepoOpts(.xit), 72 io: std.Io, 73 allocator: std.mem.Allocator, 74 repo: *rp.Repo(.xit, repo_opts), 75 ) !void { 76 const Repo = rp.Repo(.xit, repo_opts); 77 const DB = Repo.DB; 78 const Save = struct { 79 core: *Repo.Core, 80 io: std.Io, 81 allocator: std.mem.Allocator, 82 83 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 84 var moment = try DB.HashMap(.read_write).init(cursor.*); 85 const state = Repo.State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 86 if (!try refreshInTransaction(repo_opts, state, &moment, ctx.io, ctx.allocator, &.{}, null)) return error.CancelTransaction; 87 88 // record the user action for undo 89 try xit.undo.write(repo_opts, state, std.Io.Timestamp.now(ctx.io, .real).toSeconds(), .{ .custom = .{ .action_kind = undo_action } }); 90 } 91 }; 92 93 // held across the reads and the writes, so a concurrent refresh can't 94 // change the entries these decisions were made from. 95 try repo.core.db_file.lock(io, .exclusive); 96 defer repo.core.db_file.unlock(io); 97 98 const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); 99 history.appendContext(.{ .slot = try history.getSlot(-1) }, Save{ .core = &repo.core, .io = io, .allocator = allocator }) catch |err| switch (err) { 100 error.CancelTransaction => {}, 101 else => return err, 102 }; 103 } 104 105 // reconcile them inside the caller's transaction, so the index lands with the 106 // change that invalidated it. `moves` are the refs that change moved, whose 107 // old oids are the cheapest bases to derive the new tips from. reports whether 108 // anything was written, so a caller that owns an otherwise empty transaction 109 // can cancel it 110 pub fn refreshInTransaction( 111 comptime repo_opts: rp.RepoOpts(.xit), 112 state: rp.Repo(.xit, repo_opts).State(.read_write), 113 moment: *rp.Repo(.xit, repo_opts).DB.HashMap(.read_write), 114 io: std.Io, 115 allocator: std.mem.Allocator, 116 moves: []const Move, 117 progress_ctx_maybe: ?repo_opts.ProgressCtx, 118 ) !bool { 119 const Repo = rp.Repo(.xit, repo_opts); 120 const DB = Repo.DB; 121 const Oid = [hash.hexLen(repo_opts.hash)]u8; 122 const oid_len = comptime hash.byteLen(repo_opts.hash); 123 // an entry's key slot: the raw commit oid followed by its timestamp 124 const Entry = [oid_len + @sizeOf(u64)]u8; 125 126 // one version to build: the commit it covers, and the commit whose version 127 // it derives from (null = walk the whole history). 128 const Work = struct { 129 oid: Oid, 130 timestamp: u64, 131 base: ?Oid, 132 }; 133 134 var arena = std.heap.ArenaAllocator.init(allocator); 135 defer arena.deinit(); 136 const aa = arena.allocator(); 137 138 var work: std.ArrayList(Work) = .empty; 139 var gone: std.ArrayList(Oid) = .empty; 140 var default_tip: ?Oid = null; 141 // the versions in the index, each with its commit's timestamp. the ones 142 // built here join them, so a later tip can derive from an earlier one. 143 var versions: std.AutoArrayHashMapUnmanaged(Oid, u64) = .empty; 144 145 { 146 const read_state = state.readOnly(); 147 148 if (try moment.getCursor(hash.hashInt(repo_opts.hash, index_key))) |index_cursor| { 149 const index = try DB.HashMap(.read_only).init(index_cursor); 150 var iter = try index.iterator(); 151 while (try iter.next()) |cursor| { 152 const pair = try cursor.readKeyValuePair(); 153 var entry: Entry = undefined; 154 if ((try pair.key_cursor.readBytes(&entry)).len != entry.len) return error.InvalidCommitIndex; 155 const oid = std.fmt.bytesToHex(entry[0..oid_len].*, .lower); 156 try versions.put(aa, oid, std.mem.readInt(u64, entry[oid_len..], .big)); 157 } 158 } 159 160 // what every head except the events branch and every tag points at 161 var tips: std.ArrayList(Oid) = .empty; 162 { 163 var branches = try rf.RefIterator(.xit, repo_opts).init(read_state, io, allocator, .head, .beginning); 164 defer branches.deinit(); 165 while (try branches.next()) |ref| { 166 if (std.mem.eql(u8, ref.name, evt.events_ref.name)) continue; 167 try tips.append(aa, (try rf.readRecur(.xit, repo_opts, read_state, io, .{ .ref = .{ .kind = .head, .name = ref.name } })) orelse continue); 168 } 169 } 170 { 171 var tags = try rf.RefIterator(.xit, repo_opts).init(read_state, io, allocator, .tag, .beginning); 172 defer tags.deinit(); 173 while (try tags.next()) |ref| { 174 try tips.append(aa, (try rf.readRecur(.xit, repo_opts, read_state, io, .{ .ref = .{ .kind = .tag, .name = ref.name } })) orelse continue); 175 } 176 } 177 178 // the commits a version is wanted for, annotated tags peeled. refs 179 // sharing a commit share its version. 180 var wanted: std.AutoArrayHashMapUnmanaged(Oid, void) = .empty; 181 var missing: std.AutoArrayHashMapUnmanaged(Oid, Work) = .empty; 182 for (tips.items) |tip| { 183 // a tip that already has a version needs no reading 184 if (versions.contains(tip)) { 185 try wanted.put(aa, tip, {}); 186 continue; 187 } 188 var commit = obj.Object(.xit, repo_opts).initCommit(read_state, io, allocator, &tip) catch continue; 189 defer commit.deinit(); 190 try wanted.put(aa, commit.oid, {}); 191 if (versions.contains(commit.oid)) continue; 192 const entry = try missing.getOrPut(aa, commit.oid); 193 if (!entry.found_existing) entry.value_ptr.* = .{ 194 .oid = commit.oid, 195 .timestamp = commit.content.commit.metadata.timestamp, 196 .base = null, 197 }; 198 if (entry.value_ptr.base == null) entry.value_ptr.base = movedFrom(repo_opts, moves, &tip, versions); 199 } 200 201 var head_buffer: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; 202 if (rf.readHead(.xit, repo_opts, read_state, io, &head_buffer) catch null) |head| switch (head) { 203 .ref => |ref| default_tip = try rf.readRecur(.xit, repo_opts, read_state, io, .{ .ref = .{ .kind = .head, .name = ref.name } }), 204 .oid => {}, 205 }; 206 207 try work.appendSlice(aa, missing.values()); 208 // oldest first, so a later tip derives from an earlier one 209 std.mem.sort(Work, work.items, {}, struct { 210 fn lessThan(_: void, a: Work, b: Work) bool { 211 return a.timestamp < b.timestamp; 212 } 213 }.lessThan); 214 215 for (versions.keys()) |oid| { 216 if (!wanted.contains(oid)) try gone.append(aa, oid); 217 } 218 } 219 220 if (work.items.len == 0 and gone.items.len == 0) return false; 221 222 { 223 const index_hash = hash.hashInt(repo_opts.hash, index_key); 224 225 for (work.items) |item| { 226 const index = try DB.HashMap(.read_write).init(try moment.putCursor(index_hash)); 227 const base = item.base orelse nearestVersion(repo_opts.hash, versions, default_tip, item.timestamp); 228 229 var entry: Entry = undefined; 230 _ = try std.fmt.hexToBytes(entry[0..oid_len], &item.oid); 231 std.mem.writeInt(u64, entry[oid_len..], item.timestamp, .big); 232 const oid_int = hash.bytesToInt(repo_opts.hash, entry[0..oid_len]); 233 try index.putKey(oid_int, .{ .bytes = &entry }); 234 var version_cursor = try index.putCursor(oid_int); 235 if (base) |base_oid| { 236 const base_cursor = (try index.getCursor(try hash.hexToInt(repo_opts.hash, &base_oid))) orelse return error.InvalidCommitIndex; 237 try version_cursor.write(.{ .slot = base_cursor.slot() }); 238 } 239 const version = try DB.SortedMap(.read_write).init(version_cursor); 240 241 if (base) |*base_oid| { 242 try derive(repo_opts, state.readOnly(), io, allocator, version, base_oid, &item.oid, progress_ctx_maybe); 243 } else { 244 try indexHistory(repo_opts, state.readOnly(), io, allocator, version, &item.oid, progress_ctx_maybe); 245 } 246 247 try versions.put(aa, item.oid, item.timestamp); 248 // the next version copies this one's slot, so everything 249 // written so far has to be shared rather than mutated 250 try state.core.db.freeze(); 251 } 252 253 // removing last keeps a tip that is going away available as a base 254 const index = try DB.HashMap(.read_write).init(try moment.putCursor(index_hash)); 255 for (gone.items) |oid| _ = try index.remove(try hash.hexToInt(repo_opts.hash, &oid)); 256 } 257 return true; 258 } 259 260 // the version a tip committed at `timestamp` derives from when no move names 261 // one: the default branch's unless it is newer than the tip, else the nearest 262 // older one, else the oldest. a guess by dates that only the cost rides on. 263 fn nearestVersion( 264 comptime hash_kind: hash.HashKind, 265 versions: std.AutoArrayHashMapUnmanaged([hash.hexLen(hash_kind)]u8, u64), 266 default_tip: ?[hash.hexLen(hash_kind)]u8, 267 timestamp: u64, 268 ) ?[hash.hexLen(hash_kind)]u8 { 269 var older: ?[hash.hexLen(hash_kind)]u8 = null; 270 var older_timestamp: u64 = 0; 271 var oldest: ?[hash.hexLen(hash_kind)]u8 = null; 272 var oldest_timestamp: u64 = std.math.maxInt(u64); 273 for (versions.keys(), versions.values()) |oid, version_timestamp| { 274 if (version_timestamp <= timestamp) { 275 if (default_tip) |tip| if (std.mem.eql(u8, &tip, &oid)) return tip; 276 if (older == null or version_timestamp > older_timestamp) { 277 older = oid; 278 older_timestamp = version_timestamp; 279 } 280 } 281 if (version_timestamp < oldest_timestamp) { 282 oldest = oid; 283 oldest_timestamp = version_timestamp; 284 } 285 } 286 return older orelse oldest; 287 } 288 289 // the commit a move took a ref to `tip` from, when it has a version to derive 290 // from. 291 fn movedFrom( 292 comptime repo_opts: rp.RepoOpts(.xit), 293 moves: []const Move, 294 tip: *const [hash.hexLen(repo_opts.hash)]u8, 295 versions: std.AutoArrayHashMapUnmanaged([hash.hexLen(repo_opts.hash)]u8, u64), 296 ) ?[hash.hexLen(repo_opts.hash)]u8 { 297 for (moves) |move| { 298 if (!std.mem.eql(u8, move.new_oid, tip)) continue; 299 if (move.old_oid.len != hash.hexLen(repo_opts.hash)) continue; 300 var oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 301 @memcpy(&oid, move.old_oid); 302 if (versions.contains(oid)) return oid; 303 } 304 return null; 305 } 306 307 // turn a copy of `base_tip`'s version into `tip`'s by moving the postings of 308 // the commits the two don't share 309 fn derive( 310 comptime repo_opts: rp.RepoOpts(.xit), 311 state: rp.Repo(.xit, repo_opts).State(.read_only), 312 io: std.Io, 313 allocator: std.mem.Allocator, 314 version: rp.Repo(.xit, repo_opts).DB.SortedMap(.read_write), 315 base_tip: *const [hash.hexLen(repo_opts.hash)]u8, 316 tip: *const [hash.hexLen(repo_opts.hash)]u8, 317 progress_ctx_maybe: ?repo_opts.ProgressCtx, 318 ) !void { 319 const DB = rp.Repo(.xit, repo_opts).DB; 320 const Ancestry = mrg.Ancestry(.xit, repo_opts); 321 var ancestry = try Ancestry.init(state, io, allocator, base_tip, tip); 322 defer ancestry.deinit(); 323 324 progress.report(repo_opts, io, progress_ctx_maybe, .{ .start = .{ .kind = .walking_commit, .estimated_total_items = 0 } }); 325 try ancestry.finish(progress_ctx_maybe); 326 progress.report(repo_opts, io, progress_ctx_maybe, .{ .end = .walking_commit }); 327 328 // the walk stops at the shared history, so it trusts timestamps: a 329 // commit dated later than a descendant can be passed from the base 330 // side before the tip turns out to reach it too, and its postings are 331 // then dropped. settling that would mean walking the whole history. 332 // a wrong addition only re-puts postings, so a fast-forward removes 333 // nothing, which the base tip picking up the tip's flag proves. 334 const base_node = ancestry.nodes.get(ancestry.tips[0]) orelse unreachable; 335 const fast_forward = base_node.flags & Ancestry.two != 0; 336 337 // a commit one tip alone reaches has postings to move. tag objects are 338 // peeled through, not walked. 339 var total: usize = 0; 340 var counted = ancestry.nodes.valueIterator(); 341 while (counted.next()) |node| { 342 if (node.tag_target != null) continue; 343 switch (node.flags & Ancestry.both) { 344 Ancestry.one => total += @intFromBool(!fast_forward), 345 Ancestry.two => total += 1, 346 else => {}, 347 } 348 } 349 350 var message: std.ArrayList(u8) = .empty; 351 defer message.deinit(allocator); 352 353 progress.report(repo_opts, io, progress_ctx_maybe, .{ .text = "Indexing commits" }); 354 progress.report(repo_opts, io, progress_ctx_maybe, .{ .start = .{ .kind = .writing_patch, .estimated_total_items = total } }); 355 var iter = ancestry.nodes.iterator(); 356 while (iter.next()) |entry| { 357 if (entry.value_ptr.tag_target != null) continue; 358 const flags = entry.value_ptr.flags & Ancestry.both; 359 if (flags == Ancestry.both or flags == 0 or (flags == Ancestry.one and fast_forward)) continue; 360 if (progress.cancelled(repo_opts, progress_ctx_maybe)) return error.ClientGone; 361 defer progress.report(repo_opts, io, progress_ctx_maybe, .{ .complete_one = .writing_patch }); 362 363 var object = try obj.Object(.xit, repo_opts).init(state, io, allocator, entry.key_ptr); 364 defer object.deinit(); 365 const key = try readPosting(repo_opts, allocator, &message, &object); 366 if (flags == Ancestry.one) { 367 try srch.remove(DB, version, allocator, &key, message.items); 368 } else { 369 try srch.add(DB, version, allocator, &key, message.items); 370 } 371 } 372 progress.report(repo_opts, io, progress_ctx_maybe, .{ .end = .writing_patch }); 373 } 374 375 // index every commit `tip` reaches into an empty `version`, as the walk 376 // reaches it 377 fn indexHistory( 378 comptime repo_opts: rp.RepoOpts(.xit), 379 state: rp.Repo(.xit, repo_opts).State(.read_only), 380 io: std.Io, 381 allocator: std.mem.Allocator, 382 version: rp.Repo(.xit, repo_opts).DB.SortedMap(.read_write), 383 tip: *const [hash.hexLen(repo_opts.hash)]u8, 384 progress_ctx_maybe: ?repo_opts.ProgressCtx, 385 ) !void { 386 const DB = rp.Repo(.xit, repo_opts).DB; 387 388 var message: std.ArrayList(u8) = .empty; 389 defer message.deinit(allocator); 390 391 // a whole history has no count until it is walked, so it reports the 392 // commits indexed so far 393 progress.report(repo_opts, io, progress_ctx_maybe, .{ .text = "Indexing commits" }); 394 progress.report(repo_opts, io, progress_ctx_maybe, .{ .start = .{ .kind = .writing_patch, .estimated_total_items = 0 } }); 395 var iter = try obj.ObjectIterator(.xit, repo_opts).init(state, io, allocator, .{ .kind = .commit }); 396 defer iter.deinit(); 397 try iter.include(tip); 398 while (try iter.next(allocator)) |object| { 399 defer object.deinit(); 400 if (progress.cancelled(repo_opts, progress_ctx_maybe)) return error.ClientGone; 401 const key = try readPosting(repo_opts, allocator, &message, object); 402 try srch.add(DB, version, allocator, &key, message.items); 403 progress.report(repo_opts, io, progress_ctx_maybe, .{ .complete_one = .writing_patch }); 404 } 405 progress.report(repo_opts, io, progress_ctx_maybe, .{ .end = .writing_patch }); 406 } 407 408 // replace `message` with the indexed prefix of `object`'s message and return 409 // the doc key it indexes under 410 fn readPosting( 411 comptime repo_opts: rp.RepoOpts(.xit), 412 allocator: std.mem.Allocator, 413 message: *std.ArrayList(u8), 414 object: *obj.Object(.xit, repo_opts), 415 ) ![docKeyLen(repo_opts.hash)]u8 { 416 message.clearRetainingCapacity(); 417 object.readMessage(allocator, message, .limited(srch.max_indexed_bytes)) catch |err| switch (err) { 418 // only the indexed prefix matters 419 error.StreamTooLong => {}, 420 else => |e| return e, 421 }; 422 return docKey(repo_opts.hash, object.content.commit.metadata.timestamp, &object.oid); 423 } 424