╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ admin.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭ find file ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ ╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │ │ │http│ ssh │ │ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
28 defer allocator.free(user_path); 29 var user_repo = try Repo.init(io, allocator, .{ .path = user_path }); 30 defer user_repo.deinit(io, allocator); 31 32 var arena = std.heap.ArenaAllocator.init(allocator); 33 defer arena.deinit(); 34 35 // 36 // define test events 37 // 38 39 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 40 const user_event_id = evt.EventWithId.randomId(prng.random()); 41 const repo_event_id = evt.EventWithId.randomId(prng.random()); 42 43 var first_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 44 const first_password_hash = try evt.User.hashPassword("correct horse battery staple", &first_password_hash_buf, io); 45 46 var second_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 47 const second_password_hash = try evt.User.hashPassword("Tr0ub4dor&3", &second_password_hash_buf, io); 48 49 const events_to_consume = [_]evt.EventWithId{ 50 .{ 51 .id = std.fmt.bytesToHex(user_event_id, .lower), 52 .author = author, 53 .event = .{ 54 .user = .{ 55 .name = "alice", 56 .email = "alice@example.test", 57 .password_hash = first_password_hash, 58 }, 59 }, 60 }, 61 // this event edits the previous one because it has the same id 62 .{ 63 .id = std.fmt.bytesToHex(user_event_id, .lower), 64 .author = author, 65 .event = .{ 66 .user = .{ 67 .name = "alice", 68 .email = "alice@example.test", 69 .password_hash = second_password_hash, 70 }, 71 }, 72 }, 73 }; 74 const repo_event = evt.EventWithId{ 75 .id = std.fmt.bytesToHex(repo_event_id, .lower), 76 .author = author, 77 .event = .{ 78 .repo = .{ 79 .user_id = std.fmt.bytesToHex(user_event_id, .lower), 80 .name = "ziglings", 81 .description = "Learn the Zig programming language by fixing tiny broken programs", 82 }, 83 }, 84 }; 85 86 // commit and consume the seed events. users go in the admin repo, repos in 87 // their owner's user repo. 88 try evt.consume(.{ .server = .{ .users_dir = temp_path } }, .admin, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume); 89 try evt.consume(.{ .server = .{ .users_dir = temp_path } }, .user, .xit, repo_opts, io, allocator, &user_repo, evt.events_ref, &.{repo_event}); 90 91 { 92 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 93 94 // get the map of users 95 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 96 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 97 98 // get the user out of the map that was edited 99 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 100 const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); 101 const user_event = try evt.read(evt.User.Record, Repo.DB, repo_opts.hash, &arena, user_map); 102 103 // the password was correctly edited 104 try std.testing.expectEqualStrings(events_to_consume[1].event.user.?.password_hash, user_event.event.password_hash); 105 } 106 107 { 108 const haxy_moment = try evt.currentMoment(repo_opts, &user_repo); 109 110 // get the map of repos 111 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 112 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 113 114 // get the repo out of the map 115 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 116 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 117 const repo_record = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); 118 119 try std.testing.expectEqualStrings(&std.fmt.bytesToHex(user_event_id, .lower), &repo_record.event.user_id); 120 try std.testing.expectEqualStrings("ziglings", repo_record.event.name); 121 122 // the repo resolves by its name alone 123 const found = (try evt.Repo.readByName(Repo.DB, repo_opts.hash, haxy_moment, &arena, "ziglings")) orelse return error.NotFound; 124 try std.testing.expectEqualSlices(u8, &repo_event_id, &found.event_id); 125 } 126 127 // 128 // remove the repo 129 // 130 131 const events_to_consume2 = [_]evt.EventWithId{ 132 .{ 133 .id = std.fmt.bytesToHex(repo_event_id, .lower), 134 .author = author, 135 .event = .{ .repo = null }, 136 }, 137 }; 138 139 // commit and consume the removal 140 try evt.consume(.{ .server = .{ .users_dir = temp_path } }, .user, .xit, repo_opts, io, allocator, &user_repo, evt.events_ref, &events_to_consume2); 141 142 { 143 const haxy_moment = try evt.currentMoment(repo_opts, &user_repo); 144 145 // get the map of repos 146 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 147 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 148 149 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 150 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 151 const repo_record = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); 152 try std.testing.expect(repo_record.removed); 153 154 // removing the repo dropped it from the name index 155 try std.testing.expectEqual(null, try evt.Repo.readByName(Repo.DB, repo_opts.hash, haxy_moment, &arena, "ziglings")); 156 } 157 158 // a non-null payload restores the repo and its indexes 159 try evt.consume(.{ .server = .{ .users_dir = temp_path } }, .user, .xit, repo_opts, io, allocator, &user_repo, evt.events_ref, &.{repo_event}); 160 161 { 162 const haxy_moment = try evt.currentMoment(repo_opts, &user_repo); 163 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 164 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 165 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 166 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 167 const repo_record = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); 168 try std.testing.expect(!repo_record.removed); 169 170 try std.testing.expect(try evt.Repo.readByName(Repo.DB, repo_opts.hash, haxy_moment, &arena, "ziglings") != null); 171 } 172 173 // 174 // remove the user 175 // 176 177 const events_to_consume3 = [_]evt.EventWithId{ 178 .{ 179 .id = std.fmt.bytesToHex(user_event_id, .lower), 180 .author = author, 181 .event = .{ .user = null }, 182 }, 183 }; 184 185 // commit and consume the removal 186 try evt.consume(.{ .server = .{ .users_dir = temp_path } }, .admin, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume3); 187 188 { 189 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 190 191 // get the map of users 192 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 193 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 194 195 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 196 const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); 197 const user_event = try evt.read(evt.User.Record, Repo.DB, repo_opts.hash, &arena, user_map); 198 try std.testing.expect(user_event.removed); 199 } 200 } 201 202 test "repos and users paginate newest first" { 203 const io = std.testing.io; 204 const allocator = std.testing.allocator; 205 206 var temp = std.testing.tmpDir(.{}); 207 defer temp.cleanup(); 208 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 209 defer allocator.free(temp_path); 210 211 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 212 const Repo = rp.Repo(.xit, repo_opts); 213 const admin_path = try std.Io.Dir.path.join(allocator, &.{ temp_path, "admin" }); 214 defer allocator.free(admin_path); 215 var admin = try Repo.init(io, allocator, .{ .path = admin_path }); 216 defer admin.deinit(io, allocator); 217 const user_path = try std.Io.Dir.path.join(allocator, &.{ temp_path, "user" }); 218 defer allocator.free(user_path); 219 var repo = try Repo.init(io, allocator, .{ .path = user_path }); 220 defer repo.deinit(io, allocator); 221 222 var arena = std.heap.ArenaAllocator.init(allocator); 223 defer arena.deinit(); 224 225 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 226 const user_id = evt.EventWithId.randomId(prng.random()); 227 var repo_ids: [4][evt.event_id_size]u8 = undefined; 228 for (&repo_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 229 230 var pw_buf: [evt.User.password_hash_max_len]u8 = undefined; 231 const pw = try evt.User.hashPassword("pw", &pw_buf, io); 232 233 // asserts the repo-id-set holds exactly `expected` ids in that order 234 const Check = struct { 235 fn order( 236 comptime DB: type, 237 comptime hash_kind: hash.HashKind, 238 moment: DB.HashMap(.read_only), 239 expected: []const *const [evt.event_id_size]u8, 240 ) !void { 241 const cursor = try moment.getCursor(hash.hashInt(hash_kind, "repo-id-set")) orelse return error.NotFound; 242 const set = try DB.SortedSet(.read_only).init(cursor); 243 try std.testing.expectEqual(expected.len, try set.count()); 244 for (expected, 0..) |id, i| { 245 const kv = (try set.getIndexKeyValuePair(@intCast(i))) orelse return error.NotFound; 246 // the key is orderKeyDesc ([created-order][event-id]); its trailing bytes are the id 247 var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; 248 _ = try kv.key_cursor.readBytes(&order_key); 249 try std.testing.expectEqualSlices(u8, id, order_key[@sizeOf(u64)..]); 250 } 251 } 252 }; 253 254 // one user in the admin repo, then four repos in creation order with 255 // deliberately skewed timestamps in the user's repo 256 try evt.consume(.{ .server = .{ .users_dir = temp_path } }, .admin, .xit, repo_opts, io, allocator, &admin, evt.events_ref, &.{ 257 .{ .id = std.fmt.bytesToHex(user_id, .lower), .author = author, .timestamp = 100, .event = .{ .user = .{ .name = "alice", .email = "alice@example.test", .password_hash = pw } } }, 258 }); 259 const events = [_]evt.EventWithId{ 260 .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .author = author, .timestamp = 10_000, .event = .{ .repo = .{ .user_id = std.fmt.bytesToHex(user_id, .lower), .name = "repo0", .description = "d0" } } }, 261 .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .author = author, .timestamp = 10, .event = .{ .repo = .{ .user_id = std.fmt.bytesToHex(user_id, .lower), .name = "repo1", .description = "d1" } } }, 262 .{ .id = std.fmt.bytesToHex(repo_ids[2], .lower), .author = author, .timestamp = 5_000, .event = .{ .repo = .{ .user_id = std.fmt.bytesToHex(user_id, .lower), .name = "repo2", .description = "d2" } } }, 263 .{ .id = std.fmt.bytesToHex(repo_ids[3], .lower), .author = author, .timestamp = 20, .event = .{ .repo = .{ .user_id = std.fmt.bytesToHex(user_id, .lower), .name = "repo3", .description = "d3" } } }, 264 }; 265 try evt.consume(.{ .server = .{ .users_dir = temp_path } }, .user, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &events); 266 267 { 268 const moment = try evt.currentMoment(repo_opts, &repo); 269 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] }); 270 271 // the single user shows up in its own ordered set 272 const admin_moment = try evt.currentMoment(repo_opts, &admin); 273 const ucur = try admin_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id-set")) orelse return error.NotFound; 274 const uset = try Repo.DB.SortedSet(.read_only).init(ucur); 275 try std.testing.expectEqual(1, try uset.count()); 276 } 277 278 // removing repo1 retains its place in the canonical order 279 try evt.consume(.{ .server = .{ .users_dir = temp_path } }, .user, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ 280 .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .author = author, .timestamp = 200, .event = .{ .repo = null } }, 281 }); 282 { 283 const moment = try evt.currentMoment(repo_opts, &repo); 284 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] }); 285 } 286 287 // update repo0 at a later timestamp -> keeps its original place 288 try evt.consume(.{ .server = .{ .users_dir = temp_path } }, .user, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ 289 .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .author = author, .timestamp = 300, .event = .{ .repo = .{ .user_id = std.fmt.bytesToHex(user_id, .lower), .name = "repo0", .description = "updated" } } }, 290 }); 291 { 292 const moment = try evt.currentMoment(repo_opts, &repo); 293 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] }); 294 295 // the value really was updated 296 const e2r_cur = try moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 297 const e2r = try Repo.DB.HashMap(.read_only).init(e2r_cur); 298 const rc = try e2r.getCursor(hash.hashInt(repo_opts.hash, &repo_ids[0])) orelse return error.NotFound; 299 const rm = try Repo.DB.HashMap(.read_only).init(rc); 300 const re = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, rm); 301 try std.testing.expectEqualStrings("updated", re.event.description); 302 } 303 } 304 305 test "fork query and removal lifecycle" { 306 const io = std.testing.io; 307 const allocator = std.testing.allocator; 308 309 var temp = std.testing.tmpDir(.{}); 310 defer temp.cleanup(); 311 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 312 defer allocator.free(temp_path); 313 314 const users_dir = try std.Io.Dir.path.join(allocator, &.{ temp_path, "users" }); 315 defer allocator.free(users_dir); 316 const admin_path = try std.Io.Dir.path.join(allocator, &.{ temp_path, "admin" }); 317 defer allocator.free(admin_path); 318 319 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 320 const user_id = evt.EventWithId.randomId(prng.random()); 321 const repo_id = evt.EventWithId.randomId(prng.random()); 322 const fork_id = evt.EventWithId.randomId(prng.random()); 323 const fork_id_hex = std.fmt.bytesToHex(fork_id, .lower); 324 325 var admin = try rp.Repo(.xit, evt.admin_repo_opts).init(io, allocator, .{ .path = admin_path }); 326 defer admin.deinit(io, allocator); 327 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .admin, .xit, evt.admin_repo_opts, io, allocator, &admin, evt.events_ref, &.{ 328 .{ 329 .id = std.fmt.bytesToHex(user_id, .lower), 330 .timestamp = 1, 331 .author = author, 332 .event = .{ .user = .{ 333 .name = author.name, 334 .email = author.email, 335 .password_hash = "", 336 } }, 337 }, 338 }); 339 const user_repo_path = try evt.userRepoPath(allocator, users_dir, &user_id); 340 defer allocator.free(user_repo_path); 341 var user_repo = try evt.initUserRepo(io, allocator, user_repo_path); 342 defer user_repo.deinit(io, allocator); 343 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .user, .xit, evt.user_repo_opts, io, allocator, &user_repo, evt.events_ref, &.{ 344 .{ 345 .id = std.fmt.bytesToHex(repo_id, .lower), 346 .timestamp = 1, 347 .author = author, 348 .event = .{ .repo = .{ 349 .user_id = std.fmt.bytesToHex(user_id, .lower), 350 .name = "repo", 351 .description = "", 352 } }, 353 }, 354 }); 355 356 const target_path = try evt.repoPath(allocator, users_dir, &user_id, &repo_id); 357 defer allocator.free(target_path); 358 var target = try rp.Repo(.xit, fork_repo_opts).init(io, allocator, .{ .path = target_path }); 359 defer target.deinit(io, allocator); 360 { 361 const file = try target.core.work_dir.createFile(io, "README", .{}); 362 defer file.close(io); 363 try file.writeStreamingAll(io, "repo\n"); 364 } 365 try target.add(io, allocator, &.{"README"}); 366 _ = try target.commit(io, allocator, .{ .message = "initial" }); 367 368 const draft_path = try fork.create(.{ .is_test = true }, io, allocator, users_dir, .{ 369 .target_branch = "master", 370 .id = fork_id_hex, 371 .user_id = user_id, 372 .repo_id = repo_id, 373 .repo_user_id = user_id, 374 .repo_name = "target", 375 .title = "add a feature", 376 .description = "a draft patch", 377 .labels = "enhancement", 378 .author = author, 379 .timestamp = 2, 380 }); 381 defer allocator.free(draft_path); 382 383 var arena = std.heap.ArenaAllocator.init(allocator); 384 defer arena.deinit(); 385 var moment = try evt.currentMoment(evt.user_repo_opts, &user_repo); 386 var record = (try evt.Fork.readById(evt.UserDB, evt.user_repo_opts.hash, moment, &arena, &fork_id)) orelse return error.NotFound; 387 try std.testing.expectEqualStrings(&std.fmt.bytesToHex(repo_id, .lower), &record.event.repo_id); 388 try std.testing.expectEqualStrings(&std.fmt.bytesToHex(user_id, .lower), &record.event.repo_user_id); 389 try std.testing.expectEqual(.draft, record.event.stage); 390 try std.testing.expectEqual(1, try activeForkCount(moment)); 391 try std.testing.expectEqual(1, try draftForkCount(moment, &repo_id)); 392 393 var draft_repo = try rp.Repo(.xit, fork_repo_opts).open(io, allocator, .{ .path = draft_path }); 394 defer draft_repo.deinit(io, allocator); 395 const draft_moment = try evt.currentMoment(fork_repo_opts, &draft_repo); 396 const draft = (try evt.Patch.readById(evt.EventDB(fork_repo_opts.hash), fork_repo_opts.hash, draft_moment, &arena, &fork_id)) orelse return error.NotFound; 397 try std.testing.expectEqualStrings("add a feature", draft.event.title); 398 try std.testing.expectEqual(null, draft.event.revision); 399 400 try fork.remove(io, allocator, users_dir, &user_id, &fork_id_hex, author); 401 try std.testing.expectError(error.FileNotFound, std.Io.Dir.accessAbsolute(io, draft_path, .{})); 402 _ = arena.reset(.retain_capacity); 403 moment = try evt.currentMoment(evt.user_repo_opts, &user_repo); 404 record = (try evt.Fork.readById(evt.UserDB, evt.user_repo_opts.hash, moment, &arena, &fork_id)) orelse return error.NotFound; 405 try std.testing.expect(record.removed); 406 try std.testing.expectEqual(0, try activeForkCount(moment)); 407 try std.testing.expectEqual(0, try draftForkCount(moment, &repo_id)); 408 409 try fork.remove(io, allocator, users_dir, &user_id, &fork_id_hex, author); 410 } 411 412 fn draftForkCount(moment: evt.UserDB.HashMap(.read_only), repo_id: []const u8) !u64 { 413 const index_cursor = try moment.getCursor(hash.hashInt(evt.user_repo_opts.hash, evt.Fork.repo_to_draft_id_set_key)) orelse return 0; 414 const index = try evt.UserDB.HashMap(.read_only).init(index_cursor); 415 const ids_cursor = try index.getCursor(hash.hashInt(evt.user_repo_opts.hash, repo_id)) orelse return 0; 416 const ids = try evt.UserDB.SortedSet(.read_only).init(ids_cursor); 417 return try ids.count(); 418 } 419 420 fn activeForkCount(moment: evt.UserDB.HashMap(.read_only)) !u64 { 421 const ids_cursor = try moment.getCursor(hash.hashInt(evt.user_repo_opts.hash, evt.Fork.active_id_set_key)) orelse return 0; 422 const ids = try evt.UserDB.SortedSet(.read_only).init(ids_cursor); 423 return try ids.count(); 424 } 425