From c1d9c0e685bb294421f38f3e046648013bf05263 Mon Sep 17 00:00:00 2001 From: Sandrine Pataut Date: Fri, 26 Dec 2025 15:28:10 +0100 Subject: [PATCH 1/3] Add rev-list --- CMakeLists.txt | 2 ++ src/main.cpp | 4 ++- src/subcommand/revlist_subcommand.cpp | 49 +++++++++++++++++++++++++++ src/subcommand/revlist_subcommand.hpp | 20 +++++++++++ test/test_revlist.py | 19 +++++++++++ 5 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/subcommand/revlist_subcommand.cpp create mode 100644 src/subcommand/revlist_subcommand.hpp create mode 100644 test/test_revlist.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 1165dbd..90b0194 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,6 +64,8 @@ set(GIT2CPP_SRC ${GIT2CPP_SOURCE_DIR}/subcommand/remote_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/subcommand/reset_subcommand.cpp ${GIT2CPP_SOURCE_DIR}/subcommand/reset_subcommand.hpp + ${GIT2CPP_SOURCE_DIR}/subcommand/revlist_subcommand.cpp + ${GIT2CPP_SOURCE_DIR}/subcommand/revlist_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/subcommand/revparse_subcommand.cpp ${GIT2CPP_SOURCE_DIR}/subcommand/revparse_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/subcommand/status_subcommand.cpp diff --git a/src/main.cpp b/src/main.cpp index bbdbcef..9a3012b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,6 +19,7 @@ #include "subcommand/reset_subcommand.hpp" #include "subcommand/status_subcommand.hpp" #include "subcommand/revparse_subcommand.hpp" +#include "subcommand/revlist_subcommand.hpp" int main(int argc, char** argv) { @@ -45,7 +46,8 @@ int main(int argc, char** argv) merge_subcommand merge(lg2_obj, app); push_subcommand push(lg2_obj, app); remote_subcommand remote(lg2_obj, app); - revparse_subcommand rev(lg2_obj, app); + revparse_subcommand revparse(lg2_obj, app); + revlist_subcommand revlist(lg2_obj, app); app.require_subcommand(/* min */ 0, /* max */ 1); diff --git a/src/subcommand/revlist_subcommand.cpp b/src/subcommand/revlist_subcommand.cpp new file mode 100644 index 0000000..09fbfa8 --- /dev/null +++ b/src/subcommand/revlist_subcommand.cpp @@ -0,0 +1,49 @@ +#include "revlist_subcommand.hpp" +#include "../wrapper/repository_wrapper.hpp" +// #include +// #include + +revlist_subcommand::revlist_subcommand(const libgit2_object&, CLI::App& app) +{ + auto* sub = app.add_subcommand("rev-list", "Lists commit objects in reverse chronological order"); + + sub->add_option("", m_commit, ""); + sub->add_option("-n,--max-count", m_max_count_flag, "Limit the output to commits."); + + sub->callback([this]() { this->run(); }); +} + +void revlist_subcommand::run() +{ + if (m_commit.empty()) + { + throw std::runtime_error("usage: git rev-list [] ... [--] [...]"); // TODO: add help info + } + + auto directory = get_current_git_path(); + auto repo = repository_wrapper::open(directory); + git_oid start_commit_oid; + int not_sha1 = git_oid_fromstrp(&start_commit_oid, m_commit.c_str()); + if (not_sha1) + { + commit_wrapper start_commit = repo.find_commit(m_commit); + start_commit_oid = start_commit.oid(); + } + + git_revwalk* walker; + git_revwalk_new(&walker, repo); + git_revwalk_push_head(walker); + + std::size_t i=0; + git_oid commit_oid; + char buf[GIT_OID_SHA1_HEXSIZE + 1]; + while (!git_revwalk_next(&commit_oid, walker) && i +#include + +#include "../utils/common.hpp" + +class revlist_subcommand +{ +public: + + explicit revlist_subcommand(const libgit2_object&, CLI::App& app); + void run(); + +private: + + std::string m_commit; + int m_max_count_flag=std::numeric_limits::max(); + +}; diff --git a/test/test_revlist.py b/test/test_revlist.py new file mode 100644 index 0000000..302bd5b --- /dev/null +++ b/test/test_revlist.py @@ -0,0 +1,19 @@ +import subprocess + +import pytest + + +def test_revlist(xtl_clone, git_config, git2cpp_path, tmp_path, monkeypatch): + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + cmd = [ + git2cpp_path, + "rev-list", + "35955995424eb9699bb604b988b5270253b1fccc", + "--max-count", + "4", + ] + p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) + assert p.returncode == 0 + assert "da1754dd6" in p.stdout From 70ee6b976fb4ad8d17d1ab0a2ee02ce4ce4fcb89 Mon Sep 17 00:00:00 2001 From: Sandrine Pataut Date: Tue, 6 Jan 2026 12:03:16 +0100 Subject: [PATCH 2/3] Add revwalk wrapper --- CMakeLists.txt | 2 ++ src/subcommand/log_subcommand.cpp | 9 +++----- src/subcommand/revlist_subcommand.cpp | 12 ++++------- src/wrapper/repository_wrapper.cpp | 14 ++++++------ src/wrapper/repository_wrapper.hpp | 3 +++ src/wrapper/revwalk_wrapper.cpp | 31 +++++++++++++++++++++++++++ src/wrapper/revwalk_wrapper.hpp | 29 +++++++++++++++++++++++++ test/test_revlist.py | 3 ++- 8 files changed, 81 insertions(+), 22 deletions(-) create mode 100644 src/wrapper/revwalk_wrapper.cpp create mode 100644 src/wrapper/revwalk_wrapper.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 90b0194..36b401b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,6 +98,8 @@ set(GIT2CPP_SRC ${GIT2CPP_SOURCE_DIR}/wrapper/remote_wrapper.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/repository_wrapper.cpp ${GIT2CPP_SOURCE_DIR}/wrapper/repository_wrapper.hpp + ${GIT2CPP_SOURCE_DIR}/wrapper/revwalk_wrapper.cpp + ${GIT2CPP_SOURCE_DIR}/wrapper/revwalk_wrapper.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/signature_wrapper.cpp ${GIT2CPP_SOURCE_DIR}/wrapper/signature_wrapper.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/status_wrapper.cpp diff --git a/src/subcommand/log_subcommand.cpp b/src/subcommand/log_subcommand.cpp index c295c7f..18ba644 100644 --- a/src/subcommand/log_subcommand.cpp +++ b/src/subcommand/log_subcommand.cpp @@ -87,22 +87,19 @@ void log_subcommand::run() auto repo = repository_wrapper::open(directory); // auto branch_name = repo.head().short_name(); - git_revwalk* walker; - git_revwalk_new(&walker, repo); - git_revwalk_push_head(walker); + revwalk_wrapper walker = repo.new_walker(); + walker.push_head(); terminal_pager pager; std::size_t i=0; git_oid commit_oid; - while (!git_revwalk_next(&commit_oid, walker) && i -// #include +#include "../wrapper/revwalk_wrapper.hpp" revlist_subcommand::revlist_subcommand(const libgit2_object&, CLI::App& app) { @@ -30,20 +29,17 @@ void revlist_subcommand::run() start_commit_oid = start_commit.oid(); } - git_revwalk* walker; - git_revwalk_new(&walker, repo); - git_revwalk_push_head(walker); + revwalk_wrapper walker = repo.new_walker(); + walker.push(start_commit_oid); std::size_t i=0; git_oid commit_oid; char buf[GIT_OID_SHA1_HEXSIZE + 1]; - while (!git_revwalk_next(&commit_oid, walker) && i -#include "../utils/git_exception.hpp" -#include "../wrapper/index_wrapper.hpp" -#include "../wrapper/object_wrapper.hpp" -#include "../wrapper/commit_wrapper.hpp" -#include "../wrapper/remote_wrapper.hpp" -#include -#include #include "../wrapper/repository_wrapper.hpp" repository_wrapper::~repository_wrapper() @@ -56,6 +49,13 @@ bool repository_wrapper::is_shallow() const return git_repository_is_shallow(*this); } +revwalk_wrapper repository_wrapper::new_walker() +{ + git_revwalk* walker; + throw_if_error(git_revwalk_new(&walker, *this)); + return revwalk_wrapper(walker); +} + // Head bool repository_wrapper::is_head_unborn() const diff --git a/src/wrapper/repository_wrapper.hpp b/src/wrapper/repository_wrapper.hpp index 49bc440..74e5eaa 100644 --- a/src/wrapper/repository_wrapper.hpp +++ b/src/wrapper/repository_wrapper.hpp @@ -14,6 +14,7 @@ #include "../wrapper/object_wrapper.hpp" #include "../wrapper/refs_wrapper.hpp" #include "../wrapper/remote_wrapper.hpp" +#include "../wrapper/revwalk_wrapper.hpp" #include "../wrapper/signature_wrapper.hpp" #include "../wrapper/wrapper_base.hpp" @@ -36,6 +37,8 @@ class repository_wrapper : public wrapper_base bool is_bare() const; bool is_shallow() const; + revwalk_wrapper new_walker(); + // Head bool is_head_unborn() const; reference_wrapper head() const; diff --git a/src/wrapper/revwalk_wrapper.cpp b/src/wrapper/revwalk_wrapper.cpp new file mode 100644 index 0000000..65ef0e5 --- /dev/null +++ b/src/wrapper/revwalk_wrapper.cpp @@ -0,0 +1,31 @@ +#include +#include + +#include "revwalk_wrapper.hpp" +#include "../utils/git_exception.hpp" + +revwalk_wrapper::revwalk_wrapper(git_revwalk* walker) + : base_type(walker) +{ +} + +revwalk_wrapper::~revwalk_wrapper() +{ + git_revwalk_free(p_resource); + p_resource=nullptr; +} + +void revwalk_wrapper::push_head() +{ + throw_if_error(git_revwalk_push_head(*this)); +} + +void revwalk_wrapper::push(git_oid& commit_oid) +{ + throw_if_error(git_revwalk_push(*this, &commit_oid)); +} + +int revwalk_wrapper::next(git_oid& commit_oid) +{ + return git_revwalk_next(&commit_oid, *this); +} diff --git a/src/wrapper/revwalk_wrapper.hpp b/src/wrapper/revwalk_wrapper.hpp new file mode 100644 index 0000000..cd1c08f --- /dev/null +++ b/src/wrapper/revwalk_wrapper.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +#include "../wrapper/wrapper_base.hpp" +#include "../wrapper/commit_wrapper.hpp" + +class revwalk_wrapper : public wrapper_base +{ +public: + + using base_type = wrapper_base; + + ~revwalk_wrapper(); + + revwalk_wrapper(revwalk_wrapper&&) noexcept = default; + revwalk_wrapper& operator=(revwalk_wrapper&&) noexcept = default; + + void push_head(); + void push(git_oid& commit_oid); + int next(git_oid& commit_oid); + +private: + + revwalk_wrapper(git_revwalk* walker); + + friend class repository_wrapper; +}; diff --git a/test/test_revlist.py b/test/test_revlist.py index 302bd5b..870b041 100644 --- a/test/test_revlist.py +++ b/test/test_revlist.py @@ -12,8 +12,9 @@ def test_revlist(xtl_clone, git_config, git2cpp_path, tmp_path, monkeypatch): "rev-list", "35955995424eb9699bb604b988b5270253b1fccc", "--max-count", - "4", + "2", ] p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) assert p.returncode == 0 assert "da1754dd6" in p.stdout + assert "2da8e13ef" not in p.stdout From 88708e64ce5297859f460dbea064bd90f29cfd74 Mon Sep 17 00:00:00 2001 From: Sandrine Pataut Date: Tue, 6 Jan 2026 15:34:50 +0100 Subject: [PATCH 3/3] Small fix in log and status --- src/subcommand/log_subcommand.cpp | 6 +++ src/subcommand/status_subcommand.cpp | 10 ++++- test/test_init.py | 63 ++++++++++++++++++---------- test/test_status.py | 35 +++++++++------- 4 files changed, 74 insertions(+), 40 deletions(-) diff --git a/src/subcommand/log_subcommand.cpp b/src/subcommand/log_subcommand.cpp index 18ba644..856ba7d 100644 --- a/src/subcommand/log_subcommand.cpp +++ b/src/subcommand/log_subcommand.cpp @@ -87,6 +87,12 @@ void log_subcommand::run() auto repo = repository_wrapper::open(directory); // auto branch_name = repo.head().short_name(); + if (repo.is_head_unborn()) + { + std::cout << "fatal: your current branch 'main' does not have any commits yet" << std::endl; + return; + } + revwalk_wrapper walker = repo.new_walker(); walker.push_head(); diff --git a/src/subcommand/status_subcommand.cpp b/src/subcommand/status_subcommand.cpp index 9e98840..02acbd6 100644 --- a/src/subcommand/status_subcommand.cpp +++ b/src/subcommand/status_subcommand.cpp @@ -194,6 +194,11 @@ void status_subcommand::run() { std::cout << "On branch " << branch_name << "\n" << std::endl; + if (repo.is_head_unborn()) + { + std::cout << "No commits yet\n" << std::endl; + } + if (sl.has_unmerged_header()) { std::cout << "You have unmerged paths.\n (fix conflicts and run \"git commit\")\n (use \"git merge --abort\" to abort the merge)\n" << std::endl; @@ -274,6 +279,9 @@ void status_subcommand::run() // TODO: check if this message should be displayed even if there are untracked files if (!(sl.has_tobecommited_header() | sl.has_notstagged_header() | sl.has_unmerged_header() | sl.has_untracked_header())) { - std::cout << treeclean_message << std::endl; + if (is_long) + { + std::cout << treeclean_message << std::endl; + } } } diff --git a/test/test_init.py b/test/test_init.py index aec88a0..1ba018e 100644 --- a/test/test_init.py +++ b/test/test_init.py @@ -1,19 +1,25 @@ -from pathlib import Path import subprocess +from pathlib import Path def test_init_in_directory(git2cpp_path, tmp_path): # tmp_path exists and is empty. assert list(tmp_path.iterdir()) == [] - cmd = [git2cpp_path, 'init', '--bare', str(tmp_path)] + cmd = [git2cpp_path, "init", "--bare", str(tmp_path)] p = subprocess.run(cmd, capture_output=True) assert p.returncode == 0 - assert p.stdout == b'' - assert p.stderr == b'' + assert p.stdout == b"" + assert p.stderr == b"" assert sorted(map(lambda path: path.name, tmp_path.iterdir())) == [ - 'HEAD', 'config', 'description', 'hooks', 'info', 'objects', 'refs' + "HEAD", + "config", + "description", + "hooks", + "info", + "objects", + "refs", ] # TODO: check this is a valid git repo @@ -24,14 +30,20 @@ def test_init_in_cwd(git2cpp_path, tmp_path, run_in_tmp_path): assert list(tmp_path.iterdir()) == [] assert Path.cwd() == tmp_path - cmd = [git2cpp_path, 'init', '--bare'] + cmd = [git2cpp_path, "init", "--bare"] p = subprocess.run(cmd, capture_output=True) assert p.returncode == 0 - assert p.stdout == b'' - assert p.stderr == b'' + assert p.stdout == b"" + assert p.stderr == b"" assert sorted(map(lambda path: path.name, tmp_path.iterdir())) == [ - 'HEAD', 'config', 'description', 'hooks', 'info', 'objects', 'refs' + "HEAD", + "config", + "description", + "hooks", + "info", + "objects", + "refs", ] # TODO: check this is a valid git repo @@ -41,38 +53,43 @@ def test_init_not_bare(git2cpp_path, tmp_path): # tmp_path exists and is empty. assert list(tmp_path.iterdir()) == [] - cmd = [git2cpp_path, 'init', '.'] + cmd = [git2cpp_path, "init", "."] p = subprocess.run(cmd, capture_output=True, cwd=tmp_path) assert p.returncode == 0 - assert p.stdout == b'' - assert p.stderr == b'' + assert p.stdout == b"" + assert p.stderr == b"" - # Directory contains just .git directory. - assert sorted(map(lambda path: path.name, tmp_path.iterdir())) == ['.git'] + # Directory contains just .git directory. + assert sorted(map(lambda path: path.name, tmp_path.iterdir())) == [".git"] # .git directory is a valid repo. - assert sorted(map(lambda path: path.name, (tmp_path / '.git').iterdir())) == [ - 'HEAD', 'config', 'description', 'hooks', 'info', 'objects', 'refs' + assert sorted(map(lambda path: path.name, (tmp_path / ".git").iterdir())) == [ + "HEAD", + "config", + "description", + "hooks", + "info", + "objects", + "refs", ] # Would like to use `git2cpp status` but it complains that 'refs/heads/master' not found - cmd = [git2cpp_path, 'log'] + cmd = [git2cpp_path, "log"] p = subprocess.run(cmd, capture_output=True, cwd=tmp_path) assert p.returncode == 0 - assert p.stdout == b'' - assert p.stderr == b'' + assert b"does not have any commits yet" in p.stdout def test_error_on_unknown_option(git2cpp_path): - cmd = [git2cpp_path, 'init', '--unknown'] + cmd = [git2cpp_path, "init", "--unknown"] p = subprocess.run(cmd, capture_output=True) assert p.returncode == 109 - assert p.stdout == b'' + assert p.stdout == b"" assert p.stderr.startswith(b"The following argument was not expected: --unknown") def test_error_on_repeated_directory(git2cpp_path): - cmd = [git2cpp_path, 'init', 'abc', 'def'] + cmd = [git2cpp_path, "init", "abc", "def"] p = subprocess.run(cmd, capture_output=True) assert p.returncode == 109 - assert p.stdout == b'' + assert p.stdout == b"" assert p.stderr.startswith(b"The following argument was not expected: def") diff --git a/test/test_status.py b/test/test_status.py index 6e97e19..93a632c 100644 --- a/test/test_status.py +++ b/test/test_status.py @@ -11,15 +11,15 @@ def test_status_new_file(xtl_clone, git2cpp_path, tmp_path, short_flag, long_fla assert (tmp_path / "xtl").exists() xtl_path = tmp_path / "xtl" - p = xtl_path / "mook_file.txt" # Untracked files - p.write_text('') + p = xtl_path / "mook_file.txt" # Untracked files + p.write_text("") - pw = xtl_path / "CMakeLists.txt" # Changes not staged for commit / modified + pw = xtl_path / "CMakeLists.txt" # Changes not staged for commit / modified pw.write_text("blablabla") - os.remove(xtl_path / "README.md") # Changes not staged for commit / deleted + os.remove(xtl_path / "README.md") # Changes not staged for commit / deleted - cmd = [git2cpp_path, 'status'] + cmd = [git2cpp_path, "status"] if short_flag != "": cmd.append(short_flag) if long_flag != "": @@ -38,27 +38,29 @@ def test_status_new_file(xtl_clone, git2cpp_path, tmp_path, short_flag, long_fla assert " D " in p.stdout assert "?? " in p.stdout + def test_status_nogit(git2cpp_path, tmp_path): - cmd = [git2cpp_path, 'status'] + cmd = [git2cpp_path, "status"] p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True) assert p.returncode != 0 + @pytest.mark.parametrize("short_flag", ["", "-s", "--short"]) @pytest.mark.parametrize("long_flag", ["", "--long"]) def test_status_add_file(xtl_clone, git2cpp_path, tmp_path, short_flag, long_flag): assert (tmp_path / "xtl").exists() xtl_path = tmp_path / "xtl" - p = xtl_path / "mook_file.txt" # Changes to be committed / new file - p.write_text('') + p = xtl_path / "mook_file.txt" # Changes to be committed / new file + p.write_text("") - os.remove(xtl_path / "README.md") # Changes to be committed / deleted + os.remove(xtl_path / "README.md") # Changes to be committed / deleted - cmd_add = [git2cpp_path, 'add', "--all"] + cmd_add = [git2cpp_path, "add", "--all"] p_add = subprocess.run(cmd_add, cwd=xtl_path, text=True) assert p_add.returncode == 0 - cmd_status = [git2cpp_path, 'status'] + cmd_status = [git2cpp_path, "status"] if short_flag != "": cmd_status.append(short_flag) if long_flag != "": @@ -77,14 +79,15 @@ def test_status_add_file(xtl_clone, git2cpp_path, tmp_path, short_flag, long_fla assert "A " in p_status.stdout assert "D " in p_status.stdout + def test_status_new_repo(git2cpp_path, tmp_path, run_in_tmp_path): # tmp_path exists and is empty. assert list(tmp_path.iterdir()) == [] - cmd = [git2cpp_path, 'init'] - p = subprocess.run(cmd, cwd = tmp_path) - - status_cmd = [git2cpp_path, 'status'] - p_status = subprocess.run(status_cmd, cwd = tmp_path) + cmd = [git2cpp_path, "init"] + p = subprocess.run(cmd, cwd=tmp_path) + assert p.returncode == 0 + status_cmd = [git2cpp_path, "status"] + p_status = subprocess.run(status_cmd, cwd=tmp_path) assert p_status.returncode == 0