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