╭ changed ────────────╮ │src/search_commit.zig│ ╰─────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │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. │ │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, │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 76 ) !void { │ │ 77 const Repo = rp.Repo(.xit, repo_opts); │ │ 78 const DB = Repo.DB; │ │ 79 + const Save = struct { │ │ 80 + core: *Repo.Core, │ │ 81 + io: std.Io, │ │ 82 + allocator: std.mem.Allocator, │ │ 83 + updates: ?[]const xit.net_server_receive_pack.AppliedRefUpdate, │ │ 84 + │ │ 85 + pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { │ │ 86 + var moment = try DB.HashMap(.read_write).init(cursor.*); │ │ 87 + const state = Repo.State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; │ │ 88 + if (!try refreshInTransaction(repo_opts, state, &moment, ctx.io, ctx.allocator, ctx.updates)) return error.CancelTransaction; │ │ 89 + │ │ 90 + // record the user action for undo │ │ 91 + try xit.undo.write(repo_opts, state, std.Io.Timestamp.now(ctx.io, .real).toSeconds(), .{ .custom = .{ .action_kind = undo_action } }); │ │ 92 + } │ │ 93 + }; │ │ 94 + │ │ 95 + // held across the reads and the writes, so a concurrent refresh can't │ │ 96 + // change the entries these decisions were made from. │ │ 97 + try repo.core.db_file.lock(io, .exclusive); │ │ 98 + defer repo.core.db_file.unlock(io); │ │ 99 + │ │100 + const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); │ │101 + history.appendContext(.{ .slot = try history.getSlot(-1) }, Save{ .core = &repo.core, .io = io, .allocator = allocator, .updates = updates }) catch |err| switch (err) {│ │102 + error.CancelTransaction => {}, │ │103 + else => return err, │ │104 + }; │ │105 +} │ │106 + │ │107 +// reconcile them inside the caller's transaction, so the index lands with the │ │108 +// push that invalidated it. `updates` is that push, whose old oids are the │ │109 +// cheapest bases to derive the new tips from. reports whether anything was │ │110 +// written, so a caller that owns an otherwise empty transaction can cancel it │ │111 +pub fn refreshInTransaction( │ │112 + comptime repo_opts: rp.RepoOpts(.xit), │ │113 + state: rp.Repo(.xit, repo_opts).State(.read_write), │ │114 + moment: *rp.Repo(.xit, repo_opts).DB.HashMap(.read_write), │ │115 + io: std.Io, │ │116 + allocator: std.mem.Allocator, │ │117 + updates: ?[]const xit.net_server_receive_pack.AppliedRefUpdate, │ │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 Tip = Commit(repo_opts.hash); │ │123 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │138 var default_tip: ?Oid = null; │ │139 // the versions a new tip can derive from │ │140 var versions: std.ArrayList(Tip) = .empty; │ │ 97 - │ │ 98 - // held across the reads and the writes, so a concurrent refresh can't │ │ 99 - // change the entries these decisions were made from. │ │100 - try repo.core.db_file.lock(io, .exclusive); │ │101 - defer repo.core.db_file.unlock(io); │ │141 │ │142 { │ │104 - var moment = try repo.core.latestMoment(); │ │105 - const state = Repo.State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } };│ │143 + const read_state = state.readOnly(); │ │144 │ │145 // the commits that already have a version │ │146 var existing: std.AutoArrayHashMapUnmanaged(Oid, void) = .empty; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │158 // what every head except the events branch and every tag points at │ │159 var tips: std.ArrayList(Oid) = .empty; │ │160 { │ │123 - var branches = try repo.listBranches(io, allocator, .beginning); │ │161 + var branches = try rf.RefIterator(.xit, repo_opts).init(read_state, io, allocator, .head, .beginning);│ │162 defer branches.deinit(); │ │163 while (try branches.next()) |ref| { │ │164 if (std.mem.eql(u8, ref.name, evt.events_ref.name)) continue; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │127 - try tips.append(aa, (try repo.readRef(io, .{ .kind = .head, .name = ref.name })) orelse continue); │ │165 + try tips.append(aa, (try rf.readRecur(.xit, repo_opts, read_state, io, .{ .ref = .{ .kind = .head, .name = ref.name } })) orelse continue);│ │166 } │ │167 } │ │168 { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │131 - var tags = try repo.listTags(io, allocator, .beginning); │ │169 + var tags = try rf.RefIterator(.xit, repo_opts).init(read_state, io, allocator, .tag, .beginning); │ │170 defer tags.deinit(); │ │171 while (try tags.next()) |ref| { │ │134 - try tips.append(aa, (try repo.readRef(io, .{ .kind = .tag, .name = ref.name })) orelse continue); │ │172 + try tips.append(aa, (try rf.readRecur(.xit, repo_opts, read_state, io, .{ .ref = .{ .kind = .tag, .name = ref.name } })) orelse continue);│ │173 } │ │174 } │ │175 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │183 try wanted.put(aa, tip, {}); │ │184 continue; │ │185 } │ │148 - var commit = obj.Object(.xit, repo_opts).initCommit(state, io, allocator, &tip) catch continue; │ │186 + var commit = obj.Object(.xit, repo_opts).initCommit(read_state, io, allocator, &tip) catch continue;│ │187 defer commit.deinit(); │ │188 try wanted.put(aa, commit.oid, {}); │ │189 if (existing.contains(commit.oid)) continue; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │197 } │ │198 │ │199 var head_buffer: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; │ │162 - if (repo.head(io, &head_buffer)) |head| switch (head) { │ │163 - .ref => |ref| default_tip = try repo.readRef(io, .{ .kind = .head, .name = ref.name }), │ │200 + if (rf.readHead(.xit, repo_opts, read_state, io, &head_buffer) catch null) |head| switch (head) { │ │201 + .ref => |ref| default_tip = try rf.readRecur(.xit, repo_opts, read_state, io, .{ .ref = .{ .kind = .head, .name = ref.name } }),│ │202 .oid => {}, │ │165 - } else |_| {} │ │203 + }; │ │204 │ │205 // an entry whose commit is gone can't be walked from, so it isn't a │ │206 // candidate. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │208 if (item.base == null) break true; │ │209 } else false; │ │210 if (unbased) for (existing.keys()) |oid| { │ │173 - var commit = obj.Object(.xit, repo_opts).initCommit(state, io, allocator, &oid) catch continue; │ │211 + var commit = obj.Object(.xit, repo_opts).initCommit(read_state, io, allocator, &oid) catch continue;│ │212 defer commit.deinit(); │ │213 try versions.append(aa, .{ .oid = oid, .timestamp = commit.content.commit.metadata.timestamp }); │ │214 }; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │225 if (!wanted.contains(oid)) try gone.append(aa, oid); │ │226 } │ │227 } │ │190 - │ │191 - if (work.items.len == 0 and gone.items.len == 0) return; │ │192 - │ │193 - const Save = struct { │ │194 - core: *Repo.Core, │ │195 - io: std.Io, │ │196 - allocator: std.mem.Allocator, │ │197 - work: []const Work, │ │198 - gone: []const Oid, │ │199 - aa: std.mem.Allocator, │ │200 - default_tip: ?Oid, │ │201 - versions: *std.ArrayList(Tip), │ │228 │ │203 - pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { │ │204 - var moment = try DB.HashMap(.read_write).init(cursor.*); │ │205 - const state = Repo.State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; │ │206 - const index_hash = hash.hashInt(repo_opts.hash, index_key); │ │229 + if (work.items.len == 0 and gone.items.len == 0) return false; │ │230 │ │208 - var message: std.ArrayList(u8) = .empty; │ │209 - defer message.deinit(ctx.allocator); │ │231 + { │ │232 + const index_hash = hash.hashInt(repo_opts.hash, index_key); │ │233 │ │211 - for (ctx.work) |item| { │ │212 - const index = try DB.HashMap(.read_write).init(try moment.putCursor(index_hash)); │ │213 - const base = item.base orelse nearestVersion(repo_opts.hash, ctx.versions.items, ctx.default_tip, item.timestamp); │ │234 + var message: std.ArrayList(u8) = .empty; │ │235 + defer message.deinit(allocator); │ │236 │ │215 - var oid_bytes: [hash.byteLen(repo_opts.hash)]u8 = undefined; │ │216 - _ = try std.fmt.hexToBytes(&oid_bytes, &item.oid); │ │217 - const oid_int = hash.bytesToInt(repo_opts.hash, &oid_bytes); │ │218 - try index.putKey(oid_int, .{ .bytes = &oid_bytes }); │ │219 - var version_cursor = try index.putCursor(oid_int); │ │220 - if (base) |base_oid| { │ │221 - const base_cursor = (try index.getCursor(try hash.hexToInt(repo_opts.hash, &base_oid))) orelse return error.InvalidCommitIndex; │ │222 - try version_cursor.write(.{ .slot = base_cursor.slot() }); │ │223 - } │ │224 - const version = try DB.SortedMap(.read_write).init(version_cursor); │ │237 + for (work.items) |item| { │ │238 + const index = try DB.HashMap(.read_write).init(try moment.putCursor(index_hash)); │ │239 + const base = item.base orelse nearestVersion(repo_opts.hash, versions.items, default_tip, item.timestamp); │ │240 │ │226 - var diff_arena = std.heap.ArenaAllocator.init(ctx.allocator); │ │227 - defer diff_arena.deinit(); │ │228 - 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);│ │229 - for (diff.removed) |commit| { │ │230 - const key = try readPosting(repo_opts, state.readOnly(), ctx.io, ctx.allocator, &message, commit); │ │231 - try srch.remove(DB, version, ctx.allocator, &key, message.items); │ │232 - } │ │233 - for (diff.added) |commit| { │ │234 - const key = try readPosting(repo_opts, state.readOnly(), ctx.io, ctx.allocator, &message, commit); │ │235 - try srch.add(DB, version, ctx.allocator, &key, message.items); │ │236 - } │ │241 + var oid_bytes: [hash.byteLen(repo_opts.hash)]u8 = undefined; │ │242 + _ = try std.fmt.hexToBytes(&oid_bytes, &item.oid); │ │243 + const oid_int = hash.bytesToInt(repo_opts.hash, &oid_bytes); │ │244 + try index.putKey(oid_int, .{ .bytes = &oid_bytes }); │ │245 + var version_cursor = try index.putCursor(oid_int); │ │246 + if (base) |base_oid| { │ │247 + const base_cursor = (try index.getCursor(try hash.hexToInt(repo_opts.hash, &base_oid))) orelse return error.InvalidCommitIndex; │ │248 + try version_cursor.write(.{ .slot = base_cursor.slot() }); │ │249 + } │ │250 + const version = try DB.SortedMap(.read_write).init(version_cursor); │ │251 │ │238 - try ctx.versions.append(ctx.aa, .{ .oid = item.oid, .timestamp = item.timestamp }); │ │239 - // the next version copies this one's slot, so everything │ │240 - // written so far has to be shared rather than mutated │ │241 - try ctx.core.db.freeze(); │ │252 + var diff_arena = std.heap.ArenaAllocator.init(allocator); │ │253 + defer diff_arena.deinit(); │ │254 + const diff = try commitDiff(repo_opts, state.readOnly(), io, allocator, diff_arena.allocator(), if (base) |*base_oid| base_oid else null, &item.oid); │ │255 + for (diff.removed) |commit| { │ │256 + const key = try readPosting(repo_opts, state.readOnly(), io, allocator, &message, commit); │ │257 + try srch.remove(DB, version, allocator, &key, message.items); │ │258 } │ │259 + for (diff.added) |commit| { │ │260 + const key = try readPosting(repo_opts, state.readOnly(), io, allocator, &message, commit); │ │261 + try srch.add(DB, version, allocator, &key, message.items); │ │262 + } │ │263 │ │244 - // removing last keeps a tip that is going away available as a base │ │245 - const index = try DB.HashMap(.read_write).init(try moment.putCursor(index_hash)); │ │246 - for (ctx.gone) |oid| _ = try index.remove(try hash.hexToInt(repo_opts.hash, &oid)); │ │264 + try versions.append(aa, .{ .oid = item.oid, .timestamp = item.timestamp }); │ │265 + // the next version copies this one's slot, so everything │ │266 + // written so far has to be shared rather than mutated │ │267 + try state.core.db.freeze(); │ │268 } │ │248 - }; │ │269 │ │250 - const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); │ │251 - try history.appendContext(.{ .slot = try history.getSlot(-1) }, Save{ │ │252 - .core = &repo.core, │ │253 - .io = io, │ │254 - .allocator = allocator, │ │255 - .work = work.items, │ │256 - .gone = gone.items, │ │257 - .aa = aa, │ │258 - .default_tip = default_tip, │ │259 - .versions = &versions, │ │260 - }); │ │270 + // removing last keeps a tip that is going away available as a base │ │271 + const index = try DB.HashMap(.read_write).init(try moment.putCursor(index_hash)); │ │272 + for (gone.items) |oid| _ = try index.remove(try hash.hexToInt(repo_opts.hash, &oid)); │ │273 + } │ │274 + return true; │ │275 } │ │276 │ │277 // the version a tip committed at `timestamp` derives from when no push names │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯