diff --git a/CMakeLists.txt b/CMakeLists.txt index ef52831..eb25f40 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,6 +70,8 @@ set(GIT2CPP_SRC ${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/stash_subcommand.cpp + ${GIT2CPP_SOURCE_DIR}/subcommand/stash_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/subcommand/status_subcommand.cpp ${GIT2CPP_SOURCE_DIR}/subcommand/status_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/utils/ansi_code.cpp diff --git a/src/main.cpp b/src/main.cpp index 8aa4c0f..5813af2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -18,6 +18,7 @@ #include "subcommand/push_subcommand.hpp" #include "subcommand/remote_subcommand.hpp" #include "subcommand/reset_subcommand.hpp" +#include "subcommand/stash_subcommand.hpp" #include "subcommand/status_subcommand.hpp" #include "subcommand/revparse_subcommand.hpp" #include "subcommand/revlist_subcommand.hpp" @@ -50,6 +51,7 @@ int main(int argc, char** argv) remote_subcommand remote(lg2_obj, app); revparse_subcommand revparse(lg2_obj, app); revlist_subcommand revlist(lg2_obj, app); + stash_subcommand stash(lg2_obj, app); app.require_subcommand(/* min */ 0, /* max */ 1); diff --git a/src/subcommand/stash_subcommand.cpp b/src/subcommand/stash_subcommand.cpp new file mode 100644 index 0000000..752ef5f --- /dev/null +++ b/src/subcommand/stash_subcommand.cpp @@ -0,0 +1,92 @@ +#include +#include +#include +#include + +#include + +#include "../subcommand/stash_subcommand.hpp" +#include "../subcommand/status_subcommand.hpp" +#include "../wrapper/repository_wrapper.hpp" + +bool has_subcommand(CLI::App* cmd) +{ + std::vector subs = { "push", "pop", "list", "apply" }; + return std::any_of(subs.begin(), subs.end(), [cmd](const std::string& s) { return cmd->got_subcommand(s); }); +} + +stash_subcommand::stash_subcommand(const libgit2_object&, CLI::App& app) +{ + auto* stash = app.add_subcommand("stash", "Stash the changes in a dirty working directory away"); + auto* push = stash->add_subcommand("push", ""); + auto* list = stash->add_subcommand("list", ""); + auto* pop = stash->add_subcommand("pop", ""); + auto* apply = stash->add_subcommand("apply", ""); + + push->add_option("-m,--message", m_message, ""); + pop->add_option("--index", m_index, ""); + apply->add_option("--index", m_index, ""); + + stash->callback([this,stash]() + { + if (!has_subcommand(stash)) + { + this->run_push(); + } + }); + push->callback([this]() { this->run_push(); }); + list->callback([this]() { this->run_list(); }); + pop->callback([this]() { this->run_pop(); }); + apply->callback([this]() { this->run_apply(); }); +} + +void stash_subcommand::run_push() +{ + auto directory = get_current_git_path(); + auto repo = repository_wrapper::open(directory); + auto author_committer_signatures = signature_wrapper::get_default_signature_from_env(repo); + + git_oid stash_id; + throw_if_error(git_stash_save(&stash_id, repo, author_committer_signatures.first, m_message.c_str(), GIT_STASH_DEFAULT)); + auto stash = repo.find_commit(stash_id); + std::cout << "Saved working directory and index state " << stash.summary() << std::endl; +} + +static int list_stash_cb(size_t index, const char* message, const git_oid* stash_id, void* payload) +{ + std::cout << "stash@{" << index << "}: " << message << std::endl; + return 0; +} + +void stash_subcommand::run_list() +{ + auto directory = get_current_git_path(); + auto repo = repository_wrapper::open(directory); + + throw_if_error(git_stash_foreach(repo, list_stash_cb, NULL)); +} + +void stash_subcommand::run_pop() +{ + auto directory = get_current_git_path(); + auto repo = repository_wrapper::open(directory); + + std::string stash_spec = "stash@{" + std::to_string(m_index) + "}"; + auto stash_obj = repo.revparse_single(stash_spec); + git_oid stash_id = stash_obj->oid(); + char id_string[GIT_OID_HEXSZ + 1]; + git_oid_tostr(id_string, sizeof(id_string), &stash_id); + + throw_if_error(git_stash_pop(repo, m_index, NULL)); + status_run(); + std::cout << "Dropped refs/stash@{" << m_index << "} (" << id_string << ")" << std::endl; +} + +void stash_subcommand::run_apply() +{ + auto directory = get_current_git_path(); + auto repo = repository_wrapper::open(directory); + + throw_if_error(git_stash_apply(repo, m_index, NULL)); + status_run(); +} diff --git a/src/subcommand/stash_subcommand.hpp b/src/subcommand/stash_subcommand.hpp new file mode 100644 index 0000000..c6a13ce --- /dev/null +++ b/src/subcommand/stash_subcommand.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include "../utils/common.hpp" + +class stash_subcommand +{ +public: + + explicit stash_subcommand(const libgit2_object&, CLI::App& app); + void run_push(); + void run_list(); + void run_pop(); + void run_apply(); + + std::vector m_options; + std::string m_message = ""; + size_t m_index = 0; +}; diff --git a/src/subcommand/status_subcommand.cpp b/src/subcommand/status_subcommand.cpp index 02acbd6..42cbe27 100644 --- a/src/subcommand/status_subcommand.cpp +++ b/src/subcommand/status_subcommand.cpp @@ -8,20 +8,19 @@ #include "status_subcommand.hpp" #include "../wrapper/status_wrapper.hpp" -#include "../wrapper/refs_wrapper.hpp" status_subcommand::status_subcommand(const libgit2_object&, CLI::App& app) { auto *sub = app.add_subcommand("status", "Show modified files in working directory, staged for your next commit"); - sub->add_flag("-s,--short", m_short_flag, "Give the output in the short-format."); - sub->add_flag("--long", m_long_flag, "Give the output in the long-format. This is the default."); + sub->add_flag("-s,--short", m_fl.m_short_flag, "Give the output in the short-format."); + sub->add_flag("--long", m_fl.m_long_flag, "Give the output in the long-format. This is the default."); // sub->add_flag("--porcelain[=]", porcelain, "Give the output in an easy-to-parse format for scripts. // This is similar to the short output, but will remain stable across Git versions and regardless of user configuration. // See below for details. The version parameter is used to specify the format version. This is optional and defaults // to the original version v1 format."); - sub->add_flag("-b,--branch", m_branch_flag, "Show the branch and tracking info even in short-format."); + sub->add_flag("-b,--branch", m_fl.m_branch_flag, "Show the branch and tracking info even in short-format."); sub->callback([this]() { this->run(); }); }; @@ -163,6 +162,11 @@ void print_not_tracked(const std::vector& entries_to_print, const s } void status_subcommand::run() +{ + status_run(m_fl); +} + +void status_run(status_subcommand_flags fl) { auto directory = get_current_git_path(); auto repo = repository_wrapper::open(directory); @@ -175,11 +179,11 @@ void status_subcommand::run() std::vector ignored_to_print{}; output_format of = output_format::DEFAULT; - if (m_short_flag) + if (fl.m_short_flag) { of = output_format::SHORT; } - if (m_long_flag) + if (fl.m_long_flag) { of = output_format::LONG; } @@ -206,7 +210,7 @@ void status_subcommand::run() } else { - if (m_branch_flag) + if (fl.m_branch_flag) { std::cout << "## " << branch_name << std::endl; } diff --git a/src/subcommand/status_subcommand.hpp b/src/subcommand/status_subcommand.hpp index ae259a9..92c14d3 100644 --- a/src/subcommand/status_subcommand.hpp +++ b/src/subcommand/status_subcommand.hpp @@ -4,6 +4,13 @@ #include "../utils/common.hpp" +struct status_subcommand_flags +{ + bool m_branch_flag = false; + bool m_long_flag = false; + bool m_short_flag = false; +}; + class status_subcommand { public: @@ -12,7 +19,7 @@ class status_subcommand void run(); private: - bool m_branch_flag = false; - bool m_long_flag = false; - bool m_short_flag = false; + status_subcommand_flags m_fl; }; + +void status_run(status_subcommand_flags fl = {}); diff --git a/src/wrapper/commit_wrapper.cpp b/src/wrapper/commit_wrapper.cpp index 92f57df..33efa9f 100644 --- a/src/wrapper/commit_wrapper.cpp +++ b/src/wrapper/commit_wrapper.cpp @@ -1,4 +1,5 @@ #include "../wrapper/commit_wrapper.hpp" +#include commit_wrapper::commit_wrapper(git_commit* commit) : base_type(commit) @@ -27,6 +28,11 @@ std::string commit_wrapper::commit_oid_tostr() const return git_oid_tostr(buf, sizeof(buf), &this->oid()); } +std::string commit_wrapper::summary() const +{ + return git_commit_summary(*this); +} + commit_list_wrapper commit_wrapper::get_parents_list() const { size_t parent_count = git_commit_parentcount(*this); diff --git a/src/wrapper/commit_wrapper.hpp b/src/wrapper/commit_wrapper.hpp index 84b35d1..4fe280f 100644 --- a/src/wrapper/commit_wrapper.hpp +++ b/src/wrapper/commit_wrapper.hpp @@ -24,6 +24,8 @@ class commit_wrapper : public wrapper_base const git_oid& oid() const; std::string commit_oid_tostr() const; + std::string summary() const; + commit_list_wrapper get_parents_list() const; private: diff --git a/test/test_stash.py b/test/test_stash.py new file mode 100644 index 0000000..286324a --- /dev/null +++ b/test/test_stash.py @@ -0,0 +1,139 @@ +import subprocess + +import pytest + + +def test_stash_push(xtl_clone, commit_env_config, git2cpp_path, tmp_path): + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + p = xtl_path / "mook_file.txt" + p.write_text("blabla") + + cmd_add = [git2cpp_path, "add", "mook_file.txt"] + p_add = subprocess.run(cmd_add, cwd=xtl_path, text=True) + assert p_add.returncode == 0 + + stash_path = xtl_path / ".git/refs/stash" + assert not stash_path.exists() + + cmd_stash = [git2cpp_path, "stash"] + p_stash = subprocess.run(cmd_stash, capture_output=True, cwd=xtl_path, text=True) + assert p_stash.returncode == 0 + assert stash_path.exists() + + +def test_stash_list(xtl_clone, commit_env_config, git2cpp_path, tmp_path): + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + p = xtl_path / "mook_file.txt" + p.write_text("blabla") + + cmd_add = [git2cpp_path, "add", "mook_file.txt"] + p_add = subprocess.run(cmd_add, cwd=xtl_path, text=True) + assert p_add.returncode == 0 + + cmd_list = [git2cpp_path, "stash", "list"] + p_list = subprocess.run(cmd_list, capture_output=True, cwd=xtl_path, text=True) + assert p_list.returncode == 0 + assert "stash@{0}" not in p_list.stdout + + cmd_stash = [git2cpp_path, "stash"] + p_stash = subprocess.run(cmd_stash, capture_output=True, cwd=xtl_path, text=True) + assert p_stash.returncode == 0 + + p_list_2 = subprocess.run(cmd_list, capture_output=True, cwd=xtl_path, text=True) + assert p_list_2.returncode == 0 + assert "stash@{0}" in p_list_2.stdout + + +@pytest.mark.parametrize("index_flag", ["", "--index"]) +def test_stash_pop(xtl_clone, commit_env_config, git2cpp_path, tmp_path, index_flag): + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + index = 0 if index_flag == "" else 1 + + for i in range(index + 1): + p = xtl_path / f"mook_file_{i}.txt" + p.write_text(f"blabla{i}") + + cmd_add = [git2cpp_path, "add", f"mook_file_{i}.txt"] + p_add = subprocess.run(cmd_add, cwd=xtl_path, text=True) + assert p_add.returncode == 0 + + cmd_stash = [git2cpp_path, "stash"] + p_stash = subprocess.run( + cmd_stash, capture_output=True, cwd=xtl_path, text=True + ) + assert p_stash.returncode == 0 + + cmd_status = [git2cpp_path, "status"] + p_status = subprocess.run( + cmd_status, capture_output=True, cwd=xtl_path, text=True + ) + assert p_status.returncode == 0 + assert "mook_file" not in p_status.stdout + + cmd_pop = [git2cpp_path, "stash", "pop"] + if index_flag != "": + cmd_pop.append(index_flag) + cmd_pop.append("1") + p_pop = subprocess.run(cmd_pop, capture_output=True, cwd=xtl_path, text=True) + assert p_pop.returncode == 0 + assert "mook_file_0" in p_pop.stdout + assert "Dropped refs/stash@{" + str(index) + "}" in p_pop.stdout + + cmd_list = [git2cpp_path, "stash", "list"] + p_list = subprocess.run(cmd_list, capture_output=True, cwd=xtl_path, text=True) + assert p_list.returncode == 0 + if index_flag == "": + assert p_list.stdout == "" + else: + assert "stash@{0}" in p_list.stdout + assert "stash@{1}" not in p_list.stdout + + +@pytest.mark.parametrize("index_flag", ["", "--index"]) +def test_stash_apply(xtl_clone, commit_env_config, git2cpp_path, tmp_path, index_flag): + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + index = 0 if index_flag == "" else 1 + + for i in range(index + 1): + p = xtl_path / f"mook_file_{i}.txt" + p.write_text(f"blabla{i}") + + cmd_add = [git2cpp_path, "add", f"mook_file_{i}.txt"] + p_add = subprocess.run(cmd_add, cwd=xtl_path, text=True) + assert p_add.returncode == 0 + + cmd_stash = [git2cpp_path, "stash"] + p_stash = subprocess.run( + cmd_stash, capture_output=True, cwd=xtl_path, text=True + ) + assert p_stash.returncode == 0 + + cmd_status = [git2cpp_path, "status"] + p_status = subprocess.run( + cmd_status, capture_output=True, cwd=xtl_path, text=True + ) + assert p_status.returncode == 0 + assert "mook_file" not in p_status.stdout + + cmd_apply = [git2cpp_path, "stash", "apply"] + if index_flag != "": + cmd_apply.append(index_flag) + cmd_apply.append("1") + p_apply = subprocess.run(cmd_apply, capture_output=True, cwd=xtl_path, text=True) + assert p_apply.returncode == 0 + assert "mook_file_0" in p_apply.stdout + + cmd_list = [git2cpp_path, "stash", "list"] + p_list = subprocess.run(cmd_list, capture_output=True, cwd=xtl_path, text=True) + assert p_list.returncode == 0 + assert "stash@{0}" in p_list.stdout + if index_flag != "": + assert "stash@{1}" in p_list.stdout