╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ admin.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭ find file ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ ╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │ │ │http│ ssh │ │ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
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