From 83d13f8238986d2c1cc7ca92b9b4157269757b3e Mon Sep 17 00:00:00 2001 From: Sandrine Pataut Date: Fri, 13 Mar 2026 10:04:47 +0100 Subject: [PATCH] Add --show-current flag to the branch subcommand --- src/subcommand/branch_subcommand.cpp | 21 +++- src/subcommand/branch_subcommand.hpp | 2 + test/test_branch.py | 175 ++++++++++++++++++++++++++- 3 files changed, 193 insertions(+), 5 deletions(-) diff --git a/src/subcommand/branch_subcommand.cpp b/src/subcommand/branch_subcommand.cpp index 82decf1..f0ea9fa 100644 --- a/src/subcommand/branch_subcommand.cpp +++ b/src/subcommand/branch_subcommand.cpp @@ -14,6 +14,7 @@ branch_subcommand::branch_subcommand(const libgit2_object&, CLI::App& app) sub->add_flag("-r,--remotes", m_remote_flag, "List or delete (if used with -d) the remote-tracking branches"); sub->add_flag("-l,--list", m_list_flag, "List branches"); sub->add_flag("-f,--force", m_force_flag, "Skips confirmation"); + sub->add_flag("--show-current", m_show_current_flag, "Print the name of the current branch. In detached HEAD state, nothing is printed."); sub->callback([this]() { this->run(); }); } @@ -23,7 +24,12 @@ void branch_subcommand::run() auto directory = get_current_git_path(); auto repo = repository_wrapper::open(directory); - if (m_list_flag || m_branch_name.empty()) + if (m_show_current_flag) + { + // TODO: if another flag, return usage/Generic options/Specific git-branch actions + run_show_current(repo); + } + else if (m_list_flag || m_branch_name.empty()) { run_list(repo); } @@ -64,9 +70,20 @@ void branch_subcommand::run_deletion(repository_wrapper& repo) delete_branch(std::move(branch)); } - void branch_subcommand::run_creation(repository_wrapper& repo) { // TODO: handle specification of starting commit repo.create_branch(m_branch_name, m_force_flag); } + +void branch_subcommand::run_show_current(const repository_wrapper& repo) +{ + auto name = repo.head_short_name(); + + if (name == "HEAD") + { + return; + } + + std::cout << name << std::endl; +} diff --git a/src/subcommand/branch_subcommand.hpp b/src/subcommand/branch_subcommand.hpp index b0d95d6..79d7900 100644 --- a/src/subcommand/branch_subcommand.hpp +++ b/src/subcommand/branch_subcommand.hpp @@ -19,6 +19,7 @@ class branch_subcommand void run_list(const repository_wrapper& repo); void run_deletion(repository_wrapper& repo); void run_creation(repository_wrapper& repo); + void run_show_current(const repository_wrapper& repo); std::string m_branch_name = {}; bool m_deletion_flag = false; @@ -26,4 +27,5 @@ class branch_subcommand bool m_remote_flag = false; bool m_list_flag = false; bool m_force_flag = false; + bool m_show_current_flag = false; }; diff --git a/test/test_branch.py b/test/test_branch.py index ffaf29b..8c32cb6 100644 --- a/test/test_branch.py +++ b/test/test_branch.py @@ -9,7 +9,7 @@ def test_branch_list(repo_init_with_commit, git2cpp_path, tmp_path): cmd = [git2cpp_path, "branch"] p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode == 0 - assert "* ma" in p.stdout + assert "* main" in p.stdout def test_branch_create_delete(repo_init_with_commit, git2cpp_path, tmp_path): @@ -22,7 +22,7 @@ def test_branch_create_delete(repo_init_with_commit, git2cpp_path, tmp_path): list_cmd = [git2cpp_path, "branch"] p_list = subprocess.run(list_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_list.returncode == 0 - assert " foregone\n* ma" in p_list.stdout + assert " foregone\n* main" in p_list.stdout del_cmd = [git2cpp_path, "branch", "-d", "foregone"] p_del = subprocess.run(del_cmd, capture_output=True, cwd=tmp_path, text=True) @@ -30,7 +30,7 @@ def test_branch_create_delete(repo_init_with_commit, git2cpp_path, tmp_path): p_list2 = subprocess.run(list_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_list2.returncode == 0 - assert "* ma" in p_list2.stdout + assert "* main" in p_list2.stdout def test_branch_nogit(git2cpp_path, tmp_path): @@ -51,3 +51,172 @@ def test_branch_new_repo(git2cpp_path, tmp_path, run_in_tmp_path): p_branch = subprocess.run(branch_cmd, cwd=tmp_path) assert p_branch.returncode == 0 + + +def test_branch_list_flag(repo_init_with_commit, git2cpp_path, tmp_path): + """Explicit -l/--list flag behaves the same as bare 'branch'.""" + assert (tmp_path / "initial.txt").exists() + + subprocess.run([git2cpp_path, "branch", "feature-a"], cwd=tmp_path, check=True) + + for flag in ["-l", "--list"]: + cmd = [git2cpp_path, "branch", flag] + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) + assert p.returncode == 0 + assert " feature-a" in p.stdout + assert "* main" in p.stdout + + +def test_branch_list_all(xtl_clone, git2cpp_path, tmp_path): + """The -a/--all flag lists both local and remote-tracking branches.""" + xtl_path = tmp_path / "xtl" + + cmd = [git2cpp_path, "branch", "-a"] + p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) + assert p.returncode == 0 + assert "* master" in p.stdout + assert "origin/" in p.stdout + + +def test_branch_list_remotes(xtl_clone, git2cpp_path, tmp_path): + """The -r/--remotes flag lists only remote-tracking branches.""" + xtl_path = tmp_path / "xtl" + + cmd = [git2cpp_path, "branch", "-r"] + p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) + assert p.returncode == 0 + assert "origin/" in p.stdout + # Local branch should NOT appear with * prefix + assert "* master" not in p.stdout + + +def test_branch_create_already_exists(repo_init_with_commit, git2cpp_path, tmp_path): + """Creating a branch that already exists should fail without --force.""" + assert (tmp_path / "initial.txt").exists() + + subprocess.run([git2cpp_path, "branch", "duplicate"], cwd=tmp_path, check=True) + + cmd = [git2cpp_path, "branch", "duplicate"] + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) + assert p.returncode != 0 + + +def test_branch_create_force_overwrite( + repo_init_with_commit, commit_env_config, git2cpp_path, tmp_path +): + """--force allows overwriting an existing branch.""" + assert (tmp_path / "initial.txt").exists() + + subprocess.run([git2cpp_path, "branch", "my-branch"], cwd=tmp_path, check=True) + + # Add a second commit so HEAD moves forward + (tmp_path / "second.txt").write_text("second") + subprocess.run([git2cpp_path, "add", "second.txt"], cwd=tmp_path, check=True) + subprocess.run( + [git2cpp_path, "commit", "-m", "Second commit"], cwd=tmp_path, check=True + ) + + # Without --force this would fail; with -f it should reset the branch to current HEAD + cmd = [git2cpp_path, "branch", "-f", "my-branch"] + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) + assert p.returncode == 0 + + +def test_branch_delete_nonexistent(repo_init_with_commit, git2cpp_path, tmp_path): + """Deleting a branch that doesn't exist should fail.""" + assert (tmp_path / "initial.txt").exists() + + cmd = [git2cpp_path, "branch", "-d", "no-such-branch"] + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) + assert p.returncode != 0 + + +def test_branch_create_multiple(repo_init_with_commit, git2cpp_path, tmp_path): + """Creating multiple branches and verifying they all appear in the listing.""" + assert (tmp_path / "initial.txt").exists() + + branches = ["alpha", "beta", "gamma"] + for name in branches: + p = subprocess.run( + [git2cpp_path, "branch", name], capture_output=True, cwd=tmp_path, text=True + ) + assert p.returncode == 0 + + cmd = [git2cpp_path, "branch"] + p_list = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) + assert p_list.returncode == 0 + for name in branches: + assert f" {name}" in p_list.stdout + # Current branch is still starred + assert "* main" in p_list.stdout + + +def test_branch_show_current(repo_init_with_commit, git2cpp_path, tmp_path): + """--show-current prints the current branch name.""" + assert (tmp_path / "initial.txt").exists() + + cmd = [git2cpp_path, "branch", "--show-current"] + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) + assert p.returncode == 0 + print(p.stdout) + # Default branch after init is "main" or "master" depending on git config + assert p.stdout.strip() == "main" + + +def test_branch_show_current_after_create_and_switch( + repo_init_with_commit, git2cpp_path, tmp_path +): + """--show-current reflects the branch we switched to.""" + assert (tmp_path / "initial.txt").exists() + + subprocess.run( + [git2cpp_path, "checkout", "-b", "new-feature"], cwd=tmp_path, check=True + ) + + cmd = [git2cpp_path, "branch", "--show-current"] + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) + assert p.returncode == 0 + assert p.stdout == "new-feature\n" + + +def test_branch_show_current_detached_head( + repo_init_with_commit, git2cpp_path, tmp_path +): + """--show-current prints nothing when HEAD is detached.""" + assert (tmp_path / "initial.txt").exists() + + result = subprocess.run( + [git2cpp_path, "rev-parse", "HEAD"], + capture_output=True, + cwd=tmp_path, + text=True, + check=True, + ) + head_sha = result.stdout.strip() + subprocess.run([git2cpp_path, "checkout", head_sha], cwd=tmp_path, check=True) + + cmd = [git2cpp_path, "branch", "--show-current"] + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) + assert p.returncode == 0 + assert p.stdout == "" + + +def test_branch_show_current_new_repo(git2cpp_path, tmp_path, run_in_tmp_path): + """--show-current prints the branch name even on a fresh repo with no commits (unborn HEAD).""" + assert list(tmp_path.iterdir()) == [] + + subprocess.run([git2cpp_path, "init", "-b", "main"], cwd=tmp_path, check=True) + + cmd = [git2cpp_path, "branch", "--show-current"] + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) + assert p.returncode == 0 + # Default branch after init is "main" or "master" depending on git config + assert p.stdout.strip() == "main" + + +def test_branch_show_current_nogit(git2cpp_path, tmp_path): + """--show-current fails gracefully outside a git repository.""" + cmd = [git2cpp_path, "branch", "--show-current"] + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) + assert p.returncode != 0 + assert "error: could not find repository at" in p.stderr