├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── erlang.yml ├── .gitignore ├── .gitmodules ├── .vscode └── settings.json ├── LICENSE ├── Makefile ├── README.md ├── c_src ├── Makefile ├── git.cpp ├── git_add.hpp ├── git_atoms.hpp ├── git_branch.hpp ├── git_cat_file.hpp ├── git_checkout.hpp ├── git_commit.hpp ├── git_config.hpp ├── git_index.hpp ├── git_remote.hpp ├── git_rev_list.hpp ├── git_rev_parse.hpp ├── git_status.hpp ├── git_tag.hpp └── git_utils.hpp ├── rebar.config ├── rebar.lock └── src ├── egit.app.src └── git.erl /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a project bug 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Environment** 11 | 12 | - CPU: ... 13 | - OS: ... 14 | - Elixir version: ... 15 | - Erlang version: ... 16 | - egit commit: ... 17 | - libgit2 version: ... 18 | 19 | **Bug description** 20 | A clear and concise description of what the bug is. 21 | -------------------------------------------------------------------------------- /.github/workflows/erlang.yml: -------------------------------------------------------------------------------- 1 | name: Erlang CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | otp: ["erlang:27"] 18 | 19 | container: 20 | image: ${{ matrix.otp }} 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Install latest g++ 25 | run: | 26 | ls -l /usr/bin/g++* 27 | update-alternatives --install /usr/bin/g++ g++ /usr/bin/gcc-12 100 28 | g++ --version 29 | git config --global user.name "Test User" 30 | git config --global user.email "test@gmail.com" 31 | - name: Install libgit2 32 | run: | 33 | apt-get update -y && apt-get install -y libgit2-dev 34 | - name: Compile 35 | run: make 36 | - name: Dialyze 37 | run: make check 38 | - name: Run tests 39 | run: make test 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.dump 3 | *.crashdump 4 | /c_src/*.o 5 | /priv/*.so 6 | /_build 7 | /doc 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "c_src/fmt"] 2 | path = c_src/fmt 3 | url = https://github.com/fmtlib/fmt.git 4 | ignore = dirty 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "makefile.configurations": [ 3 | { 4 | "name": "Default", 5 | "makeArgs": [] 6 | }, 7 | { 8 | "name": "Print make version", 9 | "makeArgs": ["--version"] 10 | } 11 | ], 12 | "cmake.configureOnOpen": false, 13 | "files.associations": { 14 | "*.mqh": "cpp", 15 | "*.mq4": "cpp", 16 | "*.mq5": "cpp", 17 | "*.hrl": "erlang", 18 | "*.es": "erlang", 19 | "format": "cpp", 20 | "array": "cpp", 21 | "hash_map": "cpp", 22 | "string_view": "cpp", 23 | "initializer_list": "cpp", 24 | "span": "cpp", 25 | "atomic": "cpp", 26 | "bit": "cpp", 27 | "*.tcc": "cpp", 28 | "cctype": "cpp", 29 | "charconv": "cpp", 30 | "clocale": "cpp", 31 | "cmath": "cpp", 32 | "compare": "cpp", 33 | "concepts": "cpp", 34 | "cstdarg": "cpp", 35 | "cstddef": "cpp", 36 | "cstdint": "cpp", 37 | "cstdio": "cpp", 38 | "cstdlib": "cpp", 39 | "cstring": "cpp", 40 | "ctime": "cpp", 41 | "cwchar": "cpp", 42 | "cwctype": "cpp", 43 | "deque": "cpp", 44 | "list": "cpp", 45 | "string": "cpp", 46 | "unordered_map": "cpp", 47 | "vector": "cpp", 48 | "exception": "cpp", 49 | "algorithm": "cpp", 50 | "functional": "cpp", 51 | "iterator": "cpp", 52 | "memory": "cpp", 53 | "memory_resource": "cpp", 54 | "netfwd": "cpp", 55 | "numeric": "cpp", 56 | "optional": "cpp", 57 | "random": "cpp", 58 | "ratio": "cpp", 59 | "system_error": "cpp", 60 | "tuple": "cpp", 61 | "type_traits": "cpp", 62 | "utility": "cpp", 63 | "fstream": "cpp", 64 | "iosfwd": "cpp", 65 | "istream": "cpp", 66 | "limits": "cpp", 67 | "new": "cpp", 68 | "numbers": "cpp", 69 | "ostream": "cpp", 70 | "sstream": "cpp", 71 | "stdexcept": "cpp", 72 | "streambuf": "cpp", 73 | "cinttypes": "cpp", 74 | "typeinfo": "cpp", 75 | "variant": "cpp", 76 | "chrono": "cpp", 77 | "typeindex": "cpp", 78 | "any": "cpp", 79 | "codecvt": "cpp", 80 | "complex": "cpp", 81 | "condition_variable": "cpp", 82 | "map": "cpp", 83 | "set": "cpp", 84 | "iomanip": "cpp", 85 | "iostream": "cpp", 86 | "mutex": "cpp", 87 | "semaphore": "cpp", 88 | "stop_token": "cpp", 89 | "thread": "cpp", 90 | "source_location": "cpp" 91 | } 92 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR=$(shell which rebar3) 2 | 3 | ifeq ($(REBAR),) 4 | $(error ERROR: rebar3 not found in PATH) 5 | endif 6 | 7 | all: compile 8 | 9 | compile clean: 10 | @$(REBAR) $@ 11 | 12 | test: 13 | $(REBAR) eunit 14 | 15 | run: 16 | $(REBAR) shell 17 | 18 | check: 19 | $(REBAR) dialyzer 20 | 21 | docs: 22 | $(REBAR) ex_doc 23 | 24 | publish cut: 25 | $(REBAR) hex $@ -r hexpm $(if $(replace),--replace) $(if $(noconfirm),--yes) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # egit - Erlang interface to Git 2 | 3 | [![build](https://github.com/saleyn/egit/actions/workflows/erlang.yml/badge.svg)](https://github.com/saleyn/egit/actions/workflows/erlang.yml) 4 | 5 | This project is an Erlang NIF wrapper to `libgit2` library. It allows to 6 | execute commands to access and manage a `git` repository without depending 7 | on the external `git` tool and internally doesn't involve any parsing of 8 | text output produced by the `git` executable. 9 | 10 | Though it appears to be stable, the project is currently in the beta stage. 11 | 12 | Source code: https://github.com/saleyn/egit 13 | 14 | Documentation: https://hexdocs.pm/egit 15 | 16 | ## Currently supported functionality 17 | 18 | - Init a repository (including creation of bare repositories) 19 | - Clone a repository 20 | - Open a repository at given local path 21 | - Fetch from remote 22 | - Pull from remote 23 | - Push to remote 24 | - Add files to repository 25 | - Commit 26 | - Checkout 27 | - Get status 28 | - Cat-file 29 | - Rev-parse 30 | - Rev-list 31 | - Branch list/create/rename/delete 32 | - Configuration get/set at various levels (e.g. system/global/local/app/default) 33 | - List files in index 34 | - List/add/delete/rename/set-url on a remote 35 | - List/create/delete tags 36 | - Reset 37 | 38 | ## Installation 39 | 40 | - Make sure you have `libgit2` installed. 41 | - On Ubuntu run: `sudo apt-get install libgit2-dev` 42 | - On Arch Linux run: `sudo pacman -S libgit2` 43 | - On Mac OS run: `brew install libgit2` 44 | 45 | - If you are building locally from source, clone [egit](https://github.com/saleyn/egit) 46 | and run: 47 | ```shell 48 | $ make 49 | ``` 50 | 51 | - For Erlang projects add the dependency in `rebar.config`: 52 | ```erlang 53 | {deps, 54 | [% ... 55 | {egit, "~> 0.1"} 56 | ]}. 57 | ``` 58 | 59 | - For Elixir projects add the dependency in `mix.exs`: 60 | ```elixir 61 | def deps do 62 | [ 63 | {:egit, "~> 0.1"} 64 | ] 65 | end 66 | ``` 67 | 68 | ## Usage 69 | 70 | To clone a repository, give it a URL and a local path: 71 | ```erlang 72 | 1> Repo = git:clone("http://github.com/saleyn/egit.git", "/tmp"). 73 | #Ref<...> 74 | ``` 75 | 76 | To open a local repository, give it a path: 77 | ```erlang 78 | 1> Repo = git:open(<<"/tmp/egit">>). 79 | #Ref<...> 80 | ``` 81 | 82 | All functions accept either charlists or binaries as arguments, so 83 | they work conveniently in Erlang and Elixir. 84 | 85 | The cloned/opened repository resource is owned by the current process, 86 | and will be automatically garbage collected when the owner process 87 | exits. 88 | 89 | After obtaining a repository reference, you can call functions in the 90 | `git` module as illustrated below. For complete reference of supported 91 | functions see the [documentation](https://hexdocs.pm/egit/git.html). 92 | 93 | ### Erlang Example 94 | 95 | ```erlang 96 | 2> git:branch_create(R, "tmp", [{target, <<"1b74c46">>}]). 97 | ok 98 | 3> git:checkout(R, "tmp"). 99 | ok 100 | 4> file:write_file("/tmp/egit/temp.txt", <<"This is a test">>). 101 | ok 102 | 5> git:add(R, "."). 103 | #{mode => added,files => [<<"temp.txt">>]} 104 | 6> git:commit(R, "Add test files"). 105 | ok 106 | 7> git:cat_file(R, <<"tmp">>, [{abbrev, 5}]). 107 | #{type => commit, 108 | author => 109 | {<<"Serge Aleynikov">>,<<"test@gmail.com">>,1686195121, -14400}, 110 | oid => <<"b85d0">>, 111 | parents => [<<"1fd4b">>]} 112 | 8> git:cat_file(R, "b85d0", [{abbrev, 5}]). 113 | #{type => tree, 114 | commits => 115 | [{<<".github">>,<<"tree">>,<<"1e41f">>,16384}, 116 | {<<".gitignore">>,<<"blob">>,<<"b893a">>,33188}, 117 | {<<".gitmodules">>,<<"blob">>,<<"2550a">>,33188}, 118 | {<<".vscode">>,<<"tree">>,<<"c7b1b">>,16384}, 119 | {<<"LICENSE">>,<<"blob">>,<<"d6456">>,33188}, 120 | {<<"Makefile">>,<<"blob">>,<<"2d635">>,33188}, 121 | {<<"README.md">>,<<"blob">>,<<"7b3d0">>,33188}, 122 | {<<"c_src">>,<<"tree">>,<<"147f3">>,16384}, 123 | {<<"rebar.config">>,<<"blob">>,<<"1f68a">>,33188}, 124 | {<<"rebar.lock">>,<<"blob">>,<<"57afc">>,33188}, 125 | {<<"src">>,<<"tree">>,<<"1bccb">>,16384}]} 126 | 8> git:cat_file(R, "b893a", [{abbrev, 5}]). 127 | #{type => blob, 128 | blob => <<"*.swp\n*.dump\n/c_src/*.o\n/c_src/fmt\n/priv/*.so\n/_build\n/doc\n">>} 129 | ``` 130 | 131 | ### Elixir example 132 | 133 | ```elixir 134 | iex(1)> repo = :git.init("/tmp/egit_repo") 135 | #Reference<0.739271388.2889220102.160795> 136 | iex(2)> :git.remote_add(repo, "origin", "git@github.com:saleyn/test_repo.git") 137 | :ok 138 | iex(3)> :git.list_remotes(repo) 139 | [{"origin", "git@github.com:saleyn/test_repo.git", [:push, :fetch]}] 140 | iex(4)> ok = File.write!("/tmp/egit_repo/README.md", <<"This is a test\n">>) 141 | :ok 142 | iex(5)> :git.add(repo, "README.md") 143 | %{mode: :added, files: ["README.md"]} 144 | iex(6)> :git.status(repo) 145 | [%{index: [{:new, "README.md"}]}] 146 | iex(7)> :git.commit(repo, "Initial commit") 147 | {:ok, "dc89c6b26b22f41d34300654f8d36252925d5d67"} 148 | ``` 149 | 150 | ## Patching 151 | 152 | If you find some functionality lacking, feel free to add missing functions 153 | and submit a PR. The implementation recommendation would be to use one of 154 | the [examples](https://github.com/libgit2/libgit2/tree/main/examples) 155 | provided with `libgit2` as a guide, add the functionality as `lg2_*()` 156 | function in `c_src/git_*.hpp`, modify `git.cpp` to call that function 157 | accordingly, write unit tests in `git.erl` and sumbmit a pull request. 158 | 159 | ## Author 160 | 161 | Serge Aleynikov 162 | 163 | ## License 164 | 165 | Apache 2.0 166 | -------------------------------------------------------------------------------- /c_src/Makefile: -------------------------------------------------------------------------------- 1 | CURDIR := $(shell pwd) 2 | BASEDIR := $(abspath $(dir $(CURDIR))) 3 | PROJECT ?= git 4 | PROJECT := $(strip $(PROJECT)) 5 | 6 | ERL_CXXFLAGS ?= $(shell erl -noshell -noinput -eval "io:format(\"-I~ts/erts-~ts/include -I~ts\", [code:root_dir(), erlang:system_info(version), code:lib_dir(erl_interface, include)]), halt(0).") 7 | ERL_LDFLAGS ?= $(shell erl -noshell -noinput -eval "io:format(\"-L~ts\", [code:lib_dir(erl_interface, lib)]), halt(0).") 8 | 9 | DEBUG ?= 0 10 | NIF_DEBUG ?= $(DEBUG) 11 | 12 | NIF_PRINT := $(if $(NIF_PRINT),-DNIF_DEBUG) 13 | 14 | CXX ?= g++ 15 | CXX_VSN ?= $(shell $(CXX) --version | sed -n '1s/^[^0-9]\+\([0-9]\+\)\(.[0-9-]\)\+.*$$/\1/p') 16 | 17 | ifeq ($(basename $(CXX)),g++) 18 | ifeq ($(shell expr $(CXX_VSN) \>= 13),1) 19 | C20_FEATURES=1 20 | endif 21 | else ifeq ($(basename $(CXX)),clang++) 22 | ifeq ($(shell expr $(CXX_VSN) \>= 15),1) 23 | C20_FEATURES=1 24 | endif 25 | endif 26 | 27 | HAVE_FORMAT ?= $(C20_FEATURES) 28 | HAVE_SRCLOC ?= $(C20_FEATURES) 29 | 30 | CPPFLAGS += \ 31 | $(if $(findstring $(HAVE_FORMAT),1),-DHAVE_FORMAT,-DFMT_HEADER_ONLY -Ifmt/include -Wno-array-bounds -Wno-stringop-overflow) \ 32 | $(if $(findstring $(HAVE_SRCLOC),1),-DHAVE_SRCLOC) 33 | 34 | # System type and C compiler/flags. 35 | 36 | ifeq ($(NIF_DEBUG),0) 37 | OPTIMIZE = -O3 -DNDEBUG 38 | else 39 | OPTIMIZE = -O0 -g 40 | endif 41 | 42 | CXXFLAGS ?= -finline-functions -Wall -std=c++20 43 | 44 | CXXFLAGS += $(OPTIMIZE) -fPIC $(ERL_CXXFLAGS) 45 | LDFLAGS += $(ERL_LDFLAGS) -lei -lgit2 -shared 46 | 47 | UNAME_SYS := $(shell uname -s) 48 | ifeq ($(UNAME_SYS),Darwin) 49 | LDFLAGS += -flat_namespace -undefined suppress 50 | endif 51 | 52 | SRC_DIR = $(CURDIR) 53 | SO_OUTPUT ?= $(BASEDIR)/priv/$(PROJECT).so 54 | 55 | SOURCES := $(wildcard $(SRC_DIR)/*.cpp) 56 | OBJECTS = $(addsuffix .o, $(basename $(SOURCES))) 57 | 58 | COMPILE_CPP = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(NIF_PRINT) -c 59 | 60 | # Targets 61 | 62 | all: $(SO_OUTPUT) 63 | 64 | clean: 65 | rm -f $(SO_OUTPUT) *.o 66 | 67 | info: 68 | @echo NIF_PRINT=$(NIF_PRINT) 69 | @echo NIF_DEBUG=$(NIF_DEBUG) 70 | @echo COMPILE_CPP=$(COMPILE_CPP) 71 | 72 | $(SO_OUTPUT): $(OBJECTS) 73 | @mkdir -p $(BASEDIR)/priv/ 74 | $(CXX) $(OBJECTS) $(LDFLAGS) -o $(SO_OUTPUT) 75 | 76 | %.o: %.cpp $(wildcard *.hpp) 77 | @if [ -z $(findstring $(HAVE_FORMAT),1) ]; then \ 78 | echo "==> Updating fmt submodule"; \ 79 | [ -z "$(shell git config --global safe.directory)" ] && git config --global --add safe.directory '*' || true; \ 80 | git submodule update --init --recursive; \ 81 | fi 82 | $(COMPILE_CPP) $(OUTPUT_OPTION) $< 83 | -------------------------------------------------------------------------------- /c_src/git.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #ifdef HAVE_FORMAT 10 | #include 11 | #else 12 | #include 13 | namespace std { using namespace fmt; } 14 | #endif 15 | 16 | #if !defined(GIT_REVSPEC_MERGE_BASE) && defined(GIT_REVPARSE_MERGE_BASE) 17 | #define GIT_REVSPEC_MERGE_BASE GIT_REVPARSE_MERGE_BASE 18 | #endif 19 | 20 | #include 21 | 22 | #ifndef GIT_OID_SHA1_HEXSIZE 23 | #define GIT_OID_SHA1_HEXSIZE GIT_OID_HEXSZ 24 | #endif 25 | 26 | #include "git_utils.hpp" 27 | #include "git_add.hpp" 28 | #include "git_cat_file.hpp" 29 | #include "git_checkout.hpp" 30 | #include "git_commit.hpp" 31 | #include "git_rev_parse.hpp" 32 | #include "git_rev_list.hpp" 33 | #include "git_config.hpp" 34 | #include "git_branch.hpp" 35 | #include "git_index.hpp" 36 | #include "git_remote.hpp" 37 | #include "git_tag.hpp" 38 | #include "git_status.hpp" 39 | 40 | static ERL_NIF_TERM to_monitored_resource(ErlNifEnv* env, git_repository* p) 41 | { 42 | ErlNifMonitor mon; 43 | ErlNifPid pid; 44 | enif_self(env, &pid); 45 | 46 | auto rp = GitRepoPtr::create(p); 47 | 48 | if (!rp) [[unlikely]] { 49 | assert(p); 50 | 51 | #ifdef NIF_DEBUG 52 | fprintf(stderr, "=egit=> Freeing repo %p [%d]\r\n", p, __LINE__); 53 | #endif 54 | 55 | git_repository_free(p); 56 | return enif_raise_exception(env, ATOM_ENOMEM); 57 | } 58 | 59 | auto result = enif_monitor_process(env, rp, &pid, &mon); 60 | 61 | if (result != 0) [[unlikely]] { 62 | #ifdef NIF_DEBUG 63 | fprintf(stderr, "=egit=> Freeing repo %p (result=%d) [%d]\r\n", rp, result, __LINE__); 64 | #endif 65 | git_repository_free(p); 66 | 67 | if (result > 0) { 68 | // Process no longer alive 69 | return enif_raise_exception(env, ATOM_ENOPROCESS); 70 | } else { 71 | assert(result < 0); 72 | // mon callback is not specified 73 | return enif_raise_exception(env, ATOM_ENOCALLBACK); 74 | } 75 | } 76 | 77 | return rp->to_enif_resource(env); 78 | } 79 | 80 | static ERL_NIF_TERM oid_to_bin(ErlNifEnv* env, git_oid const* oid, size_t len = GIT_OID_SHA1_HEXSIZE) 81 | { 82 | char buf[GIT_OID_SHA1_HEXSIZE+1]; 83 | len = std::min(len, sizeof(buf)-1); 84 | git_oid_tostr(buf, len, oid); 85 | return make_binary(env, buf); 86 | } 87 | 88 | static ERL_NIF_TERM 89 | commit_lookup_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 90 | { 91 | assert(argc == 3); 92 | 93 | GitRepoPtr* repo; 94 | if (!enif_get_resource(env, argv[0], GIT_REPO_RESOURCE, (void**)&repo)) [[unlikely]] 95 | return enif_make_badarg(env); 96 | 97 | ErlNifBinary bsha; 98 | 99 | if (!enif_inspect_binary(env, argv[1], &bsha)) [[unlikely]] 100 | return raise_badarg_exception(env, argv[1]); 101 | 102 | std::string sha = bin_to_str(bsha); 103 | 104 | git_oid oid; 105 | if (git_oid_fromstr(&oid, sha.c_str()) < 0) 106 | return ATOM_NIL; 107 | 108 | std::vector keys, vals; 109 | 110 | auto push = [&keys, &vals, env](ERL_NIF_TERM key, const char* val) { 111 | keys.push_back(key); 112 | vals.push_back(val ? make_binary(env, val) : ATOM_NIL); 113 | }; 114 | 115 | auto pushi = [&keys, &vals, env](ERL_NIF_TERM key, int64_t val) { 116 | keys.push_back(key); 117 | vals.push_back(enif_make_int64(env, val)); 118 | }; 119 | 120 | auto pusht = [&keys, &vals](ERL_NIF_TERM key, ERL_NIF_TERM val) { 121 | keys.push_back(key); 122 | vals.push_back(val); 123 | }; 124 | 125 | auto push_sign = [&, env](ERL_NIF_TERM key, git_signature const* val) { 126 | keys.push_back(key); 127 | vals.push_back(enif_make_tuple2(env, make_binary(env, val->name), make_binary(env, val->email))); 128 | }; 129 | 130 | // Smart pointer that will automatically free the commit object 131 | SmartPtr commit(git_commit_free); 132 | 133 | if (git_commit_lookup(&commit, repo->get(), &oid) < 0) 134 | return raise_git_exception(env, "Failed to find git commit " + sha); 135 | 136 | ERL_NIF_TERM head, list = argv[2]; 137 | 138 | while (enif_get_list_cell(env, list, &head, &list)) { 139 | if (enif_is_identical(head, ATOM_ENCODING)) push(ATOM_ENCODING, git_commit_message_encoding(commit)); 140 | else if (enif_is_identical(head, ATOM_MESSAGE)) push(ATOM_MESSAGE, git_commit_message (commit)); 141 | else if (enif_is_identical(head, ATOM_SUMMARY)) push(ATOM_SUMMARY, git_commit_summary (commit)); 142 | else if (enif_is_identical(head, ATOM_TIME)) pushi(ATOM_TIME, git_commit_time (commit)); 143 | else if (enif_is_identical(head, ATOM_TIME_OFFSET)) pushi(ATOM_TIME_OFFSET, git_commit_time_offset (commit) * 60L); 144 | else if (enif_is_identical(head, ATOM_COMMITTER)) push_sign(ATOM_COMMITTER, git_commit_committer (commit)); 145 | else if (enif_is_identical(head, ATOM_AUTHOR)) push_sign(ATOM_AUTHOR, git_commit_author (commit)); 146 | else if (enif_is_identical(head, ATOM_HEADER)) push(ATOM_HEADER, git_commit_raw_header (commit)); 147 | else if (enif_is_identical(head, ATOM_TREE_ID)) pusht(ATOM_TREE_ID, oid_to_bin(env, git_commit_tree_id(commit))); 148 | else [[unlikely]] 149 | return enif_make_badarg(env); 150 | } 151 | 152 | ERL_NIF_TERM map; 153 | if (!enif_make_map_from_arrays(env, &keys.front(), &vals.front(), keys.size(), &map)) [[unlikely]] 154 | return enif_raise_exception(env, ATOM_ENOMEM); 155 | 156 | return map; 157 | } 158 | 159 | static ERL_NIF_TERM clone_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 160 | { 161 | ErlNifBinary url, path; 162 | assert(argc == 2); 163 | 164 | if (!enif_inspect_binary(env, argv[0], &url) || 165 | !enif_inspect_binary(env, argv[1], &path)) [[unlikely]] 166 | return enif_make_badarg(env); 167 | 168 | std::string surl = bin_to_str(url); 169 | std::string spath = bin_to_str(path); 170 | 171 | git_repository* p{}; 172 | 173 | if (git_clone(&p, surl.c_str(), spath.c_str(), nullptr) < 0) [[unlikely]] 174 | return raise_git_exception(env, "Failed to clone git repo " + surl); 175 | 176 | #ifdef NIF_DEBUG 177 | fprintf(stderr, "=egit=> Cloned repo %p [%d]\r\n", p, __LINE__); 178 | #endif 179 | 180 | return to_monitored_resource(env, p); 181 | } 182 | 183 | static ERL_NIF_TERM init_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 184 | { 185 | assert(argc == 2); 186 | 187 | std::string path; 188 | auto bare = false; 189 | 190 | // Parse options 191 | { 192 | auto opts = argv[1]; 193 | 194 | ErlNifBinary bin; 195 | if (!enif_inspect_binary(env, argv[0], &bin) || bin.size == 0) [[unlikely]] 196 | return enif_make_badarg(env); 197 | 198 | if (!enif_is_list(env, opts)) [[unlikely]] 199 | return enif_make_badarg(env); 200 | 201 | ERL_NIF_TERM opt; 202 | 203 | while (enif_get_list_cell(env, opts, &opt, &opts)) { 204 | if (enif_is_identical(opt, ATOM_BARE)) bare = true; 205 | else [[unlikely]] 206 | return raise_badarg_exception(env, opt); 207 | } 208 | 209 | path = bin_to_str(bin); 210 | } 211 | 212 | git_repository* p{}; 213 | 214 | if (git_repository_init(&p, path.c_str(), bare) != GIT_OK) [[unlikely]] 215 | return raise_git_exception(env, std::format("Failed to init git repo {}", path)); 216 | 217 | #ifdef NIF_DEBUG 218 | fprintf(stderr, "=egit=> Init repo %p [%d]\r\n", p, __LINE__); 219 | #endif 220 | 221 | return to_monitored_resource(env, p); 222 | } 223 | 224 | static ERL_NIF_TERM open_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 225 | { 226 | ErlNifBinary path; 227 | assert(argc == 1); 228 | 229 | if (!enif_inspect_binary(env, argv[0], &path)) [[unlikely]] 230 | return enif_make_badarg(env); 231 | 232 | std::string spath = bin_to_str(path); 233 | 234 | git_repository* p{}; 235 | 236 | if (git_repository_open(&p, spath.c_str()) < 0) [[unlikely]] 237 | return raise_git_exception(env, "Failed to open git repo " + spath); 238 | 239 | return to_monitored_resource(env, p); 240 | } 241 | 242 | static ERL_NIF_TERM fetch_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 243 | { 244 | GitRepoPtr* repo; 245 | if (!enif_get_resource(env, argv[0], GIT_REPO_RESOURCE, (void**)&repo)) [[unlikely]] 246 | return enif_make_badarg(env); 247 | 248 | const char* fetch_or_pull = nullptr; 249 | 250 | if (enif_is_identical(argv[1], ATOM_FETCH)) 251 | fetch_or_pull = "fetch"; 252 | else if (enif_is_identical(argv[1], ATOM_PULL)) 253 | fetch_or_pull = "pull"; 254 | else 255 | return enif_make_badarg(env); 256 | 257 | std::string remote_name("origin"); 258 | 259 | if (argc > 2) { 260 | ErlNifBinary bin; 261 | if (!enif_inspect_binary(env, argv[2], &bin)) [[unlikely]] 262 | return enif_make_badarg(env); 263 | 264 | remote_name = bin_to_str(bin); 265 | } 266 | 267 | git_remote* remote; 268 | 269 | if (git_remote_lookup(&remote, repo->get(), remote_name.c_str()) < 0) 270 | return make_git_error(env, "Failed to lookup remote " + remote_name); 271 | if (git_remote_fetch(remote, 272 | NULL, // refspecs, NULL to use the configured ones 273 | NULL, // options, empty for defaults 274 | fetch_or_pull) < 0) // reflog message, "fetch" (or NULL) or "pull" 275 | return make_git_error(env, "Failed to fetch from " + remote_name); 276 | 277 | return ATOM_OK; 278 | } 279 | 280 | static ERL_NIF_TERM cat_file_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 281 | { 282 | assert(argc == 3); 283 | 284 | GitRepoPtr* repo; 285 | if (!enif_get_resource(env, argv[0], GIT_REPO_RESOURCE, (void**)&repo)) [[unlikely]] 286 | return enif_make_badarg(env); 287 | 288 | ErlNifBinary bin; 289 | if (!enif_inspect_binary(env, argv[1], &bin) || bin.size == 0) [[unlikely]] 290 | return enif_make_badarg(env); 291 | 292 | if (!enif_is_list(env, argv[2])) [[unlikely]] 293 | return enif_make_badarg(env); 294 | 295 | std::string filename(bin_to_str(bin)); 296 | 297 | return lg2_cat_file(env, repo->get(), filename, argv[2]); 298 | } 299 | 300 | static ERL_NIF_TERM checkout_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 301 | { 302 | assert(argc == 3); 303 | 304 | GitRepoPtr* repo; 305 | if (!enif_get_resource(env, argv[0], GIT_REPO_RESOURCE, (void**)&repo)) [[unlikely]] 306 | return enif_make_badarg(env); 307 | 308 | ErlNifBinary bin; 309 | if (!enif_inspect_binary(env, argv[1], &bin) || bin.size == 0) [[unlikely]] 310 | return enif_make_badarg(env); 311 | 312 | if (!enif_is_list(env, argv[2])) [[unlikely]] 313 | return enif_make_badarg(env); 314 | 315 | std::string rev = bin_to_str(bin); 316 | 317 | return lg2_checkout(env, repo->get(), rev, argv[2]); 318 | } 319 | 320 | static ERL_NIF_TERM add_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 321 | { 322 | assert(argc == 3); 323 | 324 | GitRepoPtr* repo; 325 | if (!enif_get_resource(env, argv[0], GIT_REPO_RESOURCE, (void**)&repo)) [[unlikely]] 326 | return enif_make_badarg(env); 327 | 328 | if (!enif_is_list(env, argv[1]) || !enif_is_list(env, argv[2])) [[unlikely]] 329 | return enif_make_badarg(env); 330 | 331 | return lg2_add(env, repo->get(), argv[1], argv[2]); 332 | } 333 | 334 | static ERL_NIF_TERM commit_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 335 | { 336 | assert(argc == 2); 337 | 338 | GitRepoPtr* repo; 339 | if (!enif_get_resource(env, argv[0], GIT_REPO_RESOURCE, (void**)&repo)) [[unlikely]] 340 | return enif_make_badarg(env); 341 | 342 | ErlNifBinary bin; 343 | if (!enif_inspect_binary(env, argv[1], &bin) || bin.size == 0) [[unlikely]] 344 | return enif_make_badarg(env); 345 | 346 | return lg2_commit(env, repo->get(), bin_to_str(bin)); 347 | } 348 | 349 | static ERL_NIF_TERM push_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 350 | { 351 | assert(argc == 3); 352 | 353 | // Parse options 354 | GitRepoPtr* repo; 355 | if (!enif_get_resource(env, argv[0], GIT_REPO_RESOURCE, (void**)&repo)) [[unlikely]] 356 | return enif_make_badarg(env); 357 | 358 | std::string sremote = "origin"; 359 | 360 | if (!term_to_str(env, argv[1], sremote)) [[unlikely]] 361 | return enif_make_badarg(env); 362 | 363 | ERL_NIF_TERM ref, list = argv[2]; 364 | 365 | if (!enif_is_list(env, list)) [[unlikely]] 366 | return raise_badarg_exception(env, list); 367 | 368 | std::vector ref_specs; 369 | 370 | while (enif_get_list_cell(env, list, &ref, &list)) { 371 | std::string str; 372 | if (!term_to_str(env, ref, str)) [[unlikely]] 373 | return raise_badarg_exception(env, ref); 374 | ref_specs.push_back(str); 375 | } 376 | 377 | std::vector cref_specs; 378 | for (auto& s : ref_specs) 379 | cref_specs.push_back(s.c_str()); 380 | 381 | git_strarray refspecs = { 382 | .strings = const_cast(&cref_specs.front()), 383 | .count = cref_specs.size() 384 | }; 385 | 386 | SmartPtr remote(git_remote_free); 387 | if (git_remote_lookup(&remote, repo->get(), sremote.c_str()) != GIT_OK) [[unlikely]] 388 | return make_git_error(env, "Unable to lookup remote"); 389 | 390 | git_push_options options; 391 | if (git_push_options_init(&options, GIT_PUSH_OPTIONS_VERSION) != GIT_OK) [[unlikely]] 392 | return make_git_error(env, "Error initializing push"); 393 | 394 | return git_remote_push(remote, &refspecs, &options) == GIT_OK 395 | ? ATOM_OK : make_git_error(env, "Error pushing to " + sremote); 396 | } 397 | 398 | static ERL_NIF_TERM rev_parse_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 399 | { 400 | assert(argc == 3); 401 | 402 | GitRepoPtr* repo; 403 | if (!enif_get_resource(env, argv[0], GIT_REPO_RESOURCE, (void**)&repo)) [[unlikely]] 404 | return enif_make_badarg(env); 405 | 406 | ErlNifBinary bin; 407 | if (!enif_inspect_binary(env, argv[1], &bin) || bin.size == 0) [[unlikely]] 408 | return enif_make_badarg(env); 409 | 410 | if (!enif_is_list(env, argv[2])) [[unlikely]] 411 | return enif_make_badarg(env); 412 | 413 | return lg2_rev_parse(env, repo->get(), bin_to_str(bin), argv[2]); 414 | } 415 | 416 | static ERL_NIF_TERM rev_list_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 417 | { 418 | assert(argc == 3); 419 | 420 | GitRepoPtr* repo; 421 | if (!enif_get_resource(env, argv[0], GIT_REPO_RESOURCE, (void**)&repo)) [[unlikely]] 422 | return enif_make_badarg(env); 423 | 424 | if (!enif_is_list(env, argv[1])) [[unlikely]] 425 | return enif_make_badarg(env); 426 | 427 | if (!enif_is_list(env, argv[2])) [[unlikely]] 428 | return enif_make_badarg(env); 429 | 430 | return lg2_rev_list(env, repo->get(), argv[1], argv[2]); 431 | } 432 | 433 | static ERL_NIF_TERM config_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 434 | { 435 | assert(argc >= 2); 436 | 437 | if (argc > 3) [[unlikely]] 438 | return enif_make_badarg(env); 439 | 440 | return lg2_config(env, argv[0], argv[1], argc == 3 ? argv[2] : 0); 441 | } 442 | 443 | static ERL_NIF_TERM branch_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 444 | { 445 | assert(argc >= 3 && argc <= 4); 446 | 447 | GitRepoPtr* repo; 448 | if (!enif_get_resource(env, argv[0], GIT_REPO_RESOURCE, (void**)&repo)) [[unlikely]] 449 | return enif_make_badarg(env); 450 | 451 | ERL_NIF_TERM op = argv[1]; 452 | 453 | return lg2_branch(env, repo->get(), op, argv[2], argc == 4 ? argv[3] : 0); 454 | } 455 | 456 | static ERL_NIF_TERM list_branches_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 457 | { 458 | assert(argc == 2); 459 | 460 | GitRepoPtr* repo; 461 | if (!enif_get_resource(env, argv[0], GIT_REPO_RESOURCE, (void**)&repo)) [[unlikely]] 462 | return enif_make_badarg(env); 463 | 464 | return lg2_branch_list(env, repo->get(), argv[1]); 465 | } 466 | 467 | static ERL_NIF_TERM list_index_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 468 | { 469 | assert(argc == 2); 470 | 471 | GitRepoPtr* repo; 472 | if (!enif_get_resource(env, argv[0], GIT_REPO_RESOURCE, (void**)&repo)) [[unlikely]] 473 | return enif_make_badarg(env); 474 | 475 | if (!enif_is_list(env, argv[1])) [[unlikely]] 476 | return enif_make_badarg(env); 477 | 478 | return lg2_index(env, repo->get(), argv[1]); 479 | } 480 | 481 | static ERL_NIF_TERM remote_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 482 | { 483 | assert(argc == 4); 484 | 485 | GitRepoPtr* repo; 486 | if (!enif_get_resource(env, argv[0], GIT_REPO_RESOURCE, (void**)&repo)) [[unlikely]] 487 | return enif_make_badarg(env); 488 | 489 | ERL_NIF_TERM op = argv[1]; 490 | 491 | ErlNifBinary name; 492 | if (!enif_inspect_binary(env, argv[2], &name) || name.size == 0) [[unlikely]] 493 | return enif_make_badarg(env); 494 | 495 | if (!(enif_is_tuple(env, op) || enif_is_atom(env, op)) || !enif_is_list(env, argv[3])) [[unlikely]] 496 | return enif_make_badarg(env); 497 | 498 | return lg2_remote(env, repo->get(), bin_to_str(name), op, argv[3]); 499 | } 500 | 501 | static ERL_NIF_TERM list_remotes_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 502 | { 503 | assert(argc == 1); 504 | 505 | GitRepoPtr* repo; 506 | if (!enif_get_resource(env, argv[0], GIT_REPO_RESOURCE, (void**)&repo)) [[unlikely]] 507 | return enif_make_badarg(env); 508 | 509 | return lg2_remotes_list(env, repo->get()); 510 | } 511 | 512 | static ERL_NIF_TERM tag_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 513 | { 514 | assert(argc == 4); 515 | 516 | GitRepoPtr* repo; 517 | if (!enif_get_resource(env, argv[0], GIT_REPO_RESOURCE, (void**)&repo)) [[unlikely]] 518 | return enif_make_badarg(env); 519 | 520 | ERL_NIF_TERM op = argv[1]; 521 | 522 | ErlNifBinary name; 523 | if (!enif_inspect_binary(env, argv[2], &name)) [[unlikely]] 524 | return enif_make_badarg(env); 525 | 526 | if (!enif_is_atom(env, op) || !enif_is_list(env, argv[3])) [[unlikely]] 527 | return enif_make_badarg(env); 528 | 529 | return lg2_tag(env, repo->get(), bin_to_str(name), op, argv[3]); 530 | } 531 | 532 | static ERL_NIF_TERM status_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 533 | { 534 | assert(argc == 2); 535 | 536 | GitRepoPtr* repo; 537 | if (!enif_get_resource(env, argv[0], GIT_REPO_RESOURCE, (void**)&repo)) [[unlikely]] 538 | return enif_make_badarg(env); 539 | 540 | if (!enif_is_list(env, argv[1])) [[unlikely]] 541 | return enif_make_badarg(env); 542 | 543 | return lg2_status(env, repo->get(), argv[1]); 544 | } 545 | 546 | static ERL_NIF_TERM reset_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) 547 | { 548 | assert(argc == 3); 549 | 550 | GitRepoPtr* repo; 551 | if (!enif_get_resource(env, argv[0], GIT_REPO_RESOURCE, (void**)&repo)) [[unlikely]] 552 | return enif_make_badarg(env); 553 | 554 | auto type = git_reset_t(0); 555 | 556 | if (enif_is_identical(ATOM_SOFT, argv[1])) type = GIT_RESET_SOFT; 557 | else if (enif_is_identical(ATOM_HARD, argv[1])) type = GIT_RESET_HARD; 558 | else if (enif_is_identical(ATOM_MIXED, argv[1])) type = GIT_RESET_MIXED; 559 | else [[unlikely]] 560 | return enif_make_badarg(env); 561 | 562 | std::string target; 563 | if (!term_to_str(env, argv[2], target)) [[unlikely]] 564 | return enif_make_badarg(env); 565 | 566 | SmartPtr id(git_object_free); 567 | 568 | if (git_revparse_single(&id, repo->get(), target.c_str()) != GIT_OK) [[unlikely]] 569 | return make_git_error(env, std::format("Failed to lookup commit {}", target)); 570 | 571 | return git_reset(repo->get(), id, type, nullptr) == GIT_OK 572 | ? ATOM_OK : make_git_error(env, "Cannot reset"); 573 | } 574 | 575 | static void resource_dtor(ErlNifEnv* env, void* arg) 576 | { 577 | assert(arg); 578 | #ifdef NIF_DEBUG 579 | fprintf(stderr, "=egit=> Releasing resource %p [%d]\r\n", arg, __LINE__); 580 | #endif 581 | static_cast(arg)->~GitRepoPtr(); 582 | } 583 | 584 | static void resource_down(ErlNifEnv* env, void* obj, ErlNifPid*, ErlNifMonitor*) 585 | { 586 | #ifdef NIF_DEBUG 587 | fprintf(stderr, "=egit=> Decremented resource ref %p [%d]\r\n", obj, __LINE__); 588 | #endif 589 | enif_release_resource(obj); 590 | } 591 | 592 | static int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info) 593 | { 594 | init_atoms(env); 595 | 596 | auto flags = (ErlNifResourceFlags)(ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER); 597 | ErlNifResourceTypeInit rti = {.dtor = &resource_dtor, .down = &resource_down}; 598 | GIT_REPO_RESOURCE = enif_open_resource_type_x(env, "git_repo_resource", &rti, flags, nullptr); 599 | 600 | git_libgit2_init(); 601 | 602 | return 0; 603 | } 604 | 605 | static int upgrade(ErlNifEnv* env, void** priv_data, void** old_priv_data, ERL_NIF_TERM load_info) { 606 | //if (old_priv_data) 607 | // enif_release_resource(old_priv_data); 608 | return 0; 609 | } 610 | 611 | static ErlNifFunc git_funcs[] = 612 | { 613 | {"init_nif", 2, init_nif}, 614 | {"clone_nif", 2, clone_nif, ERL_NIF_DIRTY_JOB_IO_BOUND}, 615 | {"open_nif", 1, open_nif, ERL_NIF_DIRTY_JOB_IO_BOUND}, 616 | {"fetch_nif", 2, fetch_nif, ERL_NIF_DIRTY_JOB_IO_BOUND}, 617 | {"fetch_nif", 3, fetch_nif, ERL_NIF_DIRTY_JOB_IO_BOUND}, 618 | {"add_nif", 3, add_nif, ERL_NIF_DIRTY_JOB_IO_BOUND}, 619 | {"checkout_nif", 3, checkout_nif, ERL_NIF_DIRTY_JOB_IO_BOUND}, 620 | {"push_nif", 3, push_nif, ERL_NIF_DIRTY_JOB_IO_BOUND}, 621 | {"commit_nif", 2, commit_nif, ERL_NIF_DIRTY_JOB_IO_BOUND}, 622 | {"commit_lookup_nif", 3, commit_lookup_nif, 0}, 623 | {"cat_file_nif", 3, cat_file_nif, ERL_NIF_DIRTY_JOB_IO_BOUND}, 624 | {"rev_parse_nif", 3, rev_parse_nif, ERL_NIF_DIRTY_JOB_IO_BOUND}, 625 | {"rev_list_nif", 3, rev_list_nif, ERL_NIF_DIRTY_JOB_IO_BOUND}, 626 | {"config_get_nif", 2, config_nif}, 627 | {"config_set_nif", 3, config_nif}, 628 | {"branch_nif", 3, branch_nif}, 629 | {"branch_nif", 4, branch_nif}, 630 | {"list_branches", 2, list_branches_nif, ERL_NIF_DIRTY_JOB_IO_BOUND}, 631 | {"list_index", 2, list_index_nif, ERL_NIF_DIRTY_JOB_IO_BOUND}, 632 | {"remote_nif", 4, remote_nif}, 633 | {"tag_nif", 4, tag_nif, ERL_NIF_DIRTY_JOB_IO_BOUND}, 634 | {"status_nif", 2, status_nif, ERL_NIF_DIRTY_JOB_IO_BOUND}, 635 | {"reset_nif", 3, reset_nif}, 636 | {"list_remotes", 1, list_remotes_nif}, 637 | }; 638 | 639 | ERL_NIF_INIT(git, git_funcs, load, NULL, upgrade, NULL); 640 | -------------------------------------------------------------------------------- /c_src/git_add.hpp: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // This code was derived from 3 | // https://github.com/libgit2/libgit2/blob/main/examples/add.c 4 | //----------------------------------------------------------------------------- 5 | // libgit2 "add" - modify the index 6 | //----------------------------------------------------------------------------- 7 | // Written by the libgit2 contributors 8 | // 9 | // To the extent possible under law, the author(s) have dedicated all copyright 10 | // and related and neighboring rights to this software to the public domain 11 | // worldwide. This software is distributed without any warranty. 12 | // 13 | // You should have received a copy of the CC0 Public Domain Dedication along 14 | // with this software. If not, see 15 | // . 16 | //----------------------------------------------------------------------------- 17 | #pragma once 18 | 19 | #include 20 | 21 | enum index_mode { 22 | INDEX_NONE, 23 | INDEX_ADD 24 | }; 25 | 26 | struct index_options { 27 | index_options() 28 | : dry_run(false), update(false), force(false) 29 | , repo(nullptr), mode(INDEX_ADD) 30 | {} 31 | 32 | bool dry_run; 33 | bool update; 34 | bool force; 35 | git_repository* repo; 36 | index_mode mode; 37 | ErlNifEnv* env; 38 | std::vector files; 39 | }; 40 | 41 | // Forward declarations for helpers 42 | static ERL_NIF_TERM 43 | parse_opts(ErlNifEnv* env, std::vector& file_specs, ERL_NIF_TERM file_specs_list, ERL_NIF_TERM opts, index_options& o) 44 | { 45 | ERL_NIF_TERM spec, opt; 46 | 47 | file_specs.clear(); 48 | 49 | while (enif_get_list_cell(env, file_specs_list, &spec, &file_specs_list)) { 50 | ErlNifBinary bin; 51 | if (!enif_inspect_binary(env, spec, &bin)) [[unlikely]] 52 | return raise_badarg_exception(env, spec); 53 | 54 | file_specs.emplace_back(bin_to_str(bin)); 55 | } 56 | 57 | while (enif_get_list_cell(env, opts, &opt, &opts)) { 58 | if (enif_is_identical(opt, ATOM_DRY_RUN)) o.dry_run = true; 59 | else if (enif_is_identical(opt, ATOM_UPDATE)) o.update = true; 60 | else if (enif_is_identical(opt, ATOM_FORCE)) o.force = true; 61 | else [[unlikely]] 62 | return raise_badarg_exception(env, opt); 63 | } 64 | 65 | return 0; 66 | } 67 | 68 | // This callback is called for each file under consideration by 69 | // git_index_(update|add)_all. 70 | // It makes use of the callback's ability to abort the action. 71 | int visit_matched_cb(const char* path, [[maybe_unused]] const char* matched_pathspec, void* payload) 72 | { 73 | auto opts = static_cast(payload); 74 | unsigned status; 75 | 76 | /* Get the file status */ 77 | if (git_status_file(&status, opts->repo, path) < 0) 78 | return -1; // Abort 79 | 80 | auto should_add = (status & GIT_STATUS_WT_MODIFIED) || (status & GIT_STATUS_WT_NEW); 81 | int ret = opts->dry_run || !should_add; // Skip = 1, Add = 0 82 | 83 | if (should_add) 84 | opts->files.push_back(make_binary(opts->env, path)); 85 | 86 | return ret; 87 | } 88 | 89 | // Implementation of "add" logic 90 | ERL_NIF_TERM lg2_add(ErlNifEnv* env, git_repository* repo, ERL_NIF_TERM file_specs_list, ERL_NIF_TERM opts) 91 | { 92 | assert(repo); 93 | 94 | git_index_matched_path_cb matched_cb = NULL; 95 | index_options options; 96 | std::vector path_specs; 97 | git_strarray array{}; 98 | 99 | options.env = env; 100 | options.repo = repo; 101 | options.mode = INDEX_ADD; 102 | 103 | // Parse the options & arguments 104 | auto res = parse_opts(env, path_specs, file_specs_list, opts, options); 105 | 106 | if (res != 0) [[unlikely]] 107 | return res; 108 | 109 | SmartPtr index(git_index_free); 110 | 111 | // Grab the repository's index 112 | if (git_repository_index(&index, repo) != GIT_OK) 113 | return make_git_error(env, "Could not open repository index"); 114 | 115 | // Setup a callback if the requested options need it 116 | matched_cb = &visit_matched_cb; 117 | 118 | array.count = path_specs.size(); 119 | std::vector strings(array.count); 120 | 121 | for (auto i = 0u; i < array.count; ++i) 122 | strings[i] = path_specs[i].c_str(); 123 | 124 | array.strings = const_cast(&strings[0]); 125 | auto flags = options.force ? GIT_INDEX_ADD_FORCE : 0; 126 | 127 | // Perform the requested action with the index and files 128 | auto err = options.update 129 | ? git_index_update_all(index, &array, matched_cb, &options) 130 | : git_index_add_all(index, &array, flags, matched_cb, &options); 131 | 132 | // Cleanup memory 133 | if (err != GIT_OK) 134 | return err; 135 | 136 | if (options.files.size()) 137 | git_index_write(index); 138 | 139 | auto files = enif_make_list_from_array(env, &options.files.front(), options.files.size()); 140 | 141 | if (options.files.empty()) 142 | return ATOM_NIL; 143 | 144 | auto mode = options.dry_run ? ATOM_DRY_RUN : ATOM_ADDED; 145 | 146 | ERL_NIF_TERM keys[] = {ATOM_MODE, ATOM_FILES}; 147 | ERL_NIF_TERM vals[] = {mode, files}; 148 | 149 | ERL_NIF_TERM map; 150 | return enif_make_map_from_arrays(env, keys, vals, 2, &map) ? map : make_error(env, ATOM_ENOMEM); 151 | } 152 | -------------------------------------------------------------------------------- /c_src/git_atoms.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | static ERL_NIF_TERM ATOM_ABBREV; 4 | static ERL_NIF_TERM ATOM_ADD; 5 | static ERL_NIF_TERM ATOM_ADDED; 6 | static ERL_NIF_TERM ATOM_ALL; 7 | static ERL_NIF_TERM ATOM_ANCESTOR; 8 | static ERL_NIF_TERM ATOM_ANY; 9 | static ERL_NIF_TERM ATOM_APP; 10 | static ERL_NIF_TERM ATOM_AUTHOR; 11 | static ERL_NIF_TERM ATOM_BADARG; 12 | static ERL_NIF_TERM ATOM_BARE; 13 | static ERL_NIF_TERM ATOM_BLOB; 14 | static ERL_NIF_TERM ATOM_BRANCH; 15 | static ERL_NIF_TERM ATOM_CHMOD_CALLS; 16 | static ERL_NIF_TERM ATOM_COMMIT; 17 | static ERL_NIF_TERM ATOM_COMMITS; 18 | static ERL_NIF_TERM ATOM_COMMITTER; 19 | static ERL_NIF_TERM ATOM_CONFLICT; 20 | static ERL_NIF_TERM ATOM_CREATE; 21 | static ERL_NIF_TERM ATOM_CTIME; 22 | static ERL_NIF_TERM ATOM_DATA; 23 | static ERL_NIF_TERM ATOM_DATE_ORDER; 24 | static ERL_NIF_TERM ATOM_DEFAULT; 25 | static ERL_NIF_TERM ATOM_DELETE; 26 | static ERL_NIF_TERM ATOM_DELETED; 27 | static ERL_NIF_TERM ATOM_DIR; 28 | static ERL_NIF_TERM ATOM_DRY_RUN; 29 | static ERL_NIF_TERM ATOM_ENCODING; 30 | static ERL_NIF_TERM ATOM_ENOCALLBACK; 31 | static ERL_NIF_TERM ATOM_ENOMEM; 32 | static ERL_NIF_TERM ATOM_ENOPROCESS; 33 | static ERL_NIF_TERM ATOM_ERROR; 34 | static ERL_NIF_TERM ATOM_FALSE; 35 | static ERL_NIF_TERM ATOM_FETCH; 36 | static ERL_NIF_TERM ATOM_FIELDS; 37 | static ERL_NIF_TERM ATOM_FILEM; 38 | static ERL_NIF_TERM ATOM_FILES; 39 | static ERL_NIF_TERM ATOM_FORCE; 40 | static ERL_NIF_TERM ATOM_FROM; 41 | static ERL_NIF_TERM ATOM_FULLNAME; 42 | static ERL_NIF_TERM ATOM_GLOBAL; 43 | static ERL_NIF_TERM ATOM_HARD; 44 | static ERL_NIF_TERM ATOM_HEADER; 45 | static ERL_NIF_TERM ATOM_HIGHEST; 46 | static ERL_NIF_TERM ATOM_IGNORE_SUBMODULES; 47 | static ERL_NIF_TERM ATOM_IGNORED; 48 | static ERL_NIF_TERM ATOM_INDEX; 49 | static ERL_NIF_TERM ATOM_LIMIT; 50 | static ERL_NIF_TERM ATOM_LINES; 51 | static ERL_NIF_TERM ATOM_LIST; 52 | static ERL_NIF_TERM ATOM_LOCAL; 53 | static ERL_NIF_TERM ATOM_MERGE_BASE; 54 | static ERL_NIF_TERM ATOM_MESSAGE; 55 | static ERL_NIF_TERM ATOM_MIXED; 56 | static ERL_NIF_TERM ATOM_MKDIR_CALLS; 57 | static ERL_NIF_TERM ATOM_MODE; 58 | static ERL_NIF_TERM ATOM_MODIFIED; 59 | static ERL_NIF_TERM ATOM_MTIME; 60 | static ERL_NIF_TERM ATOM_NAME; 61 | static ERL_NIF_TERM ATOM_NEW_NAME; 62 | static ERL_NIF_TERM ATOM_NEW; 63 | static ERL_NIF_TERM ATOM_NIL; 64 | static ERL_NIF_TERM ATOM_NONE; 65 | static ERL_NIF_TERM ATOM_NORMAL; 66 | static ERL_NIF_TERM ATOM_NOT_FOUND; 67 | static ERL_NIF_TERM ATOM_NOT; 68 | static ERL_NIF_TERM ATOM_OBJECT; 69 | static ERL_NIF_TERM ATOM_OID; 70 | static ERL_NIF_TERM ATOM_OK; 71 | static ERL_NIF_TERM ATOM_OURS; 72 | static ERL_NIF_TERM ATOM_OVERWRITE; 73 | static ERL_NIF_TERM ATOM_PARENTS; 74 | static ERL_NIF_TERM ATOM_PATH; 75 | static ERL_NIF_TERM ATOM_PATHS; 76 | static ERL_NIF_TERM ATOM_PATTERN; 77 | static ERL_NIF_TERM ATOM_PERF; 78 | static ERL_NIF_TERM ATOM_PULL; 79 | static ERL_NIF_TERM ATOM_PUSH; 80 | static ERL_NIF_TERM ATOM_RECURSIVE; 81 | static ERL_NIF_TERM ATOM_REMOTE; 82 | static ERL_NIF_TERM ATOM_RENAME; 83 | static ERL_NIF_TERM ATOM_RENAMED; 84 | static ERL_NIF_TERM ATOM_REVERSE; 85 | static ERL_NIF_TERM ATOM_SETURL; 86 | static ERL_NIF_TERM ATOM_SHA; 87 | static ERL_NIF_TERM ATOM_SIZE; 88 | static ERL_NIF_TERM ATOM_SOFT; 89 | static ERL_NIF_TERM ATOM_STAGE; 90 | static ERL_NIF_TERM ATOM_STAT_CALLS; 91 | static ERL_NIF_TERM ATOM_SUBMODULES; 92 | static ERL_NIF_TERM ATOM_SUMMARY; 93 | static ERL_NIF_TERM ATOM_SYSTEM; 94 | static ERL_NIF_TERM ATOM_TAG; 95 | static ERL_NIF_TERM ATOM_TAGGER; 96 | static ERL_NIF_TERM ATOM_TARGET; 97 | static ERL_NIF_TERM ATOM_THEIRS; 98 | static ERL_NIF_TERM ATOM_TIME_OFFSET; 99 | static ERL_NIF_TERM ATOM_TIME; 100 | static ERL_NIF_TERM ATOM_TO; 101 | static ERL_NIF_TERM ATOM_TOPO_ORDER; 102 | static ERL_NIF_TERM ATOM_TOTAL_STEPS; 103 | static ERL_NIF_TERM ATOM_TREE_ID; 104 | static ERL_NIF_TERM ATOM_TREE; 105 | static ERL_NIF_TERM ATOM_TRUE; 106 | static ERL_NIF_TERM ATOM_TYPE; 107 | static ERL_NIF_TERM ATOM_TYPECHANGE; 108 | static ERL_NIF_TERM ATOM_UNTRACKED; 109 | static ERL_NIF_TERM ATOM_UPDATE; 110 | static ERL_NIF_TERM ATOM_VERBOSE; 111 | static ERL_NIF_TERM ATOM_WORKTREE; 112 | static ERL_NIF_TERM ATOM_XDG; 113 | 114 | inline void init_atoms(ErlNifEnv* env) 115 | { 116 | ATOM_ABBREV = enif_make_atom(env, "abbrev"); 117 | ATOM_ADD = enif_make_atom(env, "add"); 118 | ATOM_ADDED = enif_make_atom(env, "added"); 119 | ATOM_ALL = enif_make_atom(env, "all"); 120 | ATOM_ANCESTOR = enif_make_atom(env, "ancestor"); 121 | ATOM_ANY = enif_make_atom(env, "any"); 122 | ATOM_APP = enif_make_atom(env, "app"); 123 | ATOM_AUTHOR = enif_make_atom(env, "author"); 124 | ATOM_BADARG = enif_make_atom(env, "badarg"); 125 | ATOM_BARE = enif_make_atom(env, "bare"); 126 | ATOM_BLOB = enif_make_atom(env, "blob"); 127 | ATOM_BRANCH = enif_make_atom(env, "branch"); 128 | ATOM_CHMOD_CALLS = enif_make_atom(env, "chmod_calls"); 129 | ATOM_COMMIT = enif_make_atom(env, "commit"); 130 | ATOM_COMMITS = enif_make_atom(env, "commits"); 131 | ATOM_COMMITTER = enif_make_atom(env, "committer"); 132 | ATOM_CONFLICT = enif_make_atom(env, "conflict"); 133 | ATOM_CREATE = enif_make_atom(env, "create"); 134 | ATOM_CTIME = enif_make_atom(env, "ctime"); 135 | ATOM_DATA = enif_make_atom(env, "data"); 136 | ATOM_DATE_ORDER = enif_make_atom(env, "date_order"); 137 | ATOM_DEFAULT = enif_make_atom(env, "default"); 138 | ATOM_DELETE = enif_make_atom(env, "delete"); 139 | ATOM_DELETED = enif_make_atom(env, "deleted"); 140 | ATOM_DIR = enif_make_atom(env, "dir"); 141 | ATOM_DRY_RUN = enif_make_atom(env, "dry_run"); 142 | ATOM_ENCODING = enif_make_atom(env, "encoding"); 143 | ATOM_ENOCALLBACK = enif_make_atom(env, "enocallback"); 144 | ATOM_ENOMEM = enif_make_atom(env, "enomem"); 145 | ATOM_ENOPROCESS = enif_make_atom(env, "enoprocess"); 146 | ATOM_ERROR = enif_make_atom(env, "error"); 147 | ATOM_FALSE = enif_make_atom(env, "false"); 148 | ATOM_FETCH = enif_make_atom(env, "fetch"); 149 | ATOM_FIELDS = enif_make_atom(env, "fields"); 150 | ATOM_FILEM = enif_make_atom(env, "filem"); 151 | ATOM_FILES = enif_make_atom(env, "files"); 152 | ATOM_FORCE = enif_make_atom(env, "force"); 153 | ATOM_FROM = enif_make_atom(env, "from"); 154 | ATOM_FULLNAME = enif_make_atom(env, "fullname"); 155 | ATOM_GLOBAL = enif_make_atom(env, "global"); 156 | ATOM_HEADER = enif_make_atom(env, "header"); 157 | ATOM_HIGHEST = enif_make_atom(env, "highest"); 158 | ATOM_IGNORE_SUBMODULES = enif_make_atom(env, "ignore_submodules"); 159 | ATOM_IGNORED = enif_make_atom(env, "ignored"); 160 | ATOM_INDEX = enif_make_atom(env, "index"); 161 | ATOM_LIMIT = enif_make_atom(env, "limit"); 162 | ATOM_LINES = enif_make_atom(env, "lines"); 163 | ATOM_LIST = enif_make_atom(env, "list"); 164 | ATOM_LOCAL = enif_make_atom(env, "local"); 165 | ATOM_MERGE_BASE = enif_make_atom(env, "merge_base"); 166 | ATOM_MESSAGE = enif_make_atom(env, "message"); 167 | ATOM_MKDIR_CALLS = enif_make_atom(env, "mkdir_calls"); 168 | ATOM_MODE = enif_make_atom(env, "mode"); 169 | ATOM_MODIFIED = enif_make_atom(env, "modified"); 170 | ATOM_MTIME = enif_make_atom(env, "mtime"); 171 | ATOM_NAME = enif_make_atom(env, "name"); 172 | ATOM_NEW = enif_make_atom(env, "new"); 173 | ATOM_NEW_NAME = enif_make_atom(env, "new_name"); 174 | ATOM_NIL = enif_make_atom(env, "nil"); 175 | ATOM_NONE = enif_make_atom(env, "none"); 176 | ATOM_NORMAL = enif_make_atom(env, "normal"); 177 | ATOM_NOT = enif_make_atom(env, "not"); 178 | ATOM_NOT_FOUND = enif_make_atom(env, "not_found"); 179 | ATOM_OBJECT = enif_make_atom(env, "object"); 180 | ATOM_OID = enif_make_atom(env, "oid"); 181 | ATOM_OK = enif_make_atom(env, "ok"); 182 | ATOM_OURS = enif_make_atom(env, "ours"); 183 | ATOM_OVERWRITE = enif_make_atom(env, "overwrite"); 184 | ATOM_PARENTS = enif_make_atom(env, "parents"); 185 | ATOM_PATH = enif_make_atom(env, "path"); 186 | ATOM_PATHS = enif_make_atom(env, "paths"); 187 | ATOM_PATTERN = enif_make_atom(env, "pattern"); 188 | ATOM_PERF = enif_make_atom(env, "perf"); 189 | ATOM_PULL = enif_make_atom(env, "pull"); 190 | ATOM_PUSH = enif_make_atom(env, "push"); 191 | ATOM_RECURSIVE = enif_make_atom(env, "recursive"); 192 | ATOM_REMOTE = enif_make_atom(env, "remote"); 193 | ATOM_RENAME = enif_make_atom(env, "rename"); 194 | ATOM_RENAMED = enif_make_atom(env, "renamed"); 195 | ATOM_REVERSE = enif_make_atom(env, "reverse"); 196 | ATOM_SETURL = enif_make_atom(env, "seturl"); 197 | ATOM_SHA = enif_make_atom(env, "sha"); 198 | ATOM_SIZE = enif_make_atom(env, "size"); 199 | ATOM_STAGE = enif_make_atom(env, "stage"); 200 | ATOM_STAT_CALLS = enif_make_atom(env, "stat_calls"); 201 | ATOM_SUBMODULES = enif_make_atom(env, "submodules"); 202 | ATOM_SUMMARY = enif_make_atom(env, "summary"); 203 | ATOM_SYSTEM = enif_make_atom(env, "system"); 204 | ATOM_TAG = enif_make_atom(env, "tag"); 205 | ATOM_TAGGER = enif_make_atom(env, "tagger"); 206 | ATOM_TARGET = enif_make_atom(env, "target"); 207 | ATOM_THEIRS = enif_make_atom(env, "theirs"); 208 | ATOM_TIME = enif_make_atom(env, "time"); 209 | ATOM_TIME_OFFSET = enif_make_atom(env, "time_offset"); 210 | ATOM_TO = enif_make_atom(env, "to"); 211 | ATOM_TOPO_ORDER = enif_make_atom(env, "topo_order"); 212 | ATOM_TOTAL_STEPS = enif_make_atom(env, "total_steps"); 213 | ATOM_TREE = enif_make_atom(env, "tree"); 214 | ATOM_TREE_ID = enif_make_atom(env, "tree_id"); 215 | ATOM_TRUE = enif_make_atom(env, "true"); 216 | ATOM_TYPE = enif_make_atom(env, "type"); 217 | ATOM_TYPECHANGE = enif_make_atom(env, "atom_typechange"); 218 | ATOM_UNTRACKED = enif_make_atom(env, "untracked"); 219 | ATOM_UPDATE = enif_make_atom(env, "update"); 220 | ATOM_VERBOSE = enif_make_atom(env, "verbose"); 221 | ATOM_WORKTREE = enif_make_atom(env, "worktree"); 222 | ATOM_XDG = enif_make_atom(env, "xdg"); 223 | ATOM_SOFT = enif_make_atom(env, "soft"); 224 | ATOM_HARD = enif_make_atom(env, "hard"); 225 | ATOM_MIXED = enif_make_atom(env, "mixed"); 226 | } 227 | -------------------------------------------------------------------------------- /c_src/git_branch.hpp: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // libgit2 "branch" - add/rename/delete branches 3 | //----------------------------------------------------------------------------- 4 | #pragma once 5 | 6 | #include 7 | 8 | namespace { 9 | enum class BranchOp { 10 | CREATE, 11 | RENAME, 12 | DELETE, 13 | }; 14 | 15 | enum NameKind { 16 | SHORT_NAME, 17 | FULL_NAME, 18 | }; 19 | } 20 | 21 | ERL_NIF_TERM lg2_branch_list(ErlNifEnv* env, git_repository* repo, ERL_NIF_TERM opts) 22 | { 23 | int arity, n; 24 | ERL_NIF_TERM opt; 25 | const ERL_NIF_TERM* tagvals; 26 | auto limit = INT32_MAX; 27 | git_branch_t list_flags = GIT_BRANCH_ALL; 28 | NameKind kind = SHORT_NAME; 29 | 30 | // Parse options 31 | { 32 | while (enif_get_list_cell(env, opts, &opt, &opts)) { 33 | if (enif_get_tuple(env, opt, &arity, &tagvals) && arity == 2) { 34 | if (enif_is_identical(tagvals[0], ATOM_LIMIT) && enif_get_int(env, tagvals[1], &n)) 35 | limit = n; 36 | else [[unlikely]] 37 | return raise_badarg_exception(env, opt); 38 | } 39 | else if (enif_is_identical(opt, ATOM_LOCAL)) list_flags = GIT_BRANCH_LOCAL; 40 | else if (enif_is_identical(opt, ATOM_REMOTE)) list_flags = GIT_BRANCH_REMOTE; 41 | else if (enif_is_identical(opt, ATOM_ALL)) list_flags = GIT_BRANCH_ALL; 42 | else if (enif_is_identical(opt, ATOM_FULLNAME)) kind = FULL_NAME; 43 | else [[unlikely]] 44 | return raise_badarg_exception(env, opt); 45 | } 46 | } 47 | 48 | SmartPtr it(git_branch_iterator_free); 49 | if (git_branch_iterator_new(&it, repo, list_flags) != GIT_OK) 50 | return make_git_error(env, "Cannot create iterator"); 51 | 52 | auto to_atom = [env](auto i) { 53 | switch (i) { 54 | case GIT_BRANCH_LOCAL: return ATOM_LOCAL; 55 | case GIT_BRANCH_REMOTE: return ATOM_REMOTE; 56 | default: return enif_make_int(env, i); 57 | } 58 | }; 59 | 60 | std::vector out; 61 | 62 | for (auto i=0; i < limit; ++i) { 63 | SmartPtr ref(git_reference_free); 64 | git_branch_t out_type; 65 | 66 | switch (git_branch_next(&ref, &out_type, it)) { 67 | case GIT_ITEROVER: 68 | goto done; 69 | case GIT_OK: 70 | out.push_back(enif_make_tuple2(env, to_atom(out_type), 71 | make_binary(env, kind == SHORT_NAME ? git_reference_shorthand(ref) : git_reference_name(ref)))); 72 | break; 73 | default: 74 | return make_git_error(env, "Failed to get next branch"); 75 | } 76 | } 77 | done: 78 | return enif_make_list_from_array(env, &out.front(), out.size()); 79 | } 80 | 81 | ERL_NIF_TERM lg2_branch( 82 | ErlNifEnv* env, git_repository* repo, ERL_NIF_TERM op, ERL_NIF_TERM name, ERL_NIF_TERM arg) 83 | { 84 | BranchOp operation; 85 | 86 | if (enif_is_identical(ATOM_CREATE, op) && arg && enif_is_list(env, arg)) 87 | operation = BranchOp::CREATE; 88 | else if (enif_is_identical(ATOM_RENAME, op) && arg && enif_is_list(env, arg)) 89 | operation = BranchOp::RENAME; 90 | else if (enif_is_identical(ATOM_DELETE, op) && !arg) 91 | operation = BranchOp::DELETE; 92 | else [[unlikely]] 93 | return enif_make_badarg(env); 94 | 95 | ErlNifBinary bname, bnew_name{}; 96 | 97 | if (!enif_inspect_binary(env, name, &bname) || bname.size == 0) [[unlikely]] 98 | return raise_badarg_exception(env, name); 99 | 100 | std::string sname(bin_to_str(bname)); 101 | SmartPtr ref(git_reference_free); 102 | int arity; 103 | ERL_NIF_TERM opt; 104 | const ERL_NIF_TERM* tagvals; 105 | auto overwrite = false; 106 | 107 | switch (operation) { 108 | case BranchOp::CREATE: { 109 | std::string target("HEAD"); 110 | // Parse options 111 | { 112 | while (enif_get_list_cell(env, arg, &opt, &arg)) { 113 | ErlNifBinary bin; 114 | if (enif_get_tuple(env, opt, &arity, &tagvals) && arity == 2) { 115 | if (enif_is_identical(tagvals[0], ATOM_TARGET) && 116 | enif_inspect_binary(env, tagvals[1], &bin) && bin.size > 0 && bin.size <= GIT_OID_SHA1_HEXSIZE) 117 | target = bin_to_str(bin); 118 | else if (enif_is_identical(ATOM_OVERWRITE, opt)) 119 | overwrite = true; 120 | else [[unlikely]] 121 | return raise_badarg_exception(env, opt); 122 | } 123 | else [[unlikely]] 124 | return raise_badarg_exception(env, opt); 125 | } 126 | } 127 | 128 | SmartPtr id(git_object_free); 129 | SmartPtr target_commit(git_commit_free); 130 | 131 | if (git_revparse_single(&id, repo, target.c_str()) != GIT_OK) 132 | return make_git_error(env, std::format("Failed to lookup commit {}", target)); 133 | 134 | // Grab the target commit we're interested in 135 | if (git_commit_lookup(&target_commit, repo, git_object_id(id)) < 0) 136 | return make_git_error(env, std::format("Failed to lookup commit {}", target)); 137 | 138 | if (git_branch_create(&ref, repo, sname.c_str(), target_commit, overwrite) != GIT_OK) [[unlikely]] 139 | make_git_error(env, std::format("Failed to create branch {}", sname)); 140 | 141 | break; 142 | } 143 | case BranchOp::RENAME: { 144 | std::string snew_name; 145 | 146 | while (enif_get_list_cell(env, arg, &opt, &arg)) { 147 | if (enif_is_identical(ATOM_OVERWRITE, opt)) { 148 | overwrite = true; 149 | continue; 150 | } 151 | if (!enif_get_tuple(env, opt, &arity, &tagvals) && arity == 2) [[unlikely]] 152 | return raise_badarg_exception(env, opt); 153 | if (enif_is_identical(tagvals[0], ATOM_NEW_NAME) && 154 | enif_inspect_binary(env, tagvals[1], &bnew_name) && bnew_name.size > 0) 155 | snew_name = bin_to_str(bnew_name); 156 | else [[unlikely]] 157 | return raise_badarg_exception(env, opt); 158 | } 159 | 160 | if (git_branch_lookup(&ref, repo, sname.c_str(), GIT_BRANCH_ALL) != GIT_OK) 161 | return make_git_error(env, std::format("Failed to find branch {}", sname)); 162 | 163 | SmartPtr out(git_reference_free); 164 | 165 | if (git_branch_move(&out, ref, snew_name.c_str(), overwrite) != GIT_OK) [[unlikely]] 166 | return make_git_error(env, std::format("Failed to rename branch {} to {}", sname, snew_name)); 167 | 168 | break; 169 | } 170 | case BranchOp::DELETE: { 171 | if (git_branch_lookup(&ref, repo, sname.c_str(), GIT_BRANCH_ALL) != GIT_OK) 172 | return make_git_error(env, std::format("Failed to find branch {}", sname)); 173 | 174 | if (git_branch_delete(ref) != GIT_OK) [[unlikely]] 175 | return make_git_error(env, "Failed to delete branch"); 176 | 177 | break; 178 | } 179 | default: 180 | assert(false); 181 | } 182 | 183 | return ATOM_OK; 184 | } -------------------------------------------------------------------------------- /c_src/git_cat_file.hpp: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // This code was derived from 3 | // https://github.com/libgit2/libgit2/blob/main/examples/cat-file.c 4 | //----------------------------------------------------------------------------- 5 | // libgit2 "cat-file" - return data from the git ODB 6 | //----------------------------------------------------------------------------- 7 | // Written by the libgit2 contributors 8 | // 9 | // To the extent possible under law, the author(s) have dedicated all copyright 10 | // and related and neighboring rights to this software to the public domain 11 | // worldwide. This software is distributed without any warranty. 12 | // 13 | // You should have received a copy of the CC0 Public Domain Dedication along 14 | // with this software. If not, see 15 | // . 16 | //----------------------------------------------------------------------------- 17 | #pragma once 18 | 19 | #include 20 | 21 | static ERL_NIF_TERM print_signature(ErlNifEnv* env, const git_signature* sig) 22 | { 23 | if (!sig) 24 | return ATOM_NIL; 25 | 26 | return enif_make_tuple4(env, 27 | make_binary(env, sig->name), 28 | make_binary(env, sig->email), 29 | enif_make_int64(env, sig->when.time), 30 | enif_make_int(env, sig->when.offset * 60)); 31 | } 32 | 33 | // Printing out a blob is simple, get the contents and print 34 | static ERL_NIF_TERM encode_blob(ErlNifEnv* env, const git_blob* blob) 35 | { 36 | auto data = std::string_view((const char*)git_blob_rawcontent(blob), (size_t)git_blob_rawsize(blob)); 37 | ERL_NIF_TERM keys[] = {ATOM_TYPE, ATOM_DATA}; 38 | ERL_NIF_TERM vals[] = {ATOM_BLOB, make_binary(env, data)}; 39 | ERL_NIF_TERM map; 40 | 41 | return enif_make_map_from_arrays(env, keys, vals, 2, &map) ? map : make_error(env, ATOM_ENOMEM); 42 | } 43 | 44 | /// Return each entry with its type, id and attributes 45 | static ERL_NIF_TERM encode_tree(ErlNifEnv* env, const git_tree *tree, int abbrev) 46 | { 47 | size_t i, max_i = (int)git_tree_entrycount(tree); 48 | const git_tree_entry *te; 49 | 50 | std::vector v; 51 | v.reserve(max_i); 52 | 53 | for (i = 0; i < max_i; ++i) { 54 | te = git_tree_entry_byindex(tree, i); 55 | 56 | v.push_back( 57 | enif_make_tuple4(env, 58 | make_binary(env, git_tree_entry_name(te)), 59 | make_binary(env, git_object_type2string(git_tree_entry_type(te))), 60 | make_binary(env, oid_to_str(git_tree_entry_id(te), abbrev)), 61 | enif_make_int(env, git_tree_entry_filemode(te)))); 62 | } 63 | 64 | ERL_NIF_TERM keys[] = {ATOM_TYPE, ATOM_COMMITS}; 65 | ERL_NIF_TERM vals[] = {ATOM_TREE, enif_make_list_from_array(env, &v.front(), v.size())}; 66 | ERL_NIF_TERM map; 67 | 68 | return enif_make_map_from_arrays(env, keys, vals, 2, &map) ? map : make_error(env, ATOM_ENOMEM); 69 | } 70 | 71 | // Commits and tags have a few interesting fields in their header. 72 | static ERL_NIF_TERM encode_commit(ErlNifEnv* env, const git_commit* commit, int abbrev) 73 | { 74 | auto max_i = (unsigned int)git_commit_parentcount(commit); 75 | std::vector v; 76 | v.reserve(max_i); 77 | 78 | for (auto i = 0u; i < max_i; ++i) 79 | v.push_back(oid_to_bin_term(env, git_commit_parent_id(commit, i), abbrev)); 80 | 81 | auto msg = git_commit_message(commit); 82 | 83 | ERL_NIF_TERM keys[] = {ATOM_TYPE, ATOM_OID, ATOM_PARENTS, ATOM_AUTHOR, ATOM_COMMITTER, ATOM_MESSAGE}; 84 | ERL_NIF_TERM vals[] = { 85 | ATOM_COMMIT, 86 | oid_to_bin_term(env, git_commit_tree_id(commit), abbrev), 87 | enif_make_list_from_array(env, &v.front(), v.size()), 88 | print_signature(env, git_commit_author(commit)), 89 | print_signature(env, git_commit_committer(commit)), 90 | msg ? make_binary(env, msg) : ATOM_NIL 91 | }; 92 | 93 | ERL_NIF_TERM map; 94 | 95 | return enif_make_map_from_arrays(env, keys, vals, msg ? 4 : 3, &map) ? map : make_error(env, ATOM_ENOMEM); 96 | } 97 | 98 | static ERL_NIF_TERM encode_tag(ErlNifEnv* env, const git_tag* tag, int abbrev) 99 | { 100 | auto msg = git_tag_message(tag); 101 | 102 | ERL_NIF_TERM keys[] = {ATOM_TYPE, ATOM_OBJECT, ATOM_TYPE, ATOM_TAG, ATOM_TAGGER, ATOM_MESSAGE}; 103 | 104 | ERL_NIF_TERM vals[] = { 105 | ATOM_TAG, 106 | oid_to_bin_term(env, git_tag_target_id(tag)), 107 | make_binary(env, git_object_type2string(git_tag_target_type(tag))), 108 | make_binary(env, git_tag_name(tag)), 109 | print_signature(env, git_tag_tagger(tag)), 110 | msg ? make_binary(env, msg) : ATOM_NIL 111 | }; 112 | 113 | ERL_NIF_TERM map; 114 | 115 | return enif_make_map_from_arrays(env, keys, vals, msg ? 5 : 4, &map) ? map : make_error(env, ATOM_ENOMEM); 116 | } 117 | 118 | enum catfile_mode { 119 | SHOW_ALL, 120 | SHOW_TYPE, 121 | SHOW_SIZE, 122 | }; 123 | 124 | // Entry point for this command 125 | static ERL_NIF_TERM lg2_cat_file(ErlNifEnv* env, git_repository* repo, std::string const& rev, ERL_NIF_TERM opts) 126 | { 127 | catfile_mode mode = SHOW_ALL; 128 | int abbrev = GIT_OID_SHA1_HEXSIZE; 129 | 130 | // Parse options 131 | { 132 | int arity, n; 133 | ERL_NIF_TERM opt; 134 | const ERL_NIF_TERM* tagvals; 135 | 136 | while (enif_get_list_cell(env, opts, &opt, &opts)) { 137 | if (enif_is_identical(opt, ATOM_TYPE)) mode = SHOW_TYPE; 138 | else if (enif_is_identical(opt, ATOM_SIZE)) mode = SHOW_SIZE; 139 | else if (enif_get_tuple(env, opt, &arity, &tagvals) && arity == 2) { 140 | if (enif_is_identical(tagvals[0], ATOM_ABBREV) && enif_get_int(env, tagvals[1], &n) && n > 0 && n <= GIT_OID_SHA1_HEXSIZE) 141 | abbrev = n; 142 | else [[unlikely]] 143 | return raise_badarg_exception(env, opt); 144 | } 145 | else [[unlikely]] 146 | return raise_badarg_exception(env, opt); 147 | } 148 | } 149 | 150 | SmartPtr obj(git_object_free); 151 | 152 | if (git_revparse_single(&obj, repo, rev.c_str()) != GIT_OK) 153 | return make_git_error(env, "Could not resolve " + rev); 154 | 155 | switch (mode) { 156 | case SHOW_TYPE: 157 | return enif_make_tuple2(env, ATOM_OK, 158 | enif_make_atom(env, git_object_type2string(git_object_type(obj)))); 159 | case SHOW_SIZE: { 160 | SmartPtr odb(git_odb_free); 161 | if (git_repository_odb(&odb, repo) != GIT_OK) [[unlikely]] 162 | return make_git_error(env, "Could not open ODB"); 163 | 164 | SmartPtr odbobj(git_odb_object_free); 165 | if (git_odb_read(&odbobj, odb.get(), git_object_id(obj)) != GIT_OK) [[unlikely]] 166 | return make_git_error(env, "Could not find obj"); 167 | 168 | return enif_make_tuple2(env, ATOM_OK, enif_make_long(env, (long)git_odb_object_size(odbobj))); 169 | } 170 | default: 171 | switch (git_object_type(obj)) { 172 | case GIT_OBJECT_BLOB: return encode_blob (env, obj.template cast()); 173 | case GIT_OBJECT_COMMIT: return encode_commit(env, obj.template cast(), abbrev); 174 | case GIT_OBJECT_TREE: return encode_tree (env, obj.template cast(), abbrev); 175 | case GIT_OBJECT_TAG: return encode_tag (env, obj.template cast(), abbrev); 176 | default: 177 | return make_git_error(env, std::format("Unknown object type {}", oid_to_str(obj))); 178 | } 179 | break; 180 | } 181 | 182 | return ATOM_OK; 183 | } 184 | -------------------------------------------------------------------------------- /c_src/git_checkout.hpp: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // This code was derived from 3 | // https://github.com/libgit2/libgit2/blob/main/examples/checkout.c 4 | //----------------------------------------------------------------------------- 5 | // libgit2 "checkout" - perform git checkouts 6 | //----------------------------------------------------------------------------- 7 | // Written by the libgit2 contributors 8 | // 9 | // To the extent possible under law, the author(s) have dedicated all copyright 10 | // and related and neighboring rights to this software to the public domain 11 | // worldwide. This software is distributed without any warranty. 12 | // 13 | // You should have received a copy of the CC0 Public Domain Dedication along 14 | // with this software. If not, see 15 | // . 16 | //----------------------------------------------------------------------------- 17 | #pragma once 18 | 19 | #include 20 | 21 | // Define the printf format specifier to use for size_t output 22 | #if defined(_MSC_VER) || defined(__MINGW32__) 23 | # define PRIuZ "Iu" 24 | # define PRIxZ "Ix" 25 | # define PRIdZ "Id" 26 | #else 27 | # define PRIuZ "zu" 28 | # define PRIxZ "zx" 29 | # define PRIdZ "zd" 30 | #endif 31 | 32 | struct checkout_options { 33 | checkout_options() : force(false), verbose(false), perf(false) {} 34 | bool force; /// force the checkout to happen 35 | bool verbose; /// show checkout progress 36 | bool perf; /// show performance data 37 | }; 38 | 39 | struct checkout_stats { 40 | int total_steps; 41 | int stat_calls; 42 | int mkdir_calls; 43 | int chmod_calls; 44 | }; 45 | 46 | int resolve_refish(git_annotated_commit** commit, git_repository* repo, const char* refish) 47 | { 48 | assert(commit != NULL); 49 | 50 | SmartPtr ref(git_reference_free); 51 | 52 | int err = git_reference_dwim(&ref, repo, refish); 53 | if (err == GIT_OK) { 54 | git_annotated_commit_from_ref(commit, repo, ref); 55 | return GIT_OK; 56 | } 57 | 58 | SmartPtr obj(git_object_free); 59 | 60 | err = git_revparse_single(&obj, repo, refish); 61 | if (err == GIT_OK) 62 | err = git_annotated_commit_lookup(commit, repo, git_object_id(obj)); 63 | 64 | return err; 65 | } 66 | 67 | // This corresponds to `git switch --guess`: if a given ref does 68 | // not exist, git will by default try to guess the reference by 69 | // seeing whether any remote has a branch called . If there 70 | // is a single remote only that has it, then it is assumed to be 71 | // the desired reference and a local branch is created for it. 72 | // 73 | // The following is a simplified implementation. It will not try 74 | // to check whether the ref is unique across all remotes. 75 | static int guess_refish(git_annotated_commit** out, git_repository* repo, const char* ref) 76 | { 77 | GitStrArray remotes; 78 | int err; 79 | 80 | if ((err = git_remote_list(&remotes, repo)) < 0) 81 | return err; 82 | 83 | SmartPtr remote_ref(git_reference_free); 84 | 85 | for (size_t i = 0; i < remotes.size(); i++) { 86 | auto refname = std::format("refs/remotes/{}/{}", remotes[i], ref); 87 | if ((err = git_reference_lookup(&remote_ref, repo, refname.c_str())) < 0 && err != GIT_ENOTFOUND) 88 | break; 89 | } 90 | 91 | if (!remote_ref) 92 | return GIT_ENOTFOUND; 93 | 94 | return git_annotated_commit_from_ref(out, repo, remote_ref); 95 | } 96 | 97 | // Implementation of checkout logic 98 | ERL_NIF_TERM lg2_checkout(ErlNifEnv* env, git_repository* repo, std::string const& rev, ERL_NIF_TERM opts) 99 | { 100 | checkout_options o; 101 | int err = 0; 102 | 103 | // Parse options 104 | { 105 | ERL_NIF_TERM opt; 106 | 107 | while (enif_get_list_cell(env, opts, &opt, &opts)) { 108 | if (enif_is_identical(opt, ATOM_VERBOSE)) o.verbose = true; 109 | else if (enif_is_identical(opt, ATOM_PERF)) o.perf = true; 110 | else if (enif_is_identical(opt, ATOM_FORCE)) o.force = true; 111 | else [[unlikely]] 112 | return raise_badarg_exception(env, opt); 113 | } 114 | } 115 | 116 | // Make sure we're not about to checkout while something else is going on 117 | auto state = git_repository_state(repo); 118 | if (state != GIT_REPOSITORY_STATE_NONE) 119 | return make_error(env, "Repository is in unexpected state " + std::to_string(state)); 120 | 121 | SmartPtr target(git_annotated_commit_free); 122 | err = resolve_refish(&target, repo, rev.c_str()) != GIT_OK 123 | && guess_refish (&target, repo, rev.c_str()) != GIT_OK; 124 | 125 | if (err) 126 | return make_git_error(env, "Failed to resolve " + rev); 127 | 128 | checkout_stats stats{}; 129 | 130 | // This function is called to report progression, ie. it's called once with 131 | // a NULL path and the number of total steps, then for each subsequent path, 132 | // the current completed_step value. 133 | auto save_checkout_progress = 134 | [](const char* path, size_t completed_steps, size_t total_steps, void* payload) 135 | { 136 | auto stats = static_cast(payload); 137 | 138 | if (payload) 139 | stats->total_steps = total_steps; 140 | }; 141 | 142 | // This function is called when the checkout completes, and is used to report the 143 | // number of syscalls performed. 144 | auto save_perf_data = 145 | [](const git_checkout_perfdata* perfdata, void* payload) 146 | { 147 | auto stats = static_cast(payload); 148 | 149 | if (stats) { 150 | stats->stat_calls = perfdata->stat_calls; 151 | stats->mkdir_calls = perfdata->mkdir_calls; 152 | stats->chmod_calls = perfdata->chmod_calls; 153 | } 154 | }; 155 | 156 | // This is the main "checkout " logic, responsible for performing 157 | // a branch-based checkout. 158 | git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; 159 | checkout_opts.checkout_strategy = o.force ? GIT_CHECKOUT_FORCE : GIT_CHECKOUT_SAFE; 160 | if (o.verbose) { 161 | checkout_opts.progress_cb = save_checkout_progress; 162 | checkout_opts.progress_payload = &stats; 163 | if (o.perf) { 164 | checkout_opts.perfdata_cb = save_perf_data; 165 | checkout_opts.perfdata_payload = &stats; 166 | } 167 | } 168 | 169 | SmartPtr target_commit(git_commit_free); 170 | 171 | /// Grab the commit we're interested to move to 172 | if (git_commit_lookup(&target_commit, repo, git_annotated_commit_id(target.get())) < 0) 173 | return make_git_error(env, "Failed to lookup commit"); 174 | 175 | // Perform the checkout so the workdir corresponds to what target_commit 176 | // contains. 177 | // 178 | // Note that it's okay to pass a git_commit here, because it will be 179 | // peeled to a tree. 180 | if (git_checkout_tree(repo, target_commit.template cast(), &checkout_opts) < 0) 181 | return make_git_error(env, "Failed to checkout tree"); 182 | 183 | // Now that the checkout has completed, we have to update HEAD. 184 | // 185 | // Depending on the "origin" of target (ie. it's an OID or a branch name), 186 | // we might need to detach HEAD. 187 | auto annotated_ref = git_annotated_commit_ref(target); 188 | 189 | if (!annotated_ref) 190 | err = git_repository_set_head_detached_from_annotated(repo, target) != GIT_OK; 191 | else { 192 | const char* target_head; 193 | 194 | SmartPtr ref(git_reference_free); 195 | SmartPtr branch(git_reference_free); 196 | 197 | if (git_reference_lookup(&ref, repo, annotated_ref) != GIT_OK) 198 | return make_git_error(env, "Failed to lookup annotated HEAD reference"); 199 | 200 | if (!git_reference_is_remote(ref)) 201 | target_head = annotated_ref; 202 | else if (git_branch_create_from_annotated(&branch, repo, rev.c_str(), target, 0) < 0) 203 | return make_git_error(env, "Failed to update HEAD reference from remote branch"); 204 | else 205 | target_head = git_reference_name(branch); 206 | 207 | err = git_repository_set_head(repo, target_head) != GIT_OK; 208 | } 209 | 210 | if (err) 211 | return make_git_error(env, "Failed to update HEAD reference"); 212 | 213 | if (!o.verbose) 214 | return ATOM_OK; 215 | 216 | ERL_NIF_TERM keys[] = {ATOM_TOTAL_STEPS, ATOM_STAT_CALLS, ATOM_MKDIR_CALLS, ATOM_CHMOD_CALLS}; 217 | ERL_NIF_TERM vals[] = { 218 | enif_make_int(env, stats.total_steps), 219 | enif_make_int(env, stats.stat_calls), 220 | enif_make_int(env, stats.mkdir_calls), 221 | enif_make_int(env, stats.chmod_calls), 222 | }; 223 | ERL_NIF_TERM map; 224 | return enif_make_map_from_arrays(env, keys, vals, 4, &map) ? map : enif_raise_exception(env, ATOM_ENOMEM); 225 | } -------------------------------------------------------------------------------- /c_src/git_commit.hpp: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // This code was derived from 3 | // https://github.com/libgit2/libgit2/blob/main/examples/commit.c 4 | //----------------------------------------------------------------------------- 5 | // libgit2 "commit" - commit changes 6 | //----------------------------------------------------------------------------- 7 | // Written by the libgit2 contributors 8 | // 9 | // To the extent possible under law, the author(s) have dedicated all copyright 10 | // and related and neighboring rights to this software to the public domain 11 | // worldwide. This software is distributed without any warranty. 12 | // 13 | // You should have received a copy of the CC0 Public Domain Dedication along 14 | // with this software. If not, see 15 | // . 16 | //----------------------------------------------------------------------------- 17 | #pragma once 18 | 19 | #include 20 | 21 | static bool has_changes(git_repository* repo, git_index* index) 22 | { 23 | auto cb = [](const char* path, 24 | unsigned int status_flags, 25 | void* payload) 26 | { 27 | auto modified = status_flags & 28 | ( 29 | GIT_STATUS_INDEX_NEW | 30 | GIT_STATUS_INDEX_MODIFIED | 31 | GIT_STATUS_INDEX_DELETED | 32 | GIT_STATUS_INDEX_RENAMED | 33 | GIT_STATUS_INDEX_TYPECHANGE 34 | ); 35 | 36 | if (modified) { 37 | auto* p = static_cast(payload); 38 | ++(*p); 39 | 40 | return -1; 41 | } 42 | return 1; 43 | }; 44 | 45 | int diff_count = 0; 46 | 47 | git_status_foreach(repo, cb, &diff_count); 48 | 49 | return diff_count; 50 | } 51 | 52 | ERL_NIF_TERM lg2_commit(ErlNifEnv* env, git_repository* repo, std::string const& comment) 53 | { 54 | SmartPtr parent(git_object_free); 55 | SmartPtr parent_commit(git_commit_free); 56 | 57 | auto error = git_revparse_single(&parent, repo, "HEAD"); 58 | if (error == GIT_ENOTFOUND) // HEAD not found. Creating first commit. 59 | error = 0; 60 | else if (error != 0) [[unlikely]] 61 | return make_git_error(env, "Error parsing a revision string"); 62 | else if (git_commit_lookup(&parent_commit, repo, git_object_id(parent))) [[unlikely]] 63 | return make_git_error(env, "Could not lookup HEAD commit"); 64 | 65 | SmartPtr index(git_index_free); 66 | if (git_repository_index(&index, repo) != GIT_OK) [[unlikely]] 67 | return make_git_error(env, "Could not open repository index"); 68 | 69 | git_oid tree_oid; 70 | if (git_index_write_tree(&tree_oid, index) != GIT_OK) [[unlikely]] 71 | return make_git_error(env, "Could not write tree"); 72 | 73 | if (git_index_write(index) != GIT_OK) [[unlikely]] 74 | return make_git_error(env, "Could not write index"); 75 | 76 | SmartPtr tree(git_tree_free); 77 | if (git_tree_lookup(&tree, repo, &tree_oid) != GIT_OK) [[unlikely]] 78 | return make_git_error(env, "Error looking up tree"); 79 | 80 | if (!has_changes(repo, index)) 81 | return enif_make_tuple2(env, ATOM_OK, ATOM_NIL); 82 | 83 | SmartPtr signature(git_signature_free); 84 | if (git_signature_default(&signature, repo) != GIT_OK) [[unlikely]] 85 | return make_git_error(env, "Error creating signature"); 86 | 87 | git_oid commit_oid; 88 | 89 | const git_commit* parents[] = { parent_commit.get() }; 90 | 91 | if (git_commit_create( 92 | &commit_oid, repo, "HEAD", signature, signature, nullptr, 93 | comment.c_str(), tree, parent ? 1 : 0, parents) != GIT_OK) [[unlikely]] 94 | return make_git_error(env, git_error_last() ? "" : "Error creating commit"); 95 | 96 | return enif_make_tuple2(env, ATOM_OK, make_binary(env, oid_to_str(commit_oid))); 97 | } -------------------------------------------------------------------------------- /c_src/git_config.hpp: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // libgit2 "config" - perform git get/set config 3 | //----------------------------------------------------------------------------- 4 | #pragma once 5 | 6 | #include 7 | 8 | ERL_NIF_TERM lg2_config(ErlNifEnv* env, ERL_NIF_TERM src, ERL_NIF_TERM kk, ERL_NIF_TERM vv) 9 | { 10 | git_config_level_t level = git_config_level_t(GIT_CONFIG_HIGHEST_LEVEL-1); 11 | GitRepoPtr* repo = nullptr; 12 | 13 | if (enif_is_identical(ATOM_DEFAULT, src)) level = git_config_level_t(0); 14 | else if (enif_is_identical(ATOM_SYSTEM, src)) level = GIT_CONFIG_LEVEL_SYSTEM; 15 | else if (enif_is_identical(ATOM_XDG, src)) level = GIT_CONFIG_LEVEL_XDG; 16 | else if (enif_is_identical(ATOM_GLOBAL, src)) level = GIT_CONFIG_LEVEL_GLOBAL; 17 | else if (enif_is_identical(ATOM_LOCAL, src)) level = GIT_CONFIG_LEVEL_LOCAL; 18 | else if (enif_is_identical(ATOM_APP, src)) level = GIT_CONFIG_LEVEL_APP; 19 | else if (enif_is_identical(ATOM_HIGHEST, src)) level = GIT_CONFIG_HIGHEST_LEVEL; 20 | else if (!enif_get_resource(env, src, GIT_REPO_RESOURCE, (void**)&repo)) [[unlikely]] 21 | return enif_make_badarg(env); 22 | 23 | if (level == git_config_level_t(GIT_CONFIG_HIGHEST_LEVEL-1) && !repo) [[unlikely]] 24 | return raise_badarg_exception(env, src); 25 | 26 | ErlNifBinary key, val; 27 | 28 | if (!enif_inspect_binary(env, kk, &key) || key.size == 0) [[unlikely]] 29 | return raise_badarg_exception(env, kk); 30 | 31 | if (vv && (!enif_inspect_binary(env, vv, &val) || val.size == 0)) [[unlikely]] 32 | return raise_badarg_exception(env, vv); 33 | 34 | SmartPtr cfg(git_config_free); 35 | 36 | if (level >= GIT_CONFIG_HIGHEST_LEVEL) { 37 | SmartPtr def_cfg(git_config_free); 38 | auto err = git_config_open_default(&def_cfg); 39 | if (err != GIT_OK) 40 | return make_git_error(env, "Unable to open default configuration"); 41 | else if (level == git_config_level_t(0)) 42 | cfg.swap(def_cfg); 43 | else if (git_config_open_level(&cfg, def_cfg, level) != GIT_OK) 44 | return make_git_error(env, std::format("Unable to open config level {}", atom_to_str(env, src))); 45 | 46 | assert(cfg.get()); 47 | } else { 48 | assert(repo); 49 | if (git_repository_config(&cfg, repo->get()) < 0) [[unlikely]] 50 | return make_git_error(env, "Unable to obtain repository config"); 51 | } 52 | 53 | std::string k = bin_to_str(key); 54 | 55 | if (vv) { 56 | std::string v = bin_to_str(val); 57 | 58 | return git_config_set_string(cfg, k.c_str(), v.c_str()) < 0 59 | ? make_git_error(env, std::format("Unable to set configuration {} = {}", k, v)) 60 | : ATOM_OK; 61 | } else { 62 | SmartPtr entry(git_config_entry_free); 63 | auto err = git_config_get_entry(&entry, cfg, k.c_str()); 64 | 65 | if (err == GIT_ENOTFOUND) 66 | return make_error(env, ATOM_NOT_FOUND); 67 | if (err < 0) [[unlikely]] 68 | return make_git_error(env, std::format("Unable to get configuration for {}", k)); 69 | 70 | return enif_make_tuple2(env, ATOM_OK, make_binary(env, entry->value)); 71 | } 72 | } -------------------------------------------------------------------------------- /c_src/git_index.hpp: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // libgit2 "index" - list index 3 | //----------------------------------------------------------------------------- 4 | 5 | // Implementation of "add" logic 6 | ERL_NIF_TERM lg2_index(ErlNifEnv* env, git_repository* repo, ERL_NIF_TERM opts) 7 | { 8 | assert(repo); 9 | 10 | auto abbrev = GIT_OID_SHA1_HEXSIZE; 11 | auto all = false; 12 | auto show_path = false; 13 | auto show_stage = false; 14 | auto show_conflict = false; 15 | auto show_oid = false; 16 | auto show_mode = false; 17 | auto show_size = false; 18 | auto show_ctime = false; 19 | auto show_mtime = false; 20 | auto count = 0; 21 | 22 | for (ERL_NIF_TERM opt; enif_get_list_cell(env, opts, &opt, &opts);) { 23 | int arity, n; 24 | const ERL_NIF_TERM* tagvals; 25 | if (enif_get_tuple(env, opt, &arity, &tagvals) && arity == 2) { 26 | if (enif_is_identical(tagvals[0], ATOM_ABBREV) && enif_get_int(env, tagvals[1], &n) && n > 0 && n <= GIT_OID_SHA1_HEXSIZE) 27 | abbrev = n; 28 | else if (enif_is_identical(tagvals[0], ATOM_FIELDS) && enif_is_identical(tagvals[1], ATOM_ALL)) 29 | all = true; 30 | else if (enif_is_identical(tagvals[0], ATOM_FIELDS) && enif_is_list(env, tagvals[1])) 31 | for (ERL_NIF_TERM cell, list = tagvals[1]; enif_get_list_cell(env, list, &cell, &list);) { 32 | if (enif_is_identical(cell, ATOM_PATH)) { all = false; show_path = true; count++; } 33 | else if (enif_is_identical(cell, ATOM_STAGE)) { all = false; show_stage = true; count++; } 34 | else if (enif_is_identical(cell, ATOM_CONFLICT)) { all = false; show_conflict = true; count++; } 35 | else if (enif_is_identical(cell, ATOM_OID)) { all = false; show_oid = true; count++; } 36 | else if (enif_is_identical(cell, ATOM_MODE)) { all = false; show_mode = true; count++; } 37 | else if (enif_is_identical(cell, ATOM_SIZE)) { all = false; show_size = true; count++; } 38 | else if (enif_is_identical(cell, ATOM_CTIME)) { all = false; show_ctime = true; count++; } 39 | else if (enif_is_identical(cell, ATOM_MTIME)) { all = false; show_mtime = true; count++; } 40 | else [[unlikely]] 41 | return raise_badarg_exception(env, opt); 42 | } 43 | else [[unlikely]] 44 | return raise_badarg_exception(env, opt); 45 | } 46 | else [[unlikely]] 47 | return raise_badarg_exception(env, opts); 48 | } 49 | 50 | if (!count) 51 | show_path = true; 52 | 53 | SmartPtr index(git_index_free); 54 | 55 | if (git_repository_index(&index, repo) != GIT_OK) [[unlikely]] 56 | return make_git_error(env, "Could not open repository index"); 57 | 58 | git_index_read(index, 0); 59 | 60 | auto ecount = git_index_entrycount(index); 61 | if (!ecount) 62 | return enif_make_list(env, 0); 63 | 64 | std::vector keys; 65 | keys.reserve(count); 66 | if (all || show_path) keys.push_back(ATOM_PATH); 67 | if (all || show_stage) keys.push_back(ATOM_STAGE); 68 | if (all || show_conflict) keys.push_back(ATOM_CONFLICT); 69 | if (all || show_oid) keys.push_back(ATOM_OID); 70 | if (all || show_mode) keys.push_back(ATOM_MODE); 71 | if (all || show_size) keys.push_back(ATOM_SIZE); 72 | if (all || show_ctime) keys.push_back(ATOM_CTIME); 73 | if (all || show_mtime) keys.push_back(ATOM_MTIME); 74 | 75 | auto stage = [](auto s) { 76 | switch (git_index_stage_t(s)) { 77 | case GIT_INDEX_STAGE_NORMAL: return ATOM_NORMAL; 78 | case GIT_INDEX_STAGE_ANCESTOR: return ATOM_ANCESTOR; 79 | case GIT_INDEX_STAGE_OURS: return ATOM_OURS; 80 | case GIT_INDEX_STAGE_THEIRS: return ATOM_THEIRS; 81 | default: return ATOM_ANY; 82 | } 83 | }; 84 | 85 | std::vector res; 86 | res.reserve(ecount); 87 | 88 | for (auto i = 0u; i < ecount; ++i) { 89 | auto e = git_index_get_byindex(index, i); 90 | 91 | std::vector vals; 92 | vals.reserve(count); 93 | if (all || show_path) vals.push_back(make_binary(env, e->path)); 94 | if (all || show_stage) vals.push_back(stage(git_index_entry_stage(e))); 95 | if (all || show_conflict) vals.push_back(git_index_entry_is_conflict(e) ? ATOM_TRUE : ATOM_FALSE); 96 | if (all || show_oid) vals.push_back(oid_to_bin_term(env, &e->id, abbrev)); 97 | if (all || show_mode) vals.push_back(enif_make_int(env, e->mode)); 98 | if (all || show_size) vals.push_back(enif_make_int64(env, e->file_size)); 99 | if (all || show_ctime) vals.push_back(enif_make_int64(env, e->ctime.seconds)); 100 | if (all || show_mtime) vals.push_back(enif_make_int64(env, e->mtime.seconds)); 101 | 102 | assert(keys.size() == vals.size()); 103 | 104 | ERL_NIF_TERM map; 105 | if (!enif_make_map_from_arrays(env, &keys.front(), &vals.front(), vals.size(), &map)) [[unlikely]] 106 | make_error(env, ATOM_ENOMEM); 107 | 108 | res.push_back(map); 109 | } 110 | 111 | return enif_make_list_from_array(env, &res.front(), res.size()); 112 | } -------------------------------------------------------------------------------- /c_src/git_remote.hpp: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // libgit2 "remote" - add/rename/delete/set-url/list remotes 3 | //----------------------------------------------------------------------------- 4 | #pragma once 5 | 6 | #include 7 | 8 | namespace { 9 | enum class RemoteOp { 10 | UNDEFINED, 11 | ADD, 12 | RENAME, 13 | DELETE, 14 | SETURL, 15 | }; 16 | } 17 | 18 | ERL_NIF_TERM lg2_remote( 19 | ErlNifEnv* env, git_repository* repo, std::string const& name, ERL_NIF_TERM op, ERL_NIF_TERM opts) 20 | { 21 | int arity; 22 | ERL_NIF_TERM opt; 23 | const ERL_NIF_TERM* tagvals; 24 | ErlNifBinary bin; 25 | RemoteOp kind = RemoteOp::UNDEFINED; 26 | auto push = false; 27 | std::string val; 28 | 29 | // Parse options 30 | if (enif_is_identical(op, ATOM_DELETE)) 31 | kind = RemoteOp::DELETE; 32 | else if (enif_get_tuple(env, op, &arity, &tagvals) && arity == 2) { 33 | if (enif_is_identical(tagvals[0], ATOM_ADD) && enif_inspect_binary(env, tagvals[1], &bin)) { 34 | kind = RemoteOp::ADD; 35 | val = bin_to_str(bin); 36 | } else if (enif_is_identical(tagvals[0], ATOM_RENAME) && enif_inspect_binary(env, tagvals[1], &bin)) { 37 | kind = RemoteOp::RENAME; 38 | val = bin_to_str(bin); 39 | } else if (enif_is_identical(tagvals[0], ATOM_SETURL) && enif_inspect_binary(env, tagvals[1], &bin)) { 40 | kind = RemoteOp::SETURL; 41 | val = bin_to_str(bin); 42 | while (enif_get_list_cell(env, opts, &opt, &opts)) { 43 | if (enif_is_identical(opt, ATOM_PUSH)) push = true; 44 | else [[unlikely]] 45 | goto ERR; 46 | } 47 | } else [[unlikely]] 48 | goto ERR; 49 | } 50 | else [[unlikely]] 51 | goto ERR; 52 | 53 | 54 | switch (kind) { 55 | case RemoteOp::ADD: { 56 | git_remote* remote; 57 | return git_remote_create(&remote, repo, name.c_str(), val.c_str()) == GIT_OK 58 | ? ATOM_OK : make_git_error(env, "Could not create remote"); 59 | } 60 | case RemoteOp::DELETE: 61 | return git_remote_delete(repo, name.c_str()) == GIT_OK 62 | ? ATOM_OK : make_git_error(env, "Could not delete remote"); 63 | case RemoteOp::SETURL: 64 | return (push ? git_remote_set_pushurl(repo, name.c_str(), val.c_str()) 65 | : git_remote_set_url(repo, name.c_str(), val.c_str())) == GIT_OK 66 | ? ATOM_OK : make_git_error(env, "Could not set-url"); 67 | case RemoteOp::RENAME: { 68 | GitStrArray problems; 69 | if (git_remote_rename(&problems, repo, name.c_str(), val.c_str()) == GIT_OK) [[likely]] 70 | return ATOM_OK; 71 | 72 | if (!problems.size()) 73 | return make_git_error(env, "Could not rename remote"); 74 | 75 | std::vector errs; 76 | errs.reserve(problems.size()); 77 | for (auto i=0u; i < problems.size(); ++i) 78 | errs.push_back(make_binary(env, problems[i])); 79 | 80 | return make_error(env, enif_make_list_from_array(env, &errs.front(), errs.size())); 81 | } 82 | default: 83 | break; 84 | } 85 | ERR: 86 | return raise_badarg_exception(env, op); 87 | } 88 | 89 | ERL_NIF_TERM lg2_remotes_list(ErlNifEnv* env, git_repository* repo) 90 | { 91 | GitStrArray remotes; 92 | 93 | if (git_remote_list(&remotes, repo) != GIT_OK) [[unlikely]] 94 | return make_git_error(env, "Could not retrieve remotes"); 95 | 96 | std::vector res; 97 | res.reserve(remotes.size()*2); 98 | 99 | for (auto i = 0u; i < remotes.size(); i++) { 100 | auto name = remotes[i]; 101 | 102 | SmartPtr remote(git_remote_free); 103 | if (git_remote_lookup(&remote, repo, name) != GIT_OK) 104 | return make_git_error(env, std::format("Could not look up remote {}", name)); 105 | 106 | auto fetch = git_remote_url(remote); 107 | auto push = git_remote_pushurl(remote); 108 | auto single = fetch && (!push || strcmp(fetch, push) == 0); 109 | 110 | ERL_NIF_TERM ll[] = {ATOM_PUSH, ATOM_FETCH}; 111 | 112 | if (push && !single) 113 | res.push_back( 114 | enif_make_tuple3(env, 115 | make_binary(env, name), 116 | make_binary(env, push), 117 | enif_make_list_from_array(env, ll, 1))); 118 | 119 | if (fetch) { 120 | ERL_NIF_TERM type = single 121 | ? enif_make_list_from_array(env, ll, 2) 122 | : enif_make_list_from_array(env, ll+1, 1); 123 | res.push_back(enif_make_tuple3(env, make_binary(env, name), make_binary(env, fetch), type)); 124 | } 125 | } 126 | 127 | return enif_make_list_from_array(env, &res.front(), res.size()); 128 | } 129 | -------------------------------------------------------------------------------- /c_src/git_rev_list.hpp: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // This code was derived from 3 | // https://github.com/libgit2/libgit2/blob/main/examples/rev-list.c 4 | //----------------------------------------------------------------------------- 5 | // libgit2 "rev-list" - transform a rev-spec into a list 6 | //----------------------------------------------------------------------------- 7 | // Written by the libgit2 contributors 8 | // 9 | // To the extent possible under law, the author(s) have dedicated all copyright 10 | // and related and neighboring rights to this software to the public domain 11 | // worldwide. This software is distributed without any warranty. 12 | // 13 | // You should have received a copy of the CC0 Public Domain Dedication along 14 | // with this software. If not, see 15 | // . 16 | //----------------------------------------------------------------------------- 17 | #pragma once 18 | 19 | #include 20 | #include 21 | 22 | static std::mutex s_walk_mutex; 23 | 24 | static int push_commit(git_revwalk* walk, const git_oid* oid, bool hide) 25 | { 26 | return hide ? git_revwalk_hide(walk, oid) : git_revwalk_push(walk, oid); 27 | } 28 | 29 | static int push_spec(git_repository* repo, git_revwalk* walk, const std::string& spec, bool hide) 30 | { 31 | SmartPtr obj(git_object_free); 32 | 33 | if (git_revparse_single(&obj, repo, spec.c_str()) != GIT_OK) 34 | return -1; 35 | 36 | return push_commit(walk, git_object_id(obj), hide); 37 | } 38 | 39 | static int push_range(git_repository* repo, git_revwalk* walk, const std::string& range, bool hide) 40 | { 41 | git_revspec revspec; 42 | 43 | if (git_revparse(&revspec, repo, range.c_str()) != GIT_OK) [[unlikely]] 44 | return -1; 45 | 46 | SmartPtr from(git_object_free, revspec.from); 47 | SmartPtr to (git_object_free, revspec.to); 48 | 49 | if (revspec.flags & GIT_REVPARSE_MERGE_BASE) { 50 | /* TODO: support "..." */ 51 | return GIT_EINVALIDSPEC; 52 | } 53 | 54 | if (push_commit(walk, git_object_id(revspec.from), !hide) != GIT_OK) 55 | return -1; 56 | 57 | return push_commit(walk, git_object_id(revspec.to), hide); 58 | } 59 | 60 | static ERL_NIF_TERM revwalk_parse_revs( 61 | ErlNifEnv* env, git_repository* repo, git_revwalk* walk, ERL_NIF_TERM rev_specs, int limit) 62 | { 63 | ERL_NIF_TERM spec; 64 | bool hide = false; 65 | 66 | int i = 0; 67 | 68 | while (enif_get_list_cell(env, rev_specs, &spec, &rev_specs) && i++ < limit) { 69 | ErlNifBinary bin; 70 | 71 | if (enif_is_identical(spec, ATOM_NOT)) { 72 | hide = !hide; 73 | continue; 74 | } 75 | else if (!enif_inspect_binary(env, spec, &bin)) [[unlikely]] 76 | return raise_badarg_exception(env, spec); 77 | 78 | auto offset = bin.size > 1 && bin.data[0] == '^' ? 1 : 0; 79 | 80 | std::string s((const char*)bin.data+offset, bin.size - offset); 81 | 82 | if (offset) { 83 | if (push_spec(repo, walk, s, !hide) != GIT_OK) [[unlikely]] 84 | return make_git_error(env, "Cannot walk the spec " + s); 85 | } else if (s.find("..") != std::string::npos) { 86 | if (push_range(repo, walk, s, hide) != GIT_OK) [[unlikely]] 87 | return make_git_error(env, "Cannot walk the range " + s);; 88 | } else { 89 | if (push_spec(repo, walk, s, hide) == GIT_OK) [[likely]] 90 | continue; 91 | 92 | git_oid oid; 93 | 94 | #ifdef GIT_EXPERIMENTAL_SHA256 95 | if (git_oid_fromstr(&oid, s.c_str(), GIT_OID_SHA1) != GIT_OK) [[unlikely]] 96 | return make_git_error(env, "Cannot get oid from " + s); 97 | #else 98 | if (git_oid_fromstr(&oid, s.c_str()) != GIT_OK) [[unlikely]] 99 | return make_git_error(env, "Cannot get oid from " + s); 100 | #endif 101 | 102 | if (push_commit(walk, &oid, hide) != GIT_OK) [[unlikely]] 103 | return make_git_error(env, "Cannot walk commit " + s); 104 | } 105 | } 106 | 107 | return 0; 108 | } 109 | 110 | ERL_NIF_TERM lg2_rev_list(ErlNifEnv* env, git_repository* repo, ERL_NIF_TERM revspecs, ERL_NIF_TERM opts) 111 | { 112 | git_sort_t sort = GIT_SORT_NONE; 113 | int limit = INT32_MAX, abbrev = GIT_OID_SHA1_HEXSIZE; 114 | 115 | // Parse options 116 | { 117 | int arity, n; 118 | ERL_NIF_TERM opt; 119 | const ERL_NIF_TERM* tagvals; 120 | 121 | while (enif_get_list_cell(env, opts, &opt, &opts)) { 122 | if (enif_is_identical(opt, ATOM_TOPO_ORDER)) sort = git_sort_t((int)sort | GIT_SORT_TOPOLOGICAL); 123 | else if (enif_is_identical(opt, ATOM_DATE_ORDER)) sort = git_sort_t((int)sort | GIT_SORT_TIME); 124 | else if (enif_is_identical(opt, ATOM_REVERSE)) sort = git_sort_t((int)sort | ((sort & ~GIT_SORT_REVERSE) ^ GIT_SORT_REVERSE)); 125 | else if (enif_get_tuple(env, opt, &arity, &tagvals) && arity == 2) { 126 | if (enif_is_identical(tagvals[0], ATOM_LIMIT) && enif_get_int(env, tagvals[1], &n) && n > 0) limit = n; 127 | else if (enif_is_identical(tagvals[0], ATOM_ABBREV) && enif_get_int(env, tagvals[1], &n) && n > 0 && n <= GIT_OID_SHA1_HEXSIZE) abbrev = n; 128 | else [[unlikely]] 129 | return raise_badarg_exception(env, opt); 130 | } 131 | else [[unlikely]] 132 | return raise_badarg_exception(env, opt); 133 | } 134 | } 135 | 136 | { 137 | // git_revwalk_new is documented not to be thread-safe, we need to 138 | // serialize access 139 | std::lock_guard guard(s_walk_mutex); 140 | 141 | SmartPtr walk(git_revwalk_free); 142 | if (git_revwalk_new(&walk, repo) != GIT_OK) [[unlikely]] 143 | return make_git_error(env, "Allocating revwalk"); 144 | 145 | git_revwalk_sorting(walk, sort); 146 | 147 | auto res = revwalk_parse_revs(env, repo, walk, revspecs, limit); 148 | 149 | if (res != 0) [[unlikely]] 150 | return res; 151 | 152 | std::vector out; 153 | git_oid oid; 154 | int i = 0; 155 | while (!git_revwalk_next(&oid, walk) && i++ < limit) { 156 | char buf[GIT_OID_SHA1_HEXSIZE+1]; 157 | git_oid_fmt(buf, &oid); 158 | buf[abbrev] = '\0'; 159 | out.push_back(make_binary(env, buf)); 160 | } 161 | 162 | return enif_make_list_from_array(env, &out.front(), out.size()); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /c_src/git_rev_parse.hpp: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // This code was derived from 3 | // https://github.com/libgit2/libgit2/blob/main/examples/rev-parse.c 4 | //----------------------------------------------------------------------------- 5 | // libgit2 "rev-parse" - parse revspecs 6 | //----------------------------------------------------------------------------- 7 | // Written by the libgit2 contributors 8 | // 9 | // To the extent possible under law, the author(s) have dedicated all copyright 10 | // and related and neighboring rights to this software to the public domain 11 | // worldwide. This software is distributed without any warranty. 12 | // 13 | // You should have received a copy of the CC0 Public Domain Dedication along 14 | // with this software. If not, see 15 | // . 16 | //----------------------------------------------------------------------------- 17 | #pragma once 18 | 19 | #include 20 | 21 | ERL_NIF_TERM lg2_rev_parse( 22 | ErlNifEnv* env, git_repository* repo, std::string const& spec, ERL_NIF_TERM opts) 23 | { 24 | int abbrev = GIT_OID_SHA1_HEXSIZE; 25 | // Parse options 26 | { 27 | int arity, n; 28 | ERL_NIF_TERM opt; 29 | const ERL_NIF_TERM* tagvals; 30 | 31 | while (enif_get_list_cell(env, opts, &opt, &opts)) { 32 | if (enif_get_tuple(env, opt, &arity, &tagvals) && arity == 2) { 33 | if (enif_is_identical(tagvals[0], ATOM_ABBREV) && enif_get_int(env, tagvals[1], &n) && n > 0 && n <= GIT_OID_SHA1_HEXSIZE) 34 | abbrev = n; 35 | else [[unlikely]] 36 | return raise_badarg_exception(env, opt); 37 | } 38 | else [[unlikely]] 39 | return raise_badarg_exception(env, opt); 40 | } 41 | } 42 | 43 | git_revspec rs; 44 | 45 | if (git_revparse(&rs, repo, spec.c_str()) != GIT_OK) [[unlikely]] 46 | return make_git_error(env, "Could not parse"); 47 | 48 | SmartPtr from(git_object_free, rs.from); 49 | SmartPtr to (git_object_free, rs.to); 50 | 51 | if ((rs.flags & GIT_REVPARSE_SINGLE) != 0) 52 | return enif_make_tuple2(env, ATOM_OK, make_binary(env, oid_to_str(rs.from, abbrev))); 53 | else if ((rs.flags & GIT_REVPARSE_RANGE) == 0) [[unlikely]] 54 | return make_git_error(env, "Invalid results from git_revparse " + spec); 55 | 56 | std::vector keys; 57 | std::vector vals; 58 | 59 | keys.push_back(ATOM_FROM); 60 | vals.push_back(make_binary(env, oid_to_str(rs.from, abbrev))); 61 | 62 | keys.push_back(ATOM_TO); 63 | vals.push_back(make_binary(env, oid_to_str(rs.to, abbrev))); 64 | 65 | if ((rs.flags & GIT_REVPARSE_MERGE_BASE) != 0) { 66 | git_oid base; 67 | if (git_merge_base(&base, repo, git_object_id(rs.from), git_object_id(rs.to)) != GIT_OK) 68 | return make_git_error(env, "Could not find merge base " + spec); 69 | 70 | keys.push_back(ATOM_MERGE_BASE); 71 | vals.push_back(make_binary(env, oid_to_str(base, abbrev))); 72 | } 73 | 74 | ERL_NIF_TERM map; 75 | return enif_make_map_from_arrays(env, &keys.front(), &vals.front(), keys.size(), &map) 76 | ? map : make_error(env, ATOM_ENOMEM); 77 | } 78 | -------------------------------------------------------------------------------- /c_src/git_status.hpp: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // This code was derived from 3 | // https://github.com/libgit2/libgit2/blob/main/examples/status.c 4 | //----------------------------------------------------------------------------- 5 | // libgit2 "status" - get the repository status 6 | //----------------------------------------------------------------------------- 7 | // Written by the libgit2 contributors 8 | // 9 | // To the extent possible under law, the author(s) have dedicated all copyright 10 | // and related and neighboring rights to this software to the public domain 11 | // worldwide. This software is distributed without any warranty. 12 | // 13 | // You should have received a copy of the CC0 Public Domain Dedication along 14 | // with this software. If not, see 15 | // . 16 | //----------------------------------------------------------------------------- 17 | #pragma once 18 | 19 | #include 20 | 21 | namespace { 22 | struct VisitorState { 23 | VisitorState(std::vector& submods, ErlNifEnv* env) 24 | : submods(submods), env(env) 25 | {} 26 | 27 | std::vector& submods; 28 | ErlNifEnv* env; 29 | }; 30 | } 31 | 32 | ERL_NIF_TERM lg2_status(ErlNifEnv* env, git_repository* repo, ERL_NIF_TERM opts) 33 | { 34 | assert(repo); 35 | 36 | git_status_options statusopt = GIT_STATUS_OPTIONS_INIT; 37 | 38 | statusopt.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; 39 | statusopt.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | 40 | GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX | 41 | GIT_STATUS_OPT_SORT_CASE_SENSITIVELY; 42 | 43 | std::vector paths; 44 | auto showbranch = false; 45 | auto showsubmod = false; 46 | 47 | if (git_repository_is_bare(repo)) [[unlikely]] 48 | return make_error(env, 49 | std::format("Cannot report status on bare repository {}", git_repository_path(repo))); 50 | 51 | //---- Parse args 52 | for (ERL_NIF_TERM opt; enif_get_list_cell(env, opts, &opt, &opts);) { 53 | int arity; 54 | const ERL_NIF_TERM* tagvals; 55 | 56 | if (enif_get_tuple(env, opt, &arity, &tagvals) && arity == 2) { 57 | ERL_NIF_TERM val; 58 | if (parse_atom_if(env, ATOM_UNTRACKED, tagvals, val)) [[likely]] { 59 | if (enif_is_identical(ATOM_NONE, val)) statusopt.flags &= ~GIT_STATUS_OPT_INCLUDE_UNTRACKED; 60 | else if (enif_is_identical(ATOM_NORMAL, val)) statusopt.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED; 61 | else if (enif_is_identical(ATOM_RECURSIVE, val)) statusopt.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED | 62 | GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; 63 | else 64 | return raise_badarg_exception(env, opt); 65 | } 66 | else if (enif_is_identical(ATOM_PATHS, tagvals[0]) && enif_is_list(env, tagvals[1])) { 67 | for (ERL_NIF_TERM o, list=tagvals[1]; enif_get_list_cell(env, list, &o, &list);) { 68 | std::string tmp; 69 | if (!term_to_str(env, o, tmp)) 70 | return raise_badarg_exception(env, opt); 71 | paths.push_back(tmp); 72 | } 73 | } 74 | else 75 | return raise_badarg_exception(env, opt); 76 | 77 | } 78 | else if (enif_is_identical(ATOM_BRANCH, opt)) showbranch = true; 79 | else if (enif_is_identical(ATOM_IGNORED, opt)) statusopt.flags |= GIT_STATUS_OPT_INCLUDE_IGNORED; 80 | else if (enif_is_identical(ATOM_IGNORE_SUBMODULES, opt)) statusopt.flags |= GIT_STATUS_OPT_EXCLUDE_SUBMODULES; 81 | else if (enif_is_identical(ATOM_SUBMODULES, opt)) { 82 | showsubmod = true; 83 | statusopt.flags &= ~GIT_STATUS_OPT_EXCLUDE_SUBMODULES; 84 | } 85 | else [[unlikely]] 86 | return raise_badarg_exception(env, opt); 87 | } 88 | 89 | std::vector cpaths; 90 | if (paths.size()) { 91 | cpaths.reserve(paths.size()); 92 | for (auto& s : paths) 93 | cpaths.push_back(s.c_str()); 94 | 95 | statusopt.pathspec = { .strings = const_cast(&cpaths.front()), .count = cpaths.size() }; 96 | } 97 | 98 | //---- Process status command 99 | 100 | // We use `git_status_list_new()` to generate a list of status 101 | // information which lets us iterate over it at our 102 | // convenience and extract the data we want to show out of 103 | // each entry. 104 | // 105 | // You can use `git_status_foreach()` or 106 | // `git_status_foreach_ext()` if you'd prefer to execute a 107 | // callback for each entry. The latter gives you more control 108 | // about what results are presented. 109 | SmartPtr status(git_status_list_free); 110 | 111 | if (git_status_list_new(&status, repo, &statusopt) != GIT_OK) [[unlikely]] 112 | return make_git_error(env, "Could not get status"); 113 | 114 | std::vector keys; 115 | std::vector vals; 116 | 117 | if (showbranch) { 118 | SmartPtr head(git_reference_free); 119 | 120 | auto error = git_repository_head(&head, repo); 121 | auto branch = error == GIT_OK ? git_reference_shorthand(head) : nullptr; 122 | 123 | if (error < 0 && error != GIT_EUNBORNBRANCH && error != GIT_ENOTFOUND) 124 | return make_git_error(env, "Failed to get current branch"); 125 | 126 | keys.push_back(ATOM_BRANCH); 127 | vals.push_back(branch ? make_binary(env, branch) : ATOM_NIL); 128 | } 129 | 130 | if (showsubmod) { 131 | std::vector submods; 132 | 133 | auto visit = [](git_submodule* sm, const char*, void* payload) 134 | { 135 | VisitorState& state = *static_cast(payload); 136 | auto path = git_submodule_path(sm); 137 | auto url = git_submodule_url(sm); 138 | state.submods.push_back( 139 | enif_make_tuple2(state.env, make_binary(state.env, path), make_binary(state.env, url))); 140 | return 0; 141 | }; 142 | 143 | VisitorState state(submods, env); 144 | if (git_submodule_foreach(repo, visit, &state) != GIT_OK) [[unlikely]] 145 | return make_git_error(env, "Cannot iterate submodules"); 146 | 147 | if (submods.size()) { 148 | keys.push_back(ATOM_SUBMODULES); 149 | vals.push_back(enif_make_list_from_array(env, &submods.front(), submods.size())); 150 | } 151 | } 152 | 153 | size_t maxi = git_status_list_entrycount(status); 154 | 155 | std::vector index_changes; 156 | std::vector wt_changes; 157 | std::vector untracked; 158 | std::vector ignored; 159 | std::vector submodules; 160 | 161 | // Changes in index 162 | for (auto i = 0u; i < maxi; ++i) { 163 | auto s = git_status_byindex(status, i); 164 | 165 | if (s->status == GIT_STATUS_CURRENT) 166 | continue; 167 | 168 | auto istatus = 169 | (s->status & GIT_STATUS_INDEX_NEW) ? ATOM_NEW : 170 | (s->status & GIT_STATUS_INDEX_MODIFIED) ? ATOM_MODIFIED : 171 | (s->status & GIT_STATUS_INDEX_DELETED) ? ATOM_DELETED : 172 | (s->status & GIT_STATUS_INDEX_RENAMED) ? ATOM_RENAMED : 173 | (s->status & GIT_STATUS_INDEX_TYPECHANGE) ? ATOM_TYPECHANGE : ATOM_NIL; 174 | 175 | auto wstatus = 176 | (s->status & GIT_STATUS_WT_MODIFIED) ? ATOM_MODIFIED : 177 | (s->status & GIT_STATUS_WT_DELETED) ? ATOM_DELETED : 178 | (s->status & GIT_STATUS_WT_RENAMED) ? ATOM_RENAMED : 179 | (s->status & GIT_STATUS_WT_TYPECHANGE) ? ATOM_TYPECHANGE : ATOM_NIL; 180 | 181 | auto idx = s->head_to_index; 182 | auto wt = s->index_to_workdir; 183 | 184 | auto fmt_path = [env](auto& v, auto xstatus, auto delta) { 185 | ERL_NIF_TERM val = ATOM_NIL; 186 | if (delta->old_file.path && delta->new_file.path) { 187 | val = strcmp(delta->old_file.path, delta->new_file.path) == 0 188 | ? enif_make_tuple2(env, xstatus, make_binary(env, delta->old_file.path)) 189 | : enif_make_tuple3(env, xstatus, 190 | make_binary(env, delta->old_file.path), 191 | make_binary(env, delta->new_file.path)); 192 | } 193 | else if (delta->old_file.path && !delta->new_file.path) 194 | val = enif_make_tuple2(env, xstatus, make_binary(env, delta->old_file.path)); 195 | else if (delta->new_file.path && !delta->old_file.path) 196 | val = enif_make_tuple2(env, xstatus, make_binary(env, delta->new_file.path)); 197 | 198 | if (val != ATOM_NIL) 199 | v.push_back(val); 200 | }; 201 | 202 | if (idx && istatus != ATOM_NIL) 203 | fmt_path(index_changes, istatus, idx); 204 | 205 | if (wt && wstatus != ATOM_NIL) 206 | fmt_path(wt_changes, wstatus, wt); 207 | 208 | if (s->status == GIT_STATUS_WT_NEW && wt->old_file.path) 209 | untracked.push_back(make_binary(env, wt->old_file.path)); 210 | 211 | if (s->status == GIT_STATUS_IGNORED && wt->old_file.path) 212 | ignored.push_back(make_binary(env, wt->old_file.path)); 213 | 214 | // A commit in a tree is how submodules are stored, take a look at its status 215 | auto smstatus = 0u; 216 | if (wt && wt->new_file.mode == GIT_FILEMODE_COMMIT && 217 | git_submodule_status(&smstatus, repo, wt->new_file.path, GIT_SUBMODULE_IGNORE_UNSPECIFIED) == GIT_OK) 218 | { 219 | auto smst = 220 | (smstatus & GIT_SUBMODULE_STATUS_WD_MODIFIED) ? ATOM_NEW : 221 | (smstatus & GIT_SUBMODULE_STATUS_WD_INDEX_MODIFIED) ? ATOM_MODIFIED : 222 | (smstatus & GIT_SUBMODULE_STATUS_WD_WD_MODIFIED) ? ATOM_MODIFIED : 223 | (smstatus & GIT_SUBMODULE_STATUS_WD_UNTRACKED) ? ATOM_UNTRACKED : ATOM_NIL; 224 | 225 | if (smst != ATOM_NIL) { 226 | submodules.push_back( 227 | enif_make_tuple2(env, smst, make_binary(env, 228 | idx && idx->old_file.path ? idx->old_file.path : 229 | idx && idx->new_file.path ? idx->new_file.path : 230 | wt && wt->old_file.path ? wt->old_file.path : 231 | wt && wt->new_file.path ? wt->new_file.path : ""))); 232 | } 233 | } 234 | } 235 | 236 | if (index_changes.size()) { 237 | keys.push_back(ATOM_INDEX); 238 | vals.push_back(enif_make_list_from_array(env, &index_changes.front(), index_changes.size())); 239 | } 240 | 241 | if (wt_changes.size()) { 242 | keys.push_back(ATOM_WORKTREE); 243 | vals.push_back(enif_make_list_from_array(env, &wt_changes.front(), wt_changes.size())); 244 | } 245 | 246 | if (untracked.size()) { 247 | keys.push_back(ATOM_UNTRACKED); 248 | vals.push_back(enif_make_list_from_array(env, &untracked.front(), untracked.size())); 249 | } 250 | 251 | if (ignored.size()) { 252 | keys.push_back(ATOM_IGNORED); 253 | vals.push_back(enif_make_list_from_array(env, &ignored.front(), ignored.size())); 254 | } 255 | 256 | if (submodules.size()) { 257 | keys.push_back(ATOM_SUBMODULES); 258 | vals.push_back(enif_make_list_from_array(env, &submodules.front(), submodules.size())); 259 | } 260 | 261 | ERL_NIF_TERM map; 262 | return enif_make_map_from_arrays(env, &keys.front(), &vals.front(), keys.size(), &map) 263 | ? map : make_error(env, ATOM_ENOMEM); 264 | } 265 | -------------------------------------------------------------------------------- /c_src/git_tag.hpp: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // This code was derived from 3 | // https://github.com/libgit2/libgit2/blob/main/examples/tag.c 4 | //----------------------------------------------------------------------------- 5 | // libgit2 "tag" - create/delete/list tags 6 | //----------------------------------------------------------------------------- 7 | // Written by the libgit2 contributors 8 | // 9 | // To the extent possible under law, the author(s) have dedicated all copyright 10 | // and related and neighboring rights to this software to the public domain 11 | // worldwide. This software is distributed without any warranty. 12 | // 13 | // You should have received a copy of the CC0 Public Domain Dedication along 14 | // with this software. If not, see 15 | // . 16 | //----------------------------------------------------------------------------- 17 | #pragma once 18 | 19 | #include 20 | 21 | namespace { 22 | enum class TagOp { 23 | CREATE, 24 | DELETE, 25 | LIST, 26 | }; 27 | } 28 | 29 | // Return the end of individual message lines 30 | static const char* end_of_lines(const char *message, int num_lines) 31 | { 32 | auto msg = message; 33 | auto num = num_lines - 1; 34 | 35 | if (!msg) [[unlikely]] 36 | return msg; 37 | 38 | while (*msg && *msg != '\n'); // first line - headline 39 | while (*msg && *msg == '\n') msg++; // skip over new lines 40 | 41 | if (num == 0) return msg; // just headline? 42 | 43 | // include individual commit/tag lines 44 | while (*msg && num-- >= 2) { 45 | while (*msg && *msg != '\n') msg++; 46 | 47 | // handle consecutive new lines 48 | if (*msg && *msg == '\n' && msg[1] == '\n') 49 | num--; 50 | 51 | while(*msg && *msg == '\n') msg++; 52 | } 53 | 54 | return msg; 55 | } 56 | 57 | // Implementation of "add" logic 58 | ERL_NIF_TERM lg2_tag(ErlNifEnv* env, git_repository* repo, std::string const& name, ERL_NIF_TERM op, ERL_NIF_TERM opts) 59 | { 60 | assert(repo); 61 | 62 | TagOp action; 63 | std::string pattern("*"), comment, target; 64 | size_t num_lines = 0; 65 | auto force = false; 66 | 67 | if (enif_is_identical(op, ATOM_LIST)) action = TagOp::LIST; 68 | else if (enif_is_identical(op, ATOM_CREATE)) action = TagOp::CREATE; 69 | else if (enif_is_identical(op, ATOM_DELETE)) action = TagOp::DELETE; 70 | else [[unlikely]] 71 | return raise_badarg_exception(env, op); 72 | 73 | // Parse args 74 | for (ERL_NIF_TERM opt; enif_get_list_cell(env, opts, &opt, &opts);) { 75 | int arity; 76 | const ERL_NIF_TERM* tagvals; 77 | 78 | if (enif_get_tuple(env, opt, &arity, &tagvals) && arity == 2) { 79 | if (!parse_if(env, ATOM_MESSAGE, tagvals, comment) && 80 | !parse_if(env, ATOM_PATTERN, tagvals, pattern) && 81 | !parse_if(env, ATOM_TARGET, tagvals, target) && 82 | !parse_if(env, ATOM_LINES, tagvals, num_lines)) [[unlikely]] 83 | return raise_badarg_exception(env, opt); 84 | } 85 | else if (enif_is_identical(ATOM_FORCE, opt)) 86 | force = true; 87 | else [[unlikely]] 88 | return raise_badarg_exception(env, opt); 89 | } 90 | 91 | ERL_NIF_TERM out; 92 | 93 | switch (action) { 94 | case TagOp::LIST: { 95 | out = enif_make_list(env, 0); 96 | GitStrArray tag_names; 97 | if (git_tag_list_match(&tag_names, pattern.c_str(), repo) != GIT_OK) [[unlikely]] 98 | return make_git_error(env, "Unable to get list of tags"); 99 | 100 | std::vector res; 101 | res.reserve(tag_names.size()); 102 | 103 | for (auto i=0u; i < tag_names.size(); ++i) { 104 | auto tag_name = tag_names[i]; 105 | 106 | SmartPtr obj(git_object_free); 107 | 108 | if (git_revparse_single(&obj, repo, tag_name) != GIT_OK) [[unlikely]] 109 | return make_git_error(env, "Failed to lookup rev"); 110 | 111 | switch (git_object_type(obj)) { 112 | case GIT_OBJECT_TAG: { 113 | auto tag = obj.template cast(); 114 | auto msg = git_tag_message(tag); 115 | auto end = end_of_lines(msg, num_lines); 116 | auto ttt = make_binary(env, git_tag_name(tag)); 117 | res.push_back( 118 | (end-msg) > 0 119 | ? enif_make_tuple2(env, ttt, 120 | make_binary(env, std::string_view(msg, end-msg))) 121 | : ttt); 122 | break; 123 | } 124 | case GIT_OBJECT_COMMIT: { 125 | auto cmt = obj.template cast(); 126 | auto msg = git_commit_message(cmt); 127 | auto end = end_of_lines(msg, num_lines); 128 | auto ttt = make_binary(env, tag_name); 129 | res.push_back( 130 | (end-msg) > 0 131 | ? enif_make_tuple2(env, ttt, 132 | make_binary(env, std::string_view(msg, end-msg))) 133 | : ttt); 134 | break; 135 | } 136 | default: 137 | res.push_back(make_binary(env, tag_name)); 138 | break; 139 | } 140 | 141 | out = enif_make_list_from_array(env, &res.front(), res.size()); 142 | } 143 | break; 144 | } 145 | case TagOp::CREATE: { 146 | if (name.empty()) [[unlikely]] 147 | return raise_badarg_exception(env, ATOM_NAME); 148 | 149 | if (target.empty()) target = "HEAD"; 150 | 151 | SmartPtr target_obj(git_object_free); 152 | if (git_revparse_single(&target_obj, repo, target.c_str()) != GIT_OK) [[unlikely]] 153 | return make_git_error(env, std::format("Unable to resolve spec {}", target)); 154 | 155 | git_oid oid; 156 | ERL_NIF_TERM res; 157 | 158 | if (comment.empty()) // Create lightweight tag 159 | res = git_tag_create_lightweight(&oid, repo, name.c_str(), target_obj, force); 160 | else { 161 | SmartPtr tagger(git_signature_free); 162 | if (git_signature_default(&tagger, repo) != GIT_OK) [[unlikely]] 163 | return make_git_error(env, "Unable to create signature"); 164 | 165 | res = git_tag_create(&oid, repo, name.c_str(), target_obj, tagger, comment.c_str(), force); 166 | } 167 | 168 | if (res != GIT_OK) [[unlikely]] 169 | return make_git_error(env, "Unable to create tag"); 170 | 171 | out = ATOM_OK; 172 | break; 173 | } 174 | case TagOp::DELETE: { 175 | if (name.empty()) [[unlikely]] 176 | return raise_badarg_exception(env, ATOM_NAME); 177 | 178 | if (git_tag_delete(repo, name.c_str()) != GIT_OK) 179 | return make_git_error(env, std::format("Unable to delete tag {}", name)); 180 | 181 | out = ATOM_OK; 182 | break; 183 | } 184 | default: 185 | return enif_make_badarg(env); 186 | } 187 | 188 | return out; 189 | } -------------------------------------------------------------------------------- /c_src/git_utils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "git_atoms.hpp" 9 | 10 | #ifdef HAVE_SRCLOC 11 | #include 12 | #endif 13 | 14 | #if ERL_NIF_MAJOR_VERSION > 2 || (ERL_NIF_MAJOR_VERSION == 2 && ERL_NIF_MINOR_VERSION >= 17) 15 | # define AM_ENCODING ERL_NIF_UTF8 16 | #else 17 | # define AM_ENCODING ERL_NIF_LATIN1 18 | #endif 19 | 20 | static ErlNifResourceType* GIT_REPO_RESOURCE; 21 | 22 | struct GitRepoPtr { 23 | static GitRepoPtr* create(git_repository* p) { 24 | auto rp = static_cast(enif_alloc_resource(GIT_REPO_RESOURCE, sizeof(GitRepoPtr))); 25 | 26 | #ifdef NIF_DEBUG 27 | fprintf(stderr, "=egit=> Allocated NIF resource %p (%p) [%d]\r\n", p, rp, __LINE__); 28 | #endif 29 | 30 | if (!rp) [[unlikely]] 31 | return nullptr; 32 | 33 | new (rp) GitRepoPtr(p); 34 | return rp; 35 | } 36 | 37 | ~GitRepoPtr() { 38 | if (m_ptr) { 39 | #ifdef NIF_DEBUG 40 | fprintf(stderr, "=egit=> Releasing repository pointer %p (%p) [%d]\r\n", m_ptr, this, __LINE__); 41 | #endif 42 | git_repository_free(m_ptr); 43 | m_ptr = nullptr; 44 | } 45 | } 46 | 47 | git_repository const* get() const { return m_ptr; } 48 | git_repository* get() { return m_ptr; } 49 | 50 | ERL_NIF_TERM to_enif_resource(ErlNifEnv* env) { 51 | ERL_NIF_TERM resource = enif_make_resource(env, (void*)this); 52 | 53 | #ifdef NIF_DEBUG 54 | fprintf(stderr, "=egit=> Moved repo ownership %p [%d]\r\n", this, __LINE__); 55 | #endif 56 | 57 | enif_release_resource((void*)this); // Grant ownership to the resource 58 | return resource; 59 | } 60 | 61 | private: 62 | GitRepoPtr(git_repository* p) : m_ptr(p) {} 63 | 64 | git_repository* m_ptr; 65 | }; 66 | 67 | /* 68 | #define SMART_PTR(var, deleter) \ 69 | std::unique_ptr::type, void(*)(decltype(var))> p ## var(var, deleter) 70 | */ 71 | 72 | template 73 | struct SmartPtr { 74 | using type = T; 75 | typedef void(*Deleter)(T*); 76 | 77 | explicit SmartPtr(Deleter del, T* p = nullptr) : m_ptr(p), m_del(del) { 78 | assert(m_del); 79 | } 80 | 81 | ~SmartPtr() { 82 | if (m_ptr) { m_del(m_ptr); m_ptr = nullptr; } 83 | } 84 | 85 | T** operator&() { return &m_ptr; } 86 | T& operator*() { assert(m_ptr); return *m_ptr; } 87 | T const* operator->() const { return m_ptr; } 88 | T* operator->() { return m_ptr; } 89 | bool operator!() const { return m_ptr == nullptr; } 90 | 91 | // Cast operator 92 | operator const T*() const { return (const T*)m_ptr; } 93 | operator T*() const { return m_ptr; } 94 | operator bool() const { return m_ptr != nullptr; } 95 | 96 | T const* get() const { return m_ptr; } 97 | T* get() { return m_ptr; } 98 | 99 | // Release ownership 100 | T* release() { auto p = m_ptr; m_ptr = nullptr; return p; } 101 | 102 | void swap(SmartPtr& src) { std::swap(m_ptr, src.m_ptr); } 103 | 104 | template 105 | U cast() { return reinterpret_cast(m_ptr); } 106 | private: 107 | T* m_ptr; 108 | Deleter m_del; 109 | }; 110 | 111 | struct GitStrArray { 112 | GitStrArray() : m_array{} {} 113 | ~GitStrArray() { if (m_array.strings) git_strarray_dispose(&m_array); } 114 | 115 | git_strarray* operator&() { return &m_array; } 116 | git_strarray& operator*() { return m_array; } 117 | git_strarray const* operator->() const { return &m_array; } 118 | git_strarray* operator->() { return &m_array; } 119 | bool operator!() const { return m_array.strings == nullptr; } 120 | 121 | git_strarray const* get() const { return &m_array; } 122 | git_strarray* get() { return &m_array; } 123 | 124 | // Cast operator 125 | operator const git_strarray*() const { return &m_array; } 126 | operator bool() const { return !this->operator!(); } 127 | 128 | size_t size() const { return m_array.count; } 129 | 130 | const char* operator[](size_t idx) const { return m_array.strings[idx]; } 131 | private: 132 | git_strarray m_array; 133 | }; 134 | 135 | 136 | template 137 | struct ScopeCleanup { 138 | ScopeCleanup(T fun) : m_fun(fun) {} 139 | ~ScopeCleanup() { m_fun(); } 140 | private: 141 | T m_fun; 142 | }; 143 | 144 | inline std::tuple 145 | make_binary(ErlNifEnv* env, size_t size) 146 | { 147 | ERL_NIF_TERM term; 148 | auto p = enif_make_new_binary(env, size, &term); 149 | return std::make_tuple(term, p); 150 | } 151 | 152 | inline ERL_NIF_TERM make_binary(ErlNifEnv* env, std::string_view const& str) 153 | { 154 | auto [term, p] = make_binary(env, str.length()); 155 | memcpy(p, str.data(), str.length()); 156 | return term; 157 | } 158 | 159 | inline bool term_to_str(ErlNifEnv* env, ERL_NIF_TERM term, std::string& out, bool prohibit_empty = true) { 160 | ErlNifBinary bin; 161 | if (!enif_inspect_binary(env, term, &bin) || (prohibit_empty && bin.size == 0)) 162 | return false; 163 | out = std::string((const char*)bin.data, bin.size); 164 | return true; 165 | } 166 | 167 | inline std::string bin_to_str(ErlNifBinary const& bin) { 168 | return std::string((const char*)bin.data, bin.size); 169 | } 170 | 171 | inline ERL_NIF_TERM oid_to_bin_term(ErlNifEnv* env, git_oid const* oid, int abbrev = GIT_OID_SHA1_HEXSIZE) 172 | { 173 | assert(abbrev <= GIT_OID_SHA1_HEXSIZE); 174 | char buf[GIT_OID_SHA1_HEXSIZE+1]; 175 | git_oid_tostr(buf, abbrev+1, oid); 176 | return make_binary(env, std::string_view(buf, abbrev)); 177 | } 178 | 179 | inline std::string oid_to_str(git_oid const& oid, int abbrev = GIT_OID_SHA1_HEXSIZE) 180 | { 181 | auto out = std::string(abbrev, '\0'); 182 | git_oid_tostr(out.data(), abbrev+1, &oid); 183 | return out; 184 | } 185 | 186 | inline std::string oid_to_str(git_oid const* oid, int abbrev = GIT_OID_SHA1_HEXSIZE) { 187 | return oid_to_str(*oid, abbrev); 188 | } 189 | 190 | inline std::string oid_to_str(git_object const* oid, int abbrev = GIT_OID_SHA1_HEXSIZE) { 191 | return oid_to_str(git_object_id(oid), abbrev); 192 | } 193 | 194 | inline std::string atom_to_str(ErlNifEnv* env, ERL_NIF_TERM atom) { 195 | unsigned int len; 196 | if (!enif_get_atom_length(env, atom, &len, AM_ENCODING)) 197 | return ""; 198 | std::string s(len, '\0'); 199 | if (!enif_get_atom(env, atom, s.data(), len+1, AM_ENCODING)) [[unlikely]] 200 | return std::format("", atom); 201 | return s; 202 | } 203 | 204 | #ifdef HAVE_SRCLOC 205 | inline ERL_NIF_TERM src_info(ErlNifEnv* env, const std::source_location& loc) 206 | { 207 | char buf[128]; 208 | snprintf(buf, sizeof(buf), "%s:%d", basename(loc.file_name()), loc.line()); 209 | return make_binary(env, buf); 210 | } 211 | #endif 212 | 213 | inline ERL_NIF_TERM fmt_git_error(ErlNifEnv* env, std::string const& pfx 214 | #ifdef HAVE_SRCLOC 215 | , const std::source_location& loc 216 | #endif 217 | ) 218 | { 219 | char buf[256]; 220 | const char* delim = ""; 221 | const char* err = ""; 222 | 223 | if (git_error_last()) 224 | err = git_error_last()->message; 225 | 226 | if (git_error_last() && !pfx.empty()) 227 | delim = ": "; 228 | 229 | #ifdef HAVE_SRCLOC 230 | snprintf(buf, sizeof(buf), "%s%s%s [%s:%d]", pfx.c_str(), delim, err, basename(loc.file_name()), loc.line()); 231 | #else 232 | snprintf(buf, sizeof(buf), "%s%s%s", pfx.c_str(), delim, err); 233 | #endif 234 | return make_binary(env, buf); 235 | } 236 | 237 | inline ERL_NIF_TERM raise_git_exception(ErlNifEnv* env, std::string const& pfx 238 | #ifdef HAVE_SRCLOC 239 | , const std::source_location loc = std::source_location::current() 240 | #endif 241 | ) 242 | { 243 | return enif_raise_exception(env, fmt_git_error(env, pfx 244 | #ifdef HAVE_SRCLOC 245 | , loc 246 | #endif 247 | )); 248 | } 249 | 250 | inline ERL_NIF_TERM raise_badarg_exception(ErlNifEnv* env, ERL_NIF_TERM err 251 | #ifdef HAVE_SRCLOC 252 | , const std::source_location loc = std::source_location::current() 253 | #endif 254 | ) 255 | { 256 | #ifdef HAVE_SRCLOC 257 | return enif_raise_exception(env, enif_make_tuple3(env, ATOM_BADARG, err, src_info(env, loc))); 258 | #else 259 | return enif_raise_exception(env, enif_make_tuple2(env, ATOM_BADARG, err)); 260 | #endif 261 | } 262 | 263 | inline ERL_NIF_TERM make_git_error(ErlNifEnv* env, std::string const& pfx 264 | #ifdef HAVE_SRCLOC 265 | , const std::source_location loc = std::source_location::current() 266 | #endif 267 | ) 268 | { 269 | return enif_make_tuple2(env, ATOM_ERROR, fmt_git_error(env, pfx 270 | #ifdef HAVE_SRCLOC 271 | , loc 272 | #endif 273 | )); 274 | } 275 | 276 | inline ERL_NIF_TERM make_error(ErlNifEnv* env, std::string_view const& err) 277 | { 278 | return enif_make_tuple2(env, ATOM_ERROR, make_binary(env, err)); 279 | } 280 | 281 | inline ERL_NIF_TERM make_error(ErlNifEnv* env, ERL_NIF_TERM err) 282 | { 283 | return enif_make_tuple2(env, ATOM_ERROR, err); 284 | } 285 | 286 | template 287 | inline bool parse_if(ErlNifEnv*, ERL_NIF_TERM match_term, const ERL_NIF_TERM kv[], T& val); 288 | 289 | template <> 290 | inline bool parse_if(ErlNifEnv* env, ERL_NIF_TERM match_term, const ERL_NIF_TERM kv[], std::string& val) 291 | { 292 | ErlNifBinary bin; 293 | auto res = enif_is_identical(kv[0], match_term) && enif_inspect_binary(env, kv[1], &bin); 294 | if (res) val = bin_to_str(bin); 295 | return res; 296 | } 297 | 298 | template 299 | requires std::is_integral::value 300 | bool parse_if(ErlNifEnv* env, ERL_NIF_TERM match_term, const ERL_NIF_TERM kv[], T& val) 301 | { 302 | long v; 303 | auto res = enif_is_identical(kv[0], match_term) && enif_get_long(env, kv[1], &v); 304 | if (res) val = T(v); 305 | return res; 306 | } 307 | 308 | bool parse_atom_if(ErlNifEnv* env, ERL_NIF_TERM match_term, const ERL_NIF_TERM kv[], ERL_NIF_TERM& val) 309 | { 310 | auto res = enif_is_identical(kv[0], match_term) && enif_is_atom(env, kv[1]); 311 | if (res) val = kv[1]; 312 | return res; 313 | } 314 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info]}. 2 | 3 | {pre_hooks, [{compile, "make -C c_src"}]}. 4 | {post_hooks, [{clean, "make -C c_src clean"}]}. 5 | 6 | {hex, [{doc, ex_doc}]}. 7 | 8 | {plugins, [rebar3_hex, rebar3_ex_doc]}. 9 | 10 | {eunit, [verbose]}. 11 | 12 | {ex_doc, [ 13 | {extras, [ 14 | {"README.md", #{title => "Overview"}}, 15 | {"LICENSE", #{title => "License"}} 16 | ]}, 17 | {main, "README.md"}, 18 | {source_url, "https://github.com/saleyn/egit"} 19 | ]}. 20 | 21 | 22 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /src/egit.app.src: -------------------------------------------------------------------------------- 1 | {application,egit, 2 | [{description,"Native git Erlang interface library"}, 3 | {vsn,"0.1.9"}, 4 | {registered,[]}, 5 | {modules,[git]}, 6 | {env,[]}, 7 | {applications,[kernel,stdlib]}, 8 | {exclude_paths,["c_src/fmt","priv/git.so","c_src/egit.o"]}, 9 | {licenses,["Apache 2.0"]}, 10 | {links,[{"GitHub","https://github.com/saleyn/egit"}]}]}. 11 | -------------------------------------------------------------------------------- /src/git.erl: -------------------------------------------------------------------------------- 1 | -module(git). 2 | -export([init/1, init/2, clone/2, open/1, fetch/1, fetch/2, 3 | pull/1, pull/2, push/1, push/2, push/3, commit_lookup/3]). 4 | -export([cat_file/2, cat_file/3, checkout/2, checkout/3]). 5 | -export([add_all/1, add/2, add/3, commit/2, 6 | rev_parse/2, rev_parse/3, rev_list/3]). 7 | -export([config_get/2, config_set/3]). 8 | -export([branch_create/2, branch_create/3]). 9 | -export([branch_rename/3, branch_rename/4, branch_delete/2]). 10 | -export([list_branches/1, list_branches/2, list_index/1, list_index/2]). 11 | -export([list_remotes/1, remote_add/3, remote_rename/3, 12 | remote_delete/2, remote_set_url/3, remote_set_url/4]). 13 | -export([tag_create/2, tag_create/3, tag_create/4, tag_delete/2]). 14 | -export([list_tags/1, list_tags/2]). 15 | -export([status/1, status/2, reset/2]). 16 | 17 | -on_load(on_load/0). 18 | 19 | -type repository() :: reference(). 20 | -type commit_opt() :: 21 | encoding | 22 | message | 23 | summary | 24 | time | 25 | time_offset | 26 | committer | 27 | author | 28 | header | 29 | tree_id. 30 | -type commit_opts() :: [commit_opt()]. 31 | -type cat_file_opt() :: type | size | {abbrev, pos_integer()}. 32 | -type cat_file_opts() :: [cat_file_opt()]. 33 | -type checkout_opt() :: force | verbose | perf. 34 | -type checkout_opts() :: [checkout_opt()]. 35 | -type checkout_stats() :: #{ 36 | chmod_calls => integer(), 37 | mkdir_calls => integer(), 38 | stat_calls => integer(), 39 | total_steps => integer() 40 | }. 41 | 42 | -type add_opt() :: verbose | dry_run | update | force. 43 | -type add_opts() :: [add_opt()]. 44 | -type add_result() :: nil | 45 | #{mode => dry_run | added, files => [binary()]} | 46 | {error, term()}. 47 | -type rev_list_opt() :: [topo_order | date_order | reverse | 48 | {limit, pos_integer()} | {abbrev, pos_integer()}]. 49 | -type rev_list_opts() :: [rev_list_opt()]. 50 | 51 | -type rev_parse_opt() :: {abbrev, pos_integer()}. 52 | -type rev_parse_opts() :: [rev_parse_opt()]. 53 | 54 | -type list_branch_opt() :: local | remote | all | fullname | {limit,pos_integer()}. 55 | %% List branch option. 56 | %%
57 | %%
local
Return only local branches
58 | %%
remote
Return only remote branches
59 | %%
all
Return all branches (default)
60 | %%
fullname
Return full branch names
61 | %%
{limit, Limit}
Return up to this number of branches
62 | %%
63 | 64 | -type list_branch_opts() :: [list_branch_opt()]. 65 | 66 | -type list_index_opt() :: {abbrev, pos_integer()} | 67 | {fields, all | [path|stage|conflict|oid|mode|size|ctime|mtime]}. 68 | %% List index option. 69 | %%
70 | %%
{abbrev, `NumChars'}
71 | %%
NumChars truncates the commit hash (must be less then 40).
72 | %%
{fields, `ListOfFields'}
73 | %%
Field list to return. If not specified, the option will default 74 | %% to `[path]'.
75 | %%
76 | 77 | -type list_index_opts() :: [list_index_opt()]. 78 | 79 | -type list_index_entry() :: #{ 80 | path => binary(), stage => [normal|ancestor|ours|theirs], 81 | conflict => boolean(), oid => binary(), 82 | mode => pos_integer(), size => non_neg_integer(), 83 | ctime => pos_integer(), mtime => pos_integer() 84 | }. 85 | 86 | -type cfg_source() :: repository() | default | system | xdg | global | local | app | highest. 87 | %% Configuration source. 88 | %% If the value is an atom, then: 89 | %%
90 | %%
default
Find default configuration file for this app
91 | %%
system
System-wide configuration file - /etc/gitconfig on Linux systems
92 | %%
xdg
XDG compatible configuration file, typically ~/.config/git/config
93 | %%
global
User-specific global configuration file, typically ~/.gitconfig
94 | %%
local
Repository specific configuration file; $WORK_DIR/.git/config on non-bare repos
95 | %%
app
Application specific configuration file; freely defined by applications
96 | %%
highest
The most specific config file available for the app
97 | %%
98 | 99 | -type branch_create_opts() :: [overwrite | {target, binary()}]. 100 | %% Branch creation options 101 | %%
102 | %%
overwrite
Force to overwrite the existing branch
103 | %%
{target, Commit}
Use the target commit (default `<<"HEAD">>')
104 | %%
105 | 106 | -type tag_opt() :: [{message, binary()} | {pattern, binary()} | {target, binary()} | {lines, integer()}]. 107 | %% Tag creation options 108 | %%
109 | %%
{message, `Msg'}
Message associated with the tag's commit
110 | %%
{pattern, `Pat'}
Pattern to search matching tags
111 | %%
{target, `SHA'}
Target commit SHA to be associated with the tag
112 | %%
{lines, `Num'}
Number of lines in the commit to store
113 | %%
114 | 115 | -type tag_opts() :: [tag_opt()]. 116 | 117 | -type status_opt() :: 118 | {untracked, none|normal|recursive} | {paths, [binary()]} | 119 | branch | ignored | submodules | ignore_submodules. 120 | %% Status function options 121 | %%
122 | %%
{untracked, `Untracked'}
123 | %% `Untracked' can be one of: 124 | %%
125 | %% 126 | %%
  • `none' - don't include untracked files
  • 127 | %%
  • `normal' - include untracked files
  • 128 | %%
  • `recursive' - include untracked files and recurse into untracked directories
  • 129 | %%
    130 | %%
    131 | %%
    {paths, `Paths'}
    `Path' is an array of path patterns to match
    132 | %%
    branch
    Include branch name
    133 | %%
    ignored
    Include ignored files
    134 | %%
    ignore_submodules
    Indicates that submodules should be skipped
    135 | %%
    submodules
    Include submodules (overrides `ignore_submodules')
    136 | %%
    137 | 138 | -type status_opts() :: [status_opt()]. 139 | 140 | -export_type([repository/0, commit_opts/0, cat_file_opt/0, checkout_opts/0, checkout_stats/0]). 141 | -export_type([rev_parse_opts/0, rev_list_opts/0, tag_opts/0, status_opts/0]). 142 | -export_type([list_index_opts/0, list_index_entry/0]). 143 | 144 | -define(LIBNAME, ?MODULE). 145 | -define(NOT_LOADED_ERROR, 146 | erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]})). 147 | 148 | -ifdef(TEST). 149 | -include_lib("eunit/include/eunit.hrl"). 150 | -endif. 151 | 152 | on_load() -> 153 | SoName = 154 | case code:priv_dir(?LIBNAME) of 155 | {error, bad_name} -> 156 | case code:which(?MODULE) of 157 | Filename when is_list(Filename) -> 158 | Dir = filename:dirname(filename:dirname(Filename)), 159 | filename:join([Dir, "priv", ?LIBNAME]); 160 | _ -> 161 | filename:join("../priv", ?LIBNAME) 162 | end; 163 | Dir -> 164 | filename:join(Dir, ?LIBNAME) 165 | end, 166 | erlang:load_nif(SoName, []). 167 | 168 | %% @doc Init a repository. 169 | %% @see init/2 170 | -spec init(binary()|string()) -> repository(). 171 | init(Path) -> init(Path, []). 172 | 173 | %% @doc Init a repository. 174 | %% If `Opts' list contains `bare', a Git repository without a working 175 | %% directory is created at the pointed path. 176 | %% Otherwise, the provided path will be considered as the working 177 | %% directory into which the .git directory will be created. 178 | -spec init(binary()|string(), [bare]) -> repository(). 179 | init(Path, Opts) -> 180 | init_nif(to_bin(Path), Opts). 181 | 182 | %% @doc Clone a remote repository to the local path 183 | -spec clone(binary()|string(), binary()|string()) -> repository(). 184 | clone(URL, Path) -> clone_nif(to_bin(URL), to_bin(Path)). 185 | 186 | %% @doc Open a local git repository 187 | -spec open(binary()|string()) -> repository(). 188 | open(Path) -> open_nif(to_bin(Path)). 189 | 190 | %% @doc Fetch from origin 191 | -spec fetch(repository()) -> ok | {error, binary()}. 192 | fetch(Repo) -> fetch_nif(Repo, fetch). 193 | 194 | %% @doc Fetch from given remote (e.g. `<<"origin">>') 195 | -spec fetch(repository(), binary()|string()) -> ok | {error, binary()}. 196 | fetch(Repo, Remote) -> fetch_nif(Repo, fetch, to_bin(Remote)). 197 | 198 | %% @doc Pull from origin 199 | -spec pull(repository()) -> ok | {error, binary()}. 200 | pull(Repo) -> fetch_nif(Repo, pull). 201 | 202 | %% @doc Pull from given remote (e.g. `<<"origin">>') 203 | -spec pull(repository(), binary()|string()) -> ok | {error, binary()}. 204 | pull(Repo, Remote) -> fetch_nif(Repo, pull, to_bin(Remote)). 205 | 206 | %% @doc Push changes to remote (`"origin"') 207 | -spec push(repository()) -> ok | {error, binary()}. 208 | push(Repo) -> push_nif(Repo, <<"origin">>, []). 209 | 210 | %% @doc Push to given remote 211 | -spec push(repository(), binary()|string()) -> ok | {error, binary()}. 212 | push(Repo, Remote) -> push_nif(Repo, to_bin(Remote), []). 213 | 214 | %% @doc Push refs to given remote 215 | -spec push(repository(), binary()|string(), [binary()|string()]) -> 216 | ok | {error, binary()}. 217 | push(Repo, Remote, Refs) when is_list(Refs) -> 218 | push_nif(Repo, to_bin(Remote), [to_bin(M) || M <- Refs]). 219 | 220 | %% @doc Provide content or type and size information for repository objects. 221 | -spec cat_file(repository(), binary()|string()) -> {ok, term()} | {error, term()}. 222 | cat_file(Repo, Rev) -> 223 | cat_file(Repo, Rev, []). 224 | 225 | %% @doc Provide content or type and size information for repository objects. 226 | %% Example: 227 | %% ``` 228 | %% 1> R = git:open("."). 229 | %% 2> git:cat_file(R, "main", [{abbrev, 5}]). 230 | %% #{type => commit, 231 | %% author => {<<"John Doh">>,<<"test@gmail.com">>,1686195121, -14400}, 232 | %% oid => <<"b85d0">>, 233 | %% parents => [<<"1fd4b">>]} 234 | %% 7> git:cat_file(R, "b85d0", [{abbrev, 5}]). 235 | %% #{type => tree, 236 | %% commits => 237 | %% [{<<".github">>,<<"tree">>,<<"1e41f">>,16384}, 238 | %% {<<".gitignore">>,<<"blob">>,<<"b893a">>,33188}, 239 | %% {<<".gitmodules">>,<<"blob">>,<<"2550a">>,33188}, 240 | %% {<<".vscode">>,<<"tree">>,<<"c7b1b">>,16384}, 241 | %% {<<"LICENSE">>,<<"blob">>,<<"d6456">>,33188}, 242 | %% {<<"Makefile">>,<<"blob">>,<<"2d635">>,33188}, 243 | %% {<<"README.md">>,<<"blob">>,<<"7b3d0">>,33188}, 244 | %% {<<"c_src">>,<<"tree">>,<<"147f3">>,16384}, 245 | %% {<<"rebar.config">>,<<"blob">>,<<"1f68a">>,33188}, 246 | %% {<<"rebar.lock">>,<<"blob">>,<<"57afc">>,33188}, 247 | %% {<<"src">>,<<"tree">>,<<"1bccb">>,16384}]} 248 | %% 8> git:cat_file(R, "b893a", [{abbrev, 5}]). 249 | %% #{type => blob, 250 | %% blob => <<"*.swp\n*.dump\n/c_src/*.o\n/c_src/fmt\n/priv/*.so\n/_build\n/doc\n">>} 251 | %% ''' 252 | -spec cat_file(repository(), binary()|string(), cat_file_opts()) -> 253 | {ok, term()} | {error, term()}. 254 | cat_file(Repo, Rev, Opts) -> 255 | cat_file_nif(Repo, to_bin(Rev), Opts). 256 | 257 | %% @doc Same as `checkout(Repo, Revision, [])'. 258 | -spec checkout(repository(), binary()|string()) -> ok | {error, term()}. 259 | checkout(Repo, Rev) -> checkout_nif(Repo, to_bin(Rev), []). 260 | 261 | %% @doc Provide content or type and size information for repository objects. 262 | %% If `Opts' contains `verbose' (and optionally `perf'), then the return is a 263 | %% map with checkout stats. 264 | -spec checkout(repository(), binary(), checkout_opts()) -> ok | checkout_stats() | {error, term()}. 265 | checkout(Repo, Revision, Opts) -> 266 | checkout_nif(Repo, to_bin(Revision), Opts). 267 | 268 | %% @doc Add all pending changes 269 | -spec add_all(repository()) -> add_result(). 270 | add_all(Repo) when is_reference(Repo) -> 271 | add_nif(Repo, [<<".">>], []). 272 | 273 | %% @doc Add files matching `PathSpecs' to index 274 | %% @see add/3 275 | -spec add(repository(), binary()|string()|[binary()|string()]) -> add_result(). 276 | add(Repo, [C|_] = PathSpec) when is_integer(C), C >= 32, C < 256 -> 277 | add_nif(Repo, [to_bin(PathSpec)], []); 278 | add(Repo, PathSpecs) when is_list(PathSpecs) -> 279 | add(Repo, PathSpecs, []); 280 | add(Repo, PathSpec) when is_binary(PathSpec) -> 281 | add_nif(Repo, [PathSpec], []). 282 | 283 | %% @doc Add files matching `PathSpecs' to index with options 284 | -spec add(repository(), [binary()|string()], add_opts()) -> add_result(). 285 | add(Repo, [C|_] = PathSpecs, Opts) when is_integer(C), C >= 32, C < 256 -> 286 | add_nif(Repo, [to_bin(PathSpecs)], Opts); 287 | add(Repo, PathSpec, Opts) when is_binary(PathSpec)-> 288 | add_nif(Repo, [PathSpec], Opts); 289 | add(Repo, PathSpecs, Opts) when is_list(PathSpecs)-> 290 | add_nif(Repo, [to_bin(B) || B <- PathSpecs], Opts). 291 | 292 | %% @doc Commit changes to a repository 293 | -spec commit(repository(), binary()|string()) -> 294 | {ok, OID::binary()} | {error, binary()|atom()}. 295 | commit(_Repo, Comment) -> 296 | commit_nif(_Repo, to_bin(Comment)). 297 | 298 | %% @doc Reverse parse a reference. 299 | %% See [https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions] 300 | %% for the formats of a `Spec'. 301 | %% 302 | %% Opts is a list of: 303 | %%
    304 | %%
    {abbrev, `NumChars'}
    305 | %%
    NumChars truncates the commit hash (must be less then 40)
    306 | %%
    307 | %% 308 | %% When a reference refers to a single object, an ok tuple with a binary 309 | %% string of the commit hash is returned. When it refers to a range 310 | %% (e.g. `HEAD..HEAD~2`), a map is returned with `from' and `to' keys. 311 | %% When using a Symmetric Difference Notation `...' (i.e. `HEAD...HEAD~4'), 312 | %% a map with three keys `from', `to', and `merge_base' is returned. 313 | %% 314 | %% Examples: 315 | %% ``` 316 | %% 2> git:rev_parse(R,<<"HEAD~4">>, [{abbrev, 7}]). 317 | %% {ok,<<"6d6f662">>} 318 | %% 3> git:rev_parse(R,<<"HEAD..HEAD~4">>, [{abbrev, 7}]). 319 | %% git:rev_parse(R,<<"HEAD..HEAD~4">>, [{abbrev, 7}]). 320 | %% #{from => <<"f791f01">>,to => <<"6d6f662">>} 321 | %% 4> git:rev_parse(R,<<"HEAD...HEAD~4">>). 322 | %% git:rev_parse(R,<<"HEAD...HEAD~4">>, [{abbrev, 7}]). 323 | %% #{from => <<"f791f01">>,merge_base => <<"6d6f662">>, to => <<"6d6f662">>} 324 | %% ''' 325 | -spec rev_parse(repository(), binary()|string(), rev_parse_opts()) -> 326 | {ok, binary()} | map() | {error, binary()|atom()}. 327 | rev_parse(Repo, Spec, Opts) -> 328 | rev_parse_nif(Repo, to_bin(Spec), Opts). 329 | 330 | %% @doc Same as `rev_parse(Repo, Spec, [])'. 331 | -spec rev_parse(repository(), binary()|string()) -> 332 | {ok, binary()} | map() | {error, binary()|atom()}. 333 | rev_parse(Repo, Spec) -> 334 | rev_parse(Repo, Spec, []). 335 | 336 | %% @doc Return the list of OIDs for the given specs. 337 | %% 338 | %% Opts is a list of: 339 | %%
    340 | %%
    topo_order | date_order | reverse
    341 | %%
    Control sorting order
    342 | %%
    {limit, `Limit'}
    343 | %%
    Limit is an integer that limits the number of refs returned
    344 | %%
    {abbrev, `NumChars'}
    345 | %%
    NumChars truncates the commit hash (must be less then 40)
    346 | %%
    347 | %% 348 | %% Example: 349 | %% ``` 350 | %% 9> git:rev_list(R, ["HEAD"], [{limit, 4}, {abbrev, 7}]). 351 | %% [<<"f791f01">>,<<"1b74c46">>,<<"c40374d">>,<<"12968bd">>] 352 | %% ''' 353 | -spec rev_list(repository(), ['not'|'Elixir.Not'|string()|binary()]|binary(), rev_list_opts()) -> 354 | #{commit_opt() => term()}. 355 | rev_list(Repo, Specs, Opts) when is_list(Specs); is_binary(Specs) -> 356 | F = fun 357 | ('not') -> 'not'; 358 | ('Elixir.Not') -> 'not'; 359 | (I) when is_list(I) -> list_to_binary(I); 360 | (I) when is_binary(I) -> I 361 | end, 362 | L = [F(I) || I <- 363 | if is_binary(Specs) -> [Specs]; 364 | true -> Specs 365 | end], 366 | rev_list_nif(Repo, L, Opts). 367 | 368 | %% @doc Lookup commit details identified by OID 369 | -spec commit_lookup(repository(), binary()|string(), [commit_opt()]) -> 370 | #{commit_opt() => term()}. 371 | commit_lookup(Repo, OID, Opts) -> 372 | commit_lookup_nif(Repo, to_bin(OID), Opts). 373 | 374 | %% @doc Get git configuration value 375 | %% Example: 376 | %% ``` 377 | %% 1> R = git:clone(<<"https://github.com/saleyn/egit.git">>, "/tmp/egit"). 378 | %% #Ref<0.170091758.2335834136.12133> 379 | %% 2> git:config_get(R, "user.name"). 380 | %% {ok,<<"John Doh">>} 381 | %% ''' 382 | -spec config_get(cfg_source(), binary()|string()) -> 383 | {ok, binary()} | {error, binary()|atom()}. 384 | config_get(Src, Key) -> 385 | config_get_nif(Src, to_bin(Key)). 386 | 387 | %% @doc Set git configuration value 388 | %% Example: 389 | %% ``` 390 | %% 1> R = git:clone(<<"https://github.com/saleyn/egit.git">>, "/tmp/egit"). 391 | %% #Ref<0.170091758.2335834136.12133> 392 | %% 2> git:config_set(R, "user.name", "Test User"). 393 | %% ok 394 | %% ''' 395 | -spec config_set(cfg_source(), binary()|string(), binary()|string()) -> 396 | ok | {error, binary()|atom()}. 397 | config_set(Src, Key, Val) -> 398 | config_set_nif(Src, to_bin(Key), to_bin(Val)). 399 | 400 | %% @doc Create a branch 401 | %% @see git:branch_create/3 402 | branch_create(Repo, Name) -> 403 | branch_create(Repo, Name, []). 404 | 405 | %% @doc Create a branch 406 | %% Example: 407 | %% ``` 408 | %% 1> R = git:clone(<<"https://github.com/saleyn/egit.git">>, "/tmp/egit"). 409 | %% #Ref<0.170091758.2335834136.12133> 410 | %% 2> git:branch_create(R, "tmp"). 411 | %% ok 412 | %% ''' 413 | -spec branch_create(repository(), binary()|string(), branch_create_opts()) -> 414 | ok | {error, binary()}. 415 | branch_create(Repo, Name, Opts) when is_list(Opts) -> 416 | branch_nif(Repo, create, to_bin(Name), Opts). 417 | 418 | %% @doc Rename a branch 419 | %% @see branch_rename/4 420 | branch_rename(Repo, OldName, NewName) -> 421 | branch_rename(Repo, OldName, NewName, []). 422 | 423 | %% @doc Rename a branch 424 | -spec branch_rename(repository(), binary()|string(), binary()|string(), [overwrite]) -> 425 | ok | {error, binary()}. 426 | branch_rename(Repo, OldName, NewName, Opts) when is_list(Opts) -> 427 | branch_nif(Repo, rename, to_bin(OldName), [{new_name, to_bin(NewName)} | Opts]). 428 | 429 | %% @doc Delete a branch 430 | -spec branch_delete(repository(), binary()|string()) -> 431 | ok | {error, binary()}. 432 | branch_delete(Repo, Name) -> 433 | branch_nif(Repo, delete, to_bin(Name)). 434 | 435 | %% @doc List branches 436 | -spec list_branches(repository(), list_branch_opts()) -> [{local|remote, binary()}]. 437 | list_branches(Repo, Opts) when is_reference(Repo), is_list(Opts) -> 438 | ?NOT_LOADED_ERROR. 439 | 440 | %% @doc Add a remote 441 | -spec remote_add(repository(), binary()|string(), binary()|string()) -> 442 | ok | {error, binary()}. 443 | remote_add(Repo, Name, URL) -> 444 | remote_nif(Repo, {add, URL}, to_bin(Name), []). 445 | 446 | %% @doc Rename a remote 447 | -spec remote_rename(repository(), binary()|string(), binary()|string()) -> 448 | ok | {error, binary()}. 449 | remote_rename(Repo, OldName, NewName) -> 450 | remote_nif(Repo, {rename, to_bin(NewName)}, to_bin(OldName), []). 451 | 452 | %% @doc Delete a remote 453 | -spec remote_delete(repository(), binary()|string()) -> 454 | ok | {error, binary()}. 455 | remote_delete(Repo, Name) -> 456 | remote_nif(Repo, delete, to_bin(Name), []). 457 | 458 | %% @doc Delete a remote 459 | -spec remote_set_url(repository(), binary()|string(), binary()|string()) -> 460 | ok | {error, binary()}. 461 | remote_set_url(Repo, Name, URL) -> 462 | remote_set_url(Repo, Name, URL, []). 463 | 464 | %% @doc Add a remote. 465 | %% If `Opts' contains `push', then the repository is pushed to the remote `URL'. 466 | remote_set_url(Repo, Name, URL, Opts) -> 467 | remote_nif(Repo, {seturl, to_bin(URL)}, to_bin(Name), Opts). 468 | 469 | %% @doc List remotes 470 | -spec list_remotes(repository()) -> [{binary(), binary()}]. 471 | list_remotes(Repo) when is_reference(Repo) -> 472 | ?NOT_LOADED_ERROR. 473 | 474 | %% @doc List branches 475 | %% @see list_branches/2 476 | list_branches(Repo) -> 477 | list_branches(Repo, []). 478 | 479 | %% @doc List index 480 | %% @see list_index/2 481 | list_index(Repo) -> 482 | list_index(Repo, []). 483 | 484 | %% @doc List index. 485 | -spec list_index(repository(), list_index_opts()) -> [list_index_entry()]. 486 | list_index(Repo, Opts) when is_reference(Repo), is_list(Opts) -> 487 | ?NOT_LOADED_ERROR. 488 | 489 | %% @doc Create a tag 490 | -spec tag_create(repository(), string()|binary()) -> 491 | ok | {error, binary()|atom()}. 492 | tag_create(Repo, Tag) -> 493 | tag_create(Repo, Tag, nil, []). 494 | 495 | %% @doc Create a tag 496 | -spec tag_create(repository(), string()|binary(), nil|string()|binary()) -> 497 | ok | {error, binary()|atom()}. 498 | tag_create(Repo, Tag, Msg) -> 499 | tag_create(Repo, Tag, Msg, []). 500 | 501 | %% @doc Create a tag 502 | -spec tag_create(repository(), string()|binary(), nil|string()|binary(), tag_opts()) -> 503 | ok | {error, binary()|atom()}. 504 | tag_create(Repo, Tag, Msg, Opts) when Msg==nil; Msg==undefined -> 505 | tag_nif(Repo, create, to_bin(Tag), Opts); 506 | tag_create(Repo, Tag, Msg, Opts0) when is_list(Msg); is_binary(Msg) -> 507 | Opts = [case X of 508 | {I, M} when is_list(M) -> {I, to_bin(M)}; 509 | {_, _} -> X; 510 | I when is_atom(I) -> X 511 | end || X <- [{message, Msg} | Opts0]], 512 | tag_nif(Repo, create, to_bin(Tag), Opts). 513 | 514 | %% @doc Delete a tag 515 | -spec tag_delete(repository(), string()|binary()) -> ok | {error, binary()|atom()}. 516 | tag_delete(Repo, Tag) -> 517 | tag_nif(Repo, delete, to_bin(Tag), []). 518 | 519 | %% @doc List all tags 520 | -spec list_tags(repository()) -> 521 | [binary()|{binary(), binary()}] | {error, binary()|atom()}. 522 | list_tags(Repo) -> 523 | tag_nif(Repo, list, <<"">>, []). 524 | 525 | %% @doc List all tags 526 | -spec list_tags(repository(), string()|binary()) -> 527 | [binary()|{binary(), binary()}] | {error, binary()|atom()}. 528 | list_tags(Repo, Pattern) -> 529 | tag_nif(Repo, list, <<"">>, [{pattern, to_bin(Pattern)}]). 530 | 531 | %% @doc Get repository status 532 | -spec status(repository()) -> map() | {error, term()}. 533 | status(Repo) -> 534 | status(Repo, []). 535 | 536 | %% @doc Get repository status 537 | -spec status(repository(), status_opts()) -> map() | {error, term()}. 538 | status(Repo, Opts) -> 539 | status_nif(Repo, Opts). 540 | 541 | %% @doc Get repository reset. 542 | %% The reset `Type' is one of: 543 | %%
    544 | %%
    soft
    The HEAD will be moved to the commit
    545 | %%
    mixed
    546 | %%
    Do a SOFT reset, plus the index will be replaced with the content of 547 | %% the commit tree
    548 | %%
    hard
    549 | %%
    Do a MIXED reset and the working directory will be replaced with the 550 | %% content of the index. Untracked and ignored files will be left alone. 551 | %%
    552 | %%
    553 | -spec reset(repository(), soft|mixed|hard, string()|binary()) -> ok | {error, term()}. 554 | reset(Repo, Type, Ref) -> 555 | reset_nif(Repo, Type, to_bin(Ref)). 556 | 557 | -spec reset(repository(), soft|mixed|hard) -> ok | {error, term()}. 558 | reset(Repo, Type) -> 559 | reset(Repo, Type, "HEAD"). 560 | 561 | %%----------------------------------------------------------------------------- 562 | %% Internal functions 563 | %%----------------------------------------------------------------------------- 564 | 565 | to_bin(B) when is_binary(B) -> B; 566 | to_bin(B) when is_list(B) -> list_to_binary(B). 567 | 568 | init_nif(Path, Opts) when is_binary(Path), is_list(Opts) -> 569 | ?NOT_LOADED_ERROR. 570 | 571 | clone_nif(URL, Path) when is_binary(URL), is_binary(Path) -> 572 | ?NOT_LOADED_ERROR. 573 | 574 | open_nif(Path) when is_binary(Path) -> 575 | ?NOT_LOADED_ERROR. 576 | 577 | fetch_nif(Repo, _Op) when is_reference(Repo) -> 578 | ?NOT_LOADED_ERROR. 579 | 580 | fetch_nif(Repo, _Op, Remote) when is_reference(Repo), is_binary(Remote) -> 581 | ?NOT_LOADED_ERROR. 582 | 583 | push_nif(Repo, Remote, Refs) when is_reference(Repo), is_binary(Remote), is_list(Refs) -> 584 | ?NOT_LOADED_ERROR. 585 | 586 | add_nif(Repo, PathSpecs, Opts) when is_reference(Repo), is_list(PathSpecs), is_list(Opts) -> 587 | ?NOT_LOADED_ERROR. 588 | 589 | cat_file_nif(Repo, Rev, Opts) when is_reference(Repo), is_binary(Rev), is_list(Opts) -> 590 | ?NOT_LOADED_ERROR. 591 | 592 | checkout_nif(Repo, Revision, Opts) when is_reference(Repo), is_binary(Revision), is_list(Opts) -> 593 | ?NOT_LOADED_ERROR. 594 | 595 | commit_nif(Repo, Comment) when is_reference(Repo), is_binary(Comment) -> 596 | ?NOT_LOADED_ERROR. 597 | 598 | commit_lookup_nif(Repo, OID, Opts) when is_reference(Repo), is_binary(OID), is_list(Opts) -> 599 | ?NOT_LOADED_ERROR. 600 | 601 | rev_parse_nif(Repo, Spec, Opts) when is_reference(Repo), is_binary(Spec), is_list(Opts) -> 602 | ?NOT_LOADED_ERROR. 603 | 604 | rev_list_nif(Repo, Specs, Opts) when is_reference(Repo), is_list(Specs), is_list(Opts) -> 605 | ?NOT_LOADED_ERROR. 606 | 607 | config_get_nif(Src, Key) when is_reference(Src) orelse is_atom(Src), is_binary(Key) -> 608 | ?NOT_LOADED_ERROR. 609 | 610 | config_set_nif(Src, Key, Val) when is_reference(Src) orelse is_atom(Src), is_binary(Key), is_binary(Val) -> 611 | ?NOT_LOADED_ERROR. 612 | 613 | remote_nif(Repo, _Op, Name, Opts) when is_reference(Repo), is_binary(Name), is_list(Opts) -> 614 | ?NOT_LOADED_ERROR. 615 | 616 | branch_nif(Repo, Op, Name) when is_reference(Repo), is_atom(Op), is_binary(Name) -> 617 | ?NOT_LOADED_ERROR. 618 | 619 | branch_nif(Repo, Op, OldName, NewNameOrOpt) 620 | when is_reference(Repo), is_atom(Op) 621 | , is_binary(OldName), is_binary(NewNameOrOpt) orelse is_list(NewNameOrOpt) -> 622 | ?NOT_LOADED_ERROR. 623 | 624 | tag_nif(Repo, Op, Tag, Opts) when is_reference(Repo), is_atom(Op), is_binary(Tag), is_list(Opts) -> 625 | ?NOT_LOADED_ERROR. 626 | 627 | status_nif(Repo, Opts) when is_reference(Repo), is_list(Opts) -> 628 | ?NOT_LOADED_ERROR. 629 | 630 | reset_nif(Repo, Type, Ref) when is_reference(Repo), is_atom(Type), is_binary(Ref) -> 631 | ?NOT_LOADED_ERROR. 632 | 633 | -ifdef(EUNIT). 634 | 635 | init_test_() -> 636 | file:del_dir_r("/tmp/egit_repo"), 637 | [ 638 | ?_assertMatch(B when is_reference(B), git:init("/tmp/egit_repo")), 639 | ?_assert(filelib:is_dir("/tmp/egit_repo/.git")), 640 | ?_assertEqual(ok, file:del_dir_r("/tmp/egit_repo")), 641 | ?_assertMatch(B when is_reference(B), git:init("/tmp/egit_repo", [bare])), 642 | ?_assertNot(filelib:is_dir("/tmp/egit_repo/.git")), 643 | ?_assert(filelib:is_regular("/tmp/egit_repo/HEAD")), 644 | ?_assertEqual(ok, file:del_dir_r("/tmp/egit_repo")) 645 | ]. 646 | 647 | clone_test_() -> 648 | file:del_dir_r("/tmp/egit"), 649 | R = git:clone(<<"https://github.com/saleyn/egit.git">>, <<"/tmp/egit">>), 650 | [ 651 | ?_assert(is_reference(R)), 652 | ?_assertMatch({ok, _}, git:rev_parse(R, <<"HEAD">>)) 653 | ]. 654 | 655 | fetch_test_() -> 656 | R = git:open(<<"/tmp/egit">>), 657 | [ 658 | ?_assert(is_reference(R)), 659 | ?_assertEqual(ok, git:fetch(R)) 660 | ]. 661 | 662 | pull_test_() -> 663 | R = git:open("/tmp/egit"), 664 | [ 665 | ?_assert(is_reference(R)), 666 | ?_assertEqual(ok, git:fetch(R)) 667 | ]. 668 | 669 | checkout_test_() -> 670 | R = git:open("/tmp/egit"), 671 | [ 672 | ?_assert(is_reference(R)), 673 | ?_assertEqual(ok, git:checkout(R, <<"main">>)) 674 | ]. 675 | 676 | commit_test_() -> 677 | R = git:open("/tmp/egit"), 678 | {ok, OID0} = git:rev_parse(R, "HEAD"), 679 | [ 680 | fun() -> 681 | ?assert(is_reference(R)), 682 | ?assertEqual([], os:cmd("echo \"\n\" >> /tmp/egit/README.md")), 683 | ?assertEqual( 684 | #{mode => dry_run, files => [<<"README.md">>]}, 685 | git:add(R, ".", [dry_run])), 686 | ?assertEqual( 687 | #{mode => added, files => [<<"README.md">>]}, 688 | git:add(R, ["."])), 689 | ?assertEqual(nil, git:add(R, ".")), 690 | {ok, OID0} = git:rev_parse(R, "HEAD"), 691 | Res = git:commit(R, "Test commit"), 692 | ?assertMatch({ok, _}, Res), 693 | {ok, OID} = Res, 694 | ?assertEqual({ok, nil}, git:commit(R, "Test commit")), 695 | ?assertEqual({ok, OID}, git:rev_parse(R, "HEAD")) 696 | end 697 | ]. 698 | 699 | rev_parse_test_() -> 700 | R = git:open("/tmp/egit"), 701 | {ok, OID0} = git:rev_parse(R, "HEAD"), 702 | [ 703 | ?_assertMatch(#{to := _, from := OID0}, git:rev_parse(R, <<"HEAD..HEAD~1">>)), 704 | ?_assertMatch(#{to := OID, from := OID0, merge_base := OID}, git:rev_parse(R, <<"HEAD...HEAD~1">>)), 705 | ?_assertMatch({error, _}, git:rev_parse(R, <<"HEAD~x">>)), 706 | fun() -> 707 | case git:rev_list(R, ["HEAD"], [{limit, 3}, {abbrev, 7}]) of 708 | [A,B,C] when is_binary(A), is_binary(B), is_binary(C), 709 | byte_size(A)==7, byte_size(B)==7, byte_size(C)==7 -> 710 | ok; 711 | _Res -> 712 | ?assert(false) 713 | end 714 | end 715 | ]. 716 | 717 | cat_file_test_() -> 718 | R = git:open("/tmp/egit"), 719 | [ 720 | ?_assertMatch( 721 | (#{type := commit, 722 | author := {User, _, Time, Offset}, 723 | oid := OID, 724 | parents := [OID1]}) 725 | when is_binary(User) andalso is_integer(Time) andalso is_integer(Offset) 726 | andalso is_binary(OID) andalso is_binary(OID1), 727 | git:cat_file(R, <<"main">>, [{abbrev, 5}]) 728 | ), 729 | 730 | ?_assertMatch( 731 | #{type := tree, 732 | commits := 733 | [{<<".github">>,<<"tree">>,_,16384}, 734 | {<<".gitignore">>,<<"blob">>,_,33188}, 735 | {<<".gitmodules">>,<<"blob">>,_,33188}, 736 | {<<".vscode">>,<<"tree">>,_,16384}, 737 | {<<"LICENSE">>,<<"blob">>,_,33188}, 738 | {<<"Makefile">>,<<"blob">>,_,33188}, 739 | {<<"README.md">>,<<"blob">>,_,33188}, 740 | {<<"c_src">>,<<"tree">>,_,16384}, 741 | {<<"rebar.config">>,<<"blob">>,_,33188}, 742 | {<<"rebar.lock">>,<<"blob">>,_,33188}, 743 | {<<"src">>,<<"tree">>,_,16384}]}, 744 | git:cat_file(R, "b85d0", [{abbrev, 5}]) 745 | ), 746 | 747 | ?_assertEqual( 748 | #{type => blob, 749 | data => <<"*.swp\n*.dump\n/c_src/*.o\n/c_src/fmt\n/priv/*.so\n/_build\n/doc\n">>}, 750 | git:cat_file(R, "b893a", [{abbrev, 5}]) 751 | ) 752 | ]. 753 | 754 | config_test_() -> 755 | R = git:open("/tmp/egit"), 756 | [ 757 | ?_assertMatch({ok, _}, git:config_get(R, "user.email")), 758 | ?_assertMatch({ok, _}, git:config_get(highest, "user.email")), 759 | ?_assertMatch({ok, _}, git:config_get(default, "user.email")), 760 | ?_assertMatch({ok, _}, git:config_get(global, "user.email")) 761 | ]. 762 | 763 | branch_test_() -> 764 | R = git:open("/tmp/egit"), 765 | [ 766 | ?_assertMatch(ok, git:branch_create(R, "tmp", [])), 767 | ?_assert(has_branch(R, "tmp")), 768 | ?_assertMatch(ok, git:branch_rename(R, "tmp", "tmp2", [overwrite])), 769 | ?_assert(has_branch(R, "tmp2")), 770 | ?_assertNot(has_branch(R, "tmp")), 771 | ?_assertMatch(ok, git:branch_delete(R, "tmp2")), 772 | ?_assertNot(has_branch(R, "tmp2")) 773 | ]. 774 | 775 | list_index_test_() -> 776 | R = git:open("/tmp/egit"), 777 | L = git:list_index(R), 778 | M = git:list_index(R, [{fields, all}]), 779 | [ 780 | ?_assert(length(L) >= 24), 781 | ?_assertMatch([_], [I || I = #{path := <<"README.md">>} <- git:list_index(R, [{fields, [path]}])]), 782 | ?_assertMatch(M, git:list_index(R, [{fields, [path,stage,conflict,oid,mode,size,ctime,mtime]}])), 783 | ?_assertEqual(length(L), length(M)) 784 | ]. 785 | 786 | remote_test_() -> 787 | R = git:open("/tmp/egit"), 788 | [ 789 | ?_assertEqual([{<<"origin">>,<<"https://github.com/saleyn/egit.git">>,[push,fetch]}], git:list_remotes(R)), 790 | ?_assertMatch( 791 | {error,<<"Could not rename remote: remote 'upstream' does not exist", _/binary>>}, 792 | git:remote_rename(R, "upstream", "upstream2")), 793 | ?_assertEqual( 794 | ok, 795 | git:remote_add(R, "upstream", <<"https://gitlab.com/saleyn/egit.git">>)), 796 | ?_assertMatch( 797 | {error,<<"Could not create remote: remote 'upstream' already exists", _/binary>>}, 798 | git:remote_add(R, "upstream", <<"https://gitlab.com/saleyn/egit.git">>)), 799 | ?_assertEqual(ok, git:remote_set_url(R, "upstream", "https://google.com/saleyn/egit.git")), 800 | ?_assertEqual( 801 | [{<<"origin">>, <<"https://github.com/saleyn/egit.git">>, [push,fetch]}, 802 | {<<"upstream">>,<<"https://google.com/saleyn/egit.git">>, [push,fetch]}], 803 | git:list_remotes(R)), 804 | ?_assertEqual(ok, git:remote_rename(R, "upstream", "upstream2")), 805 | ?_assertEqual(ok, git:remote_delete(R, "upstream2")), 806 | ?_assertEqual([{<<"origin">>,<<"https://github.com/saleyn/egit.git">>,[push,fetch]}], git:list_remotes(R)) 807 | ]. 808 | 809 | tag_test_() -> 810 | R = git:open("/tmp/egit"), 811 | [ 812 | ?_assertEqual(ok, git:tag_create(R, "v0.0.1", "This is a test\n", [{target, "f791f01"}])), 813 | ?_assertEqual(ok, git:tag_create(R, "v0.0.2")), 814 | ?_assertEqual([<<"v0.0.1">>, <<"v0.0.2">>], [T || T <- git:list_tags(R), lists:member(T, [<<"v0.0.1">>, <<"v0.0.2">>])]), 815 | ?_assertEqual(ok, git:tag_delete(R, "v0.0.1")), 816 | ?_assertEqual(ok, git:tag_delete(R, "v0.0.2")), 817 | ?_assertEqual([], [T || T <- git:list_tags(R), lists:member(T, [<<"v0.0.1">>, <<"v0.0.2">>])]) 818 | ]. 819 | 820 | status_test_() -> 821 | R = git:open("/tmp/egit"), 822 | [ 823 | ?_assertEqual(#{}, git:status(R)), 824 | ?_assertEqual(#{branch => <<"main">>}, git:status(R, [branch])), 825 | ?_assertEqual( 826 | #{submodules => [{<<"c_src/fmt">>,<<"https://github.com/fmtlib/fmt.git">>}]}, 827 | git:status(R, [submodules])), 828 | ?_assertEqual(ok, file:write_file(<<"/tmp/egit/test.txt">>, <<"Test\n">>)), 829 | ?_assertEqual(#{}, git:status(R, [{untracked, none}])), 830 | ?_assertEqual(#{untracked => [<<"test.txt">>]}, git:status(R, [{untracked, normal}])), 831 | ?_assertEqual(#{untracked => [<<"test.txt">>]}, git:status(R, [{untracked, recursive}])), 832 | ?_assertEqual(#{untracked => [<<"test.txt">>]}, git:status(R)), 833 | ?_assertEqual(#{mode => added, files => [<<"test.txt">>]}, git:add(R, "test.txt")), 834 | ?_assertEqual(#{index => [{new, <<"test.txt">>}]}, git:status(R)), 835 | ?_assertEqual(ok, file:delete("/tmp/egit/test.txt")) 836 | ]. 837 | 838 | reset_test_() -> 839 | R = git:open("/tmp/egit"), 840 | [ 841 | ?_assertEqual(ok, git:reset(R, hard)), 842 | ?_assertEqual(ok, file:write_file(<<"/tmp/egit/test.txt">>, <<"Test\n">>)), 843 | ?_assertEqual(#{mode => added, files => [<<"test.txt">>]}, git:add(R, "test.txt")), 844 | ?_assertEqual(#{index => [{new, <<"test.txt">>}]}, git:status(R)), 845 | ?_assertEqual(ok, git:reset(R, hard)), 846 | ?_assertEqual(#{}, git:status(R)), 847 | ?_assert(lists:member(file:delete("/tmp/egit/test.txt"), [ok, {error, enoent}])) 848 | ]. 849 | 850 | last_test() -> 851 | %% Delete the directory if test cases succeeded 852 | file:del_dir_r("/tmp/egit"). 853 | 854 | has_branch(R, Name) -> 855 | Nm = to_bin(Name), 856 | [B || {local, B} <- git:list_branches(R), B == Nm] /= []. 857 | 858 | -endif. 859 | --------------------------------------------------------------------------------