// Copyright 2022 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::path::Path; use git2::Oid; use crate::common::TestEnvironment; #[test] fn test_git_colocated() { let test_env = TestEnvironment::default(); let workspace_root = test_env.env_root().join("repo"); let git_repo = git2::Repository::init(&workspace_root).unwrap(); // Create an initial commit in Git std::fs::write(workspace_root.join("file"), "contents").unwrap(); git_repo .index() .unwrap() .add_path(Path::new("file")) .unwrap(); let tree1_oid = git_repo.index().unwrap().write_tree().unwrap(); let tree1 = git_repo.find_tree(tree1_oid).unwrap(); let signature = git2::Signature::new( "Someone", "someone@example.com", &git2::Time::new(1234567890, 60), ) .unwrap(); git_repo .commit( Some("refs/heads/master"), &signature, &signature, "initial", &tree1, &[], ) .unwrap(); insta::assert_snapshot!( git_repo.head().unwrap().peel_to_commit().unwrap().id().to_string(), @"e61b6729ff4292870702f2f72b2a60165679ef37" ); // Import the repo test_env.jj_cmd_ok(&workspace_root, &["init", "--git-repo", "."]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###" @ 3e9369cd54227eb88455e1834dbc08aad6a16ac4 ◉ e61b6729ff4292870702f2f72b2a60165679ef37 master HEAD@git initial ◉ 0000000000000000000000000000000000000000 "###); insta::assert_snapshot!( git_repo.head().unwrap().peel_to_commit().unwrap().id().to_string(), @"e61b6729ff4292870702f2f72b2a60165679ef37" ); // Modify the working copy. The working-copy commit should changed, but the Git // HEAD commit should not std::fs::write(workspace_root.join("file"), "modified").unwrap(); insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###" @ b26951a9c6f5c270e4d039880208952fd5faae5e ◉ e61b6729ff4292870702f2f72b2a60165679ef37 master HEAD@git initial ◉ 0000000000000000000000000000000000000000 "###); insta::assert_snapshot!( git_repo.head().unwrap().peel_to_commit().unwrap().id().to_string(), @"e61b6729ff4292870702f2f72b2a60165679ef37" ); // Create a new change from jj and check that it's reflected in Git test_env.jj_cmd_ok(&workspace_root, &["new"]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###" @ 9dbb23ff2ff5e66c43880f1042369d704f7a321e ◉ b26951a9c6f5c270e4d039880208952fd5faae5e HEAD@git ◉ e61b6729ff4292870702f2f72b2a60165679ef37 master initial ◉ 0000000000000000000000000000000000000000 "###); insta::assert_snapshot!( git_repo.head().unwrap().target().unwrap().to_string(), @"b26951a9c6f5c270e4d039880208952fd5faae5e" ); } #[test] fn test_git_colocated_unborn_branch() { let test_env = TestEnvironment::default(); let workspace_root = test_env.env_root().join("repo"); let git_repo = git2::Repository::init(&workspace_root).unwrap(); let add_file_to_index = |name: &str, data: &str| { std::fs::write(workspace_root.join(name), data).unwrap(); let mut index = git_repo.index().unwrap(); index.add_path(Path::new(name)).unwrap(); index.write().unwrap(); }; let checkout_index = || { let mut index = git_repo.index().unwrap(); index.read(true).unwrap(); // discard in-memory cache git_repo.checkout_index(Some(&mut index), None).unwrap(); }; // Initially, HEAD isn't set. test_env.jj_cmd_ok(&workspace_root, &["init", "--git-repo", "."]); assert!(git_repo.head().is_err()); assert_eq!( git_repo.find_reference("HEAD").unwrap().symbolic_target(), Some("refs/heads/master") ); insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###" @ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 ◉ 0000000000000000000000000000000000000000 "###); // Stage some change, and check out root. This shouldn't clobber the HEAD. add_file_to_index("file0", ""); let (stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["new", "root()"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" Working copy now at: kkmpptxz fcdbbd73 (empty) (no description set) Parent commit : zzzzzzzz 00000000 (empty) (no description set) Added 0 files, modified 0 files, removed 1 files "###); assert!(git_repo.head().is_err()); assert_eq!( git_repo.find_reference("HEAD").unwrap().symbolic_target(), Some("refs/heads/master") ); insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###" @ fcdbbd731496cae17161cd6be9b6cf1f759655a8 │ ◉ 1de814dbef9641cc6c5c80d2689b80778edcce09 ├─╯ ◉ 0000000000000000000000000000000000000000 "###); // Staged change shouldn't persist. checkout_index(); insta::assert_snapshot!(test_env.jj_cmd_success(&workspace_root, &["status"]), @r###" The working copy is clean Working copy : kkmpptxz fcdbbd73 (empty) (no description set) Parent commit: zzzzzzzz 00000000 (empty) (no description set) "###); // Stage some change, and create new HEAD. This shouldn't move the default // branch. add_file_to_index("file1", ""); let (stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["new"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" Working copy now at: royxmykx 76c60bf0 (empty) (no description set) Parent commit : kkmpptxz f8d5bc77 (no description set) "###); assert!(git_repo.head().unwrap().symbolic_target().is_none()); insta::assert_snapshot!( git_repo.head().unwrap().peel_to_commit().unwrap().id().to_string(), @"f8d5bc772d1147351fd6e8cea52a4f935d3b31e7" ); insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###" @ 76c60bf0a66dcbe74d74d58c23848d96f9e86e84 ◉ f8d5bc772d1147351fd6e8cea52a4f935d3b31e7 HEAD@git │ ◉ 1de814dbef9641cc6c5c80d2689b80778edcce09 ├─╯ ◉ 0000000000000000000000000000000000000000 "###); // Staged change shouldn't persist. checkout_index(); insta::assert_snapshot!(test_env.jj_cmd_success(&workspace_root, &["status"]), @r###" The working copy is clean Working copy : royxmykx 76c60bf0 (empty) (no description set) Parent commit: kkmpptxz f8d5bc77 (no description set) "###); // Assign the default branch. The branch is no longer "unborn". test_env.jj_cmd_ok(&workspace_root, &["branch", "create", "-r@-", "master"]); // Stage some change, and check out root again. This should unset the HEAD. // https://github.com/martinvonz/jj/issues/1495 add_file_to_index("file2", ""); let (stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["new", "root()"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" Working copy now at: znkkpsqq 10dd328b (empty) (no description set) Parent commit : zzzzzzzz 00000000 (empty) (no description set) Added 0 files, modified 0 files, removed 2 files "###); assert!(git_repo.head().is_err()); insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###" @ 10dd328bb906e15890e55047740eab2812a3b2f7 │ ◉ 2c576a57d2e6e8494616629cfdbb8fe5e3fea73b │ ◉ f8d5bc772d1147351fd6e8cea52a4f935d3b31e7 master ├─╯ │ ◉ 1de814dbef9641cc6c5c80d2689b80778edcce09 ├─╯ ◉ 0000000000000000000000000000000000000000 "###); // Staged change shouldn't persist. checkout_index(); insta::assert_snapshot!(test_env.jj_cmd_success(&workspace_root, &["status"]), @r###" The working copy is clean Working copy : znkkpsqq 10dd328b (empty) (no description set) Parent commit: zzzzzzzz 00000000 (empty) (no description set) "###); // New snapshot and commit can be created after the HEAD got unset. std::fs::write(workspace_root.join("file3"), "").unwrap(); let (stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["new"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" Working copy now at: wqnwkozp cab23370 (empty) (no description set) Parent commit : znkkpsqq 8f5b2638 (no description set) "###); insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###" @ cab233704a5c0b21bde070943055f22142fb2043 ◉ 8f5b263819457712a2937428b9c58a2a84afbb1c HEAD@git │ ◉ 2c576a57d2e6e8494616629cfdbb8fe5e3fea73b │ ◉ f8d5bc772d1147351fd6e8cea52a4f935d3b31e7 master ├─╯ │ ◉ 1de814dbef9641cc6c5c80d2689b80778edcce09 ├─╯ ◉ 0000000000000000000000000000000000000000 "###); } #[test] fn test_git_colocated_export_branches_on_snapshot() { // Checks that we export branches that were changed only because the working // copy was snapshotted let test_env = TestEnvironment::default(); let workspace_root = test_env.env_root().join("repo"); let git_repo = git2::Repository::init(&workspace_root).unwrap(); test_env.jj_cmd_ok(&workspace_root, &["init", "--git-repo", "."]); // Create branch pointing to the initial commit std::fs::write(workspace_root.join("file"), "initial").unwrap(); test_env.jj_cmd_ok(&workspace_root, &["branch", "create", "foo"]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###" @ 438471f3fbf1004298d8fb01eeb13663a051a643 foo ◉ 0000000000000000000000000000000000000000 "###); // The branch gets updated when we modify the working copy, and it should get // exported to Git without requiring any other changes std::fs::write(workspace_root.join("file"), "modified").unwrap(); insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###" @ fab22d1acf5bb9c5aa48cb2c3dd2132072a359ca foo ◉ 0000000000000000000000000000000000000000 "###); insta::assert_snapshot!(git_repo .find_reference("refs/heads/foo") .unwrap() .target() .unwrap() .to_string(), @"fab22d1acf5bb9c5aa48cb2c3dd2132072a359ca"); } #[test] fn test_git_colocated_rebase_on_import() { let test_env = TestEnvironment::default(); let workspace_root = test_env.env_root().join("repo"); let git_repo = git2::Repository::init(&workspace_root).unwrap(); test_env.jj_cmd_ok(&workspace_root, &["init", "--git-repo", "."]); // Make some changes in jj and check that they're reflected in git std::fs::write(workspace_root.join("file"), "contents").unwrap(); test_env.jj_cmd_ok(&workspace_root, &["commit", "-m", "add a file"]); std::fs::write(workspace_root.join("file"), "modified").unwrap(); test_env.jj_cmd_ok(&workspace_root, &["branch", "create", "master"]); test_env.jj_cmd_ok(&workspace_root, &["commit", "-m", "modify a file"]); // TODO: We shouldn't need this command here to trigger an import of the // refs/heads/master we just exported test_env.jj_cmd_ok(&workspace_root, &["st"]); // Move `master` and HEAD backwards, which should result in commit2 getting // hidden, and a new working-copy commit at the new position. let commit2_oid = git_repo .find_branch("master", git2::BranchType::Local) .unwrap() .get() .target() .unwrap(); let commit2 = git_repo.find_commit(commit2_oid).unwrap(); let commit1 = commit2.parents().next().unwrap(); git_repo.branch("master", &commit1, true).unwrap(); git_repo.set_head("refs/heads/master").unwrap(); let (stdout, stderr) = get_log_output_with_stderr(&test_env, &workspace_root); insta::assert_snapshot!(stdout, @r###" @ 7f96185cfbe36341d0f9a86ebfaeab67a5922c7e ◉ 4bcbeaba9a4b309c5f45a8807fbf5499b9714315 master HEAD@git add a file ◉ 0000000000000000000000000000000000000000 "###); insta::assert_snapshot!(stderr, @r###" Reset the working copy parent to the new Git HEAD. Abandoned 1 commits that are no longer reachable. Done importing changes from the underlying Git repo. "###); } #[test] fn test_git_colocated_branches() { let test_env = TestEnvironment::default(); let workspace_root = test_env.env_root().join("repo"); let git_repo = git2::Repository::init(&workspace_root).unwrap(); test_env.jj_cmd_ok(&workspace_root, &["init", "--git-repo", "."]); test_env.jj_cmd_ok(&workspace_root, &["new", "-m", "foo"]); test_env.jj_cmd_ok(&workspace_root, &["new", "@-", "-m", "bar"]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###" @ 3560559274ab431feea00b7b7e0b9250ecce951f bar │ ◉ 1e6f0b403ed2ff9713b5d6b1dc601e4804250cda foo ├─╯ ◉ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 HEAD@git ◉ 0000000000000000000000000000000000000000 "###); // Create a branch in jj. It should be exported to Git even though it points to // the working- copy commit. test_env.jj_cmd_ok(&workspace_root, &["branch", "create", "master"]); insta::assert_snapshot!( git_repo.find_reference("refs/heads/master").unwrap().target().unwrap().to_string(), @"3560559274ab431feea00b7b7e0b9250ecce951f" ); insta::assert_snapshot!( git_repo.head().unwrap().target().unwrap().to_string(), @"230dd059e1b059aefc0da06a2e5a7dbf22362f22" ); // Update the branch in Git let target_id = test_env.jj_cmd_success( &workspace_root, &["log", "--no-graph", "-T=commit_id", "-r=description(foo)"], ); git_repo .reference( "refs/heads/master", Oid::from_str(&target_id).unwrap(), true, "test", ) .unwrap(); let (stdout, stderr) = get_log_output_with_stderr(&test_env, &workspace_root); insta::assert_snapshot!(stdout, @r###" @ 096dc80da67094fbaa6683e2a205dddffa31f9a8 │ ◉ 1e6f0b403ed2ff9713b5d6b1dc601e4804250cda master foo ├─╯ ◉ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 HEAD@git ◉ 0000000000000000000000000000000000000000 "###); insta::assert_snapshot!(stderr, @r###" Abandoned 1 commits that are no longer reachable. Working copy now at: yqosqzyt 096dc80d (empty) (no description set) Parent commit : qpvuntsm 230dd059 (empty) (no description set) Done importing changes from the underlying Git repo. "###); } #[test] fn test_git_colocated_branch_forget() { let test_env = TestEnvironment::default(); let workspace_root = test_env.env_root().join("repo"); let _git_repo = git2::Repository::init(&workspace_root).unwrap(); test_env.jj_cmd_ok(&workspace_root, &["init", "--git-repo", "."]); test_env.jj_cmd_ok(&workspace_root, &["new"]); test_env.jj_cmd_ok(&workspace_root, &["branch", "create", "foo"]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###" @ 65b6b74e08973b88d38404430f119c8c79465250 foo ◉ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 HEAD@git ◉ 0000000000000000000000000000000000000000 "###); let stdout = test_env.jj_cmd_success(&workspace_root, &["branch", "list", "--all-remotes"]); insta::assert_snapshot!(stdout, @r###" foo: rlvkpnrz 65b6b74e (empty) (no description set) @git: rlvkpnrz 65b6b74e (empty) (no description set) "###); let (stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["branch", "forget", "foo"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @""); // A forgotten branch is deleted in the git repo. For a detailed demo explaining // this, see `test_branch_forget_export` in `test_branch_command.rs`. let stdout = test_env.jj_cmd_success(&workspace_root, &["branch", "list", "--all-remotes"]); insta::assert_snapshot!(stdout, @""); } #[test] fn test_git_colocated_conflicting_git_refs() { let test_env = TestEnvironment::default(); let workspace_root = test_env.env_root().join("repo"); git2::Repository::init(&workspace_root).unwrap(); test_env.jj_cmd_ok(&workspace_root, &["init", "--git-repo", "."]); test_env.jj_cmd_ok(&workspace_root, &["branch", "create", "main"]); let (stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["branch", "create", "main/sub"]); insta::assert_snapshot!(stdout, @""); insta::with_settings!({filters => vec![(": The lock for resource.*", ": ...")]}, { insta::assert_snapshot!(stderr, @r###" Warning: Failed to export some branches: main/sub: Failed to set: A lock could not be obtained for reference "refs/heads/main/sub": ... Hint: Git doesn't allow a branch name that looks like a parent directory of another (e.g. `foo` and `foo/bar`). Try to rename the branches that failed to export or their "parent" branches. "###); }); } #[test] fn test_git_colocated_checkout_non_empty_working_copy() { let test_env = TestEnvironment::default(); let workspace_root = test_env.env_root().join("repo"); let git_repo = git2::Repository::init(&workspace_root).unwrap(); test_env.jj_cmd_ok(&workspace_root, &["init", "--git-repo", "."]); // Create an initial commit in Git // We use this to set HEAD to master std::fs::write(workspace_root.join("file"), "contents").unwrap(); git_repo .index() .unwrap() .add_path(Path::new("file")) .unwrap(); let tree1_oid = git_repo.index().unwrap().write_tree().unwrap(); let tree1 = git_repo.find_tree(tree1_oid).unwrap(); let signature = git2::Signature::new( "Someone", "someone@example.com", &git2::Time::new(1234567890, 60), ) .unwrap(); git_repo .commit( Some("refs/heads/master"), &signature, &signature, "initial", &tree1, &[], ) .unwrap(); insta::assert_snapshot!( git_repo.head().unwrap().peel_to_commit().unwrap().id().to_string(), @"e61b6729ff4292870702f2f72b2a60165679ef37" ); std::fs::write(workspace_root.join("two"), "y").unwrap(); test_env.jj_cmd_ok(&workspace_root, &["describe", "-m", "two"]); test_env.jj_cmd_ok(&workspace_root, &["new", "@-"]); let (_, stderr) = test_env.jj_cmd_ok(&workspace_root, &["describe", "-m", "new"]); insta::assert_snapshot!(stderr, @r###" Working copy now at: kkmpptxz 4c049607 (empty) new Parent commit : lnksqltp e61b6729 master | initial "###); let git_head = git_repo.find_reference("HEAD").unwrap(); let git_head_target = git_head.symbolic_target().unwrap(); assert_eq!(git_head_target, "refs/heads/master"); insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###" @ 4c04960765ca906d0cb25b15a946be4c0dd71b8e new │ ◉ 4ec6f6506bd1903410f15b80058a7f0d8f62deea two ├─╯ ◉ e61b6729ff4292870702f2f72b2a60165679ef37 master HEAD@git initial ◉ 0000000000000000000000000000000000000000 "###); } #[test] fn test_git_colocated_fetch_deleted_or_moved_branch() { let test_env = TestEnvironment::default(); test_env.add_config("git.auto-local-branch = true"); let origin_path = test_env.env_root().join("origin"); git2::Repository::init(&origin_path).unwrap(); test_env.jj_cmd_ok(&origin_path, &["init", "--git-repo=."]); test_env.jj_cmd_ok(&origin_path, &["describe", "-m=A"]); test_env.jj_cmd_ok(&origin_path, &["branch", "create", "A"]); test_env.jj_cmd_ok(&origin_path, &["new", "-m=B_to_delete"]); test_env.jj_cmd_ok(&origin_path, &["branch", "create", "B_to_delete"]); test_env.jj_cmd_ok(&origin_path, &["new", "-m=original C", "@-"]); test_env.jj_cmd_ok(&origin_path, &["branch", "create", "C_to_move"]); let clone_path = test_env.env_root().join("clone"); git2::Repository::clone(origin_path.to_str().unwrap(), &clone_path).unwrap(); test_env.jj_cmd_ok(&clone_path, &["init", "--git-repo=."]); test_env.jj_cmd_ok(&clone_path, &["new", "A"]); insta::assert_snapshot!(get_log_output(&test_env, &clone_path), @r###" @ 0335878796213c3a701f1c9c34dcae242bee4131 │ ◉ 8d4e006fd63547965fbc3a26556a9aa531076d32 C_to_move original C ├─╯ │ ◉ 929e298ae9edf969b405a304c75c10457c47d52c B_to_delete B_to_delete ├─╯ ◉ a86754f975f953fa25da4265764adc0c62e9ce6b A HEAD@git A ◉ 0000000000000000000000000000000000000000 "###); test_env.jj_cmd_ok(&origin_path, &["branch", "delete", "B_to_delete"]); // Move branch C sideways test_env.jj_cmd_ok(&origin_path, &["describe", "C_to_move", "-m", "moved C"]); let (stdout, stderr) = test_env.jj_cmd_ok(&clone_path, &["git", "fetch"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" branch: B_to_delete@origin [deleted] untracked branch: C_to_move@origin [updated] tracked Abandoned 2 commits that are no longer reachable. "###); // "original C" and "B_to_delete" are abandoned, as the corresponding branches // were deleted or moved on the remote (#864) insta::assert_snapshot!(get_log_output(&test_env, &clone_path), @r###" ◉ 04fd29df05638156b20044b3b6136b42abcb09ab C_to_move moved C │ @ 0335878796213c3a701f1c9c34dcae242bee4131 ├─╯ ◉ a86754f975f953fa25da4265764adc0c62e9ce6b A HEAD@git A ◉ 0000000000000000000000000000000000000000 "###); } #[test] fn test_git_colocated_rebase_dirty_working_copy() { let test_env = TestEnvironment::default(); let repo_path = test_env.env_root().join("repo"); let git_repo = git2::Repository::init(&repo_path).unwrap(); test_env.jj_cmd_ok(&repo_path, &["init", "--git-repo=."]); std::fs::write(repo_path.join("file"), "base").unwrap(); test_env.jj_cmd_ok(&repo_path, &["new"]); std::fs::write(repo_path.join("file"), "old").unwrap(); test_env.jj_cmd_ok(&repo_path, &["branch", "create", "feature"]); // Make the working-copy dirty, delete the checked out branch. std::fs::write(repo_path.join("file"), "new").unwrap(); git_repo .find_reference("refs/heads/feature") .unwrap() .delete() .unwrap(); // Because the working copy is dirty, the new working-copy commit will be // diverged. Therefore, the feature branch has change-delete conflict. let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["status"]); insta::assert_snapshot!(stdout, @r###" Working copy changes: M file Working copy : rlvkpnrz d6c5e664 feature?? | (no description set) Parent commit: qpvuntsm 5973d373 (no description set) These branches have conflicts: feature Use `jj branch list` to see details. Use `jj branch set -r ` to resolve. "###); insta::assert_snapshot!(stderr, @r###" Warning: Failed to export some branches: feature: Modified ref had been deleted in Git Done importing changes from the underlying Git repo. "###); insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" @ d6c5e66473426f5ed3a24ecce8ce8b44ff23cf81 feature?? ◉ 5973d3731aba9dd86c00b4a765fbc4cc13f1e14b HEAD@git ◉ 0000000000000000000000000000000000000000 "###); // The working-copy content shouldn't be lost. insta::assert_snapshot!( std::fs::read_to_string(repo_path.join("file")).unwrap(), @"new"); } #[test] fn test_git_colocated_external_checkout() { let test_env = TestEnvironment::default(); let repo_path = test_env.env_root().join("repo"); let git_repo = git2::Repository::init(&repo_path).unwrap(); test_env.jj_cmd_ok(&repo_path, &["init", "--git-repo=."]); test_env.jj_cmd_ok(&repo_path, &["ci", "-m=A"]); test_env.jj_cmd_ok(&repo_path, &["branch", "create", "-r@-", "master"]); test_env.jj_cmd_ok(&repo_path, &["new", "-m=B", "root()"]); test_env.jj_cmd_ok(&repo_path, &["new"]); // Checked out anonymous branch insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" @ f8a23336e41840ed1757ef323402a770427dc89a ◉ eccedddfa5152d99fc8ddd1081b375387a8a382a HEAD@git B │ ◉ a86754f975f953fa25da4265764adc0c62e9ce6b master A ├─╯ ◉ 0000000000000000000000000000000000000000 "###); // Check out another branch by external command git_repo .set_head_detached( git_repo .find_reference("refs/heads/master") .unwrap() .target() .unwrap(), ) .unwrap(); // The old working-copy commit gets abandoned, but the whole branch should not // be abandoned. (#1042) let (stdout, stderr) = get_log_output_with_stderr(&test_env, &repo_path); insta::assert_snapshot!(stdout, @r###" @ adadbd65a794e2294962b3c3da9aada09fe1b472 ◉ a86754f975f953fa25da4265764adc0c62e9ce6b master HEAD@git A │ ◉ eccedddfa5152d99fc8ddd1081b375387a8a382a B ├─╯ ◉ 0000000000000000000000000000000000000000 "###); insta::assert_snapshot!(stderr, @r###" Reset the working copy parent to the new Git HEAD. "###); } #[test] fn test_git_colocated_squash_undo() { let test_env = TestEnvironment::default(); let repo_path = test_env.env_root().join("repo"); git2::Repository::init(&repo_path).unwrap(); test_env.jj_cmd_ok(&repo_path, &["init", "--git-repo=."]); test_env.jj_cmd_ok(&repo_path, &["ci", "-m=A"]); // Test the setup insta::assert_snapshot!(get_log_output_divergence(&test_env, &repo_path), @r###" @ rlvkpnrzqnoo 8f71e3b6a3be ◉ qpvuntsmwlqt a86754f975f9 A HEAD@git ◉ zzzzzzzzzzzz 000000000000 "###); test_env.jj_cmd_ok(&repo_path, &["squash"]); insta::assert_snapshot!(get_log_output_divergence(&test_env, &repo_path), @r###" @ zsuskulnrvyr f0c12b0396d9 ◉ qpvuntsmwlqt 2f376ea1478c A HEAD@git ◉ zzzzzzzzzzzz 000000000000 "###); test_env.jj_cmd_ok(&repo_path, &["undo"]); // TODO: There should be no divergence here; 2f376ea1478c should be hidden // (#922) insta::assert_snapshot!(get_log_output_divergence(&test_env, &repo_path), @r###" @ rlvkpnrzqnoo 8f71e3b6a3be ◉ qpvuntsmwlqt a86754f975f9 A HEAD@git ◉ zzzzzzzzzzzz 000000000000 "###); } #[test] fn test_git_colocated_undo_head_move() { let test_env = TestEnvironment::default(); let repo_path = test_env.env_root().join("repo"); let git_repo = git2::Repository::init(&repo_path).unwrap(); test_env.jj_cmd_ok(&repo_path, &["init", "--git-repo=."]); // Create new HEAD test_env.jj_cmd_ok(&repo_path, &["new"]); insta::assert_snapshot!( git_repo.head().unwrap().target().unwrap().to_string(), @"230dd059e1b059aefc0da06a2e5a7dbf22362f22"); insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" @ 65b6b74e08973b88d38404430f119c8c79465250 ◉ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 HEAD@git ◉ 0000000000000000000000000000000000000000 "###); // HEAD should be unset test_env.jj_cmd_ok(&repo_path, &["undo"]); assert!(git_repo.head().is_err()); insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" @ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 ◉ 0000000000000000000000000000000000000000 "###); // Create commit on non-root commit test_env.jj_cmd_ok(&repo_path, &["new"]); test_env.jj_cmd_ok(&repo_path, &["new"]); insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" @ 69b19f73cf584f162f078fb0d91c55ca39d10bc7 ◉ eb08b363bb5ef8ee549314260488980d7bbe8f63 HEAD@git ◉ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 ◉ 0000000000000000000000000000000000000000 "###); insta::assert_snapshot!( git_repo.head().unwrap().target().unwrap().to_string(), @"eb08b363bb5ef8ee549314260488980d7bbe8f63"); // HEAD should be moved back let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["undo"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" Working copy now at: royxmykx eb08b363 (empty) (no description set) Parent commit : qpvuntsm 230dd059 (empty) (no description set) "###); insta::assert_snapshot!( git_repo.head().unwrap().target().unwrap().to_string(), @"230dd059e1b059aefc0da06a2e5a7dbf22362f22"); insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" @ eb08b363bb5ef8ee549314260488980d7bbe8f63 ◉ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 HEAD@git ◉ 0000000000000000000000000000000000000000 "###); } fn get_log_output_divergence(test_env: &TestEnvironment, repo_path: &Path) -> String { let template = r#" separate(" ", change_id.short(), commit_id.short(), description.first_line(), branches, git_head, if(divergent, "!divergence!"), ) "#; test_env.jj_cmd_success(repo_path, &["log", "-T", template]) } fn get_log_output(test_env: &TestEnvironment, workspace_root: &Path) -> String { let template = r#"separate(" ", commit_id, branches, git_head, description)"#; test_env.jj_cmd_success(workspace_root, &["log", "-T", template, "-r=all()"]) } fn get_log_output_with_stderr( test_env: &TestEnvironment, workspace_root: &Path, ) -> (String, String) { let template = r#"separate(" ", commit_id, branches, git_head, description)"#; test_env.jj_cmd_ok(workspace_root, &["log", "-T", template, "-r=all()"]) } #[test] fn test_git_colocated_unreachable_commits() { let test_env = TestEnvironment::default(); let workspace_root = test_env.env_root().join("repo"); let git_repo = git2::Repository::init(&workspace_root).unwrap(); // Create an initial commit in Git let empty_tree_oid = git_repo.treebuilder(None).unwrap().write().unwrap(); let tree1 = git_repo.find_tree(empty_tree_oid).unwrap(); let signature = git2::Signature::new( "Someone", "someone@example.com", &git2::Time::new(1234567890, 60), ) .unwrap(); let oid1 = git_repo .commit( Some("refs/heads/master"), &signature, &signature, "initial", &tree1, &[], ) .unwrap(); insta::assert_snapshot!( git_repo.head().unwrap().peel_to_commit().unwrap().id().to_string(), @"2ee37513d2b5e549f7478c671a780053614bff19" ); // Add a second commit in Git let tree2 = git_repo.find_tree(empty_tree_oid).unwrap(); let signature = git2::Signature::new( "Someone", "someone@example.com", &git2::Time::new(1234567890, 62), ) .unwrap(); let oid2 = git_repo .commit( None, &signature, &signature, "next", &tree2, &[&git_repo.find_commit(oid1).unwrap()], ) .unwrap(); insta::assert_snapshot!( git_repo.head().unwrap().peel_to_commit().unwrap().id().to_string(), @"2ee37513d2b5e549f7478c671a780053614bff19" ); // Import the repo while there is no path to the second commit test_env.jj_cmd_ok(&workspace_root, &["init", "--git-repo", "."]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_root), @r###" @ 66ae47cee4f8c28ee8d7e4f5d9401b03c07e22f2 ◉ 2ee37513d2b5e549f7478c671a780053614bff19 master HEAD@git initial ◉ 0000000000000000000000000000000000000000 "###); insta::assert_snapshot!( git_repo.head().unwrap().peel_to_commit().unwrap().id().to_string(), @"2ee37513d2b5e549f7478c671a780053614bff19" ); // Check that trying to look up the second commit fails gracefully let stderr = test_env.jj_cmd_failure(&workspace_root, &["show", &oid2.to_string()]); insta::assert_snapshot!(stderr, @r###" Error: Revision "8e713ff77b54928dd4a82aaabeca44b1ae91722c" doesn't exist "###); }