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