╭ find.zig ─────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("event.zig"); 3 const xit = @import("xit"); 4 const rp = xit.repo; 5 const hash = xit.hash; 6 const rf = xit.ref; 7 const tr = xit.tree; 8 const obj = xit.object; 9 const fs = xit.fs; 10 const serve_common = @import("serve_common.zig"); 11 12 // the repo moment key holding one file map per branch tip. it sits outside the 13 // haxy moment because pushes change it and events never do. each entry is 14 // keyed by its branch name's hash: its value is the branch's file map, and its 15 // key slot holds the root tree oid that map covers rather than the name. 16 const index_key = "haxy/branch->file-name+path->oid"; 17 18 // what a search returns: the matching files and whether the cap cut the list 19 // short. 20 pub fn Results(comptime hash_kind: hash.HashKind) type { 21 return struct { 22 matches: []const Match, 23 capped: bool = false, 24 25 pub const Match = struct { 26 path: []const u8, 27 oid: [hash.byteLen(hash_kind)]u8, 28 }; 29 }; 30 } 31 32 // `branch`'s file index, or null when it has none or its entry is stale: one 33 // that doesn't cover `tree_oid`, the branch tip's root tree. 34 pub fn lookup( 35 comptime repo_opts: rp.RepoOpts(.xit), 36 moment: rp.Repo(.xit, repo_opts).DB.HashMap(.read_only), 37 branch: []const u8, 38 tree_oid: *const [hash.hexLen(repo_opts.hash)]u8, 39 ) !?rp.Repo(.xit, repo_opts).DB.SortedMap(.read_only) { 40 const DB = rp.Repo(.xit, repo_opts).DB; 41 const index_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, index_key)) orelse return null; 42 const index = try DB.HashMap(.read_only).init(index_cursor); 43 const pair = try index.getKeyValuePair(hash.hashInt(repo_opts.hash, branch)) orelse return null; 44 const entry_tree = try readTree(repo_opts, pair.key_cursor); 45 if (!std.mem.eql(u8, &entry_tree, tree_oid)) return null; 46 return try DB.SortedMap(.read_only).init(pair.value_cursor); 47 } 48 49 // the indexed files whose basename starts with `term`, ordered by basename then 50 // path. the separator between basename and path keeps a match inside the 51 // basename, so the walk stops at the first key that doesn't share the prefix. 52 pub fn search( 53 comptime repo_opts: rp.RepoOpts(.xit), 54 files: rp.Repo(.xit, repo_opts).DB.SortedMap(.read_only), 55 aa: std.mem.Allocator, 56 term: []const u8, 57 max: usize, 58 ) !Results(repo_opts.hash) { 59 const Match = Results(repo_opts.hash).Match; 60 const prefix = try aa.alloc(u8, term.len); 61 _ = std.ascii.lowerString(prefix, term); 62 63 var matches: std.ArrayList(Match) = .empty; 64 var iter = try files.iteratorFrom(prefix); 65 while (try iter.next()) |cursor| { 66 const pair = try cursor.readKeyValuePair(); 67 const key = try pair.key_cursor.readBytesAlloc(aa, null); 68 if (!std.mem.startsWith(u8, key, prefix)) break; 69 if (matches.items.len == max) return .{ .matches = matches.items, .capped = true }; 70 const separator = std.mem.indexOfScalar(u8, key, 0) orelse return error.InvalidFileIndex; 71 const value = try pair.value_cursor.readBytesAlloc(aa, null); 72 try matches.append(aa, .{ .path = key[separator + 1 ..], .oid = try parseOid(repo_opts.hash, value) }); 73 } 74 return .{ .matches = matches.items }; 75 } 76 77 // the indexed file at `path`, for a selected result the cap cut off. 78 pub fn get( 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 pub 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 // the action a file index refresh records when it runs on its own 112 pub const undo_action = "haxy/find-index"; 113 114 // reconcile every branch tip's index, in a transaction of its own 115 pub fn refresh( 116 comptime repo_opts: rp.RepoOpts(.xit), 117 io: std.Io, 118 allocator: std.mem.Allocator, 119 repo: *rp.Repo(.xit, repo_opts), 120 ) !void { 121 const Repo = rp.Repo(.xit, repo_opts); 122 const DB = Repo.DB; 123 const Save = struct { 124 core: *Repo.Core, 125 io: std.Io, 126 allocator: std.mem.Allocator, 127 128 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 129 var moment = try DB.HashMap(.read_write).init(cursor.*); 130 const state = Repo.State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 131 if (!try refreshInTransaction(repo_opts, state, &moment, ctx.io, ctx.allocator, null)) return error.CancelTransaction; 132 133 // record the user action for undo 134 try xit.undo.write(repo_opts, state, std.Io.Timestamp.now(ctx.io, .real).toSeconds(), .{ .custom = .{ .action_kind = undo_action } }); 135 } 136 }; 137 138 // held across the reads and the writes, so a concurrent refresh can't 139 // change the entries these decisions were made from. 140 try repo.core.db_file.lock(io, .exclusive); 141 defer repo.core.db_file.unlock(io); 142 143 const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); 144 history.appendContext(.{ .slot = try history.getSlot(-1) }, Save{ .core = &repo.core, .io = io, .allocator = allocator }) catch |err| switch (err) { 145 error.CancelTransaction => {}, 146 else => return err, 147 }; 148 } 149 150 // reconcile them inside the caller's transaction, so the index lands with the 151 // push that invalidated it. every branch is reconciled, since an up-to-date 152 // one costs a ref read and an oid compare. reports whether anything was 153 // written, so a caller that owns an otherwise empty transaction can cancel it 154 pub fn refreshInTransaction( 155 comptime repo_opts: rp.RepoOpts(.xit), 156 state: rp.Repo(.xit, repo_opts).State(.read_write), 157 moment: *rp.Repo(.xit, repo_opts).DB.HashMap(.read_write), 158 io: std.Io, 159 allocator: std.mem.Allocator, 160 progress_ctx_maybe: ?repo_opts.ProgressCtx, 161 ) !bool { 162 const Repo = rp.Repo(.xit, repo_opts); 163 const DB = Repo.DB; 164 const HashInt = hash.HashInt(repo_opts.hash); 165 const Tree = [hash.hexLen(repo_opts.hash)]u8; 166 167 // an entry in the index: the root tree it covers and its file map 168 const Entry = struct { 169 tree: Tree, 170 files: xit.xitdb.Slot, 171 172 fn init(pair: DB.KeyValuePairCursor(.read_only)) !@This() { 173 return .{ .tree = try readTree(repo_opts, pair.key_cursor), .files = pair.value_cursor.slot() }; 174 } 175 }; 176 // what one branch needs written 177 const Work = struct { 178 branch_hash: HashInt, 179 // the tip's root tree (null = the branch is gone, so its entry goes) 180 tree: ?Tree, 181 // the entry the branch has now 182 own: ?Entry, 183 }; 184 185 var arena = std.heap.ArenaAllocator.init(allocator); 186 defer arena.deinit(); 187 const aa = arena.allocator(); 188 189 var work: std.ArrayList(Work) = .empty; 190 var base: ?Entry = null; 191 192 { 193 const read_state = state.readOnly(); 194 195 // entries carry no branch name, so branches are matched by hash 196 var existing: std.AutoArrayHashMapUnmanaged(HashInt, Entry) = .empty; 197 if (try moment.getCursor(hash.hashInt(repo_opts.hash, index_key))) |index_cursor| { 198 const index = try DB.HashMap(.read_only).init(index_cursor); 199 var iter = try index.iterator(); 200 while (try iter.next()) |cursor| { 201 const pair = try cursor.readKeyValuePair(); 202 try existing.put(aa, pair.hash, try Entry.init(pair)); 203 } 204 } 205 206 // a brand-new branch copies the default branch's map when there is one, 207 // else any other branch's, so only its own changes are written. 208 if (existing.count() > 0) base = existing.values()[0]; 209 var head_buffer: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; 210 if (rf.readHead(.xit, repo_opts, read_state, io, &head_buffer) catch null) |head| switch (head) { 211 .ref => |ref| if (existing.get(hash.hashInt(repo_opts.hash, ref.name))) |entry| { 212 base = entry; 213 }, 214 .oid => {}, 215 }; 216 217 var branches = try rf.RefIterator(.xit, repo_opts).init(read_state, io, allocator, .head, .beginning); 218 defer branches.deinit(); 219 while (try branches.next()) |ref| { 220 if (std.mem.eql(u8, ref.name, evt.events_ref.name)) continue; 221 const branch_hash = hash.hashInt(repo_opts.hash, ref.name); 222 const own = if (existing.fetchSwapRemove(branch_hash)) |kv| kv.value else null; 223 const oid = (try rf.readRecur(.xit, repo_opts, read_state, io, .{ .ref = .{ .kind = .head, .name = ref.name } })) orelse continue; 224 const tree = try rootTreeOid(repo_opts, read_state, io, allocator, &oid); 225 if (own) |entry| if (std.mem.eql(u8, &entry.tree, &tree)) continue; 226 try work.append(aa, .{ .branch_hash = branch_hash, .tree = tree, .own = own }); 227 } 228 229 // whatever is left belongs to a branch that no longer exists 230 for (existing.keys()) |branch_hash| try work.append(aa, .{ .branch_hash = branch_hash, .tree = null, .own = null }); 231 } 232 233 if (work.items.len == 0) return false; 234 235 { 236 const index_hash = hash.hashInt(repo_opts.hash, index_key); 237 var new_branch_base = base; 238 serve_common.reportProgress(repo_opts, io, progress_ctx_maybe, .{ .text = "Indexing files" }); 239 240 var key: std.ArrayList(u8) = .empty; 241 defer key.deinit(allocator); 242 243 for (work.items) |item| { 244 if (serve_common.progressCancelled(repo_opts, progress_ctx_maybe)) return error.ClientGone; 245 const index = try DB.HashMap(.read_write).init(try moment.putCursor(index_hash)); 246 const tree = item.tree orelse { 247 _ = try index.remove(item.branch_hash); 248 continue; 249 }; 250 // the entry this one starts from (null = index the whole tree) 251 const from = item.own orelse new_branch_base; 252 253 var tree_bytes: [hash.byteLen(repo_opts.hash)]u8 = undefined; 254 _ = try std.fmt.hexToBytes(&tree_bytes, &tree); 255 // putKey only fills an empty key slot, and this one changes 256 // whenever the branch moves 257 var tree_cursor = try index.putKeyCursor(item.branch_hash); 258 try tree_cursor.write(.{ .bytes = &tree_bytes }); 259 var files_cursor = try index.putCursor(item.branch_hash); 260 if (from) |entry| try files_cursor.write(.{ .slot = entry.files }); 261 const files = try DB.SortedMap(.read_write).init(files_cursor); 262 263 if (from) |entry| { 264 var tree_diff = tr.TreeDiff(.xit, repo_opts).init(allocator); 265 defer tree_diff.deinit(); 266 try tree_diff.compare(state.readOnly(), io, &entry.tree, &tree, null); 267 serve_common.reportProgress(repo_opts, io, progress_ctx_maybe, .{ .start = .{ .kind = .writing_patch, .estimated_total_items = tree_diff.changes.count() } }); 268 for (tree_diff.changes.keys(), tree_diff.changes.values()) |path, change| { 269 defer serve_common.reportProgress(repo_opts, io, progress_ctx_maybe, .{ .complete_one = .writing_patch }); 270 try fileKey(allocator, &key, path); 271 const oid = if (change.new) |*tree_entry| indexedOid(repo_opts.hash, tree_entry) else null; 272 if (oid) |bytes| { 273 try files.put(key.items, .{ .bytes = bytes }); 274 } else { 275 _ = try files.remove(key.items); 276 } 277 } 278 } else { 279 // a whole tree has no count until it is walked, so it reports 280 // the files it has indexed so far 281 serve_common.reportProgress(repo_opts, io, progress_ctx_maybe, .{ .start = .{ .kind = .writing_patch, .estimated_total_items = 0 } }); 282 var paths = std.heap.ArenaAllocator.init(allocator); 283 defer paths.deinit(); 284 try indexTree(repo_opts, state.readOnly(), io, allocator, paths.allocator(), files, &key, "", &tree, progress_ctx_maybe); 285 } 286 serve_common.reportProgress(repo_opts, io, progress_ctx_maybe, .{ .end = .writing_patch }); 287 288 // the first entry ever written becomes the base for the next 289 // new branch, so only one branch walks a whole tree. 290 if (new_branch_base == null) new_branch_base = .{ .tree = tree, .files = (try index.getSlot(item.branch_hash)) orelse unreachable }; 291 // a later entry copies an earlier one's slot, so everything 292 // written so far has to be shared rather than mutated 293 try state.core.db.freeze(); 294 } 295 } 296 return true; 297 } 298 299 // insert every file under `oid` (a tree), as tr.Tree.read walks one. 300 fn indexTree( 301 comptime repo_opts: rp.RepoOpts(.xit), 302 state: rp.Repo(.xit, repo_opts).State(.read_only), 303 io: std.Io, 304 allocator: std.mem.Allocator, 305 paths: std.mem.Allocator, 306 files: rp.Repo(.xit, repo_opts).DB.SortedMap(.read_write), 307 key: *std.ArrayList(u8), 308 prefix: []const u8, 309 oid: *const [hash.hexLen(repo_opts.hash)]u8, 310 progress_ctx_maybe: ?repo_opts.ProgressCtx, 311 ) !void { 312 var object = try obj.Object(.xit, repo_opts).init(state, io, allocator, oid); 313 defer object.deinit(); 314 const tree = switch (object.content) { 315 .tree => |tree| tree, 316 else => return, 317 }; 318 for (tree.entries.keys(), tree.entries.values()) |name, *tree_entry| { 319 if (serve_common.progressCancelled(repo_opts, progress_ctx_maybe)) return error.ClientGone; 320 const path = try fs.joinPath(paths, &.{ prefix, name }); 321 if (tree_entry.isTree()) { 322 try indexTree(repo_opts, state, io, allocator, paths, files, key, path, &std.fmt.bytesToHex(tree_entry.oid, .lower), progress_ctx_maybe); 323 } else if (indexedOid(repo_opts.hash, tree_entry)) |bytes| { 324 try fileKey(allocator, key, path); 325 try files.put(key.items, .{ .bytes = bytes }); 326 serve_common.reportProgress(repo_opts, io, progress_ctx_maybe, .{ .complete_one = .writing_patch }); 327 } 328 } 329 } 330 331 // the root tree an index entry covers, read from its key slot as hex. 332 fn readTree( 333 comptime repo_opts: rp.RepoOpts(.xit), 334 key_cursor: rp.Repo(.xit, repo_opts).DB.Cursor(.read_only), 335 ) ![hash.hexLen(repo_opts.hash)]u8 { 336 var tree: [hash.byteLen(repo_opts.hash)]u8 = undefined; 337 if ((try key_cursor.readBytes(&tree)).len != tree.len) return error.InvalidFileIndex; 338 return std.fmt.bytesToHex(tree, .lower); 339 } 340 341 // build a file's key: its lowercased basename, a separator, then its full path. 342 fn fileKey(allocator: std.mem.Allocator, key: *std.ArrayList(u8), path: []const u8) !void { 343 const slash = std.mem.lastIndexOfScalar(u8, path, '/'); 344 const base = if (slash) |i| path[i + 1 ..] else path; 345 key.clearRetainingCapacity(); 346 try key.ensureTotalCapacity(allocator, base.len + 1 + path.len); 347 for (base) |char| key.appendAssumeCapacity(std.ascii.toLower(char)); 348 key.appendAssumeCapacity(0); 349 key.appendSliceAssumeCapacity(path); 350 } 351 352 // the blob oid a tree entry is indexed under, or null for a submodule: its oid 353 // is a commit in another repo, not a file here. 354 fn indexedOid(comptime hash_kind: hash.HashKind, tree_entry: *const tr.TreeEntry(hash_kind)) ?[]const u8 { 355 if (tree_entry.mode.content.object_type == .gitlink) return null; 356 return &tree_entry.oid; 357 } 358 359 fn parseOid(comptime hash_kind: hash.HashKind, value: []const u8) ![hash.byteLen(hash_kind)]u8 { 360 const len = comptime hash.byteLen(hash_kind); 361 if (value.len != len) return error.InvalidFileIndex; 362 return value[0..len].*; 363 } 364