╭ 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 11 // the repo moment key holding one file map per branch tip. it sits outside the 12 // haxy moment because pushes change it and events never do. each entry is 13 // keyed by its branch name's hash: its value is the branch's file map, and its 14 // key slot holds the root tree oid that map covers rather than the name. 15 const index_key = "branch->file-name+path->oid"; 16 17 // what a search returns: the matching files and whether the cap cut the list 18 // short. 19 pub fn Results(comptime hash_kind: hash.HashKind) type { 20 return struct { 21 matches: []const Match, 22 capped: bool = false, 23 24 pub const Match = struct { 25 path: []const u8, 26 oid: [hash.byteLen(hash_kind)]u8, 27 }; 28 }; 29 } 30 31 // `branch`'s file index, or null when it has none or its entry is stale: one 32 // that doesn't cover `tree_oid`, the branch tip's root tree. 33 pub fn lookup( 34 comptime repo_opts: rp.RepoOpts(.xit), 35 moment: rp.Repo(.xit, repo_opts).DB.HashMap(.read_only), 36 branch: []const u8, 37 tree_oid: *const [hash.hexLen(repo_opts.hash)]u8, 38 ) !?rp.Repo(.xit, repo_opts).DB.SortedMap(.read_only) { 39 const DB = rp.Repo(.xit, repo_opts).DB; 40 const index_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, index_key)) orelse return null; 41 const index = try DB.HashMap(.read_only).init(index_cursor); 42 const pair = try index.getKeyValuePair(hash.hashInt(repo_opts.hash, branch)) orelse return null; 43 const entry_tree = try readTree(repo_opts, pair.key_cursor); 44 if (!std.mem.eql(u8, &entry_tree, tree_oid)) return null; 45 return try DB.SortedMap(.read_only).init(pair.value_cursor); 46 } 47 48 // the indexed files whose basename starts with `term`, ordered by basename then 49 // path. the separator between basename and path keeps a match inside the 50 // basename, so the walk stops at the first key that doesn't share the prefix. 51 pub fn search( 52 comptime repo_opts: rp.RepoOpts(.xit), 53 files: rp.Repo(.xit, repo_opts).DB.SortedMap(.read_only), 54 aa: std.mem.Allocator, 55 term: []const u8, 56 max: usize, 57 ) !Results(repo_opts.hash) { 58 const Match = Results(repo_opts.hash).Match; 59 const prefix = try aa.alloc(u8, term.len); 60 _ = std.ascii.lowerString(prefix, term); 61 62 var matches: std.ArrayList(Match) = .empty; 63 var iter = try files.iteratorFrom(prefix); 64 while (try iter.next()) |cursor| { 65 const pair = try cursor.readKeyValuePair(); 66 const key = try pair.key_cursor.readBytesAlloc(aa, null); 67 if (!std.mem.startsWith(u8, key, prefix)) break; 68 if (matches.items.len == max) return .{ .matches = matches.items, .capped = true }; 69 const separator = std.mem.indexOfScalar(u8, key, 0) orelse return error.InvalidFileIndex; 70 const value = try pair.value_cursor.readBytesAlloc(aa, null); 71 try matches.append(aa, .{ .path = key[separator + 1 ..], .oid = try parseOid(repo_opts.hash, value) }); 72 } 73 return .{ .matches = matches.items }; 74 } 75 76 // the indexed file at `path`, for a selected result the cap cut off. 77 pub fn get( 78 comptime repo_opts: rp.RepoOpts(.xit), 79 files: rp.Repo(.xit, repo_opts).DB.SortedMap(.read_only), 80 aa: std.mem.Allocator, 81 path: []const u8, 82 ) !?[hash.byteLen(repo_opts.hash)]u8 { 83 var key: std.ArrayList(u8) = .empty; 84 try fileKey(aa, &key, path); 85 const cursor = (try files.getCursor(key.items)) orelse return null; 86 return try parseOid(repo_opts.hash, try cursor.readBytesAlloc(aa, null)); 87 } 88 89 // peel an oid to the root tree it names, following commits and tags. 90 pub fn rootTreeOid( 91 comptime repo_opts: rp.RepoOpts(.xit), 92 state: rp.Repo(.xit, repo_opts).State(.read_only), 93 io: std.Io, 94 allocator: std.mem.Allocator, 95 oid: *const [hash.hexLen(repo_opts.hash)]u8, 96 ) ![hash.hexLen(repo_opts.hash)]u8 { 97 var current = oid.*; 98 while (true) { 99 var object = try obj.Object(.xit, repo_opts).init(state, io, allocator, &current); 100 defer object.deinit(); 101 switch (object.content) { 102 .tree => return current, 103 .commit => |commit| current = commit.tree, 104 .tag => |tag| current = tag.target, 105 .blob => return error.ObjectInvalid, 106 } 107 } 108 } 109 110 // reconcile every branch tip's index. this runs outside the caller's 111 // transaction, and reconciles unconditionally: an up-to-date branch costs one 112 // ref read and an oid compare. 113 pub fn refresh( 114 comptime repo_opts: rp.RepoOpts(.xit), 115 io: std.Io, 116 allocator: std.mem.Allocator, 117 repo: *rp.Repo(.xit, repo_opts), 118 ) !void { 119 const Repo = rp.Repo(.xit, repo_opts); 120 const DB = Repo.DB; 121 const HashInt = hash.HashInt(repo_opts.hash); 122 const Tree = [hash.hexLen(repo_opts.hash)]u8; 123 124 // an entry in the index: the root tree it covers and its file map 125 const Entry = struct { 126 tree: Tree, 127 files: xit.xitdb.Slot, 128 129 fn init(pair: DB.KeyValuePairCursor(.read_only)) !@This() { 130 return .{ .tree = try readTree(repo_opts, pair.key_cursor), .files = pair.value_cursor.slot() }; 131 } 132 }; 133 // what one branch needs written 134 const Work = struct { 135 branch_hash: HashInt, 136 // the tip's root tree (null = the branch is gone, so its entry goes) 137 tree: ?Tree, 138 // the entry the branch has now 139 own: ?Entry, 140 }; 141 142 var arena = std.heap.ArenaAllocator.init(allocator); 143 defer arena.deinit(); 144 const aa = arena.allocator(); 145 146 var work: std.ArrayList(Work) = .empty; 147 var base: ?Entry = null; 148 149 // held across the reads and the writes, so a concurrent refresh can't 150 // change the entries these decisions were made from. 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. 157 { 158 var moment = try repo.core.latestMoment(); 159 const state = Repo.State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; 160 161 // entries carry no branch name, so branches are matched by hash 162 var existing: std.AutoArrayHashMapUnmanaged(HashInt, Entry) = .empty; 163 if (try moment.getCursor(hash.hashInt(repo_opts.hash, index_key))) |index_cursor| { 164 const index = try DB.HashMap(.read_only).init(index_cursor); 165 var iter = try index.iterator(); 166 while (try iter.next()) |cursor| { 167 const pair = try cursor.readKeyValuePair(); 168 try existing.put(aa, pair.hash, try Entry.init(pair)); 169 } 170 } 171 172 // a brand-new branch copies the default branch's map when there is one, 173 // else any other branch's, so only its own changes are written. 174 if (existing.count() > 0) base = existing.values()[0]; 175 var head_buffer: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; 176 if (repo.head(io, &head_buffer)) |head| switch (head) { 177 .ref => |ref| if (existing.get(hash.hashInt(repo_opts.hash, ref.name))) |entry| { 178 base = entry; 179 }, 180 .oid => {}, 181 } else |_| {} 182 183 var branches = try repo.listBranches(io, allocator, .beginning); 184 defer branches.deinit(); 185 while (try branches.next()) |ref| { 186 if (std.mem.eql(u8, ref.name, evt.events_ref.name)) continue; 187 const branch_hash = hash.hashInt(repo_opts.hash, ref.name); 188 const own = if (existing.fetchSwapRemove(branch_hash)) |kv| kv.value else null; 189 const oid = (try repo.readRef(io, .{ .kind = .head, .name = ref.name })) orelse continue; 190 const tree = try rootTreeOid(repo_opts, state, io, allocator, &oid); 191 if (own) |entry| if (std.mem.eql(u8, &entry.tree, &tree)) continue; 192 try work.append(aa, .{ .branch_hash = branch_hash, .tree = tree, .own = own }); 193 } 194 195 // whatever is left belongs to a branch that no longer exists 196 for (existing.keys()) |branch_hash| try work.append(aa, .{ .branch_hash = branch_hash, .tree = null, .own = null }); 197 } 198 199 const Save = struct { 200 core: *Repo.Core, 201 io: std.Io, 202 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, 208 209 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 210 var moment = try DB.HashMap(.read_write).init(cursor.*); 211 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); 227 228 var key: std.ArrayList(u8) = .empty; 229 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); 241 } 242 } 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); 247 } 248 } 249 }; 250 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 } 272 } 273 274 // insert every file under `oid` (a tree), as tr.Tree.read walks one. 275 fn indexTree( 276 comptime repo_opts: rp.RepoOpts(.xit), 277 state: rp.Repo(.xit, repo_opts).State(.read_only), 278 io: std.Io, 279 allocator: std.mem.Allocator, 280 paths: std.mem.Allocator, 281 files: rp.Repo(.xit, repo_opts).DB.SortedMap(.read_write), 282 key: *std.ArrayList(u8), 283 prefix: []const u8, 284 oid: *const [hash.hexLen(repo_opts.hash)]u8, 285 ) !void { 286 var object = try obj.Object(.xit, repo_opts).init(state, io, allocator, oid); 287 defer object.deinit(); 288 const tree = switch (object.content) { 289 .tree => |tree| tree, 290 else => return, 291 }; 292 for (tree.entries.keys(), tree.entries.values()) |name, *tree_entry| { 293 const path = try fs.joinPath(paths, &.{ prefix, name }); 294 if (tree_entry.isTree()) { 295 try indexTree(repo_opts, state, io, allocator, paths, files, key, path, &std.fmt.bytesToHex(tree_entry.oid, .lower)); 296 } else if (indexedOid(repo_opts.hash, tree_entry)) |bytes| { 297 try fileKey(allocator, key, path); 298 try files.put(key.items, .{ .bytes = bytes }); 299 } 300 } 301 } 302 303 // the root tree an index entry covers, read from its key slot as hex. 304 fn readTree( 305 comptime repo_opts: rp.RepoOpts(.xit), 306 key_cursor: rp.Repo(.xit, repo_opts).DB.Cursor(.read_only), 307 ) ![hash.hexLen(repo_opts.hash)]u8 { 308 var tree: [hash.byteLen(repo_opts.hash)]u8 = undefined; 309 if ((try key_cursor.readBytes(&tree)).len != tree.len) return error.InvalidFileIndex; 310 return std.fmt.bytesToHex(tree, .lower); 311 } 312 313 // build a file's key: its lowercased basename, a separator, then its full path. 314 fn fileKey(allocator: std.mem.Allocator, key: *std.ArrayList(u8), path: []const u8) !void { 315 const slash = std.mem.lastIndexOfScalar(u8, path, '/'); 316 const base = if (slash) |i| path[i + 1 ..] else path; 317 key.clearRetainingCapacity(); 318 try key.ensureTotalCapacity(allocator, base.len + 1 + path.len); 319 for (base) |char| key.appendAssumeCapacity(std.ascii.toLower(char)); 320 key.appendAssumeCapacity(0); 321 key.appendSliceAssumeCapacity(path); 322 } 323 324 // the blob oid a tree entry is indexed under, or null for a submodule: its oid 325 // is a commit in another repo, not a file here. 326 fn indexedOid(comptime hash_kind: hash.HashKind, tree_entry: *const tr.TreeEntry(hash_kind)) ?[]const u8 { 327 if (tree_entry.mode.content.object_type == .gitlink) return null; 328 return &tree_entry.oid; 329 } 330 331 fn parseOid(comptime hash_kind: hash.HashKind, value: []const u8) ![hash.byteLen(hash_kind)]u8 { 332 const len = comptime hash.byteLen(hash_kind); 333 if (value.len != len) return error.InvalidFileIndex; 334 return value[0..len].*; 335 } 336