╭ changed ───╮ │src/find.zig│ ╰────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │151 try repo.core.db_file.lock(io, .exclusive); │ │152 defer repo.core.db_file.unlock(io); │ │153 │ │154 - // decide everything before opening a write transaction: a slot copied from │ │155 - // a base must come from an earlier transaction, otherwise the base's own │ │156 - // nodes are mutated in place rather than shared. │ │154 { │ │155 var moment = try repo.core.latestMoment(); │ │156 const state = Repo.State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } };│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │192 // whatever is left belongs to a branch that no longer exists │ │193 for (existing.keys()) |branch_hash| try work.append(aa, .{ .branch_hash = branch_hash, .tree = null, .own = null });│ │194 } │ │195 + │ │196 + if (work.items.len == 0) return; │ │197 │ │198 const Save = struct { │ │199 core: *Repo.Core, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │200 io: std.Io, │ │201 allocator: std.mem.Allocator, │ │203 - branch_hash: HashInt, │ │204 - // the tip's root tree (null removes the branch's entry) │ │205 - tree: ?Tree, │ │206 - // the entry the new one starts from (null = index the whole tree)│ │207 - from: ?Entry, │ │202 + work: []const Work, │ │203 + base: ?Entry, │ │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 } }; │ │212 - const index = try DB.HashMap(.read_write).init(try moment.putCursor(hash.hashInt(repo_opts.hash, index_key)));│ │213 - const tree = ctx.tree orelse { │ │214 - _ = try index.remove(ctx.branch_hash); │ │215 - return; │ │216 - }; │ │217 - │ │218 - var tree_bytes: [hash.byteLen(repo_opts.hash)]u8 = undefined; │ │219 - _ = try std.fmt.hexToBytes(&tree_bytes, &tree); │ │220 - // putKey only fills an empty key slot, and this one changes whenever │ │221 - // the branch moves │ │222 - var tree_cursor = try index.putKeyCursor(ctx.branch_hash); │ │223 - try tree_cursor.write(.{ .bytes = &tree_bytes }); │ │224 - var files_cursor = try index.putCursor(ctx.branch_hash); │ │225 - if (ctx.from) |from| try files_cursor.write(.{ .slot = from.files }); │ │226 - const files = try DB.SortedMap(.read_write).init(files_cursor); │ │208 + const index_hash = hash.hashInt(repo_opts.hash, index_key); │ │209 + var new_branch_base = ctx.base; │ │210 │ │211 var key: std.ArrayList(u8) = .empty; │ │212 defer key.deinit(ctx.allocator); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │230 - if (ctx.from) |from| { │ │231 - var tree_diff = tr.TreeDiff(.xit, repo_opts).init(ctx.allocator); │ │232 - defer tree_diff.deinit(); │ │233 - try tree_diff.compare(state.readOnly(), ctx.io, &from.tree, &tree, null); │ │234 - for (tree_diff.changes.keys(), tree_diff.changes.values()) |path, change| { │ │235 - try fileKey(ctx.allocator, &key, path); │ │236 - const oid = if (change.new) |*tree_entry| indexedOid(repo_opts.hash, tree_entry) else null; │ │237 - if (oid) |bytes| { │ │238 - try files.put(key.items, .{ .bytes = bytes }); │ │239 - } else { │ │240 - _ = try files.remove(key.items); │ │213 + │ │214 + for (ctx.work) |item| { │ │215 + const index = try DB.HashMap(.read_write).init(try moment.putCursor(index_hash)); │ │216 + const tree = item.tree orelse { │ │217 + _ = try index.remove(item.branch_hash); │ │218 + continue; │ │219 + }; │ │220 + // the entry this one starts from (null = index the whole tree) │ │221 + const from = item.own orelse new_branch_base; │ │222 + │ │223 + var tree_bytes: [hash.byteLen(repo_opts.hash)]u8 = undefined; │ │224 + _ = try std.fmt.hexToBytes(&tree_bytes, &tree); │ │225 + // putKey only fills an empty key slot, and this one changes │ │226 + // whenever the branch moves │ │227 + var tree_cursor = try index.putKeyCursor(item.branch_hash); │ │228 + try tree_cursor.write(.{ .bytes = &tree_bytes }); │ │229 + var files_cursor = try index.putCursor(item.branch_hash); │ │230 + if (from) |entry| try files_cursor.write(.{ .slot = entry.files }); │ │231 + const files = try DB.SortedMap(.read_write).init(files_cursor); │ │232 + │ │233 + if (from) |entry| { │ │234 + var tree_diff = tr.TreeDiff(.xit, repo_opts).init(ctx.allocator); │ │235 + defer tree_diff.deinit(); │ │236 + try tree_diff.compare(state.readOnly(), ctx.io, &entry.tree, &tree, null); │ │237 + for (tree_diff.changes.keys(), tree_diff.changes.values()) |path, change| { │ │238 + try fileKey(ctx.allocator, &key, path); │ │239 + const oid = if (change.new) |*tree_entry| indexedOid(repo_opts.hash, tree_entry) else null; │ │240 + if (oid) |bytes| { │ │241 + try files.put(key.items, .{ .bytes = bytes }); │ │242 + } else { │ │243 + _ = try files.remove(key.items); │ │244 + } │ │245 } │ │246 + } else { │ │247 + var paths = std.heap.ArenaAllocator.init(ctx.allocator); │ │248 + defer paths.deinit(); │ │249 + try indexTree(repo_opts, state.readOnly(), ctx.io, ctx.allocator, paths.allocator(), files, &key, "", &tree); │ │250 } │ │243 - } else { │ │244 - var paths = std.heap.ArenaAllocator.init(ctx.allocator); │ │245 - defer paths.deinit(); │ │246 - try indexTree(repo_opts, state.readOnly(), ctx.io, ctx.allocator, paths.allocator(), files, &key, "", &tree); │ │251 + │ │252 + // the first entry ever written becomes the base for the next │ │253 + // new branch, so only one branch walks a whole tree. │ │254 + if (new_branch_base == null) new_branch_base = .{ .tree = tree, .files = (try index.getSlot(item.branch_hash)) orelse unreachable };│ │255 + // a later entry copies an earlier one's slot, so everything │ │256 + // written so far has to be shared rather than mutated │ │257 + try ctx.core.db.freeze(); │ │258 } │ │259 } │ │260 }; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │261 │ │251 - // one transaction per changed ref, so a base copied into a new entry always │ │252 - // predates the transaction that mutates it. │ │253 - for (work.items) |item| { │ │254 - const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); │ │255 - try history.appendContext(.{ .slot = try history.getSlot(-1) }, Save{ │ │256 - .core = &repo.core, │ │257 - .io = io, │ │258 - .allocator = allocator, │ │259 - .branch_hash = item.branch_hash, │ │260 - .tree = item.tree, │ │261 - .from = item.own orelse base, │ │262 - }); │ │263 - // the first entry ever written becomes the base for the next new │ │264 - // branch, so only one branch walks a whole tree. │ │265 - if (base == null and item.tree != null) { │ │266 - var moment = try repo.core.latestMoment(); │ │267 - const index_cursor = (try moment.getCursor(hash.hashInt(repo_opts.hash, index_key))) orelse unreachable;│ │268 - const index = try DB.HashMap(.read_only).init(index_cursor); │ │269 - base = try Entry.init((try index.getKeyValuePair(item.branch_hash)) orelse unreachable); │ │270 - } │ │271 - } │ │262 + const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); │ │263 + try history.appendContext(.{ .slot = try history.getSlot(-1) }, Save{ │ │264 + .core = &repo.core, │ │265 + .io = io, │ │266 + .allocator = allocator, │ │267 + .work = work.items, │ │268 + .base = base, │ │269 + }); │ │270 } │ │271 │ │272 // insert every file under `oid` (a tree), as tr.Tree.read walks one. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯