git2wrapC++20 wrapper for libgit2 | 
          
| git clone git://git.dimitrijedobrota.com/git2wrap.git | 
| Log | Files | Refs | README | LICENSE | HACKING | CONTRIBUTING | CODE_OF_CONDUCT | BUILDING | 
| commit | eb6f4f335bd61d025be76cfef7febe34f655d0f8 | 
| parent | 11f4f27c1e02d678c8ee77f5f12ebda5daf44c49 | 
| author | Dimitrije Dobrota < mail@dimitrijedobrota.com > | 
| date | Sun, 5 Jan 2025 16:23:18 +0100 | 
Proof of concept
* const char * is used because empty.c_str() != NULL
| M | .clang-tidy | | | ++++++ | 
| M | CMakeLists.txt | | | +++++ -- | 
| M | cmake/dev-mode.cmake | | | ----- | 
| D | cmake/docs-ci.cmake | | | --------------------------------------------------------------------------------- | 
| D | cmake/docs.cmake | | | ---------------------------------------------- | 
| D | docs/Doxyfile.in | | | -------------------------------- | 
| D | docs/conf.py.in | | | ------ | 
| D | docs/pages/about.dox | | | ------- | 
| M | include/git2wrap/git2wrap.hpp | | | +++++++++++++++++++++++++++++++++++++++++++ --------------------------------------- | 
| D | source/git2wrap.cpp | | | ------------- | 
| A | source/libgit2.cpp | | | ++++++++++++++++++ | 
| A | source/repository.cpp | | | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| M | test/source/git2wrap_test.cpp | | | +++++++++++++++ ---- | 
13 files changed, 176 insertions(+), 285 deletions(-)
diff --git a/ .clang-tidy b/ .clang-tidy
          @@ -11,9 +11,15 @@ 
          Checks: "*,\
        
        
            -llvm-include-order,\
            -llvmlibc-*,\
            -modernize-use-nodiscard,\
            -modernize-use-trailing-return-type,\
            -cppcoreguidelines-pro-type-vararg,\
            -hicpp-vararg,\
            -misc-include-cleaner,\
            -misc-non-private-member-variables-in-classes"
          WarningsAsErrors: ''
          CheckOptions:
            - key: 'misc-include-cleaner.IgnoreHeaders'
              value: "git2.h"
            - key: 'bugprone-argument-comment.StrictMode'
              value: 'true'
          # Prefer using enum classes with 2 values for parameters instead of bools
        
        diff --git a/ CMakeLists.txt b/ CMakeLists.txt
          @@ -4,7 +4,7 @@ 
          include(cmake/prelude.cmake)
        
        
          project(
              git2wrap
              VERSION 0.1.0
              VERSION 0.1.1
              DESCRIPTION "C++ 20 wrapper for libgit2"
              HOMEPAGE_URL "https://git.dimitrijedobrota.com/git2wrap.git"
              LANGUAGES CXX
        
        
          @@ -17,10 +17,13 @@ 
          include(cmake/variables.cmake)
        
        
          add_library(
              git2wrap_git2wrap
              source/git2wrap.cpp
              source/libgit2.cpp
              source/repository.cpp
          )
          add_library(git2wrap::git2wrap ALIAS git2wrap_git2wrap)
          target_link_libraries(git2wrap_git2wrap PRIVATE git2)
          include(GenerateExportHeader)
          generate_export_header(
              git2wrap_git2wrap
        
        diff --git a/ cmake/dev-mode.cmake b/ cmake/dev-mode.cmake
          @@ -5,11 +5,6 @@ 
          if(BUILD_TESTING)
        
        
            add_subdirectory(test)
          endif()
          option(BUILD_MCSS_DOCS "Build documentation using Doxygen and m.css" OFF)
          if(BUILD_MCSS_DOCS)
            include(cmake/docs.cmake)
          endif()
          option(ENABLE_COVERAGE "Enable coverage support separate from CTest's" OFF)
          if(ENABLE_COVERAGE)
            include(cmake/coverage.cmake)
        
        diff --git a/ cmake/docs-ci.cmake b/ cmake/docs-ci.cmake
@@ -1,112 +0,0 @@
cmake_minimum_required(VERSION 3.14)
          foreach(var IN ITEMS PROJECT_BINARY_DIR PROJECT_SOURCE_DIR)
            if(NOT DEFINED "${var}")
              message(FATAL_ERROR "${var} must be defined")
            endif()
          endforeach()
          set(bin "${PROJECT_BINARY_DIR}")
          set(src "${PROJECT_SOURCE_DIR}")
          # ---- Dependencies ----
          set(mcss_SOURCE_DIR "${bin}/docs/.ci")
          if(NOT IS_DIRECTORY "${mcss_SOURCE_DIR}")
            file(MAKE_DIRECTORY "${mcss_SOURCE_DIR}")
            file(
                DOWNLOAD
                https://github.com/friendlyanon/m.css/releases/download/release-1/mcss.zip
                "${mcss_SOURCE_DIR}/mcss.zip"
                STATUS status
                EXPECTED_MD5 00cd2757ebafb9bcba7f5d399b3bec7f
            )
            if(NOT status MATCHES "^0;")
              message(FATAL_ERROR "Download failed with ${status}")
            endif()
            execute_process(
                COMMAND "${CMAKE_COMMAND}" -E tar xf mcss.zip
                WORKING_DIRECTORY "${mcss_SOURCE_DIR}"
                RESULT_VARIABLE result
            )
            if(NOT result EQUAL "0")
              message(FATAL_ERROR "Extraction failed with ${result}")
            endif()
            file(REMOVE "${mcss_SOURCE_DIR}/mcss.zip")
          endif()
          find_program(Python3_EXECUTABLE NAMES python3 python)
          if(NOT Python3_EXECUTABLE)
            message(FATAL_ERROR "Python executable was not found")
          endif()
          # ---- Process project() call in CMakeLists.txt ----
          file(READ "${src}/CMakeLists.txt" content)
          string(FIND "${content}" "project(" index)
          if(index EQUAL "-1")
            message(FATAL_ERROR "Could not find \"project(\"")
          endif()
          string(SUBSTRING "${content}" "${index}" -1 content)
          string(FIND "${content}" "\n)\n" index)
          if(index EQUAL "-1")
            message(FATAL_ERROR "Could not find \"\\n)\\n\"")
          endif()
          string(SUBSTRING "${content}" 0 "${index}" content)
          file(WRITE "${bin}/docs-ci.project.cmake" "docs_${content}\n)\n")
          macro(list_pop_front list out)
            list(GET "${list}" 0 "${out}")
            list(REMOVE_AT "${list}" 0)
          endmacro()
          function(docs_project name)
            cmake_parse_arguments(PARSE_ARGV 1 "" "" "VERSION;DESCRIPTION;HOMEPAGE_URL" LANGUAGES)
            set(PROJECT_NAME "${name}" PARENT_SCOPE)
            if(DEFINED _VERSION)
              set(PROJECT_VERSION "${_VERSION}" PARENT_SCOPE)
              string(REGEX MATCH "^[0-9]+(\\.[0-9]+)*" versions "${_VERSION}")
              string(REPLACE . ";" versions "${versions}")
              set(suffixes MAJOR MINOR PATCH TWEAK)
              while(NOT versions STREQUAL "" AND NOT suffixes STREQUAL "")
                list_pop_front(versions version)
                list_pop_front(suffixes suffix)
                set("PROJECT_VERSION_${suffix}" "${version}" PARENT_SCOPE)
              endwhile()
            endif()
            if(DEFINED _DESCRIPTION)
              set(PROJECT_DESCRIPTION "${_DESCRIPTION}" PARENT_SCOPE)
            endif()
            if(DEFINED _HOMEPAGE_URL)
              set(PROJECT_HOMEPAGE_URL "${_HOMEPAGE_URL}" PARENT_SCOPE)
            endif()
          endfunction()
          include("${bin}/docs-ci.project.cmake")
          # ---- Generate docs ----
          if(NOT DEFINED DOXYGEN_OUTPUT_DIRECTORY)
            set(DOXYGEN_OUTPUT_DIRECTORY "${bin}/docs")
          endif()
          set(out "${DOXYGEN_OUTPUT_DIRECTORY}")
          foreach(file IN ITEMS Doxyfile conf.py)
            configure_file("${src}/docs/${file}.in" "${bin}/docs/${file}" @ONLY)
          endforeach()
          set(mcss_script "${mcss_SOURCE_DIR}/documentation/doxygen.py")
          set(config "${bin}/docs/conf.py")
          file(REMOVE_RECURSE "${out}/html" "${out}/xml")
          execute_process(
              COMMAND "${Python3_EXECUTABLE}" "${mcss_script}" "${config}"
              WORKING_DIRECTORY "${bin}/docs"
              RESULT_VARIABLE result
          )
          if(NOT result EQUAL "0")
            message(FATAL_ERROR "m.css returned with ${result}")
          endif()
        
        diff --git a/ cmake/docs.cmake b/ cmake/docs.cmake
@@ -1,46 +0,0 @@
# ---- Dependencies ----
          set(extract_timestamps "")
          if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.24")
            set(extract_timestamps DOWNLOAD_EXTRACT_TIMESTAMP YES)
          endif()
          include(FetchContent)
          FetchContent_Declare(
              mcss URL
              https://github.com/friendlyanon/m.css/releases/download/release-1/mcss.zip
              URL_MD5 00cd2757ebafb9bcba7f5d399b3bec7f
              SOURCE_DIR "${PROJECT_BINARY_DIR}/mcss"
              UPDATE_DISCONNECTED YES
              ${extract_timestamps}
          )
          FetchContent_MakeAvailable(mcss)
          find_package(Python3 3.6 REQUIRED)
          # ---- Declare documentation target ----
          set(
              DOXYGEN_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/docs"
              CACHE PATH "Path for the generated Doxygen documentation"
          )
          set(working_dir "${PROJECT_BINARY_DIR}/docs")
          foreach(file IN ITEMS Doxyfile conf.py)
            configure_file("docs/${file}.in" "${working_dir}/${file}" @ONLY)
          endforeach()
          set(mcss_script "${mcss_SOURCE_DIR}/documentation/doxygen.py")
          set(config "${working_dir}/conf.py")
          add_custom_target(
              docs
              COMMAND "${CMAKE_COMMAND}" -E remove_directory
              "${DOXYGEN_OUTPUT_DIRECTORY}/html"
              "${DOXYGEN_OUTPUT_DIRECTORY}/xml"
              COMMAND "${Python3_EXECUTABLE}" "${mcss_script}" "${config}"
              COMMENT "Building documentation using Doxygen and m.css"
              WORKING_DIRECTORY "${working_dir}"
              VERBATIM
          )
        
        diff --git a/ docs/Doxyfile.in b/ docs/Doxyfile.in
@@ -1,32 +0,0 @@
# Configuration for Doxygen for use with CMake
          # Only options that deviate from the default are included
          # To create a new Doxyfile containing all available options, call `doxygen -g`
          # Get Project name and version from CMake
          PROJECT_NAME = "@PROJECT_NAME@"
          PROJECT_NUMBER = "@PROJECT_VERSION@"
          # Add sources
          INPUT = "@PROJECT_SOURCE_DIR@/README.md" "@PROJECT_SOURCE_DIR@/include" "@PROJECT_SOURCE_DIR@/docs/pages"
          EXTRACT_ALL = YES
          RECURSIVE = YES
          OUTPUT_DIRECTORY = "@DOXYGEN_OUTPUT_DIRECTORY@"
          # Use the README as a main page
          USE_MDFILE_AS_MAINPAGE = "@PROJECT_SOURCE_DIR@/README.md"
          # set relative include paths
          FULL_PATH_NAMES = YES
          STRIP_FROM_PATH = "@PROJECT_SOURCE_DIR@/include" "@PROJECT_SOURCE_DIR@"
          STRIP_FROM_INC_PATH =
          # We use m.css to generate the html documentation, so we only need XML output
          GENERATE_XML = YES
          GENERATE_HTML = NO
          GENERATE_LATEX = NO
          XML_PROGRAMLISTING = NO
          CREATE_SUBDIRS = NO
          # Include all directories, files and namespaces in the documentation
          # Disable to include only explicitly documented objects
          M_SHOW_UNDOCUMENTED = YES
        
        diff --git a/ docs/conf.py.in b/ docs/conf.py.in
@@ -1,6 +0,0 @@
DOXYFILE = 'Doxyfile'
          LINKS_NAVBAR1 = [
              (None, 'pages', [(None, 'about')]),
              (None, 'namespaces', []),
          ]
        
        diff --git a/ docs/pages/about.dox b/ docs/pages/about.dox
@@ -1,7 +0,0 @@
/**
           * @page about About
           * @section about-doxygen Doxygen documentation
           * This page is auto generated using
           * <a href="https://www.doxygen.nl/">Doxygen</a>, making use of some useful
           * <a href="https://www.doxygen.nl/manual/commands.html">special commands</a>.
           */
        
        diff --git a/ include/git2wrap/git2wrap.hpp b/ include/git2wrap/git2wrap.hpp
@@ -1,70 +1,77 @@
#pragma once
          #include <exception>
          #include <string>
          #include <git2.h>
          #include "git2wrap/git2wrap_export.hpp"
          /**
           * A note about the MSVC warning C4251:
           * This warning should be suppressed for private data members of the project's
           * exported classes, because there are too many ways to work around it and all
           * involve some kind of trade-off (increased code complexity requiring more
           * developer time, writing boilerplate code, longer compile times), but those
           * solutions are very situational and solve things in slightly different ways,
           * depending on the requirements of the project.
           * That is to say, there is no general solution.
           *
           * What can be done instead is understand where issues could arise where this
           * warning is spotting a legitimate bug. I will give the general description of
           * this warning's cause and break it down to make it trivial to understand.
           *
           * C4251 is emitted when an exported class has a non-static data member of a
           * non-exported class type.
           *
           * The exported class in our case is the class below (exported_class), which
           * has a non-static data member (m_name) of a non-exported class type
           * (std::string).
           *
           * The rationale here is that the user of the exported class could attempt to
           * access (directly, or via an inline member function) a static data member or
           * a non-inline member function of the data member, resulting in a linker
           * error.
           * Inline member function above means member functions that are defined (not
           * declared) in the class definition.
           *
           * Since this exported class never makes these non-exported types available to
           * the user, we can safely ignore this warning. It's fine if there are
           * non-exported class types as private member variables, because they are only
           * accessed by the members of the exported class itself.
           *
           * The name() method below returns a pointer to the stored null-terminated
           * string as a fundamental type (char const), so this is safe to use anywhere.
           * The only downside is that you can have dangling pointers if the pointer
           * outlives the class instance which stored the string.
           *
           * Shared libraries are not easy, they need some discipline to get right, but
           * they also solve some other problems that make them worth the time invested.
           */
          /**
           * @brief Reports the name of the library
           *
           * Please see the note above for considerations when creating shared libraries.
           */
          class GIT2WRAP_EXPORT exported_class
          namespace git2wrap
          {
          class GIT2WRAP_EXPORT error : public std::exception
          {
          public:
            /**
             * @brief Initializes the name field to the name of the project
             */
            exported_class();
            explicit error(int err, const git_error* git_err)
                : m_error(err)
                , m_klass(git_err->klass)
                , m_message(git_err->message)
            {
            }
            /**
             * @brief Returns a non-owning pointer to the string stored in this class
             */
            auto name() const -> char const*;
            const char* get_message() const { return m_message.c_str(); }
            int get_klass() const { return m_klass; }
            int get_error() const { return m_error; }
          private:
            GIT2WRAP_SUPPRESS_C4251
            std::string m_name;
            int m_error;
            int m_klass;
            std::string m_message;
          };
          class GIT2WRAP_EXPORT libgit2
          {
          public:
            libgit2();
            ~libgit2();
            libgit2(const libgit2&) = delete;
            libgit2(libgit2&&) = delete;
            libgit2& operator=(const libgit2&) = delete;
            libgit2& operator=(libgit2&&) = delete;
          private:
            int m_cinit = 0;
          };
          class GIT2WRAP_EXPORT repository
          {
          public:
            using init_options = git_repository_init_options;
            using clone_options = git_clone_options;
            explicit repository(git_repository* repo);
            repository(const char* path, unsigned is_bare);
            repository(const char* path, init_options* opts);
            ~repository();
            repository(const repository&) = delete;
            repository(repository&&) = delete;
            repository& operator=(const repository&) = delete;
            repository& operator=(repository&&) = delete;
            static repository clone(const char* url,
                                    const char* local_path,
                                    const clone_options* options);
            static repository open(const char* path);
            static repository open(const char* path,
                                   unsigned flags,
                                   const char* ceiling_dirs);
          private:
            git_repository* m_repo = nullptr;
          };
          }  // namespace git2wrap
        
        diff --git a/ source/git2wrap.cpp b/ source/git2wrap.cpp
@@ -1,13 +0,0 @@
#include <string>
          #include "git2wrap/git2wrap.hpp"
          exported_class::exported_class()
              : m_name {"git2wrap"}
          {
          }
          auto exported_class::name() const -> char const*
          {
            return m_name.c_str();
          }
        
        diff --git a/ source/libgit2.cpp b/ source/libgit2.cpp
@@ -0,0 +1,18 @@
#include "git2wrap/git2wrap.hpp"
          namespace git2wrap
          {
          libgit2::libgit2()
          {
            if (m_cinit = git_libgit2_init(); m_cinit < 0) {
              throw error(m_cinit, git_error_last());
            }
          }
          libgit2::~libgit2()
          {
            git_libgit2_shutdown();
          }
          }  // namespace git2wrap
        
        diff --git a/ source/repository.cpp b/ source/repository.cpp
@@ -0,0 +1,67 @@
#include "git2wrap/git2wrap.hpp"
          namespace git2wrap
          {
          repository::repository(git_repository* repo)
              : m_repo(repo)
          {
          }
          repository::repository(const char* path, unsigned is_bare)
          {
            if (auto err = git_repository_init(&m_repo, path, is_bare)) {
              throw error(err, git_error_last());
            }
          }
          repository::repository(const char* path, init_options* opts)
          {
            if (auto err = git_repository_init_ext(&m_repo, path, opts)) {
              throw error(err, git_error_last());
            }
          }
          repository::~repository()
          {
            git_repository_free(m_repo);
          }
          repository repository::clone(const char* url,
                                       const char* local_path,
                                       const clone_options* options)
          {
            git_repository* repo = nullptr;
            if (auto err = git_clone(&repo, url, local_path, options)) {
              throw error(err, git_error_last());
            }
            return repository(repo);
          }
          repository repository::open(const char* path)
          {
            git_repository* repo = nullptr;
            if (auto err = git_repository_open(&repo, path)) {
              throw error(err, git_error_last());
            }
            return repository(repo);
          }
          repository repository::open(const char* path,
                                      unsigned flags,
                                      const char* ceiling_dirs)
          {
            git_repository* repo = nullptr;
            if (auto err = git_repository_open_ext(&repo, path, flags, ceiling_dirs)) {
              throw error(err, git_error_last());
            }
            return repository(repo);
          }
          }  // namespace git2wrap
        
        diff --git a/ test/source/git2wrap_test.cpp b/ test/source/git2wrap_test.cpp
@@ -1,10 +1,21 @@
#include <string>
          #include <format>
          #include <iostream>
          #include "git2wrap/git2wrap.hpp"
          auto main() -> int
          int main()
          {
            auto const exported = exported_class {};
            try {
              using namespace git2wrap;  // NOLINT
            return std::string("git2wrap") == exported.name() ? 0 : 1;
              const libgit2 git;
            } catch (const git2wrap::error& err) {
              std::cerr << std::format("Error %d/%d: %s\n",
                                       err.get_error(),
                                       err.get_klass(),
                                       err.get_message());
            }
            return 0;
          }