Skip to content

Commit 993d195

Browse files
committed
Initial commit
0 parents  commit 993d195

22 files changed

+394
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
build/
2+
__pycache__/

.pytest.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[pytest]
2+
testpaths =
3+
test

LICENSE

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
BSD 3-Clause License
2+
3+
Copyright (c) 2025, QuantStack.
4+
All rights reserved.
5+
6+
Redistribution and use in source and binary forms, with or without
7+
modification, are permitted provided that the following conditions are met:
8+
9+
1. Redistributions of source code must retain the above copyright notice, this
10+
list of conditions and the following disclaimer.
11+
12+
2. Redistributions in binary form must reproduce the above copyright notice,
13+
this list of conditions and the following disclaimer in the documentation
14+
and/or other materials provided with the distribution.
15+
16+
3. Neither the name of the copyright holder nor the names of its
17+
contributors may be used to endorse or promote products derived from
18+
this software without specific prior written permission.
19+
20+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# git2cpp
2+
3+
This is a C++ wrapper of [libgit2](https://libgit2.org/) to provide a command-line interface (CLI)
4+
to `git` functionality. The intended use is in WebAssembly in-browser terminals (see
5+
[cockle](https://github.com/jupyterlite/cockle) and
6+
[JupyterLite terminal](https://github.com/jupyterlite/terminal) projects) but it can be compiled and
7+
used on any POSIX-compliant system.
8+
9+
See `overview.md` for further details.
10+
11+
Developer's workflow using `micromamba` to manage the dependencies:
12+
13+
```bash
14+
micromamba create -f dev-environment.yml
15+
micromamba activate git2cpp-dev
16+
meson setup build
17+
cd build
18+
meson compile
19+
```
20+
21+
The `git2cpp` executable can then be run, e.g. `./git2cpp -v`.
22+
23+
The CLI is tested using `python`. From the top-level directory:
24+
25+
```bash
26+
pytest -v
27+
```

dev-environment.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
name: git2cpp-dev
2+
channels:
3+
- conda-forge
4+
dependencies:
5+
- cli11
6+
- libgit2
7+
- meson
8+
- pkg-config
9+
- python
10+
- pytest

meson.build

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
project(
2+
'git2cpp',
3+
'cpp',
4+
default_options: [
5+
'cpp_std=c++20',
6+
#'werror=true',
7+
],
8+
license: 'BSD-3-Clause',
9+
)
10+
11+
cli11_dep = dependency('CLI11')
12+
libgit2_dep = dependency('libgit2')
13+
14+
subdir('src')
15+
16+
executable(
17+
'git2cpp',
18+
src_files,
19+
dependencies: [
20+
cli11_dep,
21+
libgit2_dep,
22+
],
23+
)

overview.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Overview
2+
3+
The motivation is to provide a `git`-like command-line interface (CLI) to the
4+
[JupyterLite terminal](https://github.com/jupyterlite/terminal) but allow most of the development
5+
work to occur outside of a WebAssembly toolchain for ease.
6+
7+
[libgit2](https://libgit2.org/) is a portable pure C implementation of `git` core functionality to
8+
allow library access rather than a CLI. There are many different programming language wrappers for
9+
it. It does include some CLI functionality in the form of examples, but these are not of core
10+
interest to most maintainers and users of the library, and are therefore not complete.
11+
12+
The idea here is to write a C++ wrapper to `libgit2` to provide a CLI, starting with simple `git`
13+
functionality and expanding the scope over time. All development work can occur on POSIX systems
14+
away from a WebAssembly toolchain, and the final step is to use
15+
[Emscripten-forge](https://emscripten-forge.org/) recipes to build it for
16+
[cockle](https://github.com/jupyterlite/cockle), just like other commands.
17+
18+
There are two areas of functionality to be written in C++:
19+
1. Thin C++ wrappers of the various `libgit2` C objects to give automatical deallocation of
20+
resources as necessary.
21+
2. `git` command and subcommand implementations using [CLI11](https://github.com/CLIUtils/CLI11)
22+
for argument parsing, as it used in [mamba](https://github.com/mamba-org/mamba) for example.
23+
24+
`libgit2` is well tested and well maintained, so we do not envisage unit testing of our C++
25+
wrappers. The testing is therefore of the CLI, and hence does not have to be performed in C++ but
26+
can use `python` or `node` or similar instead.
27+
28+
We will ignore some aspects of `git` initially that are hard to implement in a single-thread
29+
WebAssembly runtime, such as opening an editor for `git rebase` and so on. Instead we will
30+
probably use a fairly simple sequence of prompts and accepting `stdin` inputs from the user, and
31+
work towards full integration with editors at a later date.
32+
33+
The `git2cpp` name of the project and executable follow from `libgit2` which includes its own `git2`
34+
and hence it would not be sensible to duplicate that name. In a terminal in which this is the only
35+
`git` implementation available we would expect users to set up aliases so that the usual `git`
36+
command name can be used.
37+
38+
The alternative approach which we have rejected would be to start with the `git` source code itself,
39+
and build that for WebAssembly. That would involve removing functionality such as starting new
40+
processes to open editors by patching the source code for the WebAssembly build. That approach would
41+
be a much less pleasant developer experience than the one proposed here, given that all work would
42+
have to occur using a WebAssembly toolchain.
43+
44+
All design decisions here are subject to change based on who the main developers will be and their
45+
preferred tools, etc.

src/git_exception.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#include <git2.h>
2+
3+
#include "git_exception.hpp"
4+
5+
void throwIfError(int exitCode)
6+
{
7+
if (exitCode < 0) {
8+
throw GitException(git_error_last()->message, exitCode);
9+
}
10+
}
11+
12+
13+
GitException::GitException(const std::string& message, int errorCode)
14+
: _message(message), _errorCode(errorCode)
15+
{}
16+
17+
int GitException::errorCode() const
18+
{
19+
return _errorCode;
20+
}
21+
22+
const char* GitException::what() const noexcept
23+
{
24+
return _message.c_str();
25+
}

src/git_exception.hpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#pragma once
2+
3+
#include <exception>
4+
#include <string>
5+
6+
void throwIfError(int exitCode);
7+
8+
class GitException : public std::exception
9+
{
10+
public:
11+
GitException(const std::string& message, int errorCode);
12+
13+
int errorCode() const;
14+
15+
const char* what() const noexcept override;
16+
17+
private:
18+
std::string _message;
19+
int _errorCode;
20+
};

src/main.cpp

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#include <CLI/CLI.hpp>
2+
#include <git2.h> // For version number only
3+
#include <iostream>
4+
5+
#include "git_exception.hpp"
6+
#include "version.hpp"
7+
#include "subcommand/init_subcommand.hpp"
8+
9+
int main(int argc, char** argv)
10+
{
11+
int exitCode = 0;
12+
try
13+
{
14+
CLI::App app{"Git using C++ wrapper of libgit2"};
15+
16+
// Top-level command options.
17+
auto version = app.add_flag("-v,--version", "Show version");
18+
19+
// Sub commands
20+
InitSubcommand init(app);
21+
22+
app.parse(argc, argv);
23+
24+
if (version->count())
25+
{
26+
std::cout << "git2cpp version " << GIT2CPP_VERSION_STRING << " (libgit2 " << LIBGIT2_VERSION << ")" << std::endl;
27+
}
28+
}
29+
catch (const CLI::Error& e)
30+
{
31+
std::cerr << e.what() << std::endl;
32+
exitCode = 1;
33+
}
34+
catch (const GitException& e)
35+
{
36+
std::cerr << e.what() << std::endl;
37+
exitCode = e.errorCode();
38+
}
39+
catch (std::exception& e) {
40+
std::cerr << e.what() << std::endl;
41+
exitCode = 1;
42+
}
43+
44+
return exitCode;
45+
}

src/meson.build

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
subdir('subcommand')
2+
subdir('wrapper')
3+
4+
src_files = files([
5+
'git_exception.cpp',
6+
'main.cpp'
7+
]) + subcommand_files + wrapper_files

src/subcommand/base_subcommand.hpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#pragma once
2+
3+
#include <CLI/CLI.hpp>
4+
5+
class BaseSubcommand
6+
{
7+
public:
8+
BaseSubcommand() = default;
9+
10+
virtual ~BaseSubcommand() = default;
11+
12+
BaseSubcommand(const BaseSubcommand&) = delete;
13+
BaseSubcommand& operator=(const BaseSubcommand&) = delete;
14+
BaseSubcommand(BaseSubcommand&&) = delete;
15+
BaseSubcommand& operator=(BaseSubcommand&&) = delete;
16+
};

src/subcommand/init_subcommand.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#include "init_subcommand.hpp"
2+
3+
//#include "../wrapper/repository_wrapper.hpp"
4+
5+
InitSubcommand::InitSubcommand(CLI::App& app)
6+
{
7+
auto *sub = app.add_subcommand("init", "Explanation of init here");
8+
9+
sub->add_flag("--bare", bare, "--- bare ---");
10+
11+
sub->callback([this]() { this->run(); });
12+
}
13+
14+
void InitSubcommand::run()
15+
{
16+
std::cout << "RUN " << bare << std::endl;
17+
//RepositoryWrapper repo;
18+
//repo.init(bare);
19+
}

src/subcommand/init_subcommand.hpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#pragma once
2+
3+
#include "base_subcommand.hpp"
4+
5+
class InitSubcommand : public BaseSubcommand
6+
{
7+
public:
8+
InitSubcommand(CLI::App& app);
9+
void run();
10+
11+
private:
12+
bool bare;
13+
};

src/subcommand/meson.build

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
subcommand_files = files([
2+
'init_subcommand.cpp',
3+
])

src/version.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#pragma once
2+
3+
#define GIT2CPP_VERSION_MAJOR 0
4+
#define GIT2CPP_VERSION_MINOR 0
5+
#define GIT2CPP_VERSION_PATCH 1
6+
7+
// e.g. ".rc0"
8+
#define GIT2CPP_VERSION_SUFFIX
9+
10+
#define STR_HELPER(x) #x
11+
#define STR(x) STR_HELPER(x)
12+
#define GIT2CPP_VERSION_STRING (STR(GIT2CPP_VERSION_MAJOR) "." STR(GIT2CPP_VERSION_MINOR) "." STR(GIT2CPP_VERSION_PATCH) GIT2CPP_VERSION_SUFFIX)

src/wrapper/base_wrapper.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#include <git2.h>
2+
3+
#include "../git_exception.hpp"
4+
#include "base_wrapper.hpp"
5+
6+
BaseWrapper::BaseWrapper()
7+
{
8+
// Fine to initialise libgit2 multiple times.
9+
throwIfError(git_libgit2_init());
10+
}
11+
12+
BaseWrapper::~BaseWrapper()
13+
{
14+
git_libgit2_shutdown();
15+
}

src/wrapper/base_wrapper.hpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#pragma once
2+
3+
#include <git2.h>
4+
5+
class BaseWrapper
6+
{
7+
public:
8+
BaseWrapper();
9+
10+
virtual ~BaseWrapper();
11+
12+
BaseWrapper(const BaseWrapper&) = delete;
13+
BaseWrapper& operator=(const BaseWrapper&) = delete;
14+
BaseWrapper(BaseWrapper&&) = delete;
15+
BaseWrapper& operator=(BaseWrapper&&) = delete;
16+
};

src/wrapper/meson.build

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
wrapper_files = files([
2+
'base_wrapper.cpp',
3+
'repository_wrapper.cpp',
4+
])

src/wrapper/repository_wrapper.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#include <iostream>
2+
#include <string>
3+
4+
#include "../git_exception.hpp"
5+
#include "repository_wrapper.hpp"
6+
7+
RepositoryWrapper::RepositoryWrapper()
8+
: _repo(nullptr)
9+
{}
10+
11+
RepositoryWrapper::~RepositoryWrapper()
12+
{
13+
if (_repo != nullptr) {
14+
git_repository_free(_repo); // no return
15+
}
16+
}
17+
18+
void RepositoryWrapper::init(bool bare)
19+
{
20+
std::cout << "repo init - start" << std::endl;
21+
22+
// what if it is already initialised???
23+
24+
// convert error code to exception
25+
std::string path = "repo";
26+
throwIfError(git_repository_init(&_repo, path.c_str(), bare));
27+
28+
std::cout << "repo init - end " << std::endl;
29+
}

src/wrapper/repository_wrapper.hpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#pragma once
2+
3+
#include "base_wrapper.hpp"
4+
5+
class RepositoryWrapper : public BaseWrapper
6+
{
7+
public:
8+
RepositoryWrapper();
9+
10+
virtual ~RepositoryWrapper();
11+
12+
void init(bool bare);
13+
14+
private:
15+
git_repository *_repo;
16+
};

0 commit comments

Comments
 (0)