╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ find.zig ─────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
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 { 155 var moment = try repo.core.latestMoment(); 156 const state = Repo.State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; 157 158 // entries carry no branch name, so branches are matched by hash 159 var existing: std.AutoArrayHashMapUnmanaged(HashInt, Entry) = .empty; 160 if (try moment.getCursor(hash.hashInt(repo_opts.hash, index_key))) |index_cursor| { 161 const index = try DB.HashMap(.read_only).init(index_cursor); 162 var iter = try index.iterator(); 163 while (try iter.next()) |cursor| { 164 const pair = try cursor.readKeyValuePair(); 165 try existing.put(aa, pair.hash, try Entry.init(pair)); 166 } 167 } 168 169 // a brand-new branch copies the default branch's map when there is one, 170 // else any other branch's, so only its own changes are written. 171 if (existing.count() > 0) base = existing.values()[0]; 172 var head_buffer: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; 173 if (repo.head(io, &head_buffer)) |head| switch (head) { 174 .ref => |ref| if (existing.get(hash.hashInt(repo_opts.hash, ref.name))) |entry| { 175 base = entry; 176 }, 177 .oid => {}, 178 } else |_| {} 179 180 var branches = try repo.listBranches(io, allocator, .beginning); 181 defer branches.deinit(); 182 while (try branches.next()) |ref| { 183 if (std.mem.eql(u8, ref.name, evt.events_ref.name)) continue; 184 const branch_hash = hash.hashInt(repo_opts.hash, ref.name); 185 const own = if (existing.fetchSwapRemove(branch_hash)) |kv| kv.value else null; 186 const oid = (try repo.readRef(io, .{ .kind = .head, .name = ref.name })) orelse continue; 187 const tree = try rootTreeOid(repo_opts, state, io, allocator, &oid); 188 if (own) |entry| if (std.mem.eql(u8, &entry.tree, &tree)) continue; 189 try work.append(aa, .{ .branch_hash = branch_hash, .tree = tree, .own = own }); 190 } 191 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, 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 } }; 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); 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 } 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 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. 273 fn indexTree( 274 comptime repo_opts: rp.RepoOpts(.xit), 275 state: rp.Repo(.xit, repo_opts).State(.read_only), 276 io: std.Io, 277 allocator: std.mem.Allocator, 278 paths: std.mem.Allocator, 279 files: rp.Repo(.xit, repo_opts).DB.SortedMap(.read_write), 280 key: *std.ArrayList(u8), 281 prefix: []const u8, 282 oid: *const [hash.hexLen(repo_opts.hash)]u8, 283 ) !void { 284 var object = try obj.Object(.xit, repo_opts).init(state, io, allocator, oid); 285 defer object.deinit(); 286 const tree = switch (object.content) { 287 .tree => |tree| tree, 288 else => return, 289 }; 290 for (tree.entries.keys(), tree.entries.values()) |name, *tree_entry| { 291 const path = try fs.joinPath(paths, &.{ prefix, name }); 292 if (tree_entry.isTree()) { 293 try indexTree(repo_opts, state, io, allocator, paths, files, key, path, &std.fmt.bytesToHex(tree_entry.oid, .lower)); 294 } else if (indexedOid(repo_opts.hash, tree_entry)) |bytes| { 295 try fileKey(allocator, key, path); 296 try files.put(key.items, .{ .bytes = bytes }); 297 } 298 } 299 } 300 301 // the root tree an index entry covers, read from its key slot as hex. 302 fn readTree( 303 comptime repo_opts: rp.RepoOpts(.xit), 304 key_cursor: rp.Repo(.xit, repo_opts).DB.Cursor(.read_only), 305 ) ![hash.hexLen(repo_opts.hash)]u8 { 306 var tree: [hash.byteLen(repo_opts.hash)]u8 = undefined; 307 if ((try key_cursor.readBytes(&tree)).len != tree.len) return error.InvalidFileIndex; 308 return std.fmt.bytesToHex(tree, .lower); 309 } 310 311 // build a file's key: its lowercased basename, a separator, then its full path. 312 fn fileKey(allocator: std.mem.Allocator, key: *std.ArrayList(u8), path: []const u8) !void { 313 const slash = std.mem.lastIndexOfScalar(u8, path, '/'); 314 const base = if (slash) |i| path[i + 1 ..] else path; 315 key.clearRetainingCapacity(); 316 try key.ensureTotalCapacity(allocator, base.len + 1 + path.len); 317 for (base) |char| key.appendAssumeCapacity(std.ascii.toLower(char)); 318 key.appendAssumeCapacity(0); 319 key.appendSliceAssumeCapacity(path); 320 } 321 322 // the blob oid a tree entry is indexed under, or null for a submodule: its oid 323 // is a commit in another repo, not a file here. 324 fn indexedOid(comptime hash_kind: hash.HashKind, tree_entry: *const tr.TreeEntry(hash_kind)) ?[]const u8 { 325 if (tree_entry.mode.content.object_type == .gitlink) return null; 326 return &tree_entry.oid; 327 } 328 329 fn parseOid(comptime hash_kind: hash.HashKind, value: []const u8) ![hash.byteLen(hash_kind)]u8 { 330 const len = comptime hash.byteLen(hash_kind); 331 if (value.len != len) return error.InvalidFileIndex; 332 return value[0..len].*; 333 } 334