├── .cargo └── config.toml ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── CI.yml ├── .gitignore ├── .npmignore ├── .yarn └── releases │ └── yarn-4.9.2.cjs ├── .yarnrc.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── __test__ └── repo.spec.mjs ├── build.rs ├── example.mjs ├── index.d.ts ├── index.js ├── package.json ├── performance.mjs ├── renovate.json ├── rustfmt.toml ├── src ├── blob.rs ├── commit.rs ├── deltas.rs ├── diff.rs ├── error.rs ├── lib.rs ├── object.rs ├── reference.rs ├── remote.rs ├── repo.rs ├── repo_builder.rs ├── rev_walk.rs ├── signature.rs ├── tag.rs ├── tree.rs └── util.rs └── yarn.lock /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.aarch64-unknown-linux-musl] 2 | linker = "aarch64-linux-musl-gcc" 3 | rustflags = ["-C", "target-feature=-crt-static"] 4 | [target.x86_64-pc-windows-msvc] 5 | rustflags = ["-C", "target-feature=+crt-static"] 6 | [target.i686-pc-windows-msvc] 7 | rustflags = ["-C", "target-feature=+crt-static"] 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | 5 | *.ts text eol=lf merge=union 6 | *.tsx text eol=lf merge=union 7 | *.rs text eol=lf merge=union 8 | *.js text eol=lf merge=union 9 | *.json text eol=lf merge=union 10 | *.debug text eol=lf merge=union 11 | 12 | # Generated codes 13 | index.js linguist-detectable=false 14 | index.d.ts linguist-detectable=false 15 | .yarn/releases/*.js linguist-detectable=false 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [Brooooooklyn] 2 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | env: 3 | DEBUG: napi:* 4 | APP_NAME: simple-git 5 | MACOSX_DEPLOYMENT_TARGET: "10.13" 6 | permissions: 7 | contents: write 8 | id-token: write 9 | "on": 10 | push: 11 | branches: 12 | - main 13 | tags-ignore: 14 | - "**" 15 | paths-ignore: 16 | - "**/*.md" 17 | - LICENSE 18 | - "**/*.gitignore" 19 | - .editorconfig 20 | - docs/** 21 | pull_request: null 22 | jobs: 23 | build: 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | settings: 28 | - host: macos-latest 29 | target: x86_64-apple-darwin 30 | build: yarn build --target x86_64-apple-darwin 31 | - host: windows-latest 32 | build: yarn build --target x86_64-pc-windows-msvc 33 | target: x86_64-pc-windows-msvc 34 | - host: ubuntu-latest 35 | target: x86_64-unknown-linux-gnu 36 | docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian 37 | build: |- 38 | set -e && 39 | apt-get update && 40 | apt-get install perl -y && 41 | rustup target add x86_64-unknown-linux-gnu && 42 | cp /usr/lib/x86_64-linux-gnu/libz.a /usr/x86_64-unknown-linux-gnu/x86_64-unknown-linux-gnu/sysroot/lib && 43 | yarn build --target x86_64-unknown-linux-gnu 44 | - host: ubuntu-latest 45 | target: x86_64-unknown-linux-musl 46 | docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine 47 | build: |- 48 | set -e && 49 | apk add perl && 50 | yarn build 51 | - host: macos-latest 52 | target: aarch64-apple-darwin 53 | build: yarn build --target aarch64-apple-darwin 54 | - host: ubuntu-latest 55 | target: aarch64-unknown-linux-gnu 56 | docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 57 | build: |- 58 | set -e && 59 | apt-get update && 60 | apt-get install -y perl && 61 | unset CC_aarch64_unknown_linux_gnu && 62 | unset CXX_aarch64_unknown_linux_gnu && 63 | yarn build --target aarch64-unknown-linux-gnu 64 | - host: ubuntu-latest 65 | target: armv7-unknown-linux-gnueabihf 66 | setup: | 67 | sudo apt-get update 68 | sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf libatomic1-armhf-cross -y 69 | build: yarn build --target armv7-unknown-linux-gnueabihf --zig --zig-link-only 70 | - host: ubuntu-latest 71 | target: aarch64-linux-android 72 | build: yarn build --target aarch64-linux-android 73 | - host: ubuntu-latest 74 | target: armv7-linux-androideabi 75 | build: yarn build --target armv7-linux-androideabi 76 | - host: ubuntu-latest 77 | target: aarch64-unknown-linux-musl 78 | docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine 79 | build: |- 80 | set -e && 81 | apk add perl && 82 | rustup target add aarch64-unknown-linux-musl && 83 | yarn build --target aarch64-unknown-linux-musl 84 | - host: ubuntu-latest 85 | target: s390x-unknown-linux-gnu 86 | setup: | 87 | sudo apt-get update 88 | sudo apt-get install -y gcc-s390x-linux-gnu 89 | build: | 90 | export CARGO_TARGET_S390X_UNKNOWN_LINUX_GNU_LINKER=s390x-linux-gnu-gcc 91 | yarn build --target s390x-unknown-linux-gnu 92 | - host: ubuntu-latest 93 | target: powerpc64le-unknown-linux-gnu 94 | setup: | 95 | sudo apt-get update 96 | sudo apt-get install -y gcc-powerpc64le-linux-gnu 97 | build: | 98 | export CARGO_TARGET_POWERPC64LE_UNKNOWN_LINUX_GNU_LINKER=powerpc64le-linux-gnu-gcc 99 | yarn build --target powerpc64le-unknown-linux-gnu 100 | - host: windows-latest 101 | target: aarch64-pc-windows-msvc 102 | build: yarn build --target aarch64-pc-windows-msvc 103 | name: stable - ${{ matrix.settings.target }} - node@20 104 | runs-on: ${{ matrix.settings.host }} 105 | steps: 106 | - uses: actions/checkout@v4 107 | - name: Setup node 108 | uses: actions/setup-node@v4 109 | if: ${{ !matrix.settings.docker }} 110 | with: 111 | node-version: 20 112 | cache: yarn 113 | - name: Install 114 | uses: dtolnay/rust-toolchain@stable 115 | if: ${{ !matrix.settings.docker }} 116 | with: 117 | toolchain: stable 118 | targets: ${{ matrix.settings.target }} 119 | - name: Cache cargo registry 120 | uses: actions/cache@v4 121 | with: 122 | path: | 123 | ~/.cargo/registry 124 | ~/.cargo/git 125 | .cargo-cache 126 | target 127 | key: ${{ matrix.settings.target }}-cargo-cache 128 | - name: Setup toolchain 129 | run: ${{ matrix.settings.setup }} 130 | if: ${{ matrix.settings.setup }} 131 | shell: bash 132 | - uses: goto-bus-stop/setup-zig@v2 133 | if: ${{ matrix.settings.target == 'armv7-unknown-linux-gnueabihf' }} 134 | with: 135 | version: 0.13.0 136 | - name: Install dependencies 137 | run: yarn install --mode=skip-build --immutable 138 | - name: Build in docker 139 | uses: addnab/docker-run-action@v3 140 | if: ${{ matrix.settings.docker }} 141 | with: 142 | image: ${{ matrix.settings.docker }} 143 | options: "-v ${{ github.workspace }}/.cargo-cache/git:/usr/local/cargo/git -v ${{ github.workspace }}/.cargo-cache/registry:/usr/local/cargo/registry -v ${{ github.workspace }}:/build -w /build" 144 | run: ${{ matrix.settings.build }} 145 | - name: Build 146 | run: ${{ matrix.settings.build }} 147 | if: ${{ !matrix.settings.docker }} 148 | shell: bash 149 | - name: Upload artifact 150 | uses: actions/upload-artifact@v4 151 | with: 152 | name: bindings-${{ matrix.settings.target }} 153 | path: ${{ env.APP_NAME }}.*.node 154 | if-no-files-found: error 155 | 156 | build-freebsd: 157 | name: Build FreeBSD 158 | runs-on: ubuntu-latest 159 | steps: 160 | - uses: actions/checkout@v4 161 | - name: Build 162 | id: build 163 | uses: cross-platform-actions/action@v0.28.0 164 | env: 165 | DEBUG: napi:* 166 | RUSTUP_IO_THREADS: 1 167 | with: 168 | operating_system: freebsd 169 | version: "14.1" 170 | memory: 8G 171 | cpu_count: 3 172 | environment_variables: "DEBUG RUSTUP_IO_THREADS" 173 | shell: bash 174 | run: | 175 | sudo pkg install -y -f curl node libnghttp2 npm perl5 176 | sudo npm install -g yarn --ignore-scripts 177 | curl https://sh.rustup.rs -sSf --output rustup.sh 178 | sh rustup.sh -y --profile minimal --default-toolchain stable 179 | source "$HOME/.cargo/env" 180 | echo "~~~~ rustc --version ~~~~" 181 | rustc --version 182 | echo "~~~~ node -v ~~~~" 183 | node -v 184 | pwd 185 | ls -lah 186 | whoami 187 | env 188 | yarn install 189 | yarn build --target x86_64-unknown-freebsd 190 | rm -rf .yarn 191 | rm -rf node_modules 192 | rm -rf target 193 | - name: Upload artifact 194 | uses: actions/upload-artifact@v4 195 | with: 196 | name: bindings-freebsd 197 | path: ${{ env.APP_NAME }}.*.node 198 | if-no-files-found: error 199 | 200 | test-macOS-windows-binding: 201 | name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }} 202 | needs: 203 | - build 204 | strategy: 205 | fail-fast: false 206 | matrix: 207 | settings: 208 | - host: macos-latest 209 | target: "x86_64-apple-darwin" 210 | architecture: "x64" 211 | - host: macos-latest 212 | target: "aarch64-apple-darwin" 213 | architecture: "arm64" 214 | - host: windows-latest 215 | target: x86_64-pc-windows-msvc 216 | architecture: "x64" 217 | node: 218 | - "18" 219 | - "20" 220 | runs-on: ${{ matrix.settings.host }} 221 | steps: 222 | - uses: actions/checkout@v4 223 | with: 224 | fetch-depth: 0 225 | - name: Setup node 226 | uses: actions/setup-node@v4 227 | with: 228 | node-version: ${{ matrix.node }} 229 | cache: yarn 230 | architecture: ${{ matrix.settings.architecture }} 231 | - name: Install dependencies 232 | run: yarn install --mode=skip-build --immutable 233 | - name: Download artifacts 234 | uses: actions/download-artifact@v4 235 | with: 236 | name: bindings-${{ matrix.settings.target }} 237 | path: . 238 | - name: List packages 239 | run: ls -R . 240 | shell: bash 241 | - name: Test bindings 242 | run: yarn test 243 | test-linux-x64-gnu-binding: 244 | name: Test bindings on Linux-x64-gnu - node@${{ matrix.node }} 245 | needs: 246 | - build 247 | strategy: 248 | fail-fast: false 249 | matrix: 250 | node: 251 | - "18" 252 | - "20" 253 | runs-on: ubuntu-latest 254 | steps: 255 | - uses: actions/checkout@v4 256 | with: 257 | fetch-depth: 0 258 | - name: Setup node 259 | uses: actions/setup-node@v4 260 | with: 261 | node-version: ${{ matrix.node }} 262 | check-latest: true 263 | cache: yarn 264 | - name: Install dependencies 265 | run: yarn install --mode=skip-build --immutable 266 | - name: Download artifacts 267 | uses: actions/download-artifact@v4 268 | with: 269 | name: bindings-x86_64-unknown-linux-gnu 270 | path: . 271 | - name: List packages 272 | run: ls -R . 273 | shell: bash 274 | - name: Test bindings 275 | run: yarn test 276 | publish: 277 | name: Publish 278 | runs-on: ubuntu-latest 279 | needs: 280 | - build-freebsd 281 | - test-macOS-windows-binding 282 | - test-linux-x64-gnu-binding 283 | steps: 284 | - uses: actions/checkout@v4 285 | - name: Setup node 286 | uses: actions/setup-node@v4 287 | with: 288 | node-version: 20 289 | cache: yarn 290 | - name: Install dependencies 291 | run: yarn install --mode=skip-build --immutable 292 | - name: Download all artifacts 293 | uses: actions/download-artifact@v4 294 | with: 295 | path: artifacts 296 | - name: create npm dirs 297 | run: yarn napi create-npm-dir -t . 298 | - name: Move artifacts 299 | run: yarn artifacts 300 | - name: List packages 301 | run: ls -R ./npm 302 | shell: bash 303 | - name: Publish 304 | run: | 305 | npm config set provenance true 306 | if git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+$"; 307 | then 308 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 309 | npm publish --access public 310 | elif git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+"; 311 | then 312 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 313 | npm publish --tag next --access public 314 | else 315 | echo "Not a release, skipping publish" 316 | fi 317 | env: 318 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 319 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 320 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node 2 | # Edit at https://www.gitignore.io/?templates=node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional REPL history 61 | .node_repl_history 62 | 63 | # Output of 'npm pack' 64 | *.tgz 65 | 66 | # Yarn Integrity file 67 | .yarn-integrity 68 | 69 | # dotenv environment variables file 70 | .env 71 | .env.test 72 | 73 | # parcel-bundler cache (https://parceljs.org/) 74 | .cache 75 | 76 | # next.js build output 77 | .next 78 | 79 | # nuxt.js build output 80 | .nuxt 81 | 82 | # rollup.js default build output 83 | dist/ 84 | 85 | # Uncomment the public line if your project uses Gatsby 86 | # https://nextjs.org/blog/next-9-1#public-directory-support 87 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 88 | # public 89 | 90 | # Storybook build outputs 91 | .out 92 | .storybook-out 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # Temporary folders 107 | tmp/ 108 | temp/ 109 | 110 | # End of https://www.gitignore.io/api/node 111 | 112 | # Created by https://www.gitignore.io/api/macos 113 | # Edit at https://www.gitignore.io/?templates=macos 114 | 115 | ### macOS ### 116 | # General 117 | .DS_Store 118 | .AppleDouble 119 | .LSOverride 120 | 121 | # Icon must end with two \r 122 | Icon 123 | 124 | # Thumbnails 125 | ._* 126 | 127 | # Files that might appear in the root of a volume 128 | .DocumentRevisions-V100 129 | .fseventsd 130 | .Spotlight-V100 131 | .TemporaryItems 132 | .Trashes 133 | .VolumeIcon.icns 134 | .com.apple.timemachine.donotpresent 135 | 136 | # Directories potentially created on remote AFP share 137 | .AppleDB 138 | .AppleDesktop 139 | Network Trash Folder 140 | Temporary Items 141 | .apdisk 142 | 143 | # End of https://www.gitignore.io/api/macos 144 | target/ 145 | Cargo.lock 146 | *.node 147 | .pnp.* 148 | .yarn/* 149 | !.yarn/patches 150 | !.yarn/plugins 151 | !.yarn/releases 152 | !.yarn/sdks 153 | !.yarn/versions 154 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .cargo 4 | .github 5 | npm 6 | .eslintrc 7 | .prettierignore 8 | rustfmt.toml 9 | yarn.lock 10 | *.node 11 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-4.9.2.cjs 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "napi-rs_simple-git" 4 | version = "0.0.0" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | chrono = "0.4" 11 | git2 = { version = "0.20", features = ["default", "vendored-libgit2", "vendored-openssl"] } 12 | libgit2-sys = { version = "*", features = ["ssh", "https", "vendored", "vendored-openssl"] } 13 | home = "0.5" 14 | once_cell = "1" 15 | 16 | [dependencies.napi] 17 | version = "2" 18 | default-features = false 19 | features = ["async", "chrono_date", "napi6"] 20 | 21 | [dependencies.napi-derive] 22 | version = "2" 23 | 24 | [target.'cfg(all(target_os = "linux", target_env = "gnu", any(target_arch = "x86_64", target_arch = "aarch64")))'.dependencies] 25 | dirs = "6" 26 | 27 | [build-dependencies] 28 | napi-build = "2" 29 | 30 | [profile.release] 31 | lto = true 32 | codegen-units = 1 33 | strip = "symbols" 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2020-present LongYinan 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `@napi-rs/simple-git` 2 | 3 | ![https://github.com/Brooooooklyn/simple-git/actions](https://github.com/Brooooooklyn/simple-git/workflows/CI/badge.svg) 4 | ![](https://img.shields.io/npm/dm/@napi-rs/simple-git.svg?sanitize=true) 5 | [![Install size](https://packagephobia.com/badge?p=@napi-rs/simple-git)](https://packagephobia.com/result?p=@napi-rs/simple-git) 6 | 7 | ## `Repository` 8 | 9 | ### Usage 10 | 11 | ```ts 12 | import { Repository } from '@napi-rs/simple-git' 13 | 14 | Repository.init('/path/to/repo') // init a git repository 15 | 16 | const repo = new Repository('/path/to/repo') // Open an existed repo 17 | 18 | const timestamp = new Date(repo.getFileLatestModifiedDate('build.rs')) // get the latest modified timestamp of a `build.rs` 19 | console.log(timestamp) // 2022-03-13T12:47:47.920Z 20 | 21 | const timestampFromAsync = new Date(await repo.getFileLatestModifiedDateAsync('build.rs')) // Async version of `getFileLatestModifiedDate` 22 | 23 | console.log(timestamp) // 2022-03-13T12:47:47.920Z 24 | ``` 25 | 26 | ### API 27 | 28 | ```ts 29 | export class Repository { 30 | static init(p: string): Repository 31 | constructor(gitDir: string) 32 | /** Retrieve and resolve the reference pointed at by HEAD. */ 33 | head(): Reference 34 | getFileLatestModifiedDate(filepath: string): number 35 | getFileLatestModifiedDateAsync(filepath: string, signal?: AbortSignal | undefined | null): Promise 36 | } 37 | ``` 38 | 39 | ## `Reference` 40 | 41 | ### Usage 42 | 43 | ```ts 44 | import { Repository } from '@napi-rs/simple-git' 45 | 46 | const repo = new Repository('/path/to/repo') // Open an existed repo 47 | 48 | const headReference = repo.head() 49 | 50 | headReference.shorthand() // 'main' 51 | headReference.name() // 'refs/heads/main' 52 | headReference.target() // 7a1256e2f847f395219980bc06c6dadf0148f18d 53 | ``` 54 | 55 | ### API 56 | 57 | ```ts 58 | /** An enumeration of all possible kinds of references. */ 59 | export const enum ReferenceType { 60 | /** A reference which points at an object id. */ 61 | Direct = 0, 62 | /** A reference which points at another reference. */ 63 | Symbolic = 1, 64 | Unknown = 2 65 | } 66 | export class Reference { 67 | /** 68 | * Ensure the reference name is well-formed. 69 | * 70 | * Validation is performed as if [`ReferenceFormat::ALLOW_ONELEVEL`] 71 | * was given to [`Reference.normalize_name`]. No normalization is 72 | * performed, however. 73 | * 74 | * ```ts 75 | * import { Reference } from '@napi-rs/simple-git' 76 | * 77 | * console.assert(Reference.is_valid_name("HEAD")); 78 | * console.assert(Reference.is_valid_name("refs/heads/main")); 79 | * 80 | * // But: 81 | * console.assert(!Reference.is_valid_name("main")); 82 | * console.assert(!Reference.is_valid_name("refs/heads/*")); 83 | * console.assert(!Reference.is_valid_name("foo//bar")); 84 | * ``` 85 | */ 86 | static isValidName(name: string): boolean 87 | /** Check if a reference is a local branch. */ 88 | isBranch(): boolean 89 | /** Check if a reference is a note. */ 90 | isNote(): boolean 91 | /** Check if a reference is a remote tracking branch */ 92 | isRemote(): boolean 93 | /** Check if a reference is a tag */ 94 | isTag(): boolean 95 | kind(): ReferenceType 96 | /** 97 | * Get the full name of a reference. 98 | * 99 | * Returns `None` if the name is not valid utf-8. 100 | */ 101 | name(): string | undefined | null 102 | /** 103 | * Get the full shorthand of a reference. 104 | * 105 | * This will transform the reference name into a name "human-readable" 106 | * version. If no shortname is appropriate, it will return the full name. 107 | * 108 | * Returns `None` if the shorthand is not valid utf-8. 109 | */ 110 | shorthand(): string | undefined | null 111 | /** 112 | * Get the OID pointed to by a direct reference. 113 | * 114 | * Only available if the reference is direct (i.e. an object id reference, 115 | * not a symbolic one). 116 | */ 117 | target(): string | undefined | null 118 | /** 119 | * Return the peeled OID target of this reference. 120 | * 121 | * This peeled OID only applies to direct references that point to a hard 122 | * Tag object: it is the result of peeling such Tag. 123 | */ 124 | targetPeel(): string | undefined | null 125 | /** 126 | * Get full name to the reference pointed to by a symbolic reference. 127 | * 128 | * May return `None` if the reference is either not symbolic or not a 129 | * valid utf-8 string. 130 | */ 131 | symbolicTarget(): string | undefined | null 132 | } 133 | ``` 134 | 135 | ## Performance 136 | 137 | Compared with the `exec` function, which gets the file's latest modified date by spawning a child process. Getting the latest modified date from the file 1000 times: 138 | 139 | ``` 140 | Child process took 1.9s 141 | @napi-rs/simple-git took 65ms 142 | ``` 143 | -------------------------------------------------------------------------------- /__test__/repo.spec.mjs: -------------------------------------------------------------------------------- 1 | import { readFile } from "node:fs/promises"; 2 | import { execSync } from "node:child_process"; 3 | import { join } from "node:path"; 4 | import { fileURLToPath } from "node:url"; 5 | 6 | import test from "ava"; 7 | 8 | const __dirname = join(fileURLToPath(import.meta.url), ".."); 9 | 10 | import { Repository } from "../index.js"; 11 | 12 | const workDir = join(__dirname, ".."); 13 | 14 | test.beforeEach((t) => { 15 | t.context.repo = new Repository(workDir); 16 | }); 17 | 18 | test("Date should be equal with cli", (t) => { 19 | const { repo } = t.context; 20 | if (process.env.CI) { 21 | t.notThrows(() => repo.getFileLatestModifiedDate(join("src", "lib.rs"))); 22 | } else { 23 | t.deepEqual( 24 | new Date( 25 | execSync("git log -1 --format=%cd --date=iso src/lib.rs", { 26 | cwd: workDir, 27 | }) 28 | .toString("utf8") 29 | .trim(), 30 | ).valueOf(), 31 | repo.getFileLatestModifiedDate(join("src", "lib.rs")), 32 | ); 33 | } 34 | }); 35 | 36 | test("Should be able to resolve head", (t) => { 37 | const { repo } = t.context; 38 | t.is( 39 | repo.head().target(), 40 | process.env.CI 41 | ? process.env.GITHUB_SHA 42 | : execSync("git rev-parse HEAD", { 43 | cwd: workDir, 44 | }) 45 | .toString("utf8") 46 | .trim(), 47 | ); 48 | }); 49 | 50 | test("Should be able to get blob content", async (t) => { 51 | if (process.platform === "win32") { 52 | t.pass("Skip test on windows"); 53 | return; 54 | } 55 | const { repo } = t.context; 56 | const blob = repo 57 | .head() 58 | .peelToTree() 59 | .getPath("__test__/repo.spec.mjs") 60 | .toObject(repo) 61 | .peelToBlob(); 62 | t.deepEqual( 63 | await readFile(join(__dirname, "repo.spec.mjs"), "utf8"), 64 | Buffer.from(blob.content()).toString("utf8"), 65 | ); 66 | }); 67 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | extern crate napi_build; 2 | 3 | fn main() { 4 | napi_build::setup(); 5 | let compile_target = std::env::var("TARGET").unwrap(); 6 | match compile_target.as_str() { 7 | "x86_64-unknown-linux-gnu" => { 8 | println!("cargo:rustc-link-search=/usr/x86_64-unknown-linux-gnu/lib"); 9 | } 10 | "armv7-unknown-linux-gnueabihf" => { 11 | const CROSS_LIB_PATH: &str = "/usr/lib/gcc-cross/arm-linux-gnueabihf"; 12 | if let Ok(version) = std::process::Command::new("ls") 13 | .arg(CROSS_LIB_PATH) 14 | .output() 15 | .map(|o| String::from_utf8(o.stdout).unwrap().trim().to_string()) 16 | { 17 | println!("cargo:rustc-link-search={CROSS_LIB_PATH}/{version}"); 18 | }; 19 | } 20 | _ => {} 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example.mjs: -------------------------------------------------------------------------------- 1 | import { join } from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | 4 | import { Repository } from './index.js' 5 | 6 | const ROOT_DIR = join(fileURLToPath(import.meta.url), '..') 7 | 8 | // Open the sub directory 9 | const repo = Repository.discover(join(ROOT_DIR, 'src')) 10 | 11 | console.info('Repo root path:', join(repo.path(), '..')) 12 | 13 | const head = repo.head() 14 | 15 | console.info('HEAD:', head.name()) 16 | console.info('HEAD shorthand:', head.shorthand()) 17 | 18 | repo.tagForeach((oid, nameBuffer) => { 19 | console.info(`Tag: ${nameBuffer.toString()} (${oid.toString()})`) 20 | }) 21 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | 4 | /* auto-generated by NAPI-RS */ 5 | 6 | export const enum DiffFlags { 7 | /** 8 | * File(s) treated as binary data. 9 | * 1 << 0 10 | */ 11 | Binary = 1, 12 | /** 13 | * File(s) treated as text data. 14 | * 1 << 1 15 | */ 16 | NotBinary = 2, 17 | /** 18 | * `id` value is known correct. 19 | * 1 << 2 20 | */ 21 | ValidId = 4, 22 | /** 23 | * File exists at this side of the delta. 24 | * 1 << 3 25 | */ 26 | Exists = 8 27 | } 28 | /** Valid modes for index and tree entries. */ 29 | export const enum FileMode { 30 | /** Unreadable */ 31 | Unreadable = 0, 32 | /** Tree */ 33 | Tree = 1, 34 | /** Blob */ 35 | Blob = 2, 36 | /** Group writable blob. Obsolete mode kept for compatibility reasons */ 37 | BlobGroupWritable = 3, 38 | /** Blob executable */ 39 | BlobExecutable = 4, 40 | /** Link */ 41 | Link = 5, 42 | /** Commit */ 43 | Commit = 6 44 | } 45 | export const enum Delta { 46 | /** No changes */ 47 | Unmodified = 0, 48 | /** Entry does not exist in old version */ 49 | Added = 1, 50 | /** Entry does not exist in new version */ 51 | Deleted = 2, 52 | /** Entry content changed between old and new */ 53 | Modified = 3, 54 | /** Entry was renamed between old and new */ 55 | Renamed = 4, 56 | /** Entry was copied from another old entry */ 57 | Copied = 5, 58 | /** Entry is ignored item in workdir */ 59 | Ignored = 6, 60 | /** Entry is untracked item in workdir */ 61 | Untracked = 7, 62 | /** Type of entry changed between old and new */ 63 | Typechange = 8, 64 | /** Entry is unreadable */ 65 | Unreadable = 9, 66 | /** Entry in the index is conflicted */ 67 | Conflicted = 10 68 | } 69 | export interface DiffOptions { 70 | /** 71 | * When generating output, include the names of unmodified files if they 72 | * are included in the `Diff`. Normally these are skipped in the formats 73 | * that list files (e.g. name-only, name-status, raw). Even with this these 74 | * will not be included in the patch format. 75 | */ 76 | showUnmodified?: boolean 77 | } 78 | export const enum ObjectType { 79 | /** Any kind of git object */ 80 | Any = 0, 81 | /** An object which corresponds to a git commit */ 82 | Commit = 1, 83 | /** An object which corresponds to a git tree */ 84 | Tree = 2, 85 | /** An object which corresponds to a git blob */ 86 | Blob = 3, 87 | /** An object which corresponds to a git tag */ 88 | Tag = 4 89 | } 90 | /** An enumeration of all possible kinds of references. */ 91 | export const enum ReferenceType { 92 | /** A reference which points at an object id. */ 93 | Direct = 0, 94 | /** A reference which points at another reference. */ 95 | Symbolic = 1, 96 | Unknown = 2 97 | } 98 | /** An enumeration of the possible directions for a remote. */ 99 | export const enum Direction { 100 | /** Data will be fetched (read) from this remote. */ 101 | Fetch = 0, 102 | /** Data will be pushed (written) to this remote. */ 103 | Push = 1 104 | } 105 | /** Configuration for how pruning is done on a fetch */ 106 | export const enum FetchPrune { 107 | /** Use the setting from the configuration */ 108 | Unspecified = 0, 109 | /** Force pruning on */ 110 | On = 1, 111 | /** Force pruning off */ 112 | Off = 2 113 | } 114 | /** Automatic tag following options. */ 115 | export const enum AutotagOption { 116 | /** Use the setting from the remote's configuration */ 117 | Unspecified = 0, 118 | /** Ask the server for tags pointing to objects we're already downloading */ 119 | Auto = 1, 120 | /** Don't ask for any tags beyond the refspecs */ 121 | None = 2, 122 | /** Ask for all the tags */ 123 | All = 3 124 | } 125 | /** 126 | * Remote redirection settings; whether redirects to another host are 127 | * permitted. 128 | * 129 | * By default, git will follow a redirect on the initial request 130 | * (`/info/refs`), but not subsequent requests. 131 | */ 132 | export const enum RemoteRedirect { 133 | /** Do not follow any off-site redirects at any stage of the fetch or push. */ 134 | None = 0, 135 | /** 136 | * Allow off-site redirects only upon the initial request. This is the 137 | * default. 138 | */ 139 | Initial = 1, 140 | /** Allow redirects at any stage in the fetch or push. */ 141 | All = 2 142 | } 143 | /** Types of credentials that can be requested by a credential callback. */ 144 | export const enum CredentialType { 145 | /** 1 << 0 */ 146 | UserPassPlaintext = 1, 147 | /** 1 << 1 */ 148 | SshKey = 2, 149 | /** 1 << 6 */ 150 | SshMemory = 64, 151 | /** 1 << 2 */ 152 | SshCustom = 4, 153 | /** 1 << 3 */ 154 | Default = 8, 155 | /** 1 << 4 */ 156 | SshInteractive = 16, 157 | /** 1 << 5 */ 158 | Username = 32 159 | } 160 | export interface CredInfo { 161 | credType: CredentialType 162 | url: string 163 | username: string 164 | } 165 | export const enum RemoteUpdateFlags { 166 | UpdateFetchHead = 1, 167 | ReportUnchanged = 2 168 | } 169 | export interface Progress { 170 | totalObjects: number 171 | indexedObjects: number 172 | receivedObjects: number 173 | localObjects: number 174 | totalDeltas: number 175 | indexedDeltas: number 176 | receivedBytes: number 177 | } 178 | export interface PushTransferProgress { 179 | current: number 180 | total: number 181 | bytes: number 182 | } 183 | /** Check whether a cred_type contains another credential type. */ 184 | export function credTypeContains(credType: CredentialType, another: CredentialType): boolean 185 | export const enum RepositoryState { 186 | Clean = 0, 187 | Merge = 1, 188 | Revert = 2, 189 | RevertSequence = 3, 190 | CherryPick = 4, 191 | CherryPickSequence = 5, 192 | Bisect = 6, 193 | Rebase = 7, 194 | RebaseInteractive = 8, 195 | RebaseMerge = 9, 196 | ApplyMailbox = 10, 197 | ApplyMailboxOrRebase = 11 198 | } 199 | export const enum RepositoryOpenFlags { 200 | /** Only open the specified path; don't walk upward searching. */ 201 | NoSearch = 0, 202 | /** Search across filesystem boundaries. */ 203 | CrossFS = 1, 204 | /** Force opening as bare repository, and defer loading its config. */ 205 | Bare = 2, 206 | /** Don't try appending `/.git` to the specified repository path. */ 207 | NoDotGit = 3, 208 | /** Respect environment variables like `$GIT_DIR`. */ 209 | FromEnv = 4 210 | } 211 | export const enum CloneLocal { 212 | /** 213 | * Auto-detect (default) 214 | * 215 | * Here libgit2 will bypass the git-aware transport for local paths, but 216 | * use a normal fetch for `file://` URLs. 217 | */ 218 | Auto = 0, 219 | /** Bypass the git-aware transport even for `file://` URLs. */ 220 | Local = 1, 221 | /** Never bypass the git-aware transport */ 222 | None = 2, 223 | /** Bypass the git-aware transport, but don't try to use hardlinks. */ 224 | NoLinks = 3 225 | } 226 | /** Orderings that may be specified for Revwalk iteration. */ 227 | export const enum Sort { 228 | /** 229 | * Sort the repository contents in no particular ordering. 230 | * 231 | * This sorting is arbitrary, implementation-specific, and subject to 232 | * change at any time. This is the default sorting for new walkers. 233 | */ 234 | None = 0, 235 | /** 236 | * Sort the repository contents in topological order (children before 237 | * parents). 238 | * 239 | * This sorting mode can be combined with time sorting. 240 | * 1 << 0 241 | */ 242 | Topological = 1, 243 | /** 244 | * Sort the repository contents by commit time. 245 | * 246 | * This sorting mode can be combined with topological sorting. 247 | * 1 << 1 248 | */ 249 | Time = 2, 250 | /** 251 | * Iterate through the repository contents in reverse order. 252 | * 253 | * This sorting mode can be combined with any others. 254 | * 1 << 2 255 | */ 256 | Reverse = 4 257 | } 258 | export declare class Blob { 259 | /** Get the id (SHA1) of a repository blob */ 260 | id(): string 261 | /** Determine if the blob content is most certainly binary or not. */ 262 | isBinary(): boolean 263 | /** Get the content of this blob. */ 264 | content(): Uint8Array 265 | /** Get the size in bytes of the contents of this blob. */ 266 | size(): bigint 267 | } 268 | export declare class Commit { 269 | /** Get the id (SHA1) of a repository object */ 270 | id(): string 271 | /** 272 | * Get the id of the tree pointed to by this commit. 273 | * 274 | * No attempts are made to fetch an object from the ODB. 275 | */ 276 | treeId(): string 277 | /** Get the tree pointed to by this commit. */ 278 | tree(): Tree 279 | /** 280 | * 281 | * The returned message will be slightly prettified by removing any 282 | * potential leading newlines. 283 | * 284 | * `None` will be returned if the message is not valid utf-8 285 | */ 286 | message(): string | null 287 | /** 288 | * Get the full message of a commit as a byte slice. 289 | * 290 | * The returned message will be slightly prettified by removing any 291 | * potential leading newlines. 292 | */ 293 | messageBytes(): Buffer 294 | /** 295 | * Get the encoding for the message of a commit, as a string representing a 296 | * standard encoding name. 297 | * 298 | * `None` will be returned if the encoding is not known 299 | */ 300 | messageEncoding(): string | null 301 | /** 302 | * Get the full raw message of a commit. 303 | * 304 | * `None` will be returned if the message is not valid utf-8 305 | */ 306 | messageRaw(): string | null 307 | /** Get the full raw message of a commit. */ 308 | messageRawBytes(): Buffer 309 | /** 310 | * Get the full raw text of the commit header. 311 | * 312 | * `None` will be returned if the message is not valid utf-8 313 | */ 314 | rawHeader(): string | null 315 | /** Get an arbitrary header field. */ 316 | headerFieldBytes(field: string): Buffer 317 | /** Get the full raw text of the commit header. */ 318 | rawHeaderBytes(): Buffer 319 | /** 320 | * Get the short "summary" of the git commit message. 321 | * 322 | * The returned message is the summary of the commit, comprising the first 323 | * paragraph of the message with whitespace trimmed and squashed. 324 | * 325 | * `None` may be returned if an error occurs or if the summary is not valid 326 | * utf-8. 327 | */ 328 | summary(): string | null 329 | /** 330 | * Get the short "summary" of the git commit message. 331 | * 332 | * The returned message is the summary of the commit, comprising the first 333 | * paragraph of the message with whitespace trimmed and squashed. 334 | * 335 | * `None` may be returned if an error occurs 336 | */ 337 | summaryBytes(): Buffer | null 338 | /** 339 | * Get the long "body" of the git commit message. 340 | * 341 | * The returned message is the body of the commit, comprising everything 342 | * but the first paragraph of the message. Leading and trailing whitespaces 343 | * are trimmed. 344 | * 345 | * `None` may be returned if an error occurs or if the summary is not valid 346 | * utf-8. 347 | */ 348 | body(): string | null 349 | /** 350 | * Get the long "body" of the git commit message. 351 | * 352 | * The returned message is the body of the commit, comprising everything 353 | * but the first paragraph of the message. Leading and trailing whitespaces 354 | * are trimmed. 355 | * 356 | * `None` may be returned if an error occurs. 357 | */ 358 | bodyBytes(): Buffer | null 359 | /** 360 | * Get the commit time (i.e. committer time) of a commit. 361 | * 362 | * The first element of the tuple is the time, in seconds, since the epoch. 363 | * The second element is the offset, in minutes, of the time zone of the 364 | * committer's preferred time zone. 365 | */ 366 | time(): Date 367 | /** Get the author of this commit. */ 368 | author(): Signature 369 | /** Get the committer of this commit. */ 370 | committer(): Signature 371 | /** 372 | * Amend this existing commit with all non-`None` values 373 | * 374 | * This creates a new commit that is exactly the same as the old commit, 375 | * except that any non-`None` values will be updated. The new commit has 376 | * the same parents as the old commit. 377 | * 378 | * For information about `update_ref`, see [`Repository::commit`]. 379 | * 380 | * [`Repository::commit`]: struct.Repository.html#method.commit 381 | */ 382 | amend(updateRef?: string | undefined | null, author?: Signature | undefined | null, committer?: Signature | undefined | null, messageEncoding?: string | undefined | null, message?: string | undefined | null, tree?: Tree | undefined | null): string 383 | /** 384 | * Get the number of parents of this commit. 385 | * 386 | * Use the `parents` iterator to return an iterator over all parents. 387 | */ 388 | parentCount(): bigint 389 | /** 390 | * Get the specified parent of the commit. 391 | * 392 | * Use the `parents` iterator to return an iterator over all parents. 393 | */ 394 | parent(i: number): Commit 395 | /** 396 | * Get the specified parent id of the commit. 397 | * 398 | * This is different from `parent`, which will attempt to load the 399 | * parent commit from the ODB. 400 | * 401 | * Use the `parent_ids` iterator to return an iterator over all parents. 402 | */ 403 | parentId(i: number): string 404 | /** Casts this Commit to be usable as an `Object` */ 405 | asObject(): GitObject 406 | } 407 | /** An iterator over the diffs in a delta */ 408 | export declare class Deltas { 409 | [Symbol.iterator](): Iterator 410 | } 411 | export declare class DiffDelta { 412 | /** 413 | * Returns the flags on the delta. 414 | * 415 | * For more information, see `DiffFlags`'s documentation. 416 | */ 417 | flags(): DiffFlags 418 | /** Returns the number of files in this delta. */ 419 | numFiles(): number 420 | /** Returns the status of this entry */ 421 | status(): Delta 422 | /** 423 | * Return the file which represents the "from" side of the diff. 424 | * 425 | * What side this means depends on the function that was used to generate 426 | * the diff and will be documented on the function itself. 427 | */ 428 | oldFile(): DiffFile 429 | /** 430 | * Return the file which represents the "to" side of the diff. 431 | * 432 | * What side this means depends on the function that was used to generate 433 | * the diff and will be documented on the function itself. 434 | */ 435 | newFile(): DiffFile 436 | } 437 | export declare class DiffFile { 438 | /** 439 | * Returns the Oid of this item. 440 | * 441 | * If this entry represents an absent side of a diff (e.g. the `old_file` 442 | * of a `Added` delta), then the oid returned will be zeroes. 443 | */ 444 | id(): string 445 | /** 446 | * Returns the path, in bytes, of the entry relative to the working 447 | * directory of the repository. 448 | */ 449 | path(): string | null 450 | /** Returns the size of this entry, in bytes */ 451 | size(): bigint 452 | /** Returns `true` if file(s) are treated as binary data. */ 453 | isBinary(): boolean 454 | /** Returns `true` if file(s) are treated as text data. */ 455 | isNotBinary(): boolean 456 | /** Returns `true` if `id` value is known correct. */ 457 | isValidId(): boolean 458 | /** Returns `true` if file exists at this side of the delta. */ 459 | exists(): boolean 460 | /** Returns file mode. */ 461 | mode(): FileMode 462 | } 463 | export declare class Diff { 464 | /** 465 | * Merge one diff into another. 466 | * 467 | * This merges items from the "from" list into the "self" list. The 468 | * resulting diff will have all items that appear in either list. 469 | * If an item appears in both lists, then it will be "merged" to appear 470 | * as if the old version was from the "onto" list and the new version 471 | * is from the "from" list (with the exception that if the item has a 472 | * pending DELETE in the middle, then it will show as deleted). 473 | */ 474 | merge(diff: Diff): void 475 | /** Returns an iterator over the deltas in this diff. */ 476 | deltas(): Deltas 477 | /** Check if deltas are sorted case sensitively or insensitively. */ 478 | isSortedIcase(): boolean 479 | } 480 | export declare class GitObject { 481 | /** Get the id (SHA1) of a repository object */ 482 | id(): string 483 | /** Get the type of the object. */ 484 | kind(): ObjectType | null 485 | /** 486 | * Recursively peel an object until an object of the specified type is met. 487 | * 488 | * If you pass `Any` as the target type, then the object will be 489 | * peeled until the type changes (e.g. a tag will be chased until the 490 | * referenced object is no longer a tag). 491 | */ 492 | peel(kind: ObjectType): GitObject 493 | /** Recursively peel an object until a blob is found */ 494 | peelToBlob(): Blob 495 | } 496 | export declare class Reference { 497 | /** 498 | * Ensure the reference name is well-formed. 499 | * 500 | * Validation is performed as if [`ReferenceFormat::ALLOW_ONELEVEL`] 501 | * was given to [`Reference.normalize_name`]. No normalization is 502 | * performed, however. 503 | * 504 | * ```ts 505 | * import { Reference } from '@napi-rs/simple-git' 506 | * 507 | * console.assert(Reference.is_valid_name("HEAD")); 508 | * console.assert(Reference.is_valid_name("refs/heads/main")); 509 | * 510 | * // But: 511 | * console.assert(!Reference.is_valid_name("main")); 512 | * console.assert(!Reference.is_valid_name("refs/heads/*")); 513 | * console.assert(!Reference.is_valid_name("foo//bar")); 514 | * ``` 515 | */ 516 | static isValidName(name: string): boolean 517 | /** Check if a reference is a local branch. */ 518 | isBranch(): boolean 519 | /** Check if a reference is a note. */ 520 | isNote(): boolean 521 | /** Check if a reference is a remote tracking branch */ 522 | isRemote(): boolean 523 | /** Check if a reference is a tag */ 524 | isTag(): boolean 525 | kind(): ReferenceType 526 | /** 527 | * Get the full name of a reference. 528 | * 529 | * Returns `None` if the name is not valid utf-8. 530 | */ 531 | name(): string | null 532 | /** 533 | * Get the full shorthand of a reference. 534 | * 535 | * This will transform the reference name into a name "human-readable" 536 | * version. If no shortname is appropriate, it will return the full name. 537 | * 538 | * Returns `None` if the shorthand is not valid utf-8. 539 | */ 540 | shorthand(): string | null 541 | /** 542 | * Get the OID pointed to by a direct reference. 543 | * 544 | * Only available if the reference is direct (i.e. an object id reference, 545 | * not a symbolic one). 546 | */ 547 | target(): string | null 548 | /** 549 | * Return the peeled OID target of this reference. 550 | * 551 | * This peeled OID only applies to direct references that point to a hard 552 | * Tag object: it is the result of peeling such Tag. 553 | */ 554 | targetPeel(): string | null 555 | /** 556 | * Peel a reference to a tree 557 | * 558 | * This method recursively peels the reference until it reaches 559 | * a tree. 560 | */ 561 | peelToTree(): Tree 562 | /** 563 | * Get full name to the reference pointed to by a symbolic reference. 564 | * 565 | * May return `None` if the reference is either not symbolic or not a 566 | * valid utf-8 string. 567 | */ 568 | symbolicTarget(): string | null 569 | /** 570 | * Resolve a symbolic reference to a direct reference. 571 | * 572 | * This method iteratively peels a symbolic reference until it resolves to 573 | * a direct reference to an OID. 574 | * 575 | * If a direct reference is passed as an argument, a copy of that 576 | * reference is returned. 577 | */ 578 | resolve(): Reference 579 | /** 580 | * Rename an existing reference. 581 | * 582 | * This method works for both direct and symbolic references. 583 | * 584 | * If the force flag is not enabled, and there's already a reference with 585 | * the given name, the renaming will fail. 586 | */ 587 | rename(newName: string, force: boolean, msg: string): Reference 588 | } 589 | export declare class Remote { 590 | /** Ensure the remote name is well-formed. */ 591 | static isValidName(name: string): boolean 592 | /** 593 | * Get the remote's name. 594 | * 595 | * Returns `None` if this remote has not yet been named or if the name is 596 | * not valid utf-8 597 | */ 598 | name(): string | null 599 | /** 600 | * Get the remote's url. 601 | * 602 | * Returns `None` if the url is not valid utf-8 603 | */ 604 | url(): string | null 605 | /** 606 | * Get the remote's pushurl. 607 | * 608 | * Returns `None` if the pushurl is not valid utf-8 609 | */ 610 | pushurl(): string | null 611 | /** 612 | * Get the remote's default branch. 613 | * 614 | * The remote (or more exactly its transport) must have connected to the 615 | * remote repository. This default branch is available as soon as the 616 | * connection to the remote is initiated and it remains available after 617 | * disconnecting. 618 | */ 619 | defaultBranch(): string 620 | /** Open a connection to a remote. */ 621 | connect(dir: Direction): void 622 | /** Check whether the remote is connected */ 623 | connected(): boolean 624 | /** Disconnect from the remote */ 625 | disconnect(): void 626 | /** 627 | * Cancel the operation 628 | * 629 | * At certain points in its operation, the network code checks whether the 630 | * operation has been cancelled and if so stops the operation. 631 | */ 632 | stop(): void 633 | /** 634 | * Download new data and update tips 635 | * 636 | * Convenience function to connect to a remote, download the data, 637 | * disconnect and update the remote-tracking branches. 638 | * 639 | */ 640 | fetch(refspecs: Array, fetchOptions?: FetchOptions | undefined | null): void 641 | /** Update the tips to the new state */ 642 | updateTips(updateFetchhead: RemoteUpdateFlags, downloadTags: AutotagOption, callbacks?: RemoteCallbacks | undefined | null, msg?: string | undefined | null): void 643 | } 644 | export declare class RemoteCallbacks { 645 | constructor() 646 | /** 647 | * The callback through which to fetch credentials if required. 648 | * 649 | * # Example 650 | * 651 | * Prepare a callback to authenticate using the `$HOME/.ssh/id_rsa` SSH key, and 652 | * extracting the username from the URL (i.e. git@github.com:rust-lang/git2-rs.git): 653 | * 654 | * ```js 655 | * import { join } from 'node:path' 656 | * import { homedir } from 'node:os' 657 | * 658 | * import { Cred, FetchOptions, RemoteCallbacks, RepoBuilder, credTypeContains } from '@napi-rs/simple-git' 659 | * 660 | * const builder = new RepoBuilder() 661 | * const remoteCallbacks = new RemoteCallbacks() 662 | * .credentials((cred) => { 663 | * return Cred.sshKey(cred.username, null, join(homedir(), '.ssh', 'id_rsa'), null) 664 | * }) 665 | * 666 | * const fetchOptions = new FetchOptions().depth(0).remoteCallback(remoteCallbacks) 667 | * 668 | * const repo = builder.branch('master') 669 | * .fetchOptions(fetchOptions) 670 | * .clone("git@github.com:rust-lang/git2-rs.git", "git2-rs") 671 | * ``` 672 | */ 673 | credentials(callback: (arg: CredInfo) => Cred): this 674 | /** The callback through which progress is monitored. */ 675 | transferProgress(callback: (arg: Progress) => void): this 676 | /** The callback through which progress of push transfer is monitored */ 677 | pushTransferProgress(callback: (current: number, total: number, bytes: number) => void): this 678 | } 679 | export declare class FetchOptions { 680 | constructor() 681 | /** Set the callbacks to use for the fetch operation. */ 682 | remoteCallback(callback: RemoteCallbacks): this 683 | /** Set the proxy options to use for the fetch operation. */ 684 | proxyOptions(options: ProxyOptions): this 685 | /** Set whether to perform a prune after the fetch. */ 686 | prune(prune: FetchPrune): this 687 | /** 688 | * Set whether to write the results to FETCH_HEAD. 689 | * 690 | * Defaults to `true`. 691 | */ 692 | updateFetchhead(update: boolean): this 693 | /** 694 | * Set fetch depth, a value less or equal to 0 is interpreted as pull 695 | * everything (effectively the same as not declaring a limit depth). 696 | */ 697 | depth(depth: number): this 698 | /** 699 | * Set how to behave regarding tags on the remote, such as auto-downloading 700 | * tags for objects we're downloading or downloading all of them. 701 | * 702 | * The default is to auto-follow tags. 703 | */ 704 | downloadTags(opt: AutotagOption): this 705 | /** 706 | * Set remote redirection settings; whether redirects to another host are 707 | * permitted. 708 | * 709 | * By default, git will follow a redirect on the initial request 710 | * (`/info/refs`), but not subsequent requests. 711 | */ 712 | followRedirects(opt: RemoteRedirect): this 713 | /** Set extra headers for this fetch operation. */ 714 | customHeaders(headers: Array): this 715 | } 716 | export declare class ProxyOptions { 717 | constructor() 718 | /** 719 | * Try to auto-detect the proxy from the git configuration. 720 | * 721 | * Note that this will override `url` specified before. 722 | */ 723 | auto(): this 724 | /** 725 | * Specify the exact URL of the proxy to use. 726 | * 727 | * Note that this will override `auto` specified before. 728 | */ 729 | url(url: string): this 730 | } 731 | export declare class Cred { 732 | /** 733 | * Create a "default" credential usable for Negotiate mechanisms like NTLM 734 | * or Kerberos authentication. 735 | */ 736 | constructor() 737 | /** 738 | * Create a new ssh key credential object used for querying an ssh-agent. 739 | * 740 | * The username specified is the username to authenticate. 741 | */ 742 | static sshKeyFromAgent(username: string): Cred 743 | /** Create a new passphrase-protected ssh key credential object. */ 744 | static sshKey(username: string, publickey: string | undefined | null, privatekey: string, passphrase?: string | undefined | null): Cred 745 | /** Create a new ssh key credential object reading the keys from memory. */ 746 | static sshKeyFromMemory(username: string, publickey: string | undefined | null, privatekey: string, passphrase?: string | undefined | null): Cred 747 | /** Create a new plain-text username and password credential object. */ 748 | static userpassPlaintext(username: string, password: string): Cred 749 | /** 750 | * Create a credential to specify a username. 751 | * 752 | * This is used with ssh authentication to query for the username if none is 753 | * specified in the URL. 754 | */ 755 | static username(username: string): Cred 756 | /** Check whether a credential object contains username information. */ 757 | hasUsername(): boolean 758 | /** Return the type of credentials that this object represents. */ 759 | credtype(): CredentialType 760 | } 761 | export declare class Repository { 762 | static init(p: string): Repository 763 | /** 764 | * Find and open an existing repository, with additional options. 765 | * 766 | * If flags contains REPOSITORY_OPEN_NO_SEARCH, the path must point 767 | * directly to a repository; otherwise, this may point to a subdirectory 768 | * of a repository, and `open_ext` will search up through parent 769 | * directories. 770 | * 771 | * If flags contains REPOSITORY_OPEN_CROSS_FS, the search through parent 772 | * directories will not cross a filesystem boundary (detected when the 773 | * stat st_dev field changes). 774 | * 775 | * If flags contains REPOSITORY_OPEN_BARE, force opening the repository as 776 | * bare even if it isn't, ignoring any working directory, and defer 777 | * loading the repository configuration for performance. 778 | * 779 | * If flags contains REPOSITORY_OPEN_NO_DOTGIT, don't try appending 780 | * `/.git` to `path`. 781 | * 782 | * If flags contains REPOSITORY_OPEN_FROM_ENV, `open_ext` will ignore 783 | * other flags and `ceiling_dirs`, and respect the same environment 784 | * variables git does. Note, however, that `path` overrides `$GIT_DIR`; to 785 | * respect `$GIT_DIR` as well, use `open_from_env`. 786 | * 787 | * ceiling_dirs specifies a list of paths that the search through parent 788 | * directories will stop before entering. Use the functions in std::env 789 | * to construct or manipulate such a path list. 790 | */ 791 | static openExt(path: string, flags: RepositoryOpenFlags, ceilingDirs: Array): Repository 792 | /** 793 | * Attempt to open an already-existing repository at or above `path` 794 | * 795 | * This starts at `path` and looks up the filesystem hierarchy 796 | * until it finds a repository. 797 | */ 798 | static discover(path: string): Repository 799 | /** 800 | * Creates a new `--bare` repository in the specified folder. 801 | * 802 | * The folder must exist prior to invoking this function. 803 | */ 804 | static initBare(path: string): Repository 805 | /** 806 | * Clone a remote repository. 807 | * 808 | * See the `RepoBuilder` struct for more information. This function will 809 | * delegate to a fresh `RepoBuilder` 810 | */ 811 | static clone(url: string, path: string): Repository 812 | /** 813 | * Clone a remote repository, initialize and update its submodules 814 | * recursively. 815 | * 816 | * This is similar to `git clone --recursive`. 817 | */ 818 | static cloneRecurse(url: string, path: string): Repository 819 | /** 820 | * Attempt to open an already-existing repository at `path`. 821 | * 822 | * The path can point to either a normal or bare repository. 823 | */ 824 | constructor(gitDir: string) 825 | /** Retrieve and resolve the reference pointed at by HEAD. */ 826 | head(): Reference 827 | /** Tests whether this repository is a shallow clone. */ 828 | isShallow(): boolean 829 | /** Tests whether this repository is empty. */ 830 | isEmpty(): boolean 831 | /** Tests whether this repository is a worktree. */ 832 | isWorktree(): boolean 833 | /** 834 | * Returns the path to the `.git` folder for normal repositories or the 835 | * repository itself for bare repositories. 836 | */ 837 | path(): string 838 | /** Returns the current state of this repository */ 839 | state(): RepositoryState 840 | /** 841 | * Get the path of the working directory for this repository. 842 | * 843 | * If this repository is bare, then `None` is returned. 844 | */ 845 | workdir(): string | null 846 | /** 847 | * Set the path to the working directory for this repository. 848 | * 849 | * If `update_link` is true, create/update the gitlink file in the workdir 850 | * and set config "core.worktree" (if workdir is not the parent of the .git 851 | * directory). 852 | */ 853 | setWorkdir(path: string, updateGitlink: boolean): void 854 | /** 855 | * Get the currently active namespace for this repository. 856 | * 857 | * If there is no namespace, or the namespace is not a valid utf8 string, 858 | * `None` is returned. 859 | */ 860 | namespace(): string | null 861 | /** Set the active namespace for this repository. */ 862 | setNamespace(namespace: string): void 863 | /** Remove the active namespace for this repository. */ 864 | removeNamespace(): void 865 | /** 866 | * Retrieves the Git merge message. 867 | * Remember to remove the message when finished. 868 | */ 869 | message(): string 870 | /** Remove the Git merge message. */ 871 | removeMessage(): void 872 | /** List all remotes for a given repository */ 873 | remotes(): Array 874 | /** Get the information for a particular remote */ 875 | findRemote(name: string): Remote | null 876 | /** 877 | * Add a remote with the default fetch refspec to the repository's 878 | * configuration. 879 | */ 880 | remote(name: string, url: string): Remote 881 | /** 882 | * Add a remote with the provided fetch refspec to the repository's 883 | * configuration. 884 | */ 885 | remoteWithFetch(name: string, url: string, refspect: string): Remote 886 | /** 887 | * Create an anonymous remote 888 | * 889 | * Create a remote with the given URL and refspec in memory. You can use 890 | * this when you have a URL instead of a remote's name. Note that anonymous 891 | * remotes cannot be converted to persisted remotes. 892 | */ 893 | remoteAnonymous(url: string): Remote 894 | /** 895 | * Give a remote a new name 896 | * 897 | * All remote-tracking branches and configuration settings for the remote 898 | * are updated. 899 | * 900 | * A temporary in-memory remote cannot be given a name with this method. 901 | * 902 | * No loaded instances of the remote with the old name will change their 903 | * name or their list of refspecs. 904 | * 905 | * The returned array of strings is a list of the non-default refspecs 906 | * which cannot be renamed and are returned for further processing by the 907 | * caller. 908 | */ 909 | remoteRename(name: string, newName: string): Array 910 | /** 911 | * Delete an existing persisted remote. 912 | * 913 | * All remote-tracking branches and configuration settings for the remote 914 | * will be removed. 915 | */ 916 | remoteDelete(name: string): this 917 | /** 918 | * Add a fetch refspec to the remote's configuration 919 | * 920 | * Add the given refspec to the fetch list in the configuration. No loaded 921 | */ 922 | remoteAddFetch(name: string, refspec: string): this 923 | /** 924 | * Add a push refspec to the remote's configuration. 925 | * 926 | * Add the given refspec to the push list in the configuration. No 927 | * loaded remote instances will be affected. 928 | */ 929 | remoteAddPush(name: string, refspec: string): this 930 | /** 931 | * Add a push refspec to the remote's configuration. 932 | * 933 | * Add the given refspec to the push list in the configuration. No 934 | * loaded remote instances will be affected. 935 | */ 936 | remoteSetUrl(name: string, url: string): this 937 | /** 938 | * Set the remote's URL for pushing in the configuration. 939 | * 940 | * Remote objects already in memory will not be affected. This assumes 941 | * the common case of a single-url remote and will otherwise return an 942 | * error. 943 | * 944 | * `None` indicates that it should be cleared. 945 | */ 946 | remoteSetPushurl(name: string, url?: string | undefined | null): this 947 | /** Lookup a reference to one of the objects in a repository. */ 948 | findTree(oid: string): Tree | null 949 | findCommit(oid: string): Commit | null 950 | /** 951 | * Create a new tag in the repository from an object 952 | * 953 | * A new reference will also be created pointing to this tag object. If 954 | * `force` is true and a reference already exists with the given name, 955 | * it'll be replaced. 956 | * 957 | * The message will not be cleaned up. 958 | * 959 | * The tag name will be checked for validity. You must avoid the characters 960 | * '~', '^', ':', ' \ ', '?', '[', and '*', and the sequences ".." and " @ 961 | * {" which have special meaning to revparse. 962 | */ 963 | tag(name: string, target: GitObject, tagger: Signature, message: string, force: boolean): string 964 | /** 965 | * Create a new tag in the repository from an object without creating a reference. 966 | * 967 | * The message will not be cleaned up. 968 | * 969 | * The tag name will be checked for validity. You must avoid the characters 970 | * '~', '^', ':', ' \ ', '?', '[', and '*', and the sequences ".." and " @ 971 | * {" which have special meaning to revparse. 972 | */ 973 | tagAnnotationCreate(name: string, target: GitObject, tagger: Signature, message: string): string 974 | /** 975 | * Create a new lightweight tag pointing at a target object 976 | * 977 | * A new direct reference will be created pointing to this target object. 978 | * If force is true and a reference already exists with the given name, 979 | * it'll be replaced. 980 | */ 981 | tagLightweight(name: string, target: GitObject, force: boolean): string 982 | /** Lookup a tag object from the repository. */ 983 | findTag(oid: string): Tag 984 | /** 985 | * Delete an existing tag reference. 986 | * 987 | * The tag name will be checked for validity, see `tag` for some rules 988 | * about valid names. 989 | */ 990 | tagDelete(name: string): void 991 | /** 992 | * Get a list with all the tags in the repository. 993 | * 994 | * An optional fnmatch pattern can also be specified. 995 | */ 996 | tagNames(pattern?: string | undefined | null): Array 997 | /** 998 | * iterate over all tags calling `cb` on each. 999 | * the callback is provided the tag id and name 1000 | */ 1001 | tagForeach(cb: (arg0: string, arg1: Buffer) => void): void 1002 | /** 1003 | * Create a diff between a tree and the working directory. 1004 | * 1005 | * The tree you provide will be used for the "old_file" side of the delta, 1006 | * and the working directory will be used for the "new_file" side. 1007 | * 1008 | * This is not the same as `git diff ` or `git diff-index 1009 | * `. Those commands use information from the index, whereas this 1010 | * function strictly returns the differences between the tree and the files 1011 | * in the working directory, regardless of the state of the index. Use 1012 | * `tree_to_workdir_with_index` to emulate those commands. 1013 | * 1014 | * To see difference between this and `tree_to_workdir_with_index`, 1015 | * consider the example of a staged file deletion where the file has then 1016 | * been put back into the working dir and further modified. The 1017 | * tree-to-workdir diff for that file is 'modified', but `git diff` would 1018 | * show status 'deleted' since there is a staged delete. 1019 | * 1020 | * If `None` is passed for `tree`, then an empty tree is used. 1021 | */ 1022 | diffTreeToWorkdir(oldTree?: Tree | undefined | null): Diff 1023 | /** 1024 | * Create a diff between a tree and the working directory using index data 1025 | * to account for staged deletes, tracked files, etc. 1026 | * 1027 | * This emulates `git diff ` by diffing the tree to the index and 1028 | * the index to the working directory and blending the results into a 1029 | * single diff that includes staged deleted, etc. 1030 | */ 1031 | diffTreeToWorkdirWithIndex(oldTree?: Tree | undefined | null): Diff 1032 | treeEntryToObject(treeEntry: TreeEntry): GitObject 1033 | /** 1034 | * Create new commit in the repository 1035 | * 1036 | * If the `update_ref` is not `None`, name of the reference that will be 1037 | * updated to point to this commit. If the reference is not direct, it will 1038 | * be resolved to a direct reference. Use "HEAD" to update the HEAD of the 1039 | * current branch and make it point to this commit. If the reference 1040 | * doesn't exist yet, it will be created. If it does exist, the first 1041 | * parent must be the tip of this branch. 1042 | */ 1043 | commit(updateRef: string | undefined | null, author: Signature, committer: Signature, message: string, tree: Tree): string 1044 | /** Create a revwalk that can be used to traverse the commit graph. */ 1045 | revWalk(): RevWalk 1046 | getFileLatestModifiedDate(filepath: string): number 1047 | getFileLatestModifiedDateAsync(filepath: string, signal?: AbortSignal | undefined | null): Promise 1048 | } 1049 | export declare class RepoBuilder { 1050 | constructor() 1051 | /** 1052 | * Indicate whether the repository will be cloned as a bare repository or 1053 | * not. 1054 | */ 1055 | bare(bare: boolean): this 1056 | /** 1057 | * Specify the name of the branch to check out after the clone. 1058 | * 1059 | * If not specified, the remote's default branch will be used. 1060 | */ 1061 | branch(branch: string): this 1062 | /** 1063 | * Configures options for bypassing the git-aware transport on clone. 1064 | * 1065 | * Bypassing it means that instead of a fetch libgit2 will copy the object 1066 | * database directory instead of figuring out what it needs, which is 1067 | * faster. If possible, it will hardlink the files to save space. 1068 | */ 1069 | cloneLocal(cloneLocal: CloneLocal): this 1070 | /** 1071 | * Options which control the fetch, including callbacks. 1072 | * 1073 | * The callbacks are used for reporting fetch progress, and for acquiring 1074 | * credentials in the event they are needed. 1075 | */ 1076 | fetchOptions(fetchOptions: FetchOptions): this 1077 | clone(url: string, path: string): Repository 1078 | } 1079 | export declare class RevWalk { 1080 | [Symbol.iterator](): Iterator 1081 | /** 1082 | * Reset a revwalk to allow re-configuring it. 1083 | * 1084 | * The revwalk is automatically reset when iteration of its commits 1085 | * completes. 1086 | */ 1087 | reset(): this 1088 | /** Set the sorting mode for a revwalk. */ 1089 | setSorting(sorting: Sort): this 1090 | /** 1091 | * Simplify the history by first-parent 1092 | * 1093 | * No parents other than the first for each commit will be enqueued. 1094 | */ 1095 | simplifyFirstParent(): this 1096 | /** 1097 | * Mark a commit to start traversal from. 1098 | * 1099 | * The given OID must belong to a commitish on the walked repository. 1100 | * 1101 | * The given commit will be used as one of the roots when starting the 1102 | * revision walk. At least one commit must be pushed onto the walker before 1103 | * a walk can be started. 1104 | */ 1105 | push(oid: string): this 1106 | /** 1107 | * Push the repository's HEAD 1108 | * 1109 | * For more information, see `push`. 1110 | */ 1111 | pushHead(): this 1112 | /** 1113 | * Push matching references 1114 | * 1115 | * The OIDs pointed to by the references that match the given glob pattern 1116 | * will be pushed to the revision walker. 1117 | * 1118 | * A leading 'refs/' is implied if not present as well as a trailing `/ \ 1119 | * *` if the glob lacks '?', ' \ *' or '['. 1120 | * 1121 | * Any references matching this glob which do not point to a commitish 1122 | * will be ignored. 1123 | */ 1124 | pushGlob(glob: string): this 1125 | /** 1126 | * Push and hide the respective endpoints of the given range. 1127 | * 1128 | * The range should be of the form `..` where each 1129 | * `` is in the form accepted by `revparse_single`. The left-hand 1130 | * commit will be hidden and the right-hand commit pushed. 1131 | */ 1132 | pushRange(range: string): this 1133 | /** 1134 | * Push the OID pointed to by a reference 1135 | * 1136 | * The reference must point to a commitish. 1137 | */ 1138 | pushRef(reference: string): this 1139 | /** Mark a commit as not of interest to this revwalk. */ 1140 | hide(oid: string): this 1141 | /** 1142 | * Hide the repository's HEAD 1143 | * 1144 | * For more information, see `hide`. 1145 | */ 1146 | hideHead(): this 1147 | /** 1148 | * Hide matching references. 1149 | * 1150 | * The OIDs pointed to by the references that match the given glob pattern 1151 | * and their ancestors will be hidden from the output on the revision walk. 1152 | * 1153 | * A leading 'refs/' is implied if not present as well as a trailing `/ \ 1154 | * *` if the glob lacks '?', ' \ *' or '['. 1155 | * 1156 | * Any references matching this glob which do not point to a commitish 1157 | * will be ignored. 1158 | */ 1159 | hideGlob(glob: string): this 1160 | /** 1161 | * Hide the OID pointed to by a reference. 1162 | * 1163 | * The reference must point to a commitish. 1164 | */ 1165 | hideRef(reference: string): this 1166 | } 1167 | /** 1168 | * A Signature is used to indicate authorship of various actions throughout the 1169 | * library. 1170 | * 1171 | * Signatures contain a name, email, and timestamp. All fields can be specified 1172 | * with `new` while the `now` constructor omits the timestamp. The 1173 | * [`Repository::signature`] method can be used to create a default signature 1174 | * with name and email values read from the configuration. 1175 | * 1176 | * [`Repository::signature`]: struct.Repository.html#method.signature 1177 | */ 1178 | export declare class Signature { 1179 | /** 1180 | * Create a new action signature with a timestamp of 'now'. 1181 | * 1182 | * See `new` for more information 1183 | */ 1184 | static now(name: string, email: string): Signature 1185 | /** 1186 | * Create a new action signature. 1187 | * 1188 | * The `time` specified is in seconds since the epoch, and the `offset` is 1189 | * the time zone offset in minutes. 1190 | * 1191 | * Returns error if either `name` or `email` contain angle brackets. 1192 | */ 1193 | constructor(name: string, email: string, time: number) 1194 | /** 1195 | * Gets the name on the signature. 1196 | * 1197 | * Returns `None` if the name is not valid utf-8 1198 | */ 1199 | name(): string | null 1200 | /** 1201 | * Gets the email on the signature. 1202 | * 1203 | * Returns `None` if the email is not valid utf-8 1204 | */ 1205 | email(): string | null 1206 | /** Return the time, in seconds, from epoch */ 1207 | when(): number 1208 | } 1209 | export declare class Tag { 1210 | /** 1211 | * Determine whether a tag name is valid, meaning that (when prefixed with refs/tags/) that 1212 | * it is a valid reference name, and that any additional tag name restrictions are imposed 1213 | * (eg, it cannot start with a -). 1214 | */ 1215 | static isValidName(name: string): boolean 1216 | /** Get the id (SHA1) of a repository object */ 1217 | id(): string 1218 | /** 1219 | * Get the message of a tag 1220 | * 1221 | * Returns None if there is no message or if it is not valid utf8 1222 | */ 1223 | message(): string | null 1224 | /** 1225 | * Get the message of a tag 1226 | * 1227 | * Returns None if there is no message 1228 | */ 1229 | messageBytes(): Buffer | null 1230 | /** 1231 | * Get the name of a tag 1232 | * 1233 | * Returns None if it is not valid utf8 1234 | */ 1235 | name(): string | null 1236 | /** Get the name of a tag */ 1237 | nameBytes(): Buffer 1238 | /** Recursively peel a tag until a non tag git_object is found */ 1239 | peel(): GitObject 1240 | } 1241 | export declare class Tree { 1242 | /** Get the id (SHA1) of a repository object */ 1243 | id(): string 1244 | /** Get the number of entries listed in a tree. */ 1245 | len(): bigint 1246 | /** Return `true` if there is not entry */ 1247 | isEmpty(): boolean 1248 | /** Returns an iterator over the entries in this tree. */ 1249 | iter(): TreeIter 1250 | /** Lookup a tree entry by SHA value */ 1251 | getId(id: string): TreeEntry | null 1252 | /** Lookup a tree entry by its position in the tree */ 1253 | get(index: number): TreeEntry | null 1254 | /** Lookup a tree entry by its filename */ 1255 | getName(name: string): TreeEntry | null 1256 | /** Lookup a tree entry by its filename */ 1257 | getPath(name: string): TreeEntry | null 1258 | } 1259 | export declare class TreeIter { 1260 | [Symbol.iterator](): Iterator 1261 | } 1262 | export declare class TreeEntry { 1263 | /** Get the id of the object pointed by the entry */ 1264 | id(): string 1265 | /** Get the name of a tree entry */ 1266 | name(): string 1267 | /** Get the filename of a tree entry */ 1268 | nameBytes(): Uint8Array 1269 | /** Convert a tree entry to the object it points to. */ 1270 | toObject(repo: Repository): GitObject 1271 | } 1272 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /* prettier-ignore */ 4 | 5 | /* auto-generated by NAPI-RS */ 6 | 7 | const { existsSync, readFileSync } = require('fs') 8 | const { join } = require('path') 9 | 10 | const { platform, arch } = process 11 | 12 | let nativeBinding = null 13 | let localFileExisted = false 14 | let loadError = null 15 | 16 | function isMusl() { 17 | // For Node 10 18 | if (!process.report || typeof process.report.getReport !== 'function') { 19 | try { 20 | const lddPath = require('child_process').execSync('which ldd').toString().trim() 21 | return readFileSync(lddPath, 'utf8').includes('musl') 22 | } catch (e) { 23 | return true 24 | } 25 | } else { 26 | const { glibcVersionRuntime } = process.report.getReport().header 27 | return !glibcVersionRuntime 28 | } 29 | } 30 | 31 | switch (platform) { 32 | case 'android': 33 | switch (arch) { 34 | case 'arm64': 35 | localFileExisted = existsSync(join(__dirname, 'simple-git.android-arm64.node')) 36 | try { 37 | if (localFileExisted) { 38 | nativeBinding = require('./simple-git.android-arm64.node') 39 | } else { 40 | nativeBinding = require('@napi-rs/simple-git-android-arm64') 41 | } 42 | } catch (e) { 43 | loadError = e 44 | } 45 | break 46 | case 'arm': 47 | localFileExisted = existsSync(join(__dirname, 'simple-git.android-arm-eabi.node')) 48 | try { 49 | if (localFileExisted) { 50 | nativeBinding = require('./simple-git.android-arm-eabi.node') 51 | } else { 52 | nativeBinding = require('@napi-rs/simple-git-android-arm-eabi') 53 | } 54 | } catch (e) { 55 | loadError = e 56 | } 57 | break 58 | default: 59 | throw new Error(`Unsupported architecture on Android ${arch}`) 60 | } 61 | break 62 | case 'win32': 63 | switch (arch) { 64 | case 'x64': 65 | localFileExisted = existsSync( 66 | join(__dirname, 'simple-git.win32-x64-msvc.node') 67 | ) 68 | try { 69 | if (localFileExisted) { 70 | nativeBinding = require('./simple-git.win32-x64-msvc.node') 71 | } else { 72 | nativeBinding = require('@napi-rs/simple-git-win32-x64-msvc') 73 | } 74 | } catch (e) { 75 | loadError = e 76 | } 77 | break 78 | case 'ia32': 79 | localFileExisted = existsSync( 80 | join(__dirname, 'simple-git.win32-ia32-msvc.node') 81 | ) 82 | try { 83 | if (localFileExisted) { 84 | nativeBinding = require('./simple-git.win32-ia32-msvc.node') 85 | } else { 86 | nativeBinding = require('@napi-rs/simple-git-win32-ia32-msvc') 87 | } 88 | } catch (e) { 89 | loadError = e 90 | } 91 | break 92 | case 'arm64': 93 | localFileExisted = existsSync( 94 | join(__dirname, 'simple-git.win32-arm64-msvc.node') 95 | ) 96 | try { 97 | if (localFileExisted) { 98 | nativeBinding = require('./simple-git.win32-arm64-msvc.node') 99 | } else { 100 | nativeBinding = require('@napi-rs/simple-git-win32-arm64-msvc') 101 | } 102 | } catch (e) { 103 | loadError = e 104 | } 105 | break 106 | default: 107 | throw new Error(`Unsupported architecture on Windows: ${arch}`) 108 | } 109 | break 110 | case 'darwin': 111 | localFileExisted = existsSync(join(__dirname, 'simple-git.darwin-universal.node')) 112 | try { 113 | if (localFileExisted) { 114 | nativeBinding = require('./simple-git.darwin-universal.node') 115 | } else { 116 | nativeBinding = require('@napi-rs/simple-git-darwin-universal') 117 | } 118 | break 119 | } catch {} 120 | switch (arch) { 121 | case 'x64': 122 | localFileExisted = existsSync(join(__dirname, 'simple-git.darwin-x64.node')) 123 | try { 124 | if (localFileExisted) { 125 | nativeBinding = require('./simple-git.darwin-x64.node') 126 | } else { 127 | nativeBinding = require('@napi-rs/simple-git-darwin-x64') 128 | } 129 | } catch (e) { 130 | loadError = e 131 | } 132 | break 133 | case 'arm64': 134 | localFileExisted = existsSync( 135 | join(__dirname, 'simple-git.darwin-arm64.node') 136 | ) 137 | try { 138 | if (localFileExisted) { 139 | nativeBinding = require('./simple-git.darwin-arm64.node') 140 | } else { 141 | nativeBinding = require('@napi-rs/simple-git-darwin-arm64') 142 | } 143 | } catch (e) { 144 | loadError = e 145 | } 146 | break 147 | default: 148 | throw new Error(`Unsupported architecture on macOS: ${arch}`) 149 | } 150 | break 151 | case 'freebsd': 152 | if (arch !== 'x64') { 153 | throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) 154 | } 155 | localFileExisted = existsSync(join(__dirname, 'simple-git.freebsd-x64.node')) 156 | try { 157 | if (localFileExisted) { 158 | nativeBinding = require('./simple-git.freebsd-x64.node') 159 | } else { 160 | nativeBinding = require('@napi-rs/simple-git-freebsd-x64') 161 | } 162 | } catch (e) { 163 | loadError = e 164 | } 165 | break 166 | case 'linux': 167 | switch (arch) { 168 | case 'x64': 169 | if (isMusl()) { 170 | localFileExisted = existsSync( 171 | join(__dirname, 'simple-git.linux-x64-musl.node') 172 | ) 173 | try { 174 | if (localFileExisted) { 175 | nativeBinding = require('./simple-git.linux-x64-musl.node') 176 | } else { 177 | nativeBinding = require('@napi-rs/simple-git-linux-x64-musl') 178 | } 179 | } catch (e) { 180 | loadError = e 181 | } 182 | } else { 183 | localFileExisted = existsSync( 184 | join(__dirname, 'simple-git.linux-x64-gnu.node') 185 | ) 186 | try { 187 | if (localFileExisted) { 188 | nativeBinding = require('./simple-git.linux-x64-gnu.node') 189 | } else { 190 | nativeBinding = require('@napi-rs/simple-git-linux-x64-gnu') 191 | } 192 | } catch (e) { 193 | loadError = e 194 | } 195 | } 196 | break 197 | case 'arm64': 198 | if (isMusl()) { 199 | localFileExisted = existsSync( 200 | join(__dirname, 'simple-git.linux-arm64-musl.node') 201 | ) 202 | try { 203 | if (localFileExisted) { 204 | nativeBinding = require('./simple-git.linux-arm64-musl.node') 205 | } else { 206 | nativeBinding = require('@napi-rs/simple-git-linux-arm64-musl') 207 | } 208 | } catch (e) { 209 | loadError = e 210 | } 211 | } else { 212 | localFileExisted = existsSync( 213 | join(__dirname, 'simple-git.linux-arm64-gnu.node') 214 | ) 215 | try { 216 | if (localFileExisted) { 217 | nativeBinding = require('./simple-git.linux-arm64-gnu.node') 218 | } else { 219 | nativeBinding = require('@napi-rs/simple-git-linux-arm64-gnu') 220 | } 221 | } catch (e) { 222 | loadError = e 223 | } 224 | } 225 | break 226 | case 'arm': 227 | if (isMusl()) { 228 | localFileExisted = existsSync( 229 | join(__dirname, 'simple-git.linux-arm-musleabihf.node') 230 | ) 231 | try { 232 | if (localFileExisted) { 233 | nativeBinding = require('./simple-git.linux-arm-musleabihf.node') 234 | } else { 235 | nativeBinding = require('@napi-rs/simple-git-linux-arm-musleabihf') 236 | } 237 | } catch (e) { 238 | loadError = e 239 | } 240 | } else { 241 | localFileExisted = existsSync( 242 | join(__dirname, 'simple-git.linux-arm-gnueabihf.node') 243 | ) 244 | try { 245 | if (localFileExisted) { 246 | nativeBinding = require('./simple-git.linux-arm-gnueabihf.node') 247 | } else { 248 | nativeBinding = require('@napi-rs/simple-git-linux-arm-gnueabihf') 249 | } 250 | } catch (e) { 251 | loadError = e 252 | } 253 | } 254 | break 255 | case 'riscv64': 256 | if (isMusl()) { 257 | localFileExisted = existsSync( 258 | join(__dirname, 'simple-git.linux-riscv64-musl.node') 259 | ) 260 | try { 261 | if (localFileExisted) { 262 | nativeBinding = require('./simple-git.linux-riscv64-musl.node') 263 | } else { 264 | nativeBinding = require('@napi-rs/simple-git-linux-riscv64-musl') 265 | } 266 | } catch (e) { 267 | loadError = e 268 | } 269 | } else { 270 | localFileExisted = existsSync( 271 | join(__dirname, 'simple-git.linux-riscv64-gnu.node') 272 | ) 273 | try { 274 | if (localFileExisted) { 275 | nativeBinding = require('./simple-git.linux-riscv64-gnu.node') 276 | } else { 277 | nativeBinding = require('@napi-rs/simple-git-linux-riscv64-gnu') 278 | } 279 | } catch (e) { 280 | loadError = e 281 | } 282 | } 283 | break 284 | case 's390x': 285 | localFileExisted = existsSync( 286 | join(__dirname, 'simple-git.linux-s390x-gnu.node') 287 | ) 288 | try { 289 | if (localFileExisted) { 290 | nativeBinding = require('./simple-git.linux-s390x-gnu.node') 291 | } else { 292 | nativeBinding = require('@napi-rs/simple-git-linux-s390x-gnu') 293 | } 294 | } catch (e) { 295 | loadError = e 296 | } 297 | break 298 | default: 299 | throw new Error(`Unsupported architecture on Linux: ${arch}`) 300 | } 301 | break 302 | default: 303 | throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) 304 | } 305 | 306 | if (!nativeBinding) { 307 | if (loadError) { 308 | throw loadError 309 | } 310 | throw new Error(`Failed to load native binding`) 311 | } 312 | 313 | const { Blob, Commit, DiffFlags, FileMode, Deltas, DiffDelta, Delta, DiffFile, Diff, ObjectType, GitObject, Reference, ReferenceType, Direction, FetchPrune, AutotagOption, RemoteRedirect, CredentialType, RemoteUpdateFlags, Remote, RemoteCallbacks, FetchOptions, ProxyOptions, Cred, credTypeContains, RepositoryState, RepositoryOpenFlags, Repository, RepoBuilder, CloneLocal, Sort, RevWalk, Signature, Tag, Tree, TreeIter, TreeEntry } = nativeBinding 314 | 315 | module.exports.Blob = Blob 316 | module.exports.Commit = Commit 317 | module.exports.DiffFlags = DiffFlags 318 | module.exports.FileMode = FileMode 319 | module.exports.Deltas = Deltas 320 | module.exports.DiffDelta = DiffDelta 321 | module.exports.Delta = Delta 322 | module.exports.DiffFile = DiffFile 323 | module.exports.Diff = Diff 324 | module.exports.ObjectType = ObjectType 325 | module.exports.GitObject = GitObject 326 | module.exports.Reference = Reference 327 | module.exports.ReferenceType = ReferenceType 328 | module.exports.Direction = Direction 329 | module.exports.FetchPrune = FetchPrune 330 | module.exports.AutotagOption = AutotagOption 331 | module.exports.RemoteRedirect = RemoteRedirect 332 | module.exports.CredentialType = CredentialType 333 | module.exports.RemoteUpdateFlags = RemoteUpdateFlags 334 | module.exports.Remote = Remote 335 | module.exports.RemoteCallbacks = RemoteCallbacks 336 | module.exports.FetchOptions = FetchOptions 337 | module.exports.ProxyOptions = ProxyOptions 338 | module.exports.Cred = Cred 339 | module.exports.credTypeContains = credTypeContains 340 | module.exports.RepositoryState = RepositoryState 341 | module.exports.RepositoryOpenFlags = RepositoryOpenFlags 342 | module.exports.Repository = Repository 343 | module.exports.RepoBuilder = RepoBuilder 344 | module.exports.CloneLocal = CloneLocal 345 | module.exports.Sort = Sort 346 | module.exports.RevWalk = RevWalk 347 | module.exports.Signature = Signature 348 | module.exports.Tag = Tag 349 | module.exports.Tree = Tree 350 | module.exports.TreeIter = TreeIter 351 | module.exports.TreeEntry = TreeEntry 352 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@napi-rs/simple-git", 3 | "version": "0.1.19", 4 | "main": "index.js", 5 | "types": "./index.d.ts", 6 | "repository": { 7 | "url": "https://github.com/Brooooooklyn/simple-git" 8 | }, 9 | "napi": { 10 | "name": "simple-git", 11 | "triples": { 12 | "additional": [ 13 | "aarch64-apple-darwin", 14 | "aarch64-linux-android", 15 | "aarch64-unknown-linux-gnu", 16 | "aarch64-unknown-linux-musl", 17 | "aarch64-pc-windows-msvc", 18 | "powerpc64le-unknown-linux-gnu", 19 | "s390x-unknown-linux-gnu", 20 | "armv7-unknown-linux-gnueabihf", 21 | "x86_64-unknown-linux-musl", 22 | "x86_64-unknown-freebsd", 23 | "armv7-linux-androideabi" 24 | ] 25 | } 26 | }, 27 | "ava": { 28 | "timeout": "3m", 29 | "workerThreads": false 30 | }, 31 | "files": [ 32 | "index.js", 33 | "index.d.ts" 34 | ], 35 | "license": "MIT", 36 | "devDependencies": { 37 | "@napi-rs/cli": "^2.18.4", 38 | "@types/node": "^22.0.0", 39 | "ava": "^6.1.2", 40 | "pretty-ms": "^9.0.0" 41 | }, 42 | "engines": { 43 | "node": ">= 10" 44 | }, 45 | "scripts": { 46 | "artifacts": "napi artifacts", 47 | "build": "napi build --platform --release", 48 | "build:debug": "napi build --platform", 49 | "prepublishOnly": "napi prepublish -t npm", 50 | "test": "ava", 51 | "version": "napi version" 52 | }, 53 | "packageManager": "yarn@4.9.2" 54 | } 55 | -------------------------------------------------------------------------------- /performance.mjs: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process' 2 | 3 | import prettyMs from 'pretty-ms' 4 | 5 | import { Repository } from './index.js' 6 | 7 | const GIT_DIR = '.' 8 | const FILE = 'src/lib.rs' 9 | 10 | const repo = new Repository(GIT_DIR) 11 | 12 | const startChildProcessTime = process.hrtime.bigint() 13 | 14 | await Promise.all( 15 | Array.from({ length: 1000 }).map( 16 | () => 17 | new Promise((resolve, reject) => { 18 | let output = '' 19 | const cp = exec( 20 | `git log -1 --format=%cd --date=iso ${FILE}`, 21 | { 22 | encoding: 'utf8', 23 | cwd: GIT_DIR, 24 | }, 25 | (err, stdout) => { 26 | if (err) { 27 | return reject(err) 28 | } 29 | output += stdout 30 | } 31 | ) 32 | cp.on('close', () => { 33 | resolve(new Date(output)) 34 | }) 35 | }) 36 | ) 37 | ) 38 | 39 | const childProcessNs = process.hrtime.bigint() - startChildProcessTime 40 | 41 | console.info( 42 | 'Child process took %s', 43 | prettyMs(Number(childProcessNs) / 1000_000) 44 | ) 45 | 46 | const startLibGit2 = process.hrtime.bigint() 47 | 48 | await Promise.all( 49 | Array.from({ length: 1000 }).map(() => 50 | repo.getFileLatestModifiedDateAsync(FILE) 51 | ) 52 | ) 53 | 54 | const libGit2Ns = process.hrtime.bigint() - startLibGit2 55 | 56 | console.info( 57 | '@napi-rs/simple-git took %s', 58 | prettyMs(Number(libGit2Ns) / 1000_000) 59 | ) 60 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", ":preserveSemverRanges"], 3 | "packageRules": [ 4 | { 5 | "automerge": true, 6 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"] 7 | } 8 | ], 9 | "lockFileMaintenance": { 10 | "enabled": true, 11 | "extends": ["schedule:monthly"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 2 2 | edition = "2021" 3 | -------------------------------------------------------------------------------- /src/blob.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use napi::bindgen_prelude::{SharedReference, Uint8Array}; 4 | use napi_derive::napi; 5 | 6 | use crate::object::GitObject; 7 | 8 | pub(crate) enum BlobParent { 9 | GitObject(SharedReference>), 10 | } 11 | 12 | impl Deref for BlobParent { 13 | type Target = git2::Blob<'static>; 14 | 15 | fn deref(&self) -> &git2::Blob<'static> { 16 | match self { 17 | BlobParent::GitObject(parent) => parent.deref(), 18 | } 19 | } 20 | } 21 | 22 | #[napi] 23 | pub struct Blob { 24 | pub(crate) inner: BlobParent, 25 | } 26 | 27 | #[napi] 28 | impl Blob { 29 | #[napi] 30 | /// Get the id (SHA1) of a repository blob 31 | pub fn id(&self) -> String { 32 | self.inner.id().to_string() 33 | } 34 | 35 | #[napi] 36 | /// Determine if the blob content is most certainly binary or not. 37 | pub fn is_binary(&self) -> bool { 38 | self.inner.is_binary() 39 | } 40 | 41 | #[napi] 42 | /// Get the content of this blob. 43 | pub fn content(&self) -> Uint8Array { 44 | self.inner.content().to_vec().into() 45 | } 46 | 47 | #[napi] 48 | /// Get the size in bytes of the contents of this blob. 49 | pub fn size(&self) -> u64 { 50 | self.inner.size() as u64 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/commit.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use napi::bindgen_prelude::*; 4 | use napi_derive::napi; 5 | 6 | use chrono::{DateTime, Utc}; 7 | 8 | use crate::{ 9 | error::IntoNapiError, 10 | object::ObjectParent, 11 | signature::{Signature, SignatureInner}, 12 | tree::{Tree, TreeParent}, 13 | }; 14 | 15 | pub(crate) enum CommitInner { 16 | Repository(SharedReference>), 17 | Commit(git2::Commit<'static>), 18 | } 19 | 20 | impl Deref for CommitInner { 21 | type Target = git2::Commit<'static>; 22 | 23 | fn deref(&self) -> &Self::Target { 24 | match self { 25 | CommitInner::Repository(r) => r.deref(), 26 | CommitInner::Commit(c) => c, 27 | } 28 | } 29 | } 30 | 31 | #[napi] 32 | pub struct Commit { 33 | pub(crate) inner: CommitInner, 34 | } 35 | 36 | #[napi] 37 | impl Commit { 38 | #[napi] 39 | /// Get the id (SHA1) of a repository object 40 | pub fn id(&self) -> String { 41 | self.inner.id().to_string() 42 | } 43 | 44 | #[napi] 45 | /// Get the id of the tree pointed to by this commit. 46 | /// 47 | /// No attempts are made to fetch an object from the ODB. 48 | pub fn tree_id(&self) -> String { 49 | self.inner.tree_id().to_string() 50 | } 51 | 52 | #[napi] 53 | /// Get the tree pointed to by this commit. 54 | pub fn tree(&self, this_ref: Reference, env: Env) -> Result { 55 | let tree = this_ref.share_with(env, |commit| { 56 | let tree = commit.inner.tree().convert("Find tree on commit failed")?; 57 | Ok(tree) 58 | })?; 59 | Ok(Tree { 60 | inner: TreeParent::Commit(tree), 61 | }) 62 | } 63 | 64 | #[napi] 65 | // Get the full message of a commit. 66 | /// 67 | /// The returned message will be slightly prettified by removing any 68 | /// potential leading newlines. 69 | /// 70 | /// `None` will be returned if the message is not valid utf-8 71 | pub fn message(&self) -> Option<&str> { 72 | self.inner.message() 73 | } 74 | 75 | #[napi] 76 | /// Get the full message of a commit as a byte slice. 77 | /// 78 | /// The returned message will be slightly prettified by removing any 79 | /// potential leading newlines. 80 | pub fn message_bytes(&self) -> Buffer { 81 | self.inner.message_bytes().to_vec().into() 82 | } 83 | 84 | #[napi] 85 | /// Get the encoding for the message of a commit, as a string representing a 86 | /// standard encoding name. 87 | /// 88 | /// `None` will be returned if the encoding is not known 89 | pub fn message_encoding(&self) -> Option<&str> { 90 | self.inner.message_encoding() 91 | } 92 | 93 | #[napi] 94 | /// Get the full raw message of a commit. 95 | /// 96 | /// `None` will be returned if the message is not valid utf-8 97 | pub fn message_raw(&self) -> Option<&str> { 98 | self.inner.message_raw() 99 | } 100 | 101 | #[napi] 102 | /// Get the full raw message of a commit. 103 | pub fn message_raw_bytes(&self) -> Buffer { 104 | self.inner.message_raw_bytes().to_vec().into() 105 | } 106 | 107 | #[napi] 108 | /// Get the full raw text of the commit header. 109 | /// 110 | /// `None` will be returned if the message is not valid utf-8 111 | pub fn raw_header(&self) -> Option<&str> { 112 | self.inner.raw_header() 113 | } 114 | 115 | #[napi] 116 | /// Get an arbitrary header field. 117 | pub fn header_field_bytes(&self, field: String) -> Result { 118 | self 119 | .inner 120 | .header_field_bytes(field) 121 | .map(|b| b.to_vec().into()) 122 | .convert_without_message() 123 | } 124 | 125 | #[napi] 126 | /// Get the full raw text of the commit header. 127 | pub fn raw_header_bytes(&self) -> Buffer { 128 | self.inner.raw_header_bytes().to_vec().into() 129 | } 130 | 131 | #[napi] 132 | /// Get the short "summary" of the git commit message. 133 | /// 134 | /// The returned message is the summary of the commit, comprising the first 135 | /// paragraph of the message with whitespace trimmed and squashed. 136 | /// 137 | /// `None` may be returned if an error occurs or if the summary is not valid 138 | /// utf-8. 139 | pub fn summary(&self) -> Option<&str> { 140 | self.inner.summary() 141 | } 142 | 143 | #[napi] 144 | /// Get the short "summary" of the git commit message. 145 | /// 146 | /// The returned message is the summary of the commit, comprising the first 147 | /// paragraph of the message with whitespace trimmed and squashed. 148 | /// 149 | /// `None` may be returned if an error occurs 150 | pub fn summary_bytes(&self) -> Option { 151 | self.inner.summary_bytes().map(|s| s.to_vec().into()) 152 | } 153 | 154 | #[napi] 155 | /// Get the long "body" of the git commit message. 156 | /// 157 | /// The returned message is the body of the commit, comprising everything 158 | /// but the first paragraph of the message. Leading and trailing whitespaces 159 | /// are trimmed. 160 | /// 161 | /// `None` may be returned if an error occurs or if the summary is not valid 162 | /// utf-8. 163 | pub fn body(&self) -> Option<&str> { 164 | self.inner.body() 165 | } 166 | 167 | #[napi] 168 | /// Get the long "body" of the git commit message. 169 | /// 170 | /// The returned message is the body of the commit, comprising everything 171 | /// but the first paragraph of the message. Leading and trailing whitespaces 172 | /// are trimmed. 173 | /// 174 | /// `None` may be returned if an error occurs. 175 | pub fn body_bytes(&self) -> Option { 176 | self.inner.body_bytes().map(|b| b.to_vec().into()) 177 | } 178 | 179 | #[napi] 180 | /// Get the commit time (i.e. committer time) of a commit. 181 | /// 182 | /// The first element of the tuple is the time, in seconds, since the epoch. 183 | /// The second element is the offset, in minutes, of the time zone of the 184 | /// committer's preferred time zone. 185 | pub fn time(&self) -> Result> { 186 | let committer_time = self.inner.time(); 187 | 188 | DateTime::from_timestamp(committer_time.seconds(), 0) 189 | .ok_or_else(|| Error::from_reason("Invalid commit time")) 190 | } 191 | 192 | #[napi] 193 | /// Get the author of this commit. 194 | pub fn author(&self, this_ref: Reference, env: Env) -> Result { 195 | let author = this_ref.share_with(env, |commit| Ok(commit.inner.author()))?; 196 | Ok(Signature { 197 | inner: SignatureInner::FromCommit(author), 198 | }) 199 | } 200 | 201 | #[napi] 202 | /// Get the committer of this commit. 203 | pub fn committer(&self, this_ref: Reference, env: Env) -> Result { 204 | let committer = this_ref.share_with(env, |commit| Ok(commit.inner.committer()))?; 205 | Ok(Signature { 206 | inner: SignatureInner::FromCommit(committer), 207 | }) 208 | } 209 | 210 | #[napi] 211 | /// Amend this existing commit with all non-`None` values 212 | /// 213 | /// This creates a new commit that is exactly the same as the old commit, 214 | /// except that any non-`None` values will be updated. The new commit has 215 | /// the same parents as the old commit. 216 | /// 217 | /// For information about `update_ref`, see [`Repository::commit`]. 218 | /// 219 | /// [`Repository::commit`]: struct.Repository.html#method.commit 220 | pub fn amend( 221 | &self, 222 | update_ref: Option<&str>, 223 | author: Option<&Signature>, 224 | committer: Option<&Signature>, 225 | message_encoding: Option<&str>, 226 | message: Option<&str>, 227 | tree: Option<&Tree>, 228 | ) -> Result { 229 | self 230 | .inner 231 | .amend( 232 | update_ref, 233 | author.map(|s| &*s.inner), 234 | committer.map(|s| &*s.inner), 235 | message_encoding, 236 | message, 237 | tree.map(|s| &*s.inner()), 238 | ) 239 | .map(|oid| oid.to_string()) 240 | .convert("Amend commit failed") 241 | } 242 | 243 | #[napi] 244 | /// Get the number of parents of this commit. 245 | /// 246 | /// Use the `parents` iterator to return an iterator over all parents. 247 | pub fn parent_count(&self) -> usize { 248 | self.inner.parent_count() 249 | } 250 | 251 | #[napi] 252 | /// Get the specified parent of the commit. 253 | /// 254 | /// Use the `parents` iterator to return an iterator over all parents. 255 | pub fn parent(&self, i: u32) -> Result { 256 | Ok(Self { 257 | inner: CommitInner::Commit( 258 | self 259 | .inner 260 | .parent(i as usize) 261 | .convert("Find parent commit failed")?, 262 | ), 263 | }) 264 | } 265 | 266 | #[napi] 267 | /// Get the specified parent id of the commit. 268 | /// 269 | /// This is different from `parent`, which will attempt to load the 270 | /// parent commit from the ODB. 271 | /// 272 | /// Use the `parent_ids` iterator to return an iterator over all parents. 273 | pub fn parent_id(&self, i: u32) -> Result { 274 | Ok( 275 | self 276 | .inner 277 | .parent_id(i as usize) 278 | .convert("Find parent commit failed")? 279 | .to_string(), 280 | ) 281 | } 282 | 283 | #[napi] 284 | /// Casts this Commit to be usable as an `Object` 285 | pub fn as_object(&self) -> crate::object::GitObject { 286 | crate::object::GitObject { 287 | inner: ObjectParent::Object(self.inner.as_object().clone()), 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/deltas.rs: -------------------------------------------------------------------------------- 1 | use napi::{bindgen_prelude::*, JsString}; 2 | use napi_derive::napi; 3 | 4 | use crate::util::path_to_javascript_string; 5 | 6 | #[napi] 7 | #[repr(u32)] 8 | pub enum DiffFlags { 9 | /// File(s) treated as binary data. 10 | /// 1 << 0 11 | Binary = 1, 12 | /// File(s) treated as text data. 13 | /// 1 << 1 14 | NotBinary = 2, 15 | /// `id` value is known correct. 16 | /// 1 << 2 17 | ValidId = 4, 18 | /// File exists at this side of the delta. 19 | /// 1 << 3 20 | Exists = 8, 21 | } 22 | 23 | impl From for git2::DiffFlags { 24 | fn from(value: DiffFlags) -> Self { 25 | match value { 26 | DiffFlags::Binary => git2::DiffFlags::BINARY, 27 | DiffFlags::NotBinary => git2::DiffFlags::NOT_BINARY, 28 | DiffFlags::ValidId => git2::DiffFlags::VALID_ID, 29 | DiffFlags::Exists => git2::DiffFlags::EXISTS, 30 | } 31 | } 32 | } 33 | 34 | impl From for DiffFlags { 35 | fn from(value: git2::DiffFlags) -> Self { 36 | match value { 37 | git2::DiffFlags::BINARY => DiffFlags::Binary, 38 | git2::DiffFlags::NOT_BINARY => DiffFlags::NotBinary, 39 | git2::DiffFlags::VALID_ID => DiffFlags::ValidId, 40 | git2::DiffFlags::EXISTS => DiffFlags::Exists, 41 | _ => DiffFlags::Binary, 42 | } 43 | } 44 | } 45 | 46 | #[napi] 47 | /// Valid modes for index and tree entries. 48 | pub enum FileMode { 49 | /// Unreadable 50 | Unreadable, 51 | /// Tree 52 | Tree, 53 | /// Blob 54 | Blob, 55 | /// Group writable blob. Obsolete mode kept for compatibility reasons 56 | BlobGroupWritable, 57 | /// Blob executable 58 | BlobExecutable, 59 | /// Link 60 | Link, 61 | /// Commit 62 | Commit, 63 | } 64 | 65 | impl From for FileMode { 66 | fn from(value: git2::FileMode) -> Self { 67 | match value { 68 | git2::FileMode::Unreadable => FileMode::Unreadable, 69 | git2::FileMode::Tree => FileMode::Tree, 70 | git2::FileMode::Blob => FileMode::Blob, 71 | git2::FileMode::BlobGroupWritable => FileMode::BlobGroupWritable, 72 | git2::FileMode::BlobExecutable => FileMode::BlobExecutable, 73 | git2::FileMode::Link => FileMode::Link, 74 | git2::FileMode::Commit => FileMode::Commit, 75 | } 76 | } 77 | } 78 | 79 | #[napi(iterator)] 80 | /// An iterator over the diffs in a delta 81 | pub struct Deltas { 82 | pub(crate) inner: SharedReference>, 83 | } 84 | 85 | #[napi] 86 | impl Generator for Deltas { 87 | type Yield = DiffDelta; 88 | type Next = (); 89 | type Return = (); 90 | 91 | fn next(&mut self, _value: Option<()>) -> Option { 92 | self.inner.next().map(|delta| DiffDelta { inner: delta }) 93 | } 94 | } 95 | 96 | #[napi] 97 | pub struct DiffDelta { 98 | pub(crate) inner: git2::DiffDelta<'static>, 99 | } 100 | 101 | #[napi] 102 | impl DiffDelta { 103 | #[napi] 104 | /// Returns the flags on the delta. 105 | /// 106 | /// For more information, see `DiffFlags`'s documentation. 107 | pub fn flags(&self) -> DiffFlags { 108 | self.inner.flags().into() 109 | } 110 | 111 | #[napi] 112 | /// Returns the number of files in this delta. 113 | pub fn num_files(&self) -> u32 { 114 | self.inner.nfiles() as u32 115 | } 116 | 117 | #[napi] 118 | /// Returns the status of this entry 119 | pub fn status(&self) -> Delta { 120 | self.inner.status().into() 121 | } 122 | 123 | #[napi] 124 | /// Return the file which represents the "from" side of the diff. 125 | /// 126 | /// What side this means depends on the function that was used to generate 127 | /// the diff and will be documented on the function itself. 128 | pub fn old_file(&self) -> DiffFile { 129 | DiffFile { 130 | inner: self.inner.old_file(), 131 | } 132 | } 133 | 134 | #[napi] 135 | /// Return the file which represents the "to" side of the diff. 136 | /// 137 | /// What side this means depends on the function that was used to generate 138 | /// the diff and will be documented on the function itself. 139 | pub fn new_file(&self) -> DiffFile { 140 | DiffFile { 141 | inner: self.inner.new_file(), 142 | } 143 | } 144 | } 145 | 146 | #[napi] 147 | pub enum Delta { 148 | /// No changes 149 | Unmodified, 150 | /// Entry does not exist in old version 151 | Added, 152 | /// Entry does not exist in new version 153 | Deleted, 154 | /// Entry content changed between old and new 155 | Modified, 156 | /// Entry was renamed between old and new 157 | Renamed, 158 | /// Entry was copied from another old entry 159 | Copied, 160 | /// Entry is ignored item in workdir 161 | Ignored, 162 | /// Entry is untracked item in workdir 163 | Untracked, 164 | /// Type of entry changed between old and new 165 | Typechange, 166 | /// Entry is unreadable 167 | Unreadable, 168 | /// Entry in the index is conflicted 169 | Conflicted, 170 | } 171 | 172 | impl From for Delta { 173 | fn from(delta: git2::Delta) -> Self { 174 | match delta { 175 | git2::Delta::Unmodified => Delta::Unmodified, 176 | git2::Delta::Added => Delta::Added, 177 | git2::Delta::Deleted => Delta::Deleted, 178 | git2::Delta::Modified => Delta::Modified, 179 | git2::Delta::Renamed => Delta::Renamed, 180 | git2::Delta::Copied => Delta::Copied, 181 | git2::Delta::Ignored => Delta::Ignored, 182 | git2::Delta::Untracked => Delta::Untracked, 183 | git2::Delta::Typechange => Delta::Typechange, 184 | git2::Delta::Unreadable => Delta::Unreadable, 185 | git2::Delta::Conflicted => Delta::Conflicted, 186 | } 187 | } 188 | } 189 | 190 | #[napi] 191 | pub struct DiffFile { 192 | pub(crate) inner: git2::DiffFile<'static>, 193 | } 194 | 195 | #[napi] 196 | impl DiffFile { 197 | #[napi] 198 | /// Returns the Oid of this item. 199 | /// 200 | /// If this entry represents an absent side of a diff (e.g. the `old_file` 201 | /// of a `Added` delta), then the oid returned will be zeroes. 202 | pub fn id(&self) -> String { 203 | self.inner.id().to_string() 204 | } 205 | 206 | #[napi] 207 | /// Returns the path, in bytes, of the entry relative to the working 208 | /// directory of the repository. 209 | pub fn path(&self, env: Env) -> Option { 210 | self 211 | .inner 212 | .path() 213 | .and_then(|p| path_to_javascript_string(&env, p).ok()) 214 | } 215 | 216 | #[napi] 217 | /// Returns the size of this entry, in bytes 218 | pub fn size(&self) -> u64 { 219 | self.inner.size() 220 | } 221 | 222 | #[napi] 223 | /// Returns `true` if file(s) are treated as binary data. 224 | pub fn is_binary(&self) -> bool { 225 | self.inner.is_binary() 226 | } 227 | 228 | #[napi] 229 | /// Returns `true` if file(s) are treated as text data. 230 | pub fn is_not_binary(&self) -> bool { 231 | self.inner.is_not_binary() 232 | } 233 | 234 | #[napi] 235 | /// Returns `true` if `id` value is known correct. 236 | pub fn is_valid_id(&self) -> bool { 237 | self.inner.is_valid_id() 238 | } 239 | 240 | #[napi] 241 | /// Returns `true` if file exists at this side of the delta. 242 | pub fn exists(&self) -> bool { 243 | self.inner.exists() 244 | } 245 | 246 | #[napi] 247 | /// Returns file mode. 248 | pub fn mode(&self) -> FileMode { 249 | self.inner.mode().into() 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/diff.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use napi::bindgen_prelude::*; 4 | use napi_derive::napi; 5 | 6 | use crate::deltas::Deltas; 7 | use crate::error::IntoNapiError; 8 | 9 | #[napi(object)] 10 | #[derive(Debug, Default)] 11 | pub struct DiffOptions { 12 | /// When generating output, include the names of unmodified files if they 13 | /// are included in the `Diff`. Normally these are skipped in the formats 14 | /// that list files (e.g. name-only, name-status, raw). Even with this these 15 | /// will not be included in the patch format. 16 | pub show_unmodified: Option, 17 | } 18 | 19 | #[napi] 20 | pub struct Diff { 21 | pub(crate) inner: SharedReference>, 22 | } 23 | 24 | #[napi] 25 | impl Diff { 26 | #[napi] 27 | /// Merge one diff into another. 28 | /// 29 | /// This merges items from the "from" list into the "self" list. The 30 | /// resulting diff will have all items that appear in either list. 31 | /// If an item appears in both lists, then it will be "merged" to appear 32 | /// as if the old version was from the "onto" list and the new version 33 | /// is from the "from" list (with the exception that if the item has a 34 | /// pending DELETE in the middle, then it will show as deleted). 35 | pub fn merge(&mut self, diff: &Diff) -> Result<()> { 36 | self 37 | .inner 38 | .merge(diff.inner.deref()) 39 | .convert_without_message() 40 | } 41 | 42 | #[napi] 43 | /// Returns an iterator over the deltas in this diff. 44 | pub fn deltas(&self, env: Env, self_ref: Reference) -> Result { 45 | Ok(Deltas { 46 | inner: self_ref.share_with(env, |diff| Ok(diff.inner.deltas()))?, 47 | }) 48 | } 49 | 50 | #[napi] 51 | /// Check if deltas are sorted case sensitively or insensitively. 52 | pub fn is_sorted_icase(&self) -> bool { 53 | self.inner.is_sorted_icase() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | pub(crate) trait IntoNapiError: Sized { 2 | type Associate; 3 | 4 | fn convert>(self, msg: S) -> Result; 5 | 6 | fn convert_without_message(self) -> Result; 7 | } 8 | 9 | impl IntoNapiError for Result { 10 | type Associate = T; 11 | 12 | #[inline] 13 | fn convert>(self, msg: S) -> Result { 14 | self.map_err(|err| { 15 | napi::Error::new( 16 | napi::Status::GenericFailure, 17 | format!("{}: {}", msg.as_ref(), err), 18 | ) 19 | }) 20 | } 21 | 22 | #[inline] 23 | fn convert_without_message(self) -> Result { 24 | self.map_err(|err| { 25 | napi::Error::new( 26 | napi::Status::GenericFailure, 27 | format!("libgit2 error: {err}"), 28 | ) 29 | }) 30 | } 31 | } 32 | 33 | pub trait NotNullError { 34 | type Associate; 35 | 36 | fn expect_not_null(self, msg: String) -> Result; 37 | } 38 | 39 | impl NotNullError for Option { 40 | type Associate = T; 41 | 42 | #[inline] 43 | fn expect_not_null(self, msg: String) -> Result { 44 | self.ok_or_else(|| napi::Error::new(napi::Status::GenericFailure, msg)) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | 3 | pub mod blob; 4 | pub mod commit; 5 | pub mod deltas; 6 | pub mod diff; 7 | mod error; 8 | pub mod object; 9 | pub mod reference; 10 | pub mod remote; 11 | pub mod repo; 12 | pub mod repo_builder; 13 | pub mod rev_walk; 14 | pub mod signature; 15 | pub mod tag; 16 | pub mod tree; 17 | pub(crate) mod util; 18 | -------------------------------------------------------------------------------- /src/object.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use napi::bindgen_prelude::*; 4 | use napi_derive::napi; 5 | 6 | use crate::{ 7 | blob::{Blob, BlobParent}, 8 | error::IntoNapiError, 9 | repo::Repository, 10 | }; 11 | 12 | #[napi] 13 | pub enum ObjectType { 14 | /// Any kind of git object 15 | Any, 16 | /// An object which corresponds to a git commit 17 | Commit, 18 | /// An object which corresponds to a git tree 19 | Tree, 20 | /// An object which corresponds to a git blob 21 | Blob, 22 | /// An object which corresponds to a git tag 23 | Tag, 24 | } 25 | 26 | impl From for ObjectType { 27 | fn from(value: git2::ObjectType) -> Self { 28 | match value { 29 | git2::ObjectType::Any => ObjectType::Any, 30 | git2::ObjectType::Commit => ObjectType::Commit, 31 | git2::ObjectType::Tree => ObjectType::Tree, 32 | git2::ObjectType::Blob => ObjectType::Blob, 33 | git2::ObjectType::Tag => ObjectType::Tag, 34 | } 35 | } 36 | } 37 | 38 | impl From for git2::ObjectType { 39 | fn from(value: ObjectType) -> Self { 40 | match value { 41 | ObjectType::Any => git2::ObjectType::Any, 42 | ObjectType::Commit => git2::ObjectType::Commit, 43 | ObjectType::Tree => git2::ObjectType::Tree, 44 | ObjectType::Blob => git2::ObjectType::Blob, 45 | ObjectType::Tag => git2::ObjectType::Tag, 46 | } 47 | } 48 | } 49 | 50 | pub(crate) enum ObjectParent { 51 | Repository(SharedReference>), 52 | Object(git2::Object<'static>), 53 | } 54 | 55 | impl Deref for ObjectParent { 56 | type Target = git2::Object<'static>; 57 | 58 | fn deref(&self) -> &git2::Object<'static> { 59 | match self { 60 | ObjectParent::Repository(parent) => parent.deref(), 61 | ObjectParent::Object(parent) => &parent, 62 | } 63 | } 64 | } 65 | 66 | #[napi] 67 | pub struct GitObject { 68 | pub(crate) inner: ObjectParent, 69 | } 70 | 71 | #[napi] 72 | impl GitObject { 73 | #[napi] 74 | /// Get the id (SHA1) of a repository object 75 | pub fn id(&self) -> String { 76 | self.inner.id().to_string() 77 | } 78 | 79 | #[napi] 80 | /// Get the type of the object. 81 | pub fn kind(&self) -> Option { 82 | self.inner.kind().map(|k| k.into()) 83 | } 84 | 85 | #[napi] 86 | /// Recursively peel an object until an object of the specified type is met. 87 | /// 88 | /// If you pass `Any` as the target type, then the object will be 89 | /// peeled until the type changes (e.g. a tag will be chased until the 90 | /// referenced object is no longer a tag). 91 | pub fn peel(&self, kind: ObjectType) -> Result { 92 | Ok(GitObject { 93 | inner: ObjectParent::Object(self.inner.peel(kind.into()).convert("Peel object failed")?), 94 | }) 95 | } 96 | 97 | #[napi] 98 | /// Recursively peel an object until a blob is found 99 | pub fn peel_to_blob(&self, env: Env, self_ref: Reference) -> Result { 100 | let blob = self_ref.share_with(env, |obj| { 101 | obj.inner.peel_to_blob().convert_without_message() 102 | })?; 103 | Ok(Blob { 104 | inner: BlobParent::GitObject(blob), 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/reference.rs: -------------------------------------------------------------------------------- 1 | use napi::bindgen_prelude::*; 2 | use napi_derive::napi; 3 | 4 | use crate::error::IntoNapiError; 5 | use crate::tree::{Tree, TreeParent}; 6 | 7 | #[napi] 8 | pub struct Reference { 9 | pub(crate) inner: 10 | napi::bindgen_prelude::SharedReference>, 11 | } 12 | 13 | #[napi] 14 | #[derive(PartialEq, Eq, Debug)] 15 | /// An enumeration of all possible kinds of references. 16 | pub enum ReferenceType { 17 | /// A reference which points at an object id. 18 | Direct, 19 | 20 | /// A reference which points at another reference. 21 | Symbolic, 22 | 23 | Unknown, 24 | } 25 | 26 | #[napi] 27 | impl Reference { 28 | #[napi] 29 | /// Ensure the reference name is well-formed. 30 | /// 31 | /// Validation is performed as if [`ReferenceFormat::ALLOW_ONELEVEL`] 32 | /// was given to [`Reference.normalize_name`]. No normalization is 33 | /// performed, however. 34 | /// 35 | /// ```ts 36 | /// import { Reference } from '@napi-rs/simple-git' 37 | /// 38 | /// console.assert(Reference.is_valid_name("HEAD")); 39 | /// console.assert(Reference.is_valid_name("refs/heads/main")); 40 | /// 41 | /// // But: 42 | /// console.assert(!Reference.is_valid_name("main")); 43 | /// console.assert(!Reference.is_valid_name("refs/heads/*")); 44 | /// console.assert(!Reference.is_valid_name("foo//bar")); 45 | /// ``` 46 | pub fn is_valid_name(name: String) -> bool { 47 | git2::Reference::is_valid_name(&name) 48 | } 49 | 50 | #[napi] 51 | /// Check if a reference is a local branch. 52 | pub fn is_branch(&self) -> Result { 53 | Ok(self.inner.is_branch()) 54 | } 55 | 56 | #[napi] 57 | /// Check if a reference is a note. 58 | pub fn is_note(&self) -> Result { 59 | Ok(self.inner.is_note()) 60 | } 61 | 62 | #[napi] 63 | /// Check if a reference is a remote tracking branch 64 | pub fn is_remote(&self) -> Result { 65 | Ok(self.inner.is_remote()) 66 | } 67 | 68 | #[napi] 69 | /// Check if a reference is a tag 70 | pub fn is_tag(&self) -> Result { 71 | Ok(self.inner.is_tag()) 72 | } 73 | 74 | #[napi] 75 | pub fn kind(&self) -> Result { 76 | match self.inner.kind() { 77 | Some(git2::ReferenceType::Symbolic) => Ok(ReferenceType::Symbolic), 78 | Some(git2::ReferenceType::Direct) => Ok(ReferenceType::Direct), 79 | _ => Ok(ReferenceType::Unknown), 80 | } 81 | } 82 | 83 | #[napi] 84 | /// Get the full name of a reference. 85 | /// 86 | /// Returns `None` if the name is not valid utf-8. 87 | pub fn name(&self) -> Option { 88 | self.inner.name().map(|s| s.to_string()) 89 | } 90 | 91 | #[napi] 92 | /// Get the full shorthand of a reference. 93 | /// 94 | /// This will transform the reference name into a name "human-readable" 95 | /// version. If no shortname is appropriate, it will return the full name. 96 | /// 97 | /// Returns `None` if the shorthand is not valid utf-8. 98 | pub fn shorthand(&self) -> Option { 99 | self.inner.shorthand().map(|s| s.to_string()) 100 | } 101 | 102 | #[napi] 103 | /// Get the OID pointed to by a direct reference. 104 | /// 105 | /// Only available if the reference is direct (i.e. an object id reference, 106 | /// not a symbolic one). 107 | pub fn target(&self) -> Option { 108 | self.inner.target().map(|oid| oid.to_string()) 109 | } 110 | 111 | #[napi] 112 | /// Return the peeled OID target of this reference. 113 | /// 114 | /// This peeled OID only applies to direct references that point to a hard 115 | /// Tag object: it is the result of peeling such Tag. 116 | pub fn target_peel(&self) -> Option { 117 | self.inner.target_peel().map(|oid| oid.to_string()) 118 | } 119 | 120 | #[napi] 121 | /// Peel a reference to a tree 122 | /// 123 | /// This method recursively peels the reference until it reaches 124 | /// a tree. 125 | pub fn peel_to_tree( 126 | &self, 127 | env: Env, 128 | self_ref: napi::bindgen_prelude::Reference, 129 | ) -> Result { 130 | Ok(Tree { 131 | inner: TreeParent::Reference(self_ref.share_with(env, |reference| { 132 | reference.inner.peel_to_tree().convert_without_message() 133 | })?), 134 | }) 135 | } 136 | 137 | #[napi] 138 | /// Get full name to the reference pointed to by a symbolic reference. 139 | /// 140 | /// May return `None` if the reference is either not symbolic or not a 141 | /// valid utf-8 string. 142 | pub fn symbolic_target(&self) -> Option { 143 | self.inner.symbolic_target().map(|s| s.to_owned()) 144 | } 145 | 146 | #[napi] 147 | /// Resolve a symbolic reference to a direct reference. 148 | /// 149 | /// This method iteratively peels a symbolic reference until it resolves to 150 | /// a direct reference to an OID. 151 | /// 152 | /// If a direct reference is passed as an argument, a copy of that 153 | /// reference is returned. 154 | pub fn resolve(&self, env: Env) -> Result { 155 | let shared = self 156 | .inner 157 | .clone(env)? 158 | .share_with(env, |r| r.resolve().convert_without_message())?; 159 | Ok(Self { inner: shared }) 160 | } 161 | 162 | #[napi] 163 | /// Rename an existing reference. 164 | /// 165 | /// This method works for both direct and symbolic references. 166 | /// 167 | /// If the force flag is not enabled, and there's already a reference with 168 | /// the given name, the renaming will fail. 169 | pub fn rename( 170 | &mut self, 171 | env: Env, 172 | new_name: String, 173 | force: bool, 174 | msg: String, 175 | ) -> Result { 176 | let inner = self.inner.clone(env)?.share_with(env, |r| { 177 | r.rename(&new_name, force, &msg).convert_without_message() 178 | })?; 179 | Ok(Self { inner }) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/remote.rs: -------------------------------------------------------------------------------- 1 | use std::{mem, path::Path}; 2 | 3 | use git2::{ErrorClass, ErrorCode}; 4 | use napi::{bindgen_prelude::*, Error, NapiRaw, Status}; 5 | use napi_derive::napi; 6 | 7 | use crate::error::IntoNapiError; 8 | 9 | #[napi] 10 | /// An enumeration of the possible directions for a remote. 11 | pub enum Direction { 12 | /// Data will be fetched (read) from this remote. 13 | Fetch, 14 | /// Data will be pushed (written) to this remote. 15 | Push, 16 | } 17 | 18 | impl From for git2::Direction { 19 | fn from(value: Direction) -> Self { 20 | match value { 21 | Direction::Fetch => git2::Direction::Fetch, 22 | Direction::Push => git2::Direction::Push, 23 | } 24 | } 25 | } 26 | 27 | #[napi] 28 | /// Configuration for how pruning is done on a fetch 29 | pub enum FetchPrune { 30 | /// Use the setting from the configuration 31 | Unspecified, 32 | /// Force pruning on 33 | On, 34 | /// Force pruning off 35 | Off, 36 | } 37 | 38 | impl From for git2::FetchPrune { 39 | fn from(value: FetchPrune) -> Self { 40 | match value { 41 | FetchPrune::Unspecified => git2::FetchPrune::Unspecified, 42 | FetchPrune::On => git2::FetchPrune::On, 43 | FetchPrune::Off => git2::FetchPrune::Off, 44 | } 45 | } 46 | } 47 | 48 | #[napi] 49 | /// Automatic tag following options. 50 | pub enum AutotagOption { 51 | /// Use the setting from the remote's configuration 52 | Unspecified, 53 | /// Ask the server for tags pointing to objects we're already downloading 54 | Auto, 55 | /// Don't ask for any tags beyond the refspecs 56 | None, 57 | /// Ask for all the tags 58 | All, 59 | } 60 | 61 | impl From for git2::AutotagOption { 62 | fn from(value: AutotagOption) -> Self { 63 | match value { 64 | AutotagOption::Unspecified => git2::AutotagOption::Unspecified, 65 | AutotagOption::Auto => git2::AutotagOption::Auto, 66 | AutotagOption::None => git2::AutotagOption::None, 67 | AutotagOption::All => git2::AutotagOption::All, 68 | } 69 | } 70 | } 71 | 72 | #[napi] 73 | /// Remote redirection settings; whether redirects to another host are 74 | /// permitted. 75 | /// 76 | /// By default, git will follow a redirect on the initial request 77 | /// (`/info/refs`), but not subsequent requests. 78 | pub enum RemoteRedirect { 79 | /// Do not follow any off-site redirects at any stage of the fetch or push. 80 | None, 81 | /// Allow off-site redirects only upon the initial request. This is the 82 | /// default. 83 | Initial, 84 | /// Allow redirects at any stage in the fetch or push. 85 | All, 86 | } 87 | 88 | impl From for git2::RemoteRedirect { 89 | fn from(value: RemoteRedirect) -> Self { 90 | match value { 91 | RemoteRedirect::None => git2::RemoteRedirect::None, 92 | RemoteRedirect::Initial => git2::RemoteRedirect::Initial, 93 | RemoteRedirect::All => git2::RemoteRedirect::All, 94 | } 95 | } 96 | } 97 | 98 | #[napi] 99 | /// Types of credentials that can be requested by a credential callback. 100 | pub enum CredentialType { 101 | /// 1 << 0 102 | UserPassPlaintext = 1, 103 | /// 1 << 1 104 | SshKey = 2, 105 | /// 1 << 6 106 | SshMemory = 64, 107 | /// 1 << 2 108 | SshCustom = 4, 109 | /// 1 << 3 110 | Default = 8, 111 | /// 1 << 4 112 | SshInteractive = 16, 113 | /// 1 << 5 114 | Username = 32, 115 | } 116 | 117 | impl From for CredentialType { 118 | fn from(value: libgit2_sys::git_credtype_t) -> Self { 119 | match value { 120 | libgit2_sys::GIT_CREDTYPE_USERPASS_PLAINTEXT => CredentialType::UserPassPlaintext, 121 | libgit2_sys::GIT_CREDTYPE_SSH_KEY => CredentialType::SshKey, 122 | libgit2_sys::GIT_CREDTYPE_SSH_MEMORY => CredentialType::SshMemory, 123 | libgit2_sys::GIT_CREDTYPE_SSH_CUSTOM => CredentialType::SshCustom, 124 | libgit2_sys::GIT_CREDTYPE_DEFAULT => CredentialType::Default, 125 | libgit2_sys::GIT_CREDTYPE_SSH_INTERACTIVE => CredentialType::SshInteractive, 126 | libgit2_sys::GIT_CREDTYPE_USERNAME => CredentialType::Username, 127 | _ => CredentialType::Default, 128 | } 129 | } 130 | } 131 | 132 | impl From for CredentialType { 133 | fn from(value: git2::CredentialType) -> Self { 134 | match value { 135 | git2::CredentialType::USER_PASS_PLAINTEXT => CredentialType::UserPassPlaintext, 136 | git2::CredentialType::SSH_KEY => CredentialType::SshKey, 137 | git2::CredentialType::SSH_MEMORY => CredentialType::SshMemory, 138 | git2::CredentialType::SSH_CUSTOM => CredentialType::SshCustom, 139 | git2::CredentialType::DEFAULT => CredentialType::Default, 140 | git2::CredentialType::SSH_INTERACTIVE => CredentialType::SshInteractive, 141 | git2::CredentialType::USERNAME => CredentialType::Username, 142 | _ => CredentialType::Default, 143 | } 144 | } 145 | } 146 | 147 | impl From for git2::CredentialType { 148 | fn from(value: CredentialType) -> Self { 149 | match value { 150 | CredentialType::UserPassPlaintext => git2::CredentialType::USER_PASS_PLAINTEXT, 151 | CredentialType::SshKey => git2::CredentialType::SSH_KEY, 152 | CredentialType::SshMemory => git2::CredentialType::SSH_MEMORY, 153 | CredentialType::SshCustom => git2::CredentialType::SSH_CUSTOM, 154 | CredentialType::Default => git2::CredentialType::DEFAULT, 155 | CredentialType::SshInteractive => git2::CredentialType::SSH_INTERACTIVE, 156 | CredentialType::Username => git2::CredentialType::USERNAME, 157 | } 158 | } 159 | } 160 | 161 | #[napi(object)] 162 | pub struct CredInfo { 163 | pub cred_type: CredentialType, 164 | pub url: String, 165 | pub username: String, 166 | } 167 | 168 | #[napi] 169 | #[repr(u32)] 170 | pub enum RemoteUpdateFlags { 171 | UpdateFetchHead = 1, 172 | ReportUnchanged = 2, 173 | } 174 | 175 | impl From for git2::RemoteUpdateFlags { 176 | fn from(value: RemoteUpdateFlags) -> Self { 177 | match value { 178 | RemoteUpdateFlags::UpdateFetchHead => git2::RemoteUpdateFlags::UPDATE_FETCHHEAD, 179 | RemoteUpdateFlags::ReportUnchanged => git2::RemoteUpdateFlags::REPORT_UNCHANGED, 180 | } 181 | } 182 | } 183 | 184 | #[napi] 185 | pub struct Remote { 186 | pub(crate) inner: SharedReference>, 187 | } 188 | 189 | #[napi] 190 | impl Remote { 191 | #[napi] 192 | /// Ensure the remote name is well-formed. 193 | pub fn is_valid_name(name: String) -> bool { 194 | git2::Remote::is_valid_name(&name) 195 | } 196 | 197 | #[napi] 198 | /// Get the remote's name. 199 | /// 200 | /// Returns `None` if this remote has not yet been named or if the name is 201 | /// not valid utf-8 202 | pub fn name(&self) -> Option<&str> { 203 | self.inner.name() 204 | } 205 | 206 | #[napi] 207 | /// Get the remote's url. 208 | /// 209 | /// Returns `None` if the url is not valid utf-8 210 | pub fn url(&self) -> Option<&str> { 211 | self.inner.url() 212 | } 213 | 214 | #[napi] 215 | /// Get the remote's pushurl. 216 | /// 217 | /// Returns `None` if the pushurl is not valid utf-8 218 | pub fn pushurl(&self) -> Option<&str> { 219 | self.inner.pushurl() 220 | } 221 | 222 | #[napi] 223 | /// Get the remote's default branch. 224 | /// 225 | /// The remote (or more exactly its transport) must have connected to the 226 | /// remote repository. This default branch is available as soon as the 227 | /// connection to the remote is initiated and it remains available after 228 | /// disconnecting. 229 | pub fn default_branch(&self) -> Result { 230 | self 231 | .inner 232 | .default_branch() 233 | .convert("Get the default branch of Remote failed") 234 | .and_then(|b| { 235 | b.as_str().map(|name| name.to_owned()).ok_or_else(|| { 236 | Error::new( 237 | Status::GenericFailure, 238 | "Default branch name contains non-utf-8 characters".to_string(), 239 | ) 240 | }) 241 | }) 242 | } 243 | 244 | #[napi] 245 | /// Open a connection to a remote. 246 | pub fn connect(&mut self, dir: Direction) -> Result<()> { 247 | self.inner.connect(dir.into()).convert_without_message() 248 | } 249 | 250 | #[napi] 251 | /// Check whether the remote is connected 252 | pub fn connected(&mut self) -> bool { 253 | self.inner.connected() 254 | } 255 | 256 | #[napi] 257 | /// Disconnect from the remote 258 | pub fn disconnect(&mut self) -> Result<()> { 259 | self.inner.disconnect().convert_without_message() 260 | } 261 | 262 | #[napi] 263 | /// Cancel the operation 264 | /// 265 | /// At certain points in its operation, the network code checks whether the 266 | /// operation has been cancelled and if so stops the operation. 267 | pub fn stop(&mut self) -> Result<()> { 268 | self.inner.stop().convert_without_message() 269 | } 270 | 271 | #[napi] 272 | /// Download new data and update tips 273 | /// 274 | /// Convenience function to connect to a remote, download the data, 275 | /// disconnect and update the remote-tracking branches. 276 | /// 277 | pub fn fetch( 278 | &mut self, 279 | refspecs: Vec, 280 | fetch_options: Option<&mut FetchOptions>, 281 | ) -> Result<()> { 282 | let mut default_fetch_options = git2::FetchOptions::default(); 283 | let mut options = fetch_options 284 | .map(|o| { 285 | std::mem::swap(&mut o.inner, &mut default_fetch_options); 286 | default_fetch_options 287 | }) 288 | .unwrap_or_default(); 289 | self 290 | .inner 291 | .fetch(refspecs.as_slice(), Some(&mut options), None) 292 | .convert_without_message() 293 | } 294 | 295 | #[napi] 296 | /// Update the tips to the new state 297 | pub fn update_tips( 298 | &mut self, 299 | update_fetchhead: RemoteUpdateFlags, 300 | download_tags: AutotagOption, 301 | mut callbacks: Option<&mut RemoteCallbacks>, 302 | msg: Option, 303 | ) -> Result<()> { 304 | let callbacks = callbacks.as_mut().map(|o| &mut o.inner); 305 | self 306 | .inner 307 | .update_tips( 308 | callbacks, 309 | update_fetchhead.into(), 310 | download_tags.into(), 311 | msg.as_deref(), 312 | ) 313 | .convert_without_message() 314 | } 315 | } 316 | 317 | #[napi] 318 | pub struct RemoteCallbacks { 319 | inner: git2::RemoteCallbacks<'static>, 320 | used: bool, 321 | } 322 | 323 | #[napi] 324 | impl RemoteCallbacks { 325 | #[napi(constructor)] 326 | #[allow(clippy::new_without_default)] 327 | pub fn new() -> RemoteCallbacks { 328 | RemoteCallbacks { 329 | inner: git2::RemoteCallbacks::new(), 330 | used: false, 331 | } 332 | } 333 | 334 | #[napi] 335 | /// The callback through which to fetch credentials if required. 336 | /// 337 | /// # Example 338 | /// 339 | /// Prepare a callback to authenticate using the `$HOME/.ssh/id_rsa` SSH key, and 340 | /// extracting the username from the URL (i.e. git@github.com:rust-lang/git2-rs.git): 341 | /// 342 | /// ```js 343 | /// import { join } from 'node:path' 344 | /// import { homedir } from 'node:os' 345 | /// 346 | /// import { Cred, FetchOptions, RemoteCallbacks, RepoBuilder, credTypeContains } from '@napi-rs/simple-git' 347 | /// 348 | /// const builder = new RepoBuilder() 349 | 350 | /// const remoteCallbacks = new RemoteCallbacks() 351 | /// .credentials((cred) => { 352 | /// return Cred.sshKey(cred.username, null, join(homedir(), '.ssh', 'id_rsa'), null) 353 | /// }) 354 | /// 355 | /// const fetchOptions = new FetchOptions().depth(0).remoteCallback(remoteCallbacks) 356 | /// 357 | /// const repo = builder.branch('master') 358 | /// .fetchOptions(fetchOptions) 359 | /// .clone("git@github.com:rust-lang/git2-rs.git", "git2-rs") 360 | /// ``` 361 | pub fn credentials( 362 | &mut self, 363 | env: Env, 364 | callback: Function>, 365 | ) -> Result<&Self> { 366 | let func_ref = callback.create_ref()?; 367 | self 368 | .inner 369 | .credentials(move |url: &str, username_from_url, cred| { 370 | func_ref 371 | .borrow_back(&env) 372 | .and_then(|callback| { 373 | callback.call(CredInfo { 374 | cred_type: cred.into(), 375 | url: url.to_string(), 376 | username: username_from_url.unwrap_or("git").to_string(), 377 | }) 378 | }) 379 | .map_err(|err| { 380 | git2::Error::new( 381 | ErrorCode::Auth, 382 | ErrorClass::Callback, 383 | format!("Call credentials callback failed {err}"), 384 | ) 385 | }) 386 | .and_then(|cred| { 387 | let mut cred: ClassInstance = unsafe { 388 | FromNapiValue::from_napi_value(env.raw(), cred.raw()).map_err(|err| { 389 | git2::Error::new( 390 | ErrorCode::Auth, 391 | ErrorClass::Callback, 392 | format!("Credential callback return value is not instance of Cred: {err}"), 393 | ) 394 | })? 395 | }; 396 | if cred.used { 397 | return Err(git2::Error::new( 398 | ErrorCode::Auth, 399 | ErrorClass::Callback, 400 | "Cred can only be used once", 401 | )); 402 | } 403 | let mut c = git2::Cred::default()?; 404 | mem::swap(&mut c, &mut cred.inner); 405 | cred.used = true; 406 | Ok(c) 407 | }) 408 | }); 409 | Ok(self) 410 | } 411 | 412 | #[napi] 413 | /// The callback through which progress is monitored. 414 | pub fn transfer_progress(&mut self, env: Env, callback: FunctionRef) -> &Self { 415 | self.inner.transfer_progress(move |p| { 416 | callback 417 | .borrow_back(&env) 418 | .and_then(|cb| cb.call(p.into())) 419 | .is_ok() 420 | }); 421 | self 422 | } 423 | 424 | #[napi(ts_args_type = "callback: (current: number, total: number, bytes: number) => void")] 425 | /// The callback through which progress of push transfer is monitored 426 | pub fn push_transfer_progress( 427 | &mut self, 428 | env: Env, 429 | callback: FunctionRef, 430 | ) -> &Self { 431 | self 432 | .inner 433 | .push_transfer_progress(move |current, total, bytes| { 434 | if let Err(err) = callback.borrow_back(&env).and_then(|cb| { 435 | cb.call(PushTransferProgress { 436 | current: current as u32, 437 | total: total as u32, 438 | bytes: bytes as u32, 439 | }) 440 | }) { 441 | eprintln!("Push transfer progress callback failed: {}", err); 442 | } 443 | }); 444 | self 445 | } 446 | } 447 | 448 | #[napi] 449 | pub struct FetchOptions { 450 | pub(crate) inner: git2::FetchOptions<'static>, 451 | pub(crate) used: bool, 452 | } 453 | 454 | #[napi] 455 | impl FetchOptions { 456 | #[napi(constructor)] 457 | #[allow(clippy::new_without_default)] 458 | pub fn new() -> FetchOptions { 459 | FetchOptions { 460 | inner: git2::FetchOptions::new(), 461 | used: false, 462 | } 463 | } 464 | 465 | #[napi] 466 | /// Set the callbacks to use for the fetch operation. 467 | pub fn remote_callback(&mut self, callback: &mut RemoteCallbacks) -> Result<&Self> { 468 | if callback.used { 469 | return Err(Error::new( 470 | Status::GenericFailure, 471 | "RemoteCallbacks can only be used once".to_string(), 472 | )); 473 | } 474 | let mut cbs = git2::RemoteCallbacks::default(); 475 | mem::swap(&mut cbs, &mut callback.inner); 476 | self.inner.remote_callbacks(cbs); 477 | callback.used = true; 478 | Ok(self) 479 | } 480 | 481 | #[napi] 482 | /// Set the proxy options to use for the fetch operation. 483 | pub fn proxy_options(&mut self, options: &mut ProxyOptions) -> Result<&Self> { 484 | if options.used { 485 | return Err(Error::new( 486 | Status::GenericFailure, 487 | "ProxyOptions can only be used once".to_string(), 488 | )); 489 | } 490 | let mut opts = git2::ProxyOptions::default(); 491 | mem::swap(&mut opts, &mut options.inner); 492 | self.inner.proxy_options(opts); 493 | options.used = true; 494 | Ok(self) 495 | } 496 | 497 | #[napi] 498 | /// Set whether to perform a prune after the fetch. 499 | pub fn prune(&mut self, prune: FetchPrune) -> &Self { 500 | self.inner.prune(prune.into()); 501 | self 502 | } 503 | 504 | #[napi] 505 | /// Set whether to write the results to FETCH_HEAD. 506 | /// 507 | /// Defaults to `true`. 508 | pub fn update_fetchhead(&mut self, update: bool) -> &Self { 509 | self.inner.update_fetchhead(update); 510 | self 511 | } 512 | 513 | #[napi] 514 | /// Set fetch depth, a value less or equal to 0 is interpreted as pull 515 | /// everything (effectively the same as not declaring a limit depth). 516 | 517 | // FIXME(blyxyas): We currently don't have a test for shallow functions 518 | // because libgit2 doesn't support local shallow clones. 519 | // https://github.com/rust-lang/git2-rs/pull/979#issuecomment-1716299900 520 | pub fn depth(&mut self, depth: i32) -> &Self { 521 | self.inner.depth(depth); 522 | self 523 | } 524 | 525 | #[napi] 526 | /// Set how to behave regarding tags on the remote, such as auto-downloading 527 | /// tags for objects we're downloading or downloading all of them. 528 | /// 529 | /// The default is to auto-follow tags. 530 | pub fn download_tags(&mut self, opt: AutotagOption) -> &Self { 531 | self.inner.download_tags(opt.into()); 532 | self 533 | } 534 | 535 | #[napi] 536 | /// Set remote redirection settings; whether redirects to another host are 537 | /// permitted. 538 | /// 539 | /// By default, git will follow a redirect on the initial request 540 | /// (`/info/refs`), but not subsequent requests. 541 | pub fn follow_redirects(&mut self, opt: RemoteRedirect) -> &Self { 542 | self.inner.follow_redirects(opt.into()); 543 | self 544 | } 545 | 546 | #[napi] 547 | /// Set extra headers for this fetch operation. 548 | pub fn custom_headers(&mut self, headers: Vec<&str>) -> &Self { 549 | self.inner.custom_headers(headers.as_slice()); 550 | self 551 | } 552 | } 553 | 554 | #[napi(object)] 555 | pub struct Progress { 556 | pub total_objects: u32, 557 | pub indexed_objects: u32, 558 | pub received_objects: u32, 559 | pub local_objects: u32, 560 | pub total_deltas: u32, 561 | pub indexed_deltas: u32, 562 | pub received_bytes: u32, 563 | } 564 | 565 | impl<'a> From> for Progress { 566 | fn from(progress: git2::Progress) -> Self { 567 | Progress { 568 | total_objects: progress.total_objects() as u32, 569 | indexed_objects: progress.indexed_objects() as u32, 570 | received_objects: progress.received_objects() as u32, 571 | local_objects: progress.local_objects() as u32, 572 | total_deltas: progress.total_deltas() as u32, 573 | indexed_deltas: progress.indexed_deltas() as u32, 574 | received_bytes: progress.received_bytes() as u32, 575 | } 576 | } 577 | } 578 | 579 | #[napi(object)] 580 | pub struct PushTransferProgress { 581 | pub current: u32, 582 | pub total: u32, 583 | pub bytes: u32, 584 | } 585 | 586 | #[napi] 587 | pub struct ProxyOptions { 588 | inner: git2::ProxyOptions<'static>, 589 | used: bool, 590 | } 591 | 592 | #[napi] 593 | impl ProxyOptions { 594 | #[napi(constructor)] 595 | #[allow(clippy::new_without_default)] 596 | pub fn new() -> ProxyOptions { 597 | ProxyOptions { 598 | inner: git2::ProxyOptions::new(), 599 | used: false, 600 | } 601 | } 602 | 603 | #[napi] 604 | /// Try to auto-detect the proxy from the git configuration. 605 | /// 606 | /// Note that this will override `url` specified before. 607 | pub fn auto(&mut self) -> &Self { 608 | self.inner.auto(); 609 | self 610 | } 611 | 612 | #[napi] 613 | /// Specify the exact URL of the proxy to use. 614 | /// 615 | /// Note that this will override `auto` specified before. 616 | pub fn url(&mut self, url: String) -> &Self { 617 | self.inner.url(url.as_str()); 618 | self 619 | } 620 | } 621 | 622 | #[napi] 623 | pub struct Cred { 624 | pub(crate) inner: git2::Cred, 625 | used: bool, 626 | } 627 | 628 | #[napi] 629 | impl Cred { 630 | #[napi(constructor)] 631 | #[allow(clippy::new_without_default)] 632 | /// Create a "default" credential usable for Negotiate mechanisms like NTLM 633 | /// or Kerberos authentication. 634 | pub fn new() -> Result { 635 | Ok(Self { 636 | inner: git2::Cred::default().convert("Create Cred failed")?, 637 | used: false, 638 | }) 639 | } 640 | 641 | #[napi(factory)] 642 | /// Create a new ssh key credential object used for querying an ssh-agent. 643 | /// 644 | /// The username specified is the username to authenticate. 645 | pub fn ssh_key_from_agent(username: String) -> Result { 646 | Ok(Self { 647 | inner: git2::Cred::ssh_key_from_agent(username.as_str()).convert("Create Cred failed")?, 648 | used: false, 649 | }) 650 | } 651 | 652 | #[napi(factory)] 653 | /// Create a new passphrase-protected ssh key credential object. 654 | pub fn ssh_key( 655 | username: String, 656 | publickey: Option, 657 | privatekey: String, 658 | passphrase: Option, 659 | ) -> Result { 660 | Ok(Self { 661 | inner: git2::Cred::ssh_key( 662 | username.as_str(), 663 | publickey.as_ref().map(Path::new), 664 | std::path::Path::new(&privatekey), 665 | passphrase.as_deref(), 666 | ) 667 | .convert("Create Cred failed")?, 668 | used: false, 669 | }) 670 | } 671 | 672 | #[napi(factory)] 673 | /// Create a new ssh key credential object reading the keys from memory. 674 | pub fn ssh_key_from_memory( 675 | username: String, 676 | publickey: Option, 677 | privatekey: String, 678 | passphrase: Option, 679 | ) -> Result { 680 | Ok(Self { 681 | inner: git2::Cred::ssh_key_from_memory( 682 | username.as_str(), 683 | publickey.as_deref(), 684 | privatekey.as_str(), 685 | passphrase.as_deref(), 686 | ) 687 | .convert("Create Cred failed")?, 688 | used: false, 689 | }) 690 | } 691 | 692 | #[napi(factory)] 693 | /// Create a new plain-text username and password credential object. 694 | pub fn userpass_plaintext(username: String, password: String) -> Result { 695 | Ok(Self { 696 | inner: git2::Cred::userpass_plaintext(username.as_str(), password.as_str()) 697 | .convert("Create Cred failed")?, 698 | used: false, 699 | }) 700 | } 701 | 702 | #[napi(factory)] 703 | /// Create a credential to specify a username. 704 | /// 705 | /// This is used with ssh authentication to query for the username if none is 706 | /// specified in the URL. 707 | pub fn username(username: String) -> Result { 708 | Ok(Self { 709 | inner: git2::Cred::username(username.as_str()).convert("Create Cred failed")?, 710 | used: false, 711 | }) 712 | } 713 | 714 | #[napi] 715 | /// Check whether a credential object contains username information. 716 | pub fn has_username(&self) -> bool { 717 | self.inner.has_username() 718 | } 719 | 720 | #[napi] 721 | /// Return the type of credentials that this object represents. 722 | pub fn credtype(&self) -> CredentialType { 723 | self.inner.credtype().into() 724 | } 725 | } 726 | 727 | #[napi] 728 | /// Check whether a cred_type contains another credential type. 729 | pub fn cred_type_contains(cred_type: CredentialType, another: CredentialType) -> bool { 730 | Into::::into(cred_type).contains(another.into()) 731 | } 732 | -------------------------------------------------------------------------------- /src/repo.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::sync::RwLock; 3 | 4 | use napi::{bindgen_prelude::*, JsString}; 5 | use napi_derive::napi; 6 | use once_cell::sync::Lazy; 7 | 8 | use crate::commit::{Commit, CommitInner}; 9 | use crate::diff::Diff; 10 | use crate::error::{IntoNapiError, NotNullError}; 11 | use crate::object::{GitObject, ObjectParent}; 12 | use crate::reference; 13 | use crate::remote::Remote; 14 | use crate::rev_walk::RevWalk; 15 | use crate::signature::Signature; 16 | use crate::tag::Tag; 17 | use crate::tree::{Tree, TreeEntry, TreeParent}; 18 | use crate::util::path_to_javascript_string; 19 | 20 | static INIT_GIT_CONFIG: Lazy> = Lazy::new(|| { 21 | // Handle the `failed to stat '/root/.gitconfig'; class=Config (7)` Error 22 | #[cfg(all( 23 | target_os = "linux", 24 | target_env = "gnu", 25 | any(target_arch = "x86_64", target_arch = "aarch64") 26 | ))] 27 | { 28 | if git2::Config::find_global().is_err() { 29 | if let Some(mut git_config_dir) = dirs::home_dir() { 30 | git_config_dir.push(".gitconfig"); 31 | std::fs::write(&git_config_dir, "").map_err(|err| { 32 | Error::new( 33 | Status::GenericFailure, 34 | format!("Initialize {:?} failed {}", git_config_dir, err), 35 | ) 36 | })?; 37 | } 38 | } 39 | } 40 | Ok(()) 41 | }); 42 | 43 | #[napi] 44 | pub enum RepositoryState { 45 | Clean, 46 | Merge, 47 | Revert, 48 | RevertSequence, 49 | CherryPick, 50 | CherryPickSequence, 51 | Bisect, 52 | Rebase, 53 | RebaseInteractive, 54 | RebaseMerge, 55 | ApplyMailbox, 56 | ApplyMailboxOrRebase, 57 | } 58 | 59 | impl From for RepositoryState { 60 | fn from(value: git2::RepositoryState) -> Self { 61 | match value { 62 | git2::RepositoryState::ApplyMailbox => Self::ApplyMailbox, 63 | git2::RepositoryState::ApplyMailboxOrRebase => Self::ApplyMailboxOrRebase, 64 | git2::RepositoryState::Bisect => Self::Bisect, 65 | git2::RepositoryState::Rebase => Self::Rebase, 66 | git2::RepositoryState::RebaseInteractive => Self::RebaseInteractive, 67 | git2::RepositoryState::RebaseMerge => Self::RebaseMerge, 68 | git2::RepositoryState::CherryPick => Self::CherryPick, 69 | git2::RepositoryState::CherryPickSequence => Self::CherryPickSequence, 70 | git2::RepositoryState::Merge => Self::Merge, 71 | git2::RepositoryState::Revert => Self::Revert, 72 | git2::RepositoryState::RevertSequence => Self::RevertSequence, 73 | git2::RepositoryState::Clean => Self::Clean, 74 | } 75 | } 76 | } 77 | 78 | #[napi] 79 | pub enum RepositoryOpenFlags { 80 | /// Only open the specified path; don't walk upward searching. 81 | NoSearch, 82 | /// Search across filesystem boundaries. 83 | CrossFS, 84 | /// Force opening as bare repository, and defer loading its config. 85 | Bare, 86 | /// Don't try appending `/.git` to the specified repository path. 87 | NoDotGit, 88 | /// Respect environment variables like `$GIT_DIR`. 89 | FromEnv, 90 | } 91 | 92 | impl From for git2::RepositoryOpenFlags { 93 | fn from(val: RepositoryOpenFlags) -> Self { 94 | match val { 95 | RepositoryOpenFlags::NoSearch => git2::RepositoryOpenFlags::NO_SEARCH, 96 | RepositoryOpenFlags::CrossFS => git2::RepositoryOpenFlags::CROSS_FS, 97 | RepositoryOpenFlags::Bare => git2::RepositoryOpenFlags::BARE, 98 | RepositoryOpenFlags::NoDotGit => git2::RepositoryOpenFlags::NO_DOTGIT, 99 | RepositoryOpenFlags::FromEnv => git2::RepositoryOpenFlags::FROM_ENV, 100 | } 101 | } 102 | } 103 | 104 | pub struct GitDateTask { 105 | repo: RwLock>, 106 | filepath: String, 107 | } 108 | 109 | unsafe impl Send for GitDateTask {} 110 | 111 | #[napi] 112 | impl Task for GitDateTask { 113 | type Output = i64; 114 | type JsValue = i64; 115 | 116 | fn compute(&mut self) -> napi::Result { 117 | get_file_modified_date( 118 | &(**self 119 | .repo 120 | .read() 121 | .map_err(|err| napi::Error::new(Status::GenericFailure, format!("{err}")))?) 122 | .inner, 123 | &self.filepath, 124 | ) 125 | .convert_without_message() 126 | .and_then(|value| { 127 | value.expect_not_null(format!("Failed to get commit for [{}]", &self.filepath)) 128 | }) 129 | } 130 | 131 | fn resolve(&mut self, _env: napi::Env, output: Self::Output) -> napi::Result { 132 | Ok(output) 133 | } 134 | } 135 | 136 | #[napi] 137 | pub struct Repository { 138 | pub(crate) inner: git2::Repository, 139 | } 140 | 141 | #[napi] 142 | impl Repository { 143 | #[napi(factory)] 144 | pub fn init(p: String) -> Result { 145 | INIT_GIT_CONFIG.as_ref().map_err(|err| err.clone())?; 146 | Ok(Self { 147 | inner: git2::Repository::init(&p).map_err(|err| { 148 | Error::new( 149 | Status::GenericFailure, 150 | format!("Failed to open git repo: [{p}], reason: {err}",), 151 | ) 152 | })?, 153 | }) 154 | } 155 | 156 | #[napi(factory)] 157 | /// Find and open an existing repository, with additional options. 158 | /// 159 | /// If flags contains REPOSITORY_OPEN_NO_SEARCH, the path must point 160 | /// directly to a repository; otherwise, this may point to a subdirectory 161 | /// of a repository, and `open_ext` will search up through parent 162 | /// directories. 163 | /// 164 | /// If flags contains REPOSITORY_OPEN_CROSS_FS, the search through parent 165 | /// directories will not cross a filesystem boundary (detected when the 166 | /// stat st_dev field changes). 167 | /// 168 | /// If flags contains REPOSITORY_OPEN_BARE, force opening the repository as 169 | /// bare even if it isn't, ignoring any working directory, and defer 170 | /// loading the repository configuration for performance. 171 | /// 172 | /// If flags contains REPOSITORY_OPEN_NO_DOTGIT, don't try appending 173 | /// `/.git` to `path`. 174 | /// 175 | /// If flags contains REPOSITORY_OPEN_FROM_ENV, `open_ext` will ignore 176 | /// other flags and `ceiling_dirs`, and respect the same environment 177 | /// variables git does. Note, however, that `path` overrides `$GIT_DIR`; to 178 | /// respect `$GIT_DIR` as well, use `open_from_env`. 179 | /// 180 | /// ceiling_dirs specifies a list of paths that the search through parent 181 | /// directories will stop before entering. Use the functions in std::env 182 | /// to construct or manipulate such a path list. 183 | pub fn open_ext( 184 | path: String, 185 | flags: RepositoryOpenFlags, 186 | ceiling_dirs: Vec, 187 | ) -> Result { 188 | INIT_GIT_CONFIG.as_ref().map_err(|err| err.clone())?; 189 | Ok(Self { 190 | inner: git2::Repository::open_ext(path, flags.into(), ceiling_dirs) 191 | .convert("Failed to open git repo")?, 192 | }) 193 | } 194 | 195 | #[napi(factory)] 196 | /// Attempt to open an already-existing repository at or above `path` 197 | /// 198 | /// This starts at `path` and looks up the filesystem hierarchy 199 | /// until it finds a repository. 200 | pub fn discover(path: String) -> Result { 201 | INIT_GIT_CONFIG.as_ref().map_err(|err| err.clone())?; 202 | Ok(Self { 203 | inner: git2::Repository::discover(&path) 204 | .convert(format!("Discover git repo from [{path}] failed"))?, 205 | }) 206 | } 207 | 208 | #[napi(factory)] 209 | /// Creates a new `--bare` repository in the specified folder. 210 | /// 211 | /// The folder must exist prior to invoking this function. 212 | pub fn init_bare(path: String) -> Result { 213 | Ok(Self { 214 | inner: git2::Repository::init_bare(path).convert("Failed to init bare repo")?, 215 | }) 216 | } 217 | 218 | #[napi(factory)] 219 | /// Clone a remote repository. 220 | /// 221 | /// See the `RepoBuilder` struct for more information. This function will 222 | /// delegate to a fresh `RepoBuilder` 223 | pub fn clone(url: String, path: String) -> Result { 224 | Ok(Self { 225 | inner: git2::Repository::clone(&url, path).convert("Failed to clone repo")?, 226 | }) 227 | } 228 | 229 | #[napi(factory)] 230 | /// Clone a remote repository, initialize and update its submodules 231 | /// recursively. 232 | /// 233 | /// This is similar to `git clone --recursive`. 234 | pub fn clone_recurse(url: String, path: String) -> Result { 235 | Ok(Self { 236 | inner: git2::Repository::clone_recurse(&url, path) 237 | .convert("Failed to clone repo recursively")?, 238 | }) 239 | } 240 | 241 | #[napi(constructor)] 242 | /// Attempt to open an already-existing repository at `path`. 243 | /// 244 | /// The path can point to either a normal or bare repository. 245 | pub fn new(git_dir: String) -> Result { 246 | INIT_GIT_CONFIG.as_ref().map_err(|err| err.clone())?; 247 | Ok(Self { 248 | inner: git2::Repository::open(&git_dir).map_err(|err| { 249 | Error::new( 250 | Status::GenericFailure, 251 | format!("Failed to open git repo: [{git_dir}], reason: {err}",), 252 | ) 253 | })?, 254 | }) 255 | } 256 | 257 | #[napi] 258 | /// Retrieve and resolve the reference pointed at by HEAD. 259 | pub fn head(&self, self_ref: Reference, env: Env) -> Result { 260 | Ok(reference::Reference { 261 | inner: self_ref.share_with(env, |repo| { 262 | repo 263 | .inner 264 | .head() 265 | .convert("Get the HEAD of Repository failed") 266 | })?, 267 | }) 268 | } 269 | 270 | #[napi] 271 | /// Tests whether this repository is a shallow clone. 272 | pub fn is_shallow(&self) -> Result { 273 | Ok(self.inner.is_shallow()) 274 | } 275 | 276 | #[napi] 277 | /// Tests whether this repository is empty. 278 | pub fn is_empty(&self) -> Result { 279 | self.inner.is_empty().convert_without_message() 280 | } 281 | 282 | #[napi] 283 | /// Tests whether this repository is a worktree. 284 | pub fn is_worktree(&self) -> Result { 285 | Ok(self.inner.is_worktree()) 286 | } 287 | 288 | #[napi] 289 | /// Returns the path to the `.git` folder for normal repositories or the 290 | /// repository itself for bare repositories. 291 | pub fn path(&self, env: Env) -> Result { 292 | path_to_javascript_string(&env, self.inner.path()) 293 | } 294 | 295 | #[napi] 296 | /// Returns the current state of this repository 297 | pub fn state(&self) -> Result { 298 | Ok(self.inner.state().into()) 299 | } 300 | 301 | #[napi] 302 | /// Get the path of the working directory for this repository. 303 | /// 304 | /// If this repository is bare, then `None` is returned. 305 | pub fn workdir(&self, env: Env) -> Option { 306 | self 307 | .inner 308 | .workdir() 309 | .and_then(|path| path_to_javascript_string(&env, path).ok()) 310 | } 311 | 312 | #[napi] 313 | /// Set the path to the working directory for this repository. 314 | /// 315 | /// If `update_link` is true, create/update the gitlink file in the workdir 316 | /// and set config "core.worktree" (if workdir is not the parent of the .git 317 | /// directory). 318 | pub fn set_workdir(&self, path: String, update_gitlink: bool) -> Result<()> { 319 | self 320 | .inner 321 | .set_workdir(PathBuf::from(path).as_path(), update_gitlink) 322 | .convert_without_message()?; 323 | Ok(()) 324 | } 325 | 326 | #[napi] 327 | /// Get the currently active namespace for this repository. 328 | /// 329 | /// If there is no namespace, or the namespace is not a valid utf8 string, 330 | /// `None` is returned. 331 | pub fn namespace(&self) -> Option { 332 | self.inner.namespace().map(|n| n.to_owned()) 333 | } 334 | 335 | #[napi] 336 | /// Set the active namespace for this repository. 337 | pub fn set_namespace(&self, namespace: String) -> Result<()> { 338 | self 339 | .inner 340 | .set_namespace(&namespace) 341 | .convert_without_message()?; 342 | Ok(()) 343 | } 344 | 345 | #[napi] 346 | /// Remove the active namespace for this repository. 347 | pub fn remove_namespace(&self) -> Result<()> { 348 | self.inner.remove_namespace().convert_without_message()?; 349 | Ok(()) 350 | } 351 | 352 | #[napi] 353 | /// Retrieves the Git merge message. 354 | /// Remember to remove the message when finished. 355 | pub fn message(&self) -> Result { 356 | self 357 | .inner 358 | .message() 359 | .convert("Failed to get Git merge message") 360 | } 361 | 362 | #[napi] 363 | /// Remove the Git merge message. 364 | pub fn remove_message(&self) -> Result<()> { 365 | self 366 | .inner 367 | .remove_message() 368 | .convert("Remove the Git merge message failed") 369 | } 370 | 371 | #[napi] 372 | /// List all remotes for a given repository 373 | pub fn remotes(&self) -> Result> { 374 | self 375 | .inner 376 | .remotes() 377 | .map(|remotes| { 378 | remotes 379 | .into_iter() 380 | .flatten() 381 | .map(|name| name.to_owned()) 382 | .collect() 383 | }) 384 | .convert("Fetch remotes failed") 385 | } 386 | 387 | #[napi] 388 | /// Get the information for a particular remote 389 | pub fn find_remote( 390 | &self, 391 | self_ref: Reference, 392 | env: Env, 393 | name: String, 394 | ) -> Option { 395 | Some(Remote { 396 | inner: self_ref 397 | .share_with(env, move |repo| { 398 | repo 399 | .inner 400 | .find_remote(&name) 401 | .convert(format!("Failed to get remote [{}]", &name)) 402 | }) 403 | .ok()?, 404 | }) 405 | } 406 | 407 | #[napi] 408 | /// Add a remote with the default fetch refspec to the repository's 409 | /// configuration. 410 | pub fn remote( 411 | &mut self, 412 | env: Env, 413 | this: Reference, 414 | name: String, 415 | url: String, 416 | ) -> Result { 417 | Ok(Remote { 418 | inner: this.share_with(env, move |repo| { 419 | repo 420 | .inner 421 | .remote(&name, &url) 422 | .convert(format!("Failed to add remote [{}]", &name)) 423 | })?, 424 | }) 425 | } 426 | 427 | #[napi] 428 | /// Add a remote with the provided fetch refspec to the repository's 429 | /// configuration. 430 | pub fn remote_with_fetch( 431 | &mut self, 432 | env: Env, 433 | this: Reference, 434 | name: String, 435 | url: String, 436 | refspect: String, 437 | ) -> Result { 438 | Ok(Remote { 439 | inner: this.share_with(env, move |repo| { 440 | repo 441 | .inner 442 | .remote_with_fetch(&name, &url, &refspect) 443 | .convert("Failed to add remote") 444 | })?, 445 | }) 446 | } 447 | 448 | #[napi] 449 | /// Create an anonymous remote 450 | /// 451 | /// Create a remote with the given URL and refspec in memory. You can use 452 | /// this when you have a URL instead of a remote's name. Note that anonymous 453 | /// remotes cannot be converted to persisted remotes. 454 | pub fn remote_anonymous( 455 | &self, 456 | env: Env, 457 | this: Reference, 458 | url: String, 459 | ) -> Result { 460 | Ok(Remote { 461 | inner: this.share_with(env, move |repo| { 462 | repo 463 | .inner 464 | .remote_anonymous(&url) 465 | .convert("Failed to create anonymous remote") 466 | })?, 467 | }) 468 | } 469 | 470 | #[napi] 471 | /// Give a remote a new name 472 | /// 473 | /// All remote-tracking branches and configuration settings for the remote 474 | /// are updated. 475 | /// 476 | /// A temporary in-memory remote cannot be given a name with this method. 477 | /// 478 | /// No loaded instances of the remote with the old name will change their 479 | /// name or their list of refspecs. 480 | /// 481 | /// The returned array of strings is a list of the non-default refspecs 482 | /// which cannot be renamed and are returned for further processing by the 483 | /// caller. 484 | pub fn remote_rename(&self, name: String, new_name: String) -> Result> { 485 | Ok( 486 | self 487 | .inner 488 | .remote_rename(&name, &new_name) 489 | .convert(format!("Failed to rename remote [{}]", &name))? 490 | .into_iter() 491 | .flatten() 492 | .map(|s| s.to_owned()) 493 | .collect::>(), 494 | ) 495 | } 496 | 497 | #[napi] 498 | /// Delete an existing persisted remote. 499 | /// 500 | /// All remote-tracking branches and configuration settings for the remote 501 | /// will be removed. 502 | pub fn remote_delete(&self, name: String) -> Result<&Self> { 503 | self.inner.remote_delete(&name).convert_without_message()?; 504 | Ok(self) 505 | } 506 | 507 | #[napi] 508 | /// Add a fetch refspec to the remote's configuration 509 | /// 510 | /// Add the given refspec to the fetch list in the configuration. No loaded 511 | pub fn remote_add_fetch(&self, name: String, refspec: String) -> Result<&Self> { 512 | self 513 | .inner 514 | .remote_add_fetch(&name, &refspec) 515 | .convert_without_message()?; 516 | Ok(self) 517 | } 518 | 519 | #[napi] 520 | /// Add a push refspec to the remote's configuration. 521 | /// 522 | /// Add the given refspec to the push list in the configuration. No 523 | /// loaded remote instances will be affected. 524 | pub fn remote_add_push(&self, name: String, refspec: String) -> Result<&Self> { 525 | self 526 | .inner 527 | .remote_add_push(&name, &refspec) 528 | .convert_without_message()?; 529 | Ok(self) 530 | } 531 | 532 | #[napi] 533 | /// Add a push refspec to the remote's configuration. 534 | /// 535 | /// Add the given refspec to the push list in the configuration. No 536 | /// loaded remote instances will be affected. 537 | pub fn remote_set_url(&self, name: String, url: String) -> Result<&Self> { 538 | self 539 | .inner 540 | .remote_set_url(&name, &url) 541 | .convert_without_message()?; 542 | Ok(self) 543 | } 544 | 545 | #[napi] 546 | /// Set the remote's URL for pushing in the configuration. 547 | /// 548 | /// Remote objects already in memory will not be affected. This assumes 549 | /// the common case of a single-url remote and will otherwise return an 550 | /// error. 551 | /// 552 | /// `None` indicates that it should be cleared. 553 | pub fn remote_set_pushurl(&self, name: String, url: Option) -> Result<&Self> { 554 | self 555 | .inner 556 | .remote_set_pushurl(&name, url.as_deref()) 557 | .convert_without_message()?; 558 | Ok(self) 559 | } 560 | 561 | #[napi] 562 | /// Lookup a reference to one of the objects in a repository. 563 | pub fn find_tree(&self, oid: String, self_ref: Reference, env: Env) -> Option { 564 | Some(Tree { 565 | inner: TreeParent::Repository( 566 | self_ref 567 | .share_with(env, |repo| { 568 | repo 569 | .inner 570 | .find_tree(git2::Oid::from_str(oid.as_str()).convert(format!("Invalid OID [{oid}]"))?) 571 | .convert(format!("Find tree from OID [{oid}] failed")) 572 | }) 573 | .ok()?, 574 | ), 575 | }) 576 | } 577 | 578 | #[napi] 579 | pub fn find_commit( 580 | &self, 581 | oid: String, 582 | this_ref: Reference, 583 | env: Env, 584 | ) -> Option { 585 | let commit = this_ref 586 | .share_with(env, |repo| { 587 | repo 588 | .inner 589 | .find_commit_by_prefix(&oid) 590 | .convert(format!("Find commit from OID [{oid}] failed")) 591 | }) 592 | .ok()?; 593 | Some(Commit { 594 | inner: CommitInner::Repository(commit), 595 | }) 596 | } 597 | 598 | #[napi] 599 | /// Create a new tag in the repository from an object 600 | /// 601 | /// A new reference will also be created pointing to this tag object. If 602 | /// `force` is true and a reference already exists with the given name, 603 | /// it'll be replaced. 604 | /// 605 | /// The message will not be cleaned up. 606 | /// 607 | /// The tag name will be checked for validity. You must avoid the characters 608 | /// '~', '^', ':', ' \ ', '?', '[', and '*', and the sequences ".." and " @ 609 | /// {" which have special meaning to revparse. 610 | pub fn tag( 611 | &self, 612 | name: String, 613 | target: &GitObject, 614 | tagger: &Signature, 615 | message: String, 616 | force: bool, 617 | ) -> Result { 618 | self 619 | .inner 620 | .tag(&name, &*target.inner, &*tagger.inner, &message, force) 621 | .map(|o| o.to_string()) 622 | .convert("Failed to create tag") 623 | } 624 | 625 | #[napi] 626 | /// Create a new tag in the repository from an object without creating a reference. 627 | /// 628 | /// The message will not be cleaned up. 629 | /// 630 | /// The tag name will be checked for validity. You must avoid the characters 631 | /// '~', '^', ':', ' \ ', '?', '[', and '*', and the sequences ".." and " @ 632 | /// {" which have special meaning to revparse. 633 | pub fn tag_annotation_create( 634 | &self, 635 | name: String, 636 | target: &GitObject, 637 | tagger: &Signature, 638 | message: String, 639 | ) -> Result { 640 | self 641 | .inner 642 | .tag_annotation_create(&name, &*target.inner, &*tagger.inner, &message) 643 | .map(|o| o.to_string()) 644 | .convert("Failed to create tag annotation") 645 | } 646 | 647 | #[napi] 648 | /// Create a new lightweight tag pointing at a target object 649 | /// 650 | /// A new direct reference will be created pointing to this target object. 651 | /// If force is true and a reference already exists with the given name, 652 | /// it'll be replaced. 653 | pub fn tag_lightweight(&self, name: String, target: &GitObject, force: bool) -> Result { 654 | self 655 | .inner 656 | .tag_lightweight(&name, &*target.inner, force) 657 | .map(|o| o.to_string()) 658 | .convert("Failed to create lightweight tag") 659 | } 660 | 661 | #[napi] 662 | /// Lookup a tag object from the repository. 663 | pub fn find_tag(&self, env: Env, this: Reference, oid: String) -> Result { 664 | Ok(Tag { 665 | inner: this.share_with(env, |repo| { 666 | repo 667 | .inner 668 | .find_tag(git2::Oid::from_str(oid.as_str()).convert(format!("Invalid OID [{oid}]"))?) 669 | .convert(format!("Find tag from OID [{oid}] failed")) 670 | })?, 671 | }) 672 | } 673 | 674 | #[napi] 675 | /// Lookup a tag object by prefix hash from the repository. 676 | pub fn find_tag_by_prefix( 677 | &self, 678 | env: Env, 679 | this: Reference, 680 | prefix_hash: String, 681 | ) -> Result { 682 | Ok(Tag { 683 | inner: this.share_with(env, |repo| { 684 | repo 685 | .inner 686 | .find_tag_by_prefix(&prefix_hash) 687 | .convert(format!("Find tag from OID [{prefix_hash}] failed")) 688 | })?, 689 | }) 690 | } 691 | 692 | #[napi] 693 | /// Delete an existing tag reference. 694 | /// 695 | /// The tag name will be checked for validity, see `tag` for some rules 696 | /// about valid names. 697 | pub fn tag_delete(&self, name: String) -> Result<()> { 698 | self.inner.tag_delete(&name).convert_without_message()?; 699 | Ok(()) 700 | } 701 | 702 | #[napi] 703 | /// Get a list with all the tags in the repository. 704 | /// 705 | /// An optional fnmatch pattern can also be specified. 706 | pub fn tag_names(&self, pattern: Option) -> Result> { 707 | self 708 | .inner 709 | .tag_names(pattern.as_deref()) 710 | .convert("Failed to get tag names") 711 | .map(|tags| { 712 | tags 713 | .into_iter() 714 | .filter_map(|s| s.map(|s| s.to_owned())) 715 | .collect() 716 | }) 717 | } 718 | 719 | #[napi] 720 | /// iterate over all tags calling `cb` on each. 721 | /// the callback is provided the tag id and name 722 | pub fn tag_foreach(&self, cb: Function<(String, Buffer), bool>) -> Result<()> { 723 | self 724 | .inner 725 | .tag_foreach(|oid, name| { 726 | let oid = oid.to_string(); 727 | let name = name.to_vec(); 728 | cb.call((oid, name.into())).unwrap_or(false) 729 | }) 730 | .convert_without_message() 731 | } 732 | 733 | #[napi] 734 | /// Create a diff between a tree and the working directory. 735 | /// 736 | /// The tree you provide will be used for the "old_file" side of the delta, 737 | /// and the working directory will be used for the "new_file" side. 738 | /// 739 | /// This is not the same as `git diff ` or `git diff-index 740 | /// `. Those commands use information from the index, whereas this 741 | /// function strictly returns the differences between the tree and the files 742 | /// in the working directory, regardless of the state of the index. Use 743 | /// `tree_to_workdir_with_index` to emulate those commands. 744 | /// 745 | /// To see difference between this and `tree_to_workdir_with_index`, 746 | /// consider the example of a staged file deletion where the file has then 747 | /// been put back into the working dir and further modified. The 748 | /// tree-to-workdir diff for that file is 'modified', but `git diff` would 749 | /// show status 'deleted' since there is a staged delete. 750 | /// 751 | /// If `None` is passed for `tree`, then an empty tree is used. 752 | pub fn diff_tree_to_workdir( 753 | &self, 754 | env: Env, 755 | self_reference: Reference, 756 | old_tree: Option<&Tree>, 757 | ) -> Result { 758 | let mut diff_options = git2::DiffOptions::default(); 759 | Ok(Diff { 760 | inner: self_reference.share_with(env, |repo| { 761 | repo 762 | .inner 763 | .diff_tree_to_workdir(old_tree.map(|t| t.inner()), Some(&mut diff_options)) 764 | .convert_without_message() 765 | })?, 766 | }) 767 | } 768 | 769 | #[napi] 770 | /// Create a diff between a tree and the working directory using index data 771 | /// to account for staged deletes, tracked files, etc. 772 | /// 773 | /// This emulates `git diff ` by diffing the tree to the index and 774 | /// the index to the working directory and blending the results into a 775 | /// single diff that includes staged deleted, etc. 776 | pub fn diff_tree_to_workdir_with_index( 777 | &self, 778 | env: Env, 779 | self_reference: Reference, 780 | old_tree: Option<&Tree>, 781 | ) -> Result { 782 | let mut diff_options = git2::DiffOptions::default(); 783 | Ok(Diff { 784 | inner: self_reference.share_with(env, |repo| { 785 | repo 786 | .inner 787 | .diff_tree_to_workdir_with_index(old_tree.map(|t| t.inner()), Some(&mut diff_options)) 788 | .convert_without_message() 789 | })?, 790 | }) 791 | } 792 | 793 | #[napi] 794 | pub fn tree_entry_to_object( 795 | &self, 796 | tree_entry: &TreeEntry, 797 | this_ref: Reference, 798 | env: Env, 799 | ) -> Result { 800 | Ok(GitObject { 801 | inner: ObjectParent::Repository(this_ref.share_with(env, |repo| { 802 | tree_entry 803 | .inner 804 | .to_object(&repo.inner) 805 | .convert_without_message() 806 | })?), 807 | }) 808 | } 809 | 810 | #[napi] 811 | /// Create new commit in the repository 812 | /// 813 | /// If the `update_ref` is not `None`, name of the reference that will be 814 | /// updated to point to this commit. If the reference is not direct, it will 815 | /// be resolved to a direct reference. Use "HEAD" to update the HEAD of the 816 | /// current branch and make it point to this commit. If the reference 817 | /// doesn't exist yet, it will be created. If it does exist, the first 818 | /// parent must be the tip of this branch. 819 | pub fn commit( 820 | &self, 821 | update_ref: Option, 822 | author: &Signature, 823 | committer: &Signature, 824 | message: String, 825 | tree: &Tree, 826 | ) -> Result { 827 | self 828 | .inner 829 | .commit( 830 | update_ref.as_deref(), 831 | author.as_ref(), 832 | committer.as_ref(), 833 | message.as_str(), 834 | tree.as_ref(), 835 | &[], 836 | ) 837 | .convert_without_message() 838 | .map(|oid| oid.to_string()) 839 | } 840 | 841 | #[napi] 842 | /// Create a revwalk that can be used to traverse the commit graph. 843 | pub fn rev_walk(&self, this_ref: Reference, env: Env) -> Result { 844 | Ok(RevWalk { 845 | inner: this_ref.share_with(env, |repo| repo.inner.revwalk().convert_without_message())?, 846 | }) 847 | } 848 | 849 | #[napi] 850 | pub fn get_file_latest_modified_date(&self, filepath: String) -> Result { 851 | get_file_modified_date(&self.inner, &filepath) 852 | .convert_without_message() 853 | .and_then(|value| value.expect_not_null(format!("Failed to get commit for [{filepath}]"))) 854 | } 855 | 856 | #[napi] 857 | pub fn get_file_latest_modified_date_async( 858 | &self, 859 | self_ref: Reference, 860 | filepath: String, 861 | signal: Option, 862 | ) -> Result> { 863 | Ok(AsyncTask::with_optional_signal( 864 | GitDateTask { 865 | repo: RwLock::new(self_ref), 866 | filepath, 867 | }, 868 | signal, 869 | )) 870 | } 871 | } 872 | 873 | fn get_file_modified_date( 874 | repo: &git2::Repository, 875 | filepath: &str, 876 | ) -> std::result::Result, git2::Error> { 877 | let mut diff_options = git2::DiffOptions::new(); 878 | diff_options.disable_pathspec_match(false); 879 | diff_options.pathspec(filepath); 880 | let mut rev_walk = repo.revwalk()?; 881 | rev_walk.push_head()?; 882 | rev_walk.set_sorting(git2::Sort::TIME & git2::Sort::TOPOLOGICAL)?; 883 | let path = PathBuf::from(filepath); 884 | Ok( 885 | rev_walk 886 | .by_ref() 887 | .filter_map(|oid| oid.ok()) 888 | .find_map(|oid| { 889 | let commit = repo.find_commit(oid).ok()?; 890 | match commit.parent_count() { 891 | // commit with parent 892 | 1 => { 893 | let tree = commit.tree().ok()?; 894 | if let Ok(parent) = commit.parent(0) { 895 | let parent_tree = parent.tree().ok()?; 896 | if let Ok(diff) = 897 | repo.diff_tree_to_tree(Some(&tree), Some(&parent_tree), Some(&mut diff_options)) 898 | { 899 | if diff.deltas().len() > 0 { 900 | return Some(commit.time().seconds() * 1000); 901 | } 902 | } 903 | } 904 | } 905 | // root commit 906 | 0 => { 907 | let tree = commit.tree().ok()?; 908 | if tree.get_path(&path).is_ok() { 909 | return Some(commit.time().seconds() * 1000); 910 | } 911 | } 912 | // ignore merge commits 913 | _ => {} 914 | }; 915 | None 916 | }), 917 | ) 918 | } 919 | -------------------------------------------------------------------------------- /src/repo_builder.rs: -------------------------------------------------------------------------------- 1 | use std::{mem, path::Path}; 2 | 3 | use napi::bindgen_prelude::*; 4 | use napi_derive::napi; 5 | 6 | use crate::{error::IntoNapiError, remote::FetchOptions, repo::Repository}; 7 | 8 | #[napi] 9 | pub struct RepoBuilder { 10 | builder: git2::build::RepoBuilder<'static>, 11 | } 12 | 13 | #[napi] 14 | pub enum CloneLocal { 15 | /// Auto-detect (default) 16 | /// 17 | /// Here libgit2 will bypass the git-aware transport for local paths, but 18 | /// use a normal fetch for `file://` URLs. 19 | Auto, 20 | 21 | /// Bypass the git-aware transport even for `file://` URLs. 22 | Local, 23 | 24 | /// Never bypass the git-aware transport 25 | None, 26 | 27 | /// Bypass the git-aware transport, but don't try to use hardlinks. 28 | NoLinks, 29 | } 30 | 31 | impl From for git2::build::CloneLocal { 32 | fn from(clone_local: CloneLocal) -> Self { 33 | match clone_local { 34 | CloneLocal::Auto => git2::build::CloneLocal::Auto, 35 | CloneLocal::Local => git2::build::CloneLocal::Local, 36 | CloneLocal::None => git2::build::CloneLocal::None, 37 | CloneLocal::NoLinks => git2::build::CloneLocal::NoLinks, 38 | } 39 | } 40 | } 41 | 42 | #[napi] 43 | /// A builder struct which is used to build configuration for cloning a new git 44 | /// repository. 45 | /// 46 | /// # Example 47 | /// 48 | /// Cloning using SSH: 49 | /// 50 | /// ```rust 51 | /// use git2::{Cred, Error, RemoteCallbacks}; 52 | /// use std::env; 53 | /// use std::path::Path; 54 | /// 55 | /// // Prepare callbacks. 56 | /// let mut callbacks = RemoteCallbacks::new(); 57 | /// callbacks.credentials(|_url, username_from_url, _allowed_types| { 58 | /// Cred::ssh_key( 59 | /// username_from_url.unwrap(), 60 | /// None, 61 | /// Path::new(&format!("{}/.ssh/id_rsa", env::var("HOME").unwrap())), 62 | /// None, 63 | /// ) 64 | /// }); 65 | /// 66 | /// // Prepare fetch options. 67 | /// let mut fo = git2::FetchOptions::new(); 68 | /// fo.remote_callbacks(callbacks); 69 | /// 70 | /// // Prepare builder. 71 | /// let mut builder = git2::build::RepoBuilder::new(); 72 | /// builder.fetch_options(fo); 73 | /// 74 | /// // Clone the project. 75 | /// builder.clone( 76 | /// "git@github.com:rust-lang/git2-rs.git", 77 | /// Path::new("/tmp/git2-rs"), 78 | /// ); 79 | /// ``` 80 | impl RepoBuilder { 81 | #[napi(constructor)] 82 | #[allow(clippy::new_without_default)] 83 | pub fn new() -> Self { 84 | Self { 85 | builder: Default::default(), 86 | } 87 | } 88 | 89 | #[napi] 90 | /// Indicate whether the repository will be cloned as a bare repository or 91 | /// not. 92 | pub fn bare(&mut self, bare: bool) -> &Self { 93 | self.builder.bare(bare); 94 | self 95 | } 96 | 97 | #[napi] 98 | /// Specify the name of the branch to check out after the clone. 99 | /// 100 | /// If not specified, the remote's default branch will be used. 101 | pub fn branch(&mut self, branch: String) -> &Self { 102 | self.builder.branch(&branch); 103 | self 104 | } 105 | 106 | #[napi] 107 | /// Configures options for bypassing the git-aware transport on clone. 108 | /// 109 | /// Bypassing it means that instead of a fetch libgit2 will copy the object 110 | /// database directory instead of figuring out what it needs, which is 111 | /// faster. If possible, it will hardlink the files to save space. 112 | pub fn clone_local(&mut self, clone_local: CloneLocal) -> &Self { 113 | self.builder.clone_local(clone_local.into()); 114 | self 115 | } 116 | 117 | #[napi] 118 | /// Options which control the fetch, including callbacks. 119 | /// 120 | /// The callbacks are used for reporting fetch progress, and for acquiring 121 | /// credentials in the event they are needed. 122 | pub fn fetch_options(&mut self, fetch_options: &mut FetchOptions) -> Result<&Self> { 123 | if fetch_options.used { 124 | return Err(Error::new( 125 | Status::GenericFailure, 126 | "FetchOptions has been used, please create a new one", 127 | )); 128 | } 129 | let mut opt = git2::FetchOptions::default(); 130 | mem::swap(&mut fetch_options.inner, &mut opt); 131 | fetch_options.used = true; 132 | self.builder.fetch_options(opt); 133 | Ok(self) 134 | } 135 | 136 | #[napi] 137 | pub fn clone(&mut self, url: String, path: String) -> Result { 138 | Ok(Repository { 139 | inner: self 140 | .builder 141 | .clone(&url, Path::new(&path)) 142 | .convert("Clone failed")?, 143 | }) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/rev_walk.rs: -------------------------------------------------------------------------------- 1 | use napi::bindgen_prelude::*; 2 | use napi_derive::napi; 3 | 4 | use crate::{error::IntoNapiError, repo::Repository}; 5 | 6 | #[napi] 7 | /// Orderings that may be specified for Revwalk iteration. 8 | #[repr(u32)] 9 | pub enum Sort { 10 | /// Sort the repository contents in no particular ordering. 11 | /// 12 | /// This sorting is arbitrary, implementation-specific, and subject to 13 | /// change at any time. This is the default sorting for new walkers. 14 | None = 0, 15 | 16 | /// Sort the repository contents in topological order (children before 17 | /// parents). 18 | /// 19 | /// This sorting mode can be combined with time sorting. 20 | /// 1 << 0 21 | Topological = 1, 22 | 23 | /// Sort the repository contents by commit time. 24 | /// 25 | /// This sorting mode can be combined with topological sorting. 26 | /// 1 << 1 27 | Time = 2, 28 | 29 | /// Iterate through the repository contents in reverse order. 30 | /// 31 | /// This sorting mode can be combined with any others. 32 | /// 1 << 2 33 | Reverse = 4, 34 | } 35 | 36 | impl From for git2::Sort { 37 | fn from(value: Sort) -> Self { 38 | match value { 39 | Sort::None => git2::Sort::NONE, 40 | Sort::Topological => git2::Sort::TOPOLOGICAL, 41 | Sort::Time => git2::Sort::TIME, 42 | Sort::Reverse => git2::Sort::REVERSE, 43 | } 44 | } 45 | } 46 | 47 | #[napi(iterator)] 48 | pub struct RevWalk { 49 | pub(crate) inner: SharedReference>, 50 | } 51 | 52 | #[napi] 53 | impl Generator for RevWalk { 54 | type Yield = String; 55 | type Return = (); 56 | type Next = (); 57 | 58 | fn next(&mut self, _value: Option) -> Option { 59 | self 60 | .inner 61 | .next() 62 | .and_then(|s| s.ok().map(|oid| oid.to_string())) 63 | } 64 | } 65 | 66 | #[napi] 67 | impl RevWalk { 68 | #[napi] 69 | /// Reset a revwalk to allow re-configuring it. 70 | /// 71 | /// The revwalk is automatically reset when iteration of its commits 72 | /// completes. 73 | pub fn reset(&mut self) -> Result<&Self> { 74 | self.inner.reset().convert_without_message()?; 75 | Ok(self) 76 | } 77 | 78 | #[napi] 79 | /// Set the sorting mode for a revwalk. 80 | pub fn set_sorting(&mut self, sorting: Sort) -> Result<&Self> { 81 | self 82 | .inner 83 | .set_sorting(sorting.into()) 84 | .convert_without_message()?; 85 | Ok(self) 86 | } 87 | 88 | #[napi] 89 | /// Simplify the history by first-parent 90 | /// 91 | /// No parents other than the first for each commit will be enqueued. 92 | pub fn simplify_first_parent(&mut self) -> Result<&Self> { 93 | self 94 | .inner 95 | .simplify_first_parent() 96 | .convert_without_message()?; 97 | Ok(self) 98 | } 99 | 100 | #[napi] 101 | /// Mark a commit to start traversal from. 102 | /// 103 | /// The given OID must belong to a commitish on the walked repository. 104 | /// 105 | /// The given commit will be used as one of the roots when starting the 106 | /// revision walk. At least one commit must be pushed onto the walker before 107 | /// a walk can be started. 108 | pub fn push(&mut self, oid: String) -> Result<&Self> { 109 | let oid = git2::Oid::from_str(&oid).convert("Invalid oid")?; 110 | self.inner.push(oid).convert_without_message()?; 111 | Ok(self) 112 | } 113 | 114 | #[napi] 115 | /// Push the repository's HEAD 116 | /// 117 | /// For more information, see `push`. 118 | pub fn push_head(&mut self) -> Result<&Self> { 119 | self.inner.push_head().convert_without_message()?; 120 | Ok(self) 121 | } 122 | 123 | #[napi] 124 | /// Push matching references 125 | /// 126 | /// The OIDs pointed to by the references that match the given glob pattern 127 | /// will be pushed to the revision walker. 128 | /// 129 | /// A leading 'refs/' is implied if not present as well as a trailing `/ \ 130 | /// *` if the glob lacks '?', ' \ *' or '['. 131 | /// 132 | /// Any references matching this glob which do not point to a commitish 133 | /// will be ignored. 134 | pub fn push_glob(&mut self, glob: String) -> Result<&Self> { 135 | self.inner.push_glob(&glob).convert_without_message()?; 136 | Ok(self) 137 | } 138 | 139 | #[napi] 140 | /// Push and hide the respective endpoints of the given range. 141 | /// 142 | /// The range should be of the form `..` where each 143 | /// `` is in the form accepted by `revparse_single`. The left-hand 144 | /// commit will be hidden and the right-hand commit pushed. 145 | pub fn push_range(&mut self, range: String) -> Result<&Self> { 146 | self.inner.push_range(&range).convert_without_message()?; 147 | Ok(self) 148 | } 149 | 150 | #[napi] 151 | /// Push the OID pointed to by a reference 152 | /// 153 | /// The reference must point to a commitish. 154 | pub fn push_ref(&mut self, reference: String) -> Result<&Self> { 155 | self.inner.push_ref(&reference).convert_without_message()?; 156 | Ok(self) 157 | } 158 | 159 | #[napi] 160 | /// Mark a commit as not of interest to this revwalk. 161 | pub fn hide(&mut self, oid: String) -> Result<&Self> { 162 | let oid = git2::Oid::from_str(&oid).convert("Invalid oid")?; 163 | self.inner.hide(oid).convert_without_message()?; 164 | Ok(self) 165 | } 166 | 167 | #[napi] 168 | /// Hide the repository's HEAD 169 | /// 170 | /// For more information, see `hide`. 171 | pub fn hide_head(&mut self) -> Result<&Self> { 172 | self.inner.hide_head().convert_without_message()?; 173 | Ok(self) 174 | } 175 | 176 | #[napi] 177 | /// Hide matching references. 178 | /// 179 | /// The OIDs pointed to by the references that match the given glob pattern 180 | /// and their ancestors will be hidden from the output on the revision walk. 181 | /// 182 | /// A leading 'refs/' is implied if not present as well as a trailing `/ \ 183 | /// *` if the glob lacks '?', ' \ *' or '['. 184 | /// 185 | /// Any references matching this glob which do not point to a commitish 186 | /// will be ignored. 187 | pub fn hide_glob(&mut self, glob: String) -> Result<&Self> { 188 | self.inner.hide_glob(&glob).convert_without_message()?; 189 | Ok(self) 190 | } 191 | 192 | #[napi] 193 | /// Hide the OID pointed to by a reference. 194 | /// 195 | /// The reference must point to a commitish. 196 | pub fn hide_ref(&mut self, reference: String) -> Result<&Self> { 197 | self.inner.hide_ref(&reference).convert_without_message()?; 198 | Ok(self) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/signature.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use napi::bindgen_prelude::*; 4 | use napi_derive::napi; 5 | 6 | use crate::{commit::Commit, error::IntoNapiError}; 7 | 8 | pub(crate) enum SignatureInner { 9 | Signature(git2::Signature<'static>), 10 | FromCommit(SharedReference>), 11 | } 12 | 13 | impl Deref for SignatureInner { 14 | type Target = git2::Signature<'static>; 15 | 16 | fn deref(&self) -> &git2::Signature<'static> { 17 | match self { 18 | SignatureInner::Signature(parent) => parent, 19 | SignatureInner::FromCommit(parent) => parent, 20 | } 21 | } 22 | } 23 | 24 | #[napi] 25 | /// A Signature is used to indicate authorship of various actions throughout the 26 | /// library. 27 | /// 28 | /// Signatures contain a name, email, and timestamp. All fields can be specified 29 | /// with `new` while the `now` constructor omits the timestamp. The 30 | /// [`Repository::signature`] method can be used to create a default signature 31 | /// with name and email values read from the configuration. 32 | /// 33 | /// [`Repository::signature`]: struct.Repository.html#method.signature 34 | pub struct Signature { 35 | pub(crate) inner: SignatureInner, 36 | } 37 | 38 | #[napi] 39 | impl Signature { 40 | #[napi(factory)] 41 | /// Create a new action signature with a timestamp of 'now'. 42 | /// 43 | /// See `new` for more information 44 | pub fn now(name: String, email: String) -> Result { 45 | Ok(Signature { 46 | inner: SignatureInner::Signature( 47 | git2::Signature::now(name.as_str(), email.as_str()).convert_without_message()?, 48 | ), 49 | }) 50 | } 51 | 52 | #[napi(constructor)] 53 | /// Create a new action signature. 54 | /// 55 | /// The `time` specified is in seconds since the epoch, and the `offset` is 56 | /// the time zone offset in minutes. 57 | /// 58 | /// Returns error if either `name` or `email` contain angle brackets. 59 | pub fn new(name: String, email: String, time: i64) -> Result { 60 | Ok(Signature { 61 | inner: SignatureInner::Signature( 62 | git2::Signature::new(&name, &email, &git2::Time::new(time, 0)).convert_without_message()?, 63 | ), 64 | }) 65 | } 66 | 67 | #[napi] 68 | /// Gets the name on the signature. 69 | /// 70 | /// Returns `None` if the name is not valid utf-8 71 | pub fn name(&self) -> Option<&str> { 72 | self.inner.name() 73 | } 74 | 75 | #[napi] 76 | /// Gets the email on the signature. 77 | /// 78 | /// Returns `None` if the email is not valid utf-8 79 | pub fn email(&self) -> Option<&str> { 80 | self.inner.email() 81 | } 82 | 83 | #[napi] 84 | /// Return the time, in seconds, from epoch 85 | pub fn when(&self) -> i64 { 86 | self.inner.when().seconds() 87 | } 88 | } 89 | 90 | impl<'a> AsRef> for Signature { 91 | fn as_ref(&self) -> &git2::Signature<'a> { 92 | &self.inner 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/tag.rs: -------------------------------------------------------------------------------- 1 | use napi::bindgen_prelude::*; 2 | use napi_derive::napi; 3 | 4 | use crate::{error::IntoNapiError, object::GitObject}; 5 | 6 | #[napi] 7 | pub struct Tag { 8 | pub(crate) inner: SharedReference>, 9 | } 10 | 11 | #[napi] 12 | impl Tag { 13 | #[napi] 14 | /// Determine whether a tag name is valid, meaning that (when prefixed with refs/tags/) that 15 | /// it is a valid reference name, and that any additional tag name restrictions are imposed 16 | /// (eg, it cannot start with a -). 17 | pub fn is_valid_name(name: String) -> bool { 18 | git2::Tag::is_valid_name(&name) 19 | } 20 | 21 | #[napi] 22 | /// Get the id (SHA1) of a repository object 23 | pub fn id(&self) -> String { 24 | self.inner.id().to_string() 25 | } 26 | 27 | #[napi] 28 | /// Get the message of a tag 29 | /// 30 | /// Returns None if there is no message or if it is not valid utf8 31 | pub fn message(&self) -> Option { 32 | self.inner.message().map(|s| s.to_string()) 33 | } 34 | 35 | #[napi] 36 | /// Get the message of a tag 37 | /// 38 | /// Returns None if there is no message 39 | pub fn message_bytes(&self) -> Option { 40 | self.inner.message_bytes().map(|s| s.to_vec().into()) 41 | } 42 | 43 | #[napi] 44 | /// Get the name of a tag 45 | /// 46 | /// Returns None if it is not valid utf8 47 | pub fn name(&self) -> Option { 48 | self.inner.name().map(|s| s.to_string()) 49 | } 50 | 51 | #[napi] 52 | /// Get the name of a tag 53 | pub fn name_bytes(&self) -> Buffer { 54 | self.inner.name_bytes().to_vec().into() 55 | } 56 | 57 | #[napi] 58 | /// Recursively peel a tag until a non tag git_object is found 59 | pub fn peel(&self) -> Result { 60 | let obj = self.inner.peel().convert("Peel tag failed")?; 61 | Ok(crate::object::GitObject { 62 | inner: crate::object::ObjectParent::Object(obj), 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/tree.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | use std::path::Path; 3 | 4 | use napi::bindgen_prelude::{ 5 | Env, Error, Generator, Reference, Result, SharedReference, Uint8Array, 6 | }; 7 | use napi_derive::napi; 8 | 9 | use crate::{ 10 | error::IntoNapiError, 11 | object::{GitObject, ObjectParent}, 12 | repo::Repository, 13 | }; 14 | 15 | pub(crate) enum TreeParent { 16 | Repository(SharedReference>), 17 | Reference(SharedReference>), 18 | Commit(SharedReference>), 19 | } 20 | 21 | #[napi] 22 | pub struct Tree { 23 | pub(crate) inner: TreeParent, 24 | } 25 | 26 | #[napi] 27 | impl Tree { 28 | pub(crate) fn inner(&self) -> &git2::Tree { 29 | match &self.inner { 30 | TreeParent::Repository(parent) => parent, 31 | TreeParent::Reference(parent) => parent, 32 | TreeParent::Commit(parent) => parent, 33 | } 34 | } 35 | 36 | #[napi] 37 | /// Get the id (SHA1) of a repository object 38 | pub fn id(&self) -> String { 39 | self.inner().id().to_string() 40 | } 41 | 42 | #[napi] 43 | /// Get the number of entries listed in a tree. 44 | pub fn len(&self) -> u64 { 45 | self.inner().len() as u64 46 | } 47 | 48 | #[napi] 49 | /// Return `true` if there is not entry 50 | pub fn is_empty(&self) -> bool { 51 | self.inner().is_empty() 52 | } 53 | 54 | #[napi] 55 | /// Returns an iterator over the entries in this tree. 56 | pub fn iter(&self, this_ref: Reference, env: Env) -> Result { 57 | Ok(TreeIter { 58 | inner: this_ref.share_with(env, |tree| Ok(tree.inner().iter()))?, 59 | }) 60 | } 61 | 62 | #[napi] 63 | /// Lookup a tree entry by SHA value 64 | pub fn get_id(&self, this_ref: Reference, env: Env, id: String) -> Option { 65 | let reference = this_ref 66 | .share_with(env, |tree| { 67 | if let Some(entry) = tree 68 | .inner() 69 | .get_id(git2::Oid::from_str(&id).convert_without_message()?) 70 | { 71 | Ok(entry) 72 | } else { 73 | Err(Error::new(napi::Status::InvalidArg, "Tree entry not found")) 74 | } 75 | }) 76 | .ok()?; 77 | Some(TreeEntry { 78 | inner: TreeEntryInner::Ref(reference), 79 | }) 80 | } 81 | 82 | #[napi] 83 | /// Lookup a tree entry by its position in the tree 84 | pub fn get(&self, this_ref: Reference, env: Env, index: u32) -> Option { 85 | let reference = this_ref 86 | .share_with(env, |tree| { 87 | if let Some(entry) = tree.inner().get(index as usize) { 88 | Ok(entry) 89 | } else { 90 | Err(Error::new(napi::Status::InvalidArg, "Tree entry not found")) 91 | } 92 | }) 93 | .ok()?; 94 | Some(TreeEntry { 95 | inner: TreeEntryInner::Ref(reference), 96 | }) 97 | } 98 | 99 | #[napi] 100 | /// Lookup a tree entry by its filename 101 | pub fn get_name(&self, this_ref: Reference, env: Env, name: String) -> Option { 102 | let reference = this_ref 103 | .share_with(env, |tree| { 104 | if let Some(entry) = tree.inner().get_name(&name) { 105 | Ok(entry) 106 | } else { 107 | Err(Error::new(napi::Status::InvalidArg, "Tree entry not found")) 108 | } 109 | }) 110 | .ok()?; 111 | Some(TreeEntry { 112 | inner: TreeEntryInner::Ref(reference), 113 | }) 114 | } 115 | 116 | #[napi] 117 | /// Lookup a tree entry by its filename 118 | pub fn get_path(&self, this_ref: Reference, env: Env, name: String) -> Option { 119 | let reference = this_ref 120 | .share_with(env, |tree| { 121 | tree 122 | .inner() 123 | .get_path(Path::new(&name)) 124 | .convert_without_message() 125 | }) 126 | .ok()?; 127 | Some(TreeEntry { 128 | inner: TreeEntryInner::Ref(reference), 129 | }) 130 | } 131 | } 132 | 133 | impl<'a> AsRef> for Tree { 134 | fn as_ref(&self) -> &git2::Tree<'a> { 135 | match self.inner { 136 | TreeParent::Repository(ref parent) => parent.deref(), 137 | TreeParent::Reference(ref parent) => parent.deref(), 138 | TreeParent::Commit(ref parent) => parent.deref(), 139 | } 140 | } 141 | } 142 | 143 | #[napi(iterator)] 144 | pub struct TreeIter { 145 | pub(crate) inner: SharedReference>, 146 | } 147 | 148 | #[napi] 149 | impl Generator for TreeIter { 150 | type Yield = TreeEntry; 151 | type Return = (); 152 | type Next = (); 153 | 154 | fn next(&mut self, _value: Option<()>) -> Option { 155 | self.inner.next().map(|e| TreeEntry { 156 | inner: TreeEntryInner::Owned(e), 157 | }) 158 | } 159 | } 160 | 161 | pub(crate) enum TreeEntryInner { 162 | Owned(git2::TreeEntry<'static>), 163 | Ref(SharedReference>), 164 | } 165 | 166 | #[napi] 167 | pub struct TreeEntry { 168 | pub(crate) inner: TreeEntryInner, 169 | } 170 | 171 | impl Deref for TreeEntryInner { 172 | type Target = git2::TreeEntry<'static>; 173 | 174 | fn deref(&self) -> &Self::Target { 175 | match &self { 176 | TreeEntryInner::Owned(entry) => entry, 177 | TreeEntryInner::Ref(entry) => entry.deref(), 178 | } 179 | } 180 | } 181 | 182 | #[napi] 183 | impl TreeEntry { 184 | #[napi] 185 | /// Get the id of the object pointed by the entry 186 | pub fn id(&self) -> String { 187 | self.inner.id().to_string() 188 | } 189 | 190 | #[napi] 191 | /// Get the name of a tree entry 192 | pub fn name(&self) -> Result<&str> { 193 | self 194 | .inner 195 | .name() 196 | .ok_or_else(|| Error::from_reason("Invalid utf-8")) 197 | } 198 | 199 | #[napi] 200 | /// Get the filename of a tree entry 201 | pub fn name_bytes(&self) -> Uint8Array { 202 | self.inner.name_bytes().to_vec().into() 203 | } 204 | 205 | #[napi] 206 | /// Convert a tree entry to the object it points to. 207 | pub fn to_object(&self, env: Env, repo: Reference) -> Result { 208 | let object = repo.share_with(env, |repo| { 209 | self.inner.to_object(&repo.inner).convert_without_message() 210 | })?; 211 | Ok(GitObject { 212 | inner: ObjectParent::Repository(object), 213 | }) 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use napi::{Env, JsString, Result}; 4 | 5 | pub(crate) fn path_to_javascript_string(env: &Env, p: &Path) -> Result { 6 | #[cfg(unix)] 7 | { 8 | use std::borrow::Borrow; 9 | 10 | let path = p.to_string_lossy(); 11 | env.create_string(path.borrow()) 12 | } 13 | #[cfg(windows)] 14 | { 15 | use std::os::windows::ffi::OsStrExt; 16 | let path_buf = p.as_os_str().encode_wide().collect::>(); 17 | env.create_string_utf16(path_buf.as_slice()) 18 | } 19 | } 20 | --------------------------------------------------------------------------------