╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ find.zig ─────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭ find file ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ ╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ │ │ │http│ ssh │ │ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯
79 comptime repo_opts: rp.RepoOpts(.xit), 80 files: rp.Repo(.xit, repo_opts).DB.SortedMap(.read_only), 81 aa: std.mem.Allocator, 82 path: []const u8, 83 ) !?[hash.byteLen(repo_opts.hash)]u8 { 84 var key: std.ArrayList(u8) = .empty; 85 try fileKey(aa, &key, path); 86 const cursor = (try files.getCursor(key.items)) orelse return null; 87 return try parseOid(repo_opts.hash, try cursor.readBytesAlloc(aa, null)); 88 } 89 90 // peel an oid to the root tree it names, following commits and tags. 91 fn rootTreeOid( 92 comptime repo_opts: rp.RepoOpts(.xit), 93 state: rp.Repo(.xit, repo_opts).State(.read_only), 94 io: std.Io, 95 allocator: std.mem.Allocator, 96 oid: *const [hash.hexLen(repo_opts.hash)]u8, 97 ) ![hash.hexLen(repo_opts.hash)]u8 { 98 var current = oid.*; 99 while (true) { 100 var object = try obj.Object(.xit, repo_opts).init(state, io, allocator, &current); 101 defer object.deinit(); 102 switch (object.content) { 103 .tree => return current, 104 .commit => |commit| current = commit.tree, 105 .tag => |tag| current = tag.target, 106 .blob => return error.ObjectInvalid, 107 } 108 } 109 } 110 111 // reconcile them inside the caller's transaction, so the index lands with the 112 // change that invalidated it. every branch is reconciled, since an up-to-date 113 // one costs a ref read and an oid compare. reports whether anything was 114 // written, so a caller that owns an otherwise empty transaction can cancel it 115 pub fn refreshInTransaction( 116 comptime repo_opts: rp.RepoOpts(.xit), 117 state: rp.Repo(.xit, repo_opts).State(.read_write), 118 moment: *rp.Repo(.xit, repo_opts).DB.HashMap(.read_write), 119 io: std.Io, 120 allocator: std.mem.Allocator, 121 progress_ctx_maybe: ?repo_opts.ProgressCtx, 122 ) !bool { 123 const Repo = rp.Repo(.xit, repo_opts); 124 const DB = Repo.DB; 125 const HashInt = hash.HashInt(repo_opts.hash); 126 const Oid = [hash.hexLen(repo_opts.hash)]u8; 127 128 // an entry in the index: the tip it covers and its file map 129 const Entry = struct { 130 oid: Oid, 131 files: xit.xitdb.Slot, 132 133 fn init(pair: DB.KeyValuePairCursor(.read_only)) !@This() { 134 return .{ .oid = try readOid(repo_opts, pair.key_cursor), .files = pair.value_cursor.slot() }; 135 } 136 }; 137 // what one branch needs written 138 const Work = struct { 139 branch_hash: HashInt, 140 // the tip (null = the branch is gone, so its entry goes) 141 oid: ?Oid, 142 // the entry the branch has now 143 own: ?Entry, 144 }; 145 146 var arena = std.heap.ArenaAllocator.init(allocator); 147 defer arena.deinit(); 148 const aa = arena.allocator(); 149 150 var work: std.ArrayList(Work) = .empty; 151 var base: ?Entry = null; 152 153 { 154 const read_state = state.readOnly(); 155 156 // entries carry no branch name, so branches are matched by hash 157 var existing: std.AutoArrayHashMapUnmanaged(HashInt, Entry) = .empty; 158 if (try moment.getCursor(hash.hashInt(repo_opts.hash, index_key))) |index_cursor| { 159 const index = try DB.HashMap(.read_only).init(index_cursor); 160 var iter = try index.iterator(); 161 while (try iter.next()) |cursor| { 162 const pair = try cursor.readKeyValuePair(); 163 try existing.put(aa, pair.hash, try Entry.init(pair)); 164 } 165 } 166 167 // a brand-new branch copies the default branch's map when there is one, 168 // else any other branch's, so only its own changes are written. 169 if (existing.count() > 0) base = existing.values()[0]; 170 var head_buffer: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; 171 if (rf.readHead(.xit, repo_opts, read_state, io, &head_buffer) catch null) |head| switch (head) { 172 .ref => |ref| if (existing.get(hash.hashInt(repo_opts.hash, ref.name))) |entry| { 173 base = entry; 174 }, 175 .oid => {}, 176 }; 177 178 var branches = try rf.RefIterator(.xit, repo_opts).init(read_state, io, allocator, .head, .beginning); 179 defer branches.deinit(); 180 while (try branches.next()) |ref| { 181 if (std.mem.eql(u8, ref.name, evt.events_ref.name)) continue; 182 const branch_hash = hash.hashInt(repo_opts.hash, ref.name); 183 const own = if (existing.fetchSwapRemove(branch_hash)) |kv| kv.value else null; 184 const oid = (try rf.readRecur(.xit, repo_opts, read_state, io, .{ .ref = .{ .kind = .head, .name = ref.name } })) orelse continue; 185 if (own) |entry| if (std.mem.eql(u8, &entry.oid, &oid)) continue; 186 try work.append(aa, .{ .branch_hash = branch_hash, .oid = oid, .own = own }); 187 } 188 189 // whatever is left belongs to a branch that no longer exists 190 for (existing.keys()) |branch_hash| try work.append(aa, .{ .branch_hash = branch_hash, .oid = null, .own = null }); 191 } 192 193 if (work.items.len == 0) return false; 194 195 { 196 const index_hash = hash.hashInt(repo_opts.hash, index_key); 197 var new_branch_base = base; 198 progress.report(repo_opts, io, progress_ctx_maybe, .{ .text = "Indexing files" }); 199 200 var key: std.ArrayList(u8) = .empty; 201 defer key.deinit(allocator); 202 203 for (work.items) |item| { 204 if (progress.cancelled(repo_opts, progress_ctx_maybe)) return error.ClientGone; 205 const index = try DB.HashMap(.read_write).init(try moment.putCursor(index_hash)); 206 const tip = item.oid orelse { 207 _ = try index.remove(item.branch_hash); 208 continue; 209 }; 210 // the entry this one starts from (null = index the whole tree) 211 const from = item.own orelse new_branch_base; 212 213 var tip_bytes: [hash.byteLen(repo_opts.hash)]u8 = undefined; 214 _ = try std.fmt.hexToBytes(&tip_bytes, &tip); 215 // putKey only fills an empty key slot, and this one changes 216 // whenever the branch moves 217 var tip_cursor = try index.putKeyCursor(item.branch_hash); 218 try tip_cursor.write(.{ .bytes = &tip_bytes }); 219 var files_cursor = try index.putCursor(item.branch_hash); 220 if (from) |entry| try files_cursor.write(.{ .slot = entry.files }); 221 const files = try DB.SortedMap(.read_write).init(files_cursor); 222 223 const tree = try rootTreeOid(repo_opts, state.readOnly(), io, allocator, &tip); 224 if (from) |entry| { 225 const from_tree = try rootTreeOid(repo_opts, state.readOnly(), io, allocator, &entry.oid); 226 var tree_diff = tr.TreeDiff(.xit, repo_opts).init(allocator); 227 defer tree_diff.deinit(); 228 try tree_diff.compare(state.readOnly(), io, &from_tree, &tree, null); 229 progress.report(repo_opts, io, progress_ctx_maybe, .{ .start = .{ .kind = .writing_patch, .estimated_total_items = tree_diff.changes.count() } }); 230 for (tree_diff.changes.keys(), tree_diff.changes.values()) |path, change| { 231 defer progress.report(repo_opts, io, progress_ctx_maybe, .{ .complete_one = .writing_patch }); 232 try fileKey(allocator, &key, path); 233 const oid = if (change.new) |*tree_entry| indexedOid(repo_opts.hash, tree_entry) else null; 234 if (oid) |bytes| { 235 try files.put(key.items, .{ .bytes = bytes }); 236 } else { 237 _ = try files.remove(key.items); 238 } 239 } 240 } else { 241 // a whole tree has no count until it is walked, so it reports 242 // the files it has indexed so far 243 progress.report(repo_opts, io, progress_ctx_maybe, .{ .start = .{ .kind = .writing_patch, .estimated_total_items = 0 } }); 244 var paths = std.heap.ArenaAllocator.init(allocator); 245 defer paths.deinit(); 246 try indexTree(repo_opts, state.readOnly(), io, allocator, paths.allocator(), files, &key, "", &tree, progress_ctx_maybe); 247 } 248 progress.report(repo_opts, io, progress_ctx_maybe, .{ .end = .writing_patch }); 249 250 // the first entry ever written becomes the base for the next 251 // new branch, so only one branch walks a whole tree. 252 if (new_branch_base == null) new_branch_base = .{ .oid = tip, .files = (try index.getSlot(item.branch_hash)) orelse unreachable }; 253 // a later entry copies an earlier one's slot, so everything 254 // written so far has to be shared rather than mutated 255 try state.core.db.freeze(); 256 } 257 } 258 return true; 259 } 260 261 // insert every file under `oid` (a tree), as tr.Tree.read walks one. 262 fn indexTree( 263 comptime repo_opts: rp.RepoOpts(.xit), 264 state: rp.Repo(.xit, repo_opts).State(.read_only), 265 io: std.Io, 266 allocator: std.mem.Allocator, 267 paths: std.mem.Allocator, 268 files: rp.Repo(.xit, repo_opts).DB.SortedMap(.read_write), 269 key: *std.ArrayList(u8), 270 prefix: []const u8, 271 oid: *const [hash.hexLen(repo_opts.hash)]u8, 272 progress_ctx_maybe: ?repo_opts.ProgressCtx, 273 ) !void { 274 var object = try obj.Object(.xit, repo_opts).init(state, io, allocator, oid); 275 defer object.deinit(); 276 const tree = switch (object.content) { 277 .tree => |tree| tree, 278 else => return, 279 }; 280 for (tree.entries.keys(), tree.entries.values()) |name, *tree_entry| { 281 if (progress.cancelled(repo_opts, progress_ctx_maybe)) return error.ClientGone; 282 const path = try fs.joinPath(paths, &.{ prefix, name }); 283 if (tree_entry.isTree()) { 284 try indexTree(repo_opts, state, io, allocator, paths, files, key, path, &std.fmt.bytesToHex(tree_entry.oid, .lower), progress_ctx_maybe); 285 } else if (indexedOid(repo_opts.hash, tree_entry)) |bytes| { 286 try fileKey(allocator, key, path); 287 try files.put(key.items, .{ .bytes = bytes }); 288 progress.report(repo_opts, io, progress_ctx_maybe, .{ .complete_one = .writing_patch }); 289 } 290 } 291 } 292 293 // the tip an index entry covers, read from its key slot as hex. 294 fn readOid( 295 comptime repo_opts: rp.RepoOpts(.xit), 296 key_cursor: rp.Repo(.xit, repo_opts).DB.Cursor(.read_only), 297 ) ![hash.hexLen(repo_opts.hash)]u8 { 298 var oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 299 if ((try key_cursor.readBytes(&oid)).len != oid.len) return error.InvalidFileIndex; 300 return std.fmt.bytesToHex(oid, .lower); 301 } 302 303 // build a file's key: its lowercased basename, a separator, then its full path. 304 fn fileKey(allocator: std.mem.Allocator, key: *std.ArrayList(u8), path: []const u8) !void { 305 const slash = std.mem.lastIndexOfScalar(u8, path, '/'); 306 const base = if (slash) |i| path[i + 1 ..] else path; 307 key.clearRetainingCapacity(); 308 try key.ensureTotalCapacity(allocator, base.len + 1 + path.len); 309 for (base) |char| key.appendAssumeCapacity(std.ascii.toLower(char)); 310 key.appendAssumeCapacity(0); 311 key.appendSliceAssumeCapacity(path); 312 } 313 314 // the blob oid a tree entry is indexed under, or null for a submodule: its oid 315 // is a commit in another repo, not a file here. 316 fn indexedOid(comptime hash_kind: hash.HashKind, tree_entry: *const tr.TreeEntry(hash_kind)) ?[]const u8 { 317 if (tree_entry.mode.content.object_type == .gitlink) return null; 318 return &tree_entry.oid; 319 } 320 321 fn parseOid(comptime hash_kind: hash.HashKind, value: []const u8) ![hash.byteLen(hash_kind)]u8 { 322 const len = comptime hash.byteLen(hash_kind); 323 if (value.len != len) return error.InvalidFileIndex; 324 return value[0..len].*; 325 } 326