diff --git a/CMakeLists.txt b/CMakeLists.txt index cdcecda..30c36bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,6 +82,8 @@ set(GIT2CPP_SRC ${GIT2CPP_SOURCE_DIR}/subcommand/stash_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/subcommand/status_subcommand.cpp ${GIT2CPP_SOURCE_DIR}/subcommand/status_subcommand.hpp + ${GIT2CPP_SOURCE_DIR}/subcommand/tag_subcommand.cpp + ${GIT2CPP_SOURCE_DIR}/subcommand/tag_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/utils/ansi_code.cpp ${GIT2CPP_SOURCE_DIR}/utils/ansi_code.hpp ${GIT2CPP_SOURCE_DIR}/utils/common.cpp @@ -126,6 +128,8 @@ set(GIT2CPP_SRC ${GIT2CPP_SOURCE_DIR}/wrapper/signature_wrapper.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/status_wrapper.cpp ${GIT2CPP_SOURCE_DIR}/wrapper/status_wrapper.hpp + ${GIT2CPP_SOURCE_DIR}/wrapper/tag_wrapper.cpp + ${GIT2CPP_SOURCE_DIR}/wrapper/tag_wrapper.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/tree_wrapper.cpp ${GIT2CPP_SOURCE_DIR}/wrapper/tree_wrapper.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/wrapper_base.hpp diff --git a/src/main.cpp b/src/main.cpp index efb385c..5d0223a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,6 +23,7 @@ #include "subcommand/reset_subcommand.hpp" #include "subcommand/stash_subcommand.hpp" #include "subcommand/status_subcommand.hpp" +#include "subcommand/tag_subcommand.hpp" #include "subcommand/revparse_subcommand.hpp" #include "subcommand/revlist_subcommand.hpp" #include "subcommand/rm_subcommand.hpp" @@ -60,6 +61,7 @@ int main(int argc, char** argv) revparse_subcommand revparse(lg2_obj, app); rm_subcommand rm(lg2_obj, app); stash_subcommand stash(lg2_obj, app); + tag_subcommand tag(lg2_obj, app); app.require_subcommand(/* min */ 0, /* max */ 1); diff --git a/src/subcommand/log_subcommand.cpp b/src/subcommand/log_subcommand.cpp index 856ba7d..f5bc56b 100644 --- a/src/subcommand/log_subcommand.cpp +++ b/src/subcommand/log_subcommand.cpp @@ -78,7 +78,7 @@ void print_commit(const commit_wrapper& commit, std::string m_format_flag) print_time(author.when(), "Date:\t"); } } - std::cout << "\n " << git_commit_message(commit) << "\n" << std::endl; + std::cout << "\n " << commit.message() << "\n" << std::endl; } void log_subcommand::run() diff --git a/src/subcommand/tag_subcommand.cpp b/src/subcommand/tag_subcommand.cpp new file mode 100644 index 0000000..4dc52c1 --- /dev/null +++ b/src/subcommand/tag_subcommand.cpp @@ -0,0 +1,295 @@ +#include + +#include "../subcommand/tag_subcommand.hpp" +#include "../wrapper/commit_wrapper.hpp" +#include "../wrapper/tag_wrapper.hpp" + +tag_subcommand::tag_subcommand(const libgit2_object&, CLI::App& app) +{ + auto* sub = app.add_subcommand("tag", "Create, list, delete or verify tags"); + + sub->add_flag("-l,--list", m_list_flag, "List tags. With optional ."); + sub->add_flag("-f,--force", m_force_flag, "Replace an existing tag with the given name (instead of failing)"); + sub->add_option("-d,--delete", m_delete, "Delete existing tags with the given names."); + sub->add_option("-n", m_num_lines, " specifies how many lines from the annotation, if any, are printed when using -l. Implies --list."); + sub->add_option("-m,--message", m_message, "Tag message for annotated tags"); + sub->add_option("", m_tag_name, "Tag name"); + sub->add_option("", m_target, "Target commit (defaults to HEAD)"); + + sub->callback([this]() { this->run(); }); +} + +// Tag listing: Print individual message lines +void print_list_lines(const std::string& message, int num_lines) +{ + if (message.empty()) + { + return; + } + + size_t pos = 0; + int num = num_lines - 1; // TODO: check with git re. "- 1" + + /** first line - headline */ + size_t newline_pos = message.find('\n', pos); + if (newline_pos != std::string::npos) + { + std::cout << message.substr(pos, newline_pos - pos); + pos = newline_pos; + } + else + { + std::cout << message << std::endl; + return; + } + + /** skip over new lines */ + while (pos < message.length() && message[pos] == '\n') + { + pos++; + } + + std::cout << std::endl; + + /** print just headline? */ + if (num == 0) + { + return; + } + if (pos < message.length() && pos + 1 < message.length()) + { + std::cout << std::endl; + } + + /** print individual commit/tag lines */ + while (pos < message.length() && num >= 2) + { + std::cout << " "; + + newline_pos = message.find('\n', pos); + if (newline_pos != std::string::npos) + { + std::cout << message.substr(pos, newline_pos - pos); + pos = newline_pos; + } + else + { + std::cout << message.substr(pos); + break; + } + + // Handle consecutive newlines + if (pos + 1 < message.length() && + message[pos] == '\n' && message[pos + 1] == '\n') + { + num--; + std::cout << std::endl; + } + + while (pos < message.length() && message[pos] == '\n') + { + pos++; + } + + std::cout << std::endl; + num--; + } +} + +// Tag listing: Print an actual tag object +void print_tag(git_tag* tag, int num_lines) +{ + std::cout << std::left << std::setw(16) << git_tag_name(tag); + + if (num_lines) + { + std::string msg = git_tag_message(tag); + if (!msg.empty()) + { + print_list_lines(msg, num_lines); + } + else + { + std::cout << std::endl; + } + } + else + { + std::cout << std::endl; + } +} + +// Tag listing: Print a commit (target of a lightweight tag) +void print_commit(git_commit* commit, std::string name, int num_lines) +{ + std::cout << std::left << std::setw(16) << name; + + if (num_lines) + { + std::string msg = git_commit_message(commit); + if (!msg.empty()) + { + print_list_lines(msg, num_lines); + } + else + { + std::cout < + +#include "../utils/common.hpp" +#include "../wrapper/repository_wrapper.hpp" + +class tag_subcommand +{ +public: + + explicit tag_subcommand(const libgit2_object&, CLI::App& app); + + void run(); + +private: + + void list_tags(repository_wrapper& repo); + void delete_tag(repository_wrapper& repo); + void create_lightweight_tag(repository_wrapper& repo); + void create_tag(repository_wrapper& repo); + + std::string m_delete; + std::string m_message; + std::string m_tag_name; + std::string m_target; + bool m_list_flag = false; + bool m_force_flag = false; + int m_num_lines = 0; +}; diff --git a/src/utils/common.cpp b/src/utils/common.cpp index 1df06e6..ae541a3 100644 --- a/src/utils/common.cpp +++ b/src/utils/common.cpp @@ -5,6 +5,8 @@ #include #include +#include + #include "common.hpp" #include "git_exception.hpp" @@ -103,6 +105,11 @@ void git_strarray_wrapper::init_str_array() } } +size_t git_strarray_wrapper::size() +{ + return m_patterns.size(); +} + std::string read_file(const std::string& path) { std::ifstream file(path, std::ios::binary); diff --git a/src/utils/common.hpp b/src/utils/common.hpp index be9f360..54c5643 100644 --- a/src/utils/common.hpp +++ b/src/utils/common.hpp @@ -57,6 +57,8 @@ class git_strarray_wrapper operator git_strarray*(); + size_t size(); + private: std::vector m_patterns; git_strarray m_array; diff --git a/src/wrapper/commit_wrapper.cpp b/src/wrapper/commit_wrapper.cpp index 33efa9f..fc214cc 100644 --- a/src/wrapper/commit_wrapper.cpp +++ b/src/wrapper/commit_wrapper.cpp @@ -28,6 +28,11 @@ std::string commit_wrapper::commit_oid_tostr() const return git_oid_tostr(buf, sizeof(buf), &this->oid()); } +std::string commit_wrapper::message() const +{ + return git_commit_message(*this); +} + std::string commit_wrapper::summary() const { return git_commit_summary(*this); diff --git a/src/wrapper/commit_wrapper.hpp b/src/wrapper/commit_wrapper.hpp index 4fe280f..0db1066 100644 --- a/src/wrapper/commit_wrapper.hpp +++ b/src/wrapper/commit_wrapper.hpp @@ -24,6 +24,7 @@ class commit_wrapper : public wrapper_base const git_oid& oid() const; std::string commit_oid_tostr() const; + std::string message() const; std::string summary() const; commit_list_wrapper get_parents_list() const; diff --git a/src/wrapper/object_wrapper.cpp b/src/wrapper/object_wrapper.cpp index bf21361..7649540 100644 --- a/src/wrapper/object_wrapper.cpp +++ b/src/wrapper/object_wrapper.cpp @@ -20,3 +20,8 @@ object_wrapper::operator git_commit*() const noexcept { return reinterpret_cast(p_resource); } + +object_wrapper::operator git_tag*() const noexcept +{ + return reinterpret_cast(p_resource); +} diff --git a/src/wrapper/object_wrapper.hpp b/src/wrapper/object_wrapper.hpp index d839ade..8faf1e1 100644 --- a/src/wrapper/object_wrapper.hpp +++ b/src/wrapper/object_wrapper.hpp @@ -18,6 +18,7 @@ class object_wrapper : public wrapper_base const git_oid& oid() const; operator git_commit*() const noexcept; + operator git_tag*() const noexcept; private: diff --git a/src/wrapper/repository_wrapper.cpp b/src/wrapper/repository_wrapper.cpp index a7fcf2b..7fb46c7 100644 --- a/src/wrapper/repository_wrapper.cpp +++ b/src/wrapper/repository_wrapper.cpp @@ -505,3 +505,20 @@ diff_wrapper repository_wrapper::diff_index_to_workdir(std::optional repository_wrapper::tag_list_match(std::string pattern) +{ + git_strarray tag_names; + throw_if_error(git_tag_list_match(&tag_names, pattern.c_str(), *this)); + + std::vector result; + for (size_t i = 0; i < tag_names.count; ++i) + { + result.emplace_back(tag_names.strings[i]); + } + + git_strarray_dispose(&tag_names); + return result; +} diff --git a/src/wrapper/repository_wrapper.hpp b/src/wrapper/repository_wrapper.hpp index 991bdc5..428f64f 100644 --- a/src/wrapper/repository_wrapper.hpp +++ b/src/wrapper/repository_wrapper.hpp @@ -6,6 +6,7 @@ #include +#include "../utils/common.hpp" #include "../utils/git_exception.hpp" #include "../wrapper/annotated_commit_wrapper.hpp" #include "../wrapper/branch_wrapper.hpp" @@ -113,6 +114,10 @@ class repository_wrapper : public wrapper_base diff_wrapper diff_tree_to_workdir_with_index(tree_wrapper old_tree, git_diff_options* diffopts); diff_wrapper diff_index_to_workdir(std::optional index, git_diff_options* diffopts); + //Tags + // git_strarray_wrapper tag_list_match(std::string pattern); + std::vector tag_list_match(std::string pattern); + private: repository_wrapper() = default; diff --git a/src/wrapper/tag_wrapper.cpp b/src/wrapper/tag_wrapper.cpp new file mode 100644 index 0000000..e385dd4 --- /dev/null +++ b/src/wrapper/tag_wrapper.cpp @@ -0,0 +1,23 @@ +#include "../wrapper/tag_wrapper.hpp" +#include + +tag_wrapper::tag_wrapper(git_tag* tag) + : base_type(tag) +{ +} + +tag_wrapper::~tag_wrapper() +{ + git_tag_free(p_resource); + p_resource = nullptr; +} + +std::string tag_wrapper::name() +{ + return git_tag_name(*this); +} + +std::string tag_wrapper::message() +{ + return git_tag_message(*this); +} diff --git a/src/wrapper/tag_wrapper.hpp b/src/wrapper/tag_wrapper.hpp new file mode 100644 index 0000000..fb78eee --- /dev/null +++ b/src/wrapper/tag_wrapper.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +#include "../wrapper/wrapper_base.hpp" + +class tag_wrapper : public wrapper_base +{ +public: + + using base_type = wrapper_base; + + ~tag_wrapper(); + + tag_wrapper(tag_wrapper&&) noexcept = default; + tag_wrapper& operator=(tag_wrapper&&) noexcept = default; + + std::string name(); + std::string message(); + +private: + + tag_wrapper(git_tag* tag); +}; diff --git a/test/test_tag.py b/test/test_tag.py new file mode 100644 index 0000000..3b5f612 --- /dev/null +++ b/test/test_tag.py @@ -0,0 +1,295 @@ +import subprocess + +import pytest + +def test_tag_list_empty(xtl_clone, git2cpp_path, tmp_path): + """Test listing tags when there are no tags.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + cmd = [git2cpp_path, 'tag'] + p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) + assert p.returncode == 0 + assert "0.2.0" in p.stdout + + +def test_tag_create_lightweight(xtl_clone, commit_env_config, git2cpp_path, tmp_path): + """Test creating a lightweight tag.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Create a lightweight tag + create_cmd = [git2cpp_path, 'tag', 'v1.0.0'] + subprocess.run(create_cmd, capture_output=True, cwd=xtl_path, text=True, check=True) + + # List tags to verify it was created + list_cmd = [git2cpp_path, 'tag'] + p_list = subprocess.run(list_cmd, capture_output=True, cwd=xtl_path, text=True) + assert p_list.returncode == 0 + assert 'v1.0.0' in p_list.stdout + + +def test_tag_create_annotated(xtl_clone, commit_env_config, git2cpp_path, tmp_path): + """Test creating an annotated tag.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Create an annotated tag + create_cmd = [git2cpp_path, 'tag', '-m', 'Release version 1.0', 'v1.0.0'] + subprocess.run(create_cmd, capture_output=True, cwd=xtl_path, text=True, check=True) + + # List tags to verify it was created + list_cmd = [git2cpp_path, 'tag', "-n", "1"] + p_list = subprocess.run(list_cmd, capture_output=True, cwd=xtl_path, text=True) + assert p_list.returncode == 0 + assert 'v1.0.0' in p_list.stdout + assert 'Release version 1.0' in p_list.stdout + + +def test_tag_create_on_specific_commit(xtl_clone, commit_env_config, git2cpp_path, tmp_path): + """Test creating a tag on a specific commit.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Get the commit SHA before creating new commit + old_head_cmd = ['git', 'rev-parse', 'HEAD'] + p_old_head = subprocess.run(old_head_cmd, capture_output=True, cwd=xtl_path, text=True) + old_head_sha = p_old_head.stdout.strip() + + # Create a commit first + file_path = xtl_path / "test_file.txt" + file_path.write_text("test content") + + add_cmd = [git2cpp_path, 'add', 'test_file.txt'] + subprocess.run(add_cmd, cwd=xtl_path, check=True) + + commit_cmd = [git2cpp_path, 'commit', '-m', 'test commit'] + subprocess.run(commit_cmd, cwd=xtl_path, check=True) + + # Get new HEAD commit SHA + new_head_cmd = ['git', 'rev-parse', 'HEAD'] + p_new_head = subprocess.run(new_head_cmd, capture_output=True, cwd=xtl_path, text=True) + new_head_sha = p_new_head.stdout.strip() + + # Verify we actually created a new commit + assert old_head_sha != new_head_sha + + # Create tag on HEAD + tag_cmd = [git2cpp_path, 'tag', 'v1.0.0', 'HEAD'] + subprocess.run(tag_cmd, capture_output=True, cwd=xtl_path, check=True) + + # Verify tag exists + list_cmd = [git2cpp_path, 'tag'] + p_list = subprocess.run(list_cmd, capture_output=True, cwd=xtl_path, text=True) + assert p_list.returncode == 0 + assert 'v1.0.0' in p_list.stdout + + # Get commit SHA that the tag points to + tag_sha_cmd = ['git', 'rev-parse', 'v1.0.0^{commit}'] + p_tag_sha = subprocess.run(tag_sha_cmd, capture_output=True, cwd=xtl_path, text=True) + tag_sha = p_tag_sha.stdout.strip() + + # Verify tag points to new HEAD, not old HEAD + assert tag_sha == new_head_sha + assert tag_sha != old_head_sha + + +def test_tag_delete(xtl_clone, commit_env_config, git2cpp_path, tmp_path): + """Test deleting a tag.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Create a tag + create_cmd = [git2cpp_path, 'tag', 'v1.0.0'] + subprocess.run(create_cmd, capture_output=True, cwd=xtl_path, text=True, check=True) + + # Delete the tag + delete_cmd = [git2cpp_path, 'tag', '-d', 'v1.0.0'] + p_delete = subprocess.run(delete_cmd, capture_output=True, cwd=xtl_path, text=True) + assert p_delete.returncode == 0 + assert "Deleted tag 'v1.0.0'" in p_delete.stdout + + # Verify tag is gone + list_cmd = [git2cpp_path, 'tag'] + p_list = subprocess.run(list_cmd, capture_output=True, cwd=xtl_path, text=True) + assert p_list.returncode == 0 + assert 'v1.0.0' not in p_list.stdout + + +def test_tag_delete_nonexistent(xtl_clone, git2cpp_path, tmp_path): + """Test deleting a tag that doesn't exist.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Try to delete non-existent tag + delete_cmd = [git2cpp_path, 'tag', '-d', 'nonexistent'] + p_delete = subprocess.run(delete_cmd, capture_output=True, cwd=xtl_path, text=True) + assert p_delete.returncode != 0 + assert "not found" in p_delete.stderr + + +@pytest.mark.parametrize("list_flag", ["-l", "--list"]) +def test_tag_list_with_flag(xtl_clone, commit_env_config, git2cpp_path, tmp_path, list_flag): + """Test listing tags with -l or --list flag.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Create a tag + tag_cmd = [git2cpp_path, 'tag', 'v1.0.0'] + subprocess.run(tag_cmd, capture_output=True, cwd=xtl_path, text=True) + + # List tags + list_cmd = [git2cpp_path, 'tag', list_flag] + p_list = subprocess.run(list_cmd, capture_output=True, cwd=xtl_path, text=True) + assert p_list.returncode == 0 + assert 'v1.0.0' in p_list.stdout + + +def test_tag_list_with_pattern(xtl_clone, commit_env_config, git2cpp_path, tmp_path): + """Test listing tags with a pattern.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Create tags with different prefixes + tag_cmd_1 = [git2cpp_path, 'tag', 'v1.0.0'] + subprocess.run(tag_cmd_1, capture_output=True, cwd=xtl_path, text=True) + + tag_cmd_2 = [git2cpp_path, 'tag', 'v1.0.1'] + subprocess.run(tag_cmd_2, capture_output=True, cwd=xtl_path, text=True) + + tag_cmd_3 = [git2cpp_path, 'tag', 'release-1.0'] + subprocess.run(tag_cmd_3, capture_output=True, cwd=xtl_path, text=True) + + # List only tags matching pattern + list_cmd = [git2cpp_path, 'tag', '-l', 'v1.0*'] + p_list = subprocess.run(list_cmd, capture_output=True, cwd=xtl_path, text=True) + assert p_list.returncode == 0 + assert 'v1.0.0' in p_list.stdout + assert 'v1.0.1' in p_list.stdout + assert 'release-1.0' not in p_list.stdout + + +def test_tag_list_with_message_lines(xtl_clone, commit_env_config, git2cpp_path, tmp_path): + """Test listing tags with message lines (-n flag).""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Create an annotated tag with a message + create_cmd = [git2cpp_path, 'tag', '-m', 'First line\nSecond line\nThird line', 'v1.0.0'] + p_create = subprocess.run(create_cmd, capture_output=True, cwd=xtl_path, text=True) + assert p_create.returncode == 0 + + # List tags with message lines + list_cmd = [git2cpp_path, 'tag', '-n', '3', '-l'] + p_list = subprocess.run(list_cmd, capture_output=True, cwd=xtl_path, text=True) + assert p_list.returncode == 0 + assert 'v1.0.0' in p_list.stdout + # TODO: another assert after checking what should be printed with git + + +@pytest.mark.parametrize("force_flag", ["-f", "--force"]) +def test_tag_force_replace(xtl_clone, commit_env_config, git2cpp_path, tmp_path, force_flag): + """Test replacing an existing tag with -f or --force flag.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Create initial tag + create_cmd_1 = [git2cpp_path, 'tag', 'v1.0.0'] + subprocess.run(create_cmd_1, capture_output=True, cwd=xtl_path, text=True, check=True) + + # Try to create same tag without force (should fail) + create_cmd_2 = [git2cpp_path, 'tag', 'v1.0.0'] + p_create_2 = subprocess.run(create_cmd_2, capture_output=True, cwd=xtl_path) + assert p_create_2.returncode != 0 + + # Create same tag with force (should succeed) + create_cmd_3 = [git2cpp_path, 'tag', force_flag, 'v1.0.0'] + p_create_3 = subprocess.run(create_cmd_3, capture_output=True, cwd=xtl_path, text=True) + assert p_create_3.returncode == 0 + + +def test_tag_nogit(git2cpp_path, tmp_path): + """Test tag command outside a git repository.""" + cmd = [git2cpp_path, 'tag'] + p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) + assert p.returncode != 0 + + +def test_tag_annotated_no_message(xtl_clone, commit_env_config, git2cpp_path, tmp_path): + """Test creating an annotated tag without a message should fail.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Create a commit with a known message + file_path = xtl_path / "test_file.txt" + file_path.write_text("test content") + + add_cmd = [git2cpp_path, 'add', 'test_file.txt'] + subprocess.run(add_cmd, cwd=xtl_path, check=True) + + commit_cmd = [git2cpp_path, 'commit', '-m', 'my specific commit message'] + subprocess.run(commit_cmd, cwd=xtl_path, check=True) + + # Create tag with empty message (should create lightweight tag) + create_cmd = [git2cpp_path, 'tag', '-m', '', 'v1.0.0'] + subprocess.run(create_cmd, capture_output=True, cwd=xtl_path, check=True) + + # List tag with messages - lightweight tag shows commit message + list_cmd = [git2cpp_path, 'tag', '-n', '1'] + p_list = subprocess.run(list_cmd, capture_output=True, cwd=xtl_path, text=True) + assert p_list.returncode == 0 + assert 'v1.0.0' in p_list.stdout + # Lightweight tag shows the commit message, not a tag message + assert 'my specific commit message' in p_list.stdout + + +def test_tag_multiple_create_and_list(xtl_clone, commit_env_config, git2cpp_path, tmp_path): + """Test creating multiple tags and listing them.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Create multiple tags + tags = ['v1.0.0', 'v1.0.1', 'v1.1.0', 'v2.0.0'] + for tag in tags: + create_cmd = [git2cpp_path, 'tag', tag] + subprocess.run(create_cmd, capture_output=True, cwd=xtl_path, check=True) + + # List all tags + list_cmd = [git2cpp_path, 'tag'] + p_list = subprocess.run(list_cmd, capture_output=True, cwd=xtl_path, text=True) + assert p_list.returncode == 0 + + # Verify all tags are in the list + for tag in tags: + assert tag in p_list.stdout + + +def test_tag_on_new_commit(xtl_clone, commit_env_config, git2cpp_path, tmp_path): + """Test creating tags on new commits.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Tag the current commit + tag_cmd_1 = [git2cpp_path, 'tag', 'before-change'] + subprocess.run(tag_cmd_1, cwd=xtl_path, check=True) + + # Make a new commit + file_path = xtl_path / "new_file.txt" + file_path.write_text("new content") + + add_cmd = [git2cpp_path, 'add', 'new_file.txt'] + subprocess.run(add_cmd, cwd=xtl_path, check=True) + + commit_cmd = [git2cpp_path, 'commit', '-m', 'new commit'] + subprocess.run(commit_cmd, cwd=xtl_path, check=True) + + # Tag the new commit + tag_cmd_2 = [git2cpp_path, 'tag', 'after-change'] + subprocess.run(tag_cmd_2, cwd=xtl_path, check=True) + + # List tags + list_cmd = [git2cpp_path, 'tag'] + p_list = subprocess.run(list_cmd, capture_output=True, cwd=xtl_path, text=True) + assert p_list.returncode == 0 + assert 'before-change' in p_list.stdout + assert 'after-change' in p_list.stdout