Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add checkout messages
  • Loading branch information
SandrineP committed Feb 23, 2026
commit 2d7323565e885bd0b07ad4b2e360cf4df171f79c
84 changes: 52 additions & 32 deletions src/subcommand/checkout_subcommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,34 +20,6 @@ checkout_subcommand::checkout_subcommand(const libgit2_object&, CLI::App& app)
sub->callback([this]() { this->run(); });
}

void checkout_subcommand::print_message(repository_wrapper& repo)
{
auto sl = status_list_wrapper::status_list(repo);
if (sl.has_tobecommited_header())
{
bool is_long, is_coloured = false;
std::set<std::string> tracked_dir_set{};
print_tobecommited(sl, tracked_dir_set, is_long, is_coloured);
}
else 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->head_to_index->new_file.path << std::endl;
}
for (const auto* entry : sl.get_entry_list(GIT_STATUS_WT_DELETED))
{
std::cout << "\t" << entry->head_to_index->new_file.path << std::endl;
}

std::cout << "Please commit your changes or stash them before you switch branches.\nAborting" << std::endl;
}
std::cout << "Switched to branch '" << m_branch_name << "'" << std::endl;
print_tracking_info(repo, sl, true);
}

void checkout_subcommand::run()
{
auto directory = get_current_git_path();
Expand All @@ -65,14 +37,18 @@ void checkout_subcommand::run()
{
options.checkout_strategy = GIT_CHECKOUT_FORCE;
}
// else
// {
// options.checkout_strategy = GIT_CHECKOUT_SAFE;
// }
Comment thread
JohanMabille marked this conversation as resolved.
Outdated

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);

print_message(repo);
std::cout << "Switched to a new branch '" << m_branch_name << "'" << std::endl;
}
else
{
Expand All @@ -84,10 +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);

print_message(repo);
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;
}
Comment thread
JohanMabille marked this conversation as resolved.
Outdated

std::cout << "Please commit your changes or stash them before you switch branches.\nAborting" << std::endl;
return;
}
else
{
throw e;
}
return;
Comment thread
JohanMabille marked this conversation as resolved.
Outdated
}

if (sl.has_notstagged_header())
{
bool is_long = false;
bool is_coloured = false;
std::set<std::string> 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<std::string> 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);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/subcommand/status_subcommand.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ class status_subcommand
};

void print_tobecommited(status_list_wrapper& sl, std::set<std::string> tracked_dir_set, bool is_long, bool is_coloured);
void print_notstagged(status_list_wrapper& sl, std::set<std::string> 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 = {});
125 changes: 78 additions & 47 deletions test/test_checkout.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,8 @@ def test_checkout_invalid_branch(xtl_clone, git2cpp_path, tmp_path):
assert "error: could not resolve pathspec 'nonexistent'" in p_checkout.stderr


# @pytest.mark.skip(reason="Waiting for print_tobecommited and print_tracking_info implementations")
def test_checkout_with_unstaged_changes(xtl_clone, git2cpp_path, tmp_path):
"""Test that checkout warns about unstaged changes"""
"""Test that checkout shows unstaged changes when switching branches"""
assert (tmp_path / "xtl").exists()
xtl_path = tmp_path / "xtl"

Expand All @@ -113,52 +112,84 @@ def test_checkout_with_unstaged_changes(xtl_clone, git2cpp_path, tmp_path):
readme_path = xtl_path / "README.md"
readme_path.write_text("Modified content")

# Try to checkout - should warn about unstaged changes
# 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
)
print(p_checkout.stdout)
# Should show warning message (once implemented)
# 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 stayed on master 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
# print(p_branch.stdout)
# assert "* master" in p_branch.stdout


# def test_checkout_force_with_unstaged_changes(xtl_clone, git2cpp_path, tmp_path):
# """Test that checkout -f forces checkout even with unstaged changes"""
# assert (tmp_path / "xtl").exists()
# xtl_path = tmp_path / "xtl"

# # Create a new branch
# create_cmd = [git2cpp_path, 'branch', 'forcebranch']
# 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 that will be lost")

# # Force checkout - should succeed and discard changes
# checkout_cmd = [git2cpp_path, 'checkout', '-f', 'forcebranch']
# p_checkout = subprocess.run(checkout_cmd, capture_output=True, cwd=xtl_path, text=True)
# assert p_checkout.returncode == 0
# assert "Switched to branch 'forcebranch'" 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 '* forcebranch' in p_branch.stdout

# 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