just playing with tangled
at gvimdiff 1161 lines 42 kB view raw
1// Copyright 2022 The Jujutsu Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15use std::path; 16 17use indoc::formatdoc; 18use test_case::test_case; 19use testutils::git; 20 21use crate::common::to_toml_value; 22use crate::common::CommandOutput; 23use crate::common::TestEnvironment; 24use crate::common::TestWorkDir; 25 26fn set_up_non_empty_git_repo(git_repo: &gix::Repository) { 27 set_up_git_repo_with_file(git_repo, "file"); 28} 29 30fn set_up_git_repo_with_file(git_repo: &gix::Repository, filename: &str) { 31 git::add_commit( 32 git_repo, 33 "refs/heads/main", 34 filename, 35 b"content", 36 "message", 37 &[], 38 ); 39 git::set_symbolic_reference(git_repo, "HEAD", "refs/heads/main"); 40} 41 42#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))] 43#[test_case(true; "spawn a git subprocess for remote calls")] 44fn test_git_clone(subprocess: bool) { 45 let test_env = TestEnvironment::default(); 46 let root_dir = test_env.work_dir(""); 47 test_env.add_config("git.auto-local-bookmark = true"); 48 if !subprocess { 49 test_env.add_config("git.subprocess = false"); 50 } 51 let git_repo_path = test_env.env_root().join("source"); 52 let git_repo = git::init(git_repo_path); 53 54 // Clone an empty repo 55 let output = root_dir.run_jj(["git", "clone", "source", "empty"]); 56 insta::allow_duplicates! { 57 insta::assert_snapshot!(output, @r#" 58 ------- stderr ------- 59 Fetching into new repo in "$TEST_ENV/empty" 60 Nothing changed. 61 [EOF] 62 "#); 63 } 64 65 set_up_non_empty_git_repo(&git_repo); 66 67 // Clone with relative source path 68 let output = root_dir.run_jj(["git", "clone", "source", "clone"]); 69 insta::allow_duplicates! { 70 insta::assert_snapshot!(output, @r#" 71 ------- stderr ------- 72 Fetching into new repo in "$TEST_ENV/clone" 73 bookmark: main@origin [new] tracked 74 Setting the revset alias `trunk()` to `main@origin` 75 Working copy (@) now at: uuqppmxq f78d2645 (empty) (no description set) 76 Parent commit (@-) : qomsplrm ebeb70d8 main | message 77 Added 1 files, modified 0 files, removed 0 files 78 [EOF] 79 "#); 80 } 81 let clone_dir = test_env.work_dir("clone"); 82 assert!(clone_dir.root().join("file").exists()); 83 84 // Subsequent fetch should just work even if the source path was relative 85 let output = clone_dir.run_jj(["git", "fetch"]); 86 insta::allow_duplicates! { 87 insta::assert_snapshot!(output, @r" 88 ------- stderr ------- 89 Nothing changed. 90 [EOF] 91 "); 92 } 93 94 // Failed clone should clean up the destination directory 95 root_dir.create_dir("bad"); 96 let output = root_dir.run_jj(["git", "clone", "bad", "failed"]); 97 // git2's internal error is slightly different 98 if subprocess { 99 insta::assert_snapshot!(output, @r#" 100 ------- stderr ------- 101 Fetching into new repo in "$TEST_ENV/failed" 102 Error: Could not find repository at '$TEST_ENV/bad' 103 [EOF] 104 [exit status: 1] 105 "#); 106 } else { 107 insta::assert_snapshot!(output, @r#" 108 ------- stderr ------- 109 Fetching into new repo in "$TEST_ENV/failed" 110 Error: could not find repository at '$TEST_ENV/bad'; class=Repository (6) 111 [EOF] 112 [exit status: 1] 113 "#); 114 } 115 assert!(!test_env.env_root().join("failed").exists()); 116 117 // Failed clone shouldn't remove the existing destination directory 118 let failed_dir = root_dir.create_dir("failed"); 119 let output = root_dir.run_jj(["git", "clone", "bad", "failed"]); 120 // git2's internal error is slightly different 121 if subprocess { 122 insta::assert_snapshot!(output, @r#" 123 ------- stderr ------- 124 Fetching into new repo in "$TEST_ENV/failed" 125 Error: Could not find repository at '$TEST_ENV/bad' 126 [EOF] 127 [exit status: 1] 128 "#); 129 } else { 130 insta::assert_snapshot!(output, @r#" 131 ------- stderr ------- 132 Fetching into new repo in "$TEST_ENV/failed" 133 Error: could not find repository at '$TEST_ENV/bad'; class=Repository (6) 134 [EOF] 135 [exit status: 1] 136 "#); 137 } 138 assert!(failed_dir.root().exists()); 139 assert!(!failed_dir.root().join(".jj").exists()); 140 141 // Failed clone (if attempted) shouldn't remove the existing workspace 142 let output = root_dir.run_jj(["git", "clone", "bad", "clone"]); 143 insta::allow_duplicates! { 144 insta::assert_snapshot!(output, @r" 145 ------- stderr ------- 146 Error: Destination path exists and is not an empty directory 147 [EOF] 148 [exit status: 1] 149 "); 150 } 151 assert!(clone_dir.root().join(".jj").exists()); 152 153 // Try cloning into an existing workspace 154 let output = root_dir.run_jj(["git", "clone", "source", "clone"]); 155 insta::allow_duplicates! { 156 insta::assert_snapshot!(output, @r" 157 ------- stderr ------- 158 Error: Destination path exists and is not an empty directory 159 [EOF] 160 [exit status: 1] 161 "); 162 } 163 164 // Try cloning into an existing file 165 root_dir.write_file("file", "contents"); 166 let output = root_dir.run_jj(["git", "clone", "source", "file"]); 167 insta::allow_duplicates! { 168 insta::assert_snapshot!(output, @r" 169 ------- stderr ------- 170 Error: Destination path exists and is not an empty directory 171 [EOF] 172 [exit status: 1] 173 "); 174 } 175 176 // Try cloning into non-empty, non-workspace directory 177 clone_dir.remove_dir_all(".jj"); 178 let output = root_dir.run_jj(["git", "clone", "source", "clone"]); 179 insta::allow_duplicates! { 180 insta::assert_snapshot!(output, @r" 181 ------- stderr ------- 182 Error: Destination path exists and is not an empty directory 183 [EOF] 184 [exit status: 1] 185 "); 186 } 187 188 // Clone into a nested path 189 let output = root_dir.run_jj(["git", "clone", "source", "nested/path/to/repo"]); 190 insta::allow_duplicates! { 191 insta::assert_snapshot!(output, @r#" 192 ------- stderr ------- 193 Fetching into new repo in "$TEST_ENV/nested/path/to/repo" 194 bookmark: main@origin [new] tracked 195 Setting the revset alias `trunk()` to `main@origin` 196 Working copy (@) now at: uuzqqzqu cf5d593e (empty) (no description set) 197 Parent commit (@-) : qomsplrm ebeb70d8 main | message 198 Added 1 files, modified 0 files, removed 0 files 199 [EOF] 200 "#); 201 } 202} 203 204#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))] 205#[test_case(true; "spawn a git subprocess for remote calls")] 206fn test_git_clone_bad_source(subprocess: bool) { 207 let test_env = TestEnvironment::default(); 208 let root_dir = test_env.work_dir(""); 209 if !subprocess { 210 test_env.add_config("git.subprocess = false"); 211 } 212 213 let output = root_dir.run_jj(["git", "clone", "", "dest"]); 214 insta::allow_duplicates! { 215 insta::assert_snapshot!(output, @r#" 216 ------- stderr ------- 217 Error: local path "" does not specify a path to a repository 218 [EOF] 219 [exit status: 2] 220 "#); 221 } 222 223 // Invalid port number 224 let output = root_dir.run_jj(["git", "clone", "https://example.net:bad-port/bar", "dest"]); 225 insta::allow_duplicates! { 226 insta::assert_snapshot!(output, @r#" 227 ------- stderr ------- 228 Error: URL "https://example.net:bad-port/bar" can not be parsed as valid URL 229 Caused by: invalid port number 230 [EOF] 231 [exit status: 2] 232 "#); 233 } 234} 235 236#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))] 237#[test_case(true; "spawn a git subprocess for remote calls")] 238fn test_git_clone_colocate(subprocess: bool) { 239 let test_env = TestEnvironment::default(); 240 let root_dir = test_env.work_dir(""); 241 test_env.add_config("git.auto-local-bookmark = true"); 242 if !subprocess { 243 test_env.add_config("git.subprocess = false"); 244 } 245 let git_repo_path = test_env.env_root().join("source"); 246 let git_repo = git::init(git_repo_path); 247 248 // Clone an empty repo 249 let output = root_dir.run_jj(["git", "clone", "source", "empty", "--colocate"]); 250 insta::allow_duplicates! { 251 insta::assert_snapshot!(output, @r#" 252 ------- stderr ------- 253 Fetching into new repo in "$TEST_ENV/empty" 254 Nothing changed. 255 [EOF] 256 "#); 257 } 258 259 // git_target path should be relative to the store 260 let empty_dir = test_env.work_dir("empty"); 261 let git_target_file_contents = 262 String::from_utf8(empty_dir.read_file(".jj/repo/store/git_target").into()).unwrap(); 263 insta::allow_duplicates! { 264 insta::assert_snapshot!( 265 git_target_file_contents.replace(path::MAIN_SEPARATOR, "/"), 266 @"../../../.git"); 267 } 268 269 set_up_non_empty_git_repo(&git_repo); 270 271 // Clone with relative source path 272 let output = root_dir.run_jj(["git", "clone", "source", "clone", "--colocate"]); 273 insta::allow_duplicates! { 274 insta::assert_snapshot!(output, @r#" 275 ------- stderr ------- 276 Fetching into new repo in "$TEST_ENV/clone" 277 bookmark: main@origin [new] tracked 278 Setting the revset alias `trunk()` to `main@origin` 279 Working copy (@) now at: uuqppmxq f78d2645 (empty) (no description set) 280 Parent commit (@-) : qomsplrm ebeb70d8 main | message 281 Added 1 files, modified 0 files, removed 0 files 282 [EOF] 283 "#); 284 } 285 let clone_dir = test_env.work_dir("clone"); 286 assert!(clone_dir.root().join("file").exists()); 287 assert!(clone_dir.root().join(".git").exists()); 288 289 eprintln!( 290 "{:?}", 291 git_repo.head().expect("Repo head should be set").name() 292 ); 293 294 let jj_git_repo = git::open(clone_dir.root()); 295 assert_eq!( 296 jj_git_repo 297 .head_id() 298 .expect("Clone Repo HEAD should be set.") 299 .detach(), 300 git_repo 301 .head_id() 302 .expect("Repo HEAD should be set.") 303 .detach(), 304 ); 305 // ".jj" directory should be ignored at Git side. 306 let git_statuses = git::status(&jj_git_repo); 307 insta::allow_duplicates! { 308 insta::assert_debug_snapshot!(git_statuses, @r#" 309 [ 310 GitStatus { 311 path: ".jj/.gitignore", 312 status: Worktree( 313 Ignored, 314 ), 315 }, 316 GitStatus { 317 path: ".jj/repo", 318 status: Worktree( 319 Ignored, 320 ), 321 }, 322 GitStatus { 323 path: ".jj/working_copy", 324 status: Worktree( 325 Ignored, 326 ), 327 }, 328 ] 329 "#); 330 } 331 332 // The old default bookmark "master" shouldn't exist. 333 insta::allow_duplicates! { 334 insta::assert_snapshot!(get_bookmark_output(&clone_dir), @r" 335 main: qomsplrm ebeb70d8 message 336 @git: qomsplrm ebeb70d8 message 337 @origin: qomsplrm ebeb70d8 message 338 [EOF] 339 "); 340 } 341 342 // Subsequent fetch should just work even if the source path was relative 343 let output = clone_dir.run_jj(["git", "fetch"]); 344 insta::allow_duplicates! { 345 insta::assert_snapshot!(output, @r" 346 ------- stderr ------- 347 Nothing changed. 348 [EOF] 349 "); 350 } 351 352 // Failed clone should clean up the destination directory 353 root_dir.create_dir("bad"); 354 let output = root_dir.run_jj(["git", "clone", "--colocate", "bad", "failed"]); 355 // git2's internal error is slightly different 356 if subprocess { 357 insta::assert_snapshot!(output, @r#" 358 ------- stderr ------- 359 Fetching into new repo in "$TEST_ENV/failed" 360 Error: Could not find repository at '$TEST_ENV/bad' 361 [EOF] 362 [exit status: 1] 363 "#); 364 } else { 365 insta::assert_snapshot!(output, @r#" 366 ------- stderr ------- 367 Fetching into new repo in "$TEST_ENV/failed" 368 Error: could not find repository at '$TEST_ENV/bad'; class=Repository (6) 369 [EOF] 370 [exit status: 1] 371 "#); 372 } 373 assert!(!test_env.env_root().join("failed").exists()); 374 375 // Failed clone shouldn't remove the existing destination directory 376 let failed_dir = root_dir.create_dir("failed"); 377 let output = root_dir.run_jj(["git", "clone", "--colocate", "bad", "failed"]); 378 // git2's internal error is slightly different 379 if subprocess { 380 insta::assert_snapshot!(output, @r#" 381 ------- stderr ------- 382 Fetching into new repo in "$TEST_ENV/failed" 383 Error: Could not find repository at '$TEST_ENV/bad' 384 [EOF] 385 [exit status: 1] 386 "#); 387 } else { 388 insta::assert_snapshot!(output, @r#" 389 ------- stderr ------- 390 Fetching into new repo in "$TEST_ENV/failed" 391 Error: could not find repository at '$TEST_ENV/bad'; class=Repository (6) 392 [EOF] 393 [exit status: 1] 394 "#); 395 } 396 assert!(failed_dir.root().exists()); 397 assert!(!failed_dir.root().join(".git").exists()); 398 assert!(!failed_dir.root().join(".jj").exists()); 399 400 // Failed clone (if attempted) shouldn't remove the existing workspace 401 let output = root_dir.run_jj(["git", "clone", "--colocate", "bad", "clone"]); 402 insta::allow_duplicates! { 403 insta::assert_snapshot!(output, @r" 404 ------- stderr ------- 405 Error: Destination path exists and is not an empty directory 406 [EOF] 407 [exit status: 1] 408 "); 409 } 410 assert!(clone_dir.root().join(".git").exists()); 411 assert!(clone_dir.root().join(".jj").exists()); 412 413 // Try cloning into an existing workspace 414 let output = root_dir.run_jj(["git", "clone", "source", "clone", "--colocate"]); 415 insta::allow_duplicates! { 416 insta::assert_snapshot!(output, @r" 417 ------- stderr ------- 418 Error: Destination path exists and is not an empty directory 419 [EOF] 420 [exit status: 1] 421 "); 422 } 423 424 // Try cloning into an existing file 425 root_dir.write_file("file", "contents"); 426 let output = root_dir.run_jj(["git", "clone", "source", "file", "--colocate"]); 427 insta::allow_duplicates! { 428 insta::assert_snapshot!(output, @r" 429 ------- stderr ------- 430 Error: Destination path exists and is not an empty directory 431 [EOF] 432 [exit status: 1] 433 "); 434 } 435 436 // Try cloning into non-empty, non-workspace directory 437 clone_dir.remove_dir_all(".jj"); 438 let output = root_dir.run_jj(["git", "clone", "source", "clone", "--colocate"]); 439 insta::allow_duplicates! { 440 insta::assert_snapshot!(output, @r" 441 ------- stderr ------- 442 Error: Destination path exists and is not an empty directory 443 [EOF] 444 [exit status: 1] 445 "); 446 } 447 448 // Clone into a nested path 449 let output = root_dir.run_jj([ 450 "git", 451 "clone", 452 "source", 453 "nested/path/to/repo", 454 "--colocate", 455 ]); 456 insta::allow_duplicates! { 457 insta::assert_snapshot!(output, @r#" 458 ------- stderr ------- 459 Fetching into new repo in "$TEST_ENV/nested/path/to/repo" 460 bookmark: main@origin [new] tracked 461 Setting the revset alias `trunk()` to `main@origin` 462 Working copy (@) now at: vzqnnsmr 589d0921 (empty) (no description set) 463 Parent commit (@-) : qomsplrm ebeb70d8 main | message 464 Added 1 files, modified 0 files, removed 0 files 465 [EOF] 466 "#); 467 } 468} 469 470#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))] 471#[test_case(true; "spawn a git subprocess for remote calls")] 472fn test_git_clone_remote_default_bookmark(subprocess: bool) { 473 let test_env = TestEnvironment::default(); 474 let root_dir = test_env.work_dir(""); 475 if !subprocess { 476 test_env.add_config("git.subprocess = false"); 477 } 478 let git_repo_path = test_env.env_root().join("source"); 479 let git_repo = git::init(git_repo_path.clone()); 480 481 set_up_non_empty_git_repo(&git_repo); 482 483 // Create non-default bookmark in remote 484 let head_id = git_repo.head_id().unwrap().detach(); 485 git_repo 486 .reference( 487 "refs/heads/feature1", 488 head_id, 489 gix::refs::transaction::PreviousValue::MustNotExist, 490 "", 491 ) 492 .unwrap(); 493 494 // All fetched bookmarks will be imported if auto-local-bookmark is on 495 test_env.add_config("git.auto-local-bookmark = true"); 496 let output = root_dir.run_jj(["git", "clone", "source", "clone1"]); 497 insta::allow_duplicates! { 498 insta::assert_snapshot!(output, @r#" 499 ------- stderr ------- 500 Fetching into new repo in "$TEST_ENV/clone1" 501 bookmark: feature1@origin [new] tracked 502 bookmark: main@origin [new] tracked 503 Setting the revset alias `trunk()` to `main@origin` 504 Working copy (@) now at: sqpuoqvx 2ca1c979 (empty) (no description set) 505 Parent commit (@-) : qomsplrm ebeb70d8 feature1 main | message 506 Added 1 files, modified 0 files, removed 0 files 507 [EOF] 508 "#); 509 } 510 let clone_dir1 = test_env.work_dir("clone1"); 511 insta::allow_duplicates! { 512 insta::assert_snapshot!(get_bookmark_output(&clone_dir1), @r" 513 feature1: qomsplrm ebeb70d8 message 514 @origin: qomsplrm ebeb70d8 message 515 main: qomsplrm ebeb70d8 message 516 @origin: qomsplrm ebeb70d8 message 517 [EOF] 518 "); 519 } 520 521 // "trunk()" alias should be set to default bookmark "main" 522 let output = clone_dir1.run_jj(["config", "list", "--repo", "revset-aliases.'trunk()'"]); 523 insta::allow_duplicates! { 524 insta::assert_snapshot!(output, @r#" 525 revset-aliases.'trunk()' = "main@origin" 526 [EOF] 527 "#); 528 } 529 530 // Only the default bookmark will be imported if auto-local-bookmark is off 531 test_env.add_config("git.auto-local-bookmark = false"); 532 let output = root_dir.run_jj(["git", "clone", "source", "clone2"]); 533 insta::allow_duplicates! { 534 insta::assert_snapshot!(output, @r#" 535 ------- stderr ------- 536 Fetching into new repo in "$TEST_ENV/clone2" 537 bookmark: feature1@origin [new] untracked 538 bookmark: main@origin [new] untracked 539 Setting the revset alias `trunk()` to `main@origin` 540 Working copy (@) now at: rzvqmyuk 018092c2 (empty) (no description set) 541 Parent commit (@-) : qomsplrm ebeb70d8 feature1@origin main | message 542 Added 1 files, modified 0 files, removed 0 files 543 [EOF] 544 "#); 545 } 546 let clone_dir2 = test_env.work_dir("clone2"); 547 insta::allow_duplicates! { 548 insta::assert_snapshot!(get_bookmark_output(&clone_dir2), @r" 549 feature1@origin: qomsplrm ebeb70d8 message 550 main: qomsplrm ebeb70d8 message 551 @origin: qomsplrm ebeb70d8 message 552 [EOF] 553 "); 554 } 555 556 // Change the default bookmark in remote 557 git::set_symbolic_reference(&git_repo, "HEAD", "refs/heads/feature1"); 558 let output = root_dir.run_jj(["git", "clone", "source", "clone3"]); 559 insta::allow_duplicates! { 560 insta::assert_snapshot!(output, @r#" 561 ------- stderr ------- 562 Fetching into new repo in "$TEST_ENV/clone3" 563 bookmark: feature1@origin [new] untracked 564 bookmark: main@origin [new] untracked 565 Setting the revset alias `trunk()` to `feature1@origin` 566 Working copy (@) now at: nppvrztz 5fd587f4 (empty) (no description set) 567 Parent commit (@-) : qomsplrm ebeb70d8 feature1 main@origin | message 568 Added 1 files, modified 0 files, removed 0 files 569 [EOF] 570 "#); 571 } 572 let clone_dir3 = test_env.work_dir("clone3"); 573 insta::allow_duplicates! { 574 insta::assert_snapshot!(get_bookmark_output(&clone_dir3), @r" 575 feature1: qomsplrm ebeb70d8 message 576 @origin: qomsplrm ebeb70d8 message 577 main@origin: qomsplrm ebeb70d8 message 578 [EOF] 579 "); 580 } 581 582 // "trunk()" alias should be set to new default bookmark "feature1" 583 let output = clone_dir3.run_jj(["config", "list", "--repo", "revset-aliases.'trunk()'"]); 584 insta::allow_duplicates! { 585 insta::assert_snapshot!(output, @r#" 586 revset-aliases.'trunk()' = "feature1@origin" 587 [EOF] 588 "#); 589 } 590} 591 592// A branch with a strange name should get quoted in the config. Windows doesn't 593// like the strange name, so we don't run the test there. 594#[cfg(unix)] 595#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))] 596#[test_case(true; "spawn a git subprocess for remote calls")] 597fn test_git_clone_remote_default_bookmark_with_escape(subprocess: bool) { 598 let test_env = TestEnvironment::default(); 599 let root_dir = test_env.work_dir(""); 600 if !subprocess { 601 test_env.add_config("git.subprocess = false"); 602 } 603 let git_repo_path = test_env.env_root().join("source"); 604 let git_repo = git::init(git_repo_path); 605 // Create a branch to something that needs to be escaped 606 let commit_id = git::add_commit( 607 &git_repo, 608 "refs/heads/\"", 609 "file", 610 b"content", 611 "message", 612 &[], 613 ) 614 .commit_id; 615 git::set_head_to_id(&git_repo, commit_id); 616 617 let output = root_dir.run_jj(["git", "clone", "source", "clone"]); 618 insta::allow_duplicates! { 619 insta::assert_snapshot!(output, @r#" 620 ------- stderr ------- 621 Fetching into new repo in "$TEST_ENV/clone" 622 bookmark: "\""@origin [new] untracked 623 Setting the revset alias `trunk()` to `"\""@origin` 624 Working copy (@) now at: sqpuoqvx 2ca1c979 (empty) (no description set) 625 Parent commit (@-) : qomsplrm ebeb70d8 " | message 626 Added 1 files, modified 0 files, removed 0 files 627 [EOF] 628 "#); 629 } 630 631 // "trunk()" alias should be escaped and quoted 632 let clone_dir = test_env.work_dir("clone"); 633 let output = clone_dir.run_jj(["config", "list", "--repo", "revset-aliases.'trunk()'"]); 634 insta::allow_duplicates! { 635 insta::assert_snapshot!(output, @r#" 636 revset-aliases.'trunk()' = '"\""@origin' 637 [EOF] 638 "#); 639 } 640} 641 642#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))] 643#[test_case(true; "spawn a git subprocess for remote calls")] 644fn test_git_clone_ignore_working_copy(subprocess: bool) { 645 let test_env = TestEnvironment::default(); 646 let root_dir = test_env.work_dir(""); 647 if !subprocess { 648 test_env.add_config("git.subprocess = false"); 649 } 650 let git_repo_path = test_env.env_root().join("source"); 651 let git_repo = git::init(git_repo_path); 652 set_up_non_empty_git_repo(&git_repo); 653 654 // Should not update working-copy files 655 let output = root_dir.run_jj(["git", "clone", "--ignore-working-copy", "source", "clone"]); 656 insta::allow_duplicates! { 657 insta::assert_snapshot!(output, @r#" 658 ------- stderr ------- 659 Fetching into new repo in "$TEST_ENV/clone" 660 bookmark: main@origin [new] untracked 661 Setting the revset alias `trunk()` to `main@origin` 662 [EOF] 663 "#); 664 } 665 let clone_dir = test_env.work_dir("clone"); 666 667 let output = clone_dir.run_jj(["status", "--ignore-working-copy"]); 668 insta::allow_duplicates! { 669 insta::assert_snapshot!(output, @r" 670 The working copy has no changes. 671 Working copy (@) : sqpuoqvx 2ca1c979 (empty) (no description set) 672 Parent commit (@-): qomsplrm ebeb70d8 main | message 673 [EOF] 674 "); 675 } 676 677 // TODO: Correct, but might be better to check out the root commit? 678 let output = clone_dir.run_jj(["status"]); 679 insta::allow_duplicates! { 680 insta::assert_snapshot!(output, @r" 681 ------- stderr ------- 682 Error: The working copy is stale (not updated since operation eac759b9ab75). 683 Hint: Run `jj workspace update-stale` to update it. 684 See https://jj-vcs.github.io/jj/latest/working-copy/#stale-working-copy for more information. 685 [EOF] 686 [exit status: 1] 687 "); 688 } 689} 690 691#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))] 692#[test_case(true; "spawn a git subprocess for remote calls")] 693fn test_git_clone_at_operation(subprocess: bool) { 694 let test_env = TestEnvironment::default(); 695 let root_dir = test_env.work_dir(""); 696 if !subprocess { 697 test_env.add_config("git.subprocess = false"); 698 } 699 let git_repo_path = test_env.env_root().join("source"); 700 let git_repo = git::init(git_repo_path); 701 set_up_non_empty_git_repo(&git_repo); 702 703 let output = root_dir.run_jj(["git", "clone", "--at-op=@-", "source", "clone"]); 704 insta::allow_duplicates! { 705 insta::assert_snapshot!(output, @r" 706 ------- stderr ------- 707 Error: --at-op is not respected 708 [EOF] 709 [exit status: 2] 710 "); 711 } 712} 713 714#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))] 715#[test_case(true; "spawn a git subprocess for remote calls")] 716fn test_git_clone_with_remote_name(subprocess: bool) { 717 let test_env = TestEnvironment::default(); 718 let root_dir = test_env.work_dir(""); 719 test_env.add_config("git.auto-local-bookmark = true"); 720 if !subprocess { 721 test_env.add_config("git.subprocess = false"); 722 } 723 let git_repo_path = test_env.env_root().join("source"); 724 let git_repo = git::init(git_repo_path); 725 set_up_non_empty_git_repo(&git_repo); 726 727 // Clone with relative source path and a non-default remote name 728 let output = root_dir.run_jj(["git", "clone", "source", "clone", "--remote", "upstream"]); 729 insta::allow_duplicates! { 730 insta::assert_snapshot!(output, @r#" 731 ------- stderr ------- 732 Fetching into new repo in "$TEST_ENV/clone" 733 bookmark: main@upstream [new] tracked 734 Setting the revset alias `trunk()` to `main@upstream` 735 Working copy (@) now at: sqpuoqvx 2ca1c979 (empty) (no description set) 736 Parent commit (@-) : qomsplrm ebeb70d8 main | message 737 Added 1 files, modified 0 files, removed 0 files 738 [EOF] 739 "#); 740 } 741} 742 743#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))] 744#[test_case(true; "spawn a git subprocess for remote calls")] 745fn test_git_clone_with_remote_named_git(subprocess: bool) { 746 let test_env = TestEnvironment::default(); 747 let root_dir = test_env.work_dir(""); 748 if !subprocess { 749 test_env.add_config("git.subprocess = false"); 750 } 751 let git_repo_path = test_env.env_root().join("source"); 752 git::init(git_repo_path); 753 754 let output = root_dir.run_jj(["git", "clone", "--remote=git", "source", "dest"]); 755 insta::allow_duplicates! { 756 insta::assert_snapshot!(output, @r" 757 ------- stderr ------- 758 Error: Git remote named 'git' is reserved for local Git repository 759 [EOF] 760 [exit status: 1] 761 "); 762 } 763} 764 765#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))] 766#[test_case(true; "spawn a git subprocess for remote calls")] 767fn test_git_clone_with_remote_with_slashes(subprocess: bool) { 768 let test_env = TestEnvironment::default(); 769 let root_dir = test_env.work_dir(""); 770 if !subprocess { 771 test_env.add_config("git.subprocess = false"); 772 } 773 let git_repo_path = test_env.env_root().join("source"); 774 git::init(git_repo_path); 775 776 let output = root_dir.run_jj(["git", "clone", "--remote=slash/origin", "source", "dest"]); 777 insta::allow_duplicates! { 778 insta::assert_snapshot!(output, @r" 779 ------- stderr ------- 780 Error: Git remotes with slashes are incompatible with jj: slash/origin 781 [EOF] 782 [exit status: 1] 783 "); 784 } 785} 786 787#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))] 788#[test_case(true; "spawn a git subprocess for remote calls")] 789fn test_git_clone_trunk_deleted(subprocess: bool) { 790 let test_env = TestEnvironment::default(); 791 let root_dir = test_env.work_dir(""); 792 if !subprocess { 793 test_env.add_config("git.subprocess = false"); 794 } 795 let git_repo_path = test_env.env_root().join("source"); 796 let git_repo = git::init(git_repo_path); 797 set_up_non_empty_git_repo(&git_repo); 798 let clone_dir = test_env.work_dir("clone"); 799 800 let output = root_dir.run_jj(["git", "clone", "source", "clone"]); 801 insta::allow_duplicates! { 802 insta::assert_snapshot!(output, @r#" 803 ------- stderr ------- 804 Fetching into new repo in "$TEST_ENV/clone" 805 bookmark: main@origin [new] untracked 806 Setting the revset alias `trunk()` to `main@origin` 807 Working copy (@) now at: sqpuoqvx 2ca1c979 (empty) (no description set) 808 Parent commit (@-) : qomsplrm ebeb70d8 main | message 809 Added 1 files, modified 0 files, removed 0 files 810 [EOF] 811 "#); 812 } 813 814 let output = clone_dir.run_jj(["bookmark", "forget", "--include-remotes", "main"]); 815 insta::allow_duplicates! { 816 insta::assert_snapshot!(output, @r" 817 ------- stderr ------- 818 Forgot 1 local bookmarks. 819 Forgot 1 remote bookmarks. 820 Warning: Failed to resolve `revset-aliases.trunk()`: Revision `main@origin` doesn't exist 821 Hint: Use `jj config edit --repo` to adjust the `trunk()` alias. 822 [EOF] 823 "); 824 } 825 826 let output = clone_dir.run_jj(["log"]); 827 insta::allow_duplicates! { 828 insta::assert_snapshot!(output, @r" 829 @ sqpuoqvx test.user@example.com 2001-02-03 08:05:07 2ca1c979 830 │ (empty) (no description set) 831 ○ qomsplrm someone@example.org 1970-01-01 11:00:00 ebeb70d8 832 │ message 833 ◆ zzzzzzzz root() 00000000 834 [EOF] 835 ------- stderr ------- 836 Warning: Failed to resolve `revset-aliases.trunk()`: Revision `main@origin` doesn't exist 837 Hint: Use `jj config edit --repo` to adjust the `trunk()` alias. 838 [EOF] 839 "); 840 } 841} 842 843#[test] 844fn test_git_clone_conditional_config() { 845 let test_env = TestEnvironment::default(); 846 let root_dir = test_env.work_dir(""); 847 let source_repo_path = test_env.env_root().join("source"); 848 let old_workspace_dir = test_env.work_dir("old"); 849 let new_workspace_dir = test_env.work_dir("new"); 850 let source_git_repo = git::init(source_repo_path); 851 set_up_non_empty_git_repo(&source_git_repo); 852 853 let run_jj = |work_dir: &TestWorkDir, args: &[&str]| { 854 work_dir.run_jj_with(|cmd| { 855 cmd.args(args) 856 .env_remove("JJ_EMAIL") 857 .env_remove("JJ_OP_HOSTNAME") 858 .env_remove("JJ_OP_USERNAME") 859 }) 860 }; 861 let log_template = r#"separate(' ', author.email(), description.first_line()) ++ "\n""#; 862 let op_log_template = r#"separate(' ', user, description.first_line()) ++ "\n""#; 863 864 // Override user.email and operation.username conditionally 865 test_env.add_config(formatdoc! {" 866 user.email = 'base@example.org' 867 operation.hostname = 'base' 868 operation.username = 'base' 869 [[--scope]] 870 --when.repositories = [{new_workspace_root}] 871 user.email = 'new-repo@example.org' 872 operation.username = 'new-repo' 873 ", 874 new_workspace_root = to_toml_value(new_workspace_dir.root().to_str().unwrap()), 875 }); 876 877 // Override operation.hostname by repo config, which should be loaded into 878 // the command settings, but shouldn't be copied to the new repo. 879 run_jj(&root_dir, &["git", "init", "old"]).success(); 880 run_jj( 881 &old_workspace_dir, 882 &["config", "set", "--repo", "operation.hostname", "old-repo"], 883 ) 884 .success(); 885 run_jj(&old_workspace_dir, &["new"]).success(); 886 let output = run_jj(&old_workspace_dir, &["op", "log", "-T", op_log_template]); 887 insta::assert_snapshot!(output, @r" 888 @ base@old-repo new empty commit 889 ○ base@base add workspace 'default' 890 ○ @ 891 [EOF] 892 "); 893 894 // Clone repo at the old workspace directory. 895 let output = run_jj(&old_workspace_dir, &["git", "clone", "../source", "../new"]); 896 insta::assert_snapshot!(output, @r#" 897 ------- stderr ------- 898 Fetching into new repo in "$TEST_ENV/new" 899 bookmark: main@origin [new] untracked 900 Setting the revset alias `trunk()` to `main@origin` 901 Working copy (@) now at: zxsnswpr 9ffb42e2 (empty) (no description set) 902 Parent commit (@-) : qomsplrm ebeb70d8 main | message 903 Added 1 files, modified 0 files, removed 0 files 904 [EOF] 905 "#); 906 run_jj(&new_workspace_dir, &["new"]).success(); 907 let output = run_jj(&new_workspace_dir, &["log", "-T", log_template]); 908 insta::assert_snapshot!(output, @r" 909 @ new-repo@example.org 910 ○ new-repo@example.org 911 ◆ someone@example.org message 912 913 ~ 914 [EOF] 915 "); 916 let output = run_jj(&new_workspace_dir, &["op", "log", "-T", op_log_template]); 917 insta::assert_snapshot!(output, @r" 918 @ new-repo@base new empty commit 919 ○ new-repo@base check out git remote's default branch 920 ○ new-repo@base fetch from git remote into empty repo 921 ○ new-repo@base add workspace 'default' 922 ○ @ 923 [EOF] 924 "); 925} 926 927#[cfg(feature = "git2")] 928#[test] 929fn test_git_clone_with_depth_git2() { 930 let test_env = TestEnvironment::default(); 931 let root_dir = test_env.work_dir(""); 932 test_env.add_config("git.auto-local-bookmark = true"); 933 test_env.add_config("git.subprocess = false"); 934 let git_repo_path = test_env.env_root().join("source"); 935 let git_repo = git::init(git_repo_path); 936 set_up_non_empty_git_repo(&git_repo); 937 938 // git does support shallow clones on the local transport, so it will work 939 // (we cannot replicate git2's erroneous behaviour wrt git) 940 // local transport does not support shallow clones so we just test that the 941 // depth arg is passed on here 942 let output = root_dir.run_jj(["git", "clone", "--depth", "1", "source", "clone"]); 943 insta::assert_snapshot!(output, @r#" 944 ------- stderr ------- 945 Fetching into new repo in "$TEST_ENV/clone" 946 Error: shallow fetch is not supported by the local transport; class=Net (12) 947 [EOF] 948 [exit status: 1] 949 "#); 950} 951 952#[test] 953fn test_git_clone_with_depth_subprocess() { 954 let test_env = TestEnvironment::default(); 955 let root_dir = test_env.work_dir(""); 956 test_env.add_config("git.auto-local-bookmark = true"); 957 let clone_dir = test_env.work_dir("clone"); 958 let git_repo_path = test_env.env_root().join("source"); 959 let git_repo = git::init(git_repo_path); 960 set_up_non_empty_git_repo(&git_repo); 961 962 // git does support shallow clones on the local transport, so it will work 963 // (we cannot replicate git2's erroneous behaviour wrt git) 964 let output = root_dir.run_jj(["git", "clone", "--depth", "1", "source", "clone"]); 965 insta::assert_snapshot!(output, @r#" 966 ------- stderr ------- 967 Fetching into new repo in "$TEST_ENV/clone" 968 bookmark: main@origin [new] tracked 969 Setting the revset alias `trunk()` to `main@origin` 970 Working copy (@) now at: sqpuoqvx 2ca1c979 (empty) (no description set) 971 Parent commit (@-) : qomsplrm ebeb70d8 main | message 972 Added 1 files, modified 0 files, removed 0 files 973 [EOF] 974 "#); 975 976 let output = clone_dir.run_jj(["log"]); 977 insta::assert_snapshot!(output, @r" 978 @ sqpuoqvx test.user@example.com 2001-02-03 08:05:07 2ca1c979 979 │ (empty) (no description set) 980 ◆ qomsplrm someone@example.org 1970-01-01 11:00:00 main ebeb70d8 981 │ message 982 ~ 983 [EOF] 984 "); 985} 986 987#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))] 988#[test_case(true; "spawn a git subprocess for remote calls")] 989fn test_git_clone_invalid_immutable_heads(subprocess: bool) { 990 let test_env = TestEnvironment::default(); 991 let root_dir = test_env.work_dir(""); 992 if !subprocess { 993 test_env.add_config("git.subprocess = false"); 994 } 995 let git_repo_path = test_env.env_root().join("source"); 996 let git_repo = git::init(git_repo_path); 997 set_up_non_empty_git_repo(&git_repo); 998 999 test_env.add_config("revset-aliases.'immutable_heads()' = 'unknown'"); 1000 // Suppress lengthy warnings in commit summary template 1001 test_env.add_config("revsets.short-prefixes = ''"); 1002 1003 // The error shouldn't be counted as an immutable working-copy commit. It 1004 // should be reported. 1005 let output = root_dir.run_jj(["git", "clone", "source", "clone"]); 1006 insta::allow_duplicates! { 1007 insta::assert_snapshot!(output, @r#" 1008 ------- stderr ------- 1009 Fetching into new repo in "$TEST_ENV/clone" 1010 bookmark: main@origin [new] untracked 1011 Config error: Invalid `revset-aliases.immutable_heads()` 1012 Caused by: Revision `unknown` doesn't exist 1013 For help, see https://jj-vcs.github.io/jj/latest/config/ or use `jj help -k config`. 1014 [EOF] 1015 [exit status: 1] 1016 "#); 1017 } 1018} 1019 1020#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))] 1021#[test_case(true; "spawn a git subprocess for remote calls")] 1022fn test_git_clone_malformed(subprocess: bool) { 1023 let test_env = TestEnvironment::default(); 1024 let root_dir = test_env.work_dir(""); 1025 if !subprocess { 1026 test_env.add_config("git.subprocess = false"); 1027 } 1028 let git_repo_path = test_env.env_root().join("source"); 1029 let git_repo = git::init(git_repo_path); 1030 let clone_dir = test_env.work_dir("clone"); 1031 // we can insert ".jj" entry to create a malformed clone 1032 set_up_git_repo_with_file(&git_repo, ".jj"); 1033 1034 // TODO: Perhaps, this should be a user error, not an internal error. 1035 let output = root_dir.run_jj(["git", "clone", "source", "clone"]); 1036 insta::allow_duplicates! { 1037 insta::assert_snapshot!(output, @r#" 1038 ------- stderr ------- 1039 Fetching into new repo in "$TEST_ENV/clone" 1040 bookmark: main@origin [new] untracked 1041 Setting the revset alias `trunk()` to `main@origin` 1042 Internal error: Failed to check out commit 0a09cb41583450703459a2310d63da61456364ce 1043 Caused by: Reserved path component .jj in $TEST_ENV/clone/.jj 1044 [EOF] 1045 [exit status: 255] 1046 "#); 1047 } 1048 1049 // The cloned workspace isn't usable. 1050 let output = clone_dir.run_jj(["status"]); 1051 insta::allow_duplicates! { 1052 insta::assert_snapshot!(output, @r" 1053 ------- stderr ------- 1054 Error: The working copy is stale (not updated since operation 57e024eb3edf). 1055 Hint: Run `jj workspace update-stale` to update it. 1056 See https://jj-vcs.github.io/jj/latest/working-copy/#stale-working-copy for more information. 1057 [EOF] 1058 [exit status: 1] 1059 "); 1060 } 1061 1062 // The error can be somehow recovered. 1063 // TODO: add an update-stale flag to reset the working-copy? 1064 let output = clone_dir.run_jj(["workspace", "update-stale"]); 1065 insta::allow_duplicates! { 1066 insta::assert_snapshot!(output, @r" 1067 ------- stderr ------- 1068 Internal error: Failed to check out commit 0a09cb41583450703459a2310d63da61456364ce 1069 Caused by: Reserved path component .jj in $TEST_ENV/clone/.jj 1070 [EOF] 1071 [exit status: 255] 1072 "); 1073 } 1074 let output = clone_dir.run_jj(["new", "root()", "--ignore-working-copy"]); 1075 insta::allow_duplicates! { 1076 insta::assert_snapshot!(output, @""); 1077 } 1078 let output = clone_dir.run_jj(["status"]); 1079 insta::allow_duplicates! { 1080 insta::assert_snapshot!(output, @r" 1081 The working copy has no changes. 1082 Working copy (@) : zsuskuln f652c321 (empty) (no description set) 1083 Parent commit (@-): zzzzzzzz 00000000 (empty) (no description set) 1084 [EOF] 1085 "); 1086 } 1087} 1088 1089#[test] 1090fn test_git_clone_no_git_executable() { 1091 let test_env = TestEnvironment::default(); 1092 let root_dir = test_env.work_dir(""); 1093 test_env.add_config("git.executable-path = 'jj-test-missing-program'"); 1094 let git_repo_path = test_env.env_root().join("source"); 1095 let git_repo = git::init(git_repo_path); 1096 set_up_non_empty_git_repo(&git_repo); 1097 1098 let output = root_dir.run_jj(["git", "clone", "source", "clone"]); 1099 insta::assert_snapshot!(output.strip_stderr_last_line(), @r#" 1100 ------- stderr ------- 1101 Fetching into new repo in "$TEST_ENV/clone" 1102 Error: Could not execute the git process, found in the OS path 'jj-test-missing-program' 1103 [EOF] 1104 [exit status: 1] 1105 "#); 1106} 1107 1108#[test] 1109fn test_git_clone_no_git_executable_with_path() { 1110 let test_env = TestEnvironment::default(); 1111 let root_dir = test_env.work_dir(""); 1112 let invalid_git_executable_path = test_env.env_root().join("invalid").join("path"); 1113 test_env.add_config(format!( 1114 "git.executable-path = {}", 1115 to_toml_value(invalid_git_executable_path.to_str().unwrap()) 1116 )); 1117 let git_repo_path = test_env.env_root().join("source"); 1118 let git_repo = git::init(git_repo_path); 1119 set_up_non_empty_git_repo(&git_repo); 1120 1121 let output = root_dir.run_jj(["git", "clone", "source", "clone"]); 1122 insta::assert_snapshot!(output.strip_stderr_last_line(), @r#" 1123 ------- stderr ------- 1124 Fetching into new repo in "$TEST_ENV/clone" 1125 Error: Could not execute git process at specified path '$TEST_ENV/invalid/path' 1126 [EOF] 1127 [exit status: 1] 1128 "#); 1129} 1130 1131#[must_use] 1132fn get_bookmark_output(work_dir: &TestWorkDir) -> CommandOutput { 1133 work_dir.run_jj(["bookmark", "list", "--all-remotes"]) 1134} 1135 1136// TODO: Remove with the `git.subprocess` setting. 1137#[cfg(not(feature = "git2"))] 1138#[test] 1139fn test_git_clone_git2_warning() { 1140 let test_env = TestEnvironment::default(); 1141 let root_dir = test_env.work_dir(""); 1142 test_env.add_config("git.subprocess = false"); 1143 test_env.add_config("git.auto-local-bookmark = true"); 1144 let git_repo_path = test_env.env_root().join("source"); 1145 let git_repo = git::init(git_repo_path); 1146 1147 set_up_non_empty_git_repo(&git_repo); 1148 1149 let output = root_dir.run_jj(["git", "clone", "source", "clone"]); 1150 insta::assert_snapshot!(output, @r#" 1151 ------- stderr ------- 1152 Warning: Deprecated config: jj was compiled without `git.subprocess = false` support 1153 Fetching into new repo in "$TEST_ENV/clone" 1154 bookmark: main@origin [new] tracked 1155 Setting the revset alias `trunk()` to `main@origin` 1156 Working copy (@) now at: sqpuoqvx 2ca1c979 (empty) (no description set) 1157 Parent commit (@-) : qomsplrm ebeb70d8 main | message 1158 Added 1 files, modified 0 files, removed 0 files 1159 [EOF] 1160 "#); 1161}