import subprocess import pytest from .conftest import strip_ansi_colours def test_checkout(repo_init_with_commit, git2cpp_path, tmp_path): assert (tmp_path / "initial.txt").exists() create_cmd = [git2cpp_path, "branch", "foregone"] p_create = subprocess.run(create_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_create.returncode == 0 checkout_cmd = [git2cpp_path, "checkout", "foregone"] p_checkout = subprocess.run(checkout_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_checkout.returncode == 0 assert "Switched to branch 'foregone'" in p_checkout.stdout branch_cmd = [git2cpp_path, "branch"] p_branch = subprocess.run(branch_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_branch.returncode == 0 assert p_branch.stdout == "* foregone\n main\n" checkout_cmd[2] = "main" p_checkout2 = subprocess.run(checkout_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_checkout2.returncode == 0 assert "Switched to branch 'main'" in p_checkout2.stdout def test_checkout_b(repo_init_with_commit, git2cpp_path, tmp_path): assert (tmp_path / "initial.txt").exists() checkout_cmd = [git2cpp_path, "checkout", "-b", "foregone"] p_checkout = subprocess.run(checkout_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_checkout.returncode == 0 assert "Switched to a new branch 'foregone'" in p_checkout.stdout branch_cmd = [git2cpp_path, "branch"] p_branch = subprocess.run(branch_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_branch.returncode == 0 assert p_branch.stdout == "* foregone\n main\n" checkout_cmd.remove("-b") checkout_cmd[2] = "main" p_checkout2 = subprocess.run(checkout_cmd, cwd=tmp_path, text=True) assert p_checkout2.returncode == 0 p_branch2 = subprocess.run(branch_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_branch2.returncode == 0 assert p_branch2.stdout == " foregone\n* main\n" def test_checkout_B_force_create(repo_init_with_commit, git2cpp_path, tmp_path): """Test checkout -B to force create or reset a branch""" assert (tmp_path / "initial.txt").exists() # Create a branch first create_cmd = [git2cpp_path, "branch", "resetme"] p_create = subprocess.run(create_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_create.returncode == 0 # Use -B to reset it (should not fail even if branch exists) checkout_cmd = [git2cpp_path, "checkout", "-B", "resetme"] p_checkout = subprocess.run(checkout_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_checkout.returncode == 0 assert "Switched to a new branch 'resetme'" in p_checkout.stdout # Verify we're on the branch branch_cmd = [git2cpp_path, "branch"] p_branch = subprocess.run(branch_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_branch.returncode == 0 assert "* resetme" in p_branch.stdout def test_checkout_invalid_branch(repo_init_with_commit, git2cpp_path, tmp_path): """Test that checkout fails gracefully with invalid branch name""" assert (tmp_path / "initial.txt").exists() # Try to checkout non-existent branch checkout_cmd = [git2cpp_path, "checkout", "nonexistent"] p_checkout = subprocess.run(checkout_cmd, capture_output=True, cwd=tmp_path, text=True) # Should fail with error message assert p_checkout.returncode != 0 assert ( "error: pathspec 'nonexistent' did not match any file(s) known to git" in p_checkout.stderr ) def test_checkout_with_unstaged_changes(repo_init_with_commit, git2cpp_path, tmp_path): """Test that checkout shows unstaged changes when switching branches""" initial_file = tmp_path / "initial.txt" assert (initial_file).exists() # Create a new branch create_cmd = [git2cpp_path, "branch", "newbranch"] p_create = subprocess.run(create_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_create.returncode == 0 # Modify a file (unstaged change) initial_file.write_text("Modified content") # Checkout - should succeed and show the modified file status checkout_cmd = [git2cpp_path, "checkout", "newbranch"] p_checkout = subprocess.run(checkout_cmd, capture_output=True, cwd=tmp_path, text=True) # Should succeed and show status assert p_checkout.returncode == 0 p_checkout.stdout = strip_ansi_colours(p_checkout.stdout) assert " M initial.txt" in p_checkout.stdout assert "Switched to branch 'newbranch'" in p_checkout.stdout @pytest.mark.parametrize("force_flag", ["", "-f", "--force"]) def test_checkout_refuses_overwrite( repo_init_with_commit, commit_env_config, git2cpp_path, tmp_path, force_flag ): """Test that checkout refuses to switch when local changes would be overwritten, and switches when using --force""" initial_file = tmp_path / "initial.txt" assert (initial_file).exists() # Create a new branch and switch to it create_cmd = [git2cpp_path, "checkout", "-b", "newbranch"] p_create = subprocess.run(create_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_create.returncode == 0 # Modify initial.txt and commit it on newbranch initial_file.write_text("Content on newbranch") add_cmd = [git2cpp_path, "add", "initial.txt"] subprocess.run(add_cmd, cwd=tmp_path, text=True, check=True) commit_cmd = [git2cpp_path, "commit", "-m", "Change on newbranch"] subprocess.run(commit_cmd, cwd=tmp_path, text=True, check=True) # Switch back to default branch checkout_default_cmd = [git2cpp_path, "checkout", "main"] p_default = subprocess.run(checkout_default_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_default.returncode == 0 # Now modify initial.txt locally (unstaged) on default branch initial_file.write_text("Local modification on main") # Try to checkout newbranch checkout_cmd = [git2cpp_path, "checkout"] if force_flag != "": checkout_cmd.append(force_flag) checkout_cmd.append("newbranch") p_checkout = subprocess.run(checkout_cmd, capture_output=True, cwd=tmp_path, text=True) if force_flag == "": assert p_checkout.returncode != 0 assert ( "Your local changes to the following files would be overwritten by checkout:" in p_checkout.stdout ) assert "initial.txt" in p_checkout.stdout assert ( "Please commit your changes or stash them before you switch branches" in p_checkout.stdout ) # Verify we're still on default branch (didn't switch) branch_cmd = [git2cpp_path, "branch"] p_branch = subprocess.run(branch_cmd, capture_output=True, cwd=tmp_path, text=True) assert "* main" in p_branch.stdout else: assert "Switched to branch 'newbranch'" in p_checkout.stdout # Verify we switched to newbranch branch_cmd = [git2cpp_path, "branch"] p_branch = subprocess.run(branch_cmd, capture_output=True, cwd=tmp_path, text=True) assert "* newbranch" in p_branch.stdout def test_checkout_file_restores_modified_file(repo_init_with_commit, git2cpp_path, tmp_path): """Test that checkout -- discards working tree changes""" initial_file = tmp_path / "initial.txt" original_content = initial_file.read_text() # Modify the file (unstaged) initial_file.write_text("Modified content") assert initial_file.read_text() == "Modified content" # Restore it via checkout -- checkout_cmd = [git2cpp_path, "checkout", "--", "initial.txt"] p = subprocess.run(checkout_cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 assert initial_file.read_text() == original_content def test_checkout_file_restores_multiple_files(repo_init_with_commit, git2cpp_path, tmp_path): """Test that checkout -- restores multiple files at once""" initial_file = tmp_path / "initial.txt" # Create and commit a second file first second_file = tmp_path / "second.txt" second_file.write_text("second content") add_cmd = [git2cpp_path, "add", "second.txt"] subprocess.run(add_cmd, cwd=tmp_path, text=True, check=True) commit_cmd = [git2cpp_path, "commit", "-m", "Add second file"] subprocess.run(commit_cmd, cwd=tmp_path, text=True, check=True) original_initial = initial_file.read_text() original_second = second_file.read_text() # Modify both files initial_file.write_text("dirty initial") second_file.write_text("dirty second") checkout_cmd = [git2cpp_path, "checkout", "--", "initial.txt", "second.txt"] p = subprocess.run(checkout_cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 assert initial_file.read_text() == original_initial assert second_file.read_text() == original_second def test_checkout_file_does_not_affect_other_files(repo_init_with_commit, git2cpp_path, tmp_path): """Test that checkout -- only touches the specified file""" initial_file = tmp_path / "initial.txt" original_initial = initial_file.read_text() # Create and commit a second file second_file = tmp_path / "second.txt" second_file.write_text("second content") add_cmd = [git2cpp_path, "add", "second.txt"] subprocess.run(add_cmd, cwd=tmp_path, text=True, check=True) commit_cmd = [git2cpp_path, "commit", "-m", "Add second file"] subprocess.run(commit_cmd, cwd=tmp_path, text=True, check=True) # Modify both files initial_file.write_text("dirty initial") second_file.write_text("dirty second") # Only restore initial.txt checkout_cmd = [git2cpp_path, "checkout", "--", "initial.txt"] p = subprocess.run(checkout_cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 assert initial_file.read_text() == original_initial assert second_file.read_text() == "dirty second" def test_checkout_file_does_not_change_branch(repo_init_with_commit, git2cpp_path, tmp_path): """Test that checkout -- does not move HEAD or change the current branch""" initial_file = tmp_path / "initial.txt" original_initial = initial_file.read_text() initial_file.write_text("dirty") checkout_cmd = [git2cpp_path, "checkout", "--", "initial.txt"] p = subprocess.run(checkout_cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 assert initial_file.read_text() == original_initial branch_cmd = [git2cpp_path, "branch"] p_branch = subprocess.run(branch_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_branch.returncode == 0 assert "* main" in p_branch.stdout def test_checkout_file_nonexistent_path_fails(repo_init_with_commit, git2cpp_path, tmp_path): """Test that checkout -- fails with a non-zero exit code""" checkout_cmd = [git2cpp_path, "checkout", "--", "doesnotexist.txt"] p = subprocess.run(checkout_cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode != 0 def test_checkout_file_no_paths_fails(repo_init_with_commit, git2cpp_path, tmp_path): """Test that checkout -- with no file arguments fails""" checkout_cmd = [git2cpp_path, "checkout", "--"] p = subprocess.run(checkout_cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode != 0 assert "no branch or file specified" in p.stderr def test_checkout_branch_file_restores_modified_file(repo_init_with_commit, git2cpp_path, tmp_path): """Test that checkout -- restores the file from the branch.""" initial_file = tmp_path / "initial.txt" # Create a new commit on main so the branch switch is meaningful second_file = tmp_path / "second.txt" second_file.write_text("second content") subprocess.run([git2cpp_path, "add", "second.txt"], cwd=tmp_path, text=True, check=True) subprocess.run( [git2cpp_path, "commit", "-m", "Add second file"], cwd=tmp_path, text=True, check=True ) # Create and switch to feature branch subprocess.run([git2cpp_path, "checkout", "-b", "feature"], cwd=tmp_path, text=True, check=True) # Modify the file on feature branch and commit it initial_file.write_text("feature content") subprocess.run([git2cpp_path, "add", "initial.txt"], cwd=tmp_path, text=True, check=True) subprocess.run( [git2cpp_path, "commit", "-m", "Change initial on feature"], cwd=tmp_path, text=True, check=True, ) # Go back to main and dirty the file subprocess.run([git2cpp_path, "checkout", "main"], cwd=tmp_path, text=True, check=True) initial_file.write_text("local dirty content") # Restore only initial.txt from feature checkout_cmd = [git2cpp_path, "checkout", "feature", "initial.txt"] p = subprocess.run(checkout_cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 assert initial_file.read_text() == "feature content" branch_cmd = [git2cpp_path, "branch"] p_branch = subprocess.run(branch_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_branch.returncode == 0 assert "* main" in p_branch.stdout def test_checkout_branch_multiple_files_restores_all(repo_init_with_commit, git2cpp_path, tmp_path): """Test that checkout -- restores multiple files from the branch.""" initial_file = tmp_path / "initial.txt" second_file = tmp_path / "second.txt" second_file.write_text("second content") subprocess.run([git2cpp_path, "add", "second.txt"], cwd=tmp_path, text=True, check=True) subprocess.run( [git2cpp_path, "commit", "-m", "Add second file"], cwd=tmp_path, text=True, check=True ) # Create feature branch and modify both files there subprocess.run([git2cpp_path, "checkout", "-b", "feature"], cwd=tmp_path, text=True, check=True) initial_file.write_text("feature initial") second_file.write_text("feature second") subprocess.run( [git2cpp_path, "add", "initial.txt", "second.txt"], cwd=tmp_path, text=True, check=True ) subprocess.run( [git2cpp_path, "commit", "-m", "Change both files on feature"], cwd=tmp_path, text=True, check=True, ) # Return to main and dirty both files subprocess.run([git2cpp_path, "checkout", "main"], cwd=tmp_path, text=True, check=True) initial_file.write_text("dirty main initial") second_file.write_text("dirty main second") # Restore both files from feature checkout_cmd = [git2cpp_path, "checkout", "feature", "initial.txt", "second.txt"] p = subprocess.run(checkout_cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 assert initial_file.read_text() == "feature initial" assert second_file.read_text() == "feature second" branch_cmd = [git2cpp_path, "branch"] p_branch = subprocess.run(branch_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_branch.returncode == 0 assert "* main" in p_branch.stdout def test_checkout_tag(repo_init_with_commit, git2cpp_path, tmp_path): """checkout should detach HEAD at the tag commit.""" # Create a tag pointing to HEAD tag_cmd = [git2cpp_path, "tag", "v1.0"] p_tag = subprocess.run(tag_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_tag.returncode == 0 # Switch to the tag checkout_cmd = [git2cpp_path, "checkout", "v1.0"] p_checkout = subprocess.run(checkout_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_checkout.returncode == 0 assert "detached HEAD" in p_checkout.stdout # Verify we're detached current_branch_cmd = [git2cpp_path, "branch", "--show-current"] p_current = subprocess.run(current_branch_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_current.returncode == 0 assert p_current.stdout.strip() == "" branch_cmd = [git2cpp_path, "branch"] p_branch = subprocess.run(branch_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_branch.returncode == 0 assert "*" not in p_branch.stdout