diff --git a/src/subcommand/checkout_subcommand.cpp b/src/subcommand/checkout_subcommand.cpp index aba90fb..a66a11b 100644 --- a/src/subcommand/checkout_subcommand.cpp +++ b/src/subcommand/checkout_subcommand.cpp @@ -1,9 +1,12 @@ #include #include +#include #include "../subcommand/checkout_subcommand.hpp" +#include "../subcommand/status_subcommand.hpp" #include "../utils/git_exception.hpp" #include "../wrapper/repository_wrapper.hpp" +#include "../wrapper/status_wrapper.hpp" checkout_subcommand::checkout_subcommand(const libgit2_object&, CLI::App& app) { @@ -30,16 +33,22 @@ void checkout_subcommand::run() git_checkout_options options; git_checkout_options_init(&options, GIT_CHECKOUT_OPTIONS_VERSION); - if(m_force_checkout_flag) + if (m_force_checkout_flag) { options.checkout_strategy = GIT_CHECKOUT_FORCE; } + // else + // { + // options.checkout_strategy = GIT_CHECKOUT_SAFE; + // } if (m_create_flag || m_force_create_flag) { auto annotated_commit = create_local_branch(repo, m_branch_name, m_force_create_flag); checkout_tree(repo, annotated_commit, m_branch_name, options); update_head(repo, annotated_commit, m_branch_name); + + std::cout << "Switched to a new branch '" << m_branch_name << "'" << std::endl; } else { @@ -51,8 +60,54 @@ void checkout_subcommand::run() buffer << "error: could not resolve pathspec '" << m_branch_name << "'" << std::endl; throw std::runtime_error(buffer.str()); } - checkout_tree(repo, *optional_commit, m_branch_name, options); - update_head(repo, *optional_commit, m_branch_name); + + auto sl = status_list_wrapper::status_list(repo); + try + { + checkout_tree(repo, *optional_commit, m_branch_name, options); + update_head(repo, *optional_commit, m_branch_name); + } + catch (const git_exception& e) + { + if (sl.has_notstagged_header()) + { + std::cout << "Your local changes to the following files would be overwritten by checkout:" << std::endl; + + for (const auto* entry : sl.get_entry_list(GIT_STATUS_WT_MODIFIED)) + { + std::cout << "\t" << entry->index_to_workdir->new_file.path << std::endl; + } + for (const auto* entry : sl.get_entry_list(GIT_STATUS_WT_DELETED)) + { + std::cout << "\t" << entry->index_to_workdir->old_file.path << std::endl; + } + + std::cout << "Please commit your changes or stash them before you switch branches.\nAborting" << std::endl; + return; + } + else + { + throw e; + } + return; + } + + if (sl.has_notstagged_header()) + { + bool is_long = false; + bool is_coloured = false; + std::set tracked_dir_set{}; + print_notstagged(sl, tracked_dir_set, is_long, is_coloured); + } + if (sl.has_tobecommited_header()) + { + bool is_long = false; + bool is_coloured = false; + std::set tracked_dir_set{}; + print_tobecommited(sl, tracked_dir_set, is_long, is_coloured); + } + std::cout << "Switched to branch '" << m_branch_name << "'" << std::endl; + print_tracking_info(repo, sl, true); } } diff --git a/src/subcommand/checkout_subcommand.hpp b/src/subcommand/checkout_subcommand.hpp index e041174..58ecb6b 100644 --- a/src/subcommand/checkout_subcommand.hpp +++ b/src/subcommand/checkout_subcommand.hpp @@ -12,6 +12,7 @@ class checkout_subcommand { public: + void print_message(repository_wrapper& repo); explicit checkout_subcommand(const libgit2_object&, CLI::App& app); void run(); diff --git a/src/subcommand/status_subcommand.cpp b/src/subcommand/status_subcommand.cpp index 8bca1e5..8a95d20 100644 --- a/src/subcommand/status_subcommand.cpp +++ b/src/subcommand/status_subcommand.cpp @@ -7,7 +7,6 @@ #include #include "status_subcommand.hpp" -#include "../wrapper/status_wrapper.hpp" status_subcommand::status_subcommand(const libgit2_object&, CLI::App& app) @@ -47,14 +46,14 @@ struct print_entry std::string item; }; -std::string get_print_status(git_status_t status, output_format of) +std::string get_print_status(git_status_t status, bool is_long) { std::string entry_status; - if ((of == output_format::DEFAULT) || (of == output_format::LONG)) + if (is_long) { entry_status = get_status_msg(status).long_mod; } - else if (of == output_format::SHORT) + else { entry_status = get_status_msg(status).short_mod; } @@ -89,7 +88,7 @@ std::string get_print_item(const char* old_path, const char* new_path) } std::vector get_entries_to_print(git_status_t status, status_list_wrapper& sl, - bool head_selector, output_format of, std::set* tracked_dir_set = nullptr) + bool head_selector, bool is_long, std::set* tracked_dir_set = nullptr) { std::vector entries_to_print{}; const auto& entry_list = sl.get_entry_list(status); @@ -106,7 +105,7 @@ std::vector get_entries_to_print(git_status_t status, status_list_w update_tracked_dir_set(old_path, tracked_dir_set); - print_entry e = { get_print_status(status, of), get_print_item(old_path, new_path)}; + print_entry e = { get_print_status(status, is_long), get_print_item(old_path, new_path)}; entries_to_print.push_back(std::move(e)); } @@ -161,15 +160,12 @@ void print_not_tracked(const std::vector& entries_to_print, const s print_entries(not_tracked_entries_to_print, is_long, colour); } -void print_tracking_info(repository_wrapper& repo, status_list_wrapper& sl, status_subcommand_options options, bool is_long) +void print_tracking_info(repository_wrapper& repo, status_list_wrapper& sl, bool is_long) { - auto branch_name = repo.head_short_name(); auto tracking_info = repo.get_tracking_info(); if (is_long) { - std::cout << "On branch " << branch_name << std::endl; - if (tracking_info.has_upstream) { if(tracking_info.ahead > 0 && tracking_info.behind == 0) @@ -215,11 +211,6 @@ void print_tracking_info(repository_wrapper& repo, status_list_wrapper& sl, stat } else { - if (options.m_branch_flag) - { - std::cout << "## " << branch_name << std::endl; - } - if (tracking_info.has_upstream) { std::cout << "..." << tracking_info.upstream_name; @@ -246,63 +237,100 @@ void print_tracking_info(repository_wrapper& repo, status_list_wrapper& sl, stat } } -void print_tobecommited(status_list_wrapper& sl, output_format of, std::set tracked_dir_set, bool is_long) +void print_tobecommited(status_list_wrapper& sl, std::set tracked_dir_set, bool is_long, bool is_coloured) { - stream_colour_fn colour = termcolor::green; + + stream_colour_fn colour; + if (is_coloured) + { + colour = termcolor::green; + } + else + { + colour = termcolor::bright_white; + } + if (is_long) { std::cout << tobecommited_header; } - print_entries(get_entries_to_print(GIT_STATUS_INDEX_NEW, sl, true, of, &tracked_dir_set), is_long, colour); - print_entries(get_entries_to_print(GIT_STATUS_INDEX_MODIFIED, sl, true, of, &tracked_dir_set), is_long, colour); - print_entries(get_entries_to_print(GIT_STATUS_INDEX_DELETED, sl, true, of, &tracked_dir_set), is_long, colour); - print_entries(get_entries_to_print(GIT_STATUS_INDEX_RENAMED, sl, true, of, &tracked_dir_set), is_long, colour); - print_entries(get_entries_to_print(GIT_STATUS_INDEX_TYPECHANGE, sl, true, of, &tracked_dir_set), is_long, colour); + print_entries(get_entries_to_print(GIT_STATUS_INDEX_NEW, sl, true, is_long, &tracked_dir_set), is_long, colour); + print_entries(get_entries_to_print(GIT_STATUS_INDEX_MODIFIED, sl, true, is_long, &tracked_dir_set), is_long, colour); + print_entries(get_entries_to_print(GIT_STATUS_INDEX_DELETED, sl, true, is_long, &tracked_dir_set), is_long, colour); + print_entries(get_entries_to_print(GIT_STATUS_INDEX_RENAMED, sl, true, is_long, &tracked_dir_set), is_long, colour); + print_entries(get_entries_to_print(GIT_STATUS_INDEX_TYPECHANGE, sl, true, is_long, &tracked_dir_set), is_long, colour); if (is_long) { std::cout << std::endl; } } -void print_notstagged(status_list_wrapper& sl, output_format of, std::set tracked_dir_set, bool is_long) +void print_notstagged(status_list_wrapper& sl, std::set tracked_dir_set, bool is_long, bool is_coloured) { - stream_colour_fn colour = termcolor::red; + stream_colour_fn colour; + if (is_coloured) + { + colour = termcolor::red; + } + else + { + colour = termcolor::bright_white; + } + if (is_long) { std::cout << notstagged_header; } - print_entries(get_entries_to_print(GIT_STATUS_WT_MODIFIED, sl, false, of, &tracked_dir_set), is_long, colour); - print_entries(get_entries_to_print(GIT_STATUS_WT_DELETED, sl, false, of, &tracked_dir_set), is_long, colour); - print_entries(get_entries_to_print(GIT_STATUS_WT_TYPECHANGE, sl, false, of, &tracked_dir_set), is_long, colour); - print_entries(get_entries_to_print(GIT_STATUS_WT_RENAMED, sl, false, of, &tracked_dir_set), is_long, colour); + print_entries(get_entries_to_print(GIT_STATUS_WT_MODIFIED, sl, false, is_long, &tracked_dir_set), is_long, colour); + print_entries(get_entries_to_print(GIT_STATUS_WT_DELETED, sl, false, is_long, &tracked_dir_set), is_long, colour); + print_entries(get_entries_to_print(GIT_STATUS_WT_TYPECHANGE, sl, false, is_long, &tracked_dir_set), is_long, colour); + print_entries(get_entries_to_print(GIT_STATUS_WT_RENAMED, sl, false, is_long, &tracked_dir_set), is_long, colour); if (is_long) { std::cout << std::endl; } } -void print_unmerged(status_list_wrapper& sl, output_format of, std::set tracked_dir_set, std::set untracked_dir_set, bool is_long) +void print_unmerged(status_list_wrapper& sl, std::set tracked_dir_set, std::set untracked_dir_set, bool is_long, bool is_coloured) { - stream_colour_fn colour = termcolor::red; + stream_colour_fn colour; + if (is_coloured) + { + colour = termcolor::red; + } + else + { + colour = termcolor::bright_white; + } + if (is_long) { std::cout << unmerged_header; } - print_not_tracked(get_entries_to_print(GIT_STATUS_CONFLICTED, sl, false, of), tracked_dir_set, untracked_dir_set, is_long, colour); + print_not_tracked(get_entries_to_print(GIT_STATUS_CONFLICTED, sl, false, is_long), tracked_dir_set, untracked_dir_set, is_long, colour); if (is_long) { std::cout << std::endl; } } -void print_untracked(status_list_wrapper& sl, output_format of, std::set tracked_dir_set, std::set untracked_dir_set, bool is_long) +void print_untracked(status_list_wrapper& sl, std::set tracked_dir_set, std::set untracked_dir_set, bool is_long, bool is_coloured) { - stream_colour_fn colour = termcolor::red; + stream_colour_fn colour; + if (is_coloured) + { + colour = termcolor::red; + } + else + { + colour = termcolor::bright_white; + } + if (is_long) { std::cout << untracked_header; } - print_not_tracked(get_entries_to_print(GIT_STATUS_WT_NEW, sl, false, of), tracked_dir_set, untracked_dir_set, is_long, colour); + print_not_tracked(get_entries_to_print(GIT_STATUS_WT_NEW, sl, false, is_long), tracked_dir_set, untracked_dir_set, is_long, colour); if (is_long) { std::cout << std::endl; @@ -341,27 +369,38 @@ void status_run(status_subcommand_options options) bool is_long; is_long = ((of == output_format::DEFAULT) || (of == output_format::LONG)); - print_tracking_info(repo, sl, options, is_long); + + auto branch_name = repo.head_short_name(); + if (is_long) + { + std::cout << "On branch " << branch_name << std::endl; + } + else if (options.m_branch_flag) + { + std::cout << "## " << branch_name << std::endl; + } + bool is_coloured = true; + print_tracking_info(repo, sl, is_long); if (sl.has_tobecommited_header()) { - print_tobecommited(sl, of, tracked_dir_set,is_long); + print_tobecommited(sl, tracked_dir_set, is_long, is_coloured); } if (sl.has_notstagged_header()) { - print_notstagged(sl, of, tracked_dir_set, is_long); + print_notstagged(sl, tracked_dir_set, is_long, is_coloured); } // TODO: check if should be printed before "not stagged" files if (sl.has_unmerged_header()) { - print_unmerged(sl, of, tracked_dir_set, untracked_dir_set, is_long); + print_unmerged(sl, tracked_dir_set, untracked_dir_set, is_long, is_coloured); } if (sl.has_untracked_header()) { - print_untracked(sl, of, tracked_dir_set, untracked_dir_set, is_long); + print_untracked(sl, tracked_dir_set, untracked_dir_set, is_long, is_coloured); } // TODO: check if this message should be displayed even if there are untracked files diff --git a/src/subcommand/status_subcommand.hpp b/src/subcommand/status_subcommand.hpp index fcd1a37..064a09f 100644 --- a/src/subcommand/status_subcommand.hpp +++ b/src/subcommand/status_subcommand.hpp @@ -3,6 +3,7 @@ #include #include "../utils/common.hpp" +#include "../wrapper/status_wrapper.hpp" struct status_subcommand_options { @@ -22,4 +23,7 @@ class status_subcommand status_subcommand_options m_options; }; +void print_tobecommited(status_list_wrapper& sl, std::set tracked_dir_set, bool is_long, bool is_coloured); +void print_notstagged(status_list_wrapper& sl, std::set tracked_dir_set, bool is_long, bool is_coloured); +void print_tracking_info(repository_wrapper& repo, status_list_wrapper& sl, bool is_long); void status_run(status_subcommand_options fl = {}); diff --git a/test/test_checkout.py b/test/test_checkout.py index 3243318..d2142f7 100644 --- a/test/test_checkout.py +++ b/test/test_checkout.py @@ -7,44 +7,189 @@ def test_checkout(xtl_clone, git2cpp_path, tmp_path): assert (tmp_path / "xtl").exists() xtl_path = tmp_path / "xtl" - create_cmd = [git2cpp_path, 'branch', 'foregone'] + create_cmd = [git2cpp_path, "branch", "foregone"] p_create = subprocess.run(create_cmd, capture_output=True, cwd=xtl_path, text=True) assert p_create.returncode == 0 - checkout_cmd = [git2cpp_path, 'checkout', 'foregone'] - p_checkout = subprocess.run(checkout_cmd, capture_output=True, cwd=xtl_path, text=True) + checkout_cmd = [git2cpp_path, "checkout", "foregone"] + p_checkout = subprocess.run( + checkout_cmd, capture_output=True, cwd=xtl_path, text=True + ) assert p_checkout.returncode == 0 - assert(p_checkout.stdout == ''); + assert "Switched to branch 'foregone'" in p_checkout.stdout - branch_cmd = [git2cpp_path, 'branch'] + branch_cmd = [git2cpp_path, "branch"] p_branch = subprocess.run(branch_cmd, capture_output=True, cwd=xtl_path, text=True) assert p_branch.returncode == 0 - assert(p_branch.stdout == '* foregone\n master\n') + assert p_branch.stdout == "* foregone\n master\n" - checkout_cmd[2] = 'master' - p_checkout2 = subprocess.run(checkout_cmd, capture_output=True, cwd=xtl_path, text=True) + checkout_cmd[2] = "master" + p_checkout2 = subprocess.run( + checkout_cmd, capture_output=True, cwd=xtl_path, text=True + ) assert p_checkout2.returncode == 0 + assert "Switched to branch 'master'" in p_checkout2.stdout def test_checkout_b(xtl_clone, git2cpp_path, tmp_path): assert (tmp_path / "xtl").exists() xtl_path = tmp_path / "xtl" - checkout_cmd = [git2cpp_path, 'checkout', '-b', 'foregone'] - p_checkout = subprocess.run(checkout_cmd, capture_output=True, cwd=xtl_path, text=True) + checkout_cmd = [git2cpp_path, "checkout", "-b", "foregone"] + p_checkout = subprocess.run( + checkout_cmd, capture_output=True, cwd=xtl_path, text=True + ) assert p_checkout.returncode == 0 - assert(p_checkout.stdout == ''); + assert "Switched to a new branch 'foregone'" in p_checkout.stdout - branch_cmd = [git2cpp_path, 'branch'] + branch_cmd = [git2cpp_path, "branch"] p_branch = subprocess.run(branch_cmd, capture_output=True, cwd=xtl_path, text=True) assert p_branch.returncode == 0 - assert(p_branch.stdout == '* foregone\n master\n') + assert p_branch.stdout == "* foregone\n master\n" - checkout_cmd.remove('-b') - checkout_cmd[2] = 'master' + checkout_cmd.remove("-b") + checkout_cmd[2] = "master" p_checkout2 = subprocess.run(checkout_cmd, cwd=xtl_path, text=True) assert p_checkout2.returncode == 0 p_branch2 = subprocess.run(branch_cmd, capture_output=True, cwd=xtl_path, text=True) assert p_branch2.returncode == 0 - assert(p_branch2.stdout == ' foregone\n* master\n') + assert p_branch2.stdout == " foregone\n* master\n" + + +def test_checkout_B_force_create(xtl_clone, git2cpp_path, tmp_path): + """Test checkout -B to force create or reset a branch""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Create a branch first + create_cmd = [git2cpp_path, "branch", "resetme"] + p_create = subprocess.run(create_cmd, capture_output=True, cwd=xtl_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=xtl_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=xtl_path, text=True) + assert p_branch.returncode == 0 + assert "* resetme" in p_branch.stdout + + +def test_checkout_invalid_branch(xtl_clone, git2cpp_path, tmp_path): + """Test that checkout fails gracefully with invalid branch name""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Try to checkout non-existent branch + checkout_cmd = [git2cpp_path, "checkout", "nonexistent"] + p_checkout = subprocess.run( + checkout_cmd, capture_output=True, cwd=xtl_path, text=True + ) + + # Should fail with error message + assert p_checkout.returncode != 0 + assert "error: could not resolve pathspec 'nonexistent'" in p_checkout.stderr + + +def test_checkout_with_unstaged_changes(xtl_clone, git2cpp_path, tmp_path): + """Test that checkout shows unstaged changes when switching branches""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Create a new branch + create_cmd = [git2cpp_path, "branch", "newbranch"] + p_create = subprocess.run(create_cmd, capture_output=True, cwd=xtl_path, text=True) + assert p_create.returncode == 0 + + # Modify a file (unstaged change) + readme_path = xtl_path / "README.md" + readme_path.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=xtl_path, text=True + ) + + # Should succeed and show status + assert p_checkout.returncode == 0 + assert " M README.md" 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( + xtl_clone, 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""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # 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=xtl_path, text=True) + assert p_create.returncode == 0 + + # Modify README.md and commit it on newbranch + readme_path = xtl_path / "README.md" + readme_path.write_text("Content on newbranch") + + add_cmd = [git2cpp_path, "add", "README.md"] + subprocess.run(add_cmd, cwd=xtl_path, text=True) + + commit_cmd = [git2cpp_path, "commit", "-m", "Change on newbranch"] + subprocess.run(commit_cmd, cwd=xtl_path, text=True) + + # Switch back to master + checkout_master_cmd = [git2cpp_path, "checkout", "master"] + p_master = subprocess.run( + checkout_master_cmd, capture_output=True, cwd=xtl_path, text=True + ) + assert p_master.returncode == 0 + + # Now modify README.md locally (unstaged) on master + readme_path.write_text("Local modification on master") + + # 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=xtl_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 "README.md" 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 master (didn't switch) + branch_cmd = [git2cpp_path, "branch"] + p_branch = subprocess.run( + branch_cmd, capture_output=True, cwd=xtl_path, text=True + ) + assert "* master" 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=xtl_path, text=True + ) + assert "* newbranch" in p_branch.stdout