From dbbb92e54d60af15fe471bfe4b0e8d3c5c183f6a Mon Sep 17 00:00:00 2001 From: Chow Jia Ying Date: Tue, 13 May 2025 22:50:52 +0800 Subject: [PATCH 1/6] Implement tag --- .clang-format | 2 +- CMakeLists.txt | 103 ++++++++++++++++++++--------------------- app/gyt.cpp | 3 ++ include/commands/tag.h | 10 ++++ include/util.h | 1 + run.sh | 36 +++++++------- src/commands/tag.cpp | 51 ++++++++++++++++++++ src/object.cpp | 4 +- src/util.cpp | 6 +++ test.sh | 42 +++++++++-------- tests/CMakeLists.txt | 7 ++- tests/tag_t.cpp | 71 ++++++++++++++++++++++++++++ 12 files changed, 238 insertions(+), 98 deletions(-) create mode 100644 include/commands/tag.h mode change 100644 => 100755 run.sh create mode 100644 src/commands/tag.cpp mode change 100644 => 100755 test.sh create mode 100644 tests/tag_t.cpp diff --git a/.clang-format b/.clang-format index 5ee4a44..076c252 100644 --- a/.clang-format +++ b/.clang-format @@ -1 +1 @@ -InsertNewlineAtEOF: true \ No newline at end of file +InsertNewlineAtEOF: true diff --git a/CMakeLists.txt b/CMakeLists.txt index 3dc0059..9bb1dd1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,56 +1,51 @@ cmake_minimum_required(VERSION 3.8.2) -#Set a name and a version number for your project: - project(cpp - project - template VERSION 0.0.1 LANGUAGES CXX) - - set(CMAKE_AR / usr / bin / ar) - -#Enable C language - enable_language(C) - -#this needs to be in the top level CMakeLists.txt to enable tests - include(CTest) - - set(BOOST_ENABLE_CMAKE ON) set(Boost_CMAKE_CXX_STANDARD 17) set( - CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED - ON) - - set(BOOST_INCLUDE_LIBRARIES iostreams uuid algorithm) - - include(FetchContent) FetchContent_Declare( - Boost GIT_REPOSITORY - https : // github.com/boostorg/boost.git - GIT_TAG boost - - 1.83.0 GIT_SHALLOW - TRUE) FetchContent_MakeAvailable(Boost) - -#compile the library - add_subdirectory(src) - -#compile the application - add_subdirectory(app) - -#optionally add doxygen target to generate documentation - option(BUILD_DOCS - "Enable building of " - "documentation (requires " - "Doxygen)" OFF) if (BUILD_DOCS) - find_package(Doxygen REQUIRED) set( - DOXYGEN_EXCLUDE_PATTERNS - "${CMAKE_SOURCE_DIR}/ext/*") - doxygen_add_docs( - doxygen ${ - CMAKE_SOURCE_DIR} WORKING_DIRECTORY - ${CMAKE_CURRENT_BINARY_DIR}) - endif() - -#compile the tests - option(BUILD_TESTS - "Enable " - "building of " - "documentation " - "(requires " - "Doxygen" - ")" OFF) if (BUILD_TESTS) - add_subdirectory( - tests) endif() +# Set a name and a version number for your project: +project( + cpp-project-template + VERSION 0.0.1 + LANGUAGES CXX) + +set(CMAKE_AR /usr/bin/ar) + +# Enable C language +enable_language(C) + +# this needs to be in the top level CMakeLists.txt to enable tests +include(CTest) + +set(BOOST_ENABLE_CMAKE ON) +set(Boost_CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(BOOST_INCLUDE_LIBRARIES iostreams uuid algorithm) + +include(FetchContent) +FetchContent_Declare( + Boost + GIT_REPOSITORY https://github.com/boostorg/boost.git + GIT_TAG boost-1.83.0 + GIT_SHALLOW TRUE) +FetchContent_MakeAvailable(Boost) + +# compile the library +add_subdirectory(src) + +# compile the application +add_subdirectory(app) + +# optionally add doxygen target to generate documentation +option(BUILD_DOCS "Enable building of documentation (requires Doxygen)" OFF) +if(BUILD_DOCS) + find_package(Doxygen REQUIRED) + set(DOXYGEN_EXCLUDE_PATTERNS "${CMAKE_SOURCE_DIR}/ext/*") + doxygen_add_docs(doxygen ${CMAKE_SOURCE_DIR} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +endif() + +# compile the tests +option(BUILD_TESTS "Enable building of documentation (requires Doxygen)" OFF) +if(BUILD_TESTS) + add_subdirectory(tests) +endif() diff --git a/app/gyt.cpp b/app/gyt.cpp index 88ba76b..01e72fb 100644 --- a/app/gyt.cpp +++ b/app/gyt.cpp @@ -5,6 +5,7 @@ #include "commands/log.h" #include "commands/ls-tree.h" #include "commands/show-ref.h" +#include "commands/tag.h" #include #include #include @@ -33,6 +34,8 @@ int main(int argc, char **argv) { commands::checkout(args); } else if (command == "show-ref") { commands::showref(args); + } else if (command == "tag") { + commands::tag(args); } else { std::cerr << "Unknown command: " << command << "\n"; return -1; diff --git a/include/commands/tag.h b/include/commands/tag.h new file mode 100644 index 0000000..21a9704 --- /dev/null +++ b/include/commands/tag.h @@ -0,0 +1,10 @@ +#ifndef TAG_H +#define TAG_H + +#include +#include +namespace commands { +void tag(std::vector &args); +} // namespace commands + +#endif // TAG_H diff --git a/include/util.h b/include/util.h index 9d0b65d..e4d68a8 100644 --- a/include/util.h +++ b/include/util.h @@ -16,4 +16,5 @@ fs::perms get_unix_permissions(int mode); std::string remove_file_prefix(const fs::path &path, const fs::path &repo_prefix); std::string resolve_ref(const fs::path &ref_path, GitRepository &repo); +fs::path get_commit_path(const std::string &sha); #endif // UTIL_H diff --git a/run.sh b/run.sh old mode 100644 new mode 100755 index e4578a0..64ba4c5 --- a/run.sh +++ b/run.sh @@ -1,33 +1,31 @@ -#!/ bin / bash -set - e +#!/bin/bash +set -e #Set the default build type to Debug if no environment is specified - BUILD_TYPE = ${BUILD_TYPE : -Debug} +BUILD_TYPE=${BUILD_TYPE:-Debug} #If the environment is specified as "prod", set the build type to Release -if["$1" == "-p"]; -then BUILD_TYPE = Release fi +if [ "$1" == "-p" ]; +then + BUILD_TYPE=Release +fi #remove old build if any - if[-f "app/gyt"]; -then rm - - rf app / gyt fi +if [ -f "app/gyt" ]; +then + rm -rf app/gyt +fi #Print the selected build type - echo "Selected build type: $BUILD_TYPE" echo - "Building the project... This will take a while to install " - "dependencies for the first time." +echo "Selected build type: $BUILD_TYPE" +echo "Building the project... This will take a while to install dependencies for the first time." #Run CMake with the selected build type - cmake..- - DCMAKE_BUILD_TYPE = $BUILD_TYPE - DCMAKE_EXPORT_COMPILE_COMMANDS = - 1 - - G Ninja +cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -G Ninja #Build the project - ninja +ninja #symlink - so I can run it like gyt[arguments....] - sudo rm / - usr / local / bin / gyt sudo ln - - s "$(pwd)/app/gyt" / usr / local / bin / gyt +sudo rm /usr/local/bin/gyt +sudo ln -s "$(pwd)/app/gyt" /usr/local/bin/gyt diff --git a/src/commands/tag.cpp b/src/commands/tag.cpp new file mode 100644 index 0000000..8496ae4 --- /dev/null +++ b/src/commands/tag.cpp @@ -0,0 +1,51 @@ +#include "commands/tag.h" +#include "object.h" +#include "repository.h" +#include "tclap/CmdLine.h" +#include "util.h" +#include +#include + +namespace commands { +void tag(std::vector &args) { + TCLAP::CmdLine cmd("tag", ' ', "0.1"); + + // defines arguments + TCLAP::UnlabeledValueArg tagArg("tagname", "Tag name", true, + "HEAD", "tag name"); + TCLAP::UnlabeledValueArg commitArg( + "commit", "Commit to tag to. If not specified, defaults to HEAD", false, + "HEAD", "commit to tag"); + cmd.ignoreUnmatched(true); + cmd.add(tagArg); + cmd.add(commitArg); + cmd.parse(args); + // process args + try { + // TODO this + std::optional repo = GitRepository::find(); + if (!repo) { + throw std::runtime_error("Not a git repository"); + } + // resolve ref, basically find the commit object + + std::string commit = commitArg.isSet() ? commitArg.getValue() + : GitObject::find(*repo, "HEAD"); + std::string tag = tagArg.getValue(); + // ERROR: if the commit doesn't exist. + if (fs::is_regular_file(get_commit_path(commit))) { + throw std::runtime_error("Not a valid commit"); + } + // ERROR: if the tag already exists + fs::path tag_path = repo->repo_path("refs/tags/" + tag); + if (fs::is_regular_file(tag_path)) { + throw std::runtime_error("Tag already exists"); + } + + // create a new file in refs/tags/{name}, and write the sha of the commit. + create_file(tag_path, commit + "\n"); + } catch (std::runtime_error &err) { + std::cerr << err.what() << "\n"; + } +} +} // namespace commands diff --git a/src/object.cpp b/src/object.cpp index 3bc0cfa..18a38cf 100644 --- a/src/object.cpp +++ b/src/object.cpp @@ -18,9 +18,7 @@ GitObject::GitObject(const std::string &format) { this->format = format; } void GitObject::init() {} GitObject *GitObject::read(GitRepository &repo, const std::string &sha) { - std::string dir = sha.substr(0, 2); - std::string path = sha.substr(2); - fs::path file_path = fs::path("objects") / dir / path; + fs::path file_path = get_commit_path(sha); fs::path paths = repo.file(file_path); if (!fs::is_regular_file(paths)) { diff --git a/src/util.cpp b/src/util.cpp index e2c9338..53b7d90 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -169,3 +169,9 @@ std::string resolve_ref(const fs::path &ref_path, GitRepository &repo) { } return ref; } + +fs::path get_commit_path(const std::string &sha) { + std::string dir = sha.substr(0, 2); + std::string path = sha.substr(2); + return fs::path("objects") / dir / path; +} diff --git a/test.sh b/test.sh old mode 100644 new mode 100755 index ac8df0b..b4e0937 --- a/test.sh +++ b/test.sh @@ -1,30 +1,34 @@ -#!/ bin / bash -set - e +#!/bin/bash +set -e + +#Set the default build type to Debug if no environment is specified +BUILD_TYPE=${BUILD_TYPE:-Debug} + +#If the environment is specified as "prod", set the build type to Release +if [ "$1" == "-p" ]; +then + BUILD_TYPE=Release +fi #remove old build if any - if[-f "app/gyt"]; -then rm - - rf app / gyt fi +if [ -f "app/gyt" ]; +then + rm -rf app/gyt +fi #Print the selected build type - echo "Selected build type: $BUILD_TYPE" echo - "Building the project... This will take a while to install " - "dependencies for the first time." +echo "Selected build type: $BUILD_TYPE" +echo "Building the project... This will take a while to install dependencies for the first time." #Run CMake with the selected build type - cmake..- - DCMAKE_BUILD_TYPE = Debug - DCMAKE_EXPORT_COMPILE_COMMANDS = - 1 - DBUILD_TESTS = - ON - - G Ninja +cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -G Ninja #Build the project - ninja +ninja -#Tests - ctest +# test +ctest #symlink - so I can run it like gyt[arguments....] - sudo rm / - usr / local / bin / gyt sudo ln - - s "$(pwd)/app/gyt" / usr / local / bin / gyt +sudo rm /usr/local/bin/gyt +sudo ln -s "$(pwd)/app/gyt" /usr/local/bin/gyt diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 54f1805..2703094 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,7 +1,10 @@ -add_executable(tests tests.cpp repository_t.cpp) +add_executable(tests tests.cpp repository_t.cpp tag_t.cpp) target_include_directories(tests PUBLIC ../ext) -target_link_libraries(tests PUBLIC boost_libraries repository) +target_link_libraries(tests PUBLIC boost_libraries repository commands) +file(COPY ${CMAKE_SOURCE_DIR}/tests/gitrepo + DESTINATION ${CMAKE_BINARY_DIR}/tests + FILES_MATCHING PATTERN ".*" PATTERN "*" EXCLUDE) # allow user to run tests with `make test` or `ctest` include(../cmake/Catch.cmake) diff --git a/tests/tag_t.cpp b/tests/tag_t.cpp new file mode 100644 index 0000000..cb35cff --- /dev/null +++ b/tests/tag_t.cpp @@ -0,0 +1,71 @@ +#include "boost/algorithm/string/trim.hpp" +#include "catch2/catch.hpp" +#include "commands/tag.h" +#include "repository.h" +#include +#include + +namespace fs = std::filesystem; + +// Helper function to read the contents of a file +std::string file_contents(const fs::path &path) { + std::ifstream file(path); + if (file.is_open()) { + std::string contents((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + boost::trim_right(contents); + file.close(); + return contents; + } else { + throw std::runtime_error("Could not open file: " + path.string()); + } +} +const std::string FIRST_COMMIT = "1723ac93b92db1fc2c28de8e5da814136937f8c6"; +const std::string SECOND_COMMIT = "6c2c22e7b5b7b1682e3c14668499e84141aca0d4"; +const fs::path VALID_GIT_PATH = fs::temp_directory_path() / "gitrepo"; +const fs::path TEST_FILE_PATH = fs::current_path() / "gitrepo"; + +void setup() { + // Provide a path to a valid git repository, set up a git repo with an + // actual commit + fs::copy(TEST_FILE_PATH, VALID_GIT_PATH, fs::copy_options::recursive); + + fs::current_path(VALID_GIT_PATH); +} + +void teardown() { fs::remove_all(VALID_GIT_PATH); } + +// TODO: test cases +TEST_CASE("tag command", "[tag]") { + setup(); + SECTION("Valid git tag command - tag name only") { + std::vector args({"tag", "v1.0"}); + commands::tag(args); + + REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true)); + REQUIRE(fs::exists(".git/refs/tags/v1.0")); + REQUIRE(file_contents(".git/refs/tags/v1.0") == SECOND_COMMIT); + } + teardown(); + + setup(); + SECTION("Valid git tag command - tag name with commit") { + std::vector args({"tag", "v1.0", SECOND_COMMIT}); + commands::tag(args); + + REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true)); + REQUIRE(fs::exists(".git/refs/tags/v1.0")); + REQUIRE(file_contents(".git/refs/tags/v1.0") == SECOND_COMMIT); + } + teardown(); +} + +TEST_CASE("tag with errors", "[tag]") { + setup(); + SECTION("Git tag failed with missing tag name") {} + teardown(); + + setup(); + SECTION("Git tag failed with non-existent commit") {} + teardown(); +} From f8aedd5d993de5867ec275e94df1268aec25a3ef Mon Sep 17 00:00:00 2001 From: Chow Jia Ying Date: Wed, 14 May 2025 22:55:09 +0800 Subject: [PATCH 2/6] Use a singleton instance to manage parsing --- app/gyt.cpp | 3 +++ include/parsers/TagParser.h | 23 +++++++++++++++++++++++ src/CMakeLists.txt | 8 +++++++- src/commands/tag.cpp | 29 ++++++++++------------------- src/parsers/TagParser.cpp | 28 ++++++++++++++++++++++++++++ test.sh | 2 +- 6 files changed, 72 insertions(+), 21 deletions(-) create mode 100644 include/parsers/TagParser.h create mode 100644 src/parsers/TagParser.cpp diff --git a/app/gyt.cpp b/app/gyt.cpp index 01e72fb..4711a83 100644 --- a/app/gyt.cpp +++ b/app/gyt.cpp @@ -35,6 +35,9 @@ int main(int argc, char **argv) { } else if (command == "show-ref") { commands::showref(args); } else if (command == "tag") { + + std::vector test({"tag", "v1.0"}); + commands::tag(test); commands::tag(args); } else { std::cerr << "Unknown command: " << command << "\n"; diff --git a/include/parsers/TagParser.h b/include/parsers/TagParser.h new file mode 100644 index 0000000..f190bb2 --- /dev/null +++ b/include/parsers/TagParser.h @@ -0,0 +1,23 @@ +#ifndef TAGPARSER_H +#define TAGPARSER_H + +#include "tclap/CmdLine.h" +#include +#include + +class TagParser { +public: + static TagParser &get(); + void parse(std::vector &args); + std::string getTag(); + bool isCommitSet() const; + std::string getCommit(); + +private: + TagParser(); + TCLAP::CmdLine cmd; + TCLAP::UnlabeledValueArg tagArg; + TCLAP::UnlabeledValueArg commitArg; +}; + +#endif // TAGPARSER_H diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8a3a519..e15d02c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -24,8 +24,14 @@ target_link_libraries(object PRIVATE repository boost_libraries) target_include_directories(object PUBLIC ../include) target_compile_features(object PUBLIC cxx_std_17) +file(GLOB PARSER_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/parsers/*.cpp") +add_library(parsers ${PARSER_SOURCES}) +target_link_libraries(parsers PRIVATE repository object boost_libraries) +target_include_directories(parsers PUBLIC ../include) +target_compile_features(parsers PUBLIC cxx_std_17) + file(GLOB COMMANDS_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/commands/*.cpp") add_library(commands ${COMMANDS_SOURCES}) -target_link_libraries(commands PRIVATE repository object boost_libraries) +target_link_libraries(commands PRIVATE parsers repository object boost_libraries) target_include_directories(commands PUBLIC ../include) target_compile_features(commands PUBLIC cxx_std_17) diff --git a/src/commands/tag.cpp b/src/commands/tag.cpp index 8496ae4..ff3d00d 100644 --- a/src/commands/tag.cpp +++ b/src/commands/tag.cpp @@ -1,45 +1,36 @@ #include "commands/tag.h" #include "object.h" +#include "parsers/TagParser.h" #include "repository.h" -#include "tclap/CmdLine.h" #include "util.h" #include #include namespace commands { void tag(std::vector &args) { - TCLAP::CmdLine cmd("tag", ' ', "0.1"); - - // defines arguments - TCLAP::UnlabeledValueArg tagArg("tagname", "Tag name", true, - "HEAD", "tag name"); - TCLAP::UnlabeledValueArg commitArg( - "commit", "Commit to tag to. If not specified, defaults to HEAD", false, - "HEAD", "commit to tag"); - cmd.ignoreUnmatched(true); - cmd.add(tagArg); - cmd.add(commitArg); - cmd.parse(args); + TagParser &parser = TagParser::get(); + parser.parse(args); // process args try { - // TODO this std::optional repo = GitRepository::find(); if (!repo) { throw std::runtime_error("Not a git repository"); } // resolve ref, basically find the commit object - std::string commit = commitArg.isSet() ? commitArg.getValue() - : GitObject::find(*repo, "HEAD"); - std::string tag = tagArg.getValue(); + std::string commit = parser.isCommitSet() ? parser.getCommit() + : GitObject::find(*repo, "HEAD"); + std::string tag = parser.getTag(); // ERROR: if the commit doesn't exist. if (fs::is_regular_file(get_commit_path(commit))) { - throw std::runtime_error("Not a valid commit"); + std::string error_message = commit + ": not a valid commit"; + throw std::runtime_error(error_message); } // ERROR: if the tag already exists fs::path tag_path = repo->repo_path("refs/tags/" + tag); if (fs::is_regular_file(tag_path)) { - throw std::runtime_error("Tag already exists"); + std::string error_message = "tag '" + tag + "' already exists"; + throw std::runtime_error(error_message); } // create a new file in refs/tags/{name}, and write the sha of the commit. diff --git a/src/parsers/TagParser.cpp b/src/parsers/TagParser.cpp new file mode 100644 index 0000000..847790f --- /dev/null +++ b/src/parsers/TagParser.cpp @@ -0,0 +1,28 @@ +#include "parsers/TagParser.h" +#include +#include + +void TagParser::parse(std::vector &args) { + cmd.reset(); + cmd.parse(args); +} + +std::string TagParser::getTag() { return tagArg.getValue(); } +bool TagParser::isCommitSet() const { return commitArg.isSet(); } +std::string TagParser::getCommit() { return commitArg.getValue(); } + +TagParser &TagParser::get() { + static TagParser instance; + return instance; +} + +TagParser::TagParser() + : cmd("tag", ' ', "0.1"), + tagArg("tagname", "Tag name", true, "HEAD", "tag name"), + commitArg("commit", + "Commit to tag to. If not specified, defaults to HEAD", false, + "HEAD", "commit to tag") { + cmd.ignoreUnmatched(true); + cmd.add(tagArg); + cmd.add(commitArg); +} diff --git a/test.sh b/test.sh index b4e0937..f3ace9f 100755 --- a/test.sh +++ b/test.sh @@ -27,7 +27,7 @@ cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -G Nin ninja # test -ctest +ctest --output-on-failure #symlink - so I can run it like gyt[arguments....] sudo rm /usr/local/bin/gyt From 687eca5c4182f9154fba39941a2cc57a2da88efd Mon Sep 17 00:00:00 2001 From: Chow Jia Ying Date: Wed, 14 May 2025 22:55:26 +0800 Subject: [PATCH 3/6] Write tests for tag command --- tests/tag_t.cpp | 79 +++++++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/tests/tag_t.cpp b/tests/tag_t.cpp index cb35cff..fb2c8e4 100644 --- a/tests/tag_t.cpp +++ b/tests/tag_t.cpp @@ -23,49 +23,56 @@ std::string file_contents(const fs::path &path) { const std::string FIRST_COMMIT = "1723ac93b92db1fc2c28de8e5da814136937f8c6"; const std::string SECOND_COMMIT = "6c2c22e7b5b7b1682e3c14668499e84141aca0d4"; const fs::path VALID_GIT_PATH = fs::temp_directory_path() / "gitrepo"; -const fs::path TEST_FILE_PATH = fs::current_path() / "gitrepo"; +const fs::path OLD_CWD = fs::current_path(); +const fs::path TEST_FILE_PATH = OLD_CWD / "gitrepo"; -void setup() { - // Provide a path to a valid git repository, set up a git repo with an - // actual commit - fs::copy(TEST_FILE_PATH, VALID_GIT_PATH, fs::copy_options::recursive); +/** +Uses RAII to manage the setup and teardown of a sample Git repo +*/ +class GitRepoSetup { +public: + GitRepoSetup() { setup(); } + ~GitRepoSetup() { teardown(); } - fs::current_path(VALID_GIT_PATH); -} - -void teardown() { fs::remove_all(VALID_GIT_PATH); } + void setup() { + // set up a git repo with an actual commit + fs::copy(TEST_FILE_PATH, VALID_GIT_PATH, fs::copy_options::recursive); -// TODO: test cases -TEST_CASE("tag command", "[tag]") { - setup(); - SECTION("Valid git tag command - tag name only") { - std::vector args({"tag", "v1.0"}); - commands::tag(args); + fs::current_path(VALID_GIT_PATH); + } - REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true)); - REQUIRE(fs::exists(".git/refs/tags/v1.0")); - REQUIRE(file_contents(".git/refs/tags/v1.0") == SECOND_COMMIT); + void teardown() { + fs::remove_all(VALID_GIT_PATH); + fs::current_path(OLD_CWD); } - teardown(); +}; - setup(); - SECTION("Valid git tag command - tag name with commit") { - std::vector args({"tag", "v1.0", SECOND_COMMIT}); - commands::tag(args); +TEST_CASE("tag command", "[tag]") { + GitRepoSetup gitRepoSetup; + SECTION("Valid git tag command - tag name only", "tag name only") { + { + std::vector args({"tag", "v1.0"}); + commands::tag(args); + + REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true)); + REQUIRE(fs::exists(".git/refs/tags/v1.0")); + REQUIRE(file_contents(".git/refs/tags/v1.0") == SECOND_COMMIT); + } + SECTION("Valid git tag command - tag name with commit", + "tag name with commit") { + std::vector args({"tag", "v1.0", SECOND_COMMIT}); + commands::tag(args); - REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true)); - REQUIRE(fs::exists(".git/refs/tags/v1.0")); - REQUIRE(file_contents(".git/refs/tags/v1.0") == SECOND_COMMIT); + REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true)); + REQUIRE(fs::exists(".git/refs/tags/v1.0")); + REQUIRE(file_contents(".git/refs/tags/v1.0") == SECOND_COMMIT); + } } - teardown(); } -TEST_CASE("tag with errors", "[tag]") { - setup(); - SECTION("Git tag failed with missing tag name") {} - teardown(); - - setup(); - SECTION("Git tag failed with non-existent commit") {} - teardown(); -} +// TEST_CASE("tag with errors", "[tag errors]") { +// // setup(); +// SECTION("Git tag failed with missing tag name") {} +// SECTION("Git tag failed with non-existent commit") {} +// // teardown(); +// } From ee9e50bae0f49fdcdf36bd1c82768eddd96c9fe6 Mon Sep 17 00:00:00 2001 From: Chow Jia Ying Date: Wed, 14 May 2025 23:20:50 +0800 Subject: [PATCH 4/6] Add test cases --- app/gyt.cpp | 3 --- src/commands/tag.cpp | 46 +++++++++++++++++------------------- tests/tag_t.cpp | 56 +++++++++++++++++++++++++++----------------- 3 files changed, 56 insertions(+), 49 deletions(-) diff --git a/app/gyt.cpp b/app/gyt.cpp index 4711a83..01e72fb 100644 --- a/app/gyt.cpp +++ b/app/gyt.cpp @@ -35,9 +35,6 @@ int main(int argc, char **argv) { } else if (command == "show-ref") { commands::showref(args); } else if (command == "tag") { - - std::vector test({"tag", "v1.0"}); - commands::tag(test); commands::tag(args); } else { std::cerr << "Unknown command: " << command << "\n"; diff --git a/src/commands/tag.cpp b/src/commands/tag.cpp index ff3d00d..c453a79 100644 --- a/src/commands/tag.cpp +++ b/src/commands/tag.cpp @@ -11,32 +11,28 @@ void tag(std::vector &args) { TagParser &parser = TagParser::get(); parser.parse(args); // process args - try { - std::optional repo = GitRepository::find(); - if (!repo) { - throw std::runtime_error("Not a git repository"); - } - // resolve ref, basically find the commit object - - std::string commit = parser.isCommitSet() ? parser.getCommit() - : GitObject::find(*repo, "HEAD"); - std::string tag = parser.getTag(); - // ERROR: if the commit doesn't exist. - if (fs::is_regular_file(get_commit_path(commit))) { - std::string error_message = commit + ": not a valid commit"; - throw std::runtime_error(error_message); - } - // ERROR: if the tag already exists - fs::path tag_path = repo->repo_path("refs/tags/" + tag); - if (fs::is_regular_file(tag_path)) { - std::string error_message = "tag '" + tag + "' already exists"; - throw std::runtime_error(error_message); - } + std::optional repo = GitRepository::find(); + if (!repo) { + throw std::runtime_error("Not a git repository"); + } + // resolve ref, basically find the commit object - // create a new file in refs/tags/{name}, and write the sha of the commit. - create_file(tag_path, commit + "\n"); - } catch (std::runtime_error &err) { - std::cerr << err.what() << "\n"; + std::string commit = parser.isCommitSet() ? parser.getCommit() + : GitObject::find(*repo, "HEAD"); + std::string tag = parser.getTag(); + // ERROR: if the commit doesn't exist. + if (fs::is_regular_file(get_commit_path(commit))) { + std::string error_message = commit + ": not a valid commit"; + throw std::runtime_error(error_message); } + // ERROR: if the tag already exists + fs::path tag_path = repo->repo_path("refs/tags/" + tag); + if (fs::is_regular_file(tag_path)) { + std::string error_message = "tag '" + tag + "' already exists"; + throw std::runtime_error(error_message); + } + + // create a new file in refs/tags/{name}, and write the sha of the commit. + create_file(tag_path, commit + "\n"); } } // namespace commands diff --git a/tests/tag_t.cpp b/tests/tag_t.cpp index fb2c8e4..417668c 100644 --- a/tests/tag_t.cpp +++ b/tests/tag_t.cpp @@ -50,29 +50,43 @@ class GitRepoSetup { TEST_CASE("tag command", "[tag]") { GitRepoSetup gitRepoSetup; SECTION("Valid git tag command - tag name only", "tag name only") { - { - std::vector args({"tag", "v1.0"}); - commands::tag(args); + std::vector args({"tag", "v1.0"}); + commands::tag(args); - REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true)); - REQUIRE(fs::exists(".git/refs/tags/v1.0")); - REQUIRE(file_contents(".git/refs/tags/v1.0") == SECOND_COMMIT); - } - SECTION("Valid git tag command - tag name with commit", - "tag name with commit") { - std::vector args({"tag", "v1.0", SECOND_COMMIT}); - commands::tag(args); + REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true)); + REQUIRE(fs::exists(".git/refs/tags/v1.0")); + REQUIRE(file_contents(".git/refs/tags/v1.0") == SECOND_COMMIT); + } + SECTION("Valid git tag command - tag name with commit", + "tag name with commit") { + std::vector args({"tag", "v1.0", SECOND_COMMIT}); + commands::tag(args); - REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true)); - REQUIRE(fs::exists(".git/refs/tags/v1.0")); - REQUIRE(file_contents(".git/refs/tags/v1.0") == SECOND_COMMIT); - } + REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true)); + REQUIRE(fs::exists(".git/refs/tags/v1.0")); + REQUIRE(file_contents(".git/refs/tags/v1.0") == SECOND_COMMIT); } } -// TEST_CASE("tag with errors", "[tag errors]") { -// // setup(); -// SECTION("Git tag failed with missing tag name") {} -// SECTION("Git tag failed with non-existent commit") {} -// // teardown(); -// } +TEST_CASE("tag with errors", "[tag errors]") { + GitRepoSetup gitRepoSetup; + SECTION("Git tag already exists") { + std::vector args({"tag", "v1.0"}); + commands::tag(args); + + REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true)); + REQUIRE(fs::exists(".git/refs/tags/v1.0")); + REQUIRE(file_contents(".git/refs/tags/v1.0") == SECOND_COMMIT); + + std::vector args2({"tag", "v1.0"}); + REQUIRE_THROWS_WITH(commands::tag(args2), "tag 'v1.0' already exists"); + } + SECTION("Git tag failed with non-existent commit") { + std::vector args({"tag", "v1.0", "nonexistentcommit"}); + REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true)); + + REQUIRE_THROWS_WITH(commands::tag(args), + "nonexistentcommit: not a valid commit"); + REQUIRE(!fs::exists(".git/refs/tags/v1.0")); + } +} From 9283b7d80d15515356f5c39dd7ed1ca8c345823d Mon Sep 17 00:00:00 2001 From: Chow Jia Ying Date: Thu, 15 May 2025 07:53:52 +0800 Subject: [PATCH 5/6] change repo when needed --- include/repository.h | 2 +- src/repository.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/repository.h b/include/repository.h index d4d5e4e..3592ca9 100644 --- a/include/repository.h +++ b/include/repository.h @@ -9,7 +9,7 @@ namespace fs = std::filesystem; class GitRepository { public: GitRepository(fs::path worktree, fs::path gitdir); - GitRepository(std::string path, bool force = false); + GitRepository(const std::string &path, bool force = false); static std::optional find(const fs::path &path = fs::path("."), bool required = true); diff --git a/src/repository.cpp b/src/repository.cpp index 7590583..52852cd 100644 --- a/src/repository.cpp +++ b/src/repository.cpp @@ -6,7 +6,7 @@ namespace fs = std::filesystem; -GitRepository::GitRepository(std::string path, bool force) +GitRepository::GitRepository(const std::string &path, bool force) : worktree(fs::path(path)), gitdir(fs::path(path) / ".git") { if (!force && !fs::is_directory(gitdir)) { std::cerr << "Not a git repository: " << gitdir << "\n"; @@ -111,4 +111,4 @@ std::optional GitRepository::find(const fs::path &path, // TODO: update this to support refs void GitRepository::update_head(const std::string &new_head) { create_file(gitdir / "HEAD", new_head + "\n"); -} \ No newline at end of file +} From 2a89c99f75cfe4bf16693ea04e136e4c921fbfe4 Mon Sep 17 00:00:00 2001 From: Chow Jia Ying Date: Thu, 15 May 2025 08:20:36 +0800 Subject: [PATCH 6/6] Fix tests and make singleton pattern better --- doc/case studies/Singleton.md | 10 ++++++++++ include/parsers/TagParser.h | 7 +++++++ src/commands/tag.cpp | 6 ++---- 3 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 doc/case studies/Singleton.md diff --git a/doc/case studies/Singleton.md b/doc/case studies/Singleton.md new file mode 100644 index 0000000..00a4fbc --- /dev/null +++ b/doc/case studies/Singleton.md @@ -0,0 +1,10 @@ +# Parser and singleton + +`TagParser` is an example of a singleton class. +It was largely adapted from: https://stackoverflow.com/questions/75249277/is-singleton-with-static-unique-ptr-a-good-practice + +It stores a reference of itself as a static member. The constructor constructs and sets the CmdLine arguments. + +Some learning points: +- CmdLine, Arg classes has no default constructor, so they have to be constructed during the construction of `TagParser`, not after. +- Using a `static` reference of itself. \ No newline at end of file diff --git a/include/parsers/TagParser.h b/include/parsers/TagParser.h index f190bb2..6d9124d 100644 --- a/include/parsers/TagParser.h +++ b/include/parsers/TagParser.h @@ -14,7 +14,14 @@ class TagParser { std::string getCommit(); private: + // hides constructors to avoid accidental instantiation TagParser(); + TagParser(const TagParser &) = delete; + TagParser &operator=(const TagParser &) = delete; + TagParser(TagParser &&) = delete; + TagParser &operator=(TagParser &&) = delete; + ~TagParser() = default; + // data members TCLAP::CmdLine cmd; TCLAP::UnlabeledValueArg tagArg; TCLAP::UnlabeledValueArg commitArg; diff --git a/src/commands/tag.cpp b/src/commands/tag.cpp index c453a79..eb60742 100644 --- a/src/commands/tag.cpp +++ b/src/commands/tag.cpp @@ -3,7 +3,6 @@ #include "parsers/TagParser.h" #include "repository.h" #include "util.h" -#include #include namespace commands { @@ -15,13 +14,12 @@ void tag(std::vector &args) { if (!repo) { throw std::runtime_error("Not a git repository"); } - // resolve ref, basically find the commit object - + // resolve ref to find the commit hash std::string commit = parser.isCommitSet() ? parser.getCommit() : GitObject::find(*repo, "HEAD"); std::string tag = parser.getTag(); // ERROR: if the commit doesn't exist. - if (fs::is_regular_file(get_commit_path(commit))) { + if (!fs::exists(repo->repo_path(get_commit_path(commit)))) { std::string error_message = commit + ": not a valid commit"; throw std::runtime_error(error_message); }