╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ net.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
454 .{ .wire = .{ .ssh = .{ 455 .command = ssh_cmd_maybe, 456 } } }, 457 )); 458 459 // verify the server ref was not updated (push was denied) 460 { 461 const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 462 try std.testing.expectEqualStrings(&oid_before_denied_push, &oid_master); 463 } 464 465 // remove denyNonFastForwards from server 466 try server_repo.removeConfig(io, allocator, .{ .name = "receive.denynonfastforwards" }); 467 } 468 469 // force push 470 try client_repo.push( 471 io, 472 allocator, 473 "origin", 474 "master", 475 true, 476 .{ .wire = .{ .ssh = .{ 477 .command = ssh_cmd_maybe, 478 } } }, 479 ); 480 481 // make sure push was successful 482 { 483 const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 484 try std.testing.expectEqualStrings(&commit2, &oid_master); 485 } 486 487 // remove the remote tag 488 try client_repo.push( 489 io, 490 allocator, 491 "origin", 492 ":refs/tags/1.0.0", 493 false, 494 .{ .wire = .{ .ssh = .{ 495 .command = ssh_cmd_maybe, 496 } } }, 497 ); 498 499 // make sure push was successful 500 try std.testing.expect(null == try server_repo.readRef(io, .{ .kind = .tag, .name = "1.0.0" })); 501 } 502 503 fn testPushFork( 504 comptime hash_kind: hash.HashKind, 505 comptime port: u16, 506 io: std.Io, 507 allocator: std.mem.Allocator, 508 ) !void { 509 const repo_opts: rp.RepoOpts(.xit) = .{ .hash = hash_kind }; 510 511 var temp = std.testing.tmpDir(.{}); 512 defer temp.cleanup(); 513 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 514 defer allocator.free(temp_path); 515 516 var server_process = try runServer(io, allocator, temp_path, port); 517 defer _ = server_process.kill(io); 518 519 const target_path = (try repoOnDiskPath(io, allocator, temp_path, "target", true)).?; 520 defer allocator.free(target_path); 521 const base_oid = blk: { 522 var target_repo = try rp.Repo(.xit, repo_opts).init(io, allocator, .{ .bare = true, .path = target_path }); 523 defer target_repo.deinit(io, allocator); 524 break :blk try commitServer(&target_repo, io, allocator, .{ .files = &.{.{ .path = "main.txt", .content = "base contents\n" }} }, .{ .author = "admin <admin@example.test>", .message = "initial code" }); 525 }; 526 527 const other_path = (try repoOnDiskPath(io, allocator, temp_path, "other", true)).?; 528 defer allocator.free(other_path); 529 530 const admin_path = try std.fs.path.join(allocator, &.{ temp_path, "admin" }); 531 defer allocator.free(admin_path); 532 const repos_dir = try std.fs.path.join(allocator, &.{ temp_path, "repos" }); 533 defer allocator.free(repos_dir); 534 535 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 536 const fork_id = evt.EventWithId.randomId(prng.random()); 537 const fork_id_hex = std.fmt.bytesToHex(fork_id, .lower); 538 var user_id: [evt.event_id_size]u8 = undefined; 539 var repo_id: [evt.event_id_size]u8 = undefined; 540 const draft_path = blk: { 541 var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_path }); 542 defer admin.deinit(io, allocator); 543 var arena = std.heap.ArenaAllocator.init(allocator); 544 defer arena.deinit(); 545 const moment = try evt.currentMoment(evt.admin_repo_opts, &admin); 546 const target = (try evt.Repo.readByOwnerAndName(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, "admin", "target")) orelse return error.NotFound; 547 repo_id = target.event_id; 548 @memcpy(&user_id, target.repo.event.user_id); 549 break :blk try fork.create(repo_opts, io, allocator, repos_dir, &admin, .{ 550 .id = fork_id_hex, 551 .user_id = user_id, 552 .repo_id = repo_id, 553 .title = "add a feature", 554 .description = "a draft patch", 555 .tags = "enhancement", 556 .target_branch = "master", 557 .author = .{ .name = "admin", .email = "admin@example.test" }, 558 .timestamp = 1, 559 }); 560 }; 561 defer allocator.free(draft_path); 562 563 const client_path = try std.fs.path.join(allocator, &.{ temp_path, "client" }); 564 defer allocator.free(client_path); 565 const ssh_cmd = (try sshCommand(true, allocator, temp_path, port)).?; 566 defer allocator.free(ssh_cmd); 567 var client = try rp.Repo(.xit, .{ .hash = hash_kind, .is_test = true }).clone( 568 io, 569 allocator, 570 "git@localhost:admin/target", 571 client_path, 572 client_path, 573 null, 574 .{ .transport = .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } } }, 575 ); 576 defer client.deinit(io, allocator); 577 578 const source_oid = blk: { 579 const file = try client.core.work_dir.createFile(io, "feature.txt", .{ .truncate = true }); 580 defer file.close(io); 581 try file.writeStreamingAll(io, "fork contents\n"); 582 try client.add(io, allocator, &.{"feature.txt"}); 583 break :blk try client.commit(io, allocator, .{ .message = "add feature" }); 584 }; 585 586 const remote_url = try std.fmt.allocPrint(allocator, "git@localhost:fork/admin/target/patch:{s}", .{&fork_id_hex}); 587 defer allocator.free(remote_url); 588 try client.addRemote(io, allocator, .{ .name = "patch", .value = remote_url }); 589 try client.push(io, allocator, "patch", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } }); 590 591 var first_revision_id: [evt.event_id_size]u8 = undefined; 592 { 593 var draft = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = draft_path }); 594 defer draft.deinit(io, allocator); 595 try std.testing.expect(try draft.isBare(io, allocator)); 596 var arena = std.heap.ArenaAllocator.init(allocator); 597 defer arena.deinit(); 598 const moment = try evt.currentMoment(repo_opts, &draft); 599 const patch = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 600 try std.testing.expect(!patch.removed); 601 try std.testing.expectEqualStrings(&source_oid, &(try draft.readRef(io, fork.ref) orelse return error.NotFound)); 602 const selected = patch.event.revision orelse return error.NotFound; 603 try std.testing.expectEqualStrings("master", patch.event.target_branch); 604 try std.testing.expectEqualStrings(&source_oid, selected.source_oid); 605 try std.testing.expectEqual(@as(?u64, 1), selected.commit_count); 606 first_revision_id = try evt.parseEventId(&selected.id); 607 const revision = (try evt.PatchRev.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &first_revision_id)) orelse return error.NotFound; 608 try std.testing.expectEqualStrings(&source_oid, revision.event.source_oid); 609 } 610 611 // 612 // publish, then push another revision 613 // 614 615 { 616 var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_path }); 617 defer admin.deinit(io, allocator); 618 var target = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path }); 619 defer target.deinit(io, allocator); 620 // pushing a draft does not create haxy state or a mergeability cache 621 try std.testing.expectError(error.NotFound, evt.currentMoment(repo_opts, &target)); 622 try std.testing.expectEqual(null, try (try target.core.latestMoment()).getCursor(hash.hashInt(hash_kind, evt.Patch.patch_id_to_mergeability_key))); 623 try target.addBranch(io, .{ .name = "feature" }); 624 try pch.publish(repo_opts, io, allocator, &admin, &target, draft_path, .{ 625 .id = fork_id_hex, 626 .user_id = user_id, 627 .repo_id = repo_id, 628 .author = .{ .name = "admin", .email = "admin@example.test" }, 629 .timestamp = 2, 630 }); 631 var arena = std.heap.ArenaAllocator.init(allocator); 632 defer arena.deinit(); 633 const moment = try evt.currentMoment(repo_opts, &target); 634 const patch = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 635 try evt.Patch.update(.server, .xit, repo_opts, io, allocator, &target, &fork_id, .{ .fields = .{ 636 .title = "edited feature", 637 .description = patch.event.description, 638 .tags = patch.event.tags, 639 .target_branch = "feature", 640 } }, .{ .name = "admin", .email = "admin@example.test" }); 641 } 642 643 const feature_remote_url = try std.fmt.allocPrint(allocator, "git@localhost:fork/admin/target/patch:{s}", .{&fork_id_hex}); 644 defer allocator.free(feature_remote_url); 645 try client.addRemote(io, allocator, .{ .name = "patch-to-feature", .value = feature_remote_url }); 646 647 const second_source_oid = blk: { 648 const file = try client.core.work_dir.createFile(io, "feature.txt", .{ .truncate = true }); 649 defer file.close(io); 650 try file.writeStreamingAll(io, "updated fork contents\n"); 651 try client.add(io, allocator, &.{"feature.txt"}); 652 break :blk try client.commit(io, allocator, .{ .message = "revise feature" }); 653 }; 654 try client.push(io, allocator, "patch-to-feature", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } }); 655 656 var second_revision_id: [evt.event_id_size]u8 = undefined; 657 var second_patch_oid: [hash.hexLen(hash_kind)]u8 = undefined; 658 { 659 var draft = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = draft_path }); 660 defer draft.deinit(io, allocator); 661 var arena = std.heap.ArenaAllocator.init(allocator); 662 defer arena.deinit(); 663 const moment = try evt.currentMoment(repo_opts, &draft); 664 const patch = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 665 try std.testing.expect(patch.removed); 666 const newest = (try evt.PatchRev.readNewest(evt.EventDB(hash_kind), hash_kind, moment, &arena)) orelse return error.NotFound; 667 second_revision_id = newest.id; 668 @memcpy(&second_patch_oid, newest.record.patch_oid); 669 try std.testing.expect(!std.mem.eql(u8, &first_revision_id, &second_revision_id)); 670 try std.testing.expectEqualStrings(&second_source_oid, newest.record.event.source_oid); 671 try std.testing.expectEqualStrings("edited feature", newest.record.event.message); 672 } 673 { 674 var target = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path }); 675 defer target.deinit(io, allocator); 676 var arena = std.heap.ArenaAllocator.init(allocator); 677 defer arena.deinit(); 678 const moment = try evt.currentMoment(repo_opts, &target); 679 const patch = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 680 try std.testing.expectEqualStrings("edited feature", patch.event.title); 681 const selected = patch.event.revision orelse return error.NotFound; 682 try std.testing.expectEqualStrings(&std.fmt.bytesToHex(second_revision_id, .lower), &selected.id); 683 try std.testing.expectEqualStrings(&second_source_oid, selected.source_oid); 684 try std.testing.expectEqual(@as(?u64, 2), selected.commit_count); 685 try std.testing.expectEqualStrings("feature", patch.event.target_branch); 686 } 687 688 // 689 // reject extra refs without applying the patch update 690 // 691 692 const third_source_oid = blk: { 693 const file = try client.core.work_dir.createFile(io, "feature.txt", .{ .truncate = true }); 694 defer file.close(io); 695 try file.writeStreamingAll(io, "rejected fork contents\n"); 696 try client.add(io, allocator, &.{"feature.txt"}); 697 break :blk try client.commit(io, allocator, .{ .message = "rejected revision" }); 698 }; 699 const extra_ref_rejected = if (client.push(io, allocator, "patch-to-feature", "master:patch", false, .{ 700 .refspecs = &.{"refs/heads/master:refs/heads/extra"}, 701 .wire = .{ .ssh = .{ .command = ssh_cmd } }, 702 })) |_| false else |_| true; 703 try std.testing.expect(extra_ref_rejected); 704 { 705 var draft = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = draft_path }); 706 defer draft.deinit(io, allocator); 707 try std.testing.expectEqualStrings(&second_source_oid, &(try draft.readRef(io, fork.ref) orelse return error.NotFound)); 708 try std.testing.expectEqual(null, try draft.readRef(io, .{ .kind = .head, .name = "extra" })); 709 var arena = std.heap.ArenaAllocator.init(allocator); 710 defer arena.deinit(); 711 const moment = try evt.currentMoment(repo_opts, &draft); 712 const newest = (try evt.PatchRev.readNewest(evt.EventDB(hash_kind), hash_kind, moment, &arena)) orelse return error.NotFound; 713 try std.testing.expectEqualSlices(u8, &second_revision_id, &newest.id); 714 } 715 716 // 717 // a removed target patch stays removed after another push 718 // 719 720 { 721 var target = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path }); 722 defer target.deinit(io, allocator); 723 try evt.remove(.server, .repo, .xit, repo_opts, io, allocator, &target, &fork_id, .patch, .{ .name = "admin", .email = "admin@example.test" }); 724 } 725 try client.push(io, allocator, "patch-to-feature", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } }); 726 { 727 var target = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path }); 728 defer target.deinit(io, allocator); 729 var arena = std.heap.ArenaAllocator.init(allocator); 730 defer arena.deinit(); 731 const moment = try evt.currentMoment(repo_opts, &target); 732 const patch = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 733 try std.testing.expect(patch.removed); 734 } 735 736 // 737 // restore the patch, merge a descendant of its source oid, and push the merge 738 // 739 740 { 741 var target = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path }); 742 defer target.deinit(io, allocator); 743 var arena = std.heap.ArenaAllocator.init(allocator); 744 defer arena.deinit(); 745 const moment = try evt.currentMoment(repo_opts, &target); 746 const removed = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 747 try evt.consume(.server, .repo, .xit, repo_opts, io, allocator, &target, evt.events_ref, &.{.{ 748 .id = fork_id_hex, 749 .timestamp = 4, 750 .author = .{ .name = "admin", .email = "admin@example.test" }, 751 .event = .{ .patch = removed.event }, 752 }}); 753 } 754 const merge_parents = [_][hash.hexLen(hash_kind)]u8{ base_oid, third_source_oid }; 755 const merge_oid = try client.commit(io, allocator, .{ .message = "merge patch", .parent_oids = &merge_parents }); 756 // later commits in the push must not be mistaken for the merge itself 757 _ = try client.commit(io, allocator, .{ .message = "after merge", .allow_empty = true }); 758 try client.push(io, allocator, "origin", "master:feature", false, .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } }); 759 { 760 var target = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path }); 761 defer target.deinit(io, allocator); 762 var arena = std.heap.ArenaAllocator.init(allocator); 763 defer arena.deinit(); 764 const moment = try evt.currentMoment(repo_opts, &target); 765 const patch = (try evt.Patch.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &fork_id)) orelse return error.NotFound; 766 try std.testing.expectEqual(.merged, patch.event.status.kind()); 767 const merged_status = switch (patch.event.status) { 768 .merged => |value| value, 769 else => return error.InvalidPatch, 770 }; 771 try std.testing.expectEqual(.source, merged_status.revision); 772 const result_id = try evt.parseEventId(&merged_status.patchrev_id); 773 const result = (try evt.PatchRev.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &result_id)) orelse return error.NotFound; 774 try std.testing.expectEqualStrings(&base_oid, result.event.base_oid); 775 try std.testing.expectEqualStrings(&merge_oid, result.event.source_oid); 776 const imported = (try evt.PatchRev.readById(evt.EventDB(hash_kind), hash_kind, moment, &arena, &second_revision_id)) orelse return error.NotFound; 777 try std.testing.expectEqualStrings(&second_patch_oid, imported.patch_oid); 778 } 779 780 const other_url = try std.fmt.allocPrint(allocator, "git@localhost:fork/admin/other/patch:{s}", .{&fork_id_hex}); 781 defer allocator.free(other_url); 782 try client.addRemote(io, allocator, .{ .name = "other", .value = other_url }); 783 // early rejections need no input. give ssh eof so windows openssh 784 // doesn't hang closing stdin before forwarding the error. 785 const reject_ssh_cmd = try std.fmt.allocPrint(allocator, "{s} -n", .{ssh_cmd}); 786 defer allocator.free(reject_ssh_cmd); 787 try std.testing.expectError(error.ServerReportedError, client.push(io, allocator, "other", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = reject_ssh_cmd } } })); 788 789 // 790 // the fork remains cloneable after the target repo is removed 791 // 792 793 { 794 var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_path }); 795 defer admin.deinit(io, allocator); 796 try evt.remove(.server, .admin, .xit, evt.admin_repo_opts, io, allocator, &admin, &repo_id, .repo, .{ .name = "admin", .email = "admin@example.test" }); 797 } 798 const clone_path = try std.fs.path.join(allocator, &.{ temp_path, "fork-clone" }); 799 defer allocator.free(clone_path); 800 var fork_clone = try rp.Repo(.xit, .{ .hash = hash_kind, .is_test = true }).clone( 801 io, 802 allocator, 803 remote_url, 804 clone_path, 805 clone_path, 806 null, 807 .{ .transport = .{ .wire = .{ .ssh = .{ .command = ssh_cmd } } } }, 808 ); 809 defer fork_clone.deinit(io, allocator); 810 try std.testing.expectEqualStrings(&third_source_oid, &(try fork_clone.readRef(io, fork.ref) orelse return error.NotFound)); 811 812 { 813 var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_path }); 814 defer admin.deinit(io, allocator); 815 try evt.consume(.server, .admin, .xit, evt.admin_repo_opts, io, allocator, &admin, evt.events_ref, &.{ 816 .{ 817 .id = std.fmt.bytesToHex(repo_id, .lower), 818 .timestamp = 2, 819 .author = .{ .name = "admin", .email = "admin@example.test" }, 820 .event = .{ .repo = .{ 821 .user_id = &user_id, 822 .name = "target", 823 .description = "", 824 } }, 825 }, 826 .{ 827 .id = std.fmt.bytesToHex(user_id, .lower), 828 .timestamp = 2, 829 .author = .{ .name = "admin", .email = "admin@example.test" }, 830 .event = .{ .user = .{ 831 .name = "admin", 832 .email = "admin@example.test", 833 .password_hash = "", 834 .ssh_keys = "", 835 } }, 836 }, 837 }); 838 } 839 840 const denied_url = try std.fmt.allocPrint(allocator, "git@localhost:fork/admin/target/patch:{s}", .{&fork_id_hex}); 841 defer allocator.free(denied_url); 842 try client.addRemote(io, allocator, .{ .name = "denied", .value = denied_url }); 843 try std.testing.expectError(error.ServerReportedError, client.push(io, allocator, "denied", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = reject_ssh_cmd } } })); 844 } 845 846 fn testPushCreatesMissingRepo( 847 comptime repo_kind: rp.RepoKind, 848 comptime transport_def: net.TransportDefinition, 849 comptime port: u16, 850 io: std.Io, 851 allocator: std.mem.Allocator, 852 ) !void { 853 var temp = std.testing.tmpDir(.{}); 854 defer temp.cleanup(); 855 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 856 defer allocator.free(temp_path); 857 858 var server_process = try runServer(io, allocator, temp_path, port); 859 defer _ = server_process.kill(io); 860 861 const client_path = try std.fs.path.join(allocator, &.{ temp_path, "client" }); 862 defer allocator.free(client_path); 863 864 var client_repo = try rp.Repo(repo_kind, .{ .is_test = true }).init(io, allocator, .{ .path = client_path }); 865 defer client_repo.deinit(io, allocator); 866 867 const commit1 = blk: { 868 const hello_txt = try client_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 869 defer hello_txt.close(io); 870 try hello_txt.writeStreamingAll(io, "hello, world!"); 871 try client_repo.add(io, allocator, &.{"hello.txt"}); 872 break :blk try client_repo.commit(io, allocator, .{ .message = "let there be light" }); 873 }; 874 875 _ = try client_repo.addTag(io, allocator, .{ .name = "1.0.0", .message = "hi" }); 876 877 // the repo is not pre-registered; the push should mint the repo event under 878 // admin and create the repo on disk 879 { 880 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 881 defer allocator.free(remote_url); 882 883 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 884 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 885 } 886 887 const is_ssh = switch (transport_def) { 888 .file => false, 889 .wire => |wire_kind| .ssh == wire_kind, 890 }; 891 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 892 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 893 894 try client_repo.push( 895 io, 896 allocator, 897 "origin", 898 "master", 899 false, 900 .{ .refspecs = &.{"refs/tags/1.0.0:refs/tags/1.0.0"}, .wire = .{ .ssh = .{ 901 .command = ssh_cmd_maybe, 902 } } }, 903 ); 904 905 // the push registered admin/server; resolve its on-disk directory 906 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", false)).?; 907 defer allocator.free(server_path); 908 909 var server_repo = try rp.Repo(.xit, .{ .is_test = true }).open(io, allocator, .{ .path = server_path }); 910 defer server_repo.deinit(io, allocator); 911 try std.testing.expect(try server_repo.isBare(io, allocator)); 912 913 try std.testing.expect(null != try server_repo.readRef(io, .{ .kind = .tag, .name = "1.0.0" })); 914 915 const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 916 try std.testing.expectEqualStrings(&commit1, &oid_master); 917 } 918 919 fn testPushEvents( 920 comptime repo_kind: rp.RepoKind, 921 comptime transport_def: net.TransportDefinition, 922 comptime hash_kind: hash.HashKind, 923 comptime port: u16, 924 io: std.Io, 925 allocator: std.mem.Allocator, 926 ) !void { 927 // create the temp dir 928 var temp = std.testing.tmpDir(.{}); 929 defer temp.cleanup(); 930 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 931 defer allocator.free(temp_path); 932 933 // init server 934 var server_process = try runServer(io, allocator, temp_path, port); 935 defer _ = server_process.kill(io); 936 937 // register the repo under admin and locate its on-disk directory 938 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; 939 defer allocator.free(server_path); 940 941 const repo_opts: rp.RepoOpts(.xit) = .{ .hash = hash_kind, .is_test = true }; 942 const ServerRepo = rp.Repo(.xit, repo_opts); 943 var server_repo = try ServerRepo.init(io, allocator, .{ .bare = true, .path = server_path }); 944 defer server_repo.deinit(io, allocator); 945 946 const client_path = try std.fs.path.join(allocator, &.{ temp_path, "client" }); 947 defer allocator.free(client_path); 948 949 var client_repo = try rp.Repo(repo_kind, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .path = client_path }); 950 defer client_repo.deinit(io, allocator); 951 952 // add remote 953 { 954 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 955 defer allocator.free(remote_url); 956 957 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 958 } 959 960 const is_ssh = switch (transport_def) { 961 .file => false, 962 .wire => |wire_kind| .ssh == wire_kind, 963 }; 964 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 965 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 966 967 var arena = std.heap.ArenaAllocator.init(allocator); 968 defer arena.deinit(); 969 970 // 971 // define test events 972 // 973 974 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 975 const issue_event_id = evt.EventWithId.randomId(prng.random()); 976 977 const events_to_push = [_]evt.EventWithId{ 978 .{ 979 .id = std.fmt.bytesToHex(issue_event_id, .lower), 980 .author = .{ .name = "haxy", .email = "user@haxy" }, 981 .event = .{ 982 .issue = .{ 983 .title = "Login form clears password on validation error", 984 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 985 .tags = "bug priority-high ui", 986 }, 987 }, 988 }, 989 }; 990 991 // commit the issue on the client and push it to the server 992 try evt.consume(.local, .repo, repo_kind, .{ .hash = hash_kind, .is_test = true }, io, allocator, &client_repo, evt.events_ref, &events_to_push); 993 try client_repo.push( 994 io, 995 allocator, 996 "origin", 997 "haxy/events", 998 false, 999 .{ .wire = .{ .ssh = .{ 1000 .command = ssh_cmd_maybe, 1001 } } }, 1002 ); 1003 1004 // the push consumed the issue into the server repo's db 1005 { 1006 const haxy_moment = try evt.currentMoment(repo_opts, &server_repo); 1007 const records_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.record_map_key)) orelse return error.NotFound; 1008 const records = try ServerRepo.DB.HashMap(.read_only).init(records_cursor); 1009 const issue_cursor = try records.getCursor(hash.hashInt(repo_opts.hash, &issue_event_id)) orelse return error.NotFound; 1010 const issue = try evt.read(evt.Issue.Record, ServerRepo.DB, repo_opts.hash, &arena, try ServerRepo.DB.HashMap(.read_only).init(issue_cursor)); 1011 1012 try std.testing.expectEqualStrings(events_to_push[0].event.issue.?.description, issue.event.description); 1013 try std.testing.expectEqualStrings(events_to_push[0].event.issue.?.tags, issue.event.tags); 1014 } 1015 1016 // 1017 // edit the issue 1018 // 1019 1020 // this event edits the previous one because it has the same id 1021 const events_to_push2 = [_]evt.EventWithId{ 1022 .{ 1023 .id = std.fmt.bytesToHex(issue_event_id, .lower), 1024 .author = .{ .name = "haxy", .email = "user@haxy" }, 1025 .event = .{ 1026 .issue = .{ 1027 .title = "Login form clears password on validation error", 1028 .description = "Submitting an invalid email address resets the password field and removes typed input. Preserve the field value and show an inline validation message.", 1029 .tags = "bug priority-low ui", 1030 }, 1031 }, 1032 }, 1033 }; 1034 1035 // commit the edit on the client and push it to the server 1036 try evt.consume(.local, .repo, repo_kind, .{ .hash = hash_kind, .is_test = true }, io, allocator, &client_repo, evt.events_ref, &events_to_push2); 1037 try client_repo.push( 1038 io, 1039 allocator, 1040 "origin", 1041 "haxy/events", 1042 false, 1043 .{ .wire = .{ .ssh = .{ 1044 .command = ssh_cmd_maybe, 1045 } } }, 1046 ); 1047 1048 // the push consumed the edit into the server repo's db 1049 { 1050 const haxy_moment = try evt.currentMoment(repo_opts, &server_repo); 1051 const records_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.record_map_key)) orelse return error.NotFound; 1052 const records = try ServerRepo.DB.HashMap(.read_only).init(records_cursor); 1053 const issue_cursor = try records.getCursor(hash.hashInt(repo_opts.hash, &issue_event_id)) orelse return error.NotFound; 1054 const issue = try evt.read(evt.Issue.Record, ServerRepo.DB, repo_opts.hash, &arena, try ServerRepo.DB.HashMap(.read_only).init(issue_cursor)); 1055 1056 // the description and tags were correctly edited 1057 try std.testing.expectEqualStrings(events_to_push2[0].event.issue.?.description, issue.event.description); 1058 try std.testing.expectEqualStrings(events_to_push2[0].event.issue.?.tags, issue.event.tags); 1059 } 1060 } 1061 1062 fn testClone( 1063 comptime repo_kind: rp.RepoKind, 1064 comptime transport_def: net.TransportDefinition, 1065 comptime shell_out_to_git: bool, 1066 comptime hash_kind: hash.HashKind, 1067 comptime port: u16, 1068 io: std.Io, 1069 allocator: std.mem.Allocator, 1070 ) !void { 1071 // create the temp dir 1072 var temp = std.testing.tmpDir(.{}); 1073 defer temp.cleanup(); 1074 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 1075 defer allocator.free(temp_path); 1076 1077 // init server 1078 var server_process = try runServer(io, allocator, temp_path, port); 1079 defer _ = server_process.kill(io); 1080 1081 // register the repo under admin and locate its on-disk directory 1082 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; 1083 defer allocator.free(server_path); 1084 1085 // init server repo with default branch name as main 1086 // is_test must be false when shell_out_to_git so commits get real timestamps (needed for --shallow-since) 1087 var server_repo = try rp.Repo(.xit, .{ .hash = hash_kind, .is_test = !shell_out_to_git }).init(io, allocator, .{ .bare = true, .path = server_path, .create_default_branch = "main" }); 1088 defer server_repo.deinit(io, allocator); 1089 1090 if (shell_out_to_git) { 1091 try server_repo.addConfig(io, allocator, .{ .name = "user.name", .value = "test" }); 1092 try server_repo.addConfig(io, allocator, .{ .name = "user.email", .value = "test@test" }); 1093 try server_repo.addConfig(io, allocator, .{ .name = "uploadpack.allowfilter", .value = "true" }); 1094 } 1095 1096 // make a commit 1097 _ = try commitServer(&server_repo, io, allocator, .{ .files = &.{.{ .path = "hello.txt", .content = "hello, world!" }} }, .{ .message = "let there be light" }); 1098 1099 // tag first commit 1100 _ = try server_repo.addTag(io, allocator, .{ .name = "v1", .message = "first" }); 1101 1102 // make a commit 1103 _ = try commitServer(&server_repo, io, allocator, .{ .files = &.{.{ .path = "goodbye.txt", .content = "goodbye, world!" }} }, .{ .message = "add goodbye file" }); 1104 1105 // export server repo 1106 { 1107 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1108 defer export_file.close(io); 1109 } 1110 1111 const client_path = try std.fs.path.join(allocator, &.{ temp_path, "client" }); 1112 defer allocator.free(client_path); 1113 1114 // get remote url 1115 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 1116 defer allocator.free(remote_url); 1117 1118 const is_ssh = switch (transport_def) { 1119 .file => false, 1120 .wire => |wire_kind| .ssh == wire_kind, 1121 }; 1122 1123 if (shell_out_to_git) { 1124 const ssh_cmd = (try sshCommand(true, allocator, temp_path, port)).?; 1125 defer allocator.free(ssh_cmd); 1126 const ssh_config_arg = try std.fmt.allocPrint(allocator, "core.sshCommand={s}", .{ssh_cmd}); 1127 defer allocator.free(ssh_config_arg); 1128 1129 { 1130 var process = try std.process.spawn(io, .{ 1131 .argv = if (is_ssh) 1132 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--depth", "1", remote_url, "client" } 1133 else 1134 &.{ "git", "clone", "--quiet", "--depth", "1", remote_url, "client" }, 1135 .cwd = .{ .path = temp_path }, 1136 .stdin = .ignore, 1137 .stdout = .ignore, 1138 .stderr = .inherit, 1139 }); 1140 defer process.kill(io); 1141 const term = try process.wait(io); 1142 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1143 } 1144 1145 // make sure shallow clone was successful 1146 { 1147 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 1148 hello_txt.close(io); 1149 } 1150 1151 // make a third commit on the server 1152 _ = try commitServer(&server_repo, io, allocator, .{ .files = &.{.{ .path = "extra.txt", .content = "extra content" }} }, .{ .message = "add extra file" }); 1153 1154 // pull --unshallow to deepen the clone and get the new commit 1155 { 1156 var process = try std.process.spawn(io, .{ 1157 .argv = if (is_ssh) 1158 &.{ "git", "-c", ssh_config_arg, "pull", "--quiet", "--unshallow" } 1159 else 1160 &.{ "git", "pull", "--quiet", "--unshallow" }, 1161 .cwd = .{ .path = client_path }, 1162 .stdin = .ignore, 1163 .stdout = .ignore, 1164 .stderr = .inherit, 1165 }); 1166 defer process.kill(io); 1167 const term = try process.wait(io); 1168 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1169 } 1170 1171 // make sure unshallow pull was successful 1172 { 1173 const extra_txt = try temp.dir.openFile(io, "client/extra.txt", .{}); 1174 extra_txt.close(io); 1175 } 1176 1177 // delete client and clone again with --shallow-since 1178 try temp.dir.deleteTree(io, "client"); 1179 1180 { 1181 var process = try std.process.spawn(io, .{ 1182 .argv = if (is_ssh) 1183 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--shallow-since=2000-01-01", remote_url, "client" } 1184 else 1185 &.{ "git", "clone", "--quiet", "--shallow-since=2000-01-01", remote_url, "client" }, 1186 .cwd = .{ .path = temp_path }, 1187 .stdin = .ignore, 1188 .stdout = .ignore, 1189 .stderr = .inherit, 1190 }); 1191 defer process.kill(io); 1192 const term = try process.wait(io); 1193 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1194 } 1195 1196 // make sure shallow clone was successful 1197 { 1198 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 1199 hello_txt.close(io); 1200 } 1201 1202 // delete client and clone again with --shallow-exclude 1203 try temp.dir.deleteTree(io, "client"); 1204 1205 { 1206 var process = try std.process.spawn(io, .{ 1207 .argv = if (is_ssh) 1208 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--shallow-exclude=v1", remote_url, "client" } 1209 else 1210 &.{ "git", "clone", "--quiet", "--shallow-exclude=v1", remote_url, "client" }, 1211 .cwd = .{ .path = temp_path }, 1212 .stdin = .ignore, 1213 .stdout = .ignore, 1214 .stderr = .inherit, 1215 }); 1216 defer process.kill(io); 1217 const term = try process.wait(io); 1218 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1219 } 1220 1221 // make sure shallow clone was successful 1222 { 1223 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 1224 hello_txt.close(io); 1225 } 1226 1227 // delete client and clone again with --filter=blob:none 1228 try temp.dir.deleteTree(io, "client"); 1229 1230 { 1231 var process = try std.process.spawn(io, .{ 1232 .argv = if (is_ssh) 1233 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--filter=blob:none", remote_url, "client" } 1234 else 1235 &.{ "git", "clone", "--quiet", "--filter=blob:none", remote_url, "client" }, 1236 .cwd = .{ .path = temp_path }, 1237 .stdin = .ignore, 1238 .stdout = .ignore, 1239 .stderr = .inherit, 1240 }); 1241 defer process.kill(io); 1242 const term = try process.wait(io); 1243 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1244 } 1245 1246 // make sure partial clone was successful 1247 { 1248 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 1249 hello_txt.close(io); 1250 } 1251 1252 // delete client and clone again with --filter=tree:0 1253 try temp.dir.deleteTree(io, "client"); 1254 1255 { 1256 var process = try std.process.spawn(io, .{ 1257 .argv = if (is_ssh) 1258 &.{ "git", "-c", ssh_config_arg, "clone", "--quiet", "--filter=tree:0", remote_url, "client" } 1259 else 1260 &.{ "git", "clone", "--quiet", "--filter=tree:0", remote_url, "client" }, 1261 .cwd = .{ .path = temp_path }, 1262 .stdin = .ignore, 1263 .stdout = .ignore, 1264 .stderr = .inherit, 1265 }); 1266 defer process.kill(io); 1267 const term = try process.wait(io); 1268 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1269 } 1270 1271 // make sure treeless clone was successful 1272 { 1273 const goodbye_txt = try temp.dir.openFile(io, "client/goodbye.txt", .{}); 1274 goodbye_txt.close(io); 1275 } 1276 } else { 1277 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 1278 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1279 1280 // clone repo 1281 var clone_result = try rp.AnyRepo(repo_kind, .{ .is_test = true }).clone( 1282 io, 1283 allocator, 1284 remote_url, 1285 temp_path, 1286 client_path, 1287 null, 1288 .{ .transport = .{ .wire = .{ .ssh = .{ 1289 .command = ssh_cmd_maybe, 1290 } } } }, 1291 ); 1292 defer clone_result.deinit(io, allocator); 1293 const client_repo = &@field(clone_result, @tagName(hash_kind)); 1294 1295 // make sure HEAD points to the right default branch 1296 var current_branch_buffer = [_]u8{0} ** rf.MAX_REF_CONTENT_SIZE; 1297 const head = try client_repo.head(io, &current_branch_buffer); 1298 try std.testing.expectEqualStrings("main", head.ref.name); 1299 1300 // make sure clone was successful 1301 const hello_txt = try temp.dir.openFile(io, "client/hello.txt", .{}); 1302 defer hello_txt.close(io); 1303 } 1304 } 1305 1306 fn testFetchLarge( 1307 comptime repo_kind: rp.RepoKind, 1308 comptime transport_def: net.TransportDefinition, 1309 comptime shell_out_to_git: bool, 1310 comptime hash_kind: hash.HashKind, 1311 comptime port: u16, 1312 io: std.Io, 1313 allocator: std.mem.Allocator, 1314 ) !void { 1315 // create the temp dir 1316 var temp = std.testing.tmpDir(.{}); 1317 defer temp.cleanup(); 1318 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 1319 defer allocator.free(temp_path); 1320 1321 // init server 1322 var server_process = try runServer(io, allocator, temp_path, port); 1323 defer _ = server_process.kill(io); 1324 1325 // register the repo under admin and locate its on-disk directory 1326 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; 1327 defer allocator.free(server_path); 1328 1329 var server_repo = try rp.Repo(.xit, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .bare = true, .path = server_path }); 1330 defer server_repo.deinit(io, allocator); 1331 1332 // make a commit with files from the current repo 1333 const commit1 = try commitServer(&server_repo, io, allocator, .{ .dirs = &.{"src"} }, .{ .message = "let there be light" }); 1334 1335 // export server repo 1336 { 1337 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1338 defer export_file.close(io); 1339 } 1340 1341 if (shell_out_to_git) { 1342 try server_repo.addConfig(io, allocator, .{ .name = "uploadpack.allowrefinwant", .value = "true" }); 1343 } 1344 1345 const client_path = try std.fs.path.join(allocator, &.{ temp_path, "client" }); 1346 defer allocator.free(client_path); 1347 1348 var client_repo = try rp.Repo(repo_kind, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .path = client_path }); 1349 defer client_repo.deinit(io, allocator); 1350 1351 // add remote 1352 { 1353 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 1354 defer allocator.free(remote_url); 1355 1356 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 1357 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 1358 } 1359 1360 const is_ssh = switch (transport_def) { 1361 .file => false, 1362 .wire => |wire_kind| .ssh == wire_kind, 1363 }; 1364 1365 if (shell_out_to_git) { 1366 const ssh_cmd = (try sshCommand(true, allocator, temp_path, port)).?; 1367 defer allocator.free(ssh_cmd); 1368 const ssh_config_arg = try std.fmt.allocPrint(allocator, "core.sshCommand={s}", .{ssh_cmd}); 1369 defer allocator.free(ssh_config_arg); 1370 1371 { 1372 var process = try std.process.spawn(io, .{ 1373 .argv = if (is_ssh) 1374 &.{ "git", "-c", ssh_config_arg, "pull", "--quiet", "origin", "master" } 1375 else 1376 &.{ "git", "pull", "--quiet", "origin", "master" }, 1377 .cwd = .{ .path = client_path }, 1378 .stdin = .ignore, 1379 .stdout = .ignore, 1380 .stderr = .inherit, 1381 }); 1382 defer process.kill(io); 1383 const term = try process.wait(io); 1384 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1385 } 1386 1387 // make sure pull was successful 1388 { 1389 const oid_master = (try client_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1390 try std.testing.expectEqualStrings(&commit1, &oid_master); 1391 } 1392 1393 // make another commit on the server 1394 const commit2 = try commitServer(&server_repo, io, allocator, .{ .files = &.{.{ .path = "extra.txt", .content = "extra content" }} }, .{ .message = "add extra file" }); 1395 1396 // fetch with ref-in-want (git uses want-ref in protocol v2 when fetching named refs) 1397 { 1398 var process = try std.process.spawn(io, .{ 1399 .argv = if (is_ssh) 1400 &.{ "git", "-c", ssh_config_arg, "fetch", "--quiet", "origin", "master" } 1401 else 1402 &.{ "git", "fetch", "--quiet", "origin", "master" }, 1403 .cwd = .{ .path = client_path }, 1404 .stdin = .ignore, 1405 .stdout = .ignore, 1406 .stderr = .inherit, 1407 }); 1408 defer process.kill(io); 1409 const term = try process.wait(io); 1410 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1411 } 1412 1413 // make sure fetch with want-ref was successful 1414 { 1415 const oid_remote_master = (try client_repo.readRef(io, .{ .kind = .{ .remote = "origin" }, .name = "master" })).?; 1416 try std.testing.expectEqualStrings(&commit2, &oid_remote_master); 1417 } 1418 } else { 1419 const refspecs = &.{ 1420 "+refs/heads/master:refs/heads/master", 1421 }; 1422 1423 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 1424 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1425 1426 try client_repo.fetch( 1427 io, 1428 allocator, 1429 "origin", 1430 .{ .refspecs = refspecs, .wire = .{ .ssh = .{ 1431 .command = ssh_cmd_maybe, 1432 } } }, 1433 ); 1434 1435 // update the working dir 1436 try client_repo.restore(io, allocator, "."); 1437 } 1438 1439 // make sure fetch was successful 1440 { 1441 const oid_master = (try client_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1442 try std.testing.expectEqualStrings(&commit1, &oid_master); 1443 } 1444 } 1445 1446 fn testPushLarge( 1447 comptime repo_kind: rp.RepoKind, 1448 comptime transport_def: net.TransportDefinition, 1449 comptime shell_out_to_git: bool, 1450 comptime hash_kind: hash.HashKind, 1451 comptime port: u16, 1452 io: std.Io, 1453 allocator: std.mem.Allocator, 1454 ) !void { 1455 // create the temp dir 1456 var temp = std.testing.tmpDir(.{}); 1457 defer temp.cleanup(); 1458 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 1459 defer allocator.free(temp_path); 1460 1461 // init server 1462 var server_process = try runServer(io, allocator, temp_path, port); 1463 defer _ = server_process.kill(io); 1464 1465 // register the repo under admin and locate its on-disk directory 1466 const server_path = (try repoOnDiskPath(io, allocator, temp_path, "testrepo", true)).?; 1467 defer allocator.free(server_path); 1468 1469 var server_repo = try rp.Repo(.xit, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .bare = true, .path = server_path }); 1470 defer server_repo.deinit(io, allocator); 1471 1472 // add config 1473 try server_repo.addConfig(io, allocator, .{ .name = "http.receivepack", .value = "true" }); 1474 1475 // export server repo 1476 { 1477 const export_file = try server_repo.core.repo_dir.createFile(io, "git-daemon-export-ok", .{}); 1478 defer export_file.close(io); 1479 } 1480 1481 const client_path = try std.fs.path.join(allocator, &.{ temp_path, "client" }); 1482 defer allocator.free(client_path); 1483 1484 var client_repo = try rp.Repo(repo_kind, .{ .hash = hash_kind, .is_test = true }).init(io, allocator, .{ .path = client_path }); 1485 defer client_repo.deinit(io, allocator); 1486 1487 var client_dir = try temp.dir.openDir(io, "client", .{}); 1488 defer client_dir.close(io); 1489 1490 { 1491 const hello_txt = try client_repo.core.work_dir.createFile(io, "hello.txt", .{ .truncate = true }); 1492 defer hello_txt.close(io); 1493 try hello_txt.writeStreamingAll(io, "hello, world!"); 1494 try client_repo.add(io, allocator, &.{"hello.txt"}); 1495 } 1496 1497 // copy files from current repo into client dir 1498 const cwd = std.Io.Dir.cwd(); 1499 for (&[_][]const u8{"src"}) |dir_name| { 1500 var src_repo_dir = try cwd.openDir(io, dir_name, .{ .iterate = true }); 1501 defer src_repo_dir.close(io); 1502 1503 var dest_repo_dir = try client_dir.createDirPathOpen(io, dir_name, .{}); 1504 defer dest_repo_dir.close(io); 1505 1506 try copyDir(io, src_repo_dir, dest_repo_dir); 1507 1508 try client_repo.add(io, allocator, &.{dir_name}); 1509 } 1510 1511 _ = try client_repo.commit(io, allocator, .{ .message = "let there be light" }); 1512 1513 // change the files so git will send them as delta objects 1514 for (&[_][]const u8{"src"}) |dir_name| { 1515 var dest_repo_dir = try client_dir.createDirPathOpen(io, dir_name, .{ .open_options = .{ .iterate = true } }); 1516 defer dest_repo_dir.close(io); 1517 1518 { 1519 var iter = dest_repo_dir.iterate(); 1520 while (try iter.next(io)) |entry| { 1521 switch (entry.kind) { 1522 .file => { 1523 const file = try dest_repo_dir.openFile(io, entry.name, .{ .mode = .read_write }); 1524 defer file.close(io); 1525 var writer = file.writer(io, &.{}); 1526 try writer.interface.writeAll("EDIT"); 1527 }, 1528 else => {}, 1529 } 1530 } 1531 } 1532 1533 try client_repo.add(io, allocator, &.{dir_name}); 1534 } 1535 1536 const commit2 = try client_repo.commit(io, allocator, .{ .message = "more stuff" }); 1537 1538 // add remote 1539 { 1540 const remote_url = try remoteUrl(transport_def, allocator, "testrepo", port); 1541 defer allocator.free(remote_url); 1542 1543 try client_repo.addRemote(io, allocator, .{ .name = "origin", .value = remote_url }); 1544 try client_repo.addConfig(io, allocator, .{ .name = "branch.master.remote", .value = "origin" }); 1545 } 1546 1547 const is_ssh = switch (transport_def) { 1548 .file => false, 1549 .wire => |wire_kind| .ssh == wire_kind, 1550 }; 1551 1552 if (shell_out_to_git) { 1553 const ssh_cmd = (try sshCommand(true, allocator, temp_path, port)).?; 1554 defer allocator.free(ssh_cmd); 1555 const ssh_config_arg = try std.fmt.allocPrint(allocator, "core.sshCommand={s}", .{ssh_cmd}); 1556 defer allocator.free(ssh_config_arg); 1557 1558 // shell out to git so it will send delta objects 1559 var process = try std.process.spawn(io, .{ 1560 .argv = if (is_ssh) 1561 &.{ "git", "-c", ssh_config_arg, "push", "--quiet", "origin", "master" } 1562 else 1563 &.{ "git", "push", "--quiet", "origin", "master" }, 1564 .cwd = .{ .path = client_path }, 1565 .stdin = .ignore, 1566 .stdout = .ignore, 1567 .stderr = .inherit, 1568 }); 1569 defer process.kill(io); 1570 const term = try process.wait(io); 1571 if (term != .exited or term.exited != 0) return error.GitCommandFailed; 1572 } else { 1573 const ssh_cmd_maybe = try sshCommand(is_ssh, allocator, temp_path, port); 1574 defer if (ssh_cmd_maybe) |ssh_cmd| allocator.free(ssh_cmd); 1575 1576 try client_repo.push( 1577 io, 1578 allocator, 1579 "origin", 1580 "master", 1581 false, 1582 .{ .wire = .{ .ssh = .{ 1583 .command = ssh_cmd_maybe, 1584 } } }, 1585 ); 1586 } 1587 1588 // make sure push was successful 1589 { 1590 const oid_master = (try server_repo.readRef(io, .{ .kind = .head, .name = "master" })).?; 1591 try std.testing.expectEqualStrings(&commit2, &oid_master); 1592 1593 try std.testing.expectError(error.FileNotFound, server_repo.core.work_dir.openFile(io, "hello.txt", .{})); 1594 } 1595 } 1596 1597 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 1598 var iter = src_dir.iterate(); 1599 while (try iter.next(io)) |entry| { 1600 switch (entry.kind) { 1601 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 1602 .directory => { 1603 try dest_dir.createDirPath(io, entry.name); 1604 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1605 defer dest_entry_dir.close(io); 1606 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1607 defer src_entry_dir.close(io); 1608 try copyDir(io, src_entry_dir, dest_entry_dir); 1609 }, 1610 else => {}, 1611 } 1612 } 1613 } 1614 1615 // the dev SSH public key matching the private key written by runServer 1616 const admin_ssh_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKeIs8mJqigBZ5y84J4COgnAJJ5bHPKy+lM2SliMXbYm radar@roark"; 1617 1618 // create the admin event repo with a user holding the dev SSH public key 1619 fn setupAdmin(io: std.Io, allocator: std.mem.Allocator, data_path: []const u8) !void { 1620 const admin_repo_path = try std.fs.path.join(allocator, &.{ data_path, "admin" }); 1621 defer allocator.free(admin_repo_path); 1622 1623 var repo = try rp.Repo(.xit, evt.admin_repo_opts).init(io, allocator, .{ .bare = true, .path = admin_repo_path }); 1624 defer repo.deinit(io, allocator); 1625 1626 try repo.addConfig(io, allocator, .{ .name = "user.name", .value = "haxy" }); 1627 try repo.addConfig(io, allocator, .{ .name = "user.email", .value = "admin@haxy" }); 1628 1629 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 1630 const user_id = evt.EventWithId.randomId(prng.random()); 1631 1632 var password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 1633 const password_hash = try evt.User.hashPassword("password", &password_hash_buf, io); 1634 1635 try evt.consume(.server, .admin, .xit, evt.admin_repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{.{ 1636 .id = std.fmt.bytesToHex(user_id, .lower), 1637 .author = .{ .name = "admin", .email = "admin@example.test" }, 1638 .event = .{ .user = .{ 1639 .name = "admin", 1640 .email = "admin@example.test", 1641 .password_hash = password_hash, 1642 .ssh_keys = admin_ssh_key, 1643 } }, 1644 }}); 1645 } 1646 1647 // resolve admin/<repo_name> to its on-disk directory under <data_dir>/repos via 1648 // the event store 1649 fn repoOnDiskPath( 1650 io: std.Io, 1651 allocator: std.mem.Allocator, 1652 data_path: []const u8, 1653 repo_name: []const u8, 1654 create: bool, 1655 ) !?[]u8 { 1656 const admin_repo_path = try std.fs.path.join(allocator, &.{ data_path, "admin" }); 1657 defer allocator.free(admin_repo_path); 1658 1659 const event_id_hex = (try evt.resolveOrCreateRepo( 1660 io, 1661 allocator, 1662 admin_repo_path, 1663 "admin", 1664 repo_name, 1665 if (create) .{ .read_access = .public } else null, 1666 )) orelse return null; 1667 return try std.fs.path.join(allocator, &.{ data_path, "repos", &event_id_hex }); 1668 } 1669 1670 // build the remote URL addressing admin/<repo_name> over the given transport 1671 fn remoteUrl( 1672 comptime transport_def: net.TransportDefinition, 1673 allocator: std.mem.Allocator, 1674 repo_name: []const u8, 1675 port: u16, 1676 ) ![]u8 { 1677 return switch (transport_def) { 1678 .file => unreachable, 1679 .wire => |wire_kind| switch (wire_kind) { 1680 .http => try std.fmt.allocPrint(allocator, "http://localhost:{}/admin/{s}", .{ port, repo_name }), 1681 .raw => try std.fmt.allocPrint(allocator, "git://localhost:{}/admin/{s}", .{ port, repo_name }), 1682 .ssh => try std.fmt.allocPrint(allocator, "git@localhost:admin/{s}", .{repo_name}), 1683 }, 1684 }; 1685 } 1686 1687 // the ssh command the haxy git client should invoke, or null when not using 1688 // ssh. haxy generates a fresh host key on every run, so accept the unknown host 1689 // and skip writing to known_hosts. 1690 fn sshCommand( 1691 is_ssh: bool, 1692 allocator: std.mem.Allocator, 1693 temp_path: []const u8, 1694 port: u16, 1695 ) !?[]u8 { 1696 if (!is_ssh) return null; 1697 1698 const priv_key_path = try std.fs.path.join(allocator, &.{ temp_path, "key" }); 1699 defer allocator.free(priv_key_path); 1700 1701 if (builtin.os.tag == .windows) std.mem.replaceScalar(u8, priv_key_path, '\\', '/'); 1702 const null_path = if (builtin.os.tag == .windows) "NUL" else "/dev/null"; 1703 return try std.fmt.allocPrint(allocator, "ssh -p {} -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile={s} -o LogLevel=ERROR -o IdentitiesOnly=yes -i \"{s}\"", .{ port + 1, null_path, priv_key_path }); 1704 } 1705 1706 const ServerFiles = union(enum) { 1707 files: []const struct { path: []const u8, content: []const u8 }, 1708 dirs: []const []const u8, 1709 }; 1710 1711 /// commit files without writing the server's index or worktree. 1712 fn commitServer(repo: anytype, io: std.Io, allocator: std.mem.Allocator, files: ServerFiles, metadata: xit.object.CommitMetadata(repo.self_repo_opts.hash)) ![xit.hash.hexLen(repo.self_repo_opts.hash)]u8 { 1713 const R = @TypeOf(repo.*); 1714 const opts = repo.self_repo_opts; 1715 const Index = xit.index.Index(.xit, opts); 1716 var result: [xit.hash.hexLen(opts.hash)]u8 = undefined; 1717 const Ctx = struct { 1718 core: *R.Core, 1719 io: std.Io, 1720 allocator: std.mem.Allocator, 1721 files: ServerFiles, 1722 metadata: xit.object.CommitMetadata(opts.hash), 1723 result: *[xit.hash.hexLen(opts.hash)]u8, 1724 1725 fn addFile(ctx: @This(), state: R.State(.read_write), index: *Index, path: []const u8, content: []const u8) !void { 1726 var reader = std.Io.Reader.fixed(content); 1727 var oid: [xit.hash.byteLen(opts.hash)]u8 = undefined; 1728 try xit.object.writeObject(.xit, opts, state, ctx.io, ctx.allocator, &reader, .{ .kind = .blob, .size = content.len }, &oid); 1729 const path_parts = try xit.fs.splitPath(ctx.allocator, path); 1730 defer ctx.allocator.free(path_parts); 1731 try index.addTreeEntryFile(&.{ .oid = oid, .mode = @bitCast(@as(u32, 0o100644)) }, path_parts, content.len, 0); 1732 } 1733 1734 fn addDir(ctx: @This(), state: R.State(.read_write), index: *Index, path: []const u8) anyerror!void { 1735 var dir = try std.Io.Dir.cwd().openDir(ctx.io, path, .{ .iterate = true }); 1736 defer dir.close(ctx.io); 1737 var entries = dir.iterate(); 1738 while (try entries.next(ctx.io)) |entry| { 1739 const child = try std.fs.path.join(ctx.allocator, &.{ path, entry.name }); 1740 defer ctx.allocator.free(child); 1741 switch (entry.kind) { 1742 .directory => try ctx.addDir(state, index, child), 1743 .file => { 1744 const content = try dir.readFileAlloc(ctx.io, entry.name, ctx.allocator, .unlimited); 1745 defer ctx.allocator.free(content); 1746 try ctx.addFile(state, index, child, content); 1747 }, 1748 else => {}, 1749 } 1750 } 1751 } 1752 1753 pub fn run(ctx: @This(), cursor: *R.DB.Cursor(.read_write)) !void { 1754 var moment = try R.DB.HashMap(.read_write).init(cursor.*); 1755 const state = R.State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 1756 const head_oid = try rf.readHeadRecurMaybe(.xit, opts, state.readOnly(), ctx.io); 1757 var index = if (head_oid) |oid| try Index.initFromCommit(state.readOnly(), ctx.io, ctx.allocator, &oid) else try Index.init(state.readOnly(), ctx.io, ctx.allocator); 1758 defer index.deinit(); 1759 switch (ctx.files) { 1760 .files => |updates| for (updates) |file| try ctx.addFile(state, &index, file.path, file.content), 1761 .dirs => |dirs| for (dirs) |dir| try ctx.addDir(state, &index, dir), 1762 } 1763 var tree = try xit.object.Tree.initFromIndex(.xit, opts, state, ctx.io, ctx.allocator, &index); 1764 defer tree.deinit(); 1765 var head_buffer: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; 1766 const head = try rf.readHead(.xit, opts, state.readOnly(), ctx.io, &head_buffer) orelse return error.HeadNotFound; 1767 ctx.result.* = try xit.object.writeCommit(.xit, opts, state, ctx.io, ctx.allocator, ctx.metadata, &tree, head.ref); 1768 try xit.undo.write(opts, state, std.Io.Timestamp.now(ctx.io, .real).toSeconds(), .{ .commit = try .init(ctx.metadata.message, ctx.result.*, opts.max_read_size) }); 1769 } 1770 }; 1771 try repo.core.db_file.lock(io, .exclusive); 1772 defer repo.core.db_file.unlock(io); 1773 const history = try R.DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); 1774 try history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ 1775 .core = &repo.core, 1776 .io = io, 1777 .allocator = allocator, 1778 .files = files, 1779 .metadata = metadata, 1780 .result = &result, 1781 }); 1782 return result; 1783 } 1784