diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..3c4e9f0 --- /dev/null +++ b/.clang-format @@ -0,0 +1,89 @@ +# From https://github.com/man-group/sparrow with sparrow-specific regexes removed. + +BasedOnStyle: Mozilla + +AccessModifierOffset: '-4' +AlignAfterOpenBracket: BlockIndent +AlignEscapedNewlines: Left +AllowAllArgumentsOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: false +# Forbid one line lambdas because clang-format makes a weird split when +# single instructions lambdas are too long. +AllowShortLambdasOnASingleLine: Empty +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Allman +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeComma +BreakInheritanceList: AfterComma +BreakStringLiterals: false +ColumnLimit: '110' +ConstructorInitializerIndentWidth: '4' +ContinuationIndentWidth: '4' +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +EmptyLineAfterAccessModifier: Always +EmptyLineBeforeAccessModifier: Always +ExperimentalAutoDetectBinPacking: true +IncludeBlocks: Regroup +IncludeCategories: +- Regex: <[^.]+> + Priority: 1 +- Regex: <.+> + Priority: 2 +- Regex: '".+"' + Priority: 5 +IndentCaseLabels: true +IndentPPDirectives: AfterHash +IndentWidth: '4' +IndentWrappedFunctionNames: false +InsertBraces: true +InsertTrailingCommas: Wrapped +KeepEmptyLinesAtTheStartOfBlocks: false +LambdaBodyIndentation: Signature +Language: Cpp +MaxEmptyLinesToKeep: '2' +NamespaceIndentation: All +ObjCBlockIndentWidth: '4' +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: false +PackConstructorInitializers: Never +PenaltyBreakAssignment: 100000 +PenaltyBreakBeforeFirstCallParameter: 0 +PenaltyBreakComment: 10 +PenaltyBreakOpenParenthesis: 0 +PenaltyBreakTemplateDeclaration: 0 +PenaltyExcessCharacter: 10 +PenaltyIndentedWhitespace: 0 +PenaltyReturnTypeOnItsOwnLine: 10 +PointerAlignment: Left +QualifierAlignment: Custom # Experimental +QualifierOrder: [inline, static, constexpr, const, volatile, type] +ReflowComments: true +SeparateDefinitionBlocks: Always +SortIncludes: CaseInsensitive +SortUsingDeclarations: true +SpaceAfterCStyleCast: true +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: '2' +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: c++20 +TabWidth: '4' +UseTab: Never diff --git a/.github/workflows/deploy-wasm.yml b/.github/workflows/deploy-wasm.yml index caff961..94a4de7 100644 --- a/.github/workflows/deploy-wasm.yml +++ b/.github/workflows/deploy-wasm.yml @@ -39,10 +39,11 @@ jobs: - name: Upload artifact uses: actions/upload-pages-artifact@v4 with: - path: ./wasm/serve + path: ./wasm/serve/dist deploy: needs: build + if: github.ref == 'refs/heads/main' permissions: pages: write id-token: write diff --git a/.github/workflows/test-wasm.yml b/.github/workflows/test-wasm.yml new file mode 100644 index 0000000..5d3052f --- /dev/null +++ b/.github/workflows/test-wasm.yml @@ -0,0 +1,54 @@ +# Test WebAssembly build in cockle deployment +name: Test WebAssembly + +on: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: '3.14' + + - name: Install mamba + uses: mamba-org/setup-micromamba@v2 + with: + environment-file: wasm/wasm-environment.yml + cache-environment: true + + - name: Build + shell: bash -l {0} + working-directory: wasm + run: | + cmake . + make build-recipe + make build-test + + - name: Install playwright chromium + shell: bash -l {0} + working-directory: wasm/test + run: | + npx playwright install chromium + + - name: Upload artifact containing emscripten-forge package + uses: actions/upload-pages-artifact@v4 + with: + path: ./wasm/recipe/em-forge-recipes/output/ + + - name: Run WebAssembly tests + shell: bash -l {0} + working-directory: wasm + env: + GIT2CPP_TEST_PRIVATE_TOKEN: ${{ secrets.GIT2CPP_TEST_PRIVATE_TOKEN }} + run: | + make test diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6bd1a6c..37adfb0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,11 @@ defaults: jobs: test: name: 'Build and test' - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] steps: - name: Checkout source uses: actions/checkout@v6 @@ -44,6 +48,57 @@ jobs: ./git2cpp -v - name: Run tests + env: + GIT2CPP_TEST_PRIVATE_TOKEN: ${{ secrets.GIT2CPP_TEST_PRIVATE_TOKEN }} + run: | + pytest -v + + coverage: + name: 'Test coverage' + runs-on: ubuntu-latest + steps: + - name: Checkout source + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Create micromamba environment + uses: mamba-org/setup-micromamba@main + with: + environment-file: dev-environment.yml + cache-environment: true + create-args: lcov + + - name: Configure CMake + run: | + cmake -Bbuild -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS_DEBUG="-g -O0 --coverage" + + - name: Build with CMake + working-directory: build + run: cmake --build . --parallel 8 + + - name: Run tests + env: + GIT2CPP_TEST_PRIVATE_TOKEN: ${{ secrets.GIT2CPP_TEST_PRIVATE_TOKEN }} run: | pytest -v + - name: Collect C++ coverage + run: | + lcov --version + lcov --output-file coverage.lcov --directory . --capture + lcov --output-file coverage.lcov --extract coverage.lcov '*/git2cpp/src/*' + genhtml coverage.lcov --output-directory outdir + + - name: Upload artifact containing coverage report + uses: actions/upload-artifact@v7 + with: + name: coverage_report + path: outdir + + - name: Upload coverage to codecov + uses: codecov/codecov-action@v5 + with: + files: coverage.lcov + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true diff --git a/.gitignore b/.gitignore index 9e99726..8a84302 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ __pycache__/ .cache/ compile_commands.json serve.log +test/test-results/ + +docs/_build/ +docs/created/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..6389e1a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,31 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace + + - repo: https://github.com/asottile/pyupgrade + rev: v3.21.2 + hooks: + - id: pyupgrade + args: ['--py312-plus'] + + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v22.1.1 + hooks: + - id: clang-format + args: [--style=file] + exclude_types: [javascript,json] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.7 + hooks: + - id: ruff-check + args: [--fix] + - id: ruff-format diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..e1768e1 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,21 @@ +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: mambaforge-23.11 + jobs: + post_install: + - cmake -Bbuild -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX + - cd build && make + - python -m pip install myst-parser sphinx sphinx-book-theme + pre_build: + - cd docs && python create_markdown.py + +conda: + environment: dev-environment.yml + +sphinx: + builder: html + configuration: docs/conf.py + fail_on_warning: true diff --git a/CMakeLists.txt b/CMakeLists.txt index 3212e5a..940e418 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,8 @@ set(GIT2CPP_SRC ${GIT2CPP_SOURCE_DIR}/subcommand/checkout_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/subcommand/clone_subcommand.cpp ${GIT2CPP_SOURCE_DIR}/subcommand/clone_subcommand.hpp + ${GIT2CPP_SOURCE_DIR}/subcommand/diff_subcommand.cpp + ${GIT2CPP_SOURCE_DIR}/subcommand/diff_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/subcommand/commit_subcommand.cpp ${GIT2CPP_SOURCE_DIR}/subcommand/commit_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/subcommand/config_subcommand.cpp @@ -60,6 +62,8 @@ set(GIT2CPP_SRC ${GIT2CPP_SOURCE_DIR}/subcommand/log_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/subcommand/merge_subcommand.cpp ${GIT2CPP_SOURCE_DIR}/subcommand/merge_subcommand.hpp + ${GIT2CPP_SOURCE_DIR}/subcommand/mv_subcommand.cpp + ${GIT2CPP_SOURCE_DIR}/subcommand/mv_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/subcommand/push_subcommand.cpp ${GIT2CPP_SOURCE_DIR}/subcommand/push_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/subcommand/rebase_subcommand.cpp @@ -72,22 +76,40 @@ 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/rm_subcommand.cpp + ${GIT2CPP_SOURCE_DIR}/subcommand/rm_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}/subcommand/tag_subcommand.cpp + ${GIT2CPP_SOURCE_DIR}/subcommand/tag_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/utils/ansi_code.cpp ${GIT2CPP_SOURCE_DIR}/utils/ansi_code.hpp ${GIT2CPP_SOURCE_DIR}/utils/common.cpp ${GIT2CPP_SOURCE_DIR}/utils/common.hpp + ${GIT2CPP_SOURCE_DIR}/utils/credentials.cpp + ${GIT2CPP_SOURCE_DIR}/utils/credentials.hpp ${GIT2CPP_SOURCE_DIR}/utils/git_exception.cpp ${GIT2CPP_SOURCE_DIR}/utils/git_exception.hpp - ${GIT2CPP_SOURCE_DIR}/utils/output.cpp - ${GIT2CPP_SOURCE_DIR}/utils/output.hpp + ${GIT2CPP_SOURCE_DIR}/utils/input_output.cpp + ${GIT2CPP_SOURCE_DIR}/utils/input_output.hpp ${GIT2CPP_SOURCE_DIR}/utils/progress.cpp ${GIT2CPP_SOURCE_DIR}/utils/progress.hpp ${GIT2CPP_SOURCE_DIR}/utils/terminal_pager.cpp ${GIT2CPP_SOURCE_DIR}/utils/terminal_pager.hpp + ${GIT2CPP_SOURCE_DIR}/wasm/libgit2_internals.cpp + ${GIT2CPP_SOURCE_DIR}/wasm/libgit2_internals.hpp + ${GIT2CPP_SOURCE_DIR}/wasm/response.cpp + ${GIT2CPP_SOURCE_DIR}/wasm/response.hpp + ${GIT2CPP_SOURCE_DIR}/wasm/scope.cpp + ${GIT2CPP_SOURCE_DIR}/wasm/scope.hpp + ${GIT2CPP_SOURCE_DIR}/wasm/stream.cpp + ${GIT2CPP_SOURCE_DIR}/wasm/stream.hpp + ${GIT2CPP_SOURCE_DIR}/wasm/subtransport.cpp + ${GIT2CPP_SOURCE_DIR}/wasm/subtransport.hpp + ${GIT2CPP_SOURCE_DIR}/wasm/transport.cpp + ${GIT2CPP_SOURCE_DIR}/wasm/transport.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/annotated_commit_wrapper.cpp ${GIT2CPP_SOURCE_DIR}/wrapper/annotated_commit_wrapper.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/branch_wrapper.cpp @@ -96,10 +118,16 @@ set(GIT2CPP_SRC ${GIT2CPP_SOURCE_DIR}/wrapper/commit_wrapper.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/config_wrapper.cpp ${GIT2CPP_SOURCE_DIR}/wrapper/config_wrapper.hpp + ${GIT2CPP_SOURCE_DIR}/wrapper/diff_wrapper.cpp + ${GIT2CPP_SOURCE_DIR}/wrapper/diff_wrapper.hpp + ${GIT2CPP_SOURCE_DIR}/wrapper/diffstats_wrapper.cpp + ${GIT2CPP_SOURCE_DIR}/wrapper/diffstats_wrapper.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/index_wrapper.cpp ${GIT2CPP_SOURCE_DIR}/wrapper/index_wrapper.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/object_wrapper.cpp ${GIT2CPP_SOURCE_DIR}/wrapper/object_wrapper.hpp + ${GIT2CPP_SOURCE_DIR}/wrapper/patch_wrapper.cpp + ${GIT2CPP_SOURCE_DIR}/wrapper/patch_wrapper.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/rebase_wrapper.cpp ${GIT2CPP_SOURCE_DIR}/wrapper/rebase_wrapper.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/refs_wrapper.cpp @@ -114,6 +142,10 @@ set(GIT2CPP_SRC ${GIT2CPP_SOURCE_DIR}/wrapper/signature_wrapper.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/status_wrapper.cpp ${GIT2CPP_SOURCE_DIR}/wrapper/status_wrapper.hpp + ${GIT2CPP_SOURCE_DIR}/wrapper/tag_wrapper.cpp + ${GIT2CPP_SOURCE_DIR}/wrapper/tag_wrapper.hpp + ${GIT2CPP_SOURCE_DIR}/wrapper/tree_wrapper.cpp + ${GIT2CPP_SOURCE_DIR}/wrapper/tree_wrapper.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/wrapper_base.hpp ${GIT2CPP_SOURCE_DIR}/main.cpp ${GIT2CPP_SOURCE_DIR}/version.hpp diff --git a/README.md b/README.md index b9cc19f..9341b79 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # git2cpp +[![GithubActions](https://github.com/QuantStack/git2cpp/actions/workflows/test.yml/badge.svg)](https://github.com/QuantStack/git2cpp/actions/workflows/test.yml) +[![Documentation Status](http://readthedocs.org/projects/git2cpp/badge/?version=latest)](https://git2cpp.readthedocs.io/en/latest/?badge=latest) +[![Codecov](https://codecov.io/gh/QuantStack/git2cpp/graph/badge.svg)](https://app.codecov.io/gh/QuantStack/git2cpp) This is a C++ wrapper of [libgit2](https://libgit2.org/) to provide a command-line interface (CLI) to `git` functionality. The intended use is in WebAssembly in-browser terminals (see @@ -8,6 +11,10 @@ used on any POSIX-compliant system. See `overview.md` for further details. +## Development workflow + +### Build + Developer's workflow using `micromamba` to manage the dependencies: ```bash @@ -20,13 +27,31 @@ make -j8 The `git2cpp` executable can then be run, e.g. `./git2cpp -v`. +### Test + The CLI is tested using `python`. From the top-level directory: ```bash pytest -v ``` -# WebAssembly build and deployment +Some tests access the private repository at https://github.com/QuantStack/git2cpp-test-private using +a fine-grained github Personal Access Token (PAT). These tests are skipped by default. To run them +you will need to obtain the PAT from one of the maintainers, and run the tests as follows: + +```bash +GIT2CPP_TEST_PRIVATE_TOKEN= pytest -v +``` + +### pre-commit + +`pre-commit` runs automatically on `git commit`. To run it manually use: + +```bash +pre-commit run --all-files +``` + +## WebAssembly build and deployment The `wasm` directory contains everything needed to build the local `git2cpp` source code as an WebAssembly [Emscripten-forge](https://emscripten-forge.org/) package, create local @@ -38,3 +63,22 @@ See the `README.md` in the `wasm` directory for further details. The latest `cockle` and JupyterLite `terminal` deployments using `git2cpp` are available at [https://quantstack.net/git2cpp](https://quantstack.net/git2cpp) + +## Documentation + +The project documentation is generated from the `git2cpp` help pages. To build the documentation +locally first build `git2cpp` as usual as described above, then install the documentation +dependencies: + +```bash +micromamba install myst-parser sphinx sphinx-book-theme +``` + +and build the documentation: + +```bash +cd docs +make html +``` + +The top-level documentation page will be `docs/_build/html/index.html` diff --git a/RELEASE.md b/RELEASE.md index 2b01a44..684930f 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -21,6 +21,6 @@ This covers making a new github release in the `git2cpp` repository, and propaga The Emscripten-forge recipe at https://github.com/emscripten-forge/recipes needs to be updated with the new version number and SHA checksum. An Emscripten-forge bot runs once a day and will identify the new github release and create a PR to update the recipe. Wait for this to happen, and if the tests pass and no further changes are required, the PR can be approved and merged. -After the PR is merged to `main`, the recipe will be rebuilt and uploaded to https://prefix.dev/channels/emscripten-forge-dev/packages/git2cpp, which should only take a few minutes. +After the PR is merged to `main`, the recipe will be rebuilt and uploaded to https://prefix.dev/channels/emscripten-forge-4x/packages/git2cpp, which should only take a few minutes. Any subsequent `cockle` or JupyterLite `terminal` deployments that are rebuilt will download and use the latest `git2cpp` WebAssembly package. diff --git a/dev-environment.yml b/dev-environment.yml index c578ec0..288a620 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -6,6 +6,7 @@ dependencies: - libgit2 - cmake - pkg-config + - pre-commit - python - pytest - termcolor-cpp diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..cadcd23 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,18 @@ +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + python create_markdown.py + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..3bf0cde --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,20 @@ +from datetime import date + +project = "git2cpp" +author = "QuantStack" +copyright = f"2025-{date.today().year}" + +extensions = [ + "myst_parser", +] + +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +html_static_path = [] +html_theme = "sphinx_book_theme" +html_theme_options = { + "github_url": "https://github.com/QuantStack/git2cpp", + "home_page_in_toc": True, + "show_navbar_depth": 2, +} +html_title = "git2cpp documentation" diff --git a/docs/create_markdown.py b/docs/create_markdown.py new file mode 100644 index 0000000..0c7bec2 --- /dev/null +++ b/docs/create_markdown.py @@ -0,0 +1,87 @@ +import os +from pathlib import Path +import re +import subprocess + + +def get_filename(args): + directory = Path("created").joinpath(*args[:-1]) + filename = args[-1] + ".md" + return directory / filename + + +def sanitise_line(line): + # Remove trailing whitespace otherwise the markdown parser can insert an extra \n + line = line.rstrip() + + # Replace angular brackets with HTML equivalents. + line = line.replace(r"&", r"&") + line = line.replace(r"<", r"<") + line = line.replace(r">", r">") + + # If there are whitespace characters at the start of the line, replace the first with an   + # so that it is not discarded by the markdown parser used by the parsed-literal directive. + line = re.sub(r"^\s", r" ", line) + + return line + + +# Process a single subcommand, adding new subcommands found to to_process. +def process(args, to_process): + cmd = args + ["--help"] + cmd_string = " ".join(cmd) + filename = get_filename(args) + filename.parent.mkdir(parents=True, exist_ok=True) + + print(f"Writing '{cmd_string}' to file {filename}") + p = subprocess.run(cmd, capture_output=True, text=True, check=True) + + # Write output markdown file, identifying subcommands at the same time to provide + # links to the subcommand markdown files. + subcommands = [] + with open(filename, "w") as f: + f.write(f"({filename})=\n") # Target for links. + f.write(f"# {' '.join(args)}\n") + f.write("\n") + f.write("```{parsed-literal}\n") + + in_subcommand_section = False + for line in p.stdout.splitlines(): + if in_subcommand_section: + match = re.match(r"^( )([\w\-_]+)(\s+.*)$", line) + if match: + subcommand = match.group(2) + subcommand_filename = get_filename(args + [subcommand]) + line = ( + match.group(1) + f"[{subcommand}]({subcommand_filename})" + match.group(3) + ) + subcommands.append(subcommand) + elif line.startswith("SUBCOMMANDS:"): + in_subcommand_section = True + + f.write(sanitise_line(line) + "\n") + f.write("```\n") + + subcommands.sort() + to_process.extend(args + [subcommand] for subcommand in subcommands) + + if len(subcommands) > 0: + # Hidden table of contents for subcommands of this command/subcommand. + f.write("\n") + f.write("```{toctree}\n") + f.write(":hidden:\n") + for subcommand in subcommands: + f.write(f"{args[-1]}/{subcommand}\n") + f.write("```\n") + + +if __name__ == "__main__": + # Modify the PATH so that git2cpp is found by name, as using a full path will cause the help + # pages to write that full path. + git2cpp_dir = Path(__file__).parent.parent / "build" + os.environ["PATH"] = f"{git2cpp_dir}{os.pathsep}{os.environ['PATH']}" + + to_process = [["git2cpp"]] + while len(to_process) > 0: + subcommand = to_process.pop(0) + process(subcommand, to_process) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..462e7d4 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,17 @@ +# Overview + +`git2cpp` is a C++ wrapper of [libgit2](https://libgit2.org/) to provide a command-line interface +(CLI) to `git` functionality. The intended use is in WebAssembly in-browser terminals (the +[cockle](https://github.com/jupyterlite/cockle) and +[JupyterLite terminal](https://github.com/jupyterlite/terminal) projects) but it can be compiled and +used on any POSIX-compliant system. + +The Help pages here are generated from the `git2cpp` command and subcommands to show the +functionality that is currently supported. If there are features missing that you would like to use, +please create an issue in the [git2cpp github repository](https://github.com/QuantStack/git2cpp). + +```{toctree} +:caption: Help pages +:hidden: +created/git2cpp +``` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..90d4005 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +# This is not a python project but it contains python tests so here are settings for python linting. +[tool.ruff] +line-length = 100 +target-version = "py312" +[tool.ruff.format] +line-ending = "lf" diff --git a/src/main.cpp b/src/main.cpp index 6aaec34..712885e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,28 +1,33 @@ -#include #include -#include // For version number only #include -#include "utils/git_exception.hpp" -#include "version.hpp" +#include +#include // For version number only + #include "subcommand/add_subcommand.hpp" #include "subcommand/branch_subcommand.hpp" #include "subcommand/checkout_subcommand.hpp" #include "subcommand/clone_subcommand.hpp" #include "subcommand/commit_subcommand.hpp" #include "subcommand/config_subcommand.hpp" +#include "subcommand/diff_subcommand.hpp" #include "subcommand/fetch_subcommand.hpp" #include "subcommand/init_subcommand.hpp" #include "subcommand/log_subcommand.hpp" #include "subcommand/merge_subcommand.hpp" +#include "subcommand/mv_subcommand.hpp" #include "subcommand/push_subcommand.hpp" #include "subcommand/rebase_subcommand.hpp" #include "subcommand/remote_subcommand.hpp" #include "subcommand/reset_subcommand.hpp" +#include "subcommand/revlist_subcommand.hpp" +#include "subcommand/revparse_subcommand.hpp" +#include "subcommand/rm_subcommand.hpp" #include "subcommand/stash_subcommand.hpp" #include "subcommand/status_subcommand.hpp" -#include "subcommand/revparse_subcommand.hpp" -#include "subcommand/revlist_subcommand.hpp" +#include "subcommand/tag_subcommand.hpp" +#include "utils/git_exception.hpp" +#include "version.hpp" int main(int argc, char** argv) { @@ -44,16 +49,20 @@ int main(int argc, char** argv) clone_subcommand clone(lg2_obj, app); commit_subcommand commit(lg2_obj, app); config_subcommand config(lg2_obj, app); + diff_subcommand diff(lg2_obj, app); fetch_subcommand fetch(lg2_obj, app); reset_subcommand reset(lg2_obj, app); log_subcommand log(lg2_obj, app); merge_subcommand merge(lg2_obj, app); + mv_subcommand mv(lg2_obj, app); push_subcommand push(lg2_obj, app); rebase_subcommand rebase(lg2_obj, app); remote_subcommand remote(lg2_obj, app); - revparse_subcommand revparse(lg2_obj, app); revlist_subcommand revlist(lg2_obj, app); + revparse_subcommand revparse(lg2_obj, app); + rm_subcommand rm(lg2_obj, app); stash_subcommand stash(lg2_obj, app); + tag_subcommand tag(lg2_obj, app); app.require_subcommand(/* min */ 0, /* max */ 1); @@ -61,7 +70,8 @@ int main(int argc, char** argv) if (version->count()) { - std::cout << "git2cpp version " << GIT2CPP_VERSION_STRING << " (libgit2 " << LIBGIT2_VERSION << ")" << std::endl; + std::cout << "git2cpp version " << GIT2CPP_VERSION_STRING << " (libgit2 " << LIBGIT2_VERSION + << ")" << std::endl; } else if (app.get_subcommands().size() == 0) { @@ -78,7 +88,8 @@ int main(int argc, char** argv) std::cerr << e.what() << std::endl; exit_code = e.error_code(); } - catch (std::exception& e) { + catch (std::exception& e) + { std::cerr << e.what() << std::endl; exit_code = 1; } diff --git a/src/subcommand/add_subcommand.cpp b/src/subcommand/add_subcommand.cpp index e1aa4f4..ca52940 100644 --- a/src/subcommand/add_subcommand.cpp +++ b/src/subcommand/add_subcommand.cpp @@ -1,13 +1,13 @@ +#include "add_subcommand.hpp" + #include -#include "add_subcommand.hpp" #include "../wrapper/index_wrapper.hpp" #include "../wrapper/repository_wrapper.hpp" - add_subcommand::add_subcommand(const libgit2_object&, CLI::App& app) { - auto *sub = app.add_subcommand("add", "Add file contents to the index"); + auto* sub = app.add_subcommand("add", "Add file contents to the index"); sub->add_option("", m_add_files, "Files to add"); @@ -16,10 +16,14 @@ add_subcommand::add_subcommand(const libgit2_object&, CLI::App& app) // sub->add_flag("-u,--update", update_flag, ""); // sub->add_flag("-v,--verbose", verbose_flag, ""); - sub->callback([this]() { this->run(); }); + sub->callback( + [this]() + { + this->run(); + } + ); }; - void add_subcommand::run() { auto directory = get_current_git_path(); diff --git a/src/subcommand/add_subcommand.hpp b/src/subcommand/add_subcommand.hpp index 6f31dff..0504799 100644 --- a/src/subcommand/add_subcommand.hpp +++ b/src/subcommand/add_subcommand.hpp @@ -12,6 +12,7 @@ class add_subcommand void run(); private: + bool m_all_flag = false; std::vector m_add_files; }; diff --git a/src/subcommand/branch_subcommand.cpp b/src/subcommand/branch_subcommand.cpp index 82decf1..3d233c4 100644 --- a/src/subcommand/branch_subcommand.cpp +++ b/src/subcommand/branch_subcommand.cpp @@ -1,6 +1,7 @@ +#include "../subcommand/branch_subcommand.hpp" + #include -#include "../subcommand/branch_subcommand.hpp" #include "../wrapper/repository_wrapper.hpp" branch_subcommand::branch_subcommand(const libgit2_object&, CLI::App& app) @@ -14,8 +15,18 @@ branch_subcommand::branch_subcommand(const libgit2_object&, CLI::App& app) sub->add_flag("-r,--remotes", m_remote_flag, "List or delete (if used with -d) the remote-tracking branches"); sub->add_flag("-l,--list", m_list_flag, "List branches"); sub->add_flag("-f,--force", m_force_flag, "Skips confirmation"); + sub->add_flag( + "--show-current", + m_show_current_flag, + "Print the name of the current branch. In detached HEAD state, nothing is printed." + ); - sub->callback([this]() { this->run(); }); + sub->callback( + [this]() + { + this->run(); + } + ); } void branch_subcommand::run() @@ -23,7 +34,12 @@ void branch_subcommand::run() auto directory = get_current_git_path(); auto repo = repository_wrapper::open(directory); - if (m_list_flag || m_branch_name.empty()) + if (m_show_current_flag) + { + // TODO: if another flag, return usage/Generic options/Specific git-branch actions + run_show_current(repo); + } + else if (m_list_flag || m_branch_name.empty()) { run_list(repo); } @@ -64,9 +80,20 @@ void branch_subcommand::run_deletion(repository_wrapper& repo) delete_branch(std::move(branch)); } - void branch_subcommand::run_creation(repository_wrapper& repo) { // TODO: handle specification of starting commit repo.create_branch(m_branch_name, m_force_flag); } + +void branch_subcommand::run_show_current(const repository_wrapper& repo) +{ + auto name = repo.head_short_name(); + + if (name == "HEAD") + { + return; + } + + std::cout << name << std::endl; +} diff --git a/src/subcommand/branch_subcommand.hpp b/src/subcommand/branch_subcommand.hpp index b0d95d6..79d7900 100644 --- a/src/subcommand/branch_subcommand.hpp +++ b/src/subcommand/branch_subcommand.hpp @@ -19,6 +19,7 @@ class branch_subcommand void run_list(const repository_wrapper& repo); void run_deletion(repository_wrapper& repo); void run_creation(repository_wrapper& repo); + void run_show_current(const repository_wrapper& repo); std::string m_branch_name = {}; bool m_deletion_flag = false; @@ -26,4 +27,5 @@ class branch_subcommand bool m_remote_flag = false; bool m_list_flag = false; bool m_force_flag = false; + bool m_show_current_flag = false; }; diff --git a/src/subcommand/checkout_subcommand.cpp b/src/subcommand/checkout_subcommand.cpp index 8ae8ebe..1c3dfdb 100644 --- a/src/subcommand/checkout_subcommand.cpp +++ b/src/subcommand/checkout_subcommand.cpp @@ -1,9 +1,13 @@ +#include "../subcommand/checkout_subcommand.hpp" + #include +#include #include -#include "../subcommand/checkout_subcommand.hpp" +#include "../subcommand/status_subcommand.hpp" #include "../utils/git_exception.hpp" #include "../wrapper/repository_wrapper.hpp" +#include "../wrapper/status_wrapper.hpp" checkout_subcommand::checkout_subcommand(const libgit2_object&, CLI::App& app) { @@ -12,9 +16,35 @@ checkout_subcommand::checkout_subcommand(const libgit2_object&, CLI::App& app) sub->add_option("", m_branch_name, "Branch to checkout"); sub->add_flag("-b", m_create_flag, "Create a new branch before checking it out"); sub->add_flag("-B", m_force_create_flag, "Create a new branch or reset it if it exists before checking it out"); - sub->add_flag("-f, --force", m_force_checkout_flag, "When switching branches, proceed even if the index or the working tree differs from HEAD, and even if there are untracked files in the way"); + sub->add_flag( + "-f, --force", + m_force_checkout_flag, + "When switching branches, proceed even if the index or the working tree differs from HEAD, and even if there are untracked files in the way" + ); + + sub->callback( + [this]() + { + this->run(); + } + ); +} + +void print_no_switch(status_list_wrapper& sl) +{ + std::cout << "Your local changes to the following files would be overwritten by checkout:" << std::endl; + + for (const auto* entry : sl.get_entry_list(GIT_STATUS_WT_MODIFIED)) + { + std::cout << "\t" << entry->index_to_workdir->new_file.path << std::endl; + } + for (const auto* entry : sl.get_entry_list(GIT_STATUS_WT_DELETED)) + { + std::cout << "\t" << entry->index_to_workdir->old_file.path << std::endl; + } - sub->callback([this]() { this->run(); }); + std::cout << "Please commit your changes or stash them before you switch branches.\nAborting" << std::endl; + return; } void checkout_subcommand::run() @@ -30,16 +60,22 @@ void checkout_subcommand::run() git_checkout_options options; git_checkout_options_init(&options, GIT_CHECKOUT_OPTIONS_VERSION); - if(m_force_checkout_flag) + if (m_force_checkout_flag) { options.checkout_strategy = GIT_CHECKOUT_FORCE; } + else + { + options.checkout_strategy = GIT_CHECKOUT_SAFE; + } if (m_create_flag || m_force_create_flag) { auto annotated_commit = create_local_branch(repo, m_branch_name, m_force_create_flag); checkout_tree(repo, annotated_commit, m_branch_name, options); update_head(repo, annotated_commit, m_branch_name); + + std::cout << "Switched to a new branch '" << m_branch_name << "'" << std::endl; } else { @@ -51,24 +87,49 @@ void checkout_subcommand::run() buffer << "error: could not resolve pathspec '" << m_branch_name << "'" << std::endl; throw std::runtime_error(buffer.str()); } - checkout_tree(repo, *optional_commit, m_branch_name, options); - update_head(repo, *optional_commit, m_branch_name); + + auto sl = status_list_wrapper::status_list(repo); + try + { + checkout_tree(repo, *optional_commit, m_branch_name, options); + update_head(repo, *optional_commit, m_branch_name); + } + catch (const git_exception& e) + { + if (sl.has_notstagged_header()) + { + print_no_switch(sl); + } + throw e; + } + + if (sl.has_notstagged_header()) + { + bool is_long = false; + bool is_coloured = false; + std::set tracked_dir_set{}; + print_notstagged(sl, tracked_dir_set, is_long, is_coloured); + } + if (sl.has_tobecommited_header()) + { + bool is_long = false; + bool is_coloured = false; + std::set tracked_dir_set{}; + print_tobecommited(sl, tracked_dir_set, is_long, is_coloured); + } + std::cout << "Switched to branch '" << m_branch_name << "'" << std::endl; + print_tracking_info(repo, sl, true); } } -annotated_commit_wrapper checkout_subcommand::create_local_branch -( - repository_wrapper& repo, - const std::string_view target_name, - bool force -) +annotated_commit_wrapper +checkout_subcommand::create_local_branch(repository_wrapper& repo, const std::string_view target_name, bool force) { auto branch = repo.create_branch(target_name, force); return repo.find_annotated_commit(branch); } -void checkout_subcommand::checkout_tree -( +void checkout_subcommand::checkout_tree( const repository_wrapper& repo, const annotated_commit_wrapper& target_annotated_commit, const std::string_view target_name, @@ -79,8 +140,7 @@ void checkout_subcommand::checkout_tree throw_if_error(git_checkout_tree(repo, target_commit, &options)); } -void checkout_subcommand::update_head -( +void checkout_subcommand::update_head( repository_wrapper& repo, const annotated_commit_wrapper& target_annotated_commit, const std::string_view target_name diff --git a/src/subcommand/checkout_subcommand.hpp b/src/subcommand/checkout_subcommand.hpp index e041174..99661d4 100644 --- a/src/subcommand/checkout_subcommand.hpp +++ b/src/subcommand/checkout_subcommand.hpp @@ -17,23 +17,17 @@ class checkout_subcommand private: - annotated_commit_wrapper create_local_branch - ( - repository_wrapper& repo, - const std::string_view target_name, - bool force - ); + annotated_commit_wrapper + create_local_branch(repository_wrapper& repo, const std::string_view target_name, bool force); - void checkout_tree - ( + void checkout_tree( const repository_wrapper& repo, const annotated_commit_wrapper& target_annotated_commit, const std::string_view target_name, const git_checkout_options& options ); - void update_head - ( + void update_head( repository_wrapper& repo, const annotated_commit_wrapper& target_annotated_commit, const std::string_view target_name diff --git a/src/subcommand/clone_subcommand.cpp b/src/subcommand/clone_subcommand.cpp index acc6f14..af945ce 100644 --- a/src/subcommand/clone_subcommand.cpp +++ b/src/subcommand/clone_subcommand.cpp @@ -1,8 +1,11 @@ +#include "../subcommand/clone_subcommand.hpp" + #include -#include "../subcommand/clone_subcommand.hpp" -#include "../utils/output.hpp" +#include "../utils/credentials.hpp" +#include "../utils/input_output.hpp" #include "../utils/progress.hpp" +#include "../wasm/scope.hpp" #include "../wrapper/repository_wrapper.hpp" clone_subcommand::clone_subcommand(const libgit2_object&, CLI::App& app) @@ -12,15 +15,23 @@ clone_subcommand::clone_subcommand(const libgit2_object&, CLI::App& app) sub->add_option("", m_repository, "The (possibly remote) repository to clone from.")->required(); sub->add_option("", m_directory, "The name of a new directory to clone into."); sub->add_option("--depth", m_depth, "Create a shallow clone of that depth."); - // sub->add_option("--shallow-since", m_shallow_since, "