╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
260 261 // get the last object id 262 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 263 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 264 _ = try last_object_id_cursor.readBytes(&last_object_id); 265 266 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 267 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 268 269 try std.testing.expectEqual(2, try haxy.count()); 270 271 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 272 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 273 274 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 275 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 276 277 // get the map of issues 278 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 279 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 280 281 // get the issue out of the map that was edited 282 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id)) orelse return error.NotFound; 283 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 284 const first_issue = try evt.read(evt.Issue, Repo.DB, repo_opts.hash, &arena, first_issue_map); 285 286 // the description is no longer edited 287 try std.testing.expectEqualStrings(events_to_consume[0].event.issue.?.description, first_issue.description); 288 289 // the tags are no longer edited 290 try std.testing.expectEqualStrings(events_to_consume[0].event.issue.?.tags, first_issue.tags); 291 292 // an event added by the second push is no longer there because it was wiped out by the rebase 293 try std.testing.expect(null == try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &events_to_consume2[0].id))); 294 } 295 296 // 297 // rebase the branch so it no longer includes the original issue 298 // 299 300 { 301 var json: std.Io.Writer.Allocating = .init(allocator); 302 defer json.deinit(); 303 304 for (events_to_consume3, 0..) |event, i| { 305 json.clearRetainingCapacity(); 306 307 try std.json.Stringify.value(event, .{}, &json.writer); 308 309 // commit the event into a special branch 310 _ = try repo.commitAtRef( 311 io, 312 allocator, 313 .{ 314 .message = json.written(), 315 // the first event should have no parents, 316 // because we want to drop the very first commit. 317 // and the rest will have null which causes them 318 // to have the previous commit as their parent 319 .parent_oids = if (i == 0) &.{} else null, 320 }, 321 null, 322 .{ .kind = .head, .name = "haxy/meta" }, 323 ); 324 } 325 } 326 327 // 328 // consume events into the database 329 // 330 331 { 332 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 333 334 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 335 336 // read the moment we just created 337 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 338 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 339 340 // get the last object id 341 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 342 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 343 _ = try last_object_id_cursor.readBytes(&last_object_id); 344 345 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 346 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 347 348 try std.testing.expectEqual(1, try haxy.count()); 349 350 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 351 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 352 353 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 354 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 355 356 // get the map of issues 357 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 358 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 359 360 // the first issue no longer exists 361 try std.testing.expect(null == try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id))); 362 } 363 } 364 365 test "merge" { 366 const io = std.testing.io; 367 const allocator = std.testing.allocator; 368 const temp_dir_name = "temp-event-merge"; 369 370 // create the temp dir 371 const cwd = std.Io.Dir.cwd(); 372 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 373 if (temp_dir_or_err) |*temp_dir| { 374 temp_dir.close(io); 375 try cwd.deleteTree(io, temp_dir_name); 376 } else |_| {} 377 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 378 defer cwd.deleteTree(io, temp_dir_name) catch {}; 379 defer temp_dir.close(io); 380 381 const cwd_path = try std.process.currentPathAlloc(io, allocator); 382 defer allocator.free(cwd_path); 383 384 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 385 defer allocator.free(work_path); 386 387 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 388 const Repo = rp.Repo(.xit, repo_opts); 389 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 390 defer repo.deinit(io, allocator); 391 392 var arena = std.heap.ArenaAllocator.init(allocator); 393 defer arena.deinit(); 394 395 // 396 // define test events 397 // 398 399 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 400 401 const events_to_consume = [_]evt.EventWithId{ 402 .{ 403 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 404 .event = .{ 405 .issue = .{ 406 .title = "Login form clears password on validation error", 407 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 408 .tags = "bug\x00priority-high\x00ui", 409 }, 410 }, 411 }, 412 .{ 413 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 414 .event = .{ 415 .issue = .{ 416 .title = "Search results ignore archived project filter", 417 .description = "Filtering search results to active projects still returns issues from archived projects. Apply the archived flag before ranking results.", 418 .tags = "bug\x00search\x00backend", 419 }, 420 }, 421 }, 422 .{ 423 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 424 .event = .{ 425 .issue = .{ 426 .title = "Issue list does not persist selected sort order", 427 .description = "Changing the issue list sort order is lost after refresh. Store the selected sort field and direction with the user's view preferences.", 428 .tags = "enhancement\x00frontend\x00preferences", 429 }, 430 }, 431 }, 432 }; 433 434 // 435 // insert issues as commits in the repo 436 // 437 438 var first_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 439 440 { 441 var json: std.Io.Writer.Allocating = .init(allocator); 442 defer json.deinit(); 443 444 for (events_to_consume, 0..) |event, i| { 445 json.clearRetainingCapacity(); 446 447 try std.json.Stringify.value(event, .{}, &json.writer); 448 449 // commit the event into a special branch 450 const oid = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 451 if (i == 0) { 452 _ = try std.fmt.hexToBytes(&first_oid, &oid); 453 } 454 } 455 } 456 457 // 458 // consume events into the database 459 // 460 461 { 462 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 463 464 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 465 466 // read the moment we just created 467 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 468 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 469 470 // get the last object id 471 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 472 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 473 _ = try last_object_id_cursor.readBytes(&last_object_id); 474 475 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 476 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 477 478 try std.testing.expectEqual(1, try haxy.count()); 479 480 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 481 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 482 483 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 484 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 485 486 // get the map of issues 487 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 488 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 489 490 // get the issue out of the map 491 var first_issue_id: [evt.event_id_size]u8 = undefined; 492 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume[0].id); 493 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 494 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 495 const first_issue = try evt.read(evt.Issue, Repo.DB, repo_opts.hash, &arena, first_issue_map); 496 497 try std.testing.expectEqualStrings(events_to_consume[0].event.issue.?.description, first_issue.description); 498 try std.testing.expectEqualStrings(events_to_consume[0].event.issue.?.tags, first_issue.tags); 499 } 500 501 // 502 // define more test events 503 // 504 505 const events_to_consume2 = [_]evt.EventWithId{ 506 .{ 507 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 508 .event = .{ 509 .issue = .{ 510 .title = "Kanban card status badge falls behind after drag", 511 .description = "Moving an issue between columns updates the card position immediately, but the status badge keeps showing the previous state until refresh.", 512 .tags = "bug\x00kanban\x00frontend", 513 }, 514 }, 515 }, 516 .{ 517 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 518 .event = .{ 519 .issue = .{ 520 .title = "Assignee autocomplete omits recently invited users", 521 .description = "Users invited during the current session do not appear in the assignee picker until the project page is reloaded.", 522 .tags = "bug\x00assignees\x00api", 523 }, 524 }, 525 }, 526 .{ 527 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 528 .event = .{ 529 .issue = .{ 530 .title = "Add due date warning for issues blocked by dependencies", 531 .description = "Show a warning when an issue's due date is earlier than an unresolved blocking issue so planners can adjust the schedule.", 532 .tags = "enhancement\x00planning\x00dependencies", 533 }, 534 }, 535 }, 536 }; 537 538 // 539 // insert issues as commits in the repo 540 // 541 542 { 543 var json: std.Io.Writer.Allocating = .init(allocator); 544 defer json.deinit(); 545 546 for (events_to_consume2, 0..) |event, i| { 547 json.clearRetainingCapacity(); 548 549 try std.json.Stringify.value(event, .{}, &json.writer); 550 551 // commit the event into a special branch 552 _ = try repo.commitAtRef( 553 io, 554 allocator, 555 // make the first commit a child of the first commit on the haxy/meta branch 556 .{ .parent_oids = if (i == 0) &.{std.fmt.bytesToHex(first_oid, .lower)} else null, .message = json.written() }, 557 null, 558 .{ .kind = .head, .name = "haxy/other" }, 559 ); 560 } 561 } 562 563 // 564 // check out the haxy/meta branch and merge the haxy/other branch into it 565 // 566 567 const merge_oid = blk: { 568 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "haxy/meta" } } }); 569 defer result.deinit(); 570 571 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = .{ .kind = .head, .name = "haxy/other" } }} } } }, null); 572 defer merge.deinit(); 573 574 try std.testing.expect(.success == merge.result); 575 576 break :blk merge.result.success.oid; 577 }; 578 579 // 580 // consume events into the database 581 // 582 583 { 584 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 585 586 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 587 588 // read the moment we just created 589 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 590 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 591 592 // get the last object id 593 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 594 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 595 _ = try last_object_id_cursor.readBytes(&last_object_id); 596 597 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 598 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 599 600 try std.testing.expectEqual(2, try haxy.count()); 601 602 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 603 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 604 605 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 606 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 607 608 // get the map of issues 609 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 610 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 611 612 // make sure one of the original issues is still there 613 { 614 // get the issue out of the map 615 var first_issue_id: [evt.event_id_size]u8 = undefined; 616 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume[0].id); 617 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 618 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 619 const first_issue = try evt.read(evt.Issue, Repo.DB, repo_opts.hash, &arena, first_issue_map); 620 621 try std.testing.expectEqualStrings(events_to_consume[0].event.issue.?.description, first_issue.description); 622 try std.testing.expectEqualStrings(events_to_consume[0].event.issue.?.tags, first_issue.tags); 623 } 624 625 // make sure one of the new issues is there 626 { 627 // get the issue out of the map 628 var first_issue_id: [evt.event_id_size]u8 = undefined; 629 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume2[0].id); 630 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 631 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 632 const first_issue = try evt.read(evt.Issue, Repo.DB, repo_opts.hash, &arena, first_issue_map); 633 634 try std.testing.expectEqualStrings(events_to_consume2[0].event.issue.?.description, first_issue.description); 635 try std.testing.expectEqualStrings(events_to_consume2[0].event.issue.?.tags, first_issue.tags); 636 } 637 } 638 639 // 640 // insert issues as commits in the repo 641 // 642 643 { 644 var json: std.Io.Writer.Allocating = .init(allocator); 645 defer json.deinit(); 646 647 for (events_to_consume, 0..) |event, i| { 648 json.clearRetainingCapacity(); 649 650 try std.json.Stringify.value(event, .{}, &json.writer); 651 652 // commit the event into a special branch 653 _ = try repo.commitAtRef( 654 io, 655 allocator, 656 // make the first event have no parents so we completely rebase the branch, 657 // undoing the merge that we did above 658 .{ .parent_oids = if (i == 0) &.{} else null, .message = json.written() }, 659 null, 660 .{ .kind = .head, .name = "haxy/meta" }, 661 ); 662 } 663 } 664 665 // 666 // consume events into the database 667 // 668 669 { 670 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 671 672 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 673 674 // read the moment we just created 675 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 676 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 677 678 // get the last object id 679 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 680 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 681 _ = try last_object_id_cursor.readBytes(&last_object_id); 682 683 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 684 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 685 686 try std.testing.expectEqual(1, try haxy.count()); 687 688 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 689 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 690 691 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 692 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 693 694 // get the map of issues 695 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 696 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 697 698 // make sure the new issue is not there 699 { 700 // get the issue out of the map 701 var first_issue_id: [evt.event_id_size]u8 = undefined; 702 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume2[0].id); 703 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)); 704 try std.testing.expect(null == first_issue_cursor); 705 } 706 } 707 708 // 709 // define test events that modify the same issue in conflicting ways 710 // 711 712 const events_to_consume3 = [_]evt.EventWithId{ 713 .{ 714 .id = events_to_consume[0].id, 715 .event = .{ 716 .issue = .{ 717 .title = "Login form clears password on validation error", 718 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 719 .tags = "bug\x00priority-low\x00ui", 720 }, 721 }, 722 }, 723 .{ 724 .id = events_to_consume[0].id, 725 .event = .{ 726 .issue = .{ 727 .title = "Login form clears password on validation error", 728 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 729 .tags = "bug\x00priority-medium\x00ui", 730 }, 731 }, 732 }, 733 }; 734 735 // 736 // insert issues as commits in the repo on different branches 737 // 738 739 { 740 var json: std.Io.Writer.Allocating = .init(allocator); 741 defer json.deinit(); 742 743 for (events_to_consume3, 0..) |event, i| { 744 json.clearRetainingCapacity(); 745 746 try std.json.Stringify.value(event, .{}, &json.writer); 747 748 // commit the event into a special branch 749 _ = try repo.commitAtRef( 750 io, 751 allocator, 752 .{ .parent_oids = &.{merge_oid}, .message = json.written() }, 753 null, 754 .{ .kind = .head, .name = switch (i) { 755 0 => "haxy/meta", 756 1 => "haxy/other", 757 else => unreachable, 758 } }, 759 ); 760 } 761 } 762 763 // 764 // check out the haxy/meta branch and merge the haxy/other branch into it 765 // 766 767 { 768 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "haxy/meta" } } }); 769 defer result.deinit(); 770 771 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = .{ .kind = .head, .name = "haxy/other" } }} } } }, null); 772 defer merge.deinit(); 773 774 try std.testing.expect(.success == merge.result); 775 } 776 777 // 778 // consume events into the database 779 // 780 781 try std.testing.expectError(error.MergeConflict, evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" })); 782 } 783 784 test "user and repo" { 785 const io = std.testing.io; 786 const allocator = std.testing.allocator; 787 const temp_dir_name = "temp-event-user-and-repo"; 788 789 // create the temp dir 790 const cwd = std.Io.Dir.cwd(); 791 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 792 if (temp_dir_or_err) |*temp_dir| { 793 temp_dir.close(io); 794 try cwd.deleteTree(io, temp_dir_name); 795 } else |_| {} 796 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 797 defer cwd.deleteTree(io, temp_dir_name) catch {}; 798 defer temp_dir.close(io); 799 800 const cwd_path = try std.process.currentPathAlloc(io, allocator); 801 defer allocator.free(cwd_path); 802 803 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 804 defer allocator.free(work_path); 805 806 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 807 const Repo = rp.Repo(.xit, repo_opts); 808 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 809 defer repo.deinit(io, allocator); 810 811 var arena = std.heap.ArenaAllocator.init(allocator); 812 defer arena.deinit(); 813 814 // 815 // define test events 816 // 817 818 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 819 const user_event_id = evt.EventWithId.randomId(prng.random()); 820 const repo_event_id = evt.EventWithId.randomId(prng.random()); 821 822 var first_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 823 const first_password_hash = try evt.User.hashPassword("correct horse battery staple", &first_password_hash_buf, io); 824 825 var second_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 826 const second_password_hash = try evt.User.hashPassword("Tr0ub4dor&3", &second_password_hash_buf, io); 827 828 const events_to_consume = [_]evt.EventWithId{ 829 .{ 830 .id = std.fmt.bytesToHex(user_event_id, .lower), 831 .event = .{ 832 .user = .{ 833 .name = "alice", 834 .display_name = "Alice Example", 835 .email = "alice@example.test", 836 .password_hash = first_password_hash, 837 }, 838 }, 839 }, 840 // this event edits the previous one because it has the same id 841 .{ 842 .id = std.fmt.bytesToHex(user_event_id, .lower), 843 .event = .{ 844 .user = .{ 845 .name = "alice", 846 .display_name = "Alice Example", 847 .email = "alice@example.test", 848 .password_hash = second_password_hash, 849 }, 850 }, 851 }, 852 .{ 853 .id = std.fmt.bytesToHex(repo_event_id, .lower), 854 .event = .{ 855 .repo = .{ 856 .user_id = &user_event_id, 857 .name = "ziglings", 858 .description = "Learn the Zig programming language by fixing tiny broken programs", 859 .enable_issue = true, 860 }, 861 }, 862 }, 863 }; 864 865 // 866 // insert users and repos as commits in the repo 867 // 868 869 { 870 var json: std.Io.Writer.Allocating = .init(allocator); 871 defer json.deinit(); 872 873 for (events_to_consume) |event| { 874 json.clearRetainingCapacity(); 875 876 try std.json.Stringify.value(event, .{}, &json.writer); 877 878 // commit the event into a special branch 879 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 880 } 881 } 882 883 // 884 // consume events into the database 885 // 886 887 { 888 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 889 890 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 891 892 // read the moment we just created 893 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 894 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 895 896 // get the last object id 897 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 898 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 899 _ = try last_object_id_cursor.readBytes(&last_object_id); 900 901 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 902 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 903 904 try std.testing.expectEqual(1, try haxy.count()); 905 906 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 907 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 908 909 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 910 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 911 912 // get the map of users 913 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 914 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 915 916 // get the user out of the map that was edited 917 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 918 const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); 919 const user_event = try evt.read(evt.User, Repo.DB, repo_opts.hash, &arena, user_map); 920 921 // the password was correctly edited 922 try std.testing.expectEqualStrings(events_to_consume[1].event.user.?.password_hash, user_event.password_hash); 923 924 // get the map of repos 925 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 926 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 927 928 // get the repo out of the map 929 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 930 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 931 const repo_event = try evt.read(evt.Repo, Repo.DB, repo_opts.hash, &arena, repo_map); 932 933 try std.testing.expectEqualSlices(u8, &user_event_id, repo_event.user_id); 934 try std.testing.expectEqualStrings("ziglings", repo_event.name); 935 936 // get the repos created by the user 937 const user_id_to_repos_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repos")) orelse return error.NotFound; 938 const user_id_to_repos = try Repo.DB.HashMap(.read_only).init(user_id_to_repos_cursor); 939 940 const user_repos_cursor = try user_id_to_repos.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 941 const user_repos = try Repo.DB.CountedHashSet(.read_only).init(user_repos_cursor); 942 943 try std.testing.expectEqual(1, try user_repos.count()); 944 945 try std.testing.expect(null != try user_repos.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id))); 946 } 947 948 // 949 // remove the repo 950 // 951 952 const events_to_consume2 = [_]evt.EventWithId{ 953 .{ 954 .id = std.fmt.bytesToHex(repo_event_id, .lower), 955 .event = .{ .repo = null }, 956 }, 957 }; 958 959 { 960 var json: std.Io.Writer.Allocating = .init(allocator); 961 defer json.deinit(); 962 963 for (events_to_consume2) |event| { 964 json.clearRetainingCapacity(); 965 966 try std.json.Stringify.value(event, .{}, &json.writer); 967 968 // commit the event into a special branch 969 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 970 } 971 } 972 973 // 974 // consume the removal into the database 975 // 976 977 { 978 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 979 980 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 981 982 // read the moment we just created 983 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 984 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 985 986 // get the last object id 987 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 988 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 989 _ = try last_object_id_cursor.readBytes(&last_object_id); 990 991 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 992 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 993 994 try std.testing.expectEqual(2, try haxy.count()); 995 996 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 997 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 998 999 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 1000 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 1001 1002 // get the map of repos 1003 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 1004 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 1005 1006 try std.testing.expect(null == try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id))); 1007 1008 // get the repos created by the user 1009 const user_id_to_repos_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repos")) orelse return error.NotFound; 1010 const user_id_to_repos = try Repo.DB.HashMap(.read_only).init(user_id_to_repos_cursor); 1011 1012 const user_repos_cursor = try user_id_to_repos.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 1013 const user_repos = try Repo.DB.CountedHashSet(.read_only).init(user_repos_cursor); 1014 1015 try std.testing.expectEqual(0, try user_repos.count()); 1016 try std.testing.expect(null == try user_repos.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id))); 1017 } 1018 1019 // 1020 // remove the user 1021 // 1022 1023 const events_to_consume3 = [_]evt.EventWithId{ 1024 .{ 1025 .id = std.fmt.bytesToHex(user_event_id, .lower), 1026 .event = .{ .user = null }, 1027 }, 1028 }; 1029 1030 { 1031 var json: std.Io.Writer.Allocating = .init(allocator); 1032 defer json.deinit(); 1033 1034 for (events_to_consume3) |event| { 1035 json.clearRetainingCapacity(); 1036 1037 try std.json.Stringify.value(event, .{}, &json.writer); 1038 1039 // commit the event into a special branch 1040 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 1041 } 1042 } 1043 1044 // 1045 // consume the user removal into the database 1046 // 1047 1048 { 1049 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 1050 1051 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 1052 1053 // read the moment we just created 1054 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 1055 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 1056 1057 // get the last object id 1058 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 1059 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 1060 _ = try last_object_id_cursor.readBytes(&last_object_id); 1061 1062 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 1063 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 1064 1065 try std.testing.expectEqual(3, try haxy.count()); 1066 1067 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 1068 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 1069 1070 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 1071 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 1072 1073 // get the map of users 1074 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 1075 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 1076 1077 try std.testing.expect(null == try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id))); 1078 } 1079 } 1080