From 81992bd29261ff1cec4e3347222ad9d1cc61c28b Mon Sep 17 00:00:00 2001 From: Sandrine Pataut Date: Tue, 24 Feb 2026 10:11:22 +0100 Subject: [PATCH 1/3] Add tracking info --- src/subcommand/log_subcommand.cpp | 183 +++++++++++++++++++++++++++- test/test_log.py | 194 ++++++++++++++++++++++++++++++ 2 files changed, 372 insertions(+), 5 deletions(-) diff --git a/src/subcommand/log_subcommand.cpp b/src/subcommand/log_subcommand.cpp index f5bc56b..1ef3496 100644 --- a/src/subcommand/log_subcommand.cpp +++ b/src/subcommand/log_subcommand.cpp @@ -1,8 +1,11 @@ +#include #include #include -#include +#include +#include #include #include +#include #include @@ -50,7 +53,167 @@ void print_time(git_time intime, std::string prefix) std::cout << prefix << out << " " << sign << std::format("{:02d}", hours) << std::format("{:02d}", minutes) < get_tags_for_commit(repository_wrapper& repo, const git_oid* commit_oid) +{ + std::vector tags; + git_strarray tag_names = {0}; + + if (git_tag_list(&tag_names, repo) != 0) + { + return tags; + } + + for (size_t i = 0; i < tag_names.count; i++) + { + std::string tag_name = tag_names.strings[i]; + std::string ref_name = "refs/tags/" + std::string(tag_name); + + reference_wrapper tag_ref = repo.find_reference(ref_name); + object_wrapper peeled = tag_ref.peel(); + + if (git_oid_equal(&peeled.oid(), commit_oid)) + { + tags.push_back(std::string(tag_name)); + } + } + + git_strarray_dispose(&tag_names); + return tags; +} + +std::vector get_branches_for_commit(repository_wrapper& repo, git_branch_t type, const git_oid* commit_oid, const std::string exclude_branch) +{ + std::vector branches; + + auto branch_iter = repo.iterate_branches(type); + while (auto branch = branch_iter.next()) + { + const git_oid* branch_target = nullptr; + git_reference* ref = branch.value(); + + if (git_reference_type(ref) == GIT_REFERENCE_DIRECT) + { + branch_target = git_reference_target(ref); + } + else if (git_reference_type(ref) == GIT_REFERENCE_SYMBOLIC) + { + git_reference* resolved = nullptr; + if (git_reference_resolve(&resolved, ref) == 0) + { + branch_target = git_reference_target(resolved); + git_reference_free(resolved); + } + } + + if (branch_target && git_oid_equal(branch_target, commit_oid)) + { + std::string branch_name; + branch_name = branch->name(); + if (type == GIT_BRANCH_LOCAL) + { + if (branch_name != exclude_branch) + { + branches.push_back(branch_name); + } + } + else + { + branches.push_back(branch_name); + } + } + } + + return branches; +} + +struct commit_refs +{ + std::string head_branch; + std::vector tags; + std::vector local_branches; + std::vector remote_branches; + + bool has_refs() const { + return !head_branch.empty() || !tags.empty() || + !local_branches.empty() || !remote_branches.empty(); + } +}; + +commit_refs get_refs_for_commit(repository_wrapper& repo, const git_oid* commit_oid) +{ + commit_refs refs; + + if (!repo.is_head_unborn()) + { + auto head = repo.head(); + auto head_taget = head.target(); + if (git_oid_equal(head_taget, commit_oid)) + { + refs.head_branch = head.short_name(); + } + } + + refs.tags = get_tags_for_commit(repo, commit_oid); + refs.local_branches = get_branches_for_commit(repo, GIT_BRANCH_LOCAL, commit_oid, refs.head_branch); + refs.remote_branches = get_branches_for_commit(repo, GIT_BRANCH_REMOTE, commit_oid, ""); + + return refs; +} + +void print_refs(commit_refs refs) +{ + if (!refs.has_refs()) + { + return; + } + + std::cout << " ("; + + bool first = true; + + if (!refs.head_branch.empty()) + { + std::cout << termcolor::bold << termcolor::cyan << "HEAD" << termcolor::reset + << termcolor::yellow << " -> " << termcolor::reset + << termcolor::bold << termcolor::green << refs.head_branch << termcolor::reset + << termcolor::yellow; + first = false; + } + + for (const auto& tag :refs.tags) + { + if (!first) + { + std::cout << ", "; + } + std::cout << termcolor::bold << "tag: " << tag << termcolor::reset << termcolor::yellow; + first = false; + } + + for (const auto& remote : refs.remote_branches) + { + if (!first) + { + std::cout << ", "; + } + std::cout << termcolor::bold << termcolor::red << remote << termcolor::reset << termcolor::yellow; + first = false; + } + + for (const auto& local : refs.local_branches) + { + if (!first) + { + std::cout << ", "; + } + std::cout << termcolor::bold << termcolor::green << local << termcolor::reset << termcolor::yellow; + first = false; + } + + std::cout << ")" << termcolor::reset; +} + +void print_commit(repository_wrapper& repo, const commit_wrapper& commit, std::string m_format_flag) { std::string buf = commit.commit_oid_tostr(); @@ -58,7 +221,13 @@ void print_commit(const commit_wrapper& commit, std::string m_format_flag) signature_wrapper committer = signature_wrapper::get_commit_committer(commit); stream_colour_fn colour = termcolor::yellow; - std::cout << colour << "commit " << buf << termcolor::reset << std::endl; + std::cout << colour << "commit " << buf; + + commit_refs refs = get_refs_for_commit(repo, &commit.oid()); + print_refs(refs); + + std::cout << termcolor::reset << std::endl; + if (m_format_flag=="fuller") { std::cout << "Author:\t " << author.name() << " " << author.email() << std::endl; @@ -78,7 +247,7 @@ void print_commit(const commit_wrapper& commit, std::string m_format_flag) print_time(author.when(), "Date:\t"); } } - std::cout << "\n " << commit.message() << "\n" << std::endl; + std::cout << "\n " << commit.message(); } void log_subcommand::run() @@ -102,8 +271,12 @@ void log_subcommand::run() git_oid commit_oid; while (!walker.next(commit_oid) && i 2 else: assert p_log.stdout.count("Author") == 2 + + +def test_log_with_head_reference(xtl_clone, commit_env_config, git2cpp_path, tmp_path): + """Test that HEAD reference is shown on the latest commit.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Create a new commit + p = xtl_path / "test_file.txt" + p.write_text("test content") + + subprocess.run([git2cpp_path, "add", "test_file.txt"], cwd=xtl_path, check=True) + subprocess.run( + [git2cpp_path, "commit", "-m", "test commit"], cwd=xtl_path, check=True + ) + + # Run log with max count 1 to get only the latest commit + p_log = subprocess.run( + [git2cpp_path, "log", "-n", "1"], capture_output=True, cwd=xtl_path, text=True + ) + assert p_log.returncode == 0 + + # Check that HEAD reference is shown + assert "HEAD ->" in p_log.stdout + assert "master" in p_log.stdout or "main" in p_log.stdout + + +def test_log_with_tag(xtl_clone, commit_env_config, git2cpp_path, tmp_path): + """Test that tags are shown in log output.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Create a commit and tag it + p = xtl_path / "tagged_file.txt" + p.write_text("tagged content") + + subprocess.run([git2cpp_path, "add", "tagged_file.txt"], cwd=xtl_path, check=True) + subprocess.run( + [git2cpp_path, "commit", "-m", "tagged commit"], cwd=xtl_path, check=True + ) + + # Create a tag (using git command since git2cpp might not have tag creation yet) + subprocess.run(["git", "tag", "v1.0.0"], cwd=xtl_path, check=True) + + # Run log + p_log = subprocess.run( + [git2cpp_path, "log", "-n", "1"], capture_output=True, cwd=xtl_path, text=True + ) + assert p_log.returncode == 0 + + # Check that tag is shown + assert "tag: v1.0.0" in p_log.stdout + + +def test_log_with_multiple_tags(xtl_clone, commit_env_config, git2cpp_path, tmp_path): + """Test that multiple tags on the same commit are all shown.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Create a commit + p = xtl_path / "multi_tag_file.txt" + p.write_text("content") + + subprocess.run( + [git2cpp_path, "add", "multi_tag_file.txt"], cwd=xtl_path, check=True + ) + subprocess.run( + [git2cpp_path, "commit", "-m", "multi tag commit"], cwd=xtl_path, check=True + ) + + # Create multiple tags + subprocess.run(["git", "tag", "v1.0.0"], cwd=xtl_path, check=True) + subprocess.run(["git", "tag", "stable"], cwd=xtl_path, check=True) + subprocess.run(["git", "tag", "latest"], cwd=xtl_path, check=True) + + # Run log + p_log = subprocess.run( + [git2cpp_path, "log", "-n", "1"], capture_output=True, cwd=xtl_path, text=True + ) + assert p_log.returncode == 0 + + # Check that all tags are shown + assert "tag: v1.0.0" in p_log.stdout + assert "tag: stable" in p_log.stdout + assert "tag: latest" in p_log.stdout + + +def test_log_with_annotated_tag(xtl_clone, commit_env_config, git2cpp_path, tmp_path): + """Test that annotated tags are shown in log output.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Create a commit + p = xtl_path / "annotated_tag_file.txt" + p.write_text("content") + + subprocess.run( + [git2cpp_path, "add", "annotated_tag_file.txt"], cwd=xtl_path, check=True + ) + subprocess.run( + [git2cpp_path, "commit", "-m", "annotated tag commit"], cwd=xtl_path, check=True + ) + + # Create an annotated tag + subprocess.run( + ["git", "tag", "-a", "v2.0.0", "-m", "Version 2.0.0"], cwd=xtl_path, check=True + ) + + # Run log + p_log = subprocess.run( + [git2cpp_path, "log", "-n", "1"], capture_output=True, cwd=xtl_path, text=True + ) + assert p_log.returncode == 0 + + # Check that annotated tag is shown + assert "tag: v2.0.0" in p_log.stdout + + +def test_log_with_branch(xtl_clone, commit_env_config, git2cpp_path, tmp_path): + """Test that branches are shown in log output.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Create a commit + p = xtl_path / "branch_file.txt" + p.write_text("content") + + subprocess.run([git2cpp_path, "add", "branch_file.txt"], cwd=xtl_path, check=True) + subprocess.run( + [git2cpp_path, "commit", "-m", "branch commit"], cwd=xtl_path, check=True + ) + + # Create a new branch pointing to HEAD + subprocess.run(["git", "branch", "feature-branch"], cwd=xtl_path, check=True) + + # Run log + p_log = subprocess.run( + [git2cpp_path, "log", "-n", "1"], capture_output=True, cwd=xtl_path, text=True + ) + assert p_log.returncode == 0 + + # Check that both branches are shown (HEAD -> master/main and feature-branch) + assert "feature-branch" in p_log.stdout + + +def test_log_with_remote_branches(xtl_clone, commit_env_config, git2cpp_path, tmp_path): + """Test that remote branches are shown in log output.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # The xtl_clone fixture already has remote branches (origin/master, etc.) + # Run log to check they appear + p_log = subprocess.run( + [git2cpp_path, "log", "-n", "1"], capture_output=True, cwd=xtl_path, text=True + ) + assert p_log.returncode == 0 + + # Check that origin remote branches are shown + assert "origin/master" in p_log.stdout + + +def test_log_commit_without_references( + xtl_clone, commit_env_config, git2cpp_path, tmp_path +): + """Test that commits without any references don't show empty parentheses.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + # Create two commits - the second one will have refs, the first won't + for i in range(2): + p = xtl_path / f"file_{i}.txt" + p.write_text(f"content {i}") + subprocess.run([git2cpp_path, "add", f"file_{i}.txt"], cwd=xtl_path, check=True) + subprocess.run( + [git2cpp_path, "commit", "-m", f"commit {i}"], cwd=xtl_path, check=True + ) + + # Run log with 2 commits + p_log = subprocess.run( + [git2cpp_path, "log", "-n", "2"], capture_output=True, cwd=xtl_path, text=True + ) + assert p_log.returncode == 0 + + # First commit line should have references + lines = p_log.stdout.split("\n") + first_commit_line = [l for l in lines if l.startswith("commit")][0] + assert "(" in first_commit_line # Has references + + # Second commit (older one) should not have empty parentheses + second_commit_line = [l for l in lines if l.startswith("commit")][1] + # Should either have no parentheses or have actual references + if "(" in second_commit_line: + # If it has parentheses, they shouldn't be empty + assert "()" not in second_commit_line From 807cb093bff5f9a73edea2750ecf8b84b1f9ef34 Mon Sep 17 00:00:00 2001 From: Sandrine Pataut Date: Fri, 27 Feb 2026 10:32:52 +0100 Subject: [PATCH 2/3] address review comments --- src/subcommand/log_subcommand.cpp | 32 +++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/subcommand/log_subcommand.cpp b/src/subcommand/log_subcommand.cpp index 1ef3496..8eec2c3 100644 --- a/src/subcommand/log_subcommand.cpp +++ b/src/subcommand/log_subcommand.cpp @@ -1,9 +1,9 @@ -#include #include #include #include #include #include +#include #include #include @@ -77,11 +77,11 @@ std::vector get_tags_for_commit(repository_wrapper& repo, const git } } - git_strarray_dispose(&tag_names); + git_strarray_dispose(&tag_names); // TODO: refactor git_strarray_wrapper to use it here return tags; } -std::vector get_branches_for_commit(repository_wrapper& repo, git_branch_t type, const git_oid* commit_oid, const std::string exclude_branch) +std::vector get_branches_for_commit(repository_wrapper& repo, git_branch_t type, const git_oid& commit_oid, const std::string exclude_branch) { std::vector branches; @@ -105,7 +105,7 @@ std::vector get_branches_for_commit(repository_wrapper& repo, git_b } } - if (branch_target && git_oid_equal(branch_target, commit_oid)) + if (branch_target && git_oid_equal(branch_target, &commit_oid)) { std::string branch_name; branch_name = branch->name(); @@ -139,7 +139,7 @@ struct commit_refs } }; -commit_refs get_refs_for_commit(repository_wrapper& repo, const git_oid* commit_oid) +commit_refs get_refs_for_commit(repository_wrapper& repo, const git_oid& commit_oid) { commit_refs refs; @@ -147,20 +147,20 @@ commit_refs get_refs_for_commit(repository_wrapper& repo, const git_oid* commit_ { auto head = repo.head(); auto head_taget = head.target(); - if (git_oid_equal(head_taget, commit_oid)) + if (git_oid_equal(head_taget, &commit_oid)) { refs.head_branch = head.short_name(); } } - refs.tags = get_tags_for_commit(repo, commit_oid); + refs.tags = get_tags_for_commit(repo, &commit_oid); refs.local_branches = get_branches_for_commit(repo, GIT_BRANCH_LOCAL, commit_oid, refs.head_branch); refs.remote_branches = get_branches_for_commit(repo, GIT_BRANCH_REMOTE, commit_oid, ""); return refs; } -void print_refs(commit_refs refs) +void print_refs(const commit_refs& refs) { if (!refs.has_refs()) { @@ -223,7 +223,7 @@ void print_commit(repository_wrapper& repo, const commit_wrapper& commit, std::s stream_colour_fn colour = termcolor::yellow; std::cout << colour << "commit " << buf; - commit_refs refs = get_refs_for_commit(repo, &commit.oid()); + commit_refs refs = get_refs_for_commit(repo, commit.oid()); print_refs(refs); std::cout << termcolor::reset << std::endl; @@ -247,7 +247,19 @@ void print_commit(repository_wrapper& repo, const commit_wrapper& commit, std::s print_time(author.when(), "Date:\t"); } } - std::cout << "\n " << commit.message(); + + std::string message = commit.message(); + while (!message.empty() && message.back() == '\n') + { + message.pop_back(); + } + std::istringstream message_stream(message); + std::string line; + while (std::getline(message_stream, line)) + { + std::cout << "\n " << line; + } + std::cout << std::endl; } void log_subcommand::run() From 8e58bbc38eb8ffad926bb1366facc3baaadd052c Mon Sep 17 00:00:00 2001 From: Sandrine Pataut Date: Fri, 27 Feb 2026 15:41:02 +0100 Subject: [PATCH 3/3] address review comments --- src/subcommand/log_subcommand.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/subcommand/log_subcommand.cpp b/src/subcommand/log_subcommand.cpp index 8eec2c3..1c7fff0 100644 --- a/src/subcommand/log_subcommand.cpp +++ b/src/subcommand/log_subcommand.cpp @@ -53,7 +53,7 @@ void print_time(git_time intime, std::string prefix) std::cout << prefix << out << " " << sign << std::format("{:02d}", hours) << std::format("{:02d}", minutes) < get_tags_for_commit(repository_wrapper& repo, const git_oid* commit_oid) +std::vector get_tags_for_commit(repository_wrapper& repo, const git_oid& commit_oid) { std::vector tags; git_strarray tag_names = {0}; @@ -71,7 +71,7 @@ std::vector get_tags_for_commit(repository_wrapper& repo, const git reference_wrapper tag_ref = repo.find_reference(ref_name); object_wrapper peeled = tag_ref.peel(); - if (git_oid_equal(&peeled.oid(), commit_oid)) + if (git_oid_equal(&peeled.oid(), &commit_oid)) { tags.push_back(std::string(tag_name)); } @@ -107,8 +107,7 @@ std::vector get_branches_for_commit(repository_wrapper& repo, git_b if (branch_target && git_oid_equal(branch_target, &commit_oid)) { - std::string branch_name; - branch_name = branch->name(); + std::string branch_name(branch->name()); if (type == GIT_BRANCH_LOCAL) { if (branch_name != exclude_branch) @@ -153,7 +152,7 @@ commit_refs get_refs_for_commit(repository_wrapper& repo, const git_oid& commit_ } } - refs.tags = get_tags_for_commit(repo, &commit_oid); + refs.tags = get_tags_for_commit(repo, commit_oid); refs.local_branches = get_branches_for_commit(repo, GIT_BRANCH_LOCAL, commit_oid, refs.head_branch); refs.remote_branches = get_branches_for_commit(repo, GIT_BRANCH_REMOTE, commit_oid, "");