git2wrap

C++20 wrapper for libgit2
git clone git://git.dimitrijedobrota.com/git2wrap.git
Log | Files | Refs | README | LICENSE | HACKING | CONTRIBUTING | CODE_OF_CONDUCT | BUILDING |

commiteb6f4f335bd61d025be76cfef7febe34f655d0f8
parent11f4f27c1e02d678c8ee77f5f12ebda5daf44c49
authorDimitrije Dobrota <mail@dimitrijedobrota.com>
dateSun, 5 Jan 2025 16:23:18 +0100

Proof of concept * const char * is used because empty.c_str() != NULL

Diffstat:
M.clang-tidy|++++++
MCMakeLists.txt|+++++--
Mcmake/dev-mode.cmake|-----
Dcmake/docs-ci.cmake|---------------------------------------------------------------------------------
Dcmake/docs.cmake|----------------------------------------------
Ddocs/Doxyfile.in|--------------------------------
Ddocs/conf.py.in|------
Ddocs/pages/about.dox|-------
Minclude/git2wrap/git2wrap.hpp|+++++++++++++++++++++++++++++++++++++++++++---------------------------------------
Dsource/git2wrap.cpp|-------------
Asource/libgit2.cpp|++++++++++++++++++
Asource/repository.cpp|+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/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;
}