diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000000..98ed1523a4 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,65 @@ +name: CI +on: + pull_request: + merge_group: + +jobs: + test: + name: Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + build: [stable, beta, nightly, macos, windows] + include: + - build: stable + os: ubuntu-latest + rust: stable + - build: beta + os: ubuntu-latest + rust: beta + - build: nightly + os: ubuntu-latest + rust: nightly + - build: macos + os: macos-latest + rust: stable + - build: windows + os: windows-latest + rust: stable + steps: + - uses: actions/checkout@master + with: + submodules: true + - name: Install Rust (rustup) + run: rustup update ${{ matrix.rust }} --no-self-update && rustup default ${{ matrix.rust }} + shell: bash + - run: cargo test --locked + - run: cargo test --features https,ssh + - run: cargo run -p systest + - run: cargo test -p git2-curl + + rustfmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Install Rust + run: rustup update stable && rustup default stable && rustup component add rustfmt + - run: cargo fmt -- --check + + # The success job is here to consolidate the total success/failure state of + # all other jobs. This job is then included in the GitHub branch protection + # rule which prevents merges unless all other jobs are passing. This makes + # it easier to manage the list of jobs via this yml file and to prevent + # accidentally adding new jobs without also updating the branch protections. + success: + name: Success gate + if: always() + needs: + - test + - rustfmt + runs-on: ubuntu-latest + steps: + - run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}' + - name: Done + run: exit 0 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000000..eb3b578f1a --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,35 @@ +name: Publish +on: + workflow_dispatch: + inputs: + git2: + description: "Publish git2" + type: boolean + default: true + git2_curl: + description: "Publish git2-curl" + type: boolean + default: true + libgit2_sys: + description: "Publish libgit2-sys" + type: boolean + default: true + +permissions: + contents: write + +jobs: + publish: + name: Publish to crates.io + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + with: + submodules: true + - name: Publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + PUBLISH_GIT2: ${{ inputs.git2 }} + PUBLISH_GIT2_CURL: ${{ inputs.git2_curl }} + PUBLISH_LIBGIT2_SYS: ${{ inputs.libgit2_sys }} + run: ./ci/publish.sh diff --git a/.gitignore b/.gitignore index 2a729c83fa..ad91b1e09a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ target -Cargo.lock src/main.rs diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7c8a201ea0..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,41 +0,0 @@ -language: rust -rust: - - stable - - beta - - nightly -sudo: false -before_script: - - pip install 'travis-cargo<0.2' --user && export PATH=$HOME/.local/bin:$PATH -script: - - export CARGO_TARGET_DIR=`pwd`/target - - cargo test --no-default-features - - cargo test - - cargo run --manifest-path systest/Cargo.toml --release - - if [ "$TRAVIS_RUST_VERSION" = "nightly" ]; then - cargo test --features unstable; - cargo test --manifest-path git2-curl/Cargo.toml; - fi - - cargo doc --no-deps - - cargo doc --manifest-path=git2-curl/Cargo.toml --no-deps - - cargo doc --manifest-path=libgit2-sys/Cargo.toml --no-deps -after_success: - - travis-cargo --only nightly doc-upload - - travis-cargo coveralls --no-sudo -notifications: - email: - on_success: never -os: - - linux - - osx -addons: - apt: - sources: - - kalakris-cmake - packages: - - cmake - - libcurl4-openssl-dev - - libelf-dev - - libdw-dev -env: - global: - secure: HlDs6Eyhy/67Wmqd6frmCnOMYQyqbv2ulL9qqqFF2zdlinJ2/Z0FDs+0GHNWu0BQ9v6+51KHbhieUaz3dTYDCKPlDiA2OmE8DQuXloxrrJfGmPLc1F+cKQGn5a5FrIrLJDkEpcfWXZItRtzSPkpVNEWGA66Osx50/Nd8lkdjFYA= diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..55ccdc3b4b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,339 @@ +# Changelog + +## 0.20.2 - 2025-05-05 +[0.20.1...0.20.2](https://github.com/rust-lang/git2-rs/compare/git2-0.20.1...git2-0.20.2) + +### Added + +- Added `Status::WT_UNREADABLE`. + [#1151](https://github.com/rust-lang/git2-rs/pull/1151) + +### Fixed + +- Added missing codes for `GIT_EDIRECTORY`, `GIT_EMERGECONFLICT`, `GIT_EUNCHANGED`, `GIT_ENOTSUPPORTED`, and `GIT_EREADONLY` to `Error::raw_code`. + [#1153](https://github.com/rust-lang/git2-rs/pull/1153) +- Fixed missing initialization in `Indexer::new`. + [#1160](https://github.com/rust-lang/git2-rs/pull/1160) + +## 0.20.1 - 2025-03-17 +[0.20.0...0.20.1](https://github.com/rust-lang/git2-rs/compare/git2-0.20.0...git2-0.20.1) + +### Added + +- Added `Repository::branch_upstream_merge()` + [#1131](https://github.com/rust-lang/git2-rs/pull/1131) +- Added `Index::conflict_get()` + [#1134](https://github.com/rust-lang/git2-rs/pull/1134) +- Added `Index::conflict_remove()` + [#1133](https://github.com/rust-lang/git2-rs/pull/1133) +- Added `opts::set_cache_object_limit()` + [#1118](https://github.com/rust-lang/git2-rs/pull/1118) +- Added `Repo::merge_file_from_index()` and associated `MergeFileOptions` and `MergeFileResult`. + [#1062](https://github.com/rust-lang/git2-rs/pull/1062) + +### Changed + +- The `url` dependency minimum raised to 2.5.4 + [#1128](https://github.com/rust-lang/git2-rs/pull/1128) +- Changed the tracing callback to abort the process if the callback panics instead of randomly detecting the panic in some other function. + [#1121](https://github.com/rust-lang/git2-rs/pull/1121) +- Credential helper config (loaded with `CredentialHelper::config`) now checks for helpers that start with something that looks like an absolute path, rather than checking for a `/` or `\` anywhere in the helper string (which resolves an issue if the helper had arguments with `/` or `\`). + [#1137](https://github.com/rust-lang/git2-rs/pull/1137) + +### Fixed + +- Fixed panic in `Remote::url_bytes` if the url is empty. + [#1120](https://github.com/rust-lang/git2-rs/pull/1120) +- Fixed incorrect lifetimes on `Patch::delta`, `Patch::hunk`, and `Patch::line_in_hunk`. The return values must not outlive the `Patch`. + [#1141](https://github.com/rust-lang/git2-rs/pull/1141) +- Bumped requirement to libgit2-sys 0.18.1, which fixes linking of advapi32 on Windows. + [#1143](https://github.com/rust-lang/git2-rs/pull/1143) + + +## 0.20.0 - 2025-01-04 +[0.19.0...0.20.0](https://github.com/rust-lang/git2-rs/compare/git2-0.19.0...git2-0.20.0) + +### Added + +- `Debug` is now implemented for `transport::Service` + [#1074](https://github.com/rust-lang/git2-rs/pull/1074) +- Added `Repository::commondir` + [#1079](https://github.com/rust-lang/git2-rs/pull/1079) +- Added `Repository::merge_base_octopus` + [#1088](https://github.com/rust-lang/git2-rs/pull/1088) +- Restored impls for `PartialOrd`, `Ord`, and `Hash` for bitflags types that were inadvertently removed in a prior release. + [#1096](https://github.com/rust-lang/git2-rs/pull/1096) +- Added `CheckoutBuilder::disable_pathspec_match` + [#1107](https://github.com/rust-lang/git2-rs/pull/1107) +- Added `PackBuilder::write` + [#1110](https://github.com/rust-lang/git2-rs/pull/1110) + +### Changed + +- ❗ Updated to libgit2 [1.9.0](https://github.com/libgit2/libgit2/releases/tag/v1.9.0) + [#1111](https://github.com/rust-lang/git2-rs/pull/1111) +- ❗ Removed the `ssh_key_from_memory` Cargo feature, it was unused. + [#1087](https://github.com/rust-lang/git2-rs/pull/1087) +- ❗ Errors from `Tree::walk` are now correctly reported to the caller. + [#1098](https://github.com/rust-lang/git2-rs/pull/1098) +- ❗ The `trace_set` callback now takes a `&[u8]` instead of a `&str`. + [#1071](https://github.com/rust-lang/git2-rs/pull/1071) +- ❗ `Error::last_error` now returns `Error` instead of `Option`. + [#1072](https://github.com/rust-lang/git2-rs/pull/1072) + +### Fixed + +- Fixed `OdbReader::read` return value. + [#1061](https://github.com/rust-lang/git2-rs/pull/1061) +- When a credential helper executes a shell command, don't pop open a console window on Windows. + [#1075](https://github.com/rust-lang/git2-rs/pull/1075) + +## 0.19.0 - 2024-06-13 +[0.18.3...0.19.0](https://github.com/rust-lang/git2-rs/compare/git2-0.18.3...git2-0.19.0) + +### Added + +- Added `opts` functions to control server timeouts (`get_server_connect_timeout_in_milliseconds`, `set_server_connect_timeout_in_milliseconds`, `get_server_timeout_in_milliseconds`, `set_server_timeout_in_milliseconds`), and add `ErrorCode::Timeout`. + [#1052](https://github.com/rust-lang/git2-rs/pull/1052) + +### Changed + +- ❗ Updated to libgit2 [1.8.1](https://github.com/libgit2/libgit2/releases/tag/v1.8.1) + [#1032](https://github.com/rust-lang/git2-rs/pull/1032) +- Reduced size of the `Error` struct. + [#1053](https://github.com/rust-lang/git2-rs/pull/1053) + +### Fixed + +- Fixed some callbacks to relay the error from the callback to libgit2. + [#1043](https://github.com/rust-lang/git2-rs/pull/1043) + +## 0.18.3 - 2024-03-18 +[0.18.2...0.18.3](https://github.com/rust-lang/git2-rs/compare/git2-0.18.2...git2-0.18.3) + +### Added + +- Added `opts::` functions to get / set libgit2 mwindow options + [#1035](https://github.com/rust-lang/git2-rs/pull/1035) + + +### Changed + +- Updated examples to use clap instead of structopt + [#1007](https://github.com/rust-lang/git2-rs/pull/1007) + +## 0.18.2 - 2024-02-06 +[0.18.1...0.18.2](https://github.com/rust-lang/git2-rs/compare/git2-0.18.1...git2-0.18.2) + +### Added + +- Added `opts::set_ssl_cert_file` and `opts::set_ssl_cert_dir` for setting Certificate Authority file locations. + [#997](https://github.com/rust-lang/git2-rs/pull/997) +- Added `TreeIter::nth` which makes jumping ahead in the iterator more efficient. + [#1004](https://github.com/rust-lang/git2-rs/pull/1004) +- Added `Repository::find_commit_by_prefix` to find a commit by a shortened hash. + [#1011](https://github.com/rust-lang/git2-rs/pull/1011) +- Added `Repository::find_tag_by_prefix` to find a tag by a shortened hash. + [#1015](https://github.com/rust-lang/git2-rs/pull/1015) +- Added `Repository::find_object_by_prefix` to find an object by a shortened hash. + [#1014](https://github.com/rust-lang/git2-rs/pull/1014) + +### Changed + +- ❗ Updated to libgit2 [1.7.2](https://github.com/libgit2/libgit2/releases/tag/v1.7.2). + This fixes [CVE-2024-24575](https://github.com/libgit2/libgit2/security/advisories/GHSA-54mf-x2rh-hq9v) and [CVE-2024-24577](https://github.com/libgit2/libgit2/security/advisories/GHSA-j2v7-4f6v-gpg8). + [#1017](https://github.com/rust-lang/git2-rs/pull/1017) + +## 0.18.1 - 2023-09-20 +[0.18.0...0.18.1](https://github.com/rust-lang/git2-rs/compare/git2-0.18.0...git2-0.18.1) + +### Added + +- Added `FetchOptions::depth` to set the depth of a fetch or clone, adding support for shallow clones. + [#979](https://github.com/rust-lang/git2-rs/pull/979) + +### Fixed + +- Fixed an internal data type (`TreeWalkCbData`) to not assume it is a transparent type while casting. + [#989](https://github.com/rust-lang/git2-rs/pull/989) +- Fixed so that `DiffPatchidOptions` and `StashSaveOptions` are publicly exported allowing the corresponding APIs to actually be used. + [#988](https://github.com/rust-lang/git2-rs/pull/988) + +## 0.18.0 - 2023-08-28 +[0.17.2...0.18.0](https://github.com/rust-lang/git2-rs/compare/0.17.2...git2-0.18.0) + +### Added + +- Added `Blame::blame_buffer` for getting blame data for a file that has been modified in memory. + [#981](https://github.com/rust-lang/git2-rs/pull/981) + +### Changed + +- Updated to libgit2 [1.7.0](https://github.com/libgit2/libgit2/releases/tag/v1.7.0). + [#968](https://github.com/rust-lang/git2-rs/pull/968) +- Updated to libgit2 [1.7.1](https://github.com/libgit2/libgit2/releases/tag/v1.7.1). + [#982](https://github.com/rust-lang/git2-rs/pull/982) +- Switched from bitflags 1.x to 2.1. This brings some small changes to types generated by bitflags. + [#973](https://github.com/rust-lang/git2-rs/pull/973) +- Changed `Revwalk::with_hide_callback` to take a mutable reference to its callback to enforce type safety. + [#970](https://github.com/rust-lang/git2-rs/pull/970) +- Implemented `FusedIterator` for many iterators that can support it. + [#955](https://github.com/rust-lang/git2-rs/pull/955) + +### Fixed + +- Fixed builds with cargo's `-Zminimal-versions`. + [#960](https://github.com/rust-lang/git2-rs/pull/960) + +## 0.17.2 - 2023-05-27 +[0.17.1...0.17.2](https://github.com/rust-lang/git2-rs/compare/0.17.1...0.17.2) + +### Added +- Added support for stashing with options (which can support partial stashing). + [#930](https://github.com/rust-lang/git2-rs/pull/930) + +## 0.17.1 - 2023-04-13 +[0.17.0...0.17.1](https://github.com/rust-lang/git2-rs/compare/0.17.0...0.17.1) + +### Changed + +- Updated to libgit2 [1.6.4](https://github.com/libgit2/libgit2/releases/tag/v1.6.4). + [#948](https://github.com/rust-lang/git2-rs/pull/948) + +## 0.17.0 - 2023-04-02 +[0.16.1...0.17.0](https://github.com/rust-lang/git2-rs/compare/0.16.1...0.17.0) + +### Added + +- Added `IntoIterator` implementation for `Statuses`. + [#880](https://github.com/rust-lang/git2-rs/pull/880) +- Added `Reference::symbolic_set_target` + [#893](https://github.com/rust-lang/git2-rs/pull/893) +- Added `Copy`, `Clone`, `Debug`, `PartialEq`, and `Eq` implementations for `AutotagOption` and `FetchPrune`. + [#889](https://github.com/rust-lang/git2-rs/pull/889) +- Added `Eq` and `PartialEq` implementations for `Signature`. + [#890](https://github.com/rust-lang/git2-rs/pull/890) +- Added `Repository::discover_path`. + [#883](https://github.com/rust-lang/git2-rs/pull/883) +- Added `Submodule::repo_init`. + [#914](https://github.com/rust-lang/git2-rs/pull/914) +- Added `Tag::is_valid_name`. + [#882](https://github.com/rust-lang/git2-rs/pull/882) +- Added `Repository::set_head_bytes`. + [#931](https://github.com/rust-lang/git2-rs/pull/931) +- Added the `Indexer` type which is a low-level API for storing and indexing pack files. + [#911](https://github.com/rust-lang/git2-rs/pull/911) +- Added `Index::find_prefix`. + [#903](https://github.com/rust-lang/git2-rs/pull/903) +- Added support for the deprecated group-writeable blob mode. This adds a new variant to `FileMode`. + [#887](https://github.com/rust-lang/git2-rs/pull/887) +- Added `PushCallbacks::push_negotiation` callback and the corresponding `PushUpdate` type for getting receiving information about the updates to perform. + [#926](https://github.com/rust-lang/git2-rs/pull/926) + +### Changed + +- Updated to libgit2 [1.6.3](https://github.com/libgit2/libgit2/blob/main/docs/changelog.md#v163). + This brings in many changes, including better SSH host key support on Windows and better SSH host key algorithm negotiation. + 1.6.3 is now the minimum supported version. + [#935](https://github.com/rust-lang/git2-rs/pull/935) +- Updated libssh2-sys from 0.2 to 0.3. + This brings in numerous changes, including SHA2 algorithm support with RSA. + [#919](https://github.com/rust-lang/git2-rs/pull/919) +- Changed `RemoteCallbacks::credentials` callback error handler to correctly set the libgit2 error class. + [#918](https://github.com/rust-lang/git2-rs/pull/918) +- `DiffOptions::flag` now takes a `git_diff_option_t` type. + [#935](https://github.com/rust-lang/git2-rs/pull/935) + + +## 0.16.1 - 2023-01-20 +[0.16.0...0.16.1](https://github.com/rust-lang/git2-rs/compare/0.16.0...0.16.1) + +### Changed +- Updated to [libgit2-sys 0.14.2+1.5.1](libgit2-sys/CHANGELOG.md#0142151---2023-01-20) + +## 0.16.0 - 2023-01-10 +[0.15.0...0.16.0](https://github.com/rust-lang/git2-rs/compare/0.15.0...0.16.0) + +### Changed +- Added ability to get the SSH host key and its type. + This includes an API breaking change to the `certificate_check` callback. + [#909](https://github.com/rust-lang/git2-rs/pull/909) +- Updated to [libgit2-sys 0.14.1+1.5.0](libgit2-sys/CHANGELOG.md#0141150---2023-01-10) + +## 0.15.0 - 2022-07-28 +[0.14.4...0.15.0](https://github.com/rust-lang/git2-rs/compare/0.14.4...0.15.0) + +### Added +- Added `Repository::tag_annotation_create` binding `git_tag_annotation_create`. + [#845](https://github.com/rust-lang/git2-rs/pull/845) +- Added the `Email` type which represents a patch in mbox format for sending via email. + Added the `EmailCreateOptions` struct to control formatting of the email. + Deprecates `Diff::format_email`, use `Email::from_diff` instead. + [#847](https://github.com/rust-lang/git2-rs/pull/847) +- Added `ErrorCode::Owner` to map to the new `GIT_EOWNER` errors. + [#839](https://github.com/rust-lang/git2-rs/pull/839) +- Added `opts::set_verify_owner_validation` to set whether or not ownership validation is performed. + [#839](https://github.com/rust-lang/git2-rs/pull/839) + +### Changed +- Updated to [libgit2-sys 0.14.0+1.5.0](libgit2-sys/CHANGELOG.md#0140150---2022-07-28) +- Removed the `Iterator` implementation for `ConfigEntries` due to the unsound usage of the API which allowed values to be used after free. + Added `ConfigEntries::next` and `ConfigEntries::for_each` for iterating over all entries in a safe manor. + [#854](https://github.com/rust-lang/git2-rs/pull/854) + +## 0.14.4 - 2022-05-19 +[0.14.3...0.14.4](https://github.com/rust-lang/git2-rs/compare/0.14.3...0.14.4) + +### Added +- Added `Commit::body` and `Commit::body_bytes` for retrieving the commit message body. + [#835](https://github.com/rust-lang/git2-rs/pull/835) +- Added `Tree::get_name_bytes` to handle non-UTF-8 entry names. + [#841](https://github.com/rust-lang/git2-rs/pull/841) + +### Changed +- Updated to [libgit2-sys 0.13.4+1.4.2](libgit2-sys/CHANGELOG.md#0134142---2022-05-10) + +## 0.14.3 - 2022-04-27 +[0.14.2...0.14.3](https://github.com/rust-lang/git2-rs/compare/0.14.2...0.14.3) + +### Changed +- Updated to [libgit2-sys 0.13.3+1.4.2](libgit2-sys/CHANGELOG.md#0133142---2022-04-27) + +### Fixed +- Fixed the lifetime of `Remote::create_detached`. + [#825](https://github.com/rust-lang/git2-rs/pull/825) + +## 0.14.2 - 2022-03-10 +[0.14.1...0.14.2](https://github.com/rust-lang/git2-rs/compare/0.14.1...0.14.2) + +### Added +- Added `Odb::exists_ext` to checks if an object database has an object, with extended flags. + [#818](https://github.com/rust-lang/git2-rs/pull/818) + +### Changed +- Updated to [libgit2-sys 0.13.2+1.4.2](libgit2-sys/CHANGELOG.md#0132142---2022-03-10) + +## 0.14.1 - 2022-02-28 +[0.14.0...0.14.1](https://github.com/rust-lang/git2-rs/compare/0.14.0...0.14.1) + +### Changed +- Updated to [libgit2-sys 0.13.1+1.4.2](libgit2-sys/CHANGELOG.md#0131142---2022-02-28) + +## 0.14.0 - 2022-02-24 +[0.13.25...0.14.0](https://github.com/rust-lang/git2-rs/compare/0.13.25...0.14.0) + +### Added +- Added `opts::get_extensions` and `opts::set_extensions` to support git extensions. + [#791](https://github.com/rust-lang/git2-rs/pull/791) +- Added `PackBuilder::name` and `PackBuilder::name_bytes`. + [#806](https://github.com/rust-lang/git2-rs/pull/806) + - Deprecated `PackBuilder::hash`, use `PackBuilder::name` instead. +- Added `FetchOptions::follow_redirects` and `PushOptions::follow_redirects`. + [#806](https://github.com/rust-lang/git2-rs/pull/806) +- Added `StatusOptions::rename_threshold`. + [#806](https://github.com/rust-lang/git2-rs/pull/806) + +### Changed +- Updated to [libgit2-sys 0.13.0+1.4.1](libgit2-sys/CHANGELOG.md#0130141---2022-02-24) + [#806](https://github.com/rust-lang/git2-rs/pull/806) + [#811](https://github.com/rust-lang/git2-rs/pull/811) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..c842f1eec5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,63 @@ +# Contributing + +## Updating libgit2 + +The following steps can be used to update libgit2: + +1. Update the submodule. + There are several ways to go about this. + One way is to go to the `libgit2-sys/libgit2` directory and run `git fetch origin` to download the latest updates, and then check out a specific tag (such as `git checkout v1.4.1`). +2. Update all the references to the version: + * Update [`libgit2-sys/build.rs`](https://github.com/rust-lang/git2-rs/blob/master/libgit2-sys/build.rs). + There is a version probe (search for `cfg.range_version`) which should be updated. + * Update the version in + [`libgit2-sys/Cargo.toml`](https://github.com/rust-lang/git2-rs/blob/master/libgit2-sys/Cargo.toml). + Update the metadata portion (the part after the `+`) to match libgit2. + Also bump the Cargo version (the part before the `+`), keeping in mind + if this will be a SemVer breaking change or not. + * Update the dependency version in [`Cargo.toml`](https://github.com/rust-lang/git2-rs/blob/master/Cargo.toml) to match the version in the last step (do not include the `+` metadata). + Also update the version of the `git2` crate itself so it will pick up the change to `libgit2-sys` (also keeping in mind if it is a SemVer breaking release). + * Update the version in [`README.md`](https://github.com/rust-lang/git2-rs/blob/master/README.md) if needed. + There are two places, the `Cargo.toml` example and the description of the libgit2 version it binds with. + * If there was a SemVer-breaking version bump for either library, also update the `html_root_url` attribute in the `lib.rs` of each library. +3. Run tests. + `cargo test -p git2 -p git2-curl` is a good starting point. +4. Run `systest`. + This will validate for any C-level API problems. + + `cargo run -p systest` + + The changelog at + can be helpful for seeing what has changed. + The project has recently started labeling API and ABI breaking changes with labels: + + Alternatively, running `git diff [PREV_VERSION]..[NEW_VERSION] --ignore-all-space -- include/` can provide an overview of changes made to the API. +4. Once you have everything functional, publish a PR with the updates. + +## Release process + +Checklist for preparing for a release: + +- Make sure the versions have been bumped and are pointing at what is expected. + - Version of `libgit2-sys` + - Version of `git2` + - Version of `git2-curl` + - `git2`'s dependency on `libgit2-sys` + - `git2-curl`'s dependency on `git2` + - The libgit2 version probe in `libgit2-sys/build.rs` + - Update the version in `README.md` + - Check the `html_root_url` values in the source code. +- Update the change logs: + - [`CHANGELOG.md`](https://github.com/rust-lang/git2-rs/blob/master/CHANGELOG.md) + - [`libgit2-sys/CHANGELOG.md`](https://github.com/rust-lang/git2-rs/blob/master/libgit2-sys/CHANGELOG.md) + - [`git2-curl/CHANGELOG.md`](https://github.com/rust-lang/git2-rs/blob/master/git2-curl/CHANGELOG.md) + +There is a GitHub workflow to handle publishing to crates.io and tagging the release. There are two different ways to run it: + +- In the GitHub web UI: + 1. Go to (you can navigate here via the "Actions" tab at the top). + 2. Click the "Run workflow" drop-down on the right. + 3. Choose which crates to publish. It's OK to leave everything checked, it will skip if it is already published. Uncheck a crate if the version has been bumped in git, but you don't want to publish that particular one, yet. + 4. Click "Run workflow" +- In the CLI: + 1. Run `gh workflow run publish.yml -R rust-lang/git2-rs` diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000..d302b33c3b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1209 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "cc" +version = "1.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "clap" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "ctest2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cf53ed2a9e25d98cd444fd6e73eae7de0a26a8cb9e3f998170c6901a1afa0e5" +dependencies = [ + "cc", + "garando_syntax", + "rustc_version", +] + +[[package]] +name = "curl" +version = "0.4.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e2d5c8f48d9c0c23250e52b55e82a6ab4fdba6650c931f5a0a57a43abda812b" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2", + "windows-sys 0.59.0", +] + +[[package]] +name = "curl-sys" +version = "0.4.82+curl-8.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4d63638b5ec65f1a4ae945287b3fd035be4554bbaf211901159c9a2a74fb5be" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "windows-sys 0.59.0", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "dirs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +dependencies = [ + "cfg-if 0.1.10", + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "garando_errors" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18495ec4aced5922809efe4d2862918ff0e8d75e122bde57bba9bae45965256a" +dependencies = [ + "garando_pos", + "libc", + "serde", + "term", + "unicode-xid", +] + +[[package]] +name = "garando_pos" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9386fc75dca486daefbbf5a8d2ea6f379237f95c9b982776159cd66f220aaf" +dependencies = [ + "serde", +] + +[[package]] +name = "garando_syntax" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8a383861d12fc78c251bbcb1ec252dd8338714ce02ab0cc393cfd02f40d32b" +dependencies = [ + "bitflags 1.3.2", + "garando_errors", + "garando_pos", + "log", + "serde", + "serde_json", + "unicode-xid", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if 1.0.1", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if 1.0.1", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "git2" +version = "0.21.0" +dependencies = [ + "bitflags 2.9.1", + "clap", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "tempfile", + "time", + "url", +] + +[[package]] +name = "git2-curl" +version = "0.22.0" +dependencies = [ + "curl", + "git2", + "log", + "tempfile", + "url", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libgit2-sys" +version = "0.18.2+1.9.1" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.1", + "libc", +] + +[[package]] +name = "libssh2-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +dependencies = [ + "cc", + "cmake", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-src" +version = "300.5.0+3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "systest" +version = "0.1.0" +dependencies = [ + "ctest2", + "libc", + "libgit2-sys", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "term" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0863a3345e70f61d613eab32ee046ccd1bcc5f9105fe402c61fcd0c13eeb8b5" +dependencies = [ + "dirs", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index d175086b47..30a496deaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,34 +1,53 @@ [package] - name = "git2" -version = "0.4.3" -authors = ["Alex Crichton "] -license = "MIT/Apache-2.0" +version = "0.21.0" +authors = ["Josh Triplett ", "Alex Crichton "] +license = "MIT OR Apache-2.0" readme = "README.md" keywords = ["git"] -repository = "https://github.com/alexcrichton/git2-rs" -homepage = "https://github.com/alexcrichton/git2-rs" -documentation = "http://alexcrichton.com/git2-rs" +repository = "https://github.com/rust-lang/git2-rs" +documentation = "https://docs.rs/git2" description = """ Bindings to libgit2 for interoperating with git repositories. This library is both threadsafe and memory safe and allows both reading and writing git repositories. """ +categories = ["api-bindings"] +edition = "2021" [dependencies] -url = "1.0" -bitflags = "0.1" +url = { version = "2.5.4", optional = true } +bitflags = "2.1.0" libc = "0.2" -libgit2-sys = { path = "libgit2-sys", version = "0.4.3" } +log = "0.4.8" +libgit2-sys = { path = "libgit2-sys", version = "0.18.1" } + +[target."cfg(all(unix, not(target_os = \"macos\")))".dependencies] +openssl-sys = { version = "0.9.45", optional = true } +openssl-probe = { version = "0.1", optional = true } [dev-dependencies] -docopt = "0.6" -rustc-serialize = "0.3" -time = "0.1" -tempdir = "0.3" +clap = { version = "4.4.13", features = ["derive"] } +time = { version = "0.3.37", features = ["formatting"] } +tempfile = "3.1.0" +url = "2.5.4" [features] unstable = [] -default = ["ssh", "https"] -ssh = ["libgit2-sys/ssh"] -https = ["libgit2-sys/https"] +default = [] +ssh = ["libgit2-sys/ssh", "cred"] +https = ["libgit2-sys/https", "openssl-sys", "openssl-probe", "cred"] +# Include support for credentials, which pulls in the `url` crate and all its dependencies +cred = ["dep:url"] +vendored-libgit2 = ["libgit2-sys/vendored"] +vendored-openssl = ["openssl-sys/vendored", "libgit2-sys/vendored-openssl"] +zlib-ng-compat = ["libgit2-sys/zlib-ng-compat"] + +[workspace] +members = ["systest", "git2-curl"] + +[package.metadata.docs.rs] +features = ["https", "ssh"] + +[[examples]] +required-features = ["https", "ssh"] diff --git a/FUNDING.json b/FUNDING.json new file mode 100644 index 0000000000..480d41540c --- /dev/null +++ b/FUNDING.json @@ -0,0 +1,7 @@ +{ + "drips": { + "ethereum": { + "ownedBy": "0x298f6e7CC02D6aa94E2b135f46F1761da7A44E58" + } + } +} diff --git a/README.md b/README.md index 8b4b5e2cec..516a087a6e 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,87 @@ # git2-rs -[![Build Status](https://travis-ci.org/alexcrichton/git2-rs.svg?branch=master)](https://travis-ci.org/alexcrichton/git2-rs) -[![Build Status](https://ci.appveyor.com/api/projects/status/6vem3xgno2kuxnfm?svg=true)](https://ci.appveyor.com/project/alexcrichton/git2-rs) +[Documentation](https://docs.rs/git2) -[Documentation](http://alexcrichton.com/git2-rs/git2/index.html) +libgit2 bindings for Rust. -libgit2 bindings for Rust +``` +cargo add git2 +``` + +## Features -```toml -[dependencies] -git2 = "0.3" +By default, git2 includes support for working with local repositories, but does +not include network support (e.g. cloning remote repositories). If you want to +use features that require network support, you may need the `"https"` and/or +`"ssh"` features. If you support user-provided repository URLs, you probably +want to enable both. + +``` +cargo add git2 --features https,ssh ``` -## Building git2-rs +## Rust version requirements + +git2-rs works with stable Rust, and typically works with the most recent prior +stable release as well. + +## Version of libgit2 + +Currently this library requires libgit2 1.9.0 (or newer patch versions). The +source for libgit2 is included in the libgit2-sys crate so there's no need to +pre-install the libgit2 library, the libgit2-sys crate will figure that and/or +build that for you. On the other hand, if an appropriate version of `libgit2` +is present, `git2` will attempt to dynamically link it. + +To be more precise, the vendored `libgit2` is linked statically if two +conditions both hold: + +- The environment variable `LIBGIT2_NO_VENDOR=1` is **not** set +- **and** either a) The Cargo feature `vendored-libgit2` is set or b) an + appropriate version of `libgit2` cannot be found on the system. -First, you'll need to install _CMake_. Afterwards, just run: +In particular, note that the environment variable overrides the Cargo feature. + +## Building git2-rs ```sh -$ git clone https://github.com/alexcrichton/git2-rs +$ git clone https://github.com/rust-lang/git2-rs $ cd git2-rs $ cargo build ``` -## Building on OSX 10.10+ +### Automating Testing -Currently libssh2 requires linking against OpenSSL, and to compile libssh2 it -also needs to find the OpenSSL headers. On OSX 10.10+ the OpenSSL headers have -been removed, but if you're using Homebrew you can install them via: +Running tests and handling all of the associated edge cases on every commit +proves tedious very quickly. To automate tests and handle proper stashing and +unstashing of unstaged changes and thus avoid nasty surprises, use the +pre-commit hook found [here][pre-commit-hook] and place it into the +`.git/hooks/` with the name `pre-commit`. You may need to add execution +permissions with `chmod +x`. -```sh -brew install openssl -``` +To skip tests on a simple commit or doc-fixes, use `git commit --no-verify`. -To get this library to pick them up the [standard `rust-openssl` -instructions][instr] can be used to transitively inform libssh2-sys about where -the header files are: +## Building on macOS 10.10+ -[instr]: https://github.com/sfackler/rust-openssl#osx - -```sh -export OPENSSL_INCLUDE_DIR=`brew --prefix openssl`/include -export OPENSSL_LIB_DIR=`brew --prefix openssl`/lib -``` +If the `ssh` feature is enabled then this library depends +on libssh2 which depends on OpenSSL. To get OpenSSL working follow the +[`openssl` crate's instructions](https://github.com/sfackler/rust-openssl/blob/master/openssl/src/lib.rs#L31). # License -`git2-rs` is primarily distributed under the terms of both the MIT license and -the Apache License (Version 2.0), with portions covered by various BSD-like -licenses. +This project is licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or + https://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or + https://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in git2-rs by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. -See LICENSE-APACHE, and LICENSE-MIT for details. +[pre-commit-hook]: https://gist.github.com/glfmn/0c5e9e2b41b48007ed3497d11e3dbbfa diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 13fe54d58a..0000000000 --- a/appveyor.yml +++ /dev/null @@ -1,23 +0,0 @@ -environment: - matrix: - - TARGET: x86_64-pc-windows-gnu - MSYS_BITS: 64 - - TARGET: i686-pc-windows-gnu - MSYS_BITS: 32 - - TARGET: x86_64-pc-windows-msvc - - TARGET: i686-pc-windows-msvc -install: - - ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-nightly-$%7Benv:TARGET%7D.exe" - - rust-nightly-%TARGET%.exe /VERYSILENT /NORESTART /DIR="C:\Program Files (x86)\Rust" - - set PATH=%PATH%;C:\Program Files (x86)\Rust\bin - - if defined MSYS_BITS set PATH=%PATH%;C:\msys64\mingw%MSYS_BITS%\bin;C:\msys64\usr\bin - - set CARGO_TARGET_DIR=%APPVEYOR_BUILD_FOLDER%\target - - rustc -V - - cargo -V - -build: false - -test_script: - - cargo test - - cargo test --no-default-features - - cargo run --manifest-path systest/Cargo.toml diff --git a/ci/publish.sh b/ci/publish.sh new file mode 100755 index 0000000000..cfc084699c --- /dev/null +++ b/ci/publish.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +set -e + +function publish { + publish_this="$1" + crate_name="$2" + manifest="$3" + + if [ "$publish_this" != "true" ] + then + echo "Skipping $crate_name, publish not requested." + return + fi + + # Get the version from Cargo.toml + version=`sed -n -E 's/^version = "(.*)"/\1/p' $manifest` + + # Check crates.io if it is already published + set +e + output=`curl --fail --silent --head --location https://static.crates.io/crates/$crate_name/$version/download` + res="$?" + set -e + case $res in + 0) + echo "${crate_name}@${version} appears to already be published" + return + ;; + 22) ;; + *) + echo "Failed to check ${crate_name}@${version} res: $res" + echo "$output" + exit 1 + ;; + esac + + cargo publish --manifest-path $manifest --no-verify + + tag="${crate_name}-${version}" + git tag $tag + git push origin "$tag" +} + +publish $PUBLISH_LIBGIT2_SYS libgit2-sys libgit2-sys/Cargo.toml +publish $PUBLISH_GIT2 git2 Cargo.toml +publish $PUBLISH_GIT2_CURL git2-curl git2-curl/Cargo.toml diff --git a/examples/add.rs b/examples/add.rs index d4cb718f46..57c9bb10a9 100644 --- a/examples/add.rs +++ b/examples/add.rs @@ -15,38 +15,46 @@ #![deny(warnings)] #![allow(trivial_casts)] -extern crate git2; -extern crate docopt; -extern crate rustc_serialize; - -use std::path::Path; -use docopt::Docopt; +use clap::Parser; use git2::Repository; +use std::path::Path; -#[derive(RustcDecodable)] +#[derive(Parser)] struct Args { + #[structopt(name = "spec")] arg_spec: Vec, + #[structopt(name = "dry_run", short = 'n', long)] + /// dry run flag_dry_run: bool, + #[structopt(name = "verbose", short, long)] + /// be verbose flag_verbose: bool, + #[structopt(name = "update", short, long)] + /// update tracked files flag_update: bool, } fn run(args: &Args) -> Result<(), git2::Error> { - let repo = try!(Repository::open(&Path::new("."))); - let mut index = try!(repo.index()); + let repo = Repository::open(&Path::new("."))?; + let mut index = repo.index()?; let cb = &mut |path: &Path, _matched_spec: &[u8]| -> i32 { let status = repo.status_file(path).unwrap(); - let ret = if status.contains(git2::STATUS_WT_MODIFIED) || - status.contains(git2::STATUS_WT_NEW) { + let ret = if status.contains(git2::Status::WT_MODIFIED) + || status.contains(git2::Status::WT_NEW) + { println!("add '{}'", path.display()); 0 } else { 1 }; - if args.flag_dry_run {1} else {ret} + if args.flag_dry_run { + 1 + } else { + ret + } }; let cb = if args.flag_verbose || args.flag_update { Some(cb as &mut git2::IndexMatchedPath) @@ -55,28 +63,17 @@ fn run(args: &Args) -> Result<(), git2::Error> { }; if args.flag_update { - try!(index.update_all(args.arg_spec.iter(), cb)); + index.update_all(args.arg_spec.iter(), cb)?; } else { - try!(index.add_all(args.arg_spec.iter(), git2::ADD_DEFAULT, cb)); + index.add_all(args.arg_spec.iter(), git2::IndexAddOption::DEFAULT, cb)?; } - try!(index.write()); + index.write()?; Ok(()) } fn main() { - const USAGE: &'static str = " -usage: add [options] [--] [..] - -Options: - -n, --dry-run dry run - -v, --verbose be verbose - -u, --update update tracked files - -h, --help show this message -"; - - let args = Docopt::new(USAGE).and_then(|d| d.decode()) - .unwrap_or_else(|e| e.exit()); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/examples/blame.rs b/examples/blame.rs index b51e223d9c..202989b3f0 100644 --- a/examples/blame.rs +++ b/examples/blame.rs @@ -12,26 +12,33 @@ * . */ -extern crate git2; -extern crate docopt; -extern crate rustc_serialize; +#![deny(warnings)] -use docopt::Docopt; -use git2::{Repository, BlameOptions}; +use clap::Parser; +use git2::{BlameOptions, Repository}; +use std::io::{BufRead, BufReader}; use std::path::Path; -use std::io::{BufReader, BufRead}; -#[derive(RustcDecodable)] #[allow(non_snake_case)] +#[derive(Parser)] +#[allow(non_snake_case)] struct Args { + #[structopt(name = "path")] arg_path: String, + #[structopt(name = "spec")] arg_spec: Option, + #[structopt(short = 'M')] + /// find line moves within and across files flag_M: bool, + #[structopt(short = 'C')] + /// find line copies within and across files flag_C: bool, + #[structopt(short = 'F')] + /// follow only the first parent commits flag_F: bool, } fn run(args: &Args) -> Result<(), git2::Error> { - let repo = try!(Repository::open(".")); + let repo = Repository::open(".")?; let path = Path::new(&args.arg_path[..]); // Prepare our blame options @@ -44,12 +51,11 @@ fn run(args: &Args) -> Result<(), git2::Error> { // Parse spec if let Some(spec) = args.arg_spec.as_ref() { + let revspec = repo.revparse(spec)?; - let revspec = try!(repo.revparse(spec)); - - let (oldest, newest) = if revspec.mode().contains(git2::REVPARSE_SINGLE) { + let (oldest, newest) = if revspec.mode().contains(git2::RevparseMode::SINGLE) { (None, revspec.from()) - } else if revspec.mode().contains(git2::REVPARSE_RANGE) { + } else if revspec.mode().contains(git2::RevparseMode::RANGE) { (revspec.from(), revspec.to()) } else { (None, None) @@ -65,21 +71,24 @@ fn run(args: &Args) -> Result<(), git2::Error> { commit_id = format!("{}", commit.id()) } } - } let spec = format!("{}:{}", commit_id, path.display()); - let blame = try!(repo.blame_file(path, Some(&mut opts))); - let object = try!(repo.revparse_single(&spec[..])); - let blob = try!(repo.find_blob(object.id())); + let blame = repo.blame_file(path, Some(&mut opts))?; + let object = repo.revparse_single(&spec[..])?; + let blob = repo.find_blob(object.id())?; let reader = BufReader::new(blob.content()); for (i, line) in reader.lines().enumerate() { - if let (Ok(line), Some(hunk)) = (line, blame.get_line(i+1)) { + if let (Ok(line), Some(hunk)) = (line, blame.get_line(i + 1)) { let sig = hunk.final_signature(); - println!("{} {} <{}> {}", hunk.final_commit_id(), - String::from_utf8_lossy(sig.name_bytes()), - String::from_utf8_lossy(sig.email_bytes()), line); + println!( + "{} {} <{}> {}", + hunk.final_commit_id(), + String::from_utf8_lossy(sig.name_bytes()), + String::from_utf8_lossy(sig.email_bytes()), + line + ); } } @@ -87,17 +96,7 @@ fn run(args: &Args) -> Result<(), git2::Error> { } fn main() { - const USAGE: &'static str = " -usage: blame [options] [] - -Options: - -M find line moves within and across files - -C find line copies within and across files - -F follow only the first parent commits -"; - - let args = Docopt::new(USAGE).and_then(|d| d.decode()) - .unwrap_or_else(|e| e.exit()); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/examples/cat-file.rs b/examples/cat-file.rs index d9be6c068e..6196b9bb9f 100644 --- a/examples/cat-file.rs +++ b/examples/cat-file.rs @@ -14,41 +14,49 @@ #![deny(warnings)] -extern crate git2; -extern crate docopt; -extern crate rustc_serialize; - use std::io::{self, Write}; -use docopt::Docopt; -use git2::{Repository, ObjectType, Blob, Commit, Signature, Tag, Tree}; +use clap::Parser; +use git2::{Blob, Commit, ObjectType, Repository, Signature, Tag, Tree}; -#[derive(RustcDecodable)] +#[derive(Parser)] struct Args { + #[structopt(name = "object")] arg_object: String, + #[structopt(short = 't')] + /// show the object type flag_t: bool, + #[structopt(short = 's')] + /// show the object size flag_s: bool, + #[structopt(short = 'e')] + /// suppress all output flag_e: bool, + #[structopt(short = 'p')] + /// pretty print the contents of the object flag_p: bool, + #[structopt(name = "quiet", short, long)] + /// suppress output flag_q: bool, + #[structopt(name = "verbose", short, long)] flag_v: bool, + #[structopt(name = "dir", long = "git-dir")] + /// use the specified directory as the base directory flag_git_dir: Option, } fn run(args: &Args) -> Result<(), git2::Error> { let path = args.flag_git_dir.as_ref().map(|s| &s[..]).unwrap_or("."); - let repo = try!(Repository::open(path)); + let repo = Repository::open(path)?; - let obj = try!(repo.revparse_single(&args.arg_object)); + let obj = repo.revparse_single(&args.arg_object)?; if args.flag_v && !args.flag_q { println!("{} {}\n--", obj.kind().unwrap().str(), obj.id()); } if args.flag_t { println!("{}", obj.kind().unwrap().str()); - } else if args.flag_s { - /* ... */ - } else if args.flag_e { + } else if args.flag_s || args.flag_e { /* ... */ } else if args.flag_p { match obj.kind() { @@ -64,9 +72,7 @@ fn run(args: &Args) -> Result<(), git2::Error> { Some(ObjectType::Tree) => { show_tree(obj.as_tree().unwrap()); } - Some(ObjectType::Any) | None => { - println!("unknown {}", obj.id()) - } + Some(ObjectType::Any) | None => println!("unknown {}", obj.id()), } } Ok(()) @@ -101,41 +107,41 @@ fn show_tag(tag: &Tag) { fn show_tree(tree: &Tree) { for entry in tree.iter() { - println!("{:06o} {} {}\t{}", - entry.filemode(), - entry.kind().unwrap().str(), - entry.id(), - entry.name().unwrap()); + println!( + "{:06o} {} {}\t{}", + entry.filemode(), + entry.kind().unwrap().str(), + entry.id(), + entry.name().unwrap() + ); } } fn show_sig(header: &str, sig: Option) { - let sig = match sig { Some(s) => s, None => return }; + let sig = match sig { + Some(s) => s, + None => return, + }; let offset = sig.when().offset_minutes(); - let (sign, offset) = if offset < 0 {('-', -offset)} else {('+', offset)}; + let (sign, offset) = if offset < 0 { + ('-', -offset) + } else { + ('+', offset) + }; let (hours, minutes) = (offset / 60, offset % 60); - println!("{} {} {} {}{:02}{:02}", - header, sig, sig.when().seconds(), sign, hours, minutes); - + println!( + "{} {} {} {}{:02}{:02}", + header, + sig, + sig.when().seconds(), + sign, + hours, + minutes + ); } fn main() { - const USAGE: &'static str = " -usage: cat-file (-t | -s | -e | -p) [options] - -Options: - -t show the object type - -s show the object size - -e suppress all output - -p pretty print the contents of the object - -q suppress output - -v use verbose output - --git-dir use the specified directory as the base directory - -h, --help show this message -"; - - let args = Docopt::new(USAGE).and_then(|d| d.decode()) - .unwrap_or_else(|e| e.exit()); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/examples/clone.rs b/examples/clone.rs index a30abb5da5..f6d00b14b6 100644 --- a/examples/clone.rs +++ b/examples/clone.rs @@ -14,20 +14,18 @@ #![deny(warnings)] -extern crate git2; -extern crate docopt; -extern crate rustc_serialize; - -use docopt::Docopt; -use git2::build::{RepoBuilder, CheckoutBuilder}; -use git2::{RemoteCallbacks, Progress, FetchOptions}; +use clap::Parser; +use git2::build::{CheckoutBuilder, RepoBuilder}; +use git2::{FetchOptions, Progress, RemoteCallbacks}; use std::cell::RefCell; use std::io::{self, Write}; use std::path::{Path, PathBuf}; -#[derive(RustcDecodable)] +#[derive(Parser)] struct Args { + #[structopt(name = "url")] arg_url: String, + #[structopt(name = "path")] arg_path: String, } @@ -49,22 +47,36 @@ fn print(state: &mut State) { 0 }; let kbytes = stats.received_bytes() / 1024; - if stats.received_objects() == stats.total_objects() && false { + if stats.received_objects() == stats.total_objects() { if !state.newline { - println!(""); + println!(); state.newline = true; } - print!("Resolving deltas {}/{}\r", stats.indexed_deltas(), - stats.total_deltas()); + print!( + "Resolving deltas {}/{}\r", + stats.indexed_deltas(), + stats.total_deltas() + ); } else { - print!("net {:3}% ({:4} kb, {:5}/{:5}) / idx {:3}% ({:5}/{:5}) \ - / chk {:3}% ({:4}/{:4}) {}\r", - network_pct, kbytes, stats.received_objects(), - stats.total_objects(), - index_pct, stats.indexed_objects(), stats.total_objects(), - co_pct, state.current, state.total, - state.path.as_ref().map(|s| s.to_string_lossy().into_owned()) - .unwrap_or(String::new())); + print!( + "net {:3}% ({:4} kb, {:5}/{:5}) / idx {:3}% ({:5}/{:5}) \ + / chk {:3}% ({:4}/{:4}) {}\r", + network_pct, + kbytes, + stats.received_objects(), + stats.total_objects(), + index_pct, + stats.indexed_objects(), + stats.total_objects(), + co_pct, + state.current, + state.total, + state + .path + .as_ref() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_default() + ) } io::stdout().flush().unwrap(); } @@ -96,26 +108,19 @@ fn run(args: &Args) -> Result<(), git2::Error> { let mut fo = FetchOptions::new(); fo.remote_callbacks(cb); - try!(RepoBuilder::new().fetch_options(fo).with_checkout(co) - .clone(&args.arg_url, Path::new(&args.arg_path))); - println!(""); + RepoBuilder::new() + .fetch_options(fo) + .with_checkout(co) + .clone(&args.arg_url, Path::new(&args.arg_path))?; + println!(); Ok(()) } fn main() { - const USAGE: &'static str = " -usage: add [options] - -Options: - -h, --help show this message -"; - - let args = Docopt::new(USAGE).and_then(|d| d.decode()) - .unwrap_or_else(|e| e.exit()); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), } } - diff --git a/examples/diff.rs b/examples/diff.rs index 994404da2f..7440149ba0 100644 --- a/examples/diff.rs +++ b/examples/diff.rs @@ -14,66 +14,167 @@ #![deny(warnings)] -extern crate git2; -extern crate docopt; -extern crate rustc_serialize; - +use clap::Parser; +use git2::{Blob, Diff, DiffOptions, Error, Object, ObjectType, Oid, Repository}; +use git2::{DiffDelta, DiffFindOptions, DiffFormat, DiffHunk, DiffLine}; use std::str; -use docopt::Docopt; -use git2::{Repository, Error, Object, ObjectType, DiffOptions, Diff}; -use git2::{DiffFindOptions, DiffFormat}; - -#[derive(RustcDecodable)] #[allow(non_snake_case)] +#[derive(Parser)] +#[allow(non_snake_case)] struct Args { + #[structopt(name = "from_oid")] arg_from_oid: Option, + #[structopt(name = "to_oid")] arg_to_oid: Option, + #[structopt(name = "blobs", long)] + /// treat from_oid and to_oid as blob ids + flag_blobs: bool, + #[structopt(name = "patch", short, long)] + /// show output in patch format flag_patch: bool, + #[structopt(name = "cached", long)] + /// use staged changes as diff flag_cached: bool, + #[structopt(name = "nocached", long)] + /// do not use staged changes flag_nocached: bool, + #[structopt(name = "name-only", long)] + /// show only names of changed files flag_name_only: bool, + #[structopt(name = "name-status", long)] + /// show only names and status changes flag_name_status: bool, + #[structopt(name = "raw", long)] + /// generate the raw format flag_raw: bool, + #[structopt(name = "format", long)] + /// specify format for stat summary flag_format: Option, + #[structopt(name = "color", long)] + /// use color output flag_color: bool, + #[structopt(name = "no-color", long)] + /// never use color output flag_no_color: bool, + #[structopt(short = 'R')] + /// swap two inputs flag_R: bool, + #[structopt(name = "text", short = 'a', long)] + /// treat all files as text flag_text: bool, + #[structopt(name = "ignore-space-at-eol", long)] + /// ignore changes in whitespace at EOL flag_ignore_space_at_eol: bool, + #[structopt(name = "ignore-space-change", short = 'b', long)] + /// ignore changes in amount of whitespace flag_ignore_space_change: bool, + #[structopt(name = "ignore-all-space", short = 'w', long)] + /// ignore whitespace when comparing lines flag_ignore_all_space: bool, + #[structopt(name = "ignored", long)] + /// show untracked files flag_ignored: bool, + #[structopt(name = "untracked", long)] + /// generate diff using the patience algorithm flag_untracked: bool, + #[structopt(name = "patience", long)] + /// show ignored files as well flag_patience: bool, + #[structopt(name = "minimal", long)] + /// spend extra time to find smallest diff flag_minimal: bool, + #[structopt(name = "stat", long)] + /// generate a diffstat flag_stat: bool, + #[structopt(name = "numstat", long)] + /// similar to --stat, but more machine friendly flag_numstat: bool, + #[structopt(name = "shortstat", long)] + /// only output last line of --stat flag_shortstat: bool, + #[structopt(name = "summary", long)] + /// output condensed summary of header info flag_summary: bool, + #[structopt(name = "find-renames", short = 'M', long)] + /// set threshold for finding renames (default 50) flag_find_renames: Option, + #[structopt(name = "find-copies", short = 'C', long)] + /// set threshold for finding copies (default 50) flag_find_copies: Option, + #[structopt(name = "find-copies-harder", long)] + /// inspect unmodified files for sources of copies flag_find_copies_harder: bool, + #[structopt(name = "break_rewrites", short = 'B', long)] + /// break complete rewrite changes into pairs flag_break_rewrites: bool, + #[structopt(name = "unified", short = 'U', long)] + /// lints of context to show flag_unified: Option, + #[structopt(name = "inter-hunk-context", long)] + /// maximum lines of change between hunks flag_inter_hunk_context: Option, + #[structopt(name = "abbrev", long)] + /// length to abbreviate commits to flag_abbrev: Option, + #[structopt(name = "src-prefix", long)] + /// show given source prefix instead of 'a/' flag_src_prefix: Option, + #[structopt(name = "dst-prefix", long)] + /// show given destination prefix instead of 'b/' flag_dst_prefix: Option, + #[structopt(name = "path", long = "git-dir")] + /// path to git repository to use flag_git_dir: Option, } -const RESET: &'static str = "\u{1b}[m"; -const BOLD: &'static str = "\u{1b}[1m"; -const RED: &'static str = "\u{1b}[31m"; -const GREEN: &'static str = "\u{1b}[32m"; -const CYAN: &'static str = "\u{1b}[36m"; +const RESET: &str = "\u{1b}[m"; +const BOLD: &str = "\u{1b}[1m"; +const RED: &str = "\u{1b}[31m"; +const GREEN: &str = "\u{1b}[32m"; +const CYAN: &str = "\u{1b}[36m"; #[derive(PartialEq, Eq, Copy, Clone)] -enum Cache { Normal, Only, None } +enum Cache { + Normal, + Only, + None, +} + +fn line_color(line: &DiffLine) -> Option<&'static str> { + match line.origin() { + '+' => Some(GREEN), + '-' => Some(RED), + '>' => Some(GREEN), + '<' => Some(RED), + 'F' => Some(BOLD), + 'H' => Some(CYAN), + _ => None, + } +} + +fn print_diff_line( + _delta: DiffDelta, + _hunk: Option, + line: DiffLine, + args: &Args, +) -> bool { + if args.color() { + print!("{}", RESET); + if let Some(color) = line_color(&line) { + print!("{}", color); + } + } + match line.origin() { + '+' | '-' | ' ' => print!("{}", line.origin()), + _ => {} + } + print!("{}", str::from_utf8(line.content()).unwrap()); + true +} fn run(args: &Args) -> Result<(), Error> { let path = args.flag_git_dir.as_ref().map(|s| &s[..]).unwrap_or("."); - let repo = try!(Repository::open(path)); + let repo = Repository::open(path)?; // Prepare our diff options based on the arguments given let mut opts = DiffOptions::new(); @@ -86,45 +187,73 @@ fn run(args: &Args) -> Result<(), Error> { .include_untracked(args.flag_untracked) .patience(args.flag_patience) .minimal(args.flag_minimal); - if let Some(amt) = args.flag_unified { opts.context_lines(amt); } - if let Some(amt) = args.flag_inter_hunk_context { opts.interhunk_lines(amt); } - if let Some(amt) = args.flag_abbrev { opts.id_abbrev(amt); } - if let Some(ref s) = args.flag_src_prefix { opts.old_prefix(&s); } - if let Some(ref s) = args.flag_dst_prefix { opts.new_prefix(&s); } + if let Some(amt) = args.flag_unified { + opts.context_lines(amt); + } + if let Some(amt) = args.flag_inter_hunk_context { + opts.interhunk_lines(amt); + } + if let Some(amt) = args.flag_abbrev { + opts.id_abbrev(amt); + } + if let Some(ref s) = args.flag_src_prefix { + opts.old_prefix(&s); + } + if let Some(ref s) = args.flag_dst_prefix { + opts.new_prefix(&s); + } if let Some("diff-index") = args.flag_format.as_ref().map(|s| &s[..]) { opts.id_abbrev(40); } + if args.flag_blobs { + let b1 = resolve_blob(&repo, args.arg_from_oid.as_ref())?; + let b2 = resolve_blob(&repo, args.arg_to_oid.as_ref())?; + repo.diff_blobs( + b1.as_ref(), + None, + b2.as_ref(), + None, + Some(&mut opts), + None, + None, + None, + Some(&mut |d, h, l| print_diff_line(d, h, l, args)), + )?; + if args.color() { + print!("{}", RESET); + } + return Ok(()); + } + // Prepare the diff to inspect - let t1 = try!(tree_to_treeish(&repo, args.arg_from_oid.as_ref())); - let t2 = try!(tree_to_treeish(&repo, args.arg_to_oid.as_ref())); - let head = try!(tree_to_treeish(&repo, Some(&"HEAD".to_string()))).unwrap(); + let t1 = tree_to_treeish(&repo, args.arg_from_oid.as_ref())?; + let t2 = tree_to_treeish(&repo, args.arg_to_oid.as_ref())?; + let head = tree_to_treeish(&repo, Some(&"HEAD".to_string()))?.unwrap(); let mut diff = match (t1, t2, args.cache()) { (Some(t1), Some(t2), _) => { - try!(repo.diff_tree_to_tree(t1.as_tree(), t2.as_tree(), - Some(&mut opts))) + repo.diff_tree_to_tree(t1.as_tree(), t2.as_tree(), Some(&mut opts))? } (t1, None, Cache::None) => { let t1 = t1.unwrap_or(head); - try!(repo.diff_tree_to_workdir(t1.as_tree(), Some(&mut opts))) + repo.diff_tree_to_workdir(t1.as_tree(), Some(&mut opts))? } (t1, None, Cache::Only) => { let t1 = t1.unwrap_or(head); - try!(repo.diff_tree_to_index(t1.as_tree(), None, Some(&mut opts))) + repo.diff_tree_to_index(t1.as_tree(), None, Some(&mut opts))? } (Some(t1), None, _) => { - try!(repo.diff_tree_to_workdir_with_index(t1.as_tree(), - Some(&mut opts))) - } - (None, None, _) => { - try!(repo.diff_index_to_workdir(None, Some(&mut opts))) + repo.diff_tree_to_workdir_with_index(t1.as_tree(), Some(&mut opts))? } + (None, None, _) => repo.diff_index_to_workdir(None, Some(&mut opts))?, (None, Some(_), _) => unreachable!(), }; // Apply rename and copy detection if requested - if args.flag_break_rewrites || args.flag_find_copies_harder || - args.flag_find_renames.is_some() || args.flag_find_copies.is_some() + if args.flag_break_rewrites + || args.flag_find_copies_harder + || args.flag_find_renames.is_some() + || args.flag_find_copies.is_some() { let mut opts = DiffFindOptions::new(); if let Some(t) = args.flag_find_renames { @@ -137,92 +266,88 @@ fn run(args: &Args) -> Result<(), Error> { } opts.copies_from_unmodified(args.flag_find_copies_harder) .rewrites(args.flag_break_rewrites); - try!(diff.find_similar(Some(&mut opts))); + diff.find_similar(Some(&mut opts))?; } // Generate simple output - let stats = args.flag_stat | args.flag_numstat | args.flag_shortstat | - args.flag_summary; + let stats = args.flag_stat | args.flag_numstat | args.flag_shortstat | args.flag_summary; if stats { - try!(print_stats(&diff, args)); + print_stats(&diff, args)?; } if args.flag_patch || !stats { - if args.color() { print!("{}", RESET); } - let mut last_color = None; - try!(diff.print(args.diff_format(), |_delta, _hunk, line| { - if args.color() { - let next = match line.origin() { - '+' => Some(GREEN), - '-' => Some(RED), - '>' => Some(GREEN), - '<' => Some(RED), - 'F' => Some(BOLD), - 'H' => Some(CYAN), - _ => None - }; - if args.color() && next != last_color { - if last_color == Some(BOLD) || next == Some(BOLD) { - print!("{}", RESET); - } - print!("{}", next.unwrap_or(RESET)); - last_color = next; - } - } - - match line.origin() { - '+' | '-' | ' ' => print!("{}", line.origin()), - _ => {} - } - print!("{}", str::from_utf8(line.content()).unwrap()); - true - })); - if args.color() { print!("{}", RESET); } + diff.print(args.diff_format(), |d, h, l| print_diff_line(d, h, l, args))?; + if args.color() { + print!("{}", RESET); + } } Ok(()) } fn print_stats(diff: &Diff, args: &Args) -> Result<(), Error> { - let stats = try!(diff.stats()); - let mut format = git2::DIFF_STATS_NONE; + let stats = diff.stats()?; + let mut format = git2::DiffStatsFormat::NONE; if args.flag_stat { - format = format | git2::DIFF_STATS_FULL; + format |= git2::DiffStatsFormat::FULL; } if args.flag_shortstat { - format = format | git2::DIFF_STATS_SHORT; + format |= git2::DiffStatsFormat::SHORT; } if args.flag_numstat { - format = format | git2::DIFF_STATS_NUMBER; + format |= git2::DiffStatsFormat::NUMBER; } if args.flag_summary { - format = format | git2::DIFF_STATS_INCLUDE_SUMMARY; + format |= git2::DiffStatsFormat::INCLUDE_SUMMARY; } - let buf = try!(stats.to_buf(format, 80)); + let buf = stats.to_buf(format, 80)?; print!("{}", str::from_utf8(&*buf).unwrap()); Ok(()) } -fn tree_to_treeish<'a>(repo: &'a Repository, arg: Option<&String>) - -> Result>, Error> { - let arg = match arg { Some(s) => s, None => return Ok(None) }; - let obj = try!(repo.revparse_single(arg)); - let tree = try!(obj.peel(ObjectType::Tree)); +fn tree_to_treeish<'a>( + repo: &'a Repository, + arg: Option<&String>, +) -> Result>, Error> { + let arg = match arg { + Some(s) => s, + None => return Ok(None), + }; + let obj = repo.revparse_single(arg)?; + let tree = obj.peel(ObjectType::Tree)?; Ok(Some(tree)) } +fn resolve_blob<'a>(repo: &'a Repository, arg: Option<&String>) -> Result>, Error> { + let arg = match arg { + Some(s) => Oid::from_str(s)?, + None => return Ok(None), + }; + repo.find_blob(arg).map(|b| Some(b)) +} + impl Args { fn cache(&self) -> Cache { - if self.flag_cached {Cache::Only} - else if self.flag_nocached {Cache::None} - else {Cache::Normal} + if self.flag_cached { + Cache::Only + } else if self.flag_nocached { + Cache::None + } else { + Cache::Normal + } + } + fn color(&self) -> bool { + self.flag_color && !self.flag_no_color } - fn color(&self) -> bool { self.flag_color && !self.flag_no_color } fn diff_format(&self) -> DiffFormat { - if self.flag_patch {DiffFormat::Patch} - else if self.flag_name_only {DiffFormat::NameOnly} - else if self.flag_name_status {DiffFormat::NameStatus} - else if self.flag_raw {DiffFormat::Raw} - else { + if self.flag_patch { + DiffFormat::Patch + } else if self.flag_name_only { + DiffFormat::NameOnly + } else if self.flag_name_status { + DiffFormat::NameStatus + } else if self.flag_raw { + DiffFormat::Raw + } else { match self.flag_format.as_ref().map(|s| &s[..]) { Some("name") => DiffFormat::NameOnly, Some("name-status") => DiffFormat::NameStatus, @@ -235,47 +360,7 @@ impl Args { } fn main() { - const USAGE: &'static str = " -usage: diff [options] [ []] - -Options: - -p, --patch show output in patch format - --cached use staged changes as diff - --nocached do not use staged changes - --name-only show only names of changed files - --name-status show only names and status changes - --raw generate the raw format - --format= specify format for stat summary - --color use color output - --no-color never use color output - -R swap two inputs - -a, --text treat all files as text - --ignore-space-at-eol ignore changes in whitespace at EOL - -b, --ignore-space-change ignore changes in amount of whitespace - -w, --ignore-all-space ignore whitespace when comparing lines - --ignored show ignored files as well - --untracked show untracked files - --patience generate diff using the patience algorithm - --minimal spend extra time to find smallest diff - --stat generate a diffstat - --numstat similar to --stat, but more machine friendly - --shortstat only output last line of --stat - --summary output condensed summary of header info - -M, --find-renames set threshold for findind renames (default 50) - -C, --find-copies set threshold for finding copies (default 50) - --find-copies-harder inspect unmodified files for sources of copies - -B, --break-rewrites break complete rewrite changes into pairs - -U, --unified lints of context to show - --inter-hunk-context maximum lines of change between hunks - --abbrev length to abbreviate commits to - --src-prefix show given source prefix instead of 'a/' - --dst-prefix show given destinction prefix instead of 'b/' - --git-dir path to git repository to use - -h, --help show this message -"; - - let args = Docopt::new(USAGE).and_then(|d| d.decode()) - .unwrap_or_else(|e| e.exit()); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/examples/fetch.rs b/examples/fetch.rs index a03ea4af2e..af4aa98eb9 100644 --- a/examples/fetch.rs +++ b/examples/fetch.rs @@ -14,30 +14,27 @@ #![deny(warnings)] -extern crate git2; -extern crate docopt; -extern crate rustc_serialize; - -use docopt::Docopt; -use git2::{Repository, RemoteCallbacks, Direction, AutotagOption, FetchOptions}; +use clap::Parser; +use git2::{AutotagOption, FetchOptions, RemoteCallbacks, RemoteUpdateFlags, Repository}; use std::io::{self, Write}; use std::str; -#[derive(RustcDecodable)] +#[derive(Parser)] struct Args { + #[structopt(name = "remote")] arg_remote: Option, } fn run(args: &Args) -> Result<(), git2::Error> { - let repo = try!(Repository::open(".")); + let repo = Repository::open(".")?; let remote = args.arg_remote.as_ref().map(|s| &s[..]).unwrap_or("origin"); // Figure out whether it's a named remote or a URL println!("Fetching {} for repo", remote); let mut cb = RemoteCallbacks::new(); - let mut remote = try!(repo.find_remote(remote).or_else(|_| { - repo.remote_anonymous(remote) - })); + let mut remote = repo + .find_remote(remote) + .or_else(|_| repo.remote_anonymous(remote))?; cb.sideband_progress(|data| { print!("remote: {}", str::from_utf8(data).unwrap()); io::stdout().flush().unwrap(); @@ -61,69 +58,73 @@ fn run(args: &Args) -> Result<(), git2::Error> { // the download rate. cb.transfer_progress(|stats| { if stats.received_objects() == stats.total_objects() { - print!("Resolving deltas {}/{}\r", stats.indexed_deltas(), - stats.total_deltas()); + print!( + "Resolving deltas {}/{}\r", + stats.indexed_deltas(), + stats.total_deltas() + ); } else if stats.total_objects() > 0 { - print!("Received {}/{} objects ({}) in {} bytes\r", - stats.received_objects(), - stats.total_objects(), - stats.indexed_objects(), - stats.received_bytes()); + print!( + "Received {}/{} objects ({}) in {} bytes\r", + stats.received_objects(), + stats.total_objects(), + stats.indexed_objects(), + stats.received_bytes() + ); } io::stdout().flush().unwrap(); true }); - // Connect to the remote end specifying that we want to fetch information - // from it. - try!(remote.connect(Direction::Fetch)); - // Download the packfile and index it. This function updates the amount of // received data and the indexer stats which lets you inform the user about // progress. let mut fo = FetchOptions::new(); fo.remote_callbacks(cb); - try!(remote.download(&[], Some(&mut fo))); + remote.download(&[] as &[&str], Some(&mut fo))?; { // If there are local objects (we got a thin pack), then tell the user // how many objects we saved from having to cross the network. let stats = remote.stats(); if stats.local_objects() > 0 { - println!("\rReceived {}/{} objects in {} bytes (used {} local \ - objects)", stats.indexed_objects(), - stats.total_objects(), stats.received_bytes(), - stats.local_objects()); + println!( + "\rReceived {}/{} objects in {} bytes (used {} local \ + objects)", + stats.indexed_objects(), + stats.total_objects(), + stats.received_bytes(), + stats.local_objects() + ); } else { - println!("\rReceived {}/{} objects in {} bytes", - stats.indexed_objects(), stats.total_objects(), - stats.received_bytes()); + println!( + "\rReceived {}/{} objects in {} bytes", + stats.indexed_objects(), + stats.total_objects(), + stats.received_bytes() + ); } } // Disconnect the underlying connection to prevent from idling. - remote.disconnect(); + remote.disconnect()?; // Update the references in the remote's namespace to point to the right // commits. This may be needed even if there was no packfile to download, // which can happen e.g. when the branches have been changed but all the // needed objects are available locally. - try!(remote.update_tips(None, true, - AutotagOption::Unspecified, None)); + remote.update_tips( + None, + RemoteUpdateFlags::UPDATE_FETCHHEAD, + AutotagOption::Unspecified, + None, + )?; Ok(()) } fn main() { - const USAGE: &'static str = " -usage: fetch [options] [] - -Options: - -h, --help show this message -"; - - let args = Docopt::new(USAGE).and_then(|d| d.decode()) - .unwrap_or_else(|e| e.exit()); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/examples/init.rs b/examples/init.rs index 2af3e9657d..3ae79082d7 100644 --- a/examples/init.rs +++ b/examples/init.rs @@ -1,5 +1,5 @@ /* - * libgit2 "init" example - shows how to initialize a new repo + * libgit2 "init" example - shows how to initialize a new repo (also includes how to do an initial commit) * * Written by the libgit2 contributors * @@ -14,31 +14,42 @@ #![deny(warnings)] -extern crate git2; -extern crate docopt; -extern crate rustc_serialize; +use clap::Parser; +use git2::{Error, Repository, RepositoryInitMode, RepositoryInitOptions}; +use std::path::{Path, PathBuf}; -use docopt::Docopt; -use git2::{Repository, RepositoryInitOptions, RepositoryInitMode, Error}; -use std::path::{PathBuf, Path}; - -#[derive(RustcDecodable)] +#[derive(Parser)] struct Args { + #[structopt(name = "directory")] arg_directory: String, + #[structopt(name = "quiet", short, long)] + /// don't print information to stdout flag_quiet: bool, + #[structopt(name = "bare", long)] + /// initialize a new bare repository flag_bare: bool, + #[structopt(name = "dir", long = "template")] + /// use as an initialization template flag_template: Option, + #[structopt(name = "separate-git-dir", long)] + /// use as the .git directory flag_separate_git_dir: Option, + #[structopt(name = "initial-commit", long)] + /// create an initial empty commit flag_initial_commit: bool, + #[structopt(name = "perms", long = "shared")] + /// permissions to create the repository with flag_shared: Option, } fn run(args: &Args) -> Result<(), Error> { let mut path = PathBuf::from(&args.arg_directory); - let repo = if !args.flag_bare && args.flag_template.is_none() && - args.flag_shared.is_none() && - args.flag_separate_git_dir.is_none() { - try!(Repository::init(&path)) + let repo = if !args.flag_bare + && args.flag_template.is_none() + && args.flag_shared.is_none() + && args.flag_separate_git_dir.is_none() + { + Repository::init(&path)? } else { let mut opts = RepositoryInitOptions::new(); opts.bare(args.flag_bare); @@ -55,9 +66,9 @@ fn run(args: &Args) -> Result<(), Error> { } if let Some(ref s) = args.flag_shared { - opts.mode(try!(parse_shared(&s))); + opts.mode(parse_shared(s)?); } - try!(Repository::init_opts(&path, &opts)) + Repository::init_opts(&path, &opts)? }; // Print a message to stdout like "git init" does @@ -71,7 +82,7 @@ fn run(args: &Args) -> Result<(), Error> { } if args.flag_initial_commit { - try!(create_initial_commit(&repo)); + create_initial_commit(&repo)?; println!("Created empty initial commit"); } @@ -82,45 +93,41 @@ fn run(args: &Args) -> Result<(), Error> { /// commit in the repository. This is the helper function that does that. fn create_initial_commit(repo: &Repository) -> Result<(), Error> { // First use the config to initialize a commit signature for the user. - let sig = try!(repo.signature()); + let sig = repo.signature()?; // Now let's create an empty tree for this commit let tree_id = { - let mut index = try!(repo.index()); + let mut index = repo.index()?; // Outside of this example, you could call index.add_path() // here to put actual files into the index. For our purposes, we'll // leave it empty for now. - try!(index.write_tree()) + index.write_tree()? }; - let tree = try!(repo.find_tree(tree_id)); + let tree = repo.find_tree(tree_id)?; // Ready to create the initial commit. // // Normally creating a commit would involve looking up the current HEAD // commit and making that be the parent of the initial commit, but here this // is the first commit so there will be no parent. - try!(repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])); + repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])?; Ok(()) } fn parse_shared(shared: &str) -> Result { match shared { - "false" | "umask" => Ok(git2::REPOSITORY_INIT_SHARED_UMASK), - "true" | "group" => Ok(git2::REPOSITORY_INIT_SHARED_GROUP), - "all" | "world" => Ok(git2::REPOSITORY_INIT_SHARED_ALL), + "false" | "umask" => Ok(git2::RepositoryInitMode::SHARED_UMASK), + "true" | "group" => Ok(git2::RepositoryInitMode::SHARED_GROUP), + "all" | "world" => Ok(git2::RepositoryInitMode::SHARED_ALL), _ => { - if shared.starts_with("0") { + if shared.starts_with('0') { match u32::from_str_radix(&shared[1..], 8).ok() { - Some(n) => { - return Ok(RepositoryInitMode::from_bits_truncate(n)) - } - None => { - Err(Error::from_str("invalid octal value for --shared")) - } + Some(n) => Ok(RepositoryInitMode::from_bits_truncate(n)), + None => Err(Error::from_str("invalid octal value for --shared")), } } else { Err(Error::from_str("unknown value for --shared")) @@ -130,20 +137,7 @@ fn parse_shared(shared: &str) -> Result { } fn main() { - const USAGE: &'static str = " -usage: init [options] - -Options: - -q, --quiet don't print information to stdout - --bare initialize a new bare repository - --template use as an initialization template - --separate-git-dir use as the .git directory - --initial-commit create an initial empty commit - --shared permissions to create the repository with -"; - - let args = Docopt::new(USAGE).and_then(|d| d.decode()) - .unwrap_or_else(|e| e.exit()); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/examples/log.rs b/examples/log.rs index a679a22626..5f6fe0f210 100644 --- a/examples/log.rs +++ b/examples/log.rs @@ -14,153 +14,211 @@ #![deny(warnings)] -extern crate rustc_serialize; -extern crate docopt; -extern crate git2; -extern crate time; - +use clap::Parser; +use git2::{Commit, DiffOptions, ObjectType, Repository, Signature, Time}; +use git2::{DiffFormat, Error, Pathspec}; use std::str; -use docopt::Docopt; -use git2::{Repository, Signature, Commit, ObjectType, Time, DiffOptions}; -use git2::{Pathspec, Error, DiffFormat}; -#[derive(RustcDecodable)] +#[derive(Parser)] struct Args { - arg_commit: Vec, - arg_spec: Vec, + #[structopt(name = "topo-order", long)] + /// sort commits in topological order flag_topo_order: bool, + #[structopt(name = "date-order", long)] + /// sort commits in date order flag_date_order: bool, + #[structopt(name = "reverse", long)] + /// sort commits in reverse flag_reverse: bool, + #[structopt(name = "author", long)] + /// author to sort by flag_author: Option, + #[structopt(name = "committer", long)] + /// committer to sort by flag_committer: Option, + #[structopt(name = "pat", long = "grep")] + /// pattern to filter commit messages by flag_grep: Option, + #[structopt(name = "dir", long = "git-dir")] + /// alternative git directory to use flag_git_dir: Option, + #[structopt(name = "skip", long)] + /// number of commits to skip flag_skip: Option, + #[structopt(name = "max-count", short = 'n', long)] + /// maximum number of commits to show flag_max_count: Option, + #[structopt(name = "merges", long)] + /// only show merge commits flag_merges: bool, + #[structopt(name = "no-merges", long)] + /// don't show merge commits flag_no_merges: bool, + #[structopt(name = "no-min-parents", long)] + /// don't require a minimum number of parents flag_no_min_parents: bool, + #[structopt(name = "no-max-parents", long)] + /// don't require a maximum number of parents flag_no_max_parents: bool, + #[structopt(name = "max-parents")] + /// specify a maximum number of parents for a commit flag_max_parents: Option, + #[structopt(name = "min-parents")] + /// specify a minimum number of parents for a commit flag_min_parents: Option, + #[structopt(name = "patch", long, short)] + /// show commit diff flag_patch: bool, + #[structopt(name = "commit")] + arg_commit: Vec, + #[structopt(name = "spec", last = true)] + arg_spec: Vec, } fn run(args: &Args) -> Result<(), Error> { let path = args.flag_git_dir.as_ref().map(|s| &s[..]).unwrap_or("."); - let repo = try!(Repository::open(path)); - let mut revwalk = try!(repo.revwalk()); + let repo = Repository::open(path)?; + let mut revwalk = repo.revwalk()?; // Prepare the revwalk based on CLI parameters - let base = if args.flag_reverse {git2::SORT_REVERSE} else {git2::SORT_NONE}; - revwalk.set_sorting(base | if args.flag_topo_order { - git2::SORT_TOPOLOGICAL - } else if args.flag_date_order { - git2::SORT_TIME + let base = if args.flag_reverse { + git2::Sort::REVERSE } else { - git2::SORT_NONE - }); - for commit in args.arg_commit.iter() { - if commit.starts_with("^") { - let obj = try!(repo.revparse_single(&commit[1..])); - try!(revwalk.hide(obj.id())); - continue + git2::Sort::NONE + }; + revwalk.set_sorting( + base | if args.flag_topo_order { + git2::Sort::TOPOLOGICAL + } else if args.flag_date_order { + git2::Sort::TIME + } else { + git2::Sort::NONE + }, + )?; + for commit in &args.arg_commit { + if commit.starts_with('^') { + let obj = repo.revparse_single(&commit[1..])?; + revwalk.hide(obj.id())?; + continue; } - let revspec = try!(repo.revparse(&commit)); - if revspec.mode().contains(git2::REVPARSE_SINGLE) { - try!(revwalk.push(revspec.from().unwrap().id())); + let revspec = repo.revparse(commit)?; + if revspec.mode().contains(git2::RevparseMode::SINGLE) { + revwalk.push(revspec.from().unwrap().id())?; } else { let from = revspec.from().unwrap().id(); let to = revspec.to().unwrap().id(); - try!(revwalk.push(to)); - if revspec.mode().contains(git2::REVPARSE_MERGE_BASE) { - let base = try!(repo.merge_base(from, to)); - let o = try!(repo.find_object(base, Some(ObjectType::Commit))); - try!(revwalk.push(o.id())); + revwalk.push(to)?; + if revspec.mode().contains(git2::RevparseMode::MERGE_BASE) { + let base = repo.merge_base(from, to)?; + let o = repo.find_object(base, Some(ObjectType::Commit))?; + revwalk.push(o.id())?; } - try!(revwalk.hide(from)); + revwalk.hide(from)?; } } - if args.arg_commit.len() == 0 { - try!(revwalk.push_head()); + if args.arg_commit.is_empty() { + revwalk.push_head()?; } // Prepare our diff options and pathspec matcher let (mut diffopts, mut diffopts2) = (DiffOptions::new(), DiffOptions::new()); - for spec in args.arg_spec.iter() { + for spec in &args.arg_spec { diffopts.pathspec(spec); diffopts2.pathspec(spec); } - let ps = try!(Pathspec::new(args.arg_spec.iter())); + let ps = Pathspec::new(args.arg_spec.iter())?; // Filter our revwalk based on the CLI parameters macro_rules! filter_try { - ($e:expr) => (match $e { Ok(t) => t, Err(e) => return Some(Err(e)) }) + ($e:expr) => { + match $e { + Ok(t) => t, + Err(e) => return Some(Err(e)), + } + }; } - let revwalk = revwalk.filter_map(|id| { - let id = filter_try!(id); - let commit = filter_try!(repo.find_commit(id)); - let parents = commit.parents().len(); - if parents < args.min_parents() { return None } - if let Some(n) = args.max_parents() { - if parents >= n { return None } - } - if args.arg_spec.len() > 0 { - match commit.parents().len() { - 0 => { - let tree = filter_try!(commit.tree()); - let flags = git2::PATHSPEC_NO_MATCH_ERROR; - if ps.match_tree(&tree, flags).is_err() { return None } + let revwalk = revwalk + .filter_map(|id| { + let id = filter_try!(id); + let commit = filter_try!(repo.find_commit(id)); + let parents = commit.parents().len(); + if parents < args.min_parents() { + return None; + } + if let Some(n) = args.max_parents() { + if parents >= n { + return None; } - _ => { - let m = commit.parents().all(|parent| { - match_with_parent(&repo, &commit, &parent, &mut diffopts) - .unwrap_or(false) - }); - if !m { return None } + } + if !args.arg_spec.is_empty() { + match commit.parents().len() { + 0 => { + let tree = filter_try!(commit.tree()); + let flags = git2::PathspecFlags::NO_MATCH_ERROR; + if ps.match_tree(&tree, flags).is_err() { + return None; + } + } + _ => { + let m = commit.parents().all(|parent| { + match_with_parent(&repo, &commit, &parent, &mut diffopts) + .unwrap_or(false) + }); + if !m { + return None; + } + } } } - } - if !sig_matches(commit.author(), &args.flag_author) { return None } - if !sig_matches(commit.committer(), &args.flag_committer) { return None } - if !log_message_matches(commit.message(), &args.flag_grep) { return None } - Some(Ok(commit)) - }).skip(args.flag_skip.unwrap_or(0)).take(args.flag_max_count.unwrap_or(!0)); + if !sig_matches(&commit.author(), &args.flag_author) { + return None; + } + if !sig_matches(&commit.committer(), &args.flag_committer) { + return None; + } + if !log_message_matches(commit.message(), &args.flag_grep) { + return None; + } + Some(Ok(commit)) + }) + .skip(args.flag_skip.unwrap_or(0)) + .take(args.flag_max_count.unwrap_or(!0)); // print! for commit in revwalk { - let commit = try!(commit); + let commit = commit?; print_commit(&commit); - if !args.flag_patch || commit.parents().len() > 1 { continue } + if !args.flag_patch || commit.parents().len() > 1 { + continue; + } let a = if commit.parents().len() == 1 { - let parent = try!(commit.parent(0)); - Some(try!(parent.tree())) + let parent = commit.parent(0)?; + Some(parent.tree()?) } else { None }; - let b = try!(commit.tree()); - let diff = try!(repo.diff_tree_to_tree(a.as_ref(), Some(&b), - Some(&mut diffopts2))); - try!(diff.print(DiffFormat::Patch, |_delta, _hunk, line| { + let b = commit.tree()?; + let diff = repo.diff_tree_to_tree(a.as_ref(), Some(&b), Some(&mut diffopts2))?; + diff.print(DiffFormat::Patch, |_delta, _hunk, line| { match line.origin() { ' ' | '+' | '-' => print!("{}", line.origin()), _ => {} } print!("{}", str::from_utf8(line.content()).unwrap()); true - })); + })?; } Ok(()) } -fn sig_matches(sig: Signature, arg: &Option) -> bool { +fn sig_matches(sig: &Signature, arg: &Option) -> bool { match *arg { Some(ref s) => { - sig.name().map(|n| n.contains(s)).unwrap_or(false) || - sig.email().map(|n| n.contains(s)).unwrap_or(false) + sig.name().map(|n| n.contains(s)).unwrap_or(false) + || sig.email().map(|n| n.contains(s)).unwrap_or(false) } - None => true + None => true, } } @@ -180,81 +238,64 @@ fn print_commit(commit: &Commit) { for id in commit.parent_ids() { print!(" {:.8}", id); } - println!(""); + println!(); } let author = commit.author(); println!("Author: {}", author); print_time(&author.when(), "Date: "); - println!(""); + println!(); for line in String::from_utf8_lossy(commit.message_bytes()).lines() { println!(" {}", line); } - println!(""); + println!(); } fn print_time(time: &Time, prefix: &str) { - let (offset, sign) = match time.offset_minutes() { - n if n < 0 => (-n, '-'), - n => (n, '+'), - }; + let offset = time.offset_minutes(); let (hours, minutes) = (offset / 60, offset % 60); - let ts = time::Timespec::new(time.seconds() + - (time.offset_minutes() as i64) * 60, 0); - let time = time::at(ts); - - println!("{}{} {}{:02}{:02}", prefix, - time.strftime("%a %b %e %T %Y").unwrap(), sign, hours, minutes); + let dt = time::OffsetDateTime::from_unix_timestamp(time.seconds()).unwrap(); + let dto = dt.to_offset(time::UtcOffset::from_hms(hours as i8, minutes as i8, 0).unwrap()); + let format = time::format_description::parse("[weekday repr:short] [month repr:short] [day padding:space] [hour]:[minute]:[second] [year] [offset_hour sign:mandatory][offset_minute]") + .unwrap(); + let time_str = dto.format(&format).unwrap(); + println!("{}{}", prefix, time_str); } -fn match_with_parent(repo: &Repository, commit: &Commit, parent: &Commit, - opts: &mut DiffOptions) -> Result { - let a = try!(parent.tree()); - let b = try!(commit.tree()); - let diff = try!(repo.diff_tree_to_tree(Some(&a), Some(&b), Some(opts))); +fn match_with_parent( + repo: &Repository, + commit: &Commit, + parent: &Commit, + opts: &mut DiffOptions, +) -> Result { + let a = parent.tree()?; + let b = commit.tree()?; + let diff = repo.diff_tree_to_tree(Some(&a), Some(&b), Some(opts))?; Ok(diff.deltas().len() > 0) } impl Args { fn min_parents(&self) -> usize { - if self.flag_no_min_parents { return 0 } - self.flag_min_parents.unwrap_or(if self.flag_merges {2} else {0}) + if self.flag_no_min_parents { + return 0; + } + self.flag_min_parents + .unwrap_or(if self.flag_merges { 2 } else { 0 }) } fn max_parents(&self) -> Option { - if self.flag_no_max_parents { return None } - self.flag_max_parents.or(if self.flag_no_merges {Some(1)} else {None}) + if self.flag_no_max_parents { + return None; + } + self.flag_max_parents + .or(if self.flag_no_merges { Some(1) } else { None }) } } fn main() { - const USAGE: &'static str = " -usage: log [options] [..] [--] [..] - -Options: - --topo-order sort commits in topological order - --date-order sort commits in date order - --reverse sort commits in reverse - --author author to sort by - --committer committer to sort by - --grep pattern to filter commit messages by - --git-dir alternative git directory to use - --skip number of commits to skip - -n, --max-count maximum number of commits to show - --merges only show merge commits - --no-merges don't show merge commits - --no-min-parents don't require a minimum number of parents - --no-max-parents don't require a maximum number of parents - --max-parents specify a maximum number of parents for a commit - --min-parents specify a minimum number of parents for a commit - -p, --patch show commit diff - -h, --help show this message -"; - - let args = Docopt::new(USAGE).and_then(|d| d.decode()) - .unwrap_or_else(|e| e.exit()); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/examples/ls-remote.rs b/examples/ls-remote.rs index 7522cafd78..f88baaf906 100644 --- a/examples/ls-remote.rs +++ b/examples/ls-remote.rs @@ -14,48 +14,36 @@ #![deny(warnings)] -extern crate git2; -extern crate docopt; -extern crate rustc_serialize; +use clap::Parser; +use git2::{Direction, Repository}; -use docopt::Docopt; -use git2::{Repository, Direction}; - -#[derive(RustcDecodable)] +#[derive(Parser)] struct Args { + #[structopt(name = "remote")] arg_remote: String, } fn run(args: &Args) -> Result<(), git2::Error> { - let repo = try!(Repository::open(".")); + let repo = Repository::open(".")?; let remote = &args.arg_remote; - let mut remote = try!(repo.find_remote(remote).or_else(|_| { - repo.remote_anonymous(remote) - })); + let mut remote = repo + .find_remote(remote) + .or_else(|_| repo.remote_anonymous(remote))?; // Connect to the remote and call the printing function for each of the // remote references. - try!(remote.connect(Direction::Fetch)); + let connection = remote.connect_auth(Direction::Fetch, None, None)?; // Get the list of references on the remote and print out their name next to // what they point to. - for head in try!(remote.list()).iter() { + for head in connection.list()?.iter() { println!("{}\t{}", head.oid(), head.name()); } - Ok(()) } fn main() { - const USAGE: &'static str = " -usage: ls-remote [option] - -Options: - -h, --help show this message -"; - - let args = Docopt::new(USAGE).and_then(|d| d.decode()) - .unwrap_or_else(|e| e.exit()); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/examples/pull.rs b/examples/pull.rs new file mode 100644 index 0000000000..27f461e546 --- /dev/null +++ b/examples/pull.rs @@ -0,0 +1,208 @@ +/* + * libgit2 "pull" example - shows how to pull remote data into a local branch. + * + * Written by the libgit2 contributors + * + * To the extent possible under law, the author(s) have dedicated all copyright + * and related and neighboring rights to this software to the public domain + * worldwide. This software is distributed without any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication along + * with this software. If not, see + * . + */ + +use clap::Parser; +use git2::Repository; +use std::io::{self, Write}; +use std::str; + +#[derive(Parser)] +struct Args { + arg_remote: Option, + arg_branch: Option, +} + +fn do_fetch<'a>( + repo: &'a git2::Repository, + refs: &[&str], + remote: &'a mut git2::Remote, +) -> Result, git2::Error> { + let mut cb = git2::RemoteCallbacks::new(); + + // Print out our transfer progress. + cb.transfer_progress(|stats| { + if stats.received_objects() == stats.total_objects() { + print!( + "Resolving deltas {}/{}\r", + stats.indexed_deltas(), + stats.total_deltas() + ); + } else if stats.total_objects() > 0 { + print!( + "Received {}/{} objects ({}) in {} bytes\r", + stats.received_objects(), + stats.total_objects(), + stats.indexed_objects(), + stats.received_bytes() + ); + } + io::stdout().flush().unwrap(); + true + }); + + let mut fo = git2::FetchOptions::new(); + fo.remote_callbacks(cb); + // Always fetch all tags. + // Perform a download and also update tips + fo.download_tags(git2::AutotagOption::All); + println!("Fetching {} for repo", remote.name().unwrap()); + remote.fetch(refs, Some(&mut fo), None)?; + + // If there are local objects (we got a thin pack), then tell the user + // how many objects we saved from having to cross the network. + let stats = remote.stats(); + if stats.local_objects() > 0 { + println!( + "\rReceived {}/{} objects in {} bytes (used {} local \ + objects)", + stats.indexed_objects(), + stats.total_objects(), + stats.received_bytes(), + stats.local_objects() + ); + } else { + println!( + "\rReceived {}/{} objects in {} bytes", + stats.indexed_objects(), + stats.total_objects(), + stats.received_bytes() + ); + } + + let fetch_head = repo.find_reference("FETCH_HEAD")?; + Ok(repo.reference_to_annotated_commit(&fetch_head)?) +} + +fn fast_forward( + repo: &Repository, + lb: &mut git2::Reference, + rc: &git2::AnnotatedCommit, +) -> Result<(), git2::Error> { + let name = match lb.name() { + Some(s) => s.to_string(), + None => String::from_utf8_lossy(lb.name_bytes()).to_string(), + }; + let msg = format!("Fast-Forward: Setting {} to id: {}", name, rc.id()); + println!("{}", msg); + lb.set_target(rc.id(), &msg)?; + repo.set_head(&name)?; + repo.checkout_head(Some( + git2::build::CheckoutBuilder::default() + // For some reason the force is required to make the working directory actually get updated + // I suspect we should be adding some logic to handle dirty working directory states + // but this is just an example so maybe not. + .force(), + ))?; + Ok(()) +} + +fn normal_merge( + repo: &Repository, + local: &git2::AnnotatedCommit, + remote: &git2::AnnotatedCommit, +) -> Result<(), git2::Error> { + let local_tree = repo.find_commit(local.id())?.tree()?; + let remote_tree = repo.find_commit(remote.id())?.tree()?; + let ancestor = repo + .find_commit(repo.merge_base(local.id(), remote.id())?)? + .tree()?; + let mut idx = repo.merge_trees(&ancestor, &local_tree, &remote_tree, None)?; + + if idx.has_conflicts() { + println!("Merge conflicts detected..."); + repo.checkout_index(Some(&mut idx), None)?; + return Ok(()); + } + let result_tree = repo.find_tree(idx.write_tree_to(repo)?)?; + // now create the merge commit + let msg = format!("Merge: {} into {}", remote.id(), local.id()); + let sig = repo.signature()?; + let local_commit = repo.find_commit(local.id())?; + let remote_commit = repo.find_commit(remote.id())?; + // Do our merge commit and set current branch head to that commit. + let _merge_commit = repo.commit( + Some("HEAD"), + &sig, + &sig, + &msg, + &result_tree, + &[&local_commit, &remote_commit], + )?; + // Set working tree to match head. + repo.checkout_head(None)?; + Ok(()) +} + +fn do_merge<'a>( + repo: &'a Repository, + remote_branch: &str, + fetch_commit: git2::AnnotatedCommit<'a>, +) -> Result<(), git2::Error> { + // 1. do a merge analysis + let analysis = repo.merge_analysis(&[&fetch_commit])?; + + // 2. Do the appropriate merge + if analysis.0.is_fast_forward() { + println!("Doing a fast forward"); + // do a fast forward + let refname = format!("refs/heads/{}", remote_branch); + match repo.find_reference(&refname) { + Ok(mut r) => { + fast_forward(repo, &mut r, &fetch_commit)?; + } + Err(_) => { + // The branch doesn't exist so just set the reference to the + // commit directly. Usually this is because you are pulling + // into an empty repository. + repo.reference( + &refname, + fetch_commit.id(), + true, + &format!("Setting {} to {}", remote_branch, fetch_commit.id()), + )?; + repo.set_head(&refname)?; + repo.checkout_head(Some( + git2::build::CheckoutBuilder::default() + .allow_conflicts(true) + .conflict_style_merge(true) + .force(), + ))?; + } + }; + } else if analysis.0.is_normal() { + // do a normal merge + let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?; + normal_merge(&repo, &head_commit, &fetch_commit)?; + } else { + println!("Nothing to do..."); + } + Ok(()) +} + +fn run(args: &Args) -> Result<(), git2::Error> { + let remote_name = args.arg_remote.as_ref().map(|s| &s[..]).unwrap_or("origin"); + let remote_branch = args.arg_branch.as_ref().map(|s| &s[..]).unwrap_or("master"); + let repo = Repository::open(".")?; + let mut remote = repo.find_remote(remote_name)?; + let fetch_commit = do_fetch(&repo, &[remote_branch], &mut remote)?; + do_merge(&repo, &remote_branch, fetch_commit) +} + +fn main() { + let args = Args::parse(); + match run(&args) { + Ok(()) => {} + Err(e) => println!("error: {}", e), + } +} diff --git a/examples/rev-list.rs b/examples/rev-list.rs index d42d589e29..2eaed78e9a 100644 --- a/examples/rev-list.rs +++ b/examples/rev-list.rs @@ -15,82 +15,91 @@ #![deny(warnings)] -extern crate git2; -extern crate docopt; -extern crate rustc_serialize; +use clap::Parser; +use git2::{Error, Oid, Repository, Revwalk}; -use docopt::Docopt; -use git2::{Repository, Error, Revwalk, Oid}; - -#[derive(RustcDecodable)] +#[derive(Parser)] struct Args { - arg_spec: Vec, + #[structopt(name = "topo-order", long)] + /// sort commits in topological order flag_topo_order: bool, + #[structopt(name = "date-order", long)] + /// sort commits in date order flag_date_order: bool, + #[structopt(name = "reverse", long)] + /// sort commits in reverse flag_reverse: bool, + #[structopt(name = "not")] + /// don't show flag_not: Vec, + #[structopt(name = "spec", last = true)] + arg_spec: Vec, } fn run(args: &Args) -> Result<(), git2::Error> { - let repo = try!(Repository::open(".")); - let mut revwalk = try!(repo.revwalk()); + let repo = Repository::open(".")?; + let mut revwalk = repo.revwalk()?; - let base = if args.flag_reverse {git2::SORT_REVERSE} else {git2::SORT_NONE}; - revwalk.set_sorting(base | if args.flag_topo_order { - git2::SORT_TOPOLOGICAL - } else if args.flag_date_order { - git2::SORT_TIME + let base = if args.flag_reverse { + git2::Sort::REVERSE } else { - git2::SORT_NONE - }); + git2::Sort::NONE + }; + revwalk.set_sorting( + base | if args.flag_topo_order { + git2::Sort::TOPOLOGICAL + } else if args.flag_date_order { + git2::Sort::TIME + } else { + git2::Sort::NONE + }, + )?; - let specs = args.flag_not.iter().map(|s| (s, true)) - .chain(args.arg_spec.iter().map(|s| (s, false))) - .map(|(spec, hide)| { - if spec.starts_with("^") {(&spec[1..], !hide)} else {(&spec[..], hide)} - }); + let specs = args + .flag_not + .iter() + .map(|s| (s, true)) + .chain(args.arg_spec.iter().map(|s| (s, false))) + .map(|(spec, hide)| { + if spec.starts_with('^') { + (&spec[1..], !hide) + } else { + (&spec[..], hide) + } + }); for (spec, hide) in specs { let id = if spec.contains("..") { - let revspec = try!(repo.revparse(spec)); - if revspec.mode().contains(git2::REVPARSE_MERGE_BASE) { - return Err(Error::from_str("merge bases not implemented")) + let revspec = repo.revparse(spec)?; + if revspec.mode().contains(git2::RevparseMode::MERGE_BASE) { + return Err(Error::from_str("merge bases not implemented")); } - try!(push(&mut revwalk, revspec.from().unwrap().id(), !hide)); + push(&mut revwalk, revspec.from().unwrap().id(), !hide)?; revspec.to().unwrap().id() } else { - try!(repo.revparse_single(spec)).id() + repo.revparse_single(spec)?.id() }; - try!(push(&mut revwalk, id, hide)); + push(&mut revwalk, id, hide)?; } for id in revwalk { - let id = try!(id); + let id = id?; println!("{}", id); } Ok(()) } fn push(revwalk: &mut Revwalk, id: Oid, hide: bool) -> Result<(), Error> { - if hide {revwalk.hide(id)} else {revwalk.push(id)} + if hide { + revwalk.hide(id) + } else { + revwalk.push(id) + } } fn main() { - const USAGE: &'static str = " -usage: rev-list [options] [--] ... - -Options: - --topo-order sort commits in topological order - --date-order sort commits in date order - --reverse sort commits in reverse - --not don't show - -h, --help show this message -"; - - let args = Docopt::new(USAGE).and_then(|d| d.decode()) - .unwrap_or_else(|e| e.exit()); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), } } - diff --git a/examples/rev-parse.rs b/examples/rev-parse.rs index 3705099137..8d3934ea8c 100644 --- a/examples/rev-parse.rs +++ b/examples/rev-parse.rs @@ -14,54 +14,45 @@ #![deny(warnings)] -extern crate git2; -extern crate docopt; -extern crate rustc_serialize; - -use docopt::Docopt; +use clap::Parser; use git2::Repository; -#[derive(RustcDecodable)] +#[derive(Parser)] struct Args { + #[structopt(name = "spec")] arg_spec: String, + #[structopt(name = "dir", long = "git-dir")] + /// directory of the git repository to check flag_git_dir: Option, } fn run(args: &Args) -> Result<(), git2::Error> { let path = args.flag_git_dir.as_ref().map(|s| &s[..]).unwrap_or("."); - let repo = try!(Repository::open(path)); + let repo = Repository::open(path)?; - let revspec = try!(repo.revparse(&args.arg_spec)); + let revspec = repo.revparse(&args.arg_spec)?; - if revspec.mode().contains(git2::REVPARSE_SINGLE) { + if revspec.mode().contains(git2::RevparseMode::SINGLE) { println!("{}", revspec.from().unwrap().id()); - } else if revspec.mode().contains(git2::REVPARSE_RANGE) { + } else if revspec.mode().contains(git2::RevparseMode::RANGE) { let to = revspec.to().unwrap(); let from = revspec.from().unwrap(); println!("{}", to.id()); - if revspec.mode().contains(git2::REVPARSE_MERGE_BASE) { - let base = try!(repo.merge_base(from.id(), to.id())); + if revspec.mode().contains(git2::RevparseMode::MERGE_BASE) { + let base = repo.merge_base(from.id(), to.id())?; println!("{}", base); } println!("^{}", from.id()); } else { - return Err(git2::Error::from_str("invalid results from revparse")) + return Err(git2::Error::from_str("invalid results from revparse")); } Ok(()) } fn main() { - const USAGE: &'static str = " -usage: rev-parse [options] - -Options: - --git-dir directory for the git repository to check -"; - - let args = Docopt::new(USAGE).and_then(|d| d.decode()) - .unwrap_or_else(|e| e.exit()); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/examples/status.rs b/examples/status.rs index f2a0a5723c..0ed61711c8 100644 --- a/examples/status.rs +++ b/examples/status.rs @@ -14,46 +14,72 @@ #![deny(warnings)] -extern crate git2; -extern crate docopt; -extern crate rustc_serialize; - +use clap::Parser; +use git2::{Error, ErrorCode, Repository, StatusOptions, SubmoduleIgnore}; use std::str; use std::time::Duration; -use docopt::Docopt; -use git2::{Repository, Error, StatusOptions, ErrorCode, SubmoduleIgnore}; -#[derive(RustcDecodable)] +#[derive(Parser)] struct Args { arg_spec: Vec, + #[structopt(name = "long", long)] + /// show longer statuses (default) + _flag_long: bool, + /// show short statuses + #[structopt(name = "short", long)] flag_short: bool, - flag_long: bool, + #[structopt(name = "porcelain", long)] + /// ?? flag_porcelain: bool, + #[structopt(name = "branch", short, long)] + /// show branch information flag_branch: bool, + #[structopt(name = "z", short)] + /// ?? flag_z: bool, + #[structopt(name = "ignored", long)] + /// show ignored files as well flag_ignored: bool, + #[structopt(name = "opt-modules", long = "untracked-files")] + /// setting for showing untracked files [no|normal|all] flag_untracked_files: Option, + #[structopt(name = "opt-files", long = "ignore-submodules")] + /// setting for ignoring submodules [all] flag_ignore_submodules: Option, + #[structopt(name = "dir", long = "git-dir")] + /// git directory to analyze flag_git_dir: Option, + #[structopt(name = "repeat", long)] + /// repeatedly show status, sleeping inbetween flag_repeat: bool, + #[structopt(name = "list-submodules", long)] + /// show submodules flag_list_submodules: bool, } #[derive(Eq, PartialEq)] -enum Format { Long, Short, Porcelain } +enum Format { + Long, + Short, + Porcelain, +} fn run(args: &Args) -> Result<(), Error> { - let path = args.flag_git_dir.clone().unwrap_or(".".to_string()); - let repo = try!(Repository::open(&path)); + let path = args.flag_git_dir.clone().unwrap_or_else(|| ".".to_string()); + let repo = Repository::open(&path)?; if repo.is_bare() { - return Err(Error::from_str("cannot report status on bare repository")) + return Err(Error::from_str("cannot report status on bare repository")); } let mut opts = StatusOptions::new(); opts.include_ignored(args.flag_ignored); match args.flag_untracked_files.as_ref().map(|s| &s[..]) { - Some("no") => { opts.include_untracked(false); } - Some("normal") => { opts.include_untracked(true); } + Some("no") => { + opts.include_untracked(false); + } + Some("normal") => { + opts.include_untracked(true); + } Some("all") => { opts.include_untracked(true).recurse_untracked_dirs(true); } @@ -61,12 +87,14 @@ fn run(args: &Args) -> Result<(), Error> { None => {} } match args.flag_ignore_submodules.as_ref().map(|s| &s[..]) { - Some("all") => { opts.exclude_submodules(true); } + Some("all") => { + opts.exclude_submodules(true); + } Some(_) => return Err(Error::from_str("invalid ignore-submodules value")), None => {} } opts.include_untracked(!args.flag_ignored); - for spec in args.arg_spec.iter() { + for spec in &args.arg_spec { opts.pathspec(spec); } @@ -75,41 +103,44 @@ fn run(args: &Args) -> Result<(), Error> { println!("\u{1b}[H\u{1b}[2J"); } - let statuses = try!(repo.statuses(Some(&mut opts))); + let statuses = repo.statuses(Some(&mut opts))?; if args.flag_branch { - try!(show_branch(&repo, args.format())); + show_branch(&repo, &args.format())?; } if args.flag_list_submodules { - try!(print_submodules(&repo)); + print_submodules(&repo)?; } if args.format() == Format::Long { - print_long(statuses); + print_long(&statuses); } else { - print_short(&repo, statuses); + print_short(&repo, &statuses); } if args.flag_repeat { std::thread::sleep(Duration::new(10, 0)); } else { - return Ok(()) + return Ok(()); } } } -fn show_branch(repo: &Repository, format: Format) -> Result<(), Error> { +fn show_branch(repo: &Repository, format: &Format) -> Result<(), Error> { let head = match repo.head() { Ok(head) => Some(head), - Err(ref e) if e.code() == ErrorCode::UnbornBranch || - e.code() == ErrorCode::NotFound => None, + Err(ref e) if e.code() == ErrorCode::UnbornBranch || e.code() == ErrorCode::NotFound => { + None + } Err(e) => return Err(e), }; let head = head.as_ref().and_then(|h| h.shorthand()); - if format == Format::Long { - println!("# On branch {}", - head.unwrap_or("Not currently on any branch")); + if format == &Format::Long { + println!( + "# On branch {}", + head.unwrap_or("Not currently on any branch") + ); } else { println!("## {}", head.unwrap_or("HEAD (no branch)")); } @@ -117,50 +148,57 @@ fn show_branch(repo: &Repository, format: Format) -> Result<(), Error> { } fn print_submodules(repo: &Repository) -> Result<(), Error> { - let modules = try!(repo.submodules()); + let modules = repo.submodules()?; println!("# Submodules"); - for sm in modules.iter() { - println!("# - submodule '{}' at {}", sm.name().unwrap(), - sm.path().display()); + for sm in &modules { + println!( + "# - submodule '{}' at {}", + sm.name().unwrap(), + sm.path().display() + ); } Ok(()) } // This function print out an output similar to git's status command in long // form, including the command-line hints. -fn print_long(statuses: git2::Statuses) { +fn print_long(statuses: &git2::Statuses) { let mut header = false; let mut rm_in_workdir = false; let mut changes_in_index = false; let mut changed_in_workdir = false; // Print index changes - for entry in statuses.iter().filter(|e| e.status() != git2::STATUS_CURRENT) { - if entry.status().contains(git2::STATUS_WT_DELETED) { + for entry in statuses + .iter() + .filter(|e| e.status() != git2::Status::CURRENT) + { + if entry.status().contains(git2::Status::WT_DELETED) { rm_in_workdir = true; } let istatus = match entry.status() { - s if s.contains(git2::STATUS_INDEX_NEW) => "new file: ", - s if s.contains(git2::STATUS_INDEX_MODIFIED) => "modified: ", - s if s.contains(git2::STATUS_INDEX_DELETED) => "deleted: ", - s if s.contains(git2::STATUS_INDEX_RENAMED) => "renamed: ", - s if s.contains(git2::STATUS_INDEX_TYPECHANGE) => "typechange:", + s if s.contains(git2::Status::INDEX_NEW) => "new file: ", + s if s.contains(git2::Status::INDEX_MODIFIED) => "modified: ", + s if s.contains(git2::Status::INDEX_DELETED) => "deleted: ", + s if s.contains(git2::Status::INDEX_RENAMED) => "renamed: ", + s if s.contains(git2::Status::INDEX_TYPECHANGE) => "typechange:", _ => continue, }; if !header { - println!("\ + println!( + "\ # Changes to be committed: # (use \"git reset HEAD ...\" to unstage) -#"); +#" + ); header = true; } let old_path = entry.head_to_index().unwrap().old_file().path(); let new_path = entry.head_to_index().unwrap().new_file().path(); match (old_path, new_path) { - (Some(ref old), Some(ref new)) if old != new => { - println!("#\t{} {} -> {}", istatus, old.display(), - new.display()); + (Some(old), Some(new)) if old != new => { + println!("#\t{} {} -> {}", istatus, old.display(), new.display()); } (old, new) => { println!("#\t{} {}", istatus, old.or(new).unwrap().display()); @@ -176,38 +214,39 @@ fn print_long(statuses: git2::Statuses) { // Print workdir changes to tracked files for entry in statuses.iter() { - // With `STATUS_OPT_INCLUDE_UNMODIFIED` (not used in this example) + // With `Status::OPT_INCLUDE_UNMODIFIED` (not used in this example) // `index_to_workdir` may not be `None` even if there are no differences, // in which case it will be a `Delta::Unmodified`. - if entry.status() == git2::STATUS_CURRENT || - entry.index_to_workdir().is_none() { - continue + if entry.status() == git2::Status::CURRENT || entry.index_to_workdir().is_none() { + continue; } let istatus = match entry.status() { - s if s.contains(git2::STATUS_WT_MODIFIED) => "modified: ", - s if s.contains(git2::STATUS_WT_DELETED) => "deleted: ", - s if s.contains(git2::STATUS_WT_RENAMED) => "renamed: ", - s if s.contains(git2::STATUS_WT_TYPECHANGE) => "typechange:", + s if s.contains(git2::Status::WT_MODIFIED) => "modified: ", + s if s.contains(git2::Status::WT_DELETED) => "deleted: ", + s if s.contains(git2::Status::WT_RENAMED) => "renamed: ", + s if s.contains(git2::Status::WT_TYPECHANGE) => "typechange:", _ => continue, }; if !header { - println!("\ + println!( + "\ # Changes not staged for commit: # (use \"git add{} ...\" to update what will be committed) # (use \"git checkout -- ...\" to discard changes in working directory) #\ - ", if rm_in_workdir {"/rm"} else {""}); + ", + if rm_in_workdir { "/rm" } else { "" } + ); header = true; } let old_path = entry.index_to_workdir().unwrap().old_file().path(); let new_path = entry.index_to_workdir().unwrap().new_file().path(); match (old_path, new_path) { - (Some(ref old), Some(ref new)) if old != new => { - println!("#\t{} {} -> {}", istatus, old.display(), - new.display()); + (Some(old), Some(new)) if old != new => { + println!("#\t{} {} -> {}", istatus, old.display(), new.display()); } (old, new) => { println!("#\t{} {}", istatus, old.or(new).unwrap().display()); @@ -222,12 +261,17 @@ fn print_long(statuses: git2::Statuses) { header = false; // Print untracked files - for entry in statuses.iter().filter(|e| e.status() == git2::STATUS_WT_NEW) { + for entry in statuses + .iter() + .filter(|e| e.status() == git2::Status::WT_NEW) + { if !header { - println!("\ + println!( + "\ # Untracked files # (use \"git add ...\" to include in what will be committed) -#"); +#" + ); header = true; } let file = entry.index_to_workdir().unwrap().old_file().path().unwrap(); @@ -236,12 +280,17 @@ fn print_long(statuses: git2::Statuses) { header = false; // Print ignored files - for entry in statuses.iter().filter(|e| e.status() == git2::STATUS_IGNORED) { + for entry in statuses + .iter() + .filter(|e| e.status() == git2::Status::IGNORED) + { if !header { - println!("\ + println!( + "\ # Ignored files # (use \"git add -f ...\" to include in what will be committed) -#"); +#" + ); header = true; } let file = entry.index_to_workdir().unwrap().old_file().path().unwrap(); @@ -249,39 +298,49 @@ fn print_long(statuses: git2::Statuses) { } if !changes_in_index && changed_in_workdir { - println!("no changes added to commit (use \"git add\" and/or \ - \"git commit -a\")"); + println!( + "no changes added to commit (use \"git add\" and/or \ + \"git commit -a\")" + ); } } // This version of the output prefixes each path with two status columns and // shows submodule status information. -fn print_short(repo: &Repository, statuses: git2::Statuses) { - for entry in statuses.iter().filter(|e| e.status() != git2::STATUS_CURRENT) { +fn print_short(repo: &Repository, statuses: &git2::Statuses) { + for entry in statuses + .iter() + .filter(|e| e.status() != git2::Status::CURRENT) + { let mut istatus = match entry.status() { - s if s.contains(git2::STATUS_INDEX_NEW) => 'A', - s if s.contains(git2::STATUS_INDEX_MODIFIED) => 'M', - s if s.contains(git2::STATUS_INDEX_DELETED) => 'D', - s if s.contains(git2::STATUS_INDEX_RENAMED) => 'R', - s if s.contains(git2::STATUS_INDEX_TYPECHANGE) => 'T', + s if s.contains(git2::Status::INDEX_NEW) => 'A', + s if s.contains(git2::Status::INDEX_MODIFIED) => 'M', + s if s.contains(git2::Status::INDEX_DELETED) => 'D', + s if s.contains(git2::Status::INDEX_RENAMED) => 'R', + s if s.contains(git2::Status::INDEX_TYPECHANGE) => 'T', _ => ' ', }; let mut wstatus = match entry.status() { - s if s.contains(git2::STATUS_WT_NEW) => { - if istatus == ' ' { istatus = '?'; } '?' + s if s.contains(git2::Status::WT_NEW) => { + if istatus == ' ' { + istatus = '?'; + } + '?' } - s if s.contains(git2::STATUS_WT_MODIFIED) => 'M', - s if s.contains(git2::STATUS_WT_DELETED) => 'D', - s if s.contains(git2::STATUS_WT_RENAMED) => 'R', - s if s.contains(git2::STATUS_WT_TYPECHANGE) => 'T', + s if s.contains(git2::Status::WT_MODIFIED) => 'M', + s if s.contains(git2::Status::WT_DELETED) => 'D', + s if s.contains(git2::Status::WT_RENAMED) => 'R', + s if s.contains(git2::Status::WT_TYPECHANGE) => 'T', _ => ' ', }; - if entry.status().contains(git2::STATUS_IGNORED) { + if entry.status().contains(git2::Status::IGNORED) { istatus = '!'; wstatus = '!'; } - if istatus == '?' && wstatus == '?' { continue } + if istatus == '?' && wstatus == '?' { + continue; + } let mut extra = ""; // A commit in a tree is how submodules are stored, so let's go take a @@ -290,18 +349,19 @@ fn print_short(repo: &Repository, statuses: git2::Statuses) { // TODO: check for GIT_FILEMODE_COMMIT let status = entry.index_to_workdir().and_then(|diff| { let ignore = SubmoduleIgnore::Unspecified; - diff.new_file().path_bytes() + diff.new_file() + .path_bytes() .and_then(|s| str::from_utf8(s).ok()) .and_then(|name| repo.submodule_status(name, ignore).ok()) }); if let Some(status) = status { - if status.contains(git2::SUBMODULE_STATUS_WD_MODIFIED) { + if status.contains(git2::SubmoduleStatus::WD_MODIFIED) { extra = " (new commits)"; - } else if status.contains(git2::SUBMODULE_STATUS_WD_INDEX_MODIFIED) { - extra = " (modified content)"; - } else if status.contains(git2::SUBMODULE_STATUS_WD_WD_MODIFIED) { + } else if status.contains(git2::SubmoduleStatus::WD_INDEX_MODIFIED) + || status.contains(git2::SubmoduleStatus::WD_WD_MODIFIED) + { extra = " (modified content)"; - } else if status.contains(git2::SUBMODULE_STATUS_WD_UNTRACKED) { + } else if status.contains(git2::SubmoduleStatus::WD_UNTRACKED) { extra = " (untracked content)"; } } @@ -312,60 +372,68 @@ fn print_short(repo: &Repository, statuses: git2::Statuses) { b = diff.new_file().path(); } if let Some(diff) = entry.index_to_workdir() { - a = a.or(diff.old_file().path()); - b = b.or(diff.old_file().path()); + a = a.or_else(|| diff.old_file().path()); + b = b.or_else(|| diff.old_file().path()); c = diff.new_file().path(); } match (istatus, wstatus) { - ('R', 'R') => println!("RR {} {} {}{}", a.unwrap().display(), - b.unwrap().display(), c.unwrap().display(), - extra), - ('R', w) => println!("R{} {} {}{}", w, a.unwrap().display(), - b.unwrap().display(), extra), - (i, 'R') => println!("{}R {} {}{}", i, a.unwrap().display(), - c.unwrap().display(), extra), + ('R', 'R') => println!( + "RR {} {} {}{}", + a.unwrap().display(), + b.unwrap().display(), + c.unwrap().display(), + extra + ), + ('R', w) => println!( + "R{} {} {}{}", + w, + a.unwrap().display(), + b.unwrap().display(), + extra + ), + (i, 'R') => println!( + "{}R {} {}{}", + i, + a.unwrap().display(), + c.unwrap().display(), + extra + ), (i, w) => println!("{}{} {}{}", i, w, a.unwrap().display(), extra), } } - for entry in statuses.iter().filter(|e| e.status() == git2::STATUS_WT_NEW) { - println!("?? {}", entry.index_to_workdir().unwrap().old_file() - .path().unwrap().display()); + for entry in statuses + .iter() + .filter(|e| e.status() == git2::Status::WT_NEW) + { + println!( + "?? {}", + entry + .index_to_workdir() + .unwrap() + .old_file() + .path() + .unwrap() + .display() + ); } } impl Args { fn format(&self) -> Format { - if self.flag_short { Format::Short } - else if self.flag_long { Format::Long } - else if self.flag_porcelain { Format::Porcelain } - else if self.flag_z { Format::Porcelain } - else { Format::Long } + if self.flag_short { + Format::Short + } else if self.flag_porcelain || self.flag_z { + Format::Porcelain + } else { + Format::Long + } } } fn main() { - const USAGE: &'static str = " -usage: status [options] [--] [..] - -Options: - -s, --short show short statuses - --long show longer statuses (default) - --porcelain ?? - -b, --branch show branch information - -z ?? - --ignored show ignored files as well - --untracked-files setting for showing untracked files [no|normal|all] - --ignore-submodules setting for ignoring submodules [all] - --git-dir git directory to analyze - --repeat repeatedly show status, sleeping inbetween - --list-submodules show submodules - -h, --help show this message -"; - - let args = Docopt::new(USAGE).and_then(|d| d.decode()) - .unwrap_or_else(|e| e.exit()); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/examples/tag.rs b/examples/tag.rs index a584908d09..5d2af02063 100644 --- a/examples/tag.rs +++ b/examples/tag.rs @@ -14,52 +14,59 @@ #![deny(warnings)] -extern crate git2; -extern crate docopt; -extern crate rustc_serialize; - +use clap::Parser; +use git2::{Commit, Error, Repository, Tag}; use std::str; -use docopt::Docopt; -use git2::{Repository, Error, Tag, Commit}; -#[derive(RustcDecodable)] +#[derive(Parser)] struct Args { arg_tagname: Option, arg_object: Option, arg_pattern: Option, + #[structopt(name = "n", short)] + /// specify number of lines from the annotation to print flag_n: Option, + #[structopt(name = "force", short, long)] + /// replace an existing tag with the given name flag_force: bool, + #[structopt(name = "list", short, long)] + /// list tags with names matching the pattern given flag_list: bool, + #[structopt(name = "tag", short, long = "delete")] + /// delete the tag specified flag_delete: Option, + #[structopt(name = "msg", short, long = "message")] + /// message for a new tag flag_message: Option, } fn run(args: &Args) -> Result<(), Error> { - let repo = try!(Repository::open(".")); + let repo = Repository::open(".")?; if let Some(ref name) = args.arg_tagname { let target = args.arg_object.as_ref().map(|s| &s[..]).unwrap_or("HEAD"); - let obj = try!(repo.revparse_single(target)); + let obj = repo.revparse_single(target)?; if let Some(ref message) = args.flag_message { - let sig = try!(repo.signature()); - try!(repo.tag(&name, &obj, &sig, &message, args.flag_force)); + let sig = repo.signature()?; + repo.tag(name, &obj, &sig, message, args.flag_force)?; } else { - try!(repo.tag_lightweight(&name, &obj, args.flag_force)); + repo.tag_lightweight(name, &obj, args.flag_force)?; } - } else if let Some(ref name) = args.flag_delete { - let obj = try!(repo.revparse_single(name)); - let id = try!(obj.short_id()); - try!(repo.tag_delete(name)); - println!("Deleted tag '{}' (was {})", name, - str::from_utf8(&*id).unwrap()); - + let obj = repo.revparse_single(name)?; + let id = obj.short_id()?; + repo.tag_delete(name)?; + println!( + "Deleted tag '{}' (was {})", + name, + str::from_utf8(&*id).unwrap() + ); } else if args.flag_list { let pattern = args.arg_pattern.as_ref().map(|s| &s[..]).unwrap_or("*"); - for name in try!(repo.tag_names(Some(pattern))).iter() { + for name in repo.tag_names(Some(pattern))?.iter() { let name = name.unwrap(); - let obj = try!(repo.revparse_single(name)); + let obj = repo.revparse_single(name)?; if let Some(tag) = obj.as_tag() { print_tag(tag, args); @@ -78,7 +85,7 @@ fn print_tag(tag: &Tag, args: &Args) { if args.flag_n.is_some() { print_list_lines(tag.message(), args); } else { - println!(""); + println!(); } } @@ -87,7 +94,7 @@ fn print_commit(commit: &Commit, name: &str, args: &Args) { if args.flag_n.is_some() { print_list_lines(commit.message(), args); } else { - println!(""); + println!(); } } @@ -96,12 +103,15 @@ fn print_name(name: &str) { } fn print_list_lines(message: Option<&str>, args: &Args) { - let message = match message { Some(s) => s, None => return }; + let message = match message { + Some(s) => s, + None => return, + }; let mut lines = message.lines().filter(|l| !l.trim().is_empty()); if let Some(first) = lines.next() { print!("{}", first); } - println!(""); + println!(); for line in lines.take(args.flag_n.unwrap_or(0) as usize) { print!(" {}", line); @@ -109,23 +119,7 @@ fn print_list_lines(message: Option<&str>, args: &Args) { } fn main() { - const USAGE: &'static str = " -usage: - tag [-a] [-f] [-m ] [] - tag -d - tag [-n ] -l [] - -Options: - -n specify number of lines from teh annotation to print - -f, --force replace an existing tag with the given name - -l, --list list tags with names matching the pattern given - -d, --delete delete the tag specified - -m, --message message for a new tag - -h, --help show this message -"; - - let args = Docopt::new(USAGE).and_then(|d| d.decode()) - .unwrap_or_else(|e| e.exit()); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/git2-curl/CHANGELOG.md b/git2-curl/CHANGELOG.md new file mode 100644 index 0000000000..a2eb29eef2 --- /dev/null +++ b/git2-curl/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +## 0.21.0 - 2025-01-04 +[0.20.0...0.21.0](https://github.com/rust-lang/git2-rs/compare/git2-curl-0.20.0...git2-curl-0.21.0) + +- Updated to [git2 0.20.0](../CHANGELOG.md#0200---2025-01-04) + +## 0.20.0 - 2024-06-13 +[0.19.0...0.20.0](https://github.com/rust-lang/git2-rs/compare/git2-curl-0.19.0...git2-curl-0.20.0) + +- Updated to [git2 0.19.0](../CHANGELOG.md#0190---2024-06-13) + +## 0.19.0 - 2023-08-28 +[0.18.0...0.19.0](https://github.com/rust-lang/git2-rs/compare/git2-curl-0.18.0...git2-curl-0.19.0) + +- Updated to [git2 0.18.0](../CHANGELOG.md#0180---2023-08-26) + +## 0.18.0 - 2023-04-02 +[0.17.0...0.18.0](https://github.com/rust-lang/git2-rs/compare/git2-curl-0.17.0...git2-curl-0.18.0) + +- Updated to [git2 0.17.0](../CHANGELOG.md#0170---2023-04-02) + +## 0.17.0 - 2023-01-10 +[0.16.0...0.17.0](https://github.com/rust-lang/git2-rs/compare/git2-curl-0.16.0...git2-curl-0.17.0) + +- Updated to [git2 0.16.0](../CHANGELOG.md#0160---2023-01-10) + +## 0.16.0 - 2022-07-28 +[0.15.0...0.16.0](https://github.com/rust-lang/git2-rs/compare/git2-curl-0.15.0...git2-curl-0.16.0) + +- Updated to [git2 0.15.0](../CHANGELOG.md#0150---2022-07-28) + +## 0.15.0 - 2022-02-28 +[0.14.1...0.15.0](https://github.com/rust-lang/git2-rs/compare/git2-curl-0.14.1...git2-curl-0.15.0) + +- Updated to [git2 0.14.0](../CHANGELOG.md#0140---2022-02-24) diff --git a/git2-curl/Cargo.toml b/git2-curl/Cargo.toml index 9ca1413043..dd51ac0778 100644 --- a/git2-curl/Cargo.toml +++ b/git2-curl/Cargo.toml @@ -1,29 +1,28 @@ [package] - name = "git2-curl" -version = "0.4.1" -authors = ["Alex Crichton "] -license = "MIT/Apache-2.0" -repository = "https://github.com/alexcrichton/git2-rs" -homepage = "https://github.com/alexcrichton/git2-rs" -documentation = "http://alexcrichton.com/git2-rs/git2-curl" +version = "0.22.0" +authors = ["Josh Triplett ", "Alex Crichton "] +license = "MIT OR Apache-2.0" +repository = "https://github.com/rust-lang/git2-rs" +documentation = "https://docs.rs/git2-curl" description = """ Backend for an HTTP transport in libgit2 powered by libcurl. Intended to be used with the git2 crate. """ +edition = "2021" [dependencies] -curl = { git = "https://github.com/alexcrichton/curl-rust" } -url = "1.0" -log = "0.3" -git2 = { path = "..", version = "0.4" } +curl = "0.4.33" +url = "2.5.4" +log = "0.4" +git2 = { path = "..", version = "0.21", default-features = false } [dev-dependencies] -civet = "0.8" -conduit = "0.7" -conduit-git-http-backend = "0.7" -tempdir = "0.3" +tempfile = "3.0" + +[features] +zlib-ng-compat = ["git2/zlib-ng-compat", "curl/zlib-ng-compat"] [[test]] name = "all" diff --git a/git2-curl/LICENSE-APACHE b/git2-curl/LICENSE-APACHE new file mode 120000 index 0000000000..965b606f33 --- /dev/null +++ b/git2-curl/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/git2-curl/LICENSE-MIT b/git2-curl/LICENSE-MIT new file mode 120000 index 0000000000..76219eb72e --- /dev/null +++ b/git2-curl/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/git2-curl/src/lib.rs b/git2-curl/src/lib.rs index 555c04baea..6fa4532772 100644 --- a/git2-curl/src/lib.rs +++ b/git2-curl/src/lib.rs @@ -15,35 +15,38 @@ //! > **NOTE**: At this time this crate likely does not support a `git push` //! > operation, only clones. -#![doc(html_root_url = "http://alexcrichton.com/git2-rs")] +#![doc(html_root_url = "https://docs.rs/git2-curl/0.21")] +#![deny(missing_docs)] +#![warn(rust_2018_idioms)] +#![cfg_attr(test, deny(warnings))] -extern crate git2; -extern crate curl; -extern crate url; -#[macro_use] extern crate log; - -use std::ascii::AsciiExt; use std::error; use std::io::prelude::*; use std::io::{self, Cursor}; use std::str; -use std::sync::{Once, ONCE_INIT, Arc, Mutex}; +use std::sync::{Arc, Mutex, Once}; use curl::easy::{Easy, List}; +use git2::transport::SmartSubtransportStream; +use git2::transport::{Service, SmartSubtransport, Transport}; use git2::Error; -use git2::transport::{SmartSubtransportStream}; -use git2::transport::{Transport, SmartSubtransport, Service}; +use log::{debug, info}; use url::Url; struct CurlTransport { handle: Arc>, + /// The URL of the remote server, e.g. `https://github.com/user/repo` + /// + /// This is an empty string until the first action is performed. + /// If there is an HTTP redirect, this will be updated with the new URL. + base_url: Arc>, } struct CurlSubtransport { handle: Arc>, service: &'static str, url_path: &'static str, - base_url: String, + base_url: Arc>, method: &'static str, reader: Option>>, sent_request: bool, @@ -66,48 +69,51 @@ struct CurlSubtransport { /// This function may be called concurrently, but only the first `handle` will /// be used. All others will be discarded. pub unsafe fn register(handle: Easy) { - static INIT: Once = ONCE_INIT; + static INIT: Once = Once::new(); let handle = Arc::new(Mutex::new(handle)); let handle2 = handle.clone(); INIT.call_once(move || { - git2::transport::register("http", move |remote| { - factory(remote, handle.clone()) - }).unwrap(); - git2::transport::register("https", move |remote| { - factory(remote, handle2.clone()) - }).unwrap(); + git2::transport::register("http", move |remote| factory(remote, handle.clone())).unwrap(); + git2::transport::register("https", move |remote| factory(remote, handle2.clone())).unwrap(); }); } -fn factory(remote: &git2::Remote, handle: Arc>) - -> Result { - Transport::smart(remote, true, CurlTransport { handle: handle }) +fn factory(remote: &git2::Remote<'_>, handle: Arc>) -> Result { + Transport::smart( + remote, + true, + CurlTransport { + handle: handle, + base_url: Arc::new(Mutex::new(String::new())), + }, + ) } impl SmartSubtransport for CurlTransport { - fn action(&self, url: &str, action: Service) - -> Result, Error> { + fn action( + &self, + url: &str, + action: Service, + ) -> Result, Error> { + let mut base_url = self.base_url.lock().unwrap(); + if base_url.len() == 0 { + *base_url = url.to_string(); + } let (service, path, method) = match action { - Service::UploadPackLs => { - ("upload-pack", "/info/refs?service=git-upload-pack", "GET") - } - Service::UploadPack => { - ("upload-pack", "/git-upload-pack", "POST") - } + Service::UploadPackLs => ("upload-pack", "/info/refs?service=git-upload-pack", "GET"), + Service::UploadPack => ("upload-pack", "/git-upload-pack", "POST"), Service::ReceivePackLs => { ("receive-pack", "/info/refs?service=git-receive-pack", "GET") } - Service::ReceivePack => { - ("receive-pack", "/git-receive-pack", "POST") - } + Service::ReceivePack => ("receive-pack", "/git-receive-pack", "POST"), }; info!("action {} {}", service, path); Ok(Box::new(CurlSubtransport { handle: self.handle.clone(), service: service, url_path: path, - base_url: url.to_string(), + base_url: self.base_url.clone(), method: method, reader: None, sent_request: false, @@ -120,21 +126,19 @@ impl SmartSubtransport for CurlTransport { } impl CurlSubtransport { - fn err>>(&self, err: E) -> io::Error { + fn err>>(&self, err: E) -> io::Error { io::Error::new(io::ErrorKind::Other, err) } fn execute(&mut self, data: &[u8]) -> io::Result<()> { if self.sent_request { - return Err(self.err("already sent HTTP request")) + return Err(self.err("already sent HTTP request")); } let agent = format!("git/1.0 (git2-curl {})", env!("CARGO_PKG_VERSION")); // Parse our input URL to figure out the host - let url = format!("{}{}", self.base_url, self.url_path); - let parsed = try!(Url::parse(&url).map_err(|_| { - self.err("invalid url, failed to parse") - })); + let url = format!("{}{}", self.base_url.lock().unwrap(), self.url_path); + let parsed = Url::parse(&url).map_err(|_| self.err("invalid url, failed to parse"))?; let host = match parsed.host_str() { Some(host) => host, None => return Err(self.err("invalid url, did not have a host")), @@ -143,30 +147,34 @@ impl CurlSubtransport { // Prep the request debug!("request to {}", url); let mut h = self.handle.lock().unwrap(); - try!(h.url(https://github.com/&url)); - try!(h.useragent(&agent)); - try!(h.follow_location(true)); + h.url(https://github.com/&url)?; + h.useragent(&agent)?; + h.follow_location(true)?; match self.method { - "GET" => try!(h.get(true)), - "PUT" => try!(h.put(true)), - "POST" => try!(h.post(true)), - other => try!(h.custom_request(other)), + "GET" => h.get(true)?, + "PUT" => h.put(true)?, + "POST" => h.post(true)?, + other => h.custom_request(other)?, } let mut headers = List::new(); - try!(headers.append(&format!("Host: {}", host))); + headers.append(&format!("Host: {}", host))?; if data.len() > 0 { - try!(h.post_fields_copy(data)); - try!(headers.append(&format!("Accept: application/x-git-{}-result", - self.service))); - try!(headers.append(&format!("Content-Type: \ - application/x-git-{}-request", - self.service))); + h.post_fields_copy(data)?; + headers.append(&format!( + "Accept: application/x-git-{}-result", + self.service + ))?; + headers.append(&format!( + "Content-Type: \ + application/x-git-{}-request", + self.service + ))?; } else { - try!(headers.append("Accept: */*")); + headers.append("Accept: */*")?; } - try!(headers.append("Expect:")); - try!(h.http_headers(headers)); + headers.append("Expect:")?; + h.http_headers(headers)?; let mut content_type = None; let mut data = Vec::new(); @@ -174,7 +182,7 @@ impl CurlSubtransport { let mut h = h.transfer(); // Look for the Content-Type header - try!(h.header_function(|header| { + h.header_function(|header| { let header = match str::from_utf8(header) { Ok(s) => s, Err(..) => return true, @@ -190,47 +198,73 @@ impl CurlSubtransport { } true - })); + })?; // Collect the request's response in-memory - try!(h.write_function(|buf| { + h.write_function(|buf| { data.extend_from_slice(buf); Ok(buf.len()) - })); + })?; // Send the request - try!(h.perform()); + h.perform()?; } - let code = try!(h.response_code()); + let code = h.response_code()?; if code != 200 { - return Err(self.err(&format!("failed to receive HTTP 200 response: \ - got {}", code)[..])) + return Err(self.err( + &format!( + "failed to receive HTTP 200 response: \ + got {}", + code + )[..], + )); } // Check returned headers let expected = match self.method { - "GET" => format!("application/x-git-{}-advertisement", - self.service), + "GET" => format!("application/x-git-{}-advertisement", self.service), _ => format!("application/x-git-{}-result", self.service), }; match content_type { Some(ref content_type) if *content_type != expected => { - return Err(self.err(&format!("expected a Content-Type header \ - with `{}` but found `{}`", - expected, content_type)[..])) + return Err(self.err( + &format!( + "expected a Content-Type header \ + with `{}` but found `{}`", + expected, content_type + )[..], + )) } Some(..) => {} None => { - return Err(self.err(&format!("expected a Content-Type header \ - with `{}` but didn't find one", - expected)[..])) + return Err(self.err( + &format!( + "expected a Content-Type header \ + with `{}` but didn't find one", + expected + )[..], + )) } } // Ok, time to read off some data. let rdr = Cursor::new(data); self.reader = Some(rdr); + + // If there was a redirect, update the `CurlTransport` with the new base. + if let Ok(Some(effective_url)) = h.effective_url() { + let new_base = if effective_url.ends_with(self.url_path) { + // Strip the action from the end. + &effective_url[..effective_url.len() - self.url_path.len()] + } else { + // I'm not sure if this code path makes sense, but it's what + // libgit does. + effective_url + }; + *self.base_url.lock().unwrap() = new_base.to_string(); + } + Ok(()) } } @@ -238,7 +272,7 @@ impl CurlSubtransport { impl Read for CurlSubtransport { fn read(&mut self, buf: &mut [u8]) -> io::Result { if self.reader.is_none() { - try!(self.execute(&[])); + self.execute(&[])?; } self.reader.as_mut().unwrap().read(buf) } @@ -247,9 +281,11 @@ impl Read for CurlSubtransport { impl Write for CurlSubtransport { fn write(&mut self, data: &[u8]) -> io::Result { if self.reader.is_none() { - try!(self.execute(data)); + self.execute(data)?; } Ok(data.len()) } - fn flush(&mut self) -> io::Result<()> { Ok(()) } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } } diff --git a/git2-curl/tests/all.rs b/git2-curl/tests/all.rs index 7dbc5a8e17..6b6fe9aca4 100644 --- a/git2-curl/tests/all.rs +++ b/git2-curl/tests/all.rs @@ -1,29 +1,141 @@ -extern crate conduit_git_http_backend as git_backend; -extern crate git2_curl; -extern crate civet; -extern crate conduit; -extern crate curl; -extern crate git2; -extern crate tempdir; - -use civet::{Server, Config}; +//! A simple test to verify that git2-curl can communicate to git over HTTP. + use std::fs::File; +use std::io::{BufRead, BufReader, Read, Write}; +use std::net::{TcpListener, TcpStream}; use std::path::Path; -use tempdir::TempDir; +use std::process::{Command, Stdio}; +use tempfile::TempDir; const PORT: u16 = 7848; +/// This is a very bare-bones HTTP server, enough to run git-http-backend as a CGI. +fn handle_client(stream: TcpStream, working_dir: &Path) { + let mut buf = BufReader::new(stream); + let mut line = String::new(); + if buf.read_line(&mut line).unwrap() == 0 { + panic!("unexpected termination"); + } + // Read the "METHOD path HTTP/1.1" line. + let mut parts = line.split_ascii_whitespace(); + let method = parts.next().unwrap(); + let path = parts.next().unwrap(); + let (path, query) = path.split_once('?').unwrap_or_else(|| (path, "")); + let mut content_length = 0; + let mut content_type = String::new(); + // Read headers. + loop { + let mut header = String::new(); + if buf.read_line(&mut header).unwrap() == 0 { + panic!("unexpected header"); + } + if header == "\r\n" { + break; + } + let (name, value) = header.split_once(':').unwrap(); + let name = name.to_ascii_lowercase(); + match name.as_str() { + "content-length" => content_length = value.trim().parse().unwrap_or(0), + "content-type" => content_type = value.trim().to_owned(), + _ => {} + } + } + let mut body = vec![0u8; content_length]; + if content_length > 0 { + buf.read_exact(&mut body).unwrap(); + } + + let mut cgi_env = vec![ + ("GIT_PROJECT_ROOT", "."), + ("GIT_HTTP_EXPORT_ALL", "1"), + ("REQUEST_METHOD", method), + ("PATH_INFO", path), + ("QUERY_STRING", query), + ("CONTENT_TYPE", &content_type), + ("REMOTE_USER", ""), + ("REMOTE_ADDR", "127.0.0.1"), + ("AUTH_TYPE", ""), + ("REMOTE_HOST", ""), + ("SERVER_PROTOCOL", "HTTP/1.1"), + ("REQUEST_URI", path), + ]; + + let cl = content_length.to_string(); + cgi_env.push(("CONTENT_LENGTH", cl.as_str())); + + // Spawn git-http-backend + let mut cmd = Command::new("git"); + cmd.current_dir(working_dir); + cmd.arg("http-backend"); + for (k, v) in &cgi_env { + cmd.env(k, v); + } + cmd.stdin(Stdio::piped()).stdout(Stdio::piped()); + + let mut child = cmd.spawn().expect("failed to spawn git-http-backend"); + + if content_length > 0 { + child.stdin.as_mut().unwrap().write_all(&body).unwrap(); + } + + let mut cgi_output = Vec::new(); + child + .stdout + .as_mut() + .unwrap() + .read_to_end(&mut cgi_output) + .unwrap(); + + // Split CGI output into headers and body. + let index = cgi_output + .windows(4) + .position(|w| w == b"\r\n\r\n") + .unwrap_or(0); + let (headers, body) = (&cgi_output[..index], &cgi_output[index + 4..]); + let content_length = body.len(); + + // Write HTTP response + let mut stream = buf.into_inner(); + stream + .write_all( + &format!( + "HTTP/1.1 200 ok\r\n\ + Connection: close\r\n\ + Content-Length: {content_length}\r\n" + ) + .into_bytes(), + ) + .unwrap(); + stream.write_all(headers).unwrap(); + stream.write_all(b"\r\n\r\n").unwrap(); + stream.write_all(body).unwrap(); + stream.flush().unwrap(); +} + fn main() { + let td = TempDir::new().unwrap(); + let td_path = td.path().to_owned(); + + // Spin up a server for git-http-backend + std::thread::spawn(move || { + let listener = TcpListener::bind(("localhost", PORT)).unwrap(); + for stream in listener.incoming() { + match stream { + Ok(stream) => { + let td_path = td_path.clone(); + std::thread::spawn(move || handle_client(stream, &td_path)); + } + Err(e) => { + panic!("Connection failed: {}", e); + } + } + } + }); + unsafe { git2_curl::register(curl::easy::Easy::new()); } - // Spin up a server for git-http-backend - let td = TempDir::new("wut").unwrap(); - let mut cfg = Config::new(); - cfg.port(PORT).threads(1); - let _a = Server::start(cfg, git_backend::Serve(td.path().to_path_buf())); - // Prep a repo with one file called `foo` let sig = git2::Signature::now("foo", "bar").unwrap(); let r1 = git2::Repository::init(td.path()).unwrap(); @@ -34,15 +146,20 @@ fn main() { index.add_path(Path::new("foo")).unwrap(); index.write().unwrap(); let tree_id = index.write_tree().unwrap(); - r1.commit(Some("HEAD"), &sig, &sig, "test", - &r1.find_tree(tree_id).unwrap(), - &[]).unwrap(); + r1.commit( + Some("HEAD"), + &sig, + &sig, + "test", + &r1.find_tree(tree_id).unwrap(), + &[], + ) + .unwrap(); } // Clone through the git-http-backend - let td2 = TempDir::new("wut2").unwrap(); - let r = git2::Repository::clone(&format!("http://localhost:{}", PORT), - td2.path()).unwrap(); + let td2 = TempDir::new().unwrap(); + let r = git2::Repository::clone(&format!("http://localhost:{}", PORT), td2.path()).unwrap(); assert!(File::open(&td2.path().join("foo")).is_ok()); { File::create(&td.path().join("bar")).unwrap(); @@ -52,17 +169,25 @@ fn main() { let tree_id = index.write_tree().unwrap(); let parent = r1.head().ok().and_then(|h| h.target()).unwrap(); let parent = r1.find_commit(parent).unwrap(); - r1.commit(Some("HEAD"), &sig, &sig, "test", - &r1.find_tree(tree_id).unwrap(), - &[&parent]).unwrap(); + r1.commit( + Some("HEAD"), + &sig, + &sig, + "test", + &r1.find_tree(tree_id).unwrap(), + &[&parent], + ) + .unwrap(); } let mut remote = r.find_remote("origin").unwrap(); - remote.fetch(&["refs/heads/*:refs/heads/*"], None, None).unwrap(); + remote + .fetch(&["refs/heads/*:refs/heads/*"], None, None) + .unwrap(); let b = r.find_branch("master", git2::BranchType::Local).unwrap(); let id = b.get().target().unwrap(); let obj = r.find_object(id, None).unwrap(); - r.reset(&obj, git2::ResetType::Hard, None).unwrap();; + r.reset(&obj, git2::ResetType::Hard, None).unwrap(); assert!(File::open(&td2.path().join("bar")).is_ok()); } diff --git a/libgit2-sys/CHANGELOG.md b/libgit2-sys/CHANGELOG.md new file mode 100644 index 0000000000..06ce1a7253 --- /dev/null +++ b/libgit2-sys/CHANGELOG.md @@ -0,0 +1,233 @@ +# Changelog + +## 0.18.2+1.9.1 - 2025-06-21 +[0.18.1...0.18.2](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.18.1+1.9.0...libgit2-sys-0.18.2+1.9.1) + +### Changed +- Updated to libgit2 [1.9.1](https://github.com/libgit2/libgit2/releases/tag/v1.9.1) + [#1169](https://github.com/rust-lang/git2-rs/pull/1169) + +## 0.18.1+1.9.0 - 2025-03-17 +[0.18.0...0.18.1](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.18.0+1.9.0...libgit2-sys-0.18.1+1.9.0) + +### Added + +- Added binding for `git_branch_upstream_merge` + [#1131](https://github.com/rust-lang/git2-rs/pull/1131) +- Added bindings for `git_merge_file_options` and `git_merge_file_result`, `git_merge_file_options_init`, `git_merge_file_from_index`, `git_merge_file_result_free`, and updated `git_merge_file_flag_t`. + [#1062](https://github.com/rust-lang/git2-rs/pull/1062) + +### Fixed + +- Fixed linking to advapi32 on Windows for recent nightly versions of Rust. + [#1143](https://github.com/rust-lang/git2-rs/pull/1143) + +## 0.18.0+1.9.0 - 2025-01-04 +[0.16.2...0.17.0](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.17.0+1.8.1...libgit2-sys-0.18.0+1.9.0) + +### Added + +- Added bindings for `git_repository_commondir` + [#1079](https://github.com/rust-lang/git2-rs/pull/1079) +- Added bindings for `git_merge_base_octopus` + [#1088](https://github.com/rust-lang/git2-rs/pull/1088) + +### Changed + +- ❗ Updated to libgit2 [1.9.0](https://github.com/libgit2/libgit2/releases/tag/v1.9.0) + [#1111](https://github.com/rust-lang/git2-rs/pull/1111) +- ❗ Removed the `ssh_key_from_memory` Cargo feature, it was unused. + [#1087](https://github.com/rust-lang/git2-rs/pull/1087) + +## 0.17.0+1.8.1 - 2024-06-13 +[0.16.2...0.17.0](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.16.2+1.7.2...libgit2-sys-0.17.0+1.8.1) + +### Changed + +- ❗ Updated to libgit2 [1.8.1](https://github.com/libgit2/libgit2/releases/tag/v1.8.1) + [#1032](https://github.com/rust-lang/git2-rs/pull/1032) + +## 0.16.2+1.7.2 - 2024-02-06 +[0.16.1...0.16.2](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.16.1+1.7.1...libgit2-sys-0.16.2+1.7.2) + +### Added + +- Added binding for `git_commit_lookup_prefix`. + [#1011](https://github.com/rust-lang/git2-rs/pull/1011) +- Added binding for `git_object_lookup_prefix`. + [#1014](https://github.com/rust-lang/git2-rs/pull/1014) + +### Changed + +- ❗ Updated to libgit2 [1.7.2](https://github.com/libgit2/libgit2/releases/tag/v1.7.2). + This fixes [CVE-2024-24575](https://github.com/libgit2/libgit2/security/advisories/GHSA-54mf-x2rh-hq9v) and [CVE-2024-24577](https://github.com/libgit2/libgit2/security/advisories/GHSA-j2v7-4f6v-gpg8). + [#1017](https://github.com/rust-lang/git2-rs/pull/1017) + +## 0.16.1+1.7.1 - 2023-08-28 +[0.16.0...0.16.1](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.16.0+1.7.1...libgit2-sys-0.16.1+1.7.1) + +### Fixed + +- Fixed publish of 0.16.0 missing the libgit2 submodule. + +## 0.16.0+1.7.1 - 2023-08-28 +[0.15.2...0.16.0](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.15.2+1.6.4...libgit2-sys-0.16.0+1.7.1) + +### Added + +- Added LIBGIT2_NO_VENDOR environment variable to force using the system libgit2. + [#966](https://github.com/rust-lang/git2-rs/pull/966) +- Added binding for `git_blame_buffer`. + [#981](https://github.com/rust-lang/git2-rs/pull/981) + +### Changed + +- Updated to libgit2 [1.7.0](https://github.com/libgit2/libgit2/releases/tag/v1.7.0). + [#968](https://github.com/rust-lang/git2-rs/pull/968) +- Updated to libgit2 [1.7.1](https://github.com/libgit2/libgit2/releases/tag/v1.7.1). + [#982](https://github.com/rust-lang/git2-rs/pull/982) + +### Fixed + +- Fixed builds with cargo's `-Zminimal-versions`. + [#960](https://github.com/rust-lang/git2-rs/pull/960) + + +## 0.15.2+1.6.4 - 2023-05-27 +[0.15.1...0.15.2](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.15.1+1.6.4...libgit2-sys-0.15.2+1.6.4) + +### Added + +- Added bindings for stash options. + [#930](https://github.com/rust-lang/git2-rs/pull/930) + +## 0.15.1+1.6.4 - 2023-04-13 +[0.15.0...0.15.1](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.15.0+1.6.3...libgit2-sys-0.15.1+1.6.4) + +### Changed + +- Updated to libgit2 [1.6.4](https://github.com/libgit2/libgit2/releases/tag/v1.6.4). + This brings in a minor fix on Windows when the ProgramData directory does not exist. + [#948](https://github.com/rust-lang/git2-rs/pull/948) + +## 0.15.0+1.6.3 - 2023-04-02 +[0.14.2...0.15.0](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.14.2+1.5.1...libgit2-sys-0.15.0+1.6.3) + +### Added + +- Added bindings for `git_remote_name_is_valid`, `git_reference_name_is_valid`, and `git_tag_name_is_valid`. + [#882](https://github.com/rust-lang/git2-rs/pull/882) +- Added bindings for `git_indexer` support. + [#911](https://github.com/rust-lang/git2-rs/pull/911) +- Added bindings for `git_index_find_prefix`. + [#903](https://github.com/rust-lang/git2-rs/pull/903) +- Added support for the deprecated group-writeable blob file mode. + [#887](https://github.com/rust-lang/git2-rs/pull/887) + +### Changed + +- Updated libssh2-sys from 0.2 to 0.3. + This brings in numerous changes, including SHA2 algorithm support with RSA. + [#919](https://github.com/rust-lang/git2-rs/pull/919) +- Updated to libgit2 [1.6.3](https://github.com/libgit2/libgit2/blob/main/docs/changelog.md#v163). + This brings in many changes, including better SSH host key support on Windows and better SSH host key algorithm negotiation. + 1.6.3 is now the minimum supported version. + [#935](https://github.com/rust-lang/git2-rs/pull/935) +- The `GIT_DIFF_` constants have been changed to be a `git_diff_option_t` type. + [#935](https://github.com/rust-lang/git2-rs/pull/935) + +### Fixed + +- Fixed the rerun-if-changed build script support on Windows. This is only relevant for those working within the git2-rs source tree. + [#916](https://github.com/rust-lang/git2-rs/pull/916) + +## 0.14.2+1.5.1 - 2023-01-20 +[0.14.1...0.14.2](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.14.1+1.5.0...libgit2-sys-0.14.2+1.5.1) + +### Changed +- Updated the bundled libgit2 to [1.5.1](https://github.com/libgit2/libgit2/releases/tag/v1.5.1). + [a233483a3952d6112653be86fb5ce65267e3d5ac](https://github.com/rust-lang/git2-rs/commit/a233483a3952d6112653be86fb5ce65267e3d5ac) + - Changes: [fbea439d4b6fc91c6b619d01b85ab3b7746e4c19...42e5db98b963ae503229c63e44e06e439df50e56](https://github.com/libgit2/libgit2/compare/fbea439d4b6fc91c6b619d01b85ab3b7746e4c19...42e5db98b963ae503229c63e44e06e439df50e56): + - Fixes [GHSA-8643-3wh5-rmjq](https://github.com/libgit2/libgit2/security/advisories/GHSA-8643-3wh5-rmjq) to validate SSH host keys. + - The supported libgit2 system library range is 1.5.1 to less than 1.6.0 or 1.4.5 to less than 1.5.0, which should include this fix. + +## 0.13.5+1.4.5 - 2023-01-20 +[0.13.4...0.13.5](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.13.4+1.4.2...libgit2-sys-0.13.5+1.4.5) + +### Changed +- Updated the bundled libgit2 to [1.4.5](https://github.com/libgit2/libgit2/releases/tag/v1.4.5). + - Changes: [2a0d0bd19b5d13e2ab7f3780e094404828cbb9a7...cd6f679af401eda1f172402006ef8265f8bd58ea](https://github.com/libgit2/libgit2/compare/2a0d0bd19b5d13e2ab7f3780e094404828cbb9a7...cd6f679af401eda1f172402006ef8265f8bd58ea): + - Fixes [GHSA-8643-3wh5-rmjq](https://github.com/libgit2/libgit2/security/advisories/GHSA-8643-3wh5-rmjq) to validate SSH host keys. + - The supported libgit2 system library range is 1.4.5 to less than 1.5.0. + +## 0.14.1+1.5.0 - 2023-01-10 +[0.14.0...0.14.1](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.14.0+1.5.0...libgit2-sys-0.14.1+1.5.0) + +### Added +- Added variants to `git_cert_ssh_raw_type_t`. + [#909](https://github.com/rust-lang/git2-rs/pull/909) + +## 0.14.0+1.5.0 - 2022-07-28 +[0.13.4...0.14.0](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.13.4+1.4.2...libgit2-sys-0.14.0+1.5.0) + +### Added +- Added bindings for ownership validation. + [#839](https://github.com/rust-lang/git2-rs/pull/839) + +### Changed + +- Updated the bundled libgit2 to [1.5.0](https://github.com/libgit2/libgit2/releases/tag/v1.5.0). + [#839](https://github.com/rust-lang/git2-rs/pull/839) + [#858](https://github.com/rust-lang/git2-rs/pull/858) + - Changes: [2a0d0bd19b5d13e2ab7f3780e094404828cbb9a7...fbea439d4b6fc91c6b619d01b85ab3b7746e4c19](https://github.com/libgit2/libgit2/compare/2a0d0bd19b5d13e2ab7f3780e094404828cbb9a7...fbea439d4b6fc91c6b619d01b85ab3b7746e4c19): + - The supported libgit2 system library range is 1.4.4 to less than 1.6.0. + - Fixes [CVE 2022-24765](https://github.com/libgit2/libgit2/releases/tag/v1.4.3). + +## 0.13.4+1.4.2 - 2022-05-10 +[0.13.3...0.13.4](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.13.3+1.4.2...libgit2-sys-0.13.4+1.4.2) + +### Added +- Added bindings for `git_commit_body` + [#835](https://github.com/rust-lang/git2-rs/pull/835) + +## 0.13.3+1.4.2 - 2022-04-27 +[0.13.2...0.13.3](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.13.2+1.4.2...libgit2-sys-0.13.3+1.4.2) + +### Changed +- Updated the bundled libgit2 to 1.5.0-alpha. + [#822](https://github.com/rust-lang/git2-rs/pull/822) + - Changes: [182d0d1ee933de46bf0b5a6ec269bafa77aba9a2...2a0d0bd19b5d13e2ab7f3780e094404828cbb9a7](https://github.com/libgit2/libgit2/compare/182d0d1ee933de46bf0b5a6ec269bafa77aba9a2...2a0d0bd19b5d13e2ab7f3780e094404828cbb9a7) +- Changed the pkg-config probe to restrict linking against a version of a system-installed libgit2 to a version less than 1.5.0. + Previously it would allow any version above 1.4.0 which could pick up an API-breaking version. + [#817](https://github.com/rust-lang/git2-rs/pull/817) +- When using pkg-config to locate libgit2, the system lib dirs are no longer added to the search path. + [#831](https://github.com/rust-lang/git2-rs/pull/831) +- When using the `zlib-ng-compat` Cargo feature, `libssh2-sys` is no longer automatically included unless you also enable the `ssh` feature. + [#833](https://github.com/rust-lang/git2-rs/pull/833) + +## 0.13.2+1.4.2 - 2022-03-10 +[0.13.1...0.13.2](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.13.1+1.4.2...libgit2-sys-0.13.2+1.4.2) + +### Added +- Added bindings for `git_odb_exists_ext`. + [#818](https://github.com/rust-lang/git2-rs/pull/818) + +## 0.13.1+1.4.2 - 2022-02-28 +[0.13.0...0.13.1](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.13.0+1.4.1...libgit2-sys-0.13.1+1.4.2) + +### Changed +- Updated the bundled libgit2 to [1.4.2](https://github.com/libgit2/libgit2/releases/tag/v1.4.2). + [#815](https://github.com/rust-lang/git2-rs/pull/815) + - Changes: [fdd15bcfca6b2ec4b7ecad1aa11a396cb15bd064...182d0d1ee933de46bf0b5a6ec269bafa77aba9a2](https://github.com/libgit2/libgit2/compare/fdd15bcfca6b2ec4b7ecad1aa11a396cb15bd064...182d0d1ee933de46bf0b5a6ec269bafa77aba9a2). + +## 0.13.0+1.4.1 - 2022-02-24 +[0.12.26...0.13.0](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.12.26+1.3.0...libgit2-sys-0.13.0+1.4.1) + +### Changed +- Changed libgit2-sys to use the presence of the `src` directory instead of `.git` to determine if it has a git submodule that needs updating. + [#801](https://github.com/rust-lang/git2-rs/pull/801) +- Updated the bundled libgit2 to [1.4.1](https://github.com/libgit2/libgit2/releases/tag/v1.4.1) (see also [1.4.0](https://github.com/libgit2/libgit2/releases/tag/v1.4.0)) + [#806](https://github.com/rust-lang/git2-rs/pull/806) + [#811](https://github.com/rust-lang/git2-rs/pull/811) + - Changes: [b7bad55e4bb0a285b073ba5e02b01d3f522fc95d...fdd15bcfca6b2ec4b7ecad1aa11a396cb15bd064](https://github.com/libgit2/libgit2/compare/b7bad55e4bb0a285b073ba5e02b01d3f522fc95d...fdd15bcfca6b2ec4b7ecad1aa11a396cb15bd064) + - The supported libgit2 system library range is 1.4.0 or greater. diff --git a/libgit2-sys/Cargo.toml b/libgit2-sys/Cargo.toml index 5c6ebf62d1..b2df3ca828 100644 --- a/libgit2-sys/Cargo.toml +++ b/libgit2-sys/Cargo.toml @@ -1,60 +1,40 @@ [package] - name = "libgit2-sys" -version = "0.4.3" -authors = ["Alex Crichton "] +version = "0.18.2+1.9.1" +authors = ["Josh Triplett ", "Alex Crichton "] links = "git2" build = "build.rs" -repository = "https://github.com/alexcrichton/git2-rs" -license = "MIT/Apache-2.0" +repository = "https://github.com/rust-lang/git2-rs" +license = "MIT OR Apache-2.0" description = "Native bindings to the libgit2 library" +exclude = [ + "libgit2/ci/*", + "libgit2/docs/*", + "libgit2/examples/*", + "libgit2/fuzzers/*", + "libgit2/tests/*", +] +edition = "2021" [lib] name = "libgit2_sys" path = "lib.rs" [dependencies] -libssh2-sys = { version = ">= 0", optional = true } libc = "0.2" -libz-sys = ">= 0" +libssh2-sys = { version = "0.3.0", optional = true } +libz-sys = { version = "1.1.0", default-features = false, features = ["libc"] } [build-dependencies] -pkg-config = "0.3" -cmake = "0.1.2" -gcc = "0.3" - -[target."cfg(all(unix, not(target_os = \"macos\")))".dependencies] -openssl-sys = "0.7.0" +pkg-config = "0.3.15" +cc = { version = "1.0.43", features = ['parallel'] } -[target.i686-unknown-linux-gnu.dependencies] -openssl-sys = "0.7.0" -[target.i586-unknown-linux-gnu.dependencies] -openssl-sys = "0.7.0" -[target.x86_64-unknown-linux-gnu.dependencies] -openssl-sys = "0.7.0" -[target.x86_64-unknown-linux-musl.dependencies] -openssl-sys = "0.7.0" -[target.aarch64-unknown-linux-gnu.dependencies] -openssl-sys = "0.7.0" -[target.powerpc64-unknown-linux-gnu.dependencies] -openssl-sys = "0.7.0" -[target.powerpc64le-unknown-linux-gnu.dependencies] -openssl-sys = "0.7.0" -[target.arm-unknown-linux-gnueabihf.dependencies] -openssl-sys = "0.7.0" -[target.armv7-unknown-linux-gnueabihf.dependencies] -openssl-sys = "0.7.0" -[target.i686-unknown-freebsd.dependencies] -openssl-sys = "0.7.0" -[target.x86_64-unknown-freebsd.dependencies] -openssl-sys = "0.7.0" -[target.x86_64-unknown-bitrig.dependencies] -openssl-sys = "0.7.0" -[target.x86_64-unknown-openbsd.dependencies] -openssl-sys = "0.7.0" -[target.x86_64-unknown-dragonfly.dependencies] -openssl-sys = "0.7.0" +[target.'cfg(unix)'.dependencies] +openssl-sys = { version = "0.9.45", optional = true } [features] ssh = ["libssh2-sys"] -https = [] +https = ["openssl-sys"] +vendored = [] +vendored-openssl = ["openssl-sys/vendored"] +zlib-ng-compat = ["libz-sys/zlib-ng", "libssh2-sys?/zlib-ng-compat"] diff --git a/libgit2-sys/LICENSE-APACHE b/libgit2-sys/LICENSE-APACHE new file mode 120000 index 0000000000..965b606f33 --- /dev/null +++ b/libgit2-sys/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/libgit2-sys/LICENSE-MIT b/libgit2-sys/LICENSE-MIT new file mode 120000 index 0000000000..76219eb72e --- /dev/null +++ b/libgit2-sys/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/libgit2-sys/build.rs b/libgit2-sys/build.rs index b4d58b3210..7b5a374e9b 100644 --- a/libgit2-sys/build.rs +++ b/libgit2-sys/build.rs @@ -1,175 +1,302 @@ -extern crate cmake; -extern crate gcc; -extern crate pkg_config; - use std::env; -use std::ffi::OsString; -use std::fs::{self, File}; -use std::io::prelude::*; +use std::fs; +use std::io; use std::path::{Path, PathBuf}; use std::process::Command; -macro_rules! t { - ($e:expr) => (match $e{ - Ok(e) => e, - Err(e) => panic!("{} failed with {}", stringify!($e), e), - }) +/// Tries to use system libgit2 and emits necessary build script instructions. +fn try_system_libgit2() -> Result { + let mut cfg = pkg_config::Config::new(); + match cfg.range_version("1.9.0".."1.10.0").probe("libgit2") { + Ok(lib) => { + for include in &lib.include_paths { + println!("cargo:root={}", include.display()); + } + Ok(lib) + } + Err(e) => { + println!("cargo:warning=failed to probe system libgit2: {e}"); + Err(e) + } + } } fn main() { + println!( + "cargo:rustc-check-cfg=cfg(\ + libgit2_vendored,\ + )" + ); + let https = env::var("CARGO_FEATURE_HTTPS").is_ok(); let ssh = env::var("CARGO_FEATURE_SSH").is_ok(); - if ssh { - register_dep("SSH2"); - } - if https { - register_dep("OPENSSL"); - } - let has_pkgconfig = Command::new("pkg-config").output().is_ok(); + let vendored = env::var("CARGO_FEATURE_VENDORED").is_ok(); + let zlib_ng_compat = env::var("CARGO_FEATURE_ZLIB_NG_COMPAT").is_ok(); + + // Specify `LIBGIT2_NO_VENDOR` to force to use system libgit2. + // Due to the additive nature of Cargo features, if some crate in the + // dependency graph activates `vendored` feature, there is no way to revert + // it back. This env var serves as a workaround for this purpose. + println!("cargo:rerun-if-env-changed=LIBGIT2_NO_VENDOR"); + let forced_no_vendor = env::var_os("LIBGIT2_NO_VENDOR").map_or(false, |s| s != "0"); - if env::var("LIBGIT2_SYS_USE_PKG_CONFIG").is_ok() { - if pkg_config::find_library("libgit2").is_ok() { - return + if forced_no_vendor { + if try_system_libgit2().is_err() { + panic!( + "\ +The environment variable `LIBGIT2_NO_VENDOR` has been set but no compatible system libgit2 could be found. +The build is now aborting. To disable, unset the variable or use `LIBGIT2_NO_VENDOR=0`. +", + ); } + + // We've reached here, implying we're using system libgit2. + return; } - if !Path::new("libgit2/.git").exists() { - let _ = Command::new("git").args(&["submodule", "update", "--init"]) - .status(); + // To use zlib-ng in zlib-compat mode, we have to build libgit2 ourselves. + let try_to_use_system_libgit2 = !vendored && !zlib_ng_compat; + if try_to_use_system_libgit2 && try_system_libgit2().is_ok() { + // using system libgit2 has worked + return; + } + + println!("cargo:rustc-cfg=libgit2_vendored"); + + if !Path::new("libgit2/src").exists() { + let _ = Command::new("git") + .args(&["submodule", "update", "--init", "libgit2"]) + .status(); } let target = env::var("TARGET").unwrap(); - let host = env::var("HOST").unwrap(); let windows = target.contains("windows"); - let msvc = target.contains("msvc"); - let mut cfg = cmake::Config::new("libgit2"); - - if msvc { - // libgit2 passes the /GL flag to enable whole program optimization, but - // this requires that the /LTCG flag is passed to the linker later on, - // and currently the compiler does not do that, so we disable whole - // program optimization entirely. - cfg.cflag("/GL-"); - - // Currently liblibc links to msvcrt which apparently is a dynamic CRT, - // so we need to turn this off to get it to link right. - cfg.define("STATIC_CRT", "OFF"); - } - - // libgit2 uses pkg-config to discover libssh2, but this doesn't work on - // windows as libssh2 doesn't come with a libssh2.pc file in that install - // (or when pkg-config isn't found). As a result we just manually turn on - // SSH support in libgit2 (a little jankily) here... - if ssh && (windows || !has_pkgconfig) { - if let Ok(libssh2_include) = env::var("DEP_SSH2_INCLUDE") { - if msvc { - cfg.cflag(format!("/I{}", libssh2_include)) - .cflag("/DGIT_SSH"); - } else { - cfg.cflag(format!("-I{}", libssh2_include)) - .cflag("-DGIT_SSH"); - } + let dst = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + let include = dst.join("include"); + let mut cfg = cc::Build::new(); + fs::create_dir_all(&include).unwrap(); + + // Copy over all header files + cp_r("libgit2/include", &include); + + cfg.include(&include) + .include("libgit2/src/libgit2") + .include("libgit2/src/util") + .out_dir(dst.join("build")) + .warnings(false); + + // Include all cross-platform C files + add_c_files(&mut cfg, "libgit2/src/libgit2"); + add_c_files(&mut cfg, "libgit2/src/util"); + + // These are activated by features, but they're all unconditionally always + // compiled apparently and have internal #define's to make sure they're + // compiled correctly. + add_c_files(&mut cfg, "libgit2/src/libgit2/transports"); + add_c_files(&mut cfg, "libgit2/src/libgit2/streams"); + + // Always use bundled HTTP parser (llhttp) for now + cfg.include("libgit2/deps/llhttp"); + add_c_files(&mut cfg, "libgit2/deps/llhttp"); + + // external/system xdiff is not yet supported + cfg.include("libgit2/deps/xdiff"); + add_c_files(&mut cfg, "libgit2/deps/xdiff"); + + // Use the included PCRE regex backend. + // + // Ideally these defines would be specific to the pcre files (or placed in + // a config.h), but since libgit2 already has a config.h used for other + // reasons, just define on the command-line for everything. Perhaps there + // is some way with cc to have different instructions per-file? + cfg.define("GIT_REGEX_BUILTIN", "1") + .include("libgit2/deps/pcre") + .define("HAVE_STDINT_H", Some("1")) + .define("HAVE_MEMMOVE", Some("1")) + .define("NO_RECURSE", Some("1")) + .define("NEWLINE", Some("10")) + .define("POSIX_MALLOC_THRESHOLD", Some("10")) + .define("LINK_SIZE", Some("2")) + .define("PARENS_NEST_LIMIT", Some("250")) + .define("MATCH_LIMIT", Some("10000000")) + .define("MATCH_LIMIT_RECURSION", Some("MATCH_LIMIT")) + .define("MAX_NAME_SIZE", Some("32")) + .define("MAX_NAME_COUNT", Some("10000")); + // "no symbols" warning on pcre_string_utils.c is because it is only used + // when when COMPILE_PCRE8 is not defined, which is the default. + add_c_files(&mut cfg, "libgit2/deps/pcre"); + + cfg.file("libgit2/src/util/allocators/failalloc.c"); + cfg.file("libgit2/src/util/allocators/stdalloc.c"); + + if windows { + add_c_files(&mut cfg, "libgit2/src/util/win32"); + cfg.define("STRSAFE_NO_DEPRECATE", None); + cfg.define("WIN32", None); + cfg.define("_WIN32_WINNT", Some("0x0600")); + + // libgit2's build system claims that forks like mingw-w64 of MinGW + // still want this define to use C99 stdio functions automatically. + // Apparently libgit2 breaks at runtime if this isn't here? Who knows! + if target.contains("gnu") { + cfg.define("__USE_MINGW_ANSI_STDIO", "1"); } + } else { + add_c_files(&mut cfg, "libgit2/src/util/unix"); + cfg.flag("-fvisibility=hidden"); + } + if target.contains("solaris") || target.contains("illumos") { + cfg.define("_POSIX_C_SOURCE", "200112L"); + cfg.define("__EXTENSIONS__", None); } - // When cross-compiling, we're pretty unlikely to find a `dlltool` binary - // lying around, so try to find another if it exists - if windows && !host.contains("windows") { - let c_compiler = gcc::Config::new().cargo_metadata(false) - .get_compiler(); - let exe = c_compiler.path(); - let path = env::var_os("PATH").unwrap_or(OsString::new()); - let exe = env::split_paths(&path) - .map(|p| p.join(&exe)) - .find(|p| p.exists()); - if let Some(exe) = exe { - if let Some(name) = exe.file_name().and_then(|e| e.to_str()) { - let name = name.replace("gcc", "dlltool"); - let dlltool = exe.with_file_name(name); - cfg.define("DLLTOOL", &dlltool); - } - } + let mut features = String::new(); + + features.push_str("#ifndef INCLUDE_features_h\n"); + features.push_str("#define INCLUDE_features_h\n"); + features.push_str("#define GIT_THREADS 1\n"); + features.push_str("#define GIT_TRACE 1\n"); + features.push_str("#define GIT_HTTPPARSER_BUILTIN 1\n"); + + if !target.contains("android") { + features.push_str("#define GIT_USE_NSEC 1\n"); } - if ssh { - cfg.register_dep("SSH2"); + if windows { + features.push_str("#define GIT_IO_WSAPOLL 1\n"); } else { - cfg.define("USE_SSH", "OFF"); + // Should we fallback to `select` as more systems have that? + features.push_str("#define GIT_IO_POLL 1\n"); + features.push_str("#define GIT_IO_SELECT 1\n"); } - if https { - cfg.register_dep("OPENSSL"); + + if target.contains("apple") { + features.push_str("#define GIT_USE_STAT_MTIMESPEC 1\n"); } else { - cfg.define("USE_OPENSSL", "OFF"); - } - - let _ = fs::remove_dir_all(env::var("OUT_DIR").unwrap()); - t!(fs::create_dir_all(env::var("OUT_DIR").unwrap())); - - let dst = cfg.define("BUILD_SHARED_LIBS", "OFF") - .define("BUILD_CLAR", "OFF") - .define("CURL", "OFF") - .register_dep("Z") - .build(); - - // Make sure libssh2 was detected on unix systems, because it definitely - // should have been! - if ssh && !msvc { - let flags = dst.join("build/CMakeFiles/git2.dir/flags.make"); - let mut contents = String::new(); - t!(t!(File::open(flags)).read_to_string(&mut contents)); - if !contents.contains("-DGIT_SSH") { - panic!("libgit2 failed to find libssh2, and SSH support is required"); + features.push_str("#define GIT_USE_STAT_MTIM 1\n"); + } + + if env::var("CARGO_CFG_TARGET_POINTER_WIDTH").unwrap() == "32" { + features.push_str("#define GIT_ARCH_32 1\n"); + } else { + features.push_str("#define GIT_ARCH_64 1\n"); + } + + if ssh { + if let Some(path) = env::var_os("DEP_SSH2_INCLUDE") { + cfg.include(path); + } + features.push_str("#define GIT_SSH 1\n"); + features.push_str("#define GIT_SSH_LIBSSH2 1\n"); + features.push_str("#define GIT_SSH_LIBSSH2_MEMORY_CREDENTIALS 1\n"); + } + if https { + features.push_str("#define GIT_HTTPS 1\n"); + + if windows { + features.push_str("#define GIT_WINHTTP 1\n"); + } else if target.contains("apple") { + features.push_str("#define GIT_SECURE_TRANSPORT 1\n"); + } else { + features.push_str("#define GIT_OPENSSL 1\n"); + if let Some(path) = env::var_os("DEP_OPENSSL_INCLUDE") { + cfg.include(path); + } } } + // Use the CollisionDetection SHA1 implementation. + features.push_str("#define GIT_SHA1_COLLISIONDETECT 1\n"); + cfg.define("SHA1DC_NO_STANDARD_INCLUDES", "1"); + cfg.define("SHA1DC_CUSTOM_INCLUDE_SHA1_C", "\"common.h\""); + cfg.define("SHA1DC_CUSTOM_INCLUDE_UBC_CHECK_C", "\"common.h\""); + cfg.file("libgit2/src/util/hash/collisiondetect.c"); + cfg.file("libgit2/src/util/hash/sha1dc/sha1.c"); + cfg.file("libgit2/src/util/hash/sha1dc/ubc_check.c"); + + if https { + if windows { + features.push_str("#define GIT_SHA256_WIN32 1\n"); + cfg.file("libgit2/src/util/hash/win32.c"); + } else if target.contains("apple") { + features.push_str("#define GIT_SHA256_COMMON_CRYPTO 1\n"); + cfg.file("libgit2/src/util/hash/common_crypto.c"); + } else { + features.push_str("#define GIT_SHA256_OPENSSL 1\n"); + cfg.file("libgit2/src/util/hash/openssl.c"); + } + } else { + features.push_str("#define GIT_SHA256_BUILTIN 1\n"); + cfg.file("libgit2/src/util/hash/builtin.c"); + cfg.file("libgit2/src/util/hash/rfc6234/sha224-256.c"); + } + + if let Some(path) = env::var_os("DEP_Z_INCLUDE") { + cfg.include(path); + } + + if target.contains("apple") { + features.push_str("#define GIT_USE_ICONV 1\n"); + } + + features.push_str("#endif\n"); + fs::write(include.join("git2_features.h"), features).unwrap(); + + cfg.compile("git2"); + + println!("cargo:root={}", dst.display()); + if target.contains("windows") { println!("cargo:rustc-link-lib=winhttp"); println!("cargo:rustc-link-lib=rpcrt4"); println!("cargo:rustc-link-lib=ole32"); println!("cargo:rustc-link-lib=crypt32"); - println!("cargo:rustc-link-lib=static=git2"); - println!("cargo:rustc-link-search=native={}/lib", dst.display()); - return + println!("cargo:rustc-link-lib=secur32"); + println!("cargo:rustc-link-lib=advapi32"); } - // libgit2 requires the http_parser library for the HTTP transport to be - // implemented, and it will attempt to use the system http_parser if it's - // available. Detect this situation and report using the system http parser - // the same way in this situation. - // - // Note that other dependencies of libgit2 like openssl, libz, and libssh2 - // are tracked via crates instead of this. Ideally this should be a crate as - // well. - let pkgconfig_file = dst.join("lib/pkgconfig/libgit2.pc"); - if let Ok(mut f) = File::open(&pkgconfig_file) { - let mut contents = String::new(); - t!(f.read_to_string(&mut contents)); - if contents.contains("-lhttp_parser") { - println!("cargo:rustc-link-lib=http_parser"); - } - } - - println!("cargo:rustc-link-lib=static=git2"); - println!("cargo:rustc-link-search=native={}", dst.join("lib").display()); if target.contains("apple") { println!("cargo:rustc-link-lib=iconv"); println!("cargo:rustc-link-lib=framework=Security"); println!("cargo:rustc-link-lib=framework=CoreFoundation"); } + + println!("cargo:rerun-if-changed=libgit2/include"); + println!("cargo:rerun-if-changed=libgit2/src"); + println!("cargo:rerun-if-changed=libgit2/deps"); } -fn register_dep(dep: &str) { - match env::var(&format!("DEP_{}_ROOT", dep)) { - Ok(s) => { - prepend("PKG_CONFIG_PATH", Path::new(&s).join("lib/pkgconfig")); +fn cp_r(from: impl AsRef, to: impl AsRef) { + for e in from.as_ref().read_dir().unwrap() { + let e = e.unwrap(); + let from = e.path(); + let to = to.as_ref().join(e.file_name()); + if e.file_type().unwrap().is_dir() { + fs::create_dir_all(&to).unwrap(); + cp_r(&from, &to); + } else { + println!("{} => {}", from.display(), to.display()); + fs::copy(&from, &to).unwrap(); } - Err(..) => {} } } -fn prepend(var: &str, val: PathBuf) { - let prefix = env::var(var).unwrap_or(String::new()); - let mut v = vec![val]; - v.extend(env::split_paths(&prefix)); - env::set_var(var, &env::join_paths(v).unwrap()); +fn add_c_files(build: &mut cc::Build, path: impl AsRef) { + let path = path.as_ref(); + if !path.exists() { + panic!("Path {} does not exist", path.display()); + } + // sort the C files to ensure a deterministic build for reproducible builds + let dir = path.read_dir().unwrap(); + let mut paths = dir.collect::>>().unwrap(); + paths.sort_by_key(|e| e.path()); + + for e in paths { + let path = e.path(); + if e.file_type().unwrap().is_dir() { + // skip dirs for now + } else if path.extension().and_then(|s| s.to_str()) == Some("c") { + build.file(&path); + } + } } diff --git a/libgit2-sys/lib.rs b/libgit2-sys/lib.rs index a46c3af9ac..7e1ac1e998 100644 --- a/libgit2-sys/lib.rs +++ b/libgit2-sys/lib.rs @@ -1,23 +1,31 @@ -#![doc(html_root_url = "http://alexcrichton.com/git2-rs")] -#![allow(non_camel_case_types)] +#![doc(html_root_url = "https://docs.rs/libgit2-sys/0.18")] +#![allow(non_camel_case_types, unused_extern_crates)] -extern crate libc; -#[cfg(feature = "ssh")] -extern crate libssh2_sys as libssh2; -#[cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), feature = "https"))] -extern crate openssl_sys as openssl; +// This is required to link libz when libssh2-sys is not included. extern crate libz_sys as libz; -use libc::{c_int, c_char, c_uint, size_t, c_uchar, c_void}; +use libc::{c_char, c_int, c_uchar, c_uint, c_ushort, c_void, size_t}; +#[cfg(feature = "ssh")] +use libssh2_sys as libssh2; +use std::ffi::CStr; pub const GIT_OID_RAWSZ: usize = 20; pub const GIT_OID_HEXSZ: usize = GIT_OID_RAWSZ * 2; pub const GIT_CLONE_OPTIONS_VERSION: c_uint = 1; +pub const GIT_STASH_APPLY_OPTIONS_VERSION: c_uint = 1; pub const GIT_CHECKOUT_OPTIONS_VERSION: c_uint = 1; pub const GIT_MERGE_OPTIONS_VERSION: c_uint = 1; pub const GIT_REMOTE_CALLBACKS_VERSION: c_uint = 1; pub const GIT_STATUS_OPTIONS_VERSION: c_uint = 1; pub const GIT_BLAME_OPTIONS_VERSION: c_uint = 1; +pub const GIT_PROXY_OPTIONS_VERSION: c_uint = 1; +pub const GIT_SUBMODULE_UPDATE_OPTIONS_VERSION: c_uint = 1; +pub const GIT_ODB_BACKEND_VERSION: c_uint = 1; +pub const GIT_REFDB_BACKEND_VERSION: c_uint = 1; +pub const GIT_CHERRYPICK_OPTIONS_VERSION: c_uint = 1; +pub const GIT_APPLY_OPTIONS_VERSION: c_uint = 1; +pub const GIT_REVERT_OPTIONS_VERSION: c_uint = 1; +pub const GIT_INDEXER_OPTIONS_VERSION: c_uint = 1; macro_rules! git_enum { (pub enum $name:ident { $($variants:tt)* }) => { @@ -49,10 +57,12 @@ pub enum git_commit {} pub enum git_config {} pub enum git_config_iterator {} pub enum git_index {} +pub enum git_index_conflict_iterator {} pub enum git_object {} pub enum git_reference {} pub enum git_reference_iterator {} pub enum git_annotated_commit {} +pub enum git_refdb {} pub enum git_refspec {} pub enum git_remote {} pub enum git_repository {} @@ -70,10 +80,19 @@ pub enum git_pathspec {} pub enum git_pathspec_match_list {} pub enum git_diff {} pub enum git_diff_stats {} +pub enum git_patch {} +pub enum git_rebase {} pub enum git_reflog {} pub enum git_reflog_entry {} pub enum git_describe_result {} pub enum git_packbuilder {} +pub enum git_odb {} +pub enum git_odb_stream {} +pub enum git_odb_object {} +pub enum git_worktree {} +pub enum git_transaction {} +pub enum git_mailmap {} +pub enum git_indexer {} #[repr(C)] pub struct git_revspec { @@ -101,7 +120,9 @@ pub struct git_strarray { pub count: size_t, } impl Clone for git_strarray { - fn clone(&self) -> git_strarray { *self } + fn clone(&self) -> git_strarray { + *self + } } #[repr(C)] @@ -111,7 +132,9 @@ pub struct git_oidarray { pub count: size_t, } impl Clone for git_oidarray { - fn clone(&self) -> git_oidarray { *self } + fn clone(&self) -> git_oidarray { + *self + } } #[repr(C)] @@ -122,14 +145,16 @@ pub struct git_signature { } #[repr(C)] -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct git_time { pub time: git_time_t, pub offset: c_int, + pub sign: c_char, } pub type git_off_t = i64; pub type git_time_t = i64; +pub type git_object_size_t = u64; git_enum! { pub enum git_revparse_mode_t { @@ -168,42 +193,55 @@ git_enum! { GIT_EMERGECONFLICT = -24, GIT_PASSTHROUGH = -30, GIT_ITEROVER = -31, + GIT_RETRY = -32, + GIT_EMISMATCH = -33, + GIT_EINDEXDIRTY = -34, + GIT_EAPPLYFAIL = -35, + GIT_EOWNER = -36, + GIT_TIMEOUT = -37, + GIT_EUNCHANGED = -38, + GIT_ENOTSUPPORTED = -39, + GIT_EREADONLY = -40, } } git_enum! { pub enum git_error_t { - GITERR_NONE = 0, - GITERR_NOMEMORY, - GITERR_OS, - GITERR_INVALID, - GITERR_REFERENCE, - GITERR_ZLIB, - GITERR_REPOSITORY, - GITERR_CONFIG, - GITERR_REGEX, - GITERR_ODB, - GITERR_INDEX, - GITERR_OBJECT, - GITERR_NET, - GITERR_TAG, - GITERR_TREE, - GITERR_INDEXER, - GITERR_SSL, - GITERR_SUBMODULE, - GITERR_THREAD, - GITERR_STASH, - GITERR_CHECKOUT, - GITERR_FETCHHEAD, - GITERR_MERGE, - GITERR_SSH, - GITERR_FILTER, - GITERR_REVERT, - GITERR_CALLBACK, - GITERR_CHERRYPICK, - GITERR_DESCRIBE, - GITERR_REBASE, - GITERR_FILESYSTEM, + GIT_ERROR_NONE = 0, + GIT_ERROR_NOMEMORY, + GIT_ERROR_OS, + GIT_ERROR_INVALID, + GIT_ERROR_REFERENCE, + GIT_ERROR_ZLIB, + GIT_ERROR_REPOSITORY, + GIT_ERROR_CONFIG, + GIT_ERROR_REGEX, + GIT_ERROR_ODB, + GIT_ERROR_INDEX, + GIT_ERROR_OBJECT, + GIT_ERROR_NET, + GIT_ERROR_TAG, + GIT_ERROR_TREE, + GIT_ERROR_INDEXER, + GIT_ERROR_SSL, + GIT_ERROR_SUBMODULE, + GIT_ERROR_THREAD, + GIT_ERROR_STASH, + GIT_ERROR_CHECKOUT, + GIT_ERROR_FETCHHEAD, + GIT_ERROR_MERGE, + GIT_ERROR_SSH, + GIT_ERROR_FILTER, + GIT_ERROR_REVERT, + GIT_ERROR_CALLBACK, + GIT_ERROR_CHERRYPICK, + GIT_ERROR_DESCRIBE, + GIT_ERROR_REBASE, + GIT_ERROR_FILESYSTEM, + GIT_ERROR_PATCH, + GIT_ERROR_WORKTREE, + GIT_ERROR_SHA1, + GIT_ERROR_HTTP, } } @@ -239,9 +277,9 @@ pub struct git_clone_options { pub bare: c_int, pub local: git_clone_local_t, pub checkout_branch: *const c_char, - pub repository_cb: Option, + pub repository_cb: git_repository_create_cb, pub repository_cb_payload: *mut c_void, - pub remote_cb: Option, + pub remote_cb: git_remote_create_cb, pub remote_cb_payload: *mut c_void, } @@ -263,9 +301,9 @@ pub struct git_checkout_options { pub file_mode: c_uint, pub file_open_flags: c_int, pub notify_flags: c_uint, - pub notify_cb: Option, + pub notify_cb: git_checkout_notify_cb, pub notify_payload: *mut c_void, - pub progress_cb: Option, + pub progress_cb: git_checkout_progress_cb, pub progress_payload: *mut c_void, pub paths: git_strarray, pub baseline: *mut git_tree, @@ -274,23 +312,25 @@ pub struct git_checkout_options { pub ancestor_label: *const c_char, pub our_label: *const c_char, pub their_label: *const c_char, - pub perfdata_cb: Option, + pub perfdata_cb: git_checkout_perfdata_cb, pub perfdata_payload: *mut c_void, } -pub type git_checkout_notify_cb = extern fn(git_checkout_notify_t, - *const c_char, - *const git_diff_file, - *const git_diff_file, - *const git_diff_file, - *mut c_void) -> c_int; -pub type git_checkout_progress_cb = extern fn(*const c_char, - size_t, - size_t, - *mut c_void); - -pub type git_checkout_perfdata_cb = extern fn(*const git_checkout_perfdata, - *mut c_void); +pub type git_checkout_notify_cb = Option< + extern "C" fn( + git_checkout_notify_t, + *const c_char, + *const git_diff_file, + *const git_diff_file, + *const git_diff_file, + *mut c_void, + ) -> c_int, +>; +pub type git_checkout_progress_cb = + Option; + +pub type git_checkout_perfdata_cb = + Option; #[repr(C)] pub struct git_checkout_perfdata { @@ -299,27 +339,71 @@ pub struct git_checkout_perfdata { pub chmod_calls: size_t, } +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct git_indexer_progress { + pub total_objects: c_uint, + pub indexed_objects: c_uint, + pub received_objects: c_uint, + pub local_objects: c_uint, + pub total_deltas: c_uint, + pub indexed_deltas: c_uint, + pub received_bytes: size_t, +} + +pub type git_indexer_progress_cb = + Option c_int>; + +#[deprecated( + since = "0.10.0", + note = "renamed to `git_indexer_progress` to match upstream" +)] +pub type git_transfer_progress = git_indexer_progress; + +#[repr(C)] +pub struct git_indexer_options { + pub version: c_uint, + pub progress_cb: git_indexer_progress_cb, + pub progress_cb_payload: *mut c_void, + pub verify: c_uchar, +} + +pub type git_remote_ready_cb = Option c_int>; + +git_enum! { + pub enum git_remote_update_flags { + GIT_REMOTE_UPDATE_FETCHHEAD = 1 << 0, + GIT_REMOTE_UPDATE_REPORT_UNCHANGED = 1 << 1, + } +} + #[repr(C)] pub struct git_remote_callbacks { pub version: c_uint, - pub sideband_progress: Option, - pub completion: Option c_int>, - pub credentials: Option, - pub certificate_check: Option, - pub transfer_progress: Option, - pub update_tips: Option c_int>, - pub pack_progress: Option, - pub push_transfer_progress: Option, - pub push_update_reference: Option c_int>, - pub push_negotiation: Option, - pub transport: Option, + pub sideband_progress: git_transport_message_cb, + pub completion: Option c_int>, + pub credentials: git_cred_acquire_cb, + pub certificate_check: git_transport_certificate_check_cb, + pub transfer_progress: git_indexer_progress_cb, + pub update_tips: + Option c_int>, + pub pack_progress: git_packbuilder_progress, + pub push_transfer_progress: git_push_transfer_progress, + pub push_update_reference: git_push_update_reference_cb, + pub push_negotiation: git_push_negotiation, + pub transport: git_transport_cb, + pub remote_ready: git_remote_ready_cb, pub payload: *mut c_void, + pub resolve_url: git_url_resolve_cb, + pub update_refs: Option< + extern "C" fn( + *const c_char, + *const git_oid, + *const git_oid, + *mut git_refspec, + *mut c_void, + ) -> c_int, + >, } #[repr(C)] @@ -327,11 +411,23 @@ pub struct git_fetch_options { pub version: c_int, pub callbacks: git_remote_callbacks, pub prune: git_fetch_prune_t, - pub update_fetchhead: c_int, + pub update_fetchhead: c_uint, pub download_tags: git_remote_autotag_option_t, + pub proxy_opts: git_proxy_options, + pub depth: c_int, + pub follow_redirects: git_remote_redirect_t, pub custom_headers: git_strarray, } +#[repr(C)] +pub struct git_fetch_negotiation { + refs: *const *const git_remote_head, + refs_len: size_t, + shallow_roots: *mut git_oid, + shallow_roots_len: size_t, + depth: c_int, +} + git_enum! { pub enum git_remote_autotag_option_t { GIT_REMOTE_DOWNLOAD_TAGS_UNSPECIFIED, @@ -357,24 +453,26 @@ git_enum! { } } -pub type git_transport_message_cb = extern fn(*const c_char, c_int, - *mut c_void) -> c_int; -pub type git_cred_acquire_cb = extern fn(*mut *mut git_cred, - *const c_char, *const c_char, - c_uint, *mut c_void) -> c_int; -pub type git_transfer_progress_cb = extern fn(*const git_transfer_progress, - *mut c_void) -> c_int; -pub type git_packbuilder_progress = extern fn(git_packbuilder_stage_t, c_uint, - c_uint, *mut c_void) -> c_int; -pub type git_push_transfer_progress = extern fn(c_uint, c_uint, size_t, - *mut c_void) -> c_int; -pub type git_transport_certificate_check_cb = extern fn(*mut git_cert, - c_int, - *const c_char, - *mut c_void) -> c_int; -pub type git_push_negotiation = extern fn(*mut *const git_push_update, - size_t, - *mut c_void) -> c_int; +pub type git_transport_message_cb = + Option c_int>; +pub type git_cred_acquire_cb = Option< + extern "C" fn(*mut *mut git_cred, *const c_char, *const c_char, c_uint, *mut c_void) -> c_int, +>; +pub type git_transfer_progress_cb = + Option c_int>; +pub type git_packbuilder_progress = + Option c_int>; +pub type git_push_transfer_progress = + Option c_int>; +pub type git_transport_certificate_check_cb = + Option c_int>; +pub type git_push_negotiation = + Option c_int>; + +pub type git_push_update_reference_cb = + Option c_int>; +pub type git_url_resolve_cb = + Option c_int>; #[repr(C)] pub struct git_push_update { @@ -389,6 +487,7 @@ git_enum! { GIT_CERT_NONE, GIT_CERT_X509, GIT_CERT_HOSTKEY_LIBSSH2, + GIT_CERT_STRARRAY, } } @@ -403,6 +502,10 @@ pub struct git_cert_hostkey { pub kind: git_cert_ssh_t, pub hash_md5: [u8; 16], pub hash_sha1: [u8; 20], + pub hash_sha256: [u8; 32], + pub raw_type: git_cert_ssh_raw_type_t, + pub hostkey: *const c_char, + pub hostkey_len: size_t, } #[repr(C)] @@ -416,47 +519,62 @@ git_enum! { pub enum git_cert_ssh_t { GIT_CERT_SSH_MD5 = 1 << 0, GIT_CERT_SSH_SHA1 = 1 << 1, + GIT_CERT_SSH_SHA256 = 1 << 2, + GIT_CERT_SSH_RAW = 1 << 3, } } -#[repr(C)] -#[derive(Copy, Clone)] -pub struct git_transfer_progress { - pub total_objects: c_uint, - pub indexed_objects: c_uint, - pub received_objects: c_uint, - pub local_objects: c_uint, - pub total_deltas: c_uint, - pub indexed_deltas: c_uint, - pub received_bytes: size_t, +git_enum! { + pub enum git_cert_ssh_raw_type_t { + GIT_CERT_SSH_RAW_TYPE_UNKNOWN = 0, + GIT_CERT_SSH_RAW_TYPE_RSA = 1, + GIT_CERT_SSH_RAW_TYPE_DSS = 2, + GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_256 = 3, + GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_384 = 4, + GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_521 = 5, + GIT_CERT_SSH_RAW_TYPE_KEY_ED25519 = 6, + } +} + +git_enum! { + pub enum git_diff_flag_t { + GIT_DIFF_FLAG_BINARY = 1 << 0, + GIT_DIFF_FLAG_NOT_BINARY = 1 << 1, + GIT_DIFF_FLAG_VALID_ID = 1 << 2, + GIT_DIFF_FLAG_EXISTS = 1 << 3, + } } #[repr(C)] pub struct git_diff_file { pub id: git_oid, pub path: *const c_char, - pub size: git_off_t, + pub size: git_object_size_t, pub flags: u32, pub mode: u16, + pub id_abbrev: u16, } -pub type git_repository_create_cb = extern fn(*mut *mut git_repository, - *const c_char, - c_int, *mut c_void) -> c_int; -pub type git_remote_create_cb = extern fn(*mut *mut git_remote, - *mut git_repository, - *const c_char, - *const c_char, - *mut c_void) -> c_int; +pub type git_repository_create_cb = + Option c_int>; +pub type git_remote_create_cb = Option< + extern "C" fn( + *mut *mut git_remote, + *mut git_repository, + *const c_char, + *const c_char, + *mut c_void, + ) -> c_int, +>; git_enum! { pub enum git_checkout_notify_t { GIT_CHECKOUT_NOTIFY_NONE = 0, - GIT_CHECKOUT_NOTIFY_CONFLICT = (1 << 0), - GIT_CHECKOUT_NOTIFY_DIRTY = (1 << 1), - GIT_CHECKOUT_NOTIFY_UPDATED = (1 << 2), - GIT_CHECKOUT_NOTIFY_UNTRACKED = (1 << 3), - GIT_CHECKOUT_NOTIFY_IGNORED = (1 << 4), + GIT_CHECKOUT_NOTIFY_CONFLICT = 1 << 0, + GIT_CHECKOUT_NOTIFY_DIRTY = 1 << 1, + GIT_CHECKOUT_NOTIFY_UPDATED = 1 << 2, + GIT_CHECKOUT_NOTIFY_UNTRACKED = 1 << 3, + GIT_CHECKOUT_NOTIFY_IGNORED = 1 << 4, GIT_CHECKOUT_NOTIFY_ALL = 0x0FFFF, } @@ -466,43 +584,43 @@ git_enum! { pub enum git_status_t { GIT_STATUS_CURRENT = 0, - GIT_STATUS_INDEX_NEW = (1 << 0), - GIT_STATUS_INDEX_MODIFIED = (1 << 1), - GIT_STATUS_INDEX_DELETED = (1 << 2), - GIT_STATUS_INDEX_RENAMED = (1 << 3), - GIT_STATUS_INDEX_TYPECHANGE = (1 << 4), - - GIT_STATUS_WT_NEW = (1 << 7), - GIT_STATUS_WT_MODIFIED = (1 << 8), - GIT_STATUS_WT_DELETED = (1 << 9), - GIT_STATUS_WT_TYPECHANGE = (1 << 10), - GIT_STATUS_WT_RENAMED = (1 << 11), - GIT_STATUS_WT_UNREADABLE = (1 << 12), - - GIT_STATUS_IGNORED = (1 << 14), - GIT_STATUS_CONFLICTED = (1 << 15), + GIT_STATUS_INDEX_NEW = 1 << 0, + GIT_STATUS_INDEX_MODIFIED = 1 << 1, + GIT_STATUS_INDEX_DELETED = 1 << 2, + GIT_STATUS_INDEX_RENAMED = 1 << 3, + GIT_STATUS_INDEX_TYPECHANGE = 1 << 4, + + GIT_STATUS_WT_NEW = 1 << 7, + GIT_STATUS_WT_MODIFIED = 1 << 8, + GIT_STATUS_WT_DELETED = 1 << 9, + GIT_STATUS_WT_TYPECHANGE = 1 << 10, + GIT_STATUS_WT_RENAMED = 1 << 11, + GIT_STATUS_WT_UNREADABLE = 1 << 12, + + GIT_STATUS_IGNORED = 1 << 14, + GIT_STATUS_CONFLICTED = 1 << 15, } } git_enum! { pub enum git_status_opt_t { - GIT_STATUS_OPT_INCLUDE_UNTRACKED = (1 << 0), - GIT_STATUS_OPT_INCLUDE_IGNORED = (1 << 1), - GIT_STATUS_OPT_INCLUDE_UNMODIFIED = (1 << 2), - GIT_STATUS_OPT_EXCLUDE_SUBMODULES = (1 << 3), - GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS = (1 << 4), - GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH = (1 << 5), - GIT_STATUS_OPT_RECURSE_IGNORED_DIRS = (1 << 6), - GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX = (1 << 7), - GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR = (1 << 8), - GIT_STATUS_OPT_SORT_CASE_SENSITIVELY = (1 << 9), - GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY = (1 << 10), - - GIT_STATUS_OPT_RENAMES_FROM_REWRITES = (1 << 11), - GIT_STATUS_OPT_NO_REFRESH = (1 << 12), - GIT_STATUS_OPT_UPDATE_INDEX = (1 << 13), - GIT_STATUS_OPT_INCLUDE_UNREADABLE = (1 << 14), - GIT_STATUS_OPT_INCLUDE_UNREADABLE_AS_UNTRACKED = (1 << 15), + GIT_STATUS_OPT_INCLUDE_UNTRACKED = 1 << 0, + GIT_STATUS_OPT_INCLUDE_IGNORED = 1 << 1, + GIT_STATUS_OPT_INCLUDE_UNMODIFIED = 1 << 2, + GIT_STATUS_OPT_EXCLUDE_SUBMODULES = 1 << 3, + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS = 1 << 4, + GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH = 1 << 5, + GIT_STATUS_OPT_RECURSE_IGNORED_DIRS = 1 << 6, + GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX = 1 << 7, + GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR = 1 << 8, + GIT_STATUS_OPT_SORT_CASE_SENSITIVELY = 1 << 9, + GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY = 1 << 10, + + GIT_STATUS_OPT_RENAMES_FROM_REWRITES = 1 << 11, + GIT_STATUS_OPT_NO_REFRESH = 1 << 12, + GIT_STATUS_OPT_UPDATE_INDEX = 1 << 13, + GIT_STATUS_OPT_INCLUDE_UNREADABLE = 1 << 14, + GIT_STATUS_OPT_INCLUDE_UNREADABLE_AS_UNTRACKED = 1 << 15, } } @@ -536,6 +654,8 @@ pub struct git_status_options { pub show: git_status_show_t, pub flags: c_uint, pub pathspec: git_strarray, + pub baseline: *mut git_tree, + pub rename_threshold: u16, } #[repr(C)] @@ -552,32 +672,32 @@ pub struct git_diff_delta { pub struct git_status_entry { pub status: git_status_t, pub head_to_index: *mut git_diff_delta, - pub index_to_workdir: *mut git_diff_delta + pub index_to_workdir: *mut git_diff_delta, } git_enum! { pub enum git_checkout_strategy_t { - GIT_CHECKOUT_NONE = 0, - GIT_CHECKOUT_SAFE = (1 << 0), - GIT_CHECKOUT_FORCE = (1 << 1), - GIT_CHECKOUT_RECREATE_MISSING = (1 << 2), - GIT_CHECKOUT_ALLOW_CONFLICTS = (1 << 4), - GIT_CHECKOUT_REMOVE_UNTRACKED = (1 << 5), - GIT_CHECKOUT_REMOVE_IGNORED = (1 << 6), - GIT_CHECKOUT_UPDATE_ONLY = (1 << 7), - GIT_CHECKOUT_DONT_UPDATE_INDEX = (1 << 8), - GIT_CHECKOUT_NO_REFRESH = (1 << 9), - GIT_CHECKOUT_SKIP_UNMERGED = (1 << 10), - GIT_CHECKOUT_USE_OURS = (1 << 11), - GIT_CHECKOUT_USE_THEIRS = (1 << 12), - GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH = (1 << 13), - GIT_CHECKOUT_SKIP_LOCKED_DIRECTORIES = (1 << 18), - GIT_CHECKOUT_DONT_OVERWRITE_IGNORED = (1 << 19), - GIT_CHECKOUT_CONFLICT_STYLE_MERGE = (1 << 20), - GIT_CHECKOUT_CONFLICT_STYLE_DIFF3 = (1 << 21), - - GIT_CHECKOUT_UPDATE_SUBMODULES = (1 << 16), - GIT_CHECKOUT_UPDATE_SUBMODULES_IF_CHANGED = (1 << 17), + GIT_CHECKOUT_SAFE = 0, + GIT_CHECKOUT_FORCE = 1 << 1, + GIT_CHECKOUT_RECREATE_MISSING = 1 << 2, + GIT_CHECKOUT_ALLOW_CONFLICTS = 1 << 4, + GIT_CHECKOUT_REMOVE_UNTRACKED = 1 << 5, + GIT_CHECKOUT_REMOVE_IGNORED = 1 << 6, + GIT_CHECKOUT_UPDATE_ONLY = 1 << 7, + GIT_CHECKOUT_DONT_UPDATE_INDEX = 1 << 8, + GIT_CHECKOUT_NO_REFRESH = 1 << 9, + GIT_CHECKOUT_SKIP_UNMERGED = 1 << 10, + GIT_CHECKOUT_USE_OURS = 1 << 11, + GIT_CHECKOUT_USE_THEIRS = 1 << 12, + GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH = 1 << 13, + GIT_CHECKOUT_SKIP_LOCKED_DIRECTORIES = 1 << 18, + GIT_CHECKOUT_DONT_OVERWRITE_IGNORED = 1 << 19, + GIT_CHECKOUT_CONFLICT_STYLE_MERGE = 1 << 20, + GIT_CHECKOUT_CONFLICT_STYLE_DIFF3 = 1 << 21, + GIT_CHECKOUT_NONE = 1 << 30, + + GIT_CHECKOUT_UPDATE_SUBMODULES = 1 << 16, + GIT_CHECKOUT_UPDATE_SUBMODULES_IF_CHANGED = 1 << 17, } } @@ -590,26 +710,24 @@ git_enum! { } git_enum! { - pub enum git_otype: c_int { - GIT_OBJ_ANY = -2, - GIT_OBJ_BAD = -1, - GIT_OBJ__EXT1 = 0, - GIT_OBJ_COMMIT = 1, - GIT_OBJ_TREE = 2, - GIT_OBJ_BLOB = 3, - GIT_OBJ_TAG = 4, - GIT_OBJ__EXT2 = 5, - GIT_OBJ_OFS_DELTA = 6, - GIT_OBJ_REF_DELTA = 7, + pub enum git_object_t: c_int { + GIT_OBJECT_ANY = -2, + GIT_OBJECT_INVALID = -1, + GIT_OBJECT_COMMIT = 1, + GIT_OBJECT_TREE = 2, + GIT_OBJECT_BLOB = 3, + GIT_OBJECT_TAG = 4, + GIT_OBJECT_OFS_DELTA = 6, + GIT_OBJECT_REF_DELTA = 7, } } git_enum! { - pub enum git_ref_t { - GIT_REF_INVALID = 0, - GIT_REF_OID = 1, - GIT_REF_SYMBOLIC = 2, - GIT_REF_LISTALL = GIT_REF_OID | GIT_REF_SYMBOLIC, + pub enum git_reference_t { + GIT_REFERENCE_INVALID = 0, + GIT_REFERENCE_DIRECT = 1, + GIT_REFERENCE_SYMBOLIC = 2, + GIT_REFERENCE_ALL = GIT_REFERENCE_DIRECT | GIT_REFERENCE_SYMBOLIC, } } @@ -618,6 +736,7 @@ git_enum! { GIT_FILEMODE_UNREADABLE = 0o000000, GIT_FILEMODE_TREE = 0o040000, GIT_FILEMODE_BLOB = 0o100644, + GIT_FILEMODE_BLOB_GROUP_WRITABLE = 0o100664, GIT_FILEMODE_BLOB_EXECUTABLE = 0o100755, GIT_FILEMODE_LINK = 0o120000, GIT_FILEMODE_COMMIT = 0o160000, @@ -631,16 +750,33 @@ git_enum! { } } -pub type git_treewalk_cb = extern fn(*const c_char, *const git_tree_entry, - *mut c_void) -> c_int; -pub type git_treebuilder_filter_cb = extern fn(*const git_tree_entry, - *mut c_void) -> c_int; +pub type git_treewalk_cb = + extern "C" fn(*const c_char, *const git_tree_entry, *mut c_void) -> c_int; +pub type git_treebuilder_filter_cb = + Option c_int>; + +pub type git_revwalk_hide_cb = Option c_int>; + +git_enum! { + pub enum git_tree_update_t { + GIT_TREE_UPDATE_UPSERT = 0, + GIT_TREE_UPDATE_REMOVE = 1, + } +} + +#[repr(C)] +pub struct git_tree_update { + pub action: git_tree_update_t, + pub id: git_oid, + pub filemode: git_filemode_t, + pub path: *const c_char, +} #[repr(C)] #[derive(Copy, Clone)] pub struct git_buf { pub ptr: *mut c_char, - pub asize: size_t, + pub reserved: size_t, pub size: size_t, } @@ -653,11 +789,13 @@ git_enum! { } pub const GIT_BLAME_NORMAL: u32 = 0; -pub const GIT_BLAME_TRACK_COPIES_SAME_FILE: u32 = 1<<0; -pub const GIT_BLAME_TRACK_COPIES_SAME_COMMIT_MOVES: u32 = 1<<1; -pub const GIT_BLAME_TRACK_COPIES_SAME_COMMIT_COPIES: u32 = 1<<2; -pub const GIT_BLAME_TRACK_COPIES_ANY_COMMIT_COPIES: u32 = 1<<3; -pub const GIT_BLAME_FIRST_PARENT: u32 = 1<<4; +pub const GIT_BLAME_TRACK_COPIES_SAME_FILE: u32 = 1 << 0; +pub const GIT_BLAME_TRACK_COPIES_SAME_COMMIT_MOVES: u32 = 1 << 1; +pub const GIT_BLAME_TRACK_COPIES_SAME_COMMIT_COPIES: u32 = 1 << 2; +pub const GIT_BLAME_TRACK_COPIES_ANY_COMMIT_COPIES: u32 = 1 << 3; +pub const GIT_BLAME_FIRST_PARENT: u32 = 1 << 4; +pub const GIT_BLAME_USE_MAILMAP: u32 = 1 << 5; +pub const GIT_BLAME_IGNORE_WHITESPACE: u32 = 1 << 6; #[repr(C)] #[derive(Copy, Clone)] @@ -679,41 +817,32 @@ pub struct git_blame_hunk { pub final_commit_id: git_oid, pub final_start_line_number: usize, pub final_signature: *mut git_signature, + pub final_committer: *mut git_signature, pub orig_commit_id: git_oid, pub orig_path: *const c_char, pub orig_start_line_number: usize, pub orig_signature: *mut git_signature, + pub orig_committer: *mut git_signature, + pub summary: *const c_char, pub boundary: c_char, } -pub type git_index_matched_path_cb = extern fn(*const c_char, *const c_char, - *mut c_void) -> c_int; +pub type git_index_matched_path_cb = + Option c_int>; git_enum! { - pub enum git_idxentry_extended_flag_t { - GIT_IDXENTRY_INTENT_TO_ADD = 1 << 13, - GIT_IDXENTRY_SKIP_WORKTREE = 1 << 14, - GIT_IDXENTRY_EXTENDED2 = 1 << 15, + pub enum git_index_entry_extended_flag_t { + GIT_INDEX_ENTRY_INTENT_TO_ADD = 1 << 13, + GIT_INDEX_ENTRY_SKIP_WORKTREE = 1 << 14, - GIT_IDXENTRY_UPDATE = 1 << 0, - GIT_IDXENTRY_REMOVE = 1 << 1, - GIT_IDXENTRY_UPTODATE = 1 << 2, - GIT_IDXENTRY_ADDED = 1 << 3, - - GIT_IDXENTRY_HASHED = 1 << 4, - GIT_IDXENTRY_UNHASHED = 1 << 5, - GIT_IDXENTRY_WT_REMOVE = 1 << 6, - GIT_IDXENTRY_CONFLICTED = 1 << 7, - - GIT_IDXENTRY_UNPACKED = 1 << 8, - GIT_IDXENTRY_NEW_SKIP_WORKTREE = 1 << 9, + GIT_INDEX_ENTRY_UPTODATE = 1 << 2, } } git_enum! { - pub enum git_indxentry_flag_t { - GIT_IDXENTRY_EXTENDED = 0x4000, - GIT_IDXENTRY_VALID = 0x8000, + pub enum git_index_entry_flag_t { + GIT_INDEX_ENTRY_EXTENDED = 0x4000, + GIT_INDEX_ENTRY_VALID = 0x8000, } } @@ -734,12 +863,12 @@ pub struct git_index_entry { pub path: *const c_char, } -pub const GIT_IDXENTRY_NAMEMASK: u16 = 0xfff; -pub const GIT_IDXENTRY_STAGEMASK: u16 = 0x3000; -pub const GIT_IDXENTRY_STAGESHIFT: u16 = 12; +pub const GIT_INDEX_ENTRY_NAMEMASK: u16 = 0xfff; +pub const GIT_INDEX_ENTRY_STAGEMASK: u16 = 0x3000; +pub const GIT_INDEX_ENTRY_STAGESHIFT: u16 = 12; #[repr(C)] -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct git_index_time { pub seconds: i32, pub nanoseconds: u32, @@ -749,9 +878,10 @@ pub struct git_index_time { pub struct git_config_entry { pub name: *const c_char, pub value: *const c_char, + pub backend_type: *const c_char, + pub origin_path: *const c_char, + pub include_depth: c_uint, pub level: git_config_level_t, - pub free: extern fn(*mut git_config_entry), - pub payload: *mut c_void, } git_enum! { @@ -761,7 +891,8 @@ git_enum! { GIT_CONFIG_LEVEL_XDG = 3, GIT_CONFIG_LEVEL_GLOBAL = 4, GIT_CONFIG_LEVEL_LOCAL = 5, - GIT_CONFIG_LEVEL_APP = 6, + GIT_CONFIG_LEVEL_WORKTREE = 6, + GIT_CONFIG_LEVEL_APP = 7, GIT_CONFIG_HIGHEST_LEVEL = -1, } } @@ -787,14 +918,43 @@ git_enum! { } } -pub type git_submodule_cb = extern fn(*mut git_submodule, - *const c_char, - *mut c_void) -> c_int; +pub type git_submodule_cb = + Option c_int>; + +#[repr(C)] +pub struct git_submodule_update_options { + pub version: c_uint, + pub checkout_opts: git_checkout_options, + pub fetch_opts: git_fetch_options, + pub allow_fetch: c_int, +} + +#[repr(C)] +pub struct git_writestream { + pub write: Option c_int>, + pub close: Option c_int>, + pub free: Option, +} + +git_enum! { + pub enum git_attr_value_t { + GIT_ATTR_VALUE_UNSPECIFIED = 0, + GIT_ATTR_VALUE_TRUE, + GIT_ATTR_VALUE_FALSE, + GIT_ATTR_VALUE_STRING, + } +} + +pub const GIT_ATTR_CHECK_FILE_THEN_INDEX: u32 = 0; +pub const GIT_ATTR_CHECK_INDEX_THEN_FILE: u32 = 1; +pub const GIT_ATTR_CHECK_INDEX_ONLY: u32 = 2; +pub const GIT_ATTR_CHECK_NO_SYSTEM: u32 = 1 << 2; +pub const GIT_ATTR_CHECK_INCLUDE_HEAD: u32 = 1 << 3; #[repr(C)] pub struct git_cred { pub credtype: git_credtype_t, - pub free: extern fn(*mut git_cred), + pub free: Option, } git_enum! { @@ -809,25 +969,29 @@ git_enum! { } } -pub type git_cred_ssh_interactive_callback = extern fn( - name: *const c_char, - name_len: c_int, - instruction: *const c_char, - instruction_len: c_int, - num_prompts: c_int, - prompts: *const LIBSSH2_USERAUTH_KBDINT_PROMPT, - responses: *mut LIBSSH2_USERAUTH_KBDINT_RESPONSE, - abstrakt: *mut *mut c_void -); - -pub type git_cred_sign_callback = extern fn( - session: *mut LIBSSH2_SESSION, - sig: *mut *mut c_uchar, - sig_len: *mut size_t, - data: *const c_uchar, - data_len: size_t, - abstrakt: *mut *mut c_void, -); +pub type git_cred_ssh_interactive_callback = Option< + extern "C" fn( + name: *const c_char, + name_len: c_int, + instruction: *const c_char, + instruction_len: c_int, + num_prompts: c_int, + prompts: *const LIBSSH2_USERAUTH_KBDINT_PROMPT, + responses: *mut LIBSSH2_USERAUTH_KBDINT_RESPONSE, + abstrakt: *mut *mut c_void, + ), +>; + +pub type git_cred_sign_callback = Option< + extern "C" fn( + session: *mut LIBSSH2_SESSION, + sig: *mut *mut c_uchar, + sig_len: *mut size_t, + data: *const c_uchar, + data_len: size_t, + abstrakt: *mut *mut c_void, + ), +>; pub enum LIBSSH2_SESSION {} pub enum LIBSSH2_USERAUTH_KBDINT_PROMPT {} @@ -838,12 +1002,14 @@ pub struct git_push_options { pub version: c_uint, pub pb_parallelism: c_uint, pub callbacks: git_remote_callbacks, + pub proxy_opts: git_proxy_options, + pub follow_redirects: git_remote_redirect_t, pub custom_headers: git_strarray, + pub remote_push_options: git_strarray, } -pub type git_tag_foreach_cb = extern fn(name: *const c_char, - oid: *mut git_oid, - payload: *mut c_void) -> c_int; +pub type git_tag_foreach_cb = + Option c_int>; git_enum! { pub enum git_index_add_option_t { @@ -856,9 +1022,11 @@ git_enum! { git_enum! { pub enum git_repository_open_flag_t { - GIT_REPOSITORY_OPEN_NO_SEARCH = (1 << 0), - GIT_REPOSITORY_OPEN_CROSS_FS = (1 << 1), - GIT_REPOSITORY_OPEN_BARE = (1 << 2), + GIT_REPOSITORY_OPEN_NO_SEARCH = 1 << 0, + GIT_REPOSITORY_OPEN_CROSS_FS = 1 << 1, + GIT_REPOSITORY_OPEN_BARE = 1 << 2, + GIT_REPOSITORY_OPEN_NO_DOTGIT = 1 << 3, + GIT_REPOSITORY_OPEN_FROM_ENV = 1 << 4, } } @@ -878,12 +1046,12 @@ pub const GIT_REPOSITORY_INIT_OPTIONS_VERSION: c_uint = 1; git_enum! { pub enum git_repository_init_flag_t { - GIT_REPOSITORY_INIT_BARE = (1 << 0), - GIT_REPOSITORY_INIT_NO_REINIT = (1 << 1), - GIT_REPOSITORY_INIT_NO_DOTGIT_DIR = (1 << 2), - GIT_REPOSITORY_INIT_MKDIR = (1 << 3), - GIT_REPOSITORY_INIT_MKPATH = (1 << 4), - GIT_REPOSITORY_INIT_EXTERNAL_TEMPLATE = (1 << 5), + GIT_REPOSITORY_INIT_BARE = 1 << 0, + GIT_REPOSITORY_INIT_NO_REINIT = 1 << 1, + GIT_REPOSITORY_INIT_NO_DOTGIT_DIR = 1 << 2, + GIT_REPOSITORY_INIT_MKDIR = 1 << 3, + GIT_REPOSITORY_INIT_MKPATH = 1 << 4, + GIT_REPOSITORY_INIT_EXTERNAL_TEMPLATE = 1 << 5, } } @@ -898,9 +1066,9 @@ git_enum! { git_enum! { pub enum git_sort_t { GIT_SORT_NONE = 0, - GIT_SORT_TOPOLOGICAL = (1 << 0), - GIT_SORT_TIME = (1 << 1), - GIT_SORT_REVERSE = (1 << 2), + GIT_SORT_TOPOLOGICAL = 1 << 0, + GIT_SORT_TIME = 1 << 1, + GIT_SORT_REVERSE = 1 << 2, } } @@ -944,18 +1112,19 @@ git_enum! { } } -pub type git_diff_file_cb = extern fn(*const git_diff_delta, f32, *mut c_void) - -> c_int; -pub type git_diff_hunk_cb = extern fn(*const git_diff_delta, - *const git_diff_hunk, - *mut c_void) -> c_int; -pub type git_diff_line_cb = extern fn(*const git_diff_delta, - *const git_diff_hunk, - *const git_diff_line, - *mut c_void) -> c_int; -pub type git_diff_binary_cb = extern fn(*const git_diff_delta, - *const git_diff_binary, - *mut c_void) -> c_int; +pub type git_diff_file_cb = Option c_int>; +pub type git_diff_hunk_cb = + Option c_int>; +pub type git_diff_line_cb = Option< + extern "C" fn( + *const git_diff_delta, + *const git_diff_hunk, + *const git_diff_line, + *mut c_void, + ) -> c_int, +>; +pub type git_diff_binary_cb = + Option c_int>; #[repr(C)] pub struct git_diff_hunk { @@ -1003,12 +1172,21 @@ pub struct git_diff_options { pub payload: *mut c_void, pub context_lines: u32, pub interhunk_lines: u32, + pub oid_type: git_oid_t, pub id_abbrev: u16, pub max_size: git_off_t, pub old_prefix: *const c_char, pub new_prefix: *const c_char, } +git_enum! { + pub enum git_oid_t { + GIT_OID_SHA1 = 1, + // SHA256 is still experimental so we are not going to enable it. + /* GIT_OID_SHA256 = 2, */ + } +} + git_enum! { pub enum git_diff_format_t { GIT_DIFF_FORMAT_PATCH = 1, @@ -1016,6 +1194,7 @@ git_enum! { GIT_DIFF_FORMAT_RAW = 3, GIT_DIFF_FORMAT_NAME_ONLY = 4, GIT_DIFF_FORMAT_NAME_STATUS = 5, + GIT_DIFF_FORMAT_PATCH_ID = 6, } } @@ -1029,44 +1208,47 @@ git_enum! { } } -pub type git_diff_notify_cb = extern fn(*const git_diff, - *const git_diff_delta, - *const c_char, - *mut c_void) -> c_int; - -pub type git_diff_progress_cb = extern fn (*const git_diff, - *const c_char, - *const c_char, - *mut c_void) -> c_int; - -pub const GIT_DIFF_NORMAL: u32 = 0; -pub const GIT_DIFF_REVERSE: u32 = 1 << 0; -pub const GIT_DIFF_INCLUDE_IGNORED: u32 = 1 << 1; -pub const GIT_DIFF_RECURSE_IGNORED_DIRS: u32 = 1 << 2; -pub const GIT_DIFF_INCLUDE_UNTRACKED: u32 = 1 << 3; -pub const GIT_DIFF_RECURSE_UNTRACKED_DIRS: u32 = 1 << 4; -pub const GIT_DIFF_INCLUDE_UNMODIFIED: u32 = 1 << 5; -pub const GIT_DIFF_INCLUDE_TYPECHANGE: u32 = 1 << 6; -pub const GIT_DIFF_INCLUDE_TYPECHANGE_TREES: u32 = 1 << 7; -pub const GIT_DIFF_IGNORE_FILEMODE: u32 = 1 << 8; -pub const GIT_DIFF_IGNORE_SUBMODULES: u32 = 1 << 9; -pub const GIT_DIFF_IGNORE_CASE: u32 = 1 << 10; -pub const GIT_DIFF_DISABLE_PATHSPEC_MATCH: u32 = 1 << 12; -pub const GIT_DIFF_SKIP_BINARY_CHECK: u32 = 1 << 13; -pub const GIT_DIFF_ENABLE_FAST_UNTRACKED_DIRS: u32 = 1 << 14; -pub const GIT_DIFF_UPDATE_INDEX: u32 = 1 << 15; -pub const GIT_DIFF_INCLUDE_UNREADABLE: u32 = 1 << 16; -pub const GIT_DIFF_INCLUDE_UNREADABLE_AS_UNTRACKED: u32 = 1 << 17; -pub const GIT_DIFF_FORCE_TEXT: u32 = 1 << 20; -pub const GIT_DIFF_FORCE_BINARY: u32 = 1 << 21; -pub const GIT_DIFF_IGNORE_WHITESPACE: u32 = 1 << 22; -pub const GIT_DIFF_IGNORE_WHITESPACE_CHANGE: u32 = 1 << 23; -pub const GIT_DIFF_IGNORE_WHITESPACE_EOL: u32 = 1 << 24; -pub const GIT_DIFF_SHOW_UNTRACKED_CONTENT: u32 = 1 << 25; -pub const GIT_DIFF_SHOW_UNMODIFIED: u32 = 1 << 26; -pub const GIT_DIFF_PATIENCE: u32 = 1 << 28; -pub const GIT_DIFF_MINIMAL: u32 = 1 << 29; -pub const GIT_DIFF_SHOW_BINARY: u32 = 1 << 30; +pub type git_diff_notify_cb = Option< + extern "C" fn(*const git_diff, *const git_diff_delta, *const c_char, *mut c_void) -> c_int, +>; + +pub type git_diff_progress_cb = + Option c_int>; + +git_enum! { + pub enum git_diff_option_t { + GIT_DIFF_NORMAL = 0, + GIT_DIFF_REVERSE = 1 << 0, + GIT_DIFF_INCLUDE_IGNORED = 1 << 1, + GIT_DIFF_RECURSE_IGNORED_DIRS = 1 << 2, + GIT_DIFF_INCLUDE_UNTRACKED = 1 << 3, + GIT_DIFF_RECURSE_UNTRACKED_DIRS = 1 << 4, + GIT_DIFF_INCLUDE_UNMODIFIED = 1 << 5, + GIT_DIFF_INCLUDE_TYPECHANGE = 1 << 6, + GIT_DIFF_INCLUDE_TYPECHANGE_TREES = 1 << 7, + GIT_DIFF_IGNORE_FILEMODE = 1 << 8, + GIT_DIFF_IGNORE_SUBMODULES = 1 << 9, + GIT_DIFF_IGNORE_CASE = 1 << 10, + GIT_DIFF_DISABLE_PATHSPEC_MATCH = 1 << 12, + GIT_DIFF_SKIP_BINARY_CHECK = 1 << 13, + GIT_DIFF_ENABLE_FAST_UNTRACKED_DIRS = 1 << 14, + GIT_DIFF_UPDATE_INDEX = 1 << 15, + GIT_DIFF_INCLUDE_UNREADABLE = 1 << 16, + GIT_DIFF_INCLUDE_UNREADABLE_AS_UNTRACKED = 1 << 17, + GIT_DIFF_INDENT_HEURISTIC = 1 << 18, + GIT_DIFF_IGNORE_BLANK_LINES = 1 << 19, + GIT_DIFF_FORCE_TEXT = 1 << 20, + GIT_DIFF_FORCE_BINARY = 1 << 21, + GIT_DIFF_IGNORE_WHITESPACE = 1 << 22, + GIT_DIFF_IGNORE_WHITESPACE_CHANGE = 1 << 23, + GIT_DIFF_IGNORE_WHITESPACE_EOL = 1 << 24, + GIT_DIFF_SHOW_UNTRACKED_CONTENT = 1 << 25, + GIT_DIFF_SHOW_UNMODIFIED = 1 << 26, + GIT_DIFF_PATIENCE = 1 << 28, + GIT_DIFF_MINIMAL = 1 << 29, + GIT_DIFF_SHOW_BINARY = 1 << 30, + } +} #[repr(C)] pub struct git_diff_find_options { @@ -1082,18 +1264,21 @@ pub struct git_diff_find_options { #[repr(C)] pub struct git_diff_similarity_metric { - pub file_signature: extern fn(*mut *mut c_void, - *const git_diff_file, - *const c_char, - *mut c_void) -> c_int, - pub buffer_signature: extern fn(*mut *mut c_void, - *const git_diff_file, - *const c_char, - size_t, - *mut c_void) -> c_int, - pub free_signature: extern fn(*mut c_void, *mut c_void), - pub similarity: extern fn(*mut c_int, *mut c_void, *mut c_void, - *mut c_void) -> c_int, + pub file_signature: Option< + extern "C" fn(*mut *mut c_void, *const git_diff_file, *const c_char, *mut c_void) -> c_int, + >, + pub buffer_signature: Option< + extern "C" fn( + *mut *mut c_void, + *const git_diff_file, + *const c_char, + size_t, + *mut c_void, + ) -> c_int, + >, + pub free_signature: Option, + pub similarity: + Option c_int>, pub payload: *mut c_void, } @@ -1106,19 +1291,43 @@ pub const GIT_DIFF_FIND_COPIES: u32 = 1 << 2; pub const GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED: u32 = 1 << 3; pub const GIT_DIFF_FIND_REWRITES: u32 = 1 << 4; pub const GIT_DIFF_BREAK_REWRITES: u32 = 1 << 5; -pub const GIT_DIFF_FIND_AND_BREAK_REWRITES: u32 = - GIT_DIFF_FIND_REWRITES | GIT_DIFF_BREAK_REWRITES; +pub const GIT_DIFF_FIND_AND_BREAK_REWRITES: u32 = GIT_DIFF_FIND_REWRITES | GIT_DIFF_BREAK_REWRITES; pub const GIT_DIFF_FIND_FOR_UNTRACKED: u32 = 1 << 6; pub const GIT_DIFF_FIND_ALL: u32 = 0x0ff; pub const GIT_DIFF_FIND_IGNORE_LEADING_WHITESPACE: u32 = 0; pub const GIT_DIFF_FIND_IGNORE_WHITESPACE: u32 = 1 << 12; pub const GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE: u32 = 1 << 13; pub const GIT_DIFF_FIND_EXACT_MATCH_ONLY: u32 = 1 << 14; -pub const GIT_DIFF_BREAK_REWRITES_FOR_RENAMES_ONLY : u32 = 1 << 15; +pub const GIT_DIFF_BREAK_REWRITES_FOR_RENAMES_ONLY: u32 = 1 << 15; pub const GIT_DIFF_FIND_REMOVE_UNMODIFIED: u32 = 1 << 16; +#[repr(C)] +pub struct git_diff_format_email_options { + pub version: c_uint, + pub flags: u32, + pub patch_no: usize, + pub total_patches: usize, + pub id: *const git_oid, + pub summary: *const c_char, + pub body: *const c_char, + pub author: *const git_signature, +} + +pub const GIT_DIFF_FORMAT_EMAIL_OPTIONS_VERSION: c_uint = 1; + +pub const GIT_DIFF_FORMAT_EMAIL_NONE: u32 = 0; +pub const GIT_DIFF_FORMAT_EMAIL_EXCLUDE_SUBJECT_PATCH_MARKER: u32 = 1 << 0; + +#[repr(C)] +pub struct git_diff_patchid_options { + pub version: c_uint, +} + +pub const GIT_DIFF_PATCHID_OPTIONS_VERSION: c_uint = 1; + #[repr(C)] pub struct git_diff_binary { + pub contains_data: c_uint, pub old_file: git_diff_binary_file, pub new_file: git_diff_binary_file, } @@ -1142,13 +1351,34 @@ git_enum! { #[repr(C)] pub struct git_merge_options { pub version: c_uint, - pub flags: git_merge_flag_t, + pub flags: u32, pub rename_threshold: c_uint, pub target_limit: c_uint, pub metric: *mut git_diff_similarity_metric, pub recursion_limit: c_uint, + pub default_driver: *const c_char, pub file_favor: git_merge_file_favor_t, - pub file_flags: git_merge_file_flag_t, + pub file_flags: u32, +} + +#[repr(C)] +pub struct git_merge_file_options { + pub version: c_uint, + pub ancestor_label: *const c_char, + pub our_label: *const c_char, + pub their_label: *const c_char, + pub favor: git_merge_file_favor_t, + pub flags: u32, + pub marker_size: c_ushort, +} + +#[repr(C)] +pub struct git_merge_file_result { + pub automergeable: c_uint, + pub path: *const c_char, + pub mode: c_uint, + pub ptr: *const c_char, + pub len: size_t, } git_enum! { @@ -1172,56 +1402,311 @@ git_enum! { git_enum! { pub enum git_merge_file_flag_t { GIT_MERGE_FILE_DEFAULT = 0, - GIT_MERGE_FILE_STYLE_MERGE = (1 << 0), - GIT_MERGE_FILE_STYLE_DIFF3 = (1 << 1), - GIT_MERGE_FILE_SIMPLIFY_ALNUM = (1 << 2), - GIT_MERGE_FILE_IGNORE_WHITESPACE = (1 << 3), - GIT_MERGE_FILE_IGNORE_WHITESPACE_CHANGE = (1 << 4), - GIT_MERGE_FILE_IGNORE_WHITESPACE_EOL = (1 << 5), - GIT_MERGE_FILE_DIFF_PATIENCE = (1 << 6), - GIT_MERGE_FILE_DIFF_MINIMAL = (1 << 7), + GIT_MERGE_FILE_STYLE_MERGE = 1 << 0, + GIT_MERGE_FILE_STYLE_DIFF3 = 1 << 1, + GIT_MERGE_FILE_SIMPLIFY_ALNUM = 1 << 2, + GIT_MERGE_FILE_IGNORE_WHITESPACE = 1 << 3, + GIT_MERGE_FILE_IGNORE_WHITESPACE_CHANGE = 1 << 4, + GIT_MERGE_FILE_IGNORE_WHITESPACE_EOL = 1 << 5, + GIT_MERGE_FILE_DIFF_PATIENCE = 1 << 6, + GIT_MERGE_FILE_DIFF_MINIMAL = 1 << 7, + GIT_MERGE_FILE_STYLE_ZDIFF3 = 1 << 8, + GIT_MERGE_FILE_ACCEPT_CONFLICTS = 1 << 9, + } +} + +git_enum! { + pub enum git_merge_analysis_t { + GIT_MERGE_ANALYSIS_NONE = 0, + GIT_MERGE_ANALYSIS_NORMAL = 1 << 0, + GIT_MERGE_ANALYSIS_UP_TO_DATE = 1 << 1, + GIT_MERGE_ANALYSIS_FASTFORWARD = 1 << 2, + GIT_MERGE_ANALYSIS_UNBORN = 1 << 3, + } +} + +git_enum! { + pub enum git_merge_preference_t { + GIT_MERGE_PREFERENCE_NONE = 0, + GIT_MERGE_PREFERENCE_NO_FASTFORWARD = 1 << 0, + GIT_MERGE_PREFERENCE_FASTFORWARD_ONLY = 1 << 1, } } -pub type git_transport_cb = extern fn(out: *mut *mut git_transport, - owner: *mut git_remote, - param: *mut c_void) -> c_int; +pub type git_transport_cb = Option< + extern "C" fn( + out: *mut *mut git_transport, + owner: *mut git_remote, + param: *mut c_void, + ) -> c_int, +>; #[repr(C)] pub struct git_transport { pub version: c_uint, - pub set_callbacks: extern fn(*mut git_transport, - git_transport_message_cb, - git_transport_message_cb, - git_transport_certificate_check_cb, - *mut c_void) -> c_int, - pub set_custom_headers: extern fn(*mut git_transport, - *const git_strarray) -> c_int, - pub connect: extern fn(*mut git_transport, - *const c_char, - git_cred_acquire_cb, - *mut c_void, - c_int, c_int) -> c_int, - pub ls: extern fn(*mut *mut *const git_remote_head, - *mut size_t, - *mut git_transport) -> c_int, - pub push: extern fn(*mut git_transport, - *mut git_push, - *const git_remote_callbacks) -> c_int, - pub negotiate_fetch: extern fn(*mut git_transport, - *mut git_repository, - *const *const git_remote_head, - size_t) -> c_int, - pub download_pack: extern fn(*mut git_transport, - *mut git_repository, - *mut git_transfer_progress, - git_transfer_progress_cb, - *mut c_void) -> c_int, - pub is_connected: extern fn(*mut git_transport) -> c_int, - pub read_flags: extern fn(*mut git_transport, *mut c_int) -> c_int, - pub cancel: extern fn(*mut git_transport), - pub close: extern fn(*mut git_transport) -> c_int, - pub free: extern fn(*mut git_transport), + pub connect: Option< + extern "C" fn( + transport: *mut git_transport, + url: *const c_char, + direction: c_int, + connect_opts: *const git_remote_connect_options, + ) -> c_int, + >, + pub set_connect_opts: Option< + extern "C" fn( + transport: *mut git_transport, + connect_opts: *const git_remote_connect_options, + ) -> c_int, + >, + pub capabilities: + Option c_int>, + pub ls: Option< + extern "C" fn( + out: *mut *mut *const git_remote_head, + size: *mut size_t, + transport: *mut git_transport, + ) -> c_int, + >, + pub push: Option c_int>, + pub negotiate_fetch: Option< + extern "C" fn( + transport: *mut git_transport, + repo: *mut git_repository, + fetch_data: *const git_fetch_negotiation, + ) -> c_int, + >, + pub shallow_roots: + Option c_int>, + pub download_pack: Option< + extern "C" fn( + transport: *mut git_transport, + repo: *mut git_repository, + stats: *mut git_indexer_progress, + ) -> c_int, + >, + pub is_connected: Option c_int>, + pub cancel: Option, + pub close: Option c_int>, + pub free: Option, +} + +#[repr(C)] +pub struct git_remote_connect_options { + pub version: c_uint, + pub callbacks: git_remote_callbacks, + pub proxy_opts: git_proxy_options, + pub follow_redirects: git_remote_redirect_t, + pub custom_headers: git_strarray, +} + +git_enum! { + pub enum git_remote_redirect_t { + GIT_REMOTE_REDIRECT_NONE = 1 << 0, + GIT_REMOTE_REDIRECT_INITIAL = 1 << 1, + GIT_REMOTE_REDIRECT_ALL = 1 << 2, + } +} + +#[repr(C)] +pub struct git_odb_backend { + pub version: c_uint, + pub odb: *mut git_odb, + pub read: Option< + extern "C" fn( + *mut *mut c_void, + *mut size_t, + *mut git_object_t, + *mut git_odb_backend, + *const git_oid, + ) -> c_int, + >, + + pub read_prefix: Option< + extern "C" fn( + *mut git_oid, + *mut *mut c_void, + *mut size_t, + *mut git_object_t, + *mut git_odb_backend, + *const git_oid, + size_t, + ) -> c_int, + >, + pub read_header: Option< + extern "C" fn( + *mut size_t, + *mut git_object_t, + *mut git_odb_backend, + *const git_oid, + ) -> c_int, + >, + + pub write: Option< + extern "C" fn( + *mut git_odb_backend, + *const git_oid, + *const c_void, + size_t, + git_object_t, + ) -> c_int, + >, + + pub writestream: Option< + extern "C" fn( + *mut *mut git_odb_stream, + *mut git_odb_backend, + git_object_size_t, + git_object_t, + ) -> c_int, + >, + + pub readstream: Option< + extern "C" fn( + *mut *mut git_odb_stream, + *mut size_t, + *mut git_object_t, + *mut git_odb_backend, + *const git_oid, + ) -> c_int, + >, + + pub exists: Option c_int>, + + pub exists_prefix: + Option c_int>, + + pub refresh: Option c_int>, + + pub foreach: + Option c_int>, + + pub writepack: Option< + extern "C" fn( + *mut *mut git_odb_writepack, + *mut git_odb_backend, + *mut git_odb, + git_indexer_progress_cb, + *mut c_void, + ) -> c_int, + >, + + pub writemidx: Option c_int>, + + pub freshen: Option c_int>, + + pub free: Option, +} + +git_enum! { + pub enum git_odb_lookup_flags_t { + GIT_ODB_LOOKUP_NO_REFRESH = 1 << 0, + } +} + +#[repr(C)] +pub struct git_odb_writepack { + pub backend: *mut git_odb_backend, + + pub append: Option< + extern "C" fn( + *mut git_odb_writepack, + *const c_void, + size_t, + *mut git_indexer_progress, + ) -> c_int, + >, + + pub commit: + Option c_int>, + + pub free: Option, +} + +#[repr(C)] +pub struct git_refdb_backend { + pub version: c_uint, + pub exists: Option c_int>, + pub lookup: Option< + extern "C" fn(*mut *mut git_reference, *mut git_refdb_backend, *const c_char) -> c_int, + >, + pub iterator: Option< + extern "C" fn( + *mut *mut git_reference_iterator, + *mut git_refdb_backend, + *const c_char, + ) -> c_int, + >, + pub write: Option< + extern "C" fn( + *mut git_refdb_backend, + *const git_reference, + c_int, + *const git_signature, + *const c_char, + *const git_oid, + *const c_char, + ) -> c_int, + >, + pub rename: Option< + extern "C" fn( + *mut *mut git_reference, + *mut git_refdb_backend, + *const c_char, + *const c_char, + c_int, + *const git_signature, + *const c_char, + ) -> c_int, + >, + pub del: Option< + extern "C" fn( + *mut git_refdb_backend, + *const c_char, + *const git_oid, + *const c_char, + ) -> c_int, + >, + pub compress: Option c_int>, + pub has_log: Option c_int>, + pub ensure_log: Option c_int>, + pub free: Option, + pub reflog_read: + Option c_int>, + pub reflog_write: Option c_int>, + pub reflog_rename: + Option c_int>, + pub reflog_delete: Option c_int>, + pub lock: + Option c_int>, + pub unlock: Option< + extern "C" fn( + *mut git_refdb_backend, + *mut c_void, + c_int, + c_int, + *const git_reference, + *const git_signature, + *const c_char, + ) -> c_int, + >, +} + +#[repr(C)] +pub struct git_proxy_options { + pub version: c_uint, + pub kind: git_proxy_t, + pub url: *const c_char, + pub credentials: git_cred_acquire_cb, + pub certificate_check: git_transport_certificate_check_cb, + pub payload: *mut c_void, +} + +git_enum! { + pub enum git_proxy_t { + GIT_PROXY_NONE = 0, + GIT_PROXY_AUTO = 1, + GIT_PROXY_SPECIFIED = 2, + } } git_enum! { @@ -1236,29 +1721,36 @@ git_enum! { #[repr(C)] pub struct git_smart_subtransport_stream { pub subtransport: *mut git_smart_subtransport, - pub read: extern fn(*mut git_smart_subtransport_stream, - *mut c_char, - size_t, - *mut size_t) -> c_int, - pub write: extern fn(*mut git_smart_subtransport_stream, - *const c_char, - size_t) -> c_int, - pub free: extern fn(*mut git_smart_subtransport_stream), + pub read: Option< + extern "C" fn( + *mut git_smart_subtransport_stream, + *mut c_char, + size_t, + *mut size_t, + ) -> c_int, + >, + pub write: + Option c_int>, + pub free: Option, } #[repr(C)] pub struct git_smart_subtransport { - pub action: extern fn(*mut *mut git_smart_subtransport_stream, - *mut git_smart_subtransport, - *const c_char, - git_smart_service_t) -> c_int, - pub close: extern fn(*mut git_smart_subtransport) -> c_int, - pub free: extern fn(*mut git_smart_subtransport), + pub action: Option< + extern "C" fn( + *mut *mut git_smart_subtransport_stream, + *mut git_smart_subtransport, + *const c_char, + git_smart_service_t, + ) -> c_int, + >, + pub close: Option c_int>, + pub free: Option, } -pub type git_smart_subtransport_cb = extern fn(*mut *mut git_smart_subtransport, - *mut git_transport, - *mut c_void) -> c_int; +pub type git_smart_subtransport_cb = Option< + extern "C" fn(*mut *mut git_smart_subtransport, *mut git_transport, *mut c_void) -> c_int, +>; #[repr(C)] pub struct git_smart_subtransport_definition { @@ -1300,320 +1792,648 @@ git_enum! { } } -pub type git_packbuilder_foreach_cb = extern fn(*const c_void, size_t, - *mut c_void) -> c_int; +git_enum! { + pub enum git_stash_flags { + GIT_STASH_DEFAULT = 0, + GIT_STASH_KEEP_INDEX = 1 << 0, + GIT_STASH_INCLUDE_UNTRACKED = 1 << 1, + GIT_STASH_INCLUDE_IGNORED = 1 << 2, + GIT_STASH_KEEP_ALL = 1 << 3, + } +} -/// Initialize openssl for the libgit2 library -#[cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), feature = "https"))] -pub fn openssl_init() { - if !cfg!(target_os = "linux") && !cfg!(target_os = "freebsd") { return } - - // Currently, libgit2 leverages OpenSSL for SSL support when cloning - // repositories over HTTPS. This means that we're picking up an OpenSSL - // dependency on non-Windows platforms (where it has its own HTTPS - // subsystem). As a result, we need to link to OpenSSL. - // - // Now actually *linking* to OpenSSL isn't so hard. We just need to make - // sure to use pkg-config to discover any relevant system dependencies for - // differences between distributions like CentOS and Ubuntu. The actual - // trickiness comes about when we start *distributing* the resulting - // binaries. Currently Cargo is distributed in binary form as nightlies, - // which means we're distributing a binary with OpenSSL linked in. - // - // For historical reasons, the Linux nightly builder is running a CentOS - // distribution in order to have as much ABI compatibility with other - // distributions as possible. Sadly, however, this compatibility does not - // extend to OpenSSL. Currently OpenSSL has two major versions, 0.9 and 1.0, - // which are incompatible (many ABI differences). The CentOS builder we - // build on has version 1.0, as do most distributions today. Some still have - // 0.9, however. This means that if we are to distribute the binaries built - // by the CentOS machine, we would only be compatible with OpenSSL 1.0 and - // we would fail to run (a dynamic linker error at runtime) on systems with - // only 9.8 installed (hopefully). - // - // But wait, the plot thickens! Apparently CentOS has dubbed their OpenSSL - // library as `libssl.so.10`, notably the `10` is included at the end. On - // the other hand Ubuntu, for example, only distributes `libssl.so`. This - // means that the binaries created at CentOS are hard-wired to probe for a - // file called `libssl.so.10` at runtime (using the LD_LIBRARY_PATH), which - // will not be found on ubuntu. The conclusion of this is that binaries - // built on CentOS cannot be distributed to Ubuntu and run successfully. - // - // There are a number of sneaky things we could do, including, but not - // limited to: - // - // 1. Create a shim program which runs "just before" cargo runs. The - // responsibility of this shim program would be to locate `libssl.so`, - // whatever it's called, on the current system, make sure there's a - // symlink *somewhere* called `libssl.so.10`, and then set up - // LD_LIBRARY_PATH and run the actual cargo. - // - // This approach definitely seems unconventional, and is borderline - // overkill for this problem. It's also dubious if we can find a - // libssl.so reliably on the target system. - // - // 2. Somehow re-work the CentOS installation so that the linked-against - // library is called libssl.so instead of libssl.so.10 - // - // The problem with this approach is that systems with 0.9 installed will - // start to silently fail, due to also having libraries called libssl.so - // (probably symlinked under a more appropriate version). - // - // 3. Compile Cargo against both OpenSSL 1.0 *and* OpenSSL 0.9, and - // distribute both. Also make sure that the linked-against name of the - // library is `libssl.so`. At runtime we determine which version is - // installed, and we then the appropriate binary. - // - // This approach clearly has drawbacks in terms of infrastructure and - // feasibility. - // - // 4. Build a nightly of Cargo for each distribution we'd like to support. - // You would then pick the appropriate Cargo nightly to install locally. - // - // So, with all this in mind, the decision was made to *statically* link - // OpenSSL. This solves any problem of relying on a downstream OpenSSL - // version being available. This does, however, open a can of worms related - // to security issues. It's generally a good idea to dynamically link - // OpenSSL as you'll get security updates over time without having to do - // anything (the system administrator will update the local openssl - // package). By statically linking, we're forfeiting this feature. - // - // The conclusion was made it is likely appropriate for the Cargo nightlies - // to statically link OpenSSL, but highly encourage distributions and - // packagers of Cargo to dynamically link OpenSSL. Packagers are targeting - // one system and are distributing to only that system, so none of the - // problems mentioned above would arise. - // - // In order to support this, a new package was made: openssl-static-sys. - // This package currently performs a fairly simple task: - // - // 1. Run pkg-config to discover where openssl is installed. - // 2. If openssl is installed in a nonstandard location, *and* static copies - // of the libraries are available, copy them to $OUT_DIR. - // - // This library will bring in libssl.a and libcrypto.a into the local build, - // allowing them to be picked up by this crate. This allows us to configure - // our own buildbots to have pkg-config point to these local pre-built - // copies of a static OpenSSL (with very few dependencies) while allowing - // most other builds of Cargo to naturally dynamically link OpenSSL. - // - // So in summary, if you're with me so far, we've statically linked OpenSSL - // to the Cargo binary (or any binary, for that matter) and we're ready to - // distribute it to *all* linux distributions. Remember that our original - // intent for openssl was for HTTPS support, which implies that we need some - // for of CA certificate store to validate certificates. This is normally - // installed in a standard system location. - // - // Unfortunately, as one might imagine, OpenSSL is configured for where this - // standard location is at *build time*, but it often varies widely - // per-system. Consequently, it was discovered that OpenSSL will respect the - // SSL_CERT_FILE and SSL_CERT_DIR environment variables in order to assist - // in discovering the location of this file (hurray!). - // - // So, finally getting to the point, this function solely exists to support - // our static builds of OpenSSL by probing for the "standard system - // location" of certificates and setting relevant environment variable to - // point to them. - // - // Ah, and as a final note, this is only a problem on Linux, not on OS X. On - // OS X the OpenSSL binaries are stable enough that we can just rely on - // dynamic linkage (plus they have some weird modifications to OpenSSL which - // means we wouldn't want to link statically). - openssl::probe::init_ssl_cert_env_vars(); -} - -#[cfg(any(windows, target_os = "macos", target_os = "ios", not(feature = "https")))] -pub fn openssl_init() {} +git_enum! { + pub enum git_stash_apply_flags { + GIT_STASH_APPLY_DEFAULT = 0, + GIT_STASH_APPLY_REINSTATE_INDEX = 1 << 0, + } +} -extern { +git_enum! { + pub enum git_stash_apply_progress_t { + GIT_STASH_APPLY_PROGRESS_NONE = 0, + GIT_STASH_APPLY_PROGRESS_LOADING_STASH, + GIT_STASH_APPLY_PROGRESS_ANALYZE_INDEX, + GIT_STASH_APPLY_PROGRESS_ANALYZE_MODIFIED, + GIT_STASH_APPLY_PROGRESS_ANALYZE_UNTRACKED, + GIT_STASH_APPLY_PROGRESS_CHECKOUT_UNTRACKED, + GIT_STASH_APPLY_PROGRESS_CHECKOUT_MODIFIED, + GIT_STASH_APPLY_PROGRESS_DONE, + } +} + +#[repr(C)] +pub struct git_stash_save_options { + pub version: c_uint, + pub flags: u32, + pub stasher: *const git_signature, + pub message: *const c_char, + pub paths: git_strarray, +} + +pub const GIT_STASH_SAVE_OPTIONS_VERSION: c_uint = 1; + +#[repr(C)] +pub struct git_stash_apply_options { + pub version: c_uint, + pub flags: u32, + pub checkout_options: git_checkout_options, + pub progress_cb: git_stash_apply_progress_cb, + pub progress_payload: *mut c_void, +} + +pub type git_stash_apply_progress_cb = + Option c_int>; + +pub type git_stash_cb = Option< + extern "C" fn( + index: size_t, + message: *const c_char, + stash_id: *const git_oid, + payload: *mut c_void, + ) -> c_int, +>; + +pub type git_packbuilder_foreach_cb = + Option c_int>; + +pub type git_odb_foreach_cb = + Option c_int>; + +pub type git_commit_signing_cb = Option< + extern "C" fn( + signature: *mut git_buf, + signature_field: *mut git_buf, + commit_content: *const c_char, + payload: *mut c_void, + ) -> c_int, +>; + +pub type git_commit_create_cb = Option< + extern "C" fn( + *mut git_oid, + *const git_signature, + *const git_signature, + *const c_char, + *const c_char, + *const git_tree, + usize, + *const git_commit, + *mut c_void, + ) -> c_int, +>; + +pub const GIT_REBASE_NO_OPERATION: usize = usize::max_value(); + +#[repr(C)] +pub struct git_rebase_options { + pub version: c_uint, + pub quiet: c_int, + pub inmemory: c_int, + pub rewrite_notes_ref: *const c_char, + pub merge_options: git_merge_options, + pub checkout_options: git_checkout_options, + pub commit_create_cb: git_commit_create_cb, + pub signing_cb: git_commit_signing_cb, + pub payload: *mut c_void, +} + +git_enum! { + pub enum git_rebase_operation_t { + GIT_REBASE_OPERATION_PICK = 0, + GIT_REBASE_OPERATION_REWORD, + GIT_REBASE_OPERATION_EDIT, + GIT_REBASE_OPERATION_SQUASH, + GIT_REBASE_OPERATION_FIXUP, + GIT_REBASE_OPERATION_EXEC, + } +} + +#[repr(C)] +pub struct git_rebase_operation { + pub kind: git_rebase_operation_t, + pub id: git_oid, + pub exec: *const c_char, +} + +#[repr(C)] +pub struct git_cherrypick_options { + pub version: c_uint, + pub mainline: c_uint, + pub merge_opts: git_merge_options, + pub checkout_opts: git_checkout_options, +} + +pub type git_revert_options = git_cherrypick_options; + +pub type git_apply_delta_cb = + Option c_int>; + +pub type git_apply_hunk_cb = + Option c_int>; + +git_enum! { + pub enum git_apply_flags_t { + GIT_APPLY_CHECK = 1<<0, + } +} + +#[repr(C)] +pub struct git_apply_options { + pub version: c_uint, + pub delta_cb: git_apply_delta_cb, + pub hunk_cb: git_apply_hunk_cb, + pub payload: *mut c_void, + pub flags: u32, +} + +git_enum! { + pub enum git_apply_location_t { + GIT_APPLY_LOCATION_WORKDIR = 0, + GIT_APPLY_LOCATION_INDEX = 1, + GIT_APPLY_LOCATION_BOTH = 2, + } +} + +git_enum! { + pub enum git_libgit2_opt_t { + GIT_OPT_GET_MWINDOW_SIZE = 0, + GIT_OPT_SET_MWINDOW_SIZE, + GIT_OPT_GET_MWINDOW_MAPPED_LIMIT, + GIT_OPT_SET_MWINDOW_MAPPED_LIMIT, + GIT_OPT_GET_SEARCH_PATH, + GIT_OPT_SET_SEARCH_PATH, + GIT_OPT_SET_CACHE_OBJECT_LIMIT, + GIT_OPT_SET_CACHE_MAX_SIZE, + GIT_OPT_ENABLE_CACHING, + GIT_OPT_GET_CACHED_MEMORY, + GIT_OPT_GET_TEMPLATE_PATH, + GIT_OPT_SET_TEMPLATE_PATH, + GIT_OPT_SET_SSL_CERT_LOCATIONS, + GIT_OPT_SET_USER_AGENT, + GIT_OPT_ENABLE_STRICT_OBJECT_CREATION, + GIT_OPT_ENABLE_STRICT_SYMBOLIC_REF_CREATION, + GIT_OPT_SET_SSL_CIPHERS, + GIT_OPT_GET_USER_AGENT, + GIT_OPT_ENABLE_OFS_DELTA, + GIT_OPT_ENABLE_FSYNC_GITDIR, + GIT_OPT_GET_WINDOWS_SHAREMODE, + GIT_OPT_SET_WINDOWS_SHAREMODE, + GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION, + GIT_OPT_SET_ALLOCATOR, + GIT_OPT_ENABLE_UNSAVED_INDEX_SAFETY, + GIT_OPT_GET_PACK_MAX_OBJECTS, + GIT_OPT_SET_PACK_MAX_OBJECTS, + GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS, + GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE, + GIT_OPT_GET_MWINDOW_FILE_LIMIT, + GIT_OPT_SET_MWINDOW_FILE_LIMIT, + GIT_OPT_SET_ODB_PACKED_PRIORITY, + GIT_OPT_SET_ODB_LOOSE_PRIORITY, + GIT_OPT_GET_EXTENSIONS, + GIT_OPT_SET_EXTENSIONS, + GIT_OPT_GET_OWNER_VALIDATION, + GIT_OPT_SET_OWNER_VALIDATION, + GIT_OPT_GET_HOMEDIR, + GIT_OPT_SET_HOMEDIR, + GIT_OPT_SET_SERVER_CONNECT_TIMEOUT, + GIT_OPT_GET_SERVER_CONNECT_TIMEOUT, + GIT_OPT_SET_SERVER_TIMEOUT, + GIT_OPT_GET_SERVER_TIMEOUT, + GIT_OPT_SET_USER_AGENT_PRODUCT, + GIT_OPT_GET_USER_AGENT_PRODUCT, + } +} + +git_enum! { + pub enum git_reference_format_t { + GIT_REFERENCE_FORMAT_NORMAL = 0, + GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL = 1 << 0, + GIT_REFERENCE_FORMAT_REFSPEC_PATTERN = 1 << 1, + GIT_REFERENCE_FORMAT_REFSPEC_SHORTHAND = 1 << 2, + } +} + +#[repr(C)] +pub struct git_worktree_add_options { + pub version: c_uint, + pub lock: c_int, + pub checkout_existing: c_int, + pub reference: *mut git_reference, + pub checkout_options: git_checkout_options, +} + +pub const GIT_WORKTREE_ADD_OPTIONS_VERSION: c_uint = 1; + +git_enum! { + pub enum git_worktree_prune_t { + /* Prune working tree even if working tree is valid */ + GIT_WORKTREE_PRUNE_VALID = 1 << 0, + /* Prune working tree even if it is locked */ + GIT_WORKTREE_PRUNE_LOCKED = 1 << 1, + /* Prune checked out working tree */ + GIT_WORKTREE_PRUNE_WORKING_TREE = 1 << 2, + } +} + +#[repr(C)] +pub struct git_worktree_prune_options { + pub version: c_uint, + pub flags: u32, +} + +pub const GIT_WORKTREE_PRUNE_OPTIONS_VERSION: c_uint = 1; + +pub type git_repository_mergehead_foreach_cb = + Option c_int>; + +pub type git_repository_fetchhead_foreach_cb = Option< + extern "C" fn(*const c_char, *const c_char, *const git_oid, c_uint, *mut c_void) -> c_int, +>; + +git_enum! { + pub enum git_trace_level_t { + /* No tracing will be performed. */ + GIT_TRACE_NONE = 0, + + /* Severe errors that may impact the program's execution */ + GIT_TRACE_FATAL = 1, + + /* Errors that do not impact the program's execution */ + GIT_TRACE_ERROR = 2, + + /* Warnings that suggest abnormal data */ + GIT_TRACE_WARN = 3, + + /* Informational messages about program execution */ + GIT_TRACE_INFO = 4, + + /* Detailed data that allows for debugging */ + GIT_TRACE_DEBUG = 5, + + /* Exceptionally detailed debugging data */ + GIT_TRACE_TRACE = 6, + } +} + +pub type git_trace_cb = Option; + +git_enum! { + pub enum git_feature_t { + GIT_FEATURE_THREADS = 1 << 0, + GIT_FEATURE_HTTPS = 1 << 1, + GIT_FEATURE_SSH = 1 << 2, + GIT_FEATURE_NSEC = 1 << 3, + } +} + +#[repr(C)] +pub struct git_message_trailer { + pub key: *const c_char, + pub value: *const c_char, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct git_message_trailer_array { + pub trailers: *mut git_message_trailer, + pub count: size_t, + pub _trailer_block: *mut c_char, +} + +#[repr(C)] +pub struct git_email_create_options { + pub version: c_uint, + pub flags: u32, + pub diff_opts: git_diff_options, + pub diff_find_opts: git_diff_find_options, + pub subject_prefix: *const c_char, + pub start_number: usize, + pub reroll_number: usize, +} + +pub const GIT_EMAIL_CREATE_OPTIONS_VERSION: c_uint = 1; + +git_enum! { + pub enum git_email_create_flags_t { + GIT_EMAIL_CREATE_DEFAULT = 0, + GIT_EMAIL_CREATE_OMIT_NUMBERS = 1 << 0, + GIT_EMAIL_CREATE_ALWAYS_NUMBER = 1 << 1, + GIT_EMAIL_CREATE_NO_RENAMES = 1 << 2, + } +} + +extern "C" { // threads pub fn git_libgit2_init() -> c_int; pub fn git_libgit2_shutdown() -> c_int; // repository + pub fn git_repository_new(out: *mut *mut git_repository) -> c_int; pub fn git_repository_free(repo: *mut git_repository); - pub fn git_repository_open(repo: *mut *mut git_repository, - path: *const c_char) -> c_int; - pub fn git_repository_open_ext(repo: *mut *mut git_repository, - path: *const c_char, - flags: c_uint, - ceiling_dirs: *const c_char) -> c_int; - pub fn git_repository_init(repo: *mut *mut git_repository, - path: *const c_char, - is_bare: c_uint) -> c_int; - pub fn git_repository_init_ext(out: *mut *mut git_repository, - repo_path: *const c_char, - opts: *mut git_repository_init_options) - -> c_int; - pub fn git_repository_init_init_options(opts: *mut git_repository_init_options, - version: c_uint) -> c_int; - pub fn git_repository_get_namespace(repo: *mut git_repository) - -> *const c_char; - pub fn git_repository_head(out: *mut *mut git_reference, - repo: *mut git_repository) -> c_int; - pub fn git_repository_set_head(repo: *mut git_repository, - refname: *const c_char) -> c_int; - pub fn git_repository_set_head_detached(repo: *mut git_repository, - commitish: *const git_oid) -> c_int; - pub fn git_repository_is_bare(repo: *mut git_repository) -> c_int; + pub fn git_repository_open(repo: *mut *mut git_repository, path: *const c_char) -> c_int; + pub fn git_repository_open_bare(repo: *mut *mut git_repository, path: *const c_char) -> c_int; + pub fn git_repository_open_ext( + repo: *mut *mut git_repository, + path: *const c_char, + flags: c_uint, + ceiling_dirs: *const c_char, + ) -> c_int; + pub fn git_repository_open_from_worktree( + repo: *mut *mut git_repository, + worktree: *mut git_worktree, + ) -> c_int; + pub fn git_repository_wrap_odb(repo: *mut *mut git_repository, odb: *mut git_odb) -> c_int; + pub fn git_repository_init( + repo: *mut *mut git_repository, + path: *const c_char, + is_bare: c_uint, + ) -> c_int; + pub fn git_repository_init_ext( + out: *mut *mut git_repository, + repo_path: *const c_char, + opts: *mut git_repository_init_options, + ) -> c_int; + pub fn git_repository_init_init_options( + opts: *mut git_repository_init_options, + version: c_uint, + ) -> c_int; + pub fn git_repository_get_namespace(repo: *mut git_repository) -> *const c_char; + pub fn git_repository_set_namespace( + repo: *mut git_repository, + namespace: *const c_char, + ) -> c_int; + pub fn git_repository_head(out: *mut *mut git_reference, repo: *mut git_repository) -> c_int; + pub fn git_repository_set_head(repo: *mut git_repository, refname: *const c_char) -> c_int; + + pub fn git_repository_head_detached(repo: *mut git_repository) -> c_int; + pub fn git_repository_set_head_detached( + repo: *mut git_repository, + commitish: *const git_oid, + ) -> c_int; + pub fn git_repository_set_head_detached_from_annotated( + repo: *mut git_repository, + commitish: *const git_annotated_commit, + ) -> c_int; + pub fn git_repository_set_bare(repo: *mut git_repository) -> c_int; + pub fn git_repository_is_worktree(repo: *const git_repository) -> c_int; + pub fn git_repository_is_bare(repo: *const git_repository) -> c_int; pub fn git_repository_is_empty(repo: *mut git_repository) -> c_int; pub fn git_repository_is_shallow(repo: *mut git_repository) -> c_int; - pub fn git_repository_path(repo: *mut git_repository) -> *const c_char; + pub fn git_repository_path(repo: *const git_repository) -> *const c_char; + pub fn git_repository_commondir(repo: *const git_repository) -> *const c_char; pub fn git_repository_state(repo: *mut git_repository) -> c_int; - pub fn git_repository_workdir(repo: *mut git_repository) -> *const c_char; - pub fn git_repository_set_workdir(repo: *mut git_repository, - workdir: *const c_char, - update_gitlink: c_int) -> c_int; - pub fn git_repository_index(out: *mut *mut git_index, - repo: *mut git_repository) -> c_int; - pub fn git_repository_set_index(repo: *mut git_repository, - index: *mut git_index); - pub fn git_repository_config(out: *mut *mut git_config, - repo: *mut git_repository) -> c_int; - pub fn git_repository_config_snapshot(out: *mut *mut git_config, - repo: *mut git_repository) -> c_int; - pub fn git_repository_discover(out: *mut git_buf, - start_path: *const c_char, - across_fs: c_int, - ceiling_dirs: *const c_char) -> c_int; + pub fn git_repository_workdir(repo: *const git_repository) -> *const c_char; + pub fn git_repository_set_workdir( + repo: *mut git_repository, + workdir: *const c_char, + update_gitlink: c_int, + ) -> c_int; + pub fn git_repository_index(out: *mut *mut git_index, repo: *mut git_repository) -> c_int; + pub fn git_repository_set_index(repo: *mut git_repository, index: *mut git_index) -> c_int; + + pub fn git_repository_message(buf: *mut git_buf, repo: *mut git_repository) -> c_int; + + pub fn git_repository_message_remove(repo: *mut git_repository) -> c_int; + pub fn git_repository_config(out: *mut *mut git_config, repo: *mut git_repository) -> c_int; + pub fn git_repository_set_config(repo: *mut git_repository, config: *mut git_config) -> c_int; + pub fn git_repository_config_snapshot( + out: *mut *mut git_config, + repo: *mut git_repository, + ) -> c_int; + pub fn git_repository_discover( + out: *mut git_buf, + start_path: *const c_char, + across_fs: c_int, + ceiling_dirs: *const c_char, + ) -> c_int; + pub fn git_repository_set_odb(repo: *mut git_repository, odb: *mut git_odb) -> c_int; + + pub fn git_repository_refdb(out: *mut *mut git_refdb, repo: *mut git_repository) -> c_int; + pub fn git_repository_set_refdb(repo: *mut git_repository, refdb: *mut git_refdb) -> c_int; + + pub fn git_repository_reinit_filesystem( + repo: *mut git_repository, + recurse_submodules: c_int, + ) -> c_int; + pub fn git_repository_mergehead_foreach( + repo: *mut git_repository, + callback: git_repository_mergehead_foreach_cb, + payload: *mut c_void, + ) -> c_int; + pub fn git_repository_fetchhead_foreach( + repo: *mut git_repository, + callback: git_repository_fetchhead_foreach_cb, + payload: *mut c_void, + ) -> c_int; + pub fn git_ignore_add_rule(repo: *mut git_repository, rules: *const c_char) -> c_int; + pub fn git_ignore_clear_internal_rules(repo: *mut git_repository) -> c_int; + pub fn git_ignore_path_is_ignored( + ignored: *mut c_int, + repo: *mut git_repository, + path: *const c_char, + ) -> c_int; // revparse - pub fn git_revparse(revspec: *mut git_revspec, - repo: *mut git_repository, - spec: *const c_char) -> c_int; - pub fn git_revparse_single(out: *mut *mut git_object, - repo: *mut git_repository, - spec: *const c_char) -> c_int; - pub fn git_revparse_ext(object_out: *mut *mut git_object, - reference_out: *mut *mut git_reference, - repo: *mut git_repository, - spec: *const c_char) -> c_int; + pub fn git_revparse( + revspec: *mut git_revspec, + repo: *mut git_repository, + spec: *const c_char, + ) -> c_int; + pub fn git_revparse_single( + out: *mut *mut git_object, + repo: *mut git_repository, + spec: *const c_char, + ) -> c_int; + pub fn git_revparse_ext( + object_out: *mut *mut git_object, + reference_out: *mut *mut git_reference, + repo: *mut git_repository, + spec: *const c_char, + ) -> c_int; // object - pub fn git_object_dup(dest: *mut *mut git_object, - source: *mut git_object) -> c_int; + pub fn git_object_dup(dest: *mut *mut git_object, source: *mut git_object) -> c_int; pub fn git_object_id(obj: *const git_object) -> *const git_oid; pub fn git_object_free(object: *mut git_object); - pub fn git_object_lookup(dest: *mut *mut git_object, - repo: *mut git_repository, - id: *const git_oid, - kind: git_otype) -> c_int; - pub fn git_object_type(obj: *const git_object) -> git_otype; - pub fn git_object_peel(peeled: *mut *mut git_object, - object: *const git_object, - target_type: git_otype) -> c_int; - pub fn git_object_short_id(out: *mut git_buf, - obj: *const git_object) -> c_int; - pub fn git_object_type2string(kind: git_otype) -> *const c_char; - pub fn git_object_string2type(s: *const c_char) -> git_otype; - pub fn git_object_typeisloose(kind: git_otype) -> c_int; + pub fn git_object_lookup( + dest: *mut *mut git_object, + repo: *mut git_repository, + id: *const git_oid, + kind: git_object_t, + ) -> c_int; + pub fn git_object_lookup_prefix( + dest: *mut *mut git_object, + repo: *mut git_repository, + id: *const git_oid, + len: size_t, + kind: git_object_t, + ) -> c_int; + pub fn git_object_type(obj: *const git_object) -> git_object_t; + pub fn git_object_peel( + peeled: *mut *mut git_object, + object: *const git_object, + target_type: git_object_t, + ) -> c_int; + pub fn git_object_short_id(out: *mut git_buf, obj: *const git_object) -> c_int; + pub fn git_object_type2string(kind: git_object_t) -> *const c_char; + pub fn git_object_string2type(s: *const c_char) -> git_object_t; + pub fn git_object_typeisloose(kind: git_object_t) -> c_int; // oid - pub fn git_oid_fromraw(out: *mut git_oid, raw: *const c_uchar); - pub fn git_oid_fromstrn(out: *mut git_oid, str: *const c_char, - len: size_t) -> c_int; - pub fn git_oid_tostr(out: *mut c_char, n: size_t, - id: *const git_oid) -> *mut c_char; + pub fn git_oid_fromraw(out: *mut git_oid, raw: *const c_uchar) -> c_int; + pub fn git_oid_fromstrn(out: *mut git_oid, str: *const c_char, len: size_t) -> c_int; + pub fn git_oid_tostr(out: *mut c_char, n: size_t, id: *const git_oid) -> *mut c_char; pub fn git_oid_cmp(a: *const git_oid, b: *const git_oid) -> c_int; pub fn git_oid_equal(a: *const git_oid, b: *const git_oid) -> c_int; pub fn git_oid_streq(id: *const git_oid, str: *const c_char) -> c_int; pub fn git_oid_iszero(id: *const git_oid) -> c_int; - // giterr - pub fn giterr_last() -> *const git_error; - pub fn giterr_clear(); - pub fn giterr_set_str(error_class: c_int, string: *const c_char); + // error + pub fn git_error_last() -> *const git_error; + pub fn git_error_clear(); + pub fn git_error_set_str(error_class: c_int, string: *const c_char) -> c_int; // remote - pub fn git_remote_create(out: *mut *mut git_remote, - repo: *mut git_repository, - name: *const c_char, - url: *const c_char) -> c_int; - pub fn git_remote_lookup(out: *mut *mut git_remote, - repo: *mut git_repository, - name: *const c_char) -> c_int; - pub fn git_remote_create_anonymous(out: *mut *mut git_remote, - repo: *mut git_repository, - url: *const c_char) -> c_int; - pub fn git_remote_delete(repo: *mut git_repository, - name: *const c_char) -> c_int; + pub fn git_remote_create( + out: *mut *mut git_remote, + repo: *mut git_repository, + name: *const c_char, + url: *const c_char, + ) -> c_int; + pub fn git_remote_create_with_fetchspec( + out: *mut *mut git_remote, + repo: *mut git_repository, + name: *const c_char, + url: *const c_char, + fetch: *const c_char, + ) -> c_int; + pub fn git_remote_lookup( + out: *mut *mut git_remote, + repo: *mut git_repository, + name: *const c_char, + ) -> c_int; + pub fn git_remote_create_anonymous( + out: *mut *mut git_remote, + repo: *mut git_repository, + url: *const c_char, + ) -> c_int; + pub fn git_remote_create_detached(out: *mut *mut git_remote, url: *const c_char) -> c_int; + pub fn git_remote_delete(repo: *mut git_repository, name: *const c_char) -> c_int; pub fn git_remote_free(remote: *mut git_remote); pub fn git_remote_name(remote: *const git_remote) -> *const c_char; pub fn git_remote_pushurl(/service/remote: *const git_remote) -> *const c_char; pub fn git_remote_refspec_count(remote: *const git_remote) -> size_t; pub fn git_remote_url(/service/remote: *const git_remote) -> *const c_char; - pub fn git_remote_connect(remote: *mut git_remote, - dir: git_direction, - callbacks: *const git_remote_callbacks, - custom_headers: *const git_strarray) -> c_int; + pub fn git_remote_connect( + remote: *mut git_remote, + dir: git_direction, + callbacks: *const git_remote_callbacks, + proxy_opts: *const git_proxy_options, + custom_headers: *const git_strarray, + ) -> c_int; pub fn git_remote_connected(remote: *const git_remote) -> c_int; - pub fn git_remote_disconnect(remote: *mut git_remote); - pub fn git_remote_add_fetch(repo: *mut git_repository, - remote: *const c_char, - refspec: *const c_char) -> c_int; - pub fn git_remote_add_push(repo: *mut git_repository, - remote: *const c_char, - refspec: *const c_char) -> c_int; - pub fn git_remote_download(remote: *mut git_remote, - refspecs: *const git_strarray, - opts: *const git_fetch_options) -> c_int; - pub fn git_remote_stop(remote: *mut git_remote); - pub fn git_remote_dup(dest: *mut *mut git_remote, - source: *mut git_remote) -> c_int; - pub fn git_remote_get_fetch_refspecs(array: *mut git_strarray, - remote: *const git_remote) -> c_int; - pub fn git_remote_get_refspec(remote: *const git_remote, - n: size_t) -> *const git_refspec; + pub fn git_remote_disconnect(remote: *mut git_remote) -> c_int; + pub fn git_remote_add_fetch( + repo: *mut git_repository, + remote: *const c_char, + refspec: *const c_char, + ) -> c_int; + pub fn git_remote_add_push( + repo: *mut git_repository, + remote: *const c_char, + refspec: *const c_char, + ) -> c_int; + pub fn git_remote_download( + remote: *mut git_remote, + refspecs: *const git_strarray, + opts: *const git_fetch_options, + ) -> c_int; + pub fn git_remote_stop(remote: *mut git_remote) -> c_int; + pub fn git_remote_dup(dest: *mut *mut git_remote, source: *mut git_remote) -> c_int; + pub fn git_remote_get_fetch_refspecs( + array: *mut git_strarray, + remote: *const git_remote, + ) -> c_int; + pub fn git_remote_get_push_refspecs( + array: *mut git_strarray, + remote: *const git_remote, + ) -> c_int; + pub fn git_remote_get_refspec(remote: *const git_remote, n: size_t) -> *const git_refspec; pub fn git_remote_is_valid_name(remote_name: *const c_char) -> c_int; - pub fn git_remote_list(out: *mut git_strarray, - repo: *mut git_repository) -> c_int; - pub fn git_remote_rename(problems: *mut git_strarray, - repo: *mut git_repository, - name: *const c_char, - new_name: *const c_char) -> c_int; - pub fn git_remote_fetch(remote: *mut git_remote, - refspecs: *const git_strarray, - opts: *const git_fetch_options, - reflog_message: *const c_char) -> c_int; - pub fn git_remote_push(remote: *mut git_remote, - refspecs: *const git_strarray, - opts: *const git_push_options) -> c_int; - pub fn git_remote_update_tips(remote: *mut git_remote, - callbacks: *const git_remote_callbacks, - update_fetchead: c_int, - download_tags: git_remote_autotag_option_t, - reflog_message: *const c_char) -> c_int; - pub fn git_remote_set_url(repo: *mut git_repository, - remote: *const c_char, - url: *const c_char) -> c_int; - pub fn git_remote_set_pushurl(repo: *mut git_repository, - remote: *const c_char, - pushurl: *const c_char) -> c_int; - pub fn git_remote_init_callbacks(opts: *mut git_remote_callbacks, - version: c_uint) -> c_int; - pub fn git_fetch_init_options(opts: *mut git_fetch_options, - version: c_uint) -> c_int; - pub fn git_remote_stats(remote: *mut git_remote) - -> *const git_transfer_progress; - pub fn git_remote_ls(out: *mut *mut *const git_remote_head, - size: *mut size_t, - remote: *mut git_remote) -> c_int; - pub fn git_remote_set_autotag(repo: *mut git_repository, - remote: *const c_char, - value: git_remote_autotag_option_t) -> c_int; - pub fn git_remote_prune(remote: *mut git_remote, - callbacks: *const git_remote_callbacks) -> c_int; + pub fn git_remote_name_is_valid(valid: *mut c_int, remote_name: *const c_char) -> c_int; + pub fn git_remote_list(out: *mut git_strarray, repo: *mut git_repository) -> c_int; + pub fn git_remote_rename( + problems: *mut git_strarray, + repo: *mut git_repository, + name: *const c_char, + new_name: *const c_char, + ) -> c_int; + pub fn git_remote_fetch( + remote: *mut git_remote, + refspecs: *const git_strarray, + opts: *const git_fetch_options, + reflog_message: *const c_char, + ) -> c_int; + pub fn git_remote_push( + remote: *mut git_remote, + refspecs: *const git_strarray, + opts: *const git_push_options, + ) -> c_int; + pub fn git_remote_update_tips( + remote: *mut git_remote, + callbacks: *const git_remote_callbacks, + update_flags: c_uint, + download_tags: git_remote_autotag_option_t, + reflog_message: *const c_char, + ) -> c_int; + pub fn git_remote_set_url( + repo: *mut git_repository, + remote: *const c_char, + url: *const c_char, + ) -> c_int; + pub fn git_remote_set_pushurl( + repo: *mut git_repository, + remote: *const c_char, + pushurl: *const c_char, + ) -> c_int; + pub fn git_remote_init_callbacks(opts: *mut git_remote_callbacks, version: c_uint) -> c_int; + pub fn git_fetch_init_options(opts: *mut git_fetch_options, version: c_uint) -> c_int; + pub fn git_remote_stats(remote: *mut git_remote) -> *const git_indexer_progress; + pub fn git_remote_ls( + out: *mut *mut *const git_remote_head, + size: *mut size_t, + remote: *mut git_remote, + ) -> c_int; + pub fn git_remote_set_autotag( + repo: *mut git_repository, + remote: *const c_char, + value: git_remote_autotag_option_t, + ) -> c_int; + pub fn git_remote_prune( + remote: *mut git_remote, + callbacks: *const git_remote_callbacks, + ) -> c_int; + pub fn git_remote_default_branch(out: *mut git_buf, remote: *mut git_remote) -> c_int; // refspec pub fn git_refspec_direction(spec: *const git_refspec) -> git_direction; pub fn git_refspec_dst(spec: *const git_refspec) -> *const c_char; - pub fn git_refspec_dst_matches(spec: *const git_refspec, - refname: *const c_char) -> c_int; + pub fn git_refspec_dst_matches(spec: *const git_refspec, refname: *const c_char) -> c_int; pub fn git_refspec_src(spec: *const git_refspec) -> *const c_char; - pub fn git_refspec_src_matches(spec: *const git_refspec, - refname: *const c_char) -> c_int; + pub fn git_refspec_src_matches(spec: *const git_refspec, refname: *const c_char) -> c_int; pub fn git_refspec_force(spec: *const git_refspec) -> c_int; pub fn git_refspec_string(spec: *const git_refspec) -> *const c_char; + pub fn git_refspec_transform( + out: *mut git_buf, + spec: *const git_refspec, + name: *const c_char, + ) -> c_int; + pub fn git_refspec_rtransform( + out: *mut git_buf, + spec: *const git_refspec, + name: *const c_char, + ) -> c_int; // strarray pub fn git_strarray_free(array: *mut git_strarray); @@ -1622,57 +2442,70 @@ extern { pub fn git_oidarray_free(array: *mut git_oidarray); // signature - pub fn git_signature_default(out: *mut *mut git_signature, - repo: *mut git_repository) -> c_int; + pub fn git_signature_default(out: *mut *mut git_signature, repo: *mut git_repository) -> c_int; pub fn git_signature_free(sig: *mut git_signature); - pub fn git_signature_new(out: *mut *mut git_signature, - name: *const c_char, - email: *const c_char, - time: git_time_t, - offset: c_int) -> c_int; - pub fn git_signature_now(out: *mut *mut git_signature, - name: *const c_char, - email: *const c_char) -> c_int; - pub fn git_signature_dup(dest: *mut *mut git_signature, - sig: *const git_signature) -> c_int; + pub fn git_signature_new( + out: *mut *mut git_signature, + name: *const c_char, + email: *const c_char, + time: git_time_t, + offset: c_int, + ) -> c_int; + pub fn git_signature_now( + out: *mut *mut git_signature, + name: *const c_char, + email: *const c_char, + ) -> c_int; + pub fn git_signature_dup(dest: *mut *mut git_signature, sig: *const git_signature) -> c_int; // status - pub fn git_status_list_new(out: *mut *mut git_status_list, - repo: *mut git_repository, - options: *const git_status_options) -> c_int; + pub fn git_status_list_new( + out: *mut *mut git_status_list, + repo: *mut git_repository, + options: *const git_status_options, + ) -> c_int; pub fn git_status_list_entrycount(list: *mut git_status_list) -> size_t; - pub fn git_status_byindex(statuslist: *mut git_status_list, - idx: size_t) -> *const git_status_entry; + pub fn git_status_byindex( + statuslist: *mut git_status_list, + idx: size_t, + ) -> *const git_status_entry; pub fn git_status_list_free(list: *mut git_status_list); - pub fn git_status_init_options(opts: *mut git_status_options, - version: c_uint) -> c_int; - pub fn git_status_file(status_flags: *mut c_uint, - repo: *mut git_repository, - path: *const c_char) -> c_int; - pub fn git_status_should_ignore(ignored: *mut c_int, - repo: *mut git_repository, - path: *const c_char) -> c_int; + pub fn git_status_init_options(opts: *mut git_status_options, version: c_uint) -> c_int; + pub fn git_status_file( + status_flags: *mut c_uint, + repo: *mut git_repository, + path: *const c_char, + ) -> c_int; + pub fn git_status_should_ignore( + ignored: *mut c_int, + repo: *mut git_repository, + path: *const c_char, + ) -> c_int; // clone - pub fn git_clone(out: *mut *mut git_repository, - url: *const c_char, - local_path: *const c_char, - options: *const git_clone_options) -> c_int; - pub fn git_clone_init_options(opts: *mut git_clone_options, - version: c_uint) -> c_int; + pub fn git_clone( + out: *mut *mut git_repository, + url: *const c_char, + local_path: *const c_char, + options: *const git_clone_options, + ) -> c_int; + pub fn git_clone_init_options(opts: *mut git_clone_options, version: c_uint) -> c_int; // reset - pub fn git_reset(repo: *mut git_repository, - target: *mut git_object, - reset_type: git_reset_t, - checkout_opts: *const git_checkout_options) -> c_int; - pub fn git_reset_default(repo: *mut git_repository, - target: *mut git_object, - pathspecs: *mut git_strarray) -> c_int; + pub fn git_reset( + repo: *mut git_repository, + target: *const git_object, + reset_type: git_reset_t, + checkout_opts: *const git_checkout_options, + ) -> c_int; + pub fn git_reset_default( + repo: *mut git_repository, + target: *const git_object, + pathspecs: *const git_strarray, + ) -> c_int; // reference - pub fn git_reference_cmp(ref1: *const git_reference, - ref2: *const git_reference) -> c_int; + pub fn git_reference_cmp(ref1: *const git_reference, ref2: *const git_reference) -> c_int; pub fn git_reference_delete(r: *mut git_reference) -> c_int; pub fn git_reference_free(r: *mut git_reference); pub fn git_reference_is_branch(r: *const git_reference) -> c_int; @@ -1680,879 +2513,1899 @@ extern { pub fn git_reference_is_remote(r: *const git_reference) -> c_int; pub fn git_reference_is_tag(r: *const git_reference) -> c_int; pub fn git_reference_is_valid_name(name: *const c_char) -> c_int; - pub fn git_reference_lookup(out: *mut *mut git_reference, - repo: *mut git_repository, - name: *const c_char) -> c_int; + pub fn git_reference_name_is_valid(valid: *mut c_int, refname: *const c_char) -> c_int; + pub fn git_reference_lookup( + out: *mut *mut git_reference, + repo: *mut git_repository, + name: *const c_char, + ) -> c_int; + pub fn git_reference_dwim( + out: *mut *mut git_reference, + repo: *mut git_repository, + refname: *const c_char, + ) -> c_int; pub fn git_reference_name(r: *const git_reference) -> *const c_char; - pub fn git_reference_name_to_id(out: *mut git_oid, - repo: *mut git_repository, - name: *const c_char) -> c_int; - pub fn git_reference_peel(out: *mut *mut git_object, - r: *mut git_reference, - otype: git_otype) -> c_int; - pub fn git_reference_rename(new_ref: *mut *mut git_reference, - r: *mut git_reference, - new_name: *const c_char, - force: c_int, - log_message: *const c_char) -> c_int; - pub fn git_reference_resolve(out: *mut *mut git_reference, - r: *const git_reference) -> c_int; + pub fn git_reference_name_to_id( + out: *mut git_oid, + repo: *mut git_repository, + name: *const c_char, + ) -> c_int; + pub fn git_reference_peel( + out: *mut *mut git_object, + r: *const git_reference, + otype: git_object_t, + ) -> c_int; + pub fn git_reference_rename( + new_ref: *mut *mut git_reference, + r: *mut git_reference, + new_name: *const c_char, + force: c_int, + log_message: *const c_char, + ) -> c_int; + pub fn git_reference_resolve(out: *mut *mut git_reference, r: *const git_reference) -> c_int; pub fn git_reference_shorthand(r: *const git_reference) -> *const c_char; pub fn git_reference_symbolic_target(r: *const git_reference) -> *const c_char; pub fn git_reference_target(r: *const git_reference) -> *const git_oid; pub fn git_reference_target_peel(r: *const git_reference) -> *const git_oid; - pub fn git_reference_set_target(out: *mut *mut git_reference, - r: *mut git_reference, - id: *const git_oid, - log_message: *const c_char) -> c_int; - pub fn git_reference_type(r: *const git_reference) -> git_ref_t; - pub fn git_reference_iterator_new(out: *mut *mut git_reference_iterator, - repo: *mut git_repository) -> c_int; - pub fn git_reference_iterator_glob_new(out: *mut *mut git_reference_iterator, - repo: *mut git_repository, - glob: *const c_char) -> c_int; + pub fn git_reference_set_target( + out: *mut *mut git_reference, + r: *mut git_reference, + id: *const git_oid, + log_message: *const c_char, + ) -> c_int; + pub fn git_reference_symbolic_set_target( + out: *mut *mut git_reference, + r: *mut git_reference, + target: *const c_char, + log_message: *const c_char, + ) -> c_int; + pub fn git_reference_type(r: *const git_reference) -> git_reference_t; + pub fn git_reference_iterator_new( + out: *mut *mut git_reference_iterator, + repo: *mut git_repository, + ) -> c_int; + pub fn git_reference_iterator_glob_new( + out: *mut *mut git_reference_iterator, + repo: *mut git_repository, + glob: *const c_char, + ) -> c_int; pub fn git_reference_iterator_free(iter: *mut git_reference_iterator); - pub fn git_reference_next(out: *mut *mut git_reference, - iter: *mut git_reference_iterator) -> c_int; - pub fn git_reference_next_name(out: *mut *const c_char, - iter: *mut git_reference_iterator) -> c_int; - pub fn git_reference_create(out: *mut *mut git_reference, - repo: *mut git_repository, - name: *const c_char, - id: *const git_oid, - force: c_int, - log_message: *const c_char) -> c_int; - pub fn git_reference_symbolic_create(out: *mut *mut git_reference, - repo: *mut git_repository, - name: *const c_char, - target: *const c_char, - force: c_int, - log_message: *const c_char) -> c_int; - pub fn git_reference_create_matching(out: *mut *mut git_reference, - repo: *mut git_repository, - name: *const c_char, - id: *const git_oid, - force: c_int, - current_id: *const git_oid, - log_message: *const c_char) -> c_int; - pub fn git_reference_symbolic_create_matching(out: *mut *mut git_reference, - repo: *mut git_repository, - name: *const c_char, - target: *const c_char, - force: c_int, - current_id: *const c_char, - log_message: *const c_char) - -> c_int; - pub fn git_reference_has_log(repo: *mut git_repository, - name: *const c_char) -> c_int; - pub fn git_reference_ensure_log(repo: *mut git_repository, - name: *const c_char) -> c_int; + pub fn git_reference_next( + out: *mut *mut git_reference, + iter: *mut git_reference_iterator, + ) -> c_int; + pub fn git_reference_next_name( + out: *mut *const c_char, + iter: *mut git_reference_iterator, + ) -> c_int; + pub fn git_reference_create( + out: *mut *mut git_reference, + repo: *mut git_repository, + name: *const c_char, + id: *const git_oid, + force: c_int, + log_message: *const c_char, + ) -> c_int; + pub fn git_reference_symbolic_create( + out: *mut *mut git_reference, + repo: *mut git_repository, + name: *const c_char, + target: *const c_char, + force: c_int, + log_message: *const c_char, + ) -> c_int; + pub fn git_reference_create_matching( + out: *mut *mut git_reference, + repo: *mut git_repository, + name: *const c_char, + id: *const git_oid, + force: c_int, + current_id: *const git_oid, + log_message: *const c_char, + ) -> c_int; + pub fn git_reference_symbolic_create_matching( + out: *mut *mut git_reference, + repo: *mut git_repository, + name: *const c_char, + target: *const c_char, + force: c_int, + current_id: *const c_char, + log_message: *const c_char, + ) -> c_int; + pub fn git_reference_has_log(repo: *mut git_repository, name: *const c_char) -> c_int; + pub fn git_reference_ensure_log(repo: *mut git_repository, name: *const c_char) -> c_int; + pub fn git_reference_normalize_name( + buffer_out: *mut c_char, + buffer_size: size_t, + name: *const c_char, + flags: u32, + ) -> c_int; + + // stash + pub fn git_stash_save( + out: *mut git_oid, + repo: *mut git_repository, + stasher: *const git_signature, + message: *const c_char, + flags: c_uint, + ) -> c_int; + + pub fn git_stash_save_options_init(opts: *mut git_stash_save_options, version: c_uint) + -> c_int; + + pub fn git_stash_save_with_opts( + out: *mut git_oid, + repo: *mut git_repository, + options: *const git_stash_save_options, + ) -> c_int; + + pub fn git_stash_apply_init_options( + opts: *mut git_stash_apply_options, + version: c_uint, + ) -> c_int; + + pub fn git_stash_apply( + repo: *mut git_repository, + index: size_t, + options: *const git_stash_apply_options, + ) -> c_int; + + pub fn git_stash_foreach( + repo: *mut git_repository, + callback: git_stash_cb, + payload: *mut c_void, + ) -> c_int; + + pub fn git_stash_drop(repo: *mut git_repository, index: size_t) -> c_int; + + pub fn git_stash_pop( + repo: *mut git_repository, + index: size_t, + options: *const git_stash_apply_options, + ) -> c_int; // submodules pub fn git_submodule_add_finalize(submodule: *mut git_submodule) -> c_int; - pub fn git_submodule_add_setup(submodule: *mut *mut git_submodule, - repo: *mut git_repository, - url: *const c_char, - path: *const c_char, - use_gitlink: c_int) -> c_int; - pub fn git_submodule_add_to_index(submodule: *mut git_submodule, - write_index: c_int) -> c_int; + pub fn git_submodule_add_setup( + submodule: *mut *mut git_submodule, + repo: *mut git_repository, + url: *const c_char, + path: *const c_char, + use_gitlink: c_int, + ) -> c_int; + pub fn git_submodule_add_to_index(submodule: *mut git_submodule, write_index: c_int) -> c_int; pub fn git_submodule_branch(submodule: *mut git_submodule) -> *const c_char; - pub fn git_submodule_foreach(repo: *mut git_repository, - callback: git_submodule_cb, - payload: *mut c_void) -> c_int; + pub fn git_submodule_clone( + repo: *mut *mut git_repository, + submodule: *mut git_submodule, + opts: *const git_submodule_update_options, + ) -> c_int; + pub fn git_submodule_foreach( + repo: *mut git_repository, + callback: git_submodule_cb, + payload: *mut c_void, + ) -> c_int; pub fn git_submodule_free(submodule: *mut git_submodule); pub fn git_submodule_head_id(submodule: *mut git_submodule) -> *const git_oid; + pub fn git_submodule_ignore(submodule: *mut git_submodule) -> git_submodule_ignore_t; pub fn git_submodule_index_id(submodule: *mut git_submodule) -> *const git_oid; - pub fn git_submodule_init(submodule: *mut git_submodule, - overwrite: c_int) -> c_int; - pub fn git_submodule_location(status: *mut c_uint, - submodule: *mut git_submodule) -> c_int; - pub fn git_submodule_lookup(out: *mut *mut git_submodule, - repo: *mut git_repository, - name: *const c_char) -> c_int; + pub fn git_submodule_init(submodule: *mut git_submodule, overwrite: c_int) -> c_int; + pub fn git_submodule_repo_init( + repo: *mut *mut git_repository, + submodule: *const git_submodule, + use_gitlink: c_int, + ) -> c_int; + pub fn git_submodule_location(status: *mut c_uint, submodule: *mut git_submodule) -> c_int; + pub fn git_submodule_lookup( + out: *mut *mut git_submodule, + repo: *mut git_repository, + name: *const c_char, + ) -> c_int; pub fn git_submodule_name(submodule: *mut git_submodule) -> *const c_char; - pub fn git_submodule_open(repo: *mut *mut git_repository, - submodule: *mut git_submodule) -> c_int; + pub fn git_submodule_open( + repo: *mut *mut git_repository, + submodule: *mut git_submodule, + ) -> c_int; pub fn git_submodule_path(submodule: *mut git_submodule) -> *const c_char; - pub fn git_submodule_reload(submodule: *mut git_submodule, - force: c_int) -> c_int; - pub fn git_submodule_set_ignore(repo: *mut git_repository, - name: *const c_char, - ignore: git_submodule_ignore_t) - -> c_int; - pub fn git_submodule_set_update(repo: *mut git_repository, - name: *const c_char, - update: git_submodule_update_t) - -> c_int; - pub fn git_submodule_set_url(repo: *mut git_repository, - name: *const c_char, - url: *const c_char) -> c_int; + pub fn git_submodule_reload(submodule: *mut git_submodule, force: c_int) -> c_int; + pub fn git_submodule_set_ignore( + repo: *mut git_repository, + name: *const c_char, + ignore: git_submodule_ignore_t, + ) -> c_int; + pub fn git_submodule_set_update( + repo: *mut git_repository, + name: *const c_char, + update: git_submodule_update_t, + ) -> c_int; + pub fn git_submodule_set_url( + repo: *mut git_repository, + name: *const c_char, + url: *const c_char, + ) -> c_int; pub fn git_submodule_sync(submodule: *mut git_submodule) -> c_int; - pub fn git_submodule_update_strategy(submodule: *mut git_submodule) - -> git_submodule_update_t; - // pub fn git_submodule_update(submodule: *mut git_submodule, - // init: c_int, - // options: *mut git_submodule_update_options) - // -> c_int; + pub fn git_submodule_update_strategy(submodule: *mut git_submodule) -> git_submodule_update_t; + pub fn git_submodule_update( + submodule: *mut git_submodule, + init: c_int, + options: *mut git_submodule_update_options, + ) -> c_int; + pub fn git_submodule_update_init_options( + options: *mut git_submodule_update_options, + version: c_uint, + ) -> c_int; pub fn git_submodule_url(/service/submodule: *mut git_submodule) -> *const c_char; pub fn git_submodule_wd_id(submodule: *mut git_submodule) -> *const git_oid; - pub fn git_submodule_status(status: *mut c_uint, - repo: *mut git_repository, - name: *const c_char, - ignore: git_submodule_ignore_t) -> c_int; - pub fn git_submodule_set_branch(repo: *mut git_repository, - name: *const c_char, - branch: *const c_char) -> c_int; + pub fn git_submodule_status( + status: *mut c_uint, + repo: *mut git_repository, + name: *const c_char, + ignore: git_submodule_ignore_t, + ) -> c_int; + pub fn git_submodule_set_branch( + repo: *mut git_repository, + name: *const c_char, + branch: *const c_char, + ) -> c_int; // blob pub fn git_blob_free(blob: *mut git_blob); pub fn git_blob_id(blob: *const git_blob) -> *const git_oid; pub fn git_blob_is_binary(blob: *const git_blob) -> c_int; - pub fn git_blob_lookup(blob: *mut *mut git_blob, repo: *mut git_repository, - id: *const git_oid) -> c_int; - pub fn git_blob_lookup_prefix(blob: *mut *mut git_blob, - repo: *mut git_repository, - id: *const git_oid, - len: size_t) -> c_int; + pub fn git_blob_lookup( + blob: *mut *mut git_blob, + repo: *mut git_repository, + id: *const git_oid, + ) -> c_int; + pub fn git_blob_lookup_prefix( + blob: *mut *mut git_blob, + repo: *mut git_repository, + id: *const git_oid, + len: size_t, + ) -> c_int; pub fn git_blob_rawcontent(blob: *const git_blob) -> *const c_void; - pub fn git_blob_rawsize(blob: *const git_blob) -> git_off_t; - pub fn git_blob_create_frombuffer(id: *mut git_oid, - repo: *mut git_repository, - buffer: *const c_void, - len: size_t) -> c_int; - pub fn git_blob_create_fromdisk(id: *mut git_oid, - repo: *mut git_repository, - path: *const c_char) -> c_int; - pub fn git_blob_create_fromworkdir(id: *mut git_oid, - repo: *mut git_repository, - relative_path: *const c_char) -> c_int; + pub fn git_blob_rawsize(blob: *const git_blob) -> git_object_size_t; + pub fn git_blob_create_frombuffer( + id: *mut git_oid, + repo: *mut git_repository, + buffer: *const c_void, + len: size_t, + ) -> c_int; + pub fn git_blob_create_fromdisk( + id: *mut git_oid, + repo: *mut git_repository, + path: *const c_char, + ) -> c_int; + pub fn git_blob_create_fromworkdir( + id: *mut git_oid, + repo: *mut git_repository, + relative_path: *const c_char, + ) -> c_int; + pub fn git_blob_create_fromstream( + out: *mut *mut git_writestream, + repo: *mut git_repository, + hintpath: *const c_char, + ) -> c_int; + pub fn git_blob_create_fromstream_commit( + id: *mut git_oid, + stream: *mut git_writestream, + ) -> c_int; // tree - pub fn git_tree_entry_byid(tree: *const git_tree, - id: *const git_oid) -> *const git_tree_entry; - pub fn git_tree_entry_byindex(tree: *const git_tree, - idx: size_t) -> *const git_tree_entry; - pub fn git_tree_entry_byname(tree: *const git_tree, - filename: *const c_char) -> *const git_tree_entry; - pub fn git_tree_entry_bypath(out: *mut *mut git_tree_entry, - tree: *const git_tree, - filename: *const c_char) -> c_int; - pub fn git_tree_entry_cmp(e1: *const git_tree_entry, - e2: *const git_tree_entry) -> c_int; - pub fn git_tree_entry_dup(dest: *mut *mut git_tree_entry, - src: *const git_tree_entry) -> c_int; + pub fn git_tree_entry_byid(tree: *const git_tree, id: *const git_oid) -> *const git_tree_entry; + pub fn git_tree_entry_byindex(tree: *const git_tree, idx: size_t) -> *const git_tree_entry; + pub fn git_tree_entry_byname( + tree: *const git_tree, + filename: *const c_char, + ) -> *const git_tree_entry; + pub fn git_tree_entry_bypath( + out: *mut *mut git_tree_entry, + tree: *const git_tree, + filename: *const c_char, + ) -> c_int; + pub fn git_tree_entry_cmp(e1: *const git_tree_entry, e2: *const git_tree_entry) -> c_int; + pub fn git_tree_entry_dup(dest: *mut *mut git_tree_entry, src: *const git_tree_entry) -> c_int; pub fn git_tree_entry_filemode(entry: *const git_tree_entry) -> git_filemode_t; pub fn git_tree_entry_filemode_raw(entry: *const git_tree_entry) -> git_filemode_t; pub fn git_tree_entry_free(entry: *mut git_tree_entry); pub fn git_tree_entry_id(entry: *const git_tree_entry) -> *const git_oid; pub fn git_tree_entry_name(entry: *const git_tree_entry) -> *const c_char; - pub fn git_tree_entry_to_object(out: *mut *mut git_object, - repo: *mut git_repository, - entry: *const git_tree_entry) -> c_int; - pub fn git_tree_entry_type(entry: *const git_tree_entry) -> git_otype; + pub fn git_tree_entry_to_object( + out: *mut *mut git_object, + repo: *mut git_repository, + entry: *const git_tree_entry, + ) -> c_int; + pub fn git_tree_entry_type(entry: *const git_tree_entry) -> git_object_t; pub fn git_tree_entrycount(tree: *const git_tree) -> size_t; pub fn git_tree_free(tree: *mut git_tree); pub fn git_tree_id(tree: *const git_tree) -> *const git_oid; - pub fn git_tree_lookup(tree: *mut *mut git_tree, - repo: *mut git_repository, - id: *const git_oid) -> c_int; - pub fn git_tree_walk(tree: *const git_tree, - mode: git_treewalk_mode, - callback: git_treewalk_cb, - payload: *mut c_void) -> c_int; + pub fn git_tree_lookup( + tree: *mut *mut git_tree, + repo: *mut git_repository, + id: *const git_oid, + ) -> c_int; + pub fn git_tree_walk( + tree: *const git_tree, + mode: git_treewalk_mode, + callback: git_treewalk_cb, + payload: *mut c_void, + ) -> c_int; + pub fn git_tree_create_updated( + out: *mut git_oid, + repo: *mut git_repository, + baseline: *mut git_tree, + nupdates: usize, + updates: *const git_tree_update, + ) -> c_int; // treebuilder - pub fn git_treebuilder_new(out: *mut *mut git_treebuilder, - repo: *mut git_repository, - source: *const git_tree) -> c_int; - pub fn git_treebuilder_clear(bld: *mut git_treebuilder); - pub fn git_treebuilder_entrycount(bld: *mut git_treebuilder) -> c_uint; + pub fn git_treebuilder_new( + out: *mut *mut git_treebuilder, + repo: *mut git_repository, + source: *const git_tree, + ) -> c_int; + pub fn git_treebuilder_clear(bld: *mut git_treebuilder) -> c_int; + pub fn git_treebuilder_entrycount(bld: *mut git_treebuilder) -> size_t; pub fn git_treebuilder_free(bld: *mut git_treebuilder); - pub fn git_treebuilder_get(bld: *mut git_treebuilder, - filename: *const c_char) -> *const git_tree_entry; - pub fn git_treebuilder_insert(out: *mut *const git_tree_entry, - bld: *mut git_treebuilder, - filename: *const c_char, - id: *const git_oid, - filemode: git_filemode_t) -> c_int; - pub fn git_treebuilder_remove(bld: *mut git_treebuilder, - filename: *const c_char) -> c_int; - pub fn git_treebuilder_filter(bld: *mut git_treebuilder, - filter: git_treebuilder_filter_cb, - payload: *mut c_void); - pub fn git_treebuilder_write(id: *mut git_oid, - bld: *mut git_treebuilder) -> c_int; + pub fn git_treebuilder_get( + bld: *mut git_treebuilder, + filename: *const c_char, + ) -> *const git_tree_entry; + pub fn git_treebuilder_insert( + out: *mut *const git_tree_entry, + bld: *mut git_treebuilder, + filename: *const c_char, + id: *const git_oid, + filemode: git_filemode_t, + ) -> c_int; + pub fn git_treebuilder_remove(bld: *mut git_treebuilder, filename: *const c_char) -> c_int; + pub fn git_treebuilder_filter( + bld: *mut git_treebuilder, + filter: git_treebuilder_filter_cb, + payload: *mut c_void, + ) -> c_int; + pub fn git_treebuilder_write(id: *mut git_oid, bld: *mut git_treebuilder) -> c_int; // buf - pub fn git_buf_free(buffer: *mut git_buf); + pub fn git_buf_dispose(buffer: *mut git_buf); pub fn git_buf_grow(buffer: *mut git_buf, target_size: size_t) -> c_int; - pub fn git_buf_set(buffer: *mut git_buf, data: *const c_void, - datalen: size_t) -> c_int; + pub fn git_buf_set(buffer: *mut git_buf, data: *const c_void, datalen: size_t) -> c_int; // commit pub fn git_commit_author(commit: *const git_commit) -> *const git_signature; + pub fn git_commit_author_with_mailmap( + out: *mut *mut git_signature, + commit: *const git_commit, + mailmap: *const git_mailmap, + ) -> c_int; pub fn git_commit_committer(commit: *const git_commit) -> *const git_signature; + pub fn git_commit_committer_with_mailmap( + out: *mut *mut git_signature, + commit: *const git_commit, + mailmap: *const git_mailmap, + ) -> c_int; pub fn git_commit_free(commit: *mut git_commit); pub fn git_commit_id(commit: *const git_commit) -> *const git_oid; - pub fn git_commit_lookup(commit: *mut *mut git_commit, - repo: *mut git_repository, - id: *const git_oid) -> c_int; + pub fn git_commit_lookup( + commit: *mut *mut git_commit, + repo: *mut git_repository, + id: *const git_oid, + ) -> c_int; + pub fn git_commit_lookup_prefix( + commit: *mut *mut git_commit, + repo: *mut git_repository, + id: *const git_oid, + len: size_t, + ) -> c_int; pub fn git_commit_message(commit: *const git_commit) -> *const c_char; pub fn git_commit_message_encoding(commit: *const git_commit) -> *const c_char; pub fn git_commit_message_raw(commit: *const git_commit) -> *const c_char; - pub fn git_commit_nth_gen_ancestor(commit: *mut *mut git_commit, - commit: *const git_commit, - n: c_uint) -> c_int; - pub fn git_commit_parent(out: *mut *mut git_commit, - commit: *const git_commit, - n: c_uint) -> c_int; - pub fn git_commit_parent_id(commit: *const git_commit, - n: c_uint) -> *const git_oid; + pub fn git_commit_nth_gen_ancestor( + commit: *mut *mut git_commit, + commit: *const git_commit, + n: c_uint, + ) -> c_int; + pub fn git_commit_parent( + out: *mut *mut git_commit, + commit: *const git_commit, + n: c_uint, + ) -> c_int; + pub fn git_commit_parent_id(commit: *const git_commit, n: c_uint) -> *const git_oid; pub fn git_commit_parentcount(commit: *const git_commit) -> c_uint; pub fn git_commit_raw_header(commit: *const git_commit) -> *const c_char; pub fn git_commit_summary(commit: *mut git_commit) -> *const c_char; + pub fn git_commit_body(commit: *mut git_commit) -> *const c_char; pub fn git_commit_time(commit: *const git_commit) -> git_time_t; pub fn git_commit_time_offset(commit: *const git_commit) -> c_int; - pub fn git_commit_tree(tree_out: *mut *mut git_tree, - commit: *const git_commit) -> c_int; + pub fn git_commit_tree(tree_out: *mut *mut git_tree, commit: *const git_commit) -> c_int; pub fn git_commit_tree_id(commit: *const git_commit) -> *const git_oid; - pub fn git_commit_amend(id: *mut git_oid, - commit_to_amend: *const git_commit, - update_ref: *const c_char, - author: *const git_signature, - committer: *const git_signature, - message_encoding: *const c_char, - message: *const c_char, - tree: *const git_tree) -> c_int; - pub fn git_commit_create(id: *mut git_oid, - repo: *mut git_repository, - update_ref: *const c_char, - author: *const git_signature, - committer: *const git_signature, - message_encoding: *const c_char, - message: *const c_char, - tree: *const git_tree, - parent_count: size_t, - parents: *mut *const git_commit) -> c_int; - pub fn git_commit_header_field(out: *mut git_buf, - commit: *const git_commit, - field: *const c_char) -> c_int; + pub fn git_commit_amend( + id: *mut git_oid, + commit_to_amend: *const git_commit, + update_ref: *const c_char, + author: *const git_signature, + committer: *const git_signature, + message_encoding: *const c_char, + message: *const c_char, + tree: *const git_tree, + ) -> c_int; + pub fn git_commit_create( + id: *mut git_oid, + repo: *mut git_repository, + update_ref: *const c_char, + author: *const git_signature, + committer: *const git_signature, + message_encoding: *const c_char, + message: *const c_char, + tree: *const git_tree, + parent_count: size_t, + parents: *mut *const git_commit, + ) -> c_int; + pub fn git_commit_create_buffer( + out: *mut git_buf, + repo: *mut git_repository, + author: *const git_signature, + committer: *const git_signature, + message_encoding: *const c_char, + message: *const c_char, + tree: *const git_tree, + parent_count: size_t, + parents: *mut *const git_commit, + ) -> c_int; + pub fn git_commit_header_field( + out: *mut git_buf, + commit: *const git_commit, + field: *const c_char, + ) -> c_int; + pub fn git_annotated_commit_lookup( + out: *mut *mut git_annotated_commit, + repo: *mut git_repository, + id: *const git_oid, + ) -> c_int; + pub fn git_commit_create_with_signature( + id: *mut git_oid, + repo: *mut git_repository, + commit_content: *const c_char, + signature: *const c_char, + signature_field: *const c_char, + ) -> c_int; + pub fn git_commit_extract_signature( + signature: *mut git_buf, + signed_data: *mut git_buf, + repo: *mut git_repository, + commit_id: *mut git_oid, + field: *const c_char, + ) -> c_int; // branch - pub fn git_branch_create(out: *mut *mut git_reference, - repo: *mut git_repository, - branch_name: *const c_char, - target: *const git_commit, - force: c_int) -> c_int; + pub fn git_branch_create( + out: *mut *mut git_reference, + repo: *mut git_repository, + branch_name: *const c_char, + target: *const git_commit, + force: c_int, + ) -> c_int; + pub fn git_branch_create_from_annotated( + ref_out: *mut *mut git_reference, + repository: *mut git_repository, + branch_name: *const c_char, + commit: *const git_annotated_commit, + force: c_int, + ) -> c_int; pub fn git_branch_delete(branch: *mut git_reference) -> c_int; pub fn git_branch_is_head(branch: *const git_reference) -> c_int; pub fn git_branch_iterator_free(iter: *mut git_branch_iterator); - pub fn git_branch_iterator_new(iter: *mut *mut git_branch_iterator, - repo: *mut git_repository, - list_flags: git_branch_t) -> c_int; - pub fn git_branch_lookup(out: *mut *mut git_reference, - repo: *mut git_repository, - branch_name: *const c_char, - branch_type: git_branch_t) -> c_int; - pub fn git_branch_move(out: *mut *mut git_reference, - branch: *mut git_reference, - new_branch_name: *const c_char, - force: c_int) -> c_int; - pub fn git_branch_name(out: *mut *const c_char, - branch: *const git_reference) -> c_int; - pub fn git_branch_next(out: *mut *mut git_reference, - out_type: *mut git_branch_t, - iter: *mut git_branch_iterator) -> c_int; - pub fn git_branch_set_upstream(branch: *mut git_reference, - upstream_name: *const c_char) -> c_int; - pub fn git_branch_upstream(out: *mut *mut git_reference, - branch: *const git_reference) -> c_int; + pub fn git_branch_iterator_new( + iter: *mut *mut git_branch_iterator, + repo: *mut git_repository, + list_flags: git_branch_t, + ) -> c_int; + pub fn git_branch_lookup( + out: *mut *mut git_reference, + repo: *mut git_repository, + branch_name: *const c_char, + branch_type: git_branch_t, + ) -> c_int; + pub fn git_branch_move( + out: *mut *mut git_reference, + branch: *mut git_reference, + new_branch_name: *const c_char, + force: c_int, + ) -> c_int; + pub fn git_branch_name(out: *mut *const c_char, branch: *const git_reference) -> c_int; + pub fn git_branch_name_is_valid(valid: *mut c_int, name: *const c_char) -> c_int; + pub fn git_branch_remote_name( + out: *mut git_buf, + repo: *mut git_repository, + refname: *const c_char, + ) -> c_int; + pub fn git_branch_next( + out: *mut *mut git_reference, + out_type: *mut git_branch_t, + iter: *mut git_branch_iterator, + ) -> c_int; + pub fn git_branch_set_upstream( + branch: *mut git_reference, + upstream_name: *const c_char, + ) -> c_int; + pub fn git_branch_upstream(out: *mut *mut git_reference, branch: *const git_reference) + -> c_int; + pub fn git_branch_upstream_name( + out: *mut git_buf, + repo: *mut git_repository, + refname: *const c_char, + ) -> c_int; + pub fn git_branch_upstream_remote( + out: *mut git_buf, + repo: *mut git_repository, + refname: *const c_char, + ) -> c_int; + pub fn git_branch_upstream_merge( + out: *mut git_buf, + repo: *mut git_repository, + refname: *const c_char, + ) -> c_int; // index - pub fn git_index_add(index: *mut git_index, - entry: *const git_index_entry) -> c_int; - pub fn git_index_add_all(index: *mut git_index, - pathspec: *const git_strarray, - flags: c_uint, - callback: Option, - payload: *mut c_void) -> c_int; - pub fn git_index_add_bypath(index: *mut git_index, - path: *const c_char) -> c_int; - pub fn git_index_add_frombuffer(index: *mut git_index, - entry: *const git_index_entry, - buffer: *const c_void, - len: size_t) -> c_int; - pub fn git_index_conflict_add(index: *mut git_index, - ancestor_entry: *const git_index_entry, - our_entry: *const git_index_entry, - their_entry: *const git_index_entry) -> c_int; + pub fn git_index_version(index: *mut git_index) -> c_uint; + pub fn git_index_set_version(index: *mut git_index, version: c_uint) -> c_int; + pub fn git_index_add(index: *mut git_index, entry: *const git_index_entry) -> c_int; + pub fn git_index_add_all( + index: *mut git_index, + pathspec: *const git_strarray, + flags: c_uint, + callback: git_index_matched_path_cb, + payload: *mut c_void, + ) -> c_int; + pub fn git_index_add_bypath(index: *mut git_index, path: *const c_char) -> c_int; + pub fn git_index_add_frombuffer( + index: *mut git_index, + entry: *const git_index_entry, + buffer: *const c_void, + len: size_t, + ) -> c_int; + pub fn git_index_conflict_add( + index: *mut git_index, + ancestor_entry: *const git_index_entry, + our_entry: *const git_index_entry, + their_entry: *const git_index_entry, + ) -> c_int; + pub fn git_index_conflict_remove(index: *mut git_index, path: *const c_char) -> c_int; + pub fn git_index_conflict_get( + ancestor_out: *mut *const git_index_entry, + our_out: *mut *const git_index_entry, + their_out: *mut *const git_index_entry, + index: *mut git_index, + path: *const c_char, + ) -> c_int; + pub fn git_index_conflict_iterator_new( + iter: *mut *mut git_index_conflict_iterator, + index: *mut git_index, + ) -> c_int; + pub fn git_index_conflict_next( + ancestor_out: *mut *const git_index_entry, + our_out: *mut *const git_index_entry, + their_out: *mut *const git_index_entry, + iter: *mut git_index_conflict_iterator, + ) -> c_int; + pub fn git_index_conflict_iterator_free(iter: *mut git_index_conflict_iterator); pub fn git_index_clear(index: *mut git_index) -> c_int; pub fn git_index_entry_stage(entry: *const git_index_entry) -> c_int; pub fn git_index_entrycount(entry: *const git_index) -> size_t; - pub fn git_index_find(at_pos: *mut size_t, - index: *mut git_index, - path: *const c_char) -> c_int; + pub fn git_index_find(at_pos: *mut size_t, index: *mut git_index, path: *const c_char) + -> c_int; + pub fn git_index_find_prefix( + at_pos: *mut size_t, + index: *mut git_index, + prefix: *const c_char, + ) -> c_int; pub fn git_index_free(index: *mut git_index); - pub fn git_index_get_byindex(index: *mut git_index, - n: size_t) -> *const git_index_entry; - pub fn git_index_get_bypath(index: *mut git_index, - path: *const c_char, - stage: c_int) -> *const git_index_entry; + pub fn git_index_get_byindex(index: *mut git_index, n: size_t) -> *const git_index_entry; + pub fn git_index_get_bypath( + index: *mut git_index, + path: *const c_char, + stage: c_int, + ) -> *const git_index_entry; + pub fn git_index_has_conflicts(index: *const git_index) -> c_int; pub fn git_index_new(index: *mut *mut git_index) -> c_int; - pub fn git_index_open(index: *mut *mut git_index, - index_path: *const c_char) -> c_int; + pub fn git_index_open(index: *mut *mut git_index, index_path: *const c_char) -> c_int; pub fn git_index_path(index: *const git_index) -> *const c_char; pub fn git_index_read(index: *mut git_index, force: c_int) -> c_int; - pub fn git_index_read_tree(index: *mut git_index, - tree: *const git_tree) -> c_int; - pub fn git_index_remove(index: *mut git_index, - path: *const c_char, - stage: c_int) -> c_int; - pub fn git_index_remove_all(index: *mut git_index, - pathspec: *const git_strarray, - callback: Option, - payload: *mut c_void) -> c_int; - pub fn git_index_remove_bypath(index: *mut git_index, - path: *const c_char) -> c_int; - pub fn git_index_remove_directory(index: *mut git_index, - dir: *const c_char, - stage: c_int) -> c_int; - pub fn git_index_update_all(index: *mut git_index, - pathspec: *const git_strarray, - callback: Option, - payload: *mut c_void) -> c_int; + pub fn git_index_read_tree(index: *mut git_index, tree: *const git_tree) -> c_int; + pub fn git_index_remove(index: *mut git_index, path: *const c_char, stage: c_int) -> c_int; + pub fn git_index_remove_all( + index: *mut git_index, + pathspec: *const git_strarray, + callback: git_index_matched_path_cb, + payload: *mut c_void, + ) -> c_int; + pub fn git_index_remove_bypath(index: *mut git_index, path: *const c_char) -> c_int; + pub fn git_index_remove_directory( + index: *mut git_index, + dir: *const c_char, + stage: c_int, + ) -> c_int; + pub fn git_index_update_all( + index: *mut git_index, + pathspec: *const git_strarray, + callback: git_index_matched_path_cb, + payload: *mut c_void, + ) -> c_int; pub fn git_index_write(index: *mut git_index) -> c_int; - pub fn git_index_write_tree(out: *mut git_oid, - index: *mut git_index) -> c_int; - pub fn git_index_write_tree_to(out: *mut git_oid, - index: *mut git_index, - repo: *mut git_repository) -> c_int; + pub fn git_index_write_tree(out: *mut git_oid, index: *mut git_index) -> c_int; + pub fn git_index_write_tree_to( + out: *mut git_oid, + index: *mut git_index, + repo: *mut git_repository, + ) -> c_int; // config - pub fn git_config_add_file_ondisk(cfg: *mut git_config, - path: *const c_char, - level: git_config_level_t, - force: c_int) -> c_int; - pub fn git_config_delete_entry(cfg: *mut git_config, - name: *const c_char) -> c_int; - pub fn git_config_delete_multivar(cfg: *mut git_config, - name: *const c_char, - regexp: *const c_char) -> c_int; + pub fn git_config_add_file_ondisk( + cfg: *mut git_config, + path: *const c_char, + level: git_config_level_t, + repo: *const git_repository, + force: c_int, + ) -> c_int; + pub fn git_config_delete_entry(cfg: *mut git_config, name: *const c_char) -> c_int; + pub fn git_config_delete_multivar( + cfg: *mut git_config, + name: *const c_char, + regexp: *const c_char, + ) -> c_int; pub fn git_config_find_programdata(out: *mut git_buf) -> c_int; pub fn git_config_find_global(out: *mut git_buf) -> c_int; pub fn git_config_find_system(out: *mut git_buf) -> c_int; pub fn git_config_find_xdg(out: *mut git_buf) -> c_int; pub fn git_config_free(cfg: *mut git_config); - pub fn git_config_get_bool(out: *mut c_int, - cfg: *const git_config, - name: *const c_char) -> c_int; - pub fn git_config_get_entry(out: *mut *mut git_config_entry, - cfg: *const git_config, - name: *const c_char) -> c_int; - pub fn git_config_get_int32(out: *mut i32, - cfg: *const git_config, - name: *const c_char) -> c_int; - pub fn git_config_get_int64(out: *mut i64, - cfg: *const git_config, - name: *const c_char) -> c_int; - pub fn git_config_get_string(out: *mut *const c_char, - cfg: *const git_config, - name: *const c_char) -> c_int; - pub fn git_config_get_string_buf(out: *mut git_buf, - cfg: *const git_config, - name: *const c_char) -> c_int; - pub fn git_config_get_path(out: *mut git_buf, - cfg: *const git_config, - name: *const c_char) -> c_int; + pub fn git_config_get_bool( + out: *mut c_int, + cfg: *const git_config, + name: *const c_char, + ) -> c_int; + pub fn git_config_get_entry( + out: *mut *mut git_config_entry, + cfg: *const git_config, + name: *const c_char, + ) -> c_int; + pub fn git_config_get_int32( + out: *mut i32, + cfg: *const git_config, + name: *const c_char, + ) -> c_int; + pub fn git_config_get_int64( + out: *mut i64, + cfg: *const git_config, + name: *const c_char, + ) -> c_int; + pub fn git_config_get_string( + out: *mut *const c_char, + cfg: *const git_config, + name: *const c_char, + ) -> c_int; + pub fn git_config_get_string_buf( + out: *mut git_buf, + cfg: *const git_config, + name: *const c_char, + ) -> c_int; + pub fn git_config_get_path( + out: *mut git_buf, + cfg: *const git_config, + name: *const c_char, + ) -> c_int; pub fn git_config_iterator_free(iter: *mut git_config_iterator); - pub fn git_config_iterator_glob_new(out: *mut *mut git_config_iterator, - cfg: *const git_config, - regexp: *const c_char) -> c_int; - pub fn git_config_iterator_new(out: *mut *mut git_config_iterator, - cfg: *const git_config) -> c_int; + pub fn git_config_iterator_glob_new( + out: *mut *mut git_config_iterator, + cfg: *const git_config, + regexp: *const c_char, + ) -> c_int; + pub fn git_config_iterator_new( + out: *mut *mut git_config_iterator, + cfg: *const git_config, + ) -> c_int; pub fn git_config_new(out: *mut *mut git_config) -> c_int; - pub fn git_config_next(entry: *mut *mut git_config_entry, - iter: *mut git_config_iterator) -> c_int; + pub fn git_config_next( + entry: *mut *mut git_config_entry, + iter: *mut git_config_iterator, + ) -> c_int; pub fn git_config_open_default(out: *mut *mut git_config) -> c_int; - pub fn git_config_open_global(out: *mut *mut git_config, - config: *mut git_config) -> c_int; - pub fn git_config_open_level(out: *mut *mut git_config, - parent: *const git_config, - level: git_config_level_t) -> c_int; - pub fn git_config_open_ondisk(out: *mut *mut git_config, - path: *const c_char) -> c_int; - pub fn git_config_parse_bool(out: *mut c_int, - value: *const c_char) -> c_int; - pub fn git_config_parse_int32(out: *mut i32, - value: *const c_char) -> c_int; - pub fn git_config_parse_int64(out: *mut i64, - value: *const c_char) -> c_int; - pub fn git_config_set_bool(cfg: *mut git_config, - name: *const c_char, - value: c_int) -> c_int; - pub fn git_config_set_int32(cfg: *mut git_config, - name: *const c_char, - value: i32) -> c_int; - pub fn git_config_set_int64(cfg: *mut git_config, - name: *const c_char, - value: i64) -> c_int; - pub fn git_config_set_string(cfg: *mut git_config, - name: *const c_char, - value: *const c_char) -> c_int; - pub fn git_config_snapshot(out: *mut *mut git_config, - config: *mut git_config) -> c_int; + pub fn git_config_open_global(out: *mut *mut git_config, config: *mut git_config) -> c_int; + pub fn git_config_open_level( + out: *mut *mut git_config, + parent: *const git_config, + level: git_config_level_t, + ) -> c_int; + pub fn git_config_open_ondisk(out: *mut *mut git_config, path: *const c_char) -> c_int; + pub fn git_config_parse_bool(out: *mut c_int, value: *const c_char) -> c_int; + pub fn git_config_parse_int32(out: *mut i32, value: *const c_char) -> c_int; + pub fn git_config_parse_int64(out: *mut i64, value: *const c_char) -> c_int; + pub fn git_config_set_bool(cfg: *mut git_config, name: *const c_char, value: c_int) -> c_int; + pub fn git_config_set_int32(cfg: *mut git_config, name: *const c_char, value: i32) -> c_int; + pub fn git_config_set_int64(cfg: *mut git_config, name: *const c_char, value: i64) -> c_int; + pub fn git_config_set_multivar( + cfg: *mut git_config, + name: *const c_char, + regexp: *const c_char, + value: *const c_char, + ) -> c_int; + pub fn git_config_set_string( + cfg: *mut git_config, + name: *const c_char, + value: *const c_char, + ) -> c_int; + pub fn git_config_snapshot(out: *mut *mut git_config, config: *mut git_config) -> c_int; pub fn git_config_entry_free(entry: *mut git_config_entry); + pub fn git_config_multivar_iterator_new( + out: *mut *mut git_config_iterator, + cfg: *const git_config, + name: *const c_char, + regexp: *const c_char, + ) -> c_int; + + // attr + pub fn git_attr_get( + value_out: *mut *const c_char, + repo: *mut git_repository, + flags: u32, + path: *const c_char, + name: *const c_char, + ) -> c_int; + pub fn git_attr_value(value: *const c_char) -> git_attr_value_t; // cred pub fn git_cred_default_new(out: *mut *mut git_cred) -> c_int; pub fn git_cred_has_username(cred: *mut git_cred) -> c_int; - pub fn git_cred_ssh_custom_new(out: *mut *mut git_cred, - username: *const c_char, - publickey: *const c_char, - publickey_len: size_t, - sign_callback: git_cred_sign_callback, - payload: *mut c_void) -> c_int; - pub fn git_cred_ssh_interactive_new(out: *mut *mut git_cred, - username: *const c_char, - prompt_callback: git_cred_ssh_interactive_callback, - payload: *mut c_void) -> c_int; - pub fn git_cred_ssh_key_from_agent(out: *mut *mut git_cred, - username: *const c_char) -> c_int; - pub fn git_cred_ssh_key_new(out: *mut *mut git_cred, - username: *const c_char, - publickey: *const c_char, - privatekey: *const c_char, - passphrase: *const c_char) -> c_int; - pub fn git_cred_ssh_key_memory_new(out: *mut *mut git_cred, - username: *const c_char, - publickey: *const c_char, - privatekey: *const c_char, - passphrase: *const c_char) -> c_int; - pub fn git_cred_userpass(cred: *mut *mut git_cred, - url: *const c_char, - user_from_url: *const c_char, - allowed_types: c_uint, - payload: *mut c_void) -> c_int; - pub fn git_cred_userpass_plaintext_new(out: *mut *mut git_cred, - username: *const c_char, - password: *const c_char) -> c_int; - pub fn git_cred_username_new(cred: *mut *mut git_cred, - username: *const c_char) -> c_int; + pub fn git_cred_ssh_custom_new( + out: *mut *mut git_cred, + username: *const c_char, + publickey: *const c_char, + publickey_len: size_t, + sign_callback: git_cred_sign_callback, + payload: *mut c_void, + ) -> c_int; + pub fn git_cred_ssh_interactive_new( + out: *mut *mut git_cred, + username: *const c_char, + prompt_callback: git_cred_ssh_interactive_callback, + payload: *mut c_void, + ) -> c_int; + pub fn git_cred_ssh_key_from_agent(out: *mut *mut git_cred, username: *const c_char) -> c_int; + pub fn git_cred_ssh_key_new( + out: *mut *mut git_cred, + username: *const c_char, + publickey: *const c_char, + privatekey: *const c_char, + passphrase: *const c_char, + ) -> c_int; + pub fn git_cred_ssh_key_memory_new( + out: *mut *mut git_cred, + username: *const c_char, + publickey: *const c_char, + privatekey: *const c_char, + passphrase: *const c_char, + ) -> c_int; + pub fn git_cred_userpass( + cred: *mut *mut git_cred, + url: *const c_char, + user_from_url: *const c_char, + allowed_types: c_uint, + payload: *mut c_void, + ) -> c_int; + pub fn git_cred_userpass_plaintext_new( + out: *mut *mut git_cred, + username: *const c_char, + password: *const c_char, + ) -> c_int; + pub fn git_cred_username_new(cred: *mut *mut git_cred, username: *const c_char) -> c_int; // tags - pub fn git_tag_annotation_create(oid: *mut git_oid, - repo: *mut git_repository, - tag_name: *const c_char, - target: *const git_object, - tagger: *const git_signature, - message: *const c_char) -> c_int; - pub fn git_tag_create(oid: *mut git_oid, - repo: *mut git_repository, - tag_name: *const c_char, - target: *const git_object, - tagger: *const git_signature, - message: *const c_char, - force: c_int) -> c_int; - pub fn git_tag_create_frombuffer(oid: *mut git_oid, - repo: *mut git_repository, - buffer: *const c_char, - force: c_int) -> c_int; - pub fn git_tag_create_lightweight(oid: *mut git_oid, - repo: *mut git_repository, - tag_name: *const c_char, - target: *const git_object, - force: c_int) -> c_int; - pub fn git_tag_delete(repo: *mut git_repository, - tag_name: *const c_char) -> c_int; - pub fn git_tag_foreach(repo: *mut git_repository, - callback: git_tag_foreach_cb, - payload: *mut c_void) -> c_int; + pub fn git_tag_annotation_create( + oid: *mut git_oid, + repo: *mut git_repository, + tag_name: *const c_char, + target: *const git_object, + tagger: *const git_signature, + message: *const c_char, + ) -> c_int; + pub fn git_tag_create( + oid: *mut git_oid, + repo: *mut git_repository, + tag_name: *const c_char, + target: *const git_object, + tagger: *const git_signature, + message: *const c_char, + force: c_int, + ) -> c_int; + pub fn git_tag_create_frombuffer( + oid: *mut git_oid, + repo: *mut git_repository, + buffer: *const c_char, + force: c_int, + ) -> c_int; + pub fn git_tag_create_lightweight( + oid: *mut git_oid, + repo: *mut git_repository, + tag_name: *const c_char, + target: *const git_object, + force: c_int, + ) -> c_int; + pub fn git_tag_delete(repo: *mut git_repository, tag_name: *const c_char) -> c_int; + pub fn git_tag_foreach( + repo: *mut git_repository, + callback: git_tag_foreach_cb, + payload: *mut c_void, + ) -> c_int; pub fn git_tag_free(tag: *mut git_tag); pub fn git_tag_id(tag: *const git_tag) -> *const git_oid; - pub fn git_tag_list(tag_names: *mut git_strarray, - repo: *mut git_repository) -> c_int; - pub fn git_tag_list_match(tag_names: *mut git_strarray, - pattern: *const c_char, - repo: *mut git_repository) -> c_int; - pub fn git_tag_lookup(out: *mut *mut git_tag, - repo: *mut git_repository, - id: *const git_oid) -> c_int; - pub fn git_tag_lookup_prefix(out: *mut *mut git_tag, - repo: *mut git_repository, - id: *const git_oid, - len: size_t) -> c_int; + pub fn git_tag_list(tag_names: *mut git_strarray, repo: *mut git_repository) -> c_int; + pub fn git_tag_list_match( + tag_names: *mut git_strarray, + pattern: *const c_char, + repo: *mut git_repository, + ) -> c_int; + pub fn git_tag_lookup( + out: *mut *mut git_tag, + repo: *mut git_repository, + id: *const git_oid, + ) -> c_int; + pub fn git_tag_lookup_prefix( + out: *mut *mut git_tag, + repo: *mut git_repository, + id: *const git_oid, + len: size_t, + ) -> c_int; pub fn git_tag_message(tag: *const git_tag) -> *const c_char; pub fn git_tag_name(tag: *const git_tag) -> *const c_char; - pub fn git_tag_peel(tag_target_out: *mut *mut git_object, - tag: *const git_tag) -> c_int; + pub fn git_tag_peel(tag_target_out: *mut *mut git_object, tag: *const git_tag) -> c_int; pub fn git_tag_tagger(tag: *const git_tag) -> *const git_signature; - pub fn git_tag_target(target_out: *mut *mut git_object, - tag: *const git_tag) -> c_int; + pub fn git_tag_target(target_out: *mut *mut git_object, tag: *const git_tag) -> c_int; pub fn git_tag_target_id(tag: *const git_tag) -> *const git_oid; - pub fn git_tag_target_type(tag: *const git_tag) -> git_otype; + pub fn git_tag_target_type(tag: *const git_tag) -> git_object_t; + pub fn git_tag_name_is_valid(valid: *mut c_int, tag_name: *const c_char) -> c_int; // checkout - pub fn git_checkout_head(repo: *mut git_repository, - opts: *const git_checkout_options) -> c_int; - pub fn git_checkout_index(repo: *mut git_repository, - index: *mut git_index, - opts: *const git_checkout_options) -> c_int; - pub fn git_checkout_tree(repo: *mut git_repository, - treeish: *const git_object, - opts: *const git_checkout_options) -> c_int; - pub fn git_checkout_init_options(opts: *mut git_checkout_options, - version: c_uint) -> c_int; + pub fn git_checkout_head(repo: *mut git_repository, opts: *const git_checkout_options) + -> c_int; + pub fn git_checkout_index( + repo: *mut git_repository, + index: *mut git_index, + opts: *const git_checkout_options, + ) -> c_int; + pub fn git_checkout_tree( + repo: *mut git_repository, + treeish: *const git_object, + opts: *const git_checkout_options, + ) -> c_int; + pub fn git_checkout_init_options(opts: *mut git_checkout_options, version: c_uint) -> c_int; // merge - pub fn git_annotated_commit_id(commit: *const git_annotated_commit) - -> *const git_oid; - pub fn git_annotated_commit_from_ref(out: *mut *mut git_annotated_commit, - repo: *mut git_repository, - reference: *const git_reference) - -> c_int; + pub fn git_annotated_commit_id(commit: *const git_annotated_commit) -> *const git_oid; + pub fn git_annotated_commit_ref(commit: *const git_annotated_commit) -> *const c_char; + pub fn git_annotated_commit_from_ref( + out: *mut *mut git_annotated_commit, + repo: *mut git_repository, + reference: *const git_reference, + ) -> c_int; + pub fn git_annotated_commit_from_fetchhead( + out: *mut *mut git_annotated_commit, + repo: *mut git_repository, + branch_name: *const c_char, + remote_url: *const c_char, + oid: *const git_oid, + ) -> c_int; pub fn git_annotated_commit_free(commit: *mut git_annotated_commit); - pub fn git_merge_init_options(opts: *mut git_merge_options, - version: c_uint) -> c_int; - pub fn git_merge(repo: *mut git_repository, - their_heads: *mut *const git_annotated_commit, - len: size_t, - merge_opts: *const git_merge_options, - checkout_opts: *const git_checkout_options) -> c_int; - pub fn git_merge_commits(out: *mut *mut git_index, - repo: *mut git_repository, - our_commit: *const git_commit, - their_commit: *const git_commit, - opts: *const git_merge_options) -> c_int; + pub fn git_merge_init_options(opts: *mut git_merge_options, version: c_uint) -> c_int; + pub fn git_merge( + repo: *mut git_repository, + their_heads: *mut *const git_annotated_commit, + len: size_t, + merge_opts: *const git_merge_options, + checkout_opts: *const git_checkout_options, + ) -> c_int; + pub fn git_merge_commits( + out: *mut *mut git_index, + repo: *mut git_repository, + our_commit: *const git_commit, + their_commit: *const git_commit, + opts: *const git_merge_options, + ) -> c_int; + pub fn git_merge_trees( + out: *mut *mut git_index, + repo: *mut git_repository, + ancestor_tree: *const git_tree, + our_tree: *const git_tree, + their_tree: *const git_tree, + opts: *const git_merge_options, + ) -> c_int; + pub fn git_merge_file_options_init(opts: *mut git_merge_file_options, version: c_uint) + -> c_int; pub fn git_repository_state_cleanup(repo: *mut git_repository) -> c_int; + // merge analysis + + pub fn git_merge_analysis( + analysis_out: *mut git_merge_analysis_t, + pref_out: *mut git_merge_preference_t, + repo: *mut git_repository, + their_heads: *mut *const git_annotated_commit, + their_heads_len: usize, + ) -> c_int; + + pub fn git_merge_analysis_for_ref( + analysis_out: *mut git_merge_analysis_t, + pref_out: *mut git_merge_preference_t, + repo: *mut git_repository, + git_reference: *mut git_reference, + their_heads: *mut *const git_annotated_commit, + their_heads_len: usize, + ) -> c_int; + // notes pub fn git_note_author(note: *const git_note) -> *const git_signature; pub fn git_note_committer(note: *const git_note) -> *const git_signature; - pub fn git_note_create(out: *mut git_oid, - repo: *mut git_repository, - notes_ref: *const c_char, - author: *const git_signature, - committer: *const git_signature, - oid: *const git_oid, - note: *const c_char, - force: c_int) -> c_int; - pub fn git_note_default_ref(out: *mut git_buf, - repo: *mut git_repository) -> c_int; + pub fn git_note_create( + out: *mut git_oid, + repo: *mut git_repository, + notes_ref: *const c_char, + author: *const git_signature, + committer: *const git_signature, + oid: *const git_oid, + note: *const c_char, + force: c_int, + ) -> c_int; + pub fn git_note_default_ref(out: *mut git_buf, repo: *mut git_repository) -> c_int; pub fn git_note_free(note: *mut git_note); pub fn git_note_id(note: *const git_note) -> *const git_oid; pub fn git_note_iterator_free(it: *mut git_note_iterator); - pub fn git_note_iterator_new(out: *mut *mut git_note_iterator, - repo: *mut git_repository, - notes_ref: *const c_char) -> c_int; + pub fn git_note_iterator_new( + out: *mut *mut git_note_iterator, + repo: *mut git_repository, + notes_ref: *const c_char, + ) -> c_int; pub fn git_note_message(note: *const git_note) -> *const c_char; - pub fn git_note_next(note_id: *mut git_oid, - annotated_id: *mut git_oid, - it: *mut git_note_iterator) -> c_int; - pub fn git_note_read(out: *mut *mut git_note, - repo: *mut git_repository, - notes_ref: *const c_char, - oid: *const git_oid) -> c_int; - pub fn git_note_remove(repo: *mut git_repository, - notes_ref: *const c_char, - author: *const git_signature, - committer: *const git_signature, - oid: *const git_oid) -> c_int; + pub fn git_note_next( + note_id: *mut git_oid, + annotated_id: *mut git_oid, + it: *mut git_note_iterator, + ) -> c_int; + pub fn git_note_read( + out: *mut *mut git_note, + repo: *mut git_repository, + notes_ref: *const c_char, + oid: *const git_oid, + ) -> c_int; + pub fn git_note_remove( + repo: *mut git_repository, + notes_ref: *const c_char, + author: *const git_signature, + committer: *const git_signature, + oid: *const git_oid, + ) -> c_int; // blame - pub fn git_blame_file(out: *mut *mut git_blame, - repo: *mut git_repository, - path: *const c_char, - options: *mut git_blame_options) -> c_int; + pub fn git_blame_buffer( + out: *mut *mut git_blame, + reference: *mut git_blame, + buffer: *const c_char, + buffer_len: size_t, + ) -> c_int; + pub fn git_blame_file( + out: *mut *mut git_blame, + repo: *mut git_repository, + path: *const c_char, + options: *mut git_blame_options, + ) -> c_int; pub fn git_blame_free(blame: *mut git_blame); - pub fn git_blame_init_options(opts: *mut git_blame_options, - version: c_uint) -> c_int; + pub fn git_blame_init_options(opts: *mut git_blame_options, version: c_uint) -> c_int; pub fn git_blame_get_hunk_count(blame: *mut git_blame) -> u32; - pub fn git_blame_get_hunk_byline(blame: *mut git_blame, - lineno: usize) -> *const git_blame_hunk; - pub fn git_blame_get_hunk_byindex(blame: *mut git_blame, - index: u32) -> *const git_blame_hunk; + pub fn git_blame_get_hunk_byline(blame: *mut git_blame, lineno: usize) + -> *const git_blame_hunk; + pub fn git_blame_get_hunk_byindex(blame: *mut git_blame, index: u32) -> *const git_blame_hunk; // revwalk - pub fn git_revwalk_new(out: *mut *mut git_revwalk, - repo: *mut git_repository) -> c_int; + pub fn git_revwalk_new(out: *mut *mut git_revwalk, repo: *mut git_repository) -> c_int; pub fn git_revwalk_free(walk: *mut git_revwalk); - pub fn git_revwalk_reset(walk: *mut git_revwalk); + pub fn git_revwalk_reset(walk: *mut git_revwalk) -> c_int; - pub fn git_revwalk_sorting(walk: *mut git_revwalk, sort_mode: c_uint); + pub fn git_revwalk_sorting(walk: *mut git_revwalk, sort_mode: c_uint) -> c_int; pub fn git_revwalk_push_head(walk: *mut git_revwalk) -> c_int; - pub fn git_revwalk_push(walk: *mut git_revwalk, - oid: *const git_oid) -> c_int; - pub fn git_revwalk_push_ref(walk: *mut git_revwalk, - refname: *const c_char) -> c_int; - pub fn git_revwalk_push_glob(walk: *mut git_revwalk, - glob: *const c_char) -> c_int; - pub fn git_revwalk_push_range(walk: *mut git_revwalk, - range: *const c_char) -> c_int; - pub fn git_revwalk_simplify_first_parent(walk: *mut git_revwalk); + pub fn git_revwalk_push(walk: *mut git_revwalk, oid: *const git_oid) -> c_int; + pub fn git_revwalk_push_ref(walk: *mut git_revwalk, refname: *const c_char) -> c_int; + pub fn git_revwalk_push_glob(walk: *mut git_revwalk, glob: *const c_char) -> c_int; + pub fn git_revwalk_push_range(walk: *mut git_revwalk, range: *const c_char) -> c_int; + pub fn git_revwalk_simplify_first_parent(walk: *mut git_revwalk) -> c_int; pub fn git_revwalk_hide_head(walk: *mut git_revwalk) -> c_int; - pub fn git_revwalk_hide(walk: *mut git_revwalk, - oid: *const git_oid) -> c_int; - pub fn git_revwalk_hide_ref(walk: *mut git_revwalk, - refname: *const c_char) -> c_int; - pub fn git_revwalk_hide_glob(walk: *mut git_revwalk, - refname: *const c_char) -> c_int; + pub fn git_revwalk_hide(walk: *mut git_revwalk, oid: *const git_oid) -> c_int; + pub fn git_revwalk_hide_ref(walk: *mut git_revwalk, refname: *const c_char) -> c_int; + pub fn git_revwalk_hide_glob(walk: *mut git_revwalk, refname: *const c_char) -> c_int; + pub fn git_revwalk_add_hide_cb( + walk: *mut git_revwalk, + hide_cb: git_revwalk_hide_cb, + payload: *mut c_void, + ) -> c_int; pub fn git_revwalk_next(out: *mut git_oid, walk: *mut git_revwalk) -> c_int; // merge - pub fn git_merge_base(out: *mut git_oid, - repo: *mut git_repository, - one: *const git_oid, - two: *const git_oid) -> c_int; - - pub fn git_merge_bases(out: *mut git_oidarray, - repo: *mut git_repository, - one: *const git_oid, - two: *const git_oid) -> c_int; + pub fn git_merge_base( + out: *mut git_oid, + repo: *mut git_repository, + one: *const git_oid, + two: *const git_oid, + ) -> c_int; + + pub fn git_merge_base_many( + out: *mut git_oid, + repo: *mut git_repository, + length: size_t, + input_array: *const git_oid, + ) -> c_int; + + pub fn git_merge_base_octopus( + out: *mut git_oid, + repo: *mut git_repository, + length: size_t, + input_array: *const git_oid, + ) -> c_int; + + pub fn git_merge_bases( + out: *mut git_oidarray, + repo: *mut git_repository, + one: *const git_oid, + two: *const git_oid, + ) -> c_int; + + pub fn git_merge_bases_many( + out: *mut git_oidarray, + repo: *mut git_repository, + length: size_t, + input_array: *const git_oid, + ) -> c_int; + + pub fn git_merge_file_from_index( + out: *mut git_merge_file_result, + repo: *mut git_repository, + ancestor: *const git_index_entry, + ours: *const git_index_entry, + theirs: *const git_index_entry, + opts: *const git_merge_file_options, + ) -> c_int; + + pub fn git_merge_file_result_free(file_result: *mut git_merge_file_result); // pathspec pub fn git_pathspec_free(ps: *mut git_pathspec); - pub fn git_pathspec_match_diff(out: *mut *mut git_pathspec_match_list, - diff: *mut git_diff, - flags: u32, - ps: *mut git_pathspec) -> c_int; - pub fn git_pathspec_match_index(out: *mut *mut git_pathspec_match_list, - index: *mut git_index, - flags: u32, - ps: *mut git_pathspec) -> c_int; - pub fn git_pathspec_match_list_diff_entry(m: *const git_pathspec_match_list, - pos: size_t) -> *const git_diff_delta; - pub fn git_pathspec_match_list_entry(m: *const git_pathspec_match_list, - pos: size_t) -> *const c_char; - pub fn git_pathspec_match_list_entrycount(m: *const git_pathspec_match_list) - -> size_t; - pub fn git_pathspec_match_list_failed_entry(m: *const git_pathspec_match_list, - pos: size_t) -> *const c_char; - pub fn git_pathspec_match_list_failed_entrycount( - m: *const git_pathspec_match_list) -> size_t; + pub fn git_pathspec_match_diff( + out: *mut *mut git_pathspec_match_list, + diff: *mut git_diff, + flags: u32, + ps: *mut git_pathspec, + ) -> c_int; + pub fn git_pathspec_match_index( + out: *mut *mut git_pathspec_match_list, + index: *mut git_index, + flags: u32, + ps: *mut git_pathspec, + ) -> c_int; + pub fn git_pathspec_match_list_diff_entry( + m: *const git_pathspec_match_list, + pos: size_t, + ) -> *const git_diff_delta; + pub fn git_pathspec_match_list_entry( + m: *const git_pathspec_match_list, + pos: size_t, + ) -> *const c_char; + pub fn git_pathspec_match_list_entrycount(m: *const git_pathspec_match_list) -> size_t; + pub fn git_pathspec_match_list_failed_entry( + m: *const git_pathspec_match_list, + pos: size_t, + ) -> *const c_char; + pub fn git_pathspec_match_list_failed_entrycount(m: *const git_pathspec_match_list) -> size_t; pub fn git_pathspec_match_list_free(m: *mut git_pathspec_match_list); - pub fn git_pathspec_match_tree(out: *mut *mut git_pathspec_match_list, - tree: *mut git_tree, - flags: u32, - ps: *mut git_pathspec) -> c_int; - pub fn git_pathspec_match_workdir(out: *mut *mut git_pathspec_match_list, - repo: *mut git_repository, - flags: u32, - ps: *mut git_pathspec) -> c_int; - pub fn git_pathspec_matches_path(ps: *const git_pathspec, - flags: u32, - path: *const c_char) -> c_int; - pub fn git_pathspec_new(out: *mut *mut git_pathspec, - pathspec: *const git_strarray) -> c_int; + pub fn git_pathspec_match_tree( + out: *mut *mut git_pathspec_match_list, + tree: *mut git_tree, + flags: u32, + ps: *mut git_pathspec, + ) -> c_int; + pub fn git_pathspec_match_workdir( + out: *mut *mut git_pathspec_match_list, + repo: *mut git_repository, + flags: u32, + ps: *mut git_pathspec, + ) -> c_int; + pub fn git_pathspec_matches_path( + ps: *const git_pathspec, + flags: u32, + path: *const c_char, + ) -> c_int; + pub fn git_pathspec_new(out: *mut *mut git_pathspec, pathspec: *const git_strarray) -> c_int; // diff - pub fn git_diff_blob_to_buffer(old_blob: *const git_blob, - old_as_path: *const c_char, - buffer: *const c_char, - buffer_len: size_t, - buffer_as_path: *const c_char, - options: *const git_diff_options, - file_cb: git_diff_file_cb, - binary_cb: git_diff_binary_cb, - hunk_cb: git_diff_hunk_cb, - line_cb: git_diff_line_cb, - payload: *mut c_void) -> c_int; - pub fn git_diff_blobs(old_blob: *const git_blob, - old_as_path: *const c_char, - new_blob: *const git_blob, - new_as_path: *const c_char, - options: *const git_diff_options, - file_cb: git_diff_file_cb, - binary_cb: git_diff_binary_cb, - hunk_cb: git_diff_hunk_cb, - line_cb: git_diff_line_cb, - payload: *mut c_void) -> c_int; - pub fn git_diff_buffers(old_buffer: *const c_void, - old_len: size_t, - old_as_path: *const c_char, - new_buffer: *const c_void, - new_len: size_t, - new_as_path: *const c_char, - options: *const git_diff_options, - file_cb: git_diff_file_cb, - binary_cb: git_diff_binary_cb, - hunk_cb: git_diff_hunk_cb, - line_cb: git_diff_line_cb, - payload: *mut c_void) -> c_int; - pub fn git_diff_find_similar(diff: *mut git_diff, - options: *const git_diff_find_options) -> c_int; - pub fn git_diff_find_init_options(opts: *mut git_diff_find_options, - version: c_uint) -> c_int; - pub fn git_diff_foreach(diff: *mut git_diff, - file_cb: git_diff_file_cb, - binary_cb: Option, - hunk_cb: Option, - line_cb: Option, - payload: *mut c_void) -> c_int; + pub fn git_diff_blob_to_buffer( + old_blob: *const git_blob, + old_as_path: *const c_char, + buffer: *const c_char, + buffer_len: size_t, + buffer_as_path: *const c_char, + options: *const git_diff_options, + file_cb: git_diff_file_cb, + binary_cb: git_diff_binary_cb, + hunk_cb: git_diff_hunk_cb, + line_cb: git_diff_line_cb, + payload: *mut c_void, + ) -> c_int; + pub fn git_diff_blobs( + old_blob: *const git_blob, + old_as_path: *const c_char, + new_blob: *const git_blob, + new_as_path: *const c_char, + options: *const git_diff_options, + file_cb: git_diff_file_cb, + binary_cb: git_diff_binary_cb, + hunk_cb: git_diff_hunk_cb, + line_cb: git_diff_line_cb, + payload: *mut c_void, + ) -> c_int; + pub fn git_diff_buffers( + old_buffer: *const c_void, + old_len: size_t, + old_as_path: *const c_char, + new_buffer: *const c_void, + new_len: size_t, + new_as_path: *const c_char, + options: *const git_diff_options, + file_cb: git_diff_file_cb, + binary_cb: git_diff_binary_cb, + hunk_cb: git_diff_hunk_cb, + line_cb: git_diff_line_cb, + payload: *mut c_void, + ) -> c_int; + pub fn git_diff_from_buffer( + diff: *mut *mut git_diff, + content: *const c_char, + content_len: size_t, + ) -> c_int; + pub fn git_diff_find_similar( + diff: *mut git_diff, + options: *const git_diff_find_options, + ) -> c_int; + pub fn git_diff_find_init_options(opts: *mut git_diff_find_options, version: c_uint) -> c_int; + pub fn git_diff_foreach( + diff: *mut git_diff, + file_cb: git_diff_file_cb, + binary_cb: git_diff_binary_cb, + hunk_cb: git_diff_hunk_cb, + line_cb: git_diff_line_cb, + payload: *mut c_void, + ) -> c_int; pub fn git_diff_free(diff: *mut git_diff); - pub fn git_diff_get_delta(diff: *const git_diff, - idx: size_t) -> *const git_diff_delta; - pub fn git_diff_get_stats(out: *mut *mut git_diff_stats, - diff: *mut git_diff) -> c_int; - pub fn git_diff_index_to_index(diff: *mut *mut git_diff, - repo: *mut git_repository, - old_index: *mut git_index, - new_index: *mut git_index, - opts: *const git_diff_options) -> c_int; - pub fn git_diff_index_to_workdir(diff: *mut *mut git_diff, - repo: *mut git_repository, - index: *mut git_index, - opts: *const git_diff_options) -> c_int; - pub fn git_diff_init_options(opts: *mut git_diff_options, - version: c_uint) -> c_int; + pub fn git_diff_get_delta(diff: *const git_diff, idx: size_t) -> *const git_diff_delta; + pub fn git_diff_get_stats(out: *mut *mut git_diff_stats, diff: *mut git_diff) -> c_int; + pub fn git_diff_index_to_index( + diff: *mut *mut git_diff, + repo: *mut git_repository, + old_index: *mut git_index, + new_index: *mut git_index, + opts: *const git_diff_options, + ) -> c_int; + pub fn git_diff_index_to_workdir( + diff: *mut *mut git_diff, + repo: *mut git_repository, + index: *mut git_index, + opts: *const git_diff_options, + ) -> c_int; + pub fn git_diff_init_options(opts: *mut git_diff_options, version: c_uint) -> c_int; pub fn git_diff_is_sorted_icase(diff: *const git_diff) -> c_int; - pub fn git_diff_merge(onto: *mut git_diff, - from: *const git_diff) -> c_int; + pub fn git_diff_merge(onto: *mut git_diff, from: *const git_diff) -> c_int; pub fn git_diff_num_deltas(diff: *const git_diff) -> size_t; - pub fn git_diff_num_deltas_of_type(diff: *const git_diff, - delta: git_delta_t) -> size_t; - pub fn git_diff_print(diff: *mut git_diff, - format: git_diff_format_t, - print_cb: git_diff_line_cb, - payload: *mut c_void) -> c_int; + pub fn git_diff_num_deltas_of_type(diff: *const git_diff, delta: git_delta_t) -> size_t; + pub fn git_diff_print( + diff: *mut git_diff, + format: git_diff_format_t, + print_cb: git_diff_line_cb, + payload: *mut c_void, + ) -> c_int; pub fn git_diff_stats_deletions(stats: *const git_diff_stats) -> size_t; pub fn git_diff_stats_files_changed(stats: *const git_diff_stats) -> size_t; pub fn git_diff_stats_free(stats: *mut git_diff_stats); pub fn git_diff_stats_insertions(stats: *const git_diff_stats) -> size_t; - pub fn git_diff_stats_to_buf(out: *mut git_buf, - stats: *const git_diff_stats, - format: git_diff_stats_format_t, - width: size_t) -> c_int; + pub fn git_diff_stats_to_buf( + out: *mut git_buf, + stats: *const git_diff_stats, + format: git_diff_stats_format_t, + width: size_t, + ) -> c_int; pub fn git_diff_status_char(status: git_delta_t) -> c_char; - pub fn git_diff_tree_to_index(diff: *mut *mut git_diff, - repo: *mut git_repository, - old_tree: *mut git_tree, - index: *mut git_index, - opts: *const git_diff_options) -> c_int; - pub fn git_diff_tree_to_tree(diff: *mut *mut git_diff, - repo: *mut git_repository, - old_tree: *mut git_tree, - new_tree: *mut git_tree, - opts: *const git_diff_options) -> c_int; - pub fn git_diff_tree_to_workdir(diff: *mut *mut git_diff, - repo: *mut git_repository, - old_tree: *mut git_tree, - opts: *const git_diff_options) -> c_int; - pub fn git_diff_tree_to_workdir_with_index(diff: *mut *mut git_diff, - repo: *mut git_repository, - old_tree: *mut git_tree, - opts: *const git_diff_options) - -> c_int; - - pub fn git_graph_ahead_behind(ahead: *mut size_t, behind: *mut size_t, - repo: *mut git_repository, - local: *const git_oid, upstream: *const git_oid) - -> c_int; - - pub fn git_graph_descendant_of(repo: *mut git_repository, - commit: *const git_oid, ancestor: *const git_oid) - -> c_int; + pub fn git_diff_tree_to_index( + diff: *mut *mut git_diff, + repo: *mut git_repository, + old_tree: *mut git_tree, + index: *mut git_index, + opts: *const git_diff_options, + ) -> c_int; + pub fn git_diff_tree_to_tree( + diff: *mut *mut git_diff, + repo: *mut git_repository, + old_tree: *mut git_tree, + new_tree: *mut git_tree, + opts: *const git_diff_options, + ) -> c_int; + pub fn git_diff_tree_to_workdir( + diff: *mut *mut git_diff, + repo: *mut git_repository, + old_tree: *mut git_tree, + opts: *const git_diff_options, + ) -> c_int; + pub fn git_diff_tree_to_workdir_with_index( + diff: *mut *mut git_diff, + repo: *mut git_repository, + old_tree: *mut git_tree, + opts: *const git_diff_options, + ) -> c_int; + + pub fn git_graph_ahead_behind( + ahead: *mut size_t, + behind: *mut size_t, + repo: *mut git_repository, + local: *const git_oid, + upstream: *const git_oid, + ) -> c_int; + + pub fn git_graph_descendant_of( + repo: *mut git_repository, + commit: *const git_oid, + ancestor: *const git_oid, + ) -> c_int; + + pub fn git_diff_format_email( + out: *mut git_buf, + diff: *mut git_diff, + opts: *const git_diff_format_email_options, + ) -> c_int; + pub fn git_diff_format_email_options_init( + opts: *mut git_diff_format_email_options, + version: c_uint, + ) -> c_int; + + pub fn git_diff_patchid( + out: *mut git_oid, + diff: *mut git_diff, + opts: *mut git_diff_patchid_options, + ) -> c_int; + pub fn git_diff_patchid_options_init( + opts: *mut git_diff_patchid_options, + version: c_uint, + ) -> c_int; + + // patch + pub fn git_patch_from_diff(out: *mut *mut git_patch, diff: *mut git_diff, idx: size_t) + -> c_int; + pub fn git_patch_from_blobs( + out: *mut *mut git_patch, + old_blob: *const git_blob, + old_as_path: *const c_char, + new_blob: *const git_blob, + new_as_path: *const c_char, + opts: *const git_diff_options, + ) -> c_int; + pub fn git_patch_from_blob_and_buffer( + out: *mut *mut git_patch, + old_blob: *const git_blob, + old_as_path: *const c_char, + buffer: *const c_void, + buffer_len: size_t, + buffer_as_path: *const c_char, + opts: *const git_diff_options, + ) -> c_int; + pub fn git_patch_from_buffers( + out: *mut *mut git_patch, + old_buffer: *const c_void, + old_len: size_t, + old_as_path: *const c_char, + new_buffer: *const c_void, + new_len: size_t, + new_as_path: *const c_char, + opts: *const git_diff_options, + ) -> c_int; + pub fn git_patch_free(patch: *mut git_patch); + pub fn git_patch_get_delta(patch: *const git_patch) -> *const git_diff_delta; + pub fn git_patch_num_hunks(patch: *const git_patch) -> size_t; + pub fn git_patch_line_stats( + total_context: *mut size_t, + total_additions: *mut size_t, + total_deletions: *mut size_t, + patch: *const git_patch, + ) -> c_int; + pub fn git_patch_get_hunk( + out: *mut *const git_diff_hunk, + lines_in_hunk: *mut size_t, + patch: *mut git_patch, + hunk_idx: size_t, + ) -> c_int; + pub fn git_patch_num_lines_in_hunk(patch: *const git_patch, hunk_idx: size_t) -> c_int; + pub fn git_patch_get_line_in_hunk( + out: *mut *const git_diff_line, + patch: *mut git_patch, + hunk_idx: size_t, + line_of_hunk: size_t, + ) -> c_int; + pub fn git_patch_size( + patch: *mut git_patch, + include_context: c_int, + include_hunk_headers: c_int, + include_file_headers: c_int, + ) -> size_t; + pub fn git_patch_print( + patch: *mut git_patch, + print_cb: git_diff_line_cb, + payload: *mut c_void, + ) -> c_int; + pub fn git_patch_to_buf(buf: *mut git_buf, patch: *mut git_patch) -> c_int; // reflog - pub fn git_reflog_append(reflog: *mut git_reflog, - id: *const git_oid, - committer: *const git_signature, - msg: *const c_char) -> c_int; - pub fn git_reflog_delete(repo: *mut git_repository, - name: *const c_char) -> c_int; - pub fn git_reflog_drop(reflog: *mut git_reflog, - idx: size_t, - rewrite_previous_entry: c_int) -> c_int; - pub fn git_reflog_entry_byindex(reflog: *const git_reflog, - idx: size_t) -> *const git_reflog_entry; - pub fn git_reflog_entry_committer(entry: *const git_reflog_entry) - -> *const git_signature; - pub fn git_reflog_entry_id_new(entry: *const git_reflog_entry) - -> *const git_oid; - pub fn git_reflog_entry_id_old(entry: *const git_reflog_entry) - -> *const git_oid; - pub fn git_reflog_entry_message(entry: *const git_reflog_entry) - -> *const c_char; + pub fn git_reflog_append( + reflog: *mut git_reflog, + id: *const git_oid, + committer: *const git_signature, + msg: *const c_char, + ) -> c_int; + pub fn git_reflog_delete(repo: *mut git_repository, name: *const c_char) -> c_int; + pub fn git_reflog_drop( + reflog: *mut git_reflog, + idx: size_t, + rewrite_previous_entry: c_int, + ) -> c_int; + pub fn git_reflog_entry_byindex( + reflog: *const git_reflog, + idx: size_t, + ) -> *const git_reflog_entry; + pub fn git_reflog_entry_committer(entry: *const git_reflog_entry) -> *const git_signature; + pub fn git_reflog_entry_id_new(entry: *const git_reflog_entry) -> *const git_oid; + pub fn git_reflog_entry_id_old(entry: *const git_reflog_entry) -> *const git_oid; + pub fn git_reflog_entry_message(entry: *const git_reflog_entry) -> *const c_char; pub fn git_reflog_entrycount(reflog: *mut git_reflog) -> size_t; pub fn git_reflog_free(reflog: *mut git_reflog); - pub fn git_reflog_read(out: *mut *mut git_reflog, - repo: *mut git_repository, - name: *const c_char) -> c_int; - pub fn git_reflog_rename(repo: *mut git_repository, - old_name: *const c_char, - name: *const c_char) -> c_int; + pub fn git_reflog_read( + out: *mut *mut git_reflog, + repo: *mut git_repository, + name: *const c_char, + ) -> c_int; + pub fn git_reflog_rename( + repo: *mut git_repository, + old_name: *const c_char, + name: *const c_char, + ) -> c_int; pub fn git_reflog_write(reflog: *mut git_reflog) -> c_int; // transport - pub fn git_transport_register(prefix: *const c_char, - cb: git_transport_cb, - param: *mut c_void) -> c_int; + pub fn git_transport_register( + prefix: *const c_char, + cb: git_transport_cb, + param: *mut c_void, + ) -> c_int; pub fn git_transport_unregister(prefix: *const c_char) -> c_int; - pub fn git_transport_smart(out: *mut *mut git_transport, - owner: *mut git_remote, - payload: *mut c_void) -> c_int; + pub fn git_transport_smart( + out: *mut *mut git_transport, + owner: *mut git_remote, + payload: *mut c_void, + ) -> c_int; // describe - pub fn git_describe_commit(result: *mut *mut git_describe_result, - object: *mut git_object, - opts: *mut git_describe_options) -> c_int; - pub fn git_describe_format(buf: *mut git_buf, - result: *const git_describe_result, - opts: *const git_describe_format_options) -> c_int; + pub fn git_describe_commit( + result: *mut *mut git_describe_result, + object: *mut git_object, + opts: *mut git_describe_options, + ) -> c_int; + pub fn git_describe_format( + buf: *mut git_buf, + result: *const git_describe_result, + opts: *const git_describe_format_options, + ) -> c_int; pub fn git_describe_result_free(result: *mut git_describe_result); - pub fn git_describe_workdir(out: *mut *mut git_describe_result, - repo: *mut git_repository, - opts: *mut git_describe_options) -> c_int; + pub fn git_describe_workdir( + out: *mut *mut git_describe_result, + repo: *mut git_repository, + opts: *mut git_describe_options, + ) -> c_int; // message - pub fn git_message_prettify(out: *mut git_buf, - message: *const c_char, - strip_comments: c_int, - comment_char: c_char) -> c_int; + pub fn git_message_prettify( + out: *mut git_buf, + message: *const c_char, + strip_comments: c_int, + comment_char: c_char, + ) -> c_int; + + pub fn git_message_trailers( + out: *mut git_message_trailer_array, + message: *const c_char, + ) -> c_int; + + pub fn git_message_trailer_array_free(trailer: *mut git_message_trailer_array); // packbuilder - pub fn git_packbuilder_new(out: *mut *mut git_packbuilder, - repo: *mut git_repository) -> c_int; - pub fn git_packbuilder_set_threads(pb: *mut git_packbuilder, - n: c_uint) -> c_uint; - pub fn git_packbuilder_insert(pb: *mut git_packbuilder, - id: *const git_oid, - name: *const c_char) -> c_int; - pub fn git_packbuilder_insert_tree(pb: *mut git_packbuilder, - id: *const git_oid) -> c_int; - pub fn git_packbuilder_insert_commit(pb: *mut git_packbuilder, - id: *const git_oid) -> c_int; - pub fn git_packbuilder_insert_walk(pb: *mut git_packbuilder, - walk: *mut git_revwalk) -> c_int; - pub fn git_packbuilder_insert_recur(pb: *mut git_packbuilder, - id: *const git_oid, - name: *const c_char) -> c_int; - pub fn git_packbuilder_write_buf(buf: *mut git_buf, - pb: *mut git_packbuilder) -> c_int; - pub fn git_packbuilder_write(pb: *mut git_packbuilder, - path: *const c_char, - mode: c_uint, - progress_cb: Option, - progress_cb_payload: *mut c_void) -> c_int; + pub fn git_packbuilder_new(out: *mut *mut git_packbuilder, repo: *mut git_repository) -> c_int; + pub fn git_packbuilder_set_threads(pb: *mut git_packbuilder, n: c_uint) -> c_uint; + pub fn git_packbuilder_insert( + pb: *mut git_packbuilder, + id: *const git_oid, + name: *const c_char, + ) -> c_int; + pub fn git_packbuilder_insert_tree(pb: *mut git_packbuilder, id: *const git_oid) -> c_int; + pub fn git_packbuilder_insert_commit(pb: *mut git_packbuilder, id: *const git_oid) -> c_int; + pub fn git_packbuilder_insert_walk(pb: *mut git_packbuilder, walk: *mut git_revwalk) -> c_int; + pub fn git_packbuilder_insert_recur( + pb: *mut git_packbuilder, + id: *const git_oid, + name: *const c_char, + ) -> c_int; + pub fn git_packbuilder_write_buf(buf: *mut git_buf, pb: *mut git_packbuilder) -> c_int; + pub fn git_packbuilder_write( + pb: *mut git_packbuilder, + path: *const c_char, + mode: c_uint, + progress_cb: git_indexer_progress_cb, + progress_cb_payload: *mut c_void, + ) -> c_int; + #[deprecated = "use `git_packbuilder_name` to retrieve the filename"] pub fn git_packbuilder_hash(pb: *mut git_packbuilder) -> *const git_oid; - pub fn git_packbuilder_foreach(pb: *mut git_packbuilder, - cb: git_packbuilder_foreach_cb, - payload: *mut c_void) -> c_int; - pub fn git_packbuilder_object_count(pb: *mut git_packbuilder) -> u32; - pub fn git_packbuilder_written(pb: *mut git_packbuilder) -> u32; - pub fn git_packbuilder_set_callbacks(pb: *mut git_packbuilder, - progress_cb: Option, - progress_cb_payload: *mut c_void) -> c_int; + pub fn git_packbuilder_name(pb: *mut git_packbuilder) -> *const c_char; + pub fn git_packbuilder_foreach( + pb: *mut git_packbuilder, + cb: git_packbuilder_foreach_cb, + payload: *mut c_void, + ) -> c_int; + pub fn git_packbuilder_object_count(pb: *mut git_packbuilder) -> size_t; + pub fn git_packbuilder_written(pb: *mut git_packbuilder) -> size_t; + pub fn git_packbuilder_set_callbacks( + pb: *mut git_packbuilder, + progress_cb: git_packbuilder_progress, + progress_cb_payload: *mut c_void, + ) -> c_int; pub fn git_packbuilder_free(pb: *mut git_packbuilder); + + // indexer + pub fn git_indexer_new( + out: *mut *mut git_indexer, + path: *const c_char, + mode: c_uint, + odb: *mut git_odb, + opts: *mut git_indexer_options, + ) -> c_int; + pub fn git_indexer_append( + idx: *mut git_indexer, + data: *const c_void, + size: size_t, + stats: *mut git_indexer_progress, + ) -> c_int; + pub fn git_indexer_commit(idx: *mut git_indexer, stats: *mut git_indexer_progress) -> c_int; + #[deprecated = "use `git_indexer_name` to retrieve the filename"] + pub fn git_indexer_hash(idx: *const git_indexer) -> *const git_oid; + pub fn git_indexer_name(idx: *const git_indexer) -> *const c_char; + pub fn git_indexer_free(idx: *mut git_indexer); + + pub fn git_indexer_options_init(opts: *mut git_indexer_options, version: c_uint) -> c_int; + + // odb + pub fn git_repository_odb(out: *mut *mut git_odb, repo: *mut git_repository) -> c_int; + pub fn git_odb_new(db: *mut *mut git_odb) -> c_int; + pub fn git_odb_free(db: *mut git_odb); + pub fn git_odb_open_rstream( + out: *mut *mut git_odb_stream, + len: *mut size_t, + otype: *mut git_object_t, + db: *mut git_odb, + oid: *const git_oid, + ) -> c_int; + pub fn git_odb_stream_read( + stream: *mut git_odb_stream, + buffer: *mut c_char, + len: size_t, + ) -> c_int; + pub fn git_odb_open_wstream( + out: *mut *mut git_odb_stream, + db: *mut git_odb, + size: git_object_size_t, + obj_type: git_object_t, + ) -> c_int; + pub fn git_odb_stream_write( + stream: *mut git_odb_stream, + buffer: *const c_char, + len: size_t, + ) -> c_int; + pub fn git_odb_stream_finalize_write(id: *mut git_oid, stream: *mut git_odb_stream) -> c_int; + pub fn git_odb_stream_free(stream: *mut git_odb_stream); + pub fn git_odb_foreach(db: *mut git_odb, cb: git_odb_foreach_cb, payload: *mut c_void) + -> c_int; + + pub fn git_odb_read( + out: *mut *mut git_odb_object, + odb: *mut git_odb, + oid: *const git_oid, + ) -> c_int; + + pub fn git_odb_read_header( + len_out: *mut size_t, + type_out: *mut git_object_t, + odb: *mut git_odb, + oid: *const git_oid, + ) -> c_int; + + pub fn git_odb_write( + out: *mut git_oid, + odb: *mut git_odb, + data: *const c_void, + len: size_t, + otype: git_object_t, + ) -> c_int; + + pub fn git_odb_write_pack( + out: *mut *mut git_odb_writepack, + odb: *mut git_odb, + progress_cb: git_indexer_progress_cb, + progress_payload: *mut c_void, + ) -> c_int; + + pub fn git_odb_hash( + out: *mut git_oid, + data: *const c_void, + len: size_t, + otype: git_object_t, + ) -> c_int; + + pub fn git_odb_hashfile(out: *mut git_oid, path: *const c_char, otype: git_object_t) -> c_int; + + pub fn git_odb_exists_prefix( + out: *mut git_oid, + odb: *mut git_odb, + short_oid: *const git_oid, + len: size_t, + ) -> c_int; + + pub fn git_odb_exists(odb: *mut git_odb, oid: *const git_oid) -> c_int; + pub fn git_odb_exists_ext(odb: *mut git_odb, oid: *const git_oid, flags: c_uint) -> c_int; + + pub fn git_odb_refresh(odb: *mut git_odb) -> c_int; + + pub fn git_odb_object_id(obj: *mut git_odb_object) -> *const git_oid; + pub fn git_odb_object_size(obj: *mut git_odb_object) -> size_t; + pub fn git_odb_object_type(obj: *mut git_odb_object) -> git_object_t; + pub fn git_odb_object_data(obj: *mut git_odb_object) -> *const c_void; + pub fn git_odb_object_dup(out: *mut *mut git_odb_object, obj: *mut git_odb_object) -> c_int; + pub fn git_odb_object_free(obj: *mut git_odb_object); + + pub fn git_odb_init_backend(odb: *mut git_odb_backend, version: c_uint) -> c_int; + + pub fn git_odb_add_backend( + odb: *mut git_odb, + backend: *mut git_odb_backend, + priority: c_int, + ) -> c_int; + + pub fn git_odb_backend_pack( + out: *mut *mut git_odb_backend, + objects_dir: *const c_char, + ) -> c_int; + + pub fn git_odb_backend_one_pack( + out: *mut *mut git_odb_backend, + objects_dir: *const c_char, + ) -> c_int; + + pub fn git_odb_add_disk_alternate(odb: *mut git_odb, path: *const c_char) -> c_int; + + pub fn git_odb_backend_loose( + out: *mut *mut git_odb_backend, + objects_dir: *const c_char, + compression_level: c_int, + do_fsync: c_int, + dir_mode: c_uint, + file_mode: c_uint, + ) -> c_int; + + pub fn git_odb_add_alternate( + odb: *mut git_odb, + backend: *mut git_odb_backend, + priority: c_int, + ) -> c_int; + + #[deprecated(note = "only kept for compatibility; prefer git_odb_backend_data_alloc")] + pub fn git_odb_backend_malloc(backend: *mut git_odb_backend, len: size_t) -> *mut c_void; + + pub fn git_odb_backend_data_alloc(backend: *mut git_odb_backend, len: size_t) -> *mut c_void; + pub fn git_odb_backend_data_free(backend: *mut git_odb_backend, data: *mut c_void); + + pub fn git_odb_num_backends(odb: *mut git_odb) -> size_t; + pub fn git_odb_get_backend( + backend: *mut *mut git_odb_backend, + odb: *mut git_odb, + position: size_t, + ) -> c_int; + + // mempack + pub fn git_mempack_new(out: *mut *mut git_odb_backend) -> c_int; + pub fn git_mempack_reset(backend: *mut git_odb_backend) -> c_int; + pub fn git_mempack_dump( + pack: *mut git_buf, + repo: *mut git_repository, + backend: *mut git_odb_backend, + ) -> c_int; + + // refdb + pub fn git_refdb_new(out: *mut *mut git_refdb, repo: *mut git_repository) -> c_int; + pub fn git_refdb_open(out: *mut *mut git_refdb, repo: *mut git_repository) -> c_int; + pub fn git_refdb_backend_fs( + out: *mut *mut git_refdb_backend, + repo: *mut git_repository, + ) -> c_int; + pub fn git_refdb_init_backend(backend: *mut git_refdb_backend, version: c_uint) -> c_int; + pub fn git_refdb_set_backend(refdb: *mut git_refdb, backend: *mut git_refdb_backend) -> c_int; + pub fn git_refdb_compress(refdb: *mut git_refdb) -> c_int; + pub fn git_refdb_free(refdb: *mut git_refdb); + + // rebase + pub fn git_rebase_init_options(opts: *mut git_rebase_options, version: c_uint) -> c_int; + pub fn git_rebase_init( + out: *mut *mut git_rebase, + repo: *mut git_repository, + branch: *const git_annotated_commit, + upstream: *const git_annotated_commit, + onto: *const git_annotated_commit, + opts: *const git_rebase_options, + ) -> c_int; + pub fn git_rebase_open( + out: *mut *mut git_rebase, + repo: *mut git_repository, + opts: *const git_rebase_options, + ) -> c_int; + pub fn git_rebase_operation_entrycount(rebase: *mut git_rebase) -> size_t; + pub fn git_rebase_operation_current(rebase: *mut git_rebase) -> size_t; + pub fn git_rebase_operation_byindex( + rebase: *mut git_rebase, + idx: size_t, + ) -> *mut git_rebase_operation; + pub fn git_rebase_orig_head_id(rebase: *mut git_rebase) -> *const git_oid; + pub fn git_rebase_orig_head_name(rebase: *mut git_rebase) -> *const c_char; + pub fn git_rebase_next( + operation: *mut *mut git_rebase_operation, + rebase: *mut git_rebase, + ) -> c_int; + pub fn git_rebase_inmemory_index(index: *mut *mut git_index, rebase: *mut git_rebase) -> c_int; + pub fn git_rebase_commit( + id: *mut git_oid, + rebase: *mut git_rebase, + author: *const git_signature, + committer: *const git_signature, + message_encoding: *const c_char, + message: *const c_char, + ) -> c_int; + pub fn git_rebase_abort(rebase: *mut git_rebase) -> c_int; + pub fn git_rebase_finish(rebase: *mut git_rebase, signature: *const git_signature) -> c_int; + pub fn git_rebase_free(rebase: *mut git_rebase); + + // cherrypick + pub fn git_cherrypick_init_options(opts: *mut git_cherrypick_options, version: c_uint) + -> c_int; + pub fn git_cherrypick( + repo: *mut git_repository, + commit: *mut git_commit, + options: *const git_cherrypick_options, + ) -> c_int; + pub fn git_cherrypick_commit( + out: *mut *mut git_index, + repo: *mut git_repository, + cherrypick_commit: *mut git_commit, + our_commit: *mut git_commit, + mainline: c_uint, + merge_options: *const git_merge_options, + ) -> c_int; + + // apply + pub fn git_apply_options_init(opts: *mut git_apply_options, version: c_uint) -> c_int; + pub fn git_apply_to_tree( + out: *mut *mut git_index, + repo: *mut git_repository, + preimage: *mut git_tree, + diff: *mut git_diff, + options: *const git_apply_options, + ) -> c_int; + pub fn git_apply( + repo: *mut git_repository, + diff: *mut git_diff, + location: git_apply_location_t, + options: *const git_apply_options, + ) -> c_int; + + // revert + pub fn git_revert_options_init(opts: *mut git_revert_options, version: c_uint) -> c_int; + pub fn git_revert_commit( + out: *mut *mut git_index, + repo: *mut git_repository, + revert_commit: *mut git_commit, + our_commit: *mut git_commit, + mainline: c_uint, + merge_options: *const git_merge_options, + ) -> c_int; + pub fn git_revert( + repo: *mut git_repository, + commit: *mut git_commit, + given_opts: *const git_revert_options, + ) -> c_int; + + // Common + pub fn git_libgit2_version(major: *mut c_int, minor: *mut c_int, rev: *mut c_int) -> c_int; + pub fn git_libgit2_features() -> c_int; + pub fn git_libgit2_opts(option: c_int, ...) -> c_int; + + // Worktrees + pub fn git_worktree_list(out: *mut git_strarray, repo: *mut git_repository) -> c_int; + pub fn git_worktree_lookup( + out: *mut *mut git_worktree, + repo: *mut git_repository, + name: *const c_char, + ) -> c_int; + pub fn git_worktree_open_from_repository( + out: *mut *mut git_worktree, + repo: *mut git_repository, + ) -> c_int; + pub fn git_worktree_free(wt: *mut git_worktree); + pub fn git_worktree_validate(wt: *const git_worktree) -> c_int; + pub fn git_worktree_add_options_init( + opts: *mut git_worktree_add_options, + version: c_uint, + ) -> c_int; + pub fn git_worktree_add( + out: *mut *mut git_worktree, + repo: *mut git_repository, + name: *const c_char, + path: *const c_char, + opts: *const git_worktree_add_options, + ) -> c_int; + pub fn git_worktree_lock(wt: *mut git_worktree, reason: *const c_char) -> c_int; + pub fn git_worktree_unlock(wt: *mut git_worktree) -> c_int; + pub fn git_worktree_is_locked(reason: *mut git_buf, wt: *const git_worktree) -> c_int; + pub fn git_worktree_name(wt: *const git_worktree) -> *const c_char; + pub fn git_worktree_path(wt: *const git_worktree) -> *const c_char; + pub fn git_worktree_prune_options_init( + opts: *mut git_worktree_prune_options, + version: c_uint, + ) -> c_int; + pub fn git_worktree_is_prunable( + wt: *mut git_worktree, + opts: *mut git_worktree_prune_options, + ) -> c_int; + pub fn git_worktree_prune( + wt: *mut git_worktree, + opts: *mut git_worktree_prune_options, + ) -> c_int; + + // Ref transactions + pub fn git_transaction_new(out: *mut *mut git_transaction, repo: *mut git_repository) -> c_int; + pub fn git_transaction_lock_ref(tx: *mut git_transaction, refname: *const c_char) -> c_int; + pub fn git_transaction_set_target( + tx: *mut git_transaction, + refname: *const c_char, + target: *const git_oid, + sig: *const git_signature, + msg: *const c_char, + ) -> c_int; + pub fn git_transaction_set_symbolic_target( + tx: *mut git_transaction, + refname: *const c_char, + target: *const c_char, + sig: *const git_signature, + msg: *const c_char, + ) -> c_int; + pub fn git_transaction_set_reflog( + tx: *mut git_transaction, + refname: *const c_char, + reflog: *const git_reflog, + ) -> c_int; + pub fn git_transaction_remove(tx: *mut git_transaction, refname: *const c_char) -> c_int; + pub fn git_transaction_commit(tx: *mut git_transaction) -> c_int; + pub fn git_transaction_free(tx: *mut git_transaction); + + // Mailmap + pub fn git_mailmap_new(out: *mut *mut git_mailmap) -> c_int; + pub fn git_mailmap_from_buffer( + out: *mut *mut git_mailmap, + buf: *const c_char, + len: size_t, + ) -> c_int; + pub fn git_mailmap_from_repository( + out: *mut *mut git_mailmap, + repo: *mut git_repository, + ) -> c_int; + pub fn git_mailmap_free(mm: *mut git_mailmap); + pub fn git_mailmap_resolve_signature( + out: *mut *mut git_signature, + mm: *const git_mailmap, + sig: *const git_signature, + ) -> c_int; + pub fn git_mailmap_add_entry( + mm: *mut git_mailmap, + real_name: *const c_char, + real_email: *const c_char, + replace_name: *const c_char, + replace_email: *const c_char, + ) -> c_int; + + // email + pub fn git_email_create_from_diff( + out: *mut git_buf, + diff: *mut git_diff, + patch_idx: usize, + patch_count: usize, + commit_id: *const git_oid, + summary: *const c_char, + body: *const c_char, + author: *const git_signature, + given_opts: *const git_email_create_options, + ) -> c_int; + + pub fn git_email_create_from_commit( + out: *mut git_buf, + commit: *mut git_commit, + given_opts: *const git_email_create_options, + ) -> c_int; + + pub fn git_trace_set(level: git_trace_level_t, cb: git_trace_cb) -> c_int; +} + +pub fn init() { + use std::sync::Once; + + static INIT: Once = Once::new(); + INIT.call_once(|| unsafe { + openssl_init(); + ssh_init(); + let rc = git_libgit2_init(); + if rc >= 0 { + // Note that we intentionally never schedule `git_libgit2_shutdown` + // to get called. There's not really a great time to call that and + // #276 has some more info about how automatically doing it can + // cause problems. + return; + } + + let git_error = git_error_last(); + let error = if !git_error.is_null() { + CStr::from_ptr((*git_error).message).to_string_lossy() + } else { + "unknown error".into() + }; + panic!( + "couldn't initialize the libgit2 library: {}, error: {}", + rc, error + ); + }); } -#[test] -fn smoke() { - unsafe { git_threads_init(); } +#[cfg(all(unix, feature = "https"))] +#[doc(hidden)] +pub fn openssl_init() { + openssl_sys::init(); +} + +#[cfg(any(windows, not(feature = "https")))] +#[doc(hidden)] +pub fn openssl_init() {} + +#[cfg(feature = "ssh")] +fn ssh_init() { + libssh2::init(); +} + +#[cfg(not(feature = "ssh"))] +fn ssh_init() {} + +#[doc(hidden)] +pub fn vendored() -> bool { + cfg!(libgit2_vendored) } diff --git a/libgit2-sys/libgit2 b/libgit2-sys/libgit2 index c8fe6c0975..0060d9cf56 160000 --- a/libgit2-sys/libgit2 +++ b/libgit2-sys/libgit2 @@ -1 +1 @@ -Subproject commit c8fe6c0975431e92d3dc4569734f30923b64dd18 +Subproject commit 0060d9cf5666f015b1067129bd874c6cc4c9c7ac diff --git a/src/apply.rs b/src/apply.rs new file mode 100644 index 0000000000..34dc811a01 --- /dev/null +++ b/src/apply.rs @@ -0,0 +1,208 @@ +//! git_apply support +//! see original: + +use crate::{panic, raw, util::Binding, DiffDelta, DiffHunk}; +use libc::c_int; +use std::{ffi::c_void, mem}; + +/// Possible application locations for git_apply +/// see +#[derive(Copy, Clone, Debug)] +pub enum ApplyLocation { + /// Apply the patch to the workdir + WorkDir, + /// Apply the patch to the index + Index, + /// Apply the patch to both the working directory and the index + Both, +} + +impl Binding for ApplyLocation { + type Raw = raw::git_apply_location_t; + unsafe fn from_raw(raw: raw::git_apply_location_t) -> Self { + match raw { + raw::GIT_APPLY_LOCATION_WORKDIR => Self::WorkDir, + raw::GIT_APPLY_LOCATION_INDEX => Self::Index, + raw::GIT_APPLY_LOCATION_BOTH => Self::Both, + _ => panic!("Unknown git diff binary kind"), + } + } + fn raw(&self) -> raw::git_apply_location_t { + match *self { + Self::WorkDir => raw::GIT_APPLY_LOCATION_WORKDIR, + Self::Index => raw::GIT_APPLY_LOCATION_INDEX, + Self::Both => raw::GIT_APPLY_LOCATION_BOTH, + } + } +} + +/// Options to specify when applying a diff +pub struct ApplyOptions<'cb> { + raw: raw::git_apply_options, + hunk_cb: Option>>, + delta_cb: Option>>, +} + +type HunkCB<'a> = dyn FnMut(Option>) -> bool + 'a; +type DeltaCB<'a> = dyn FnMut(Option>) -> bool + 'a; + +extern "C" fn delta_cb_c(delta: *const raw::git_diff_delta, data: *mut c_void) -> c_int { + panic::wrap(|| unsafe { + let delta = Binding::from_raw_opt(delta as *mut _); + + let payload = &mut *(data as *mut ApplyOptions<'_>); + let callback = match payload.delta_cb { + Some(ref mut c) => c, + None => return -1, + }; + + let apply = callback(delta); + if apply { + 0 + } else { + 1 + } + }) + .unwrap_or(-1) +} + +extern "C" fn hunk_cb_c(hunk: *const raw::git_diff_hunk, data: *mut c_void) -> c_int { + panic::wrap(|| unsafe { + let hunk = Binding::from_raw_opt(hunk); + + let payload = &mut *(data as *mut ApplyOptions<'_>); + let callback = match payload.hunk_cb { + Some(ref mut c) => c, + None => return -1, + }; + + let apply = callback(hunk); + if apply { + 0 + } else { + 1 + } + }) + .unwrap_or(-1) +} + +impl<'cb> ApplyOptions<'cb> { + /// Creates a new set of empty options (zeroed). + pub fn new() -> Self { + let mut opts = Self { + raw: unsafe { mem::zeroed() }, + hunk_cb: None, + delta_cb: None, + }; + assert_eq!( + unsafe { raw::git_apply_options_init(&mut opts.raw, raw::GIT_APPLY_OPTIONS_VERSION) }, + 0 + ); + opts + } + + fn flag(&mut self, opt: raw::git_apply_flags_t, val: bool) -> &mut Self { + let opt = opt as u32; + if val { + self.raw.flags |= opt; + } else { + self.raw.flags &= !opt; + } + self + } + + /// Don't actually make changes, just test that the patch applies. + pub fn check(&mut self, check: bool) -> &mut Self { + self.flag(raw::GIT_APPLY_CHECK, check) + } + + /// When applying a patch, callback that will be made per hunk. + pub fn hunk_callback(&mut self, cb: F) -> &mut Self + where + F: FnMut(Option>) -> bool + 'cb, + { + self.hunk_cb = Some(Box::new(cb) as Box>); + + self.raw.hunk_cb = Some(hunk_cb_c); + self.raw.payload = self as *mut _ as *mut _; + + self + } + + /// When applying a patch, callback that will be made per delta (file). + pub fn delta_callback(&mut self, cb: F) -> &mut Self + where + F: FnMut(Option>) -> bool + 'cb, + { + self.delta_cb = Some(Box::new(cb) as Box>); + + self.raw.delta_cb = Some(delta_cb_c); + self.raw.payload = self as *mut _ as *mut _; + + self + } + + /// Pointer to a raw git_stash_apply_options + pub unsafe fn raw(&mut self) -> *const raw::git_apply_options { + &self.raw as *const _ + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{fs::File, io::Write, path::Path}; + + #[test] + fn smoke_test() { + let (_td, repo) = crate::test::repo_init(); + let diff = t!(repo.diff_tree_to_workdir(None, None)); + let mut count_hunks = 0; + let mut count_delta = 0; + { + let mut opts = ApplyOptions::new(); + opts.hunk_callback(|_hunk| { + count_hunks += 1; + true + }); + opts.delta_callback(|_delta| { + count_delta += 1; + true + }); + t!(repo.apply(&diff, ApplyLocation::Both, Some(&mut opts))); + } + assert_eq!(count_hunks, 0); + assert_eq!(count_delta, 0); + } + + #[test] + fn apply_hunks_and_delta() { + let file_path = Path::new("foo.txt"); + let (td, repo) = crate::test::repo_init(); + // create new file + t!(t!(File::create(&td.path().join(file_path))).write_all(b"bar")); + // stage the new file + t!(t!(repo.index()).add_path(file_path)); + // now change workdir version + t!(t!(File::create(&td.path().join(file_path))).write_all(b"foo\nbar")); + + let diff = t!(repo.diff_index_to_workdir(None, None)); + assert_eq!(diff.deltas().len(), 1); + let mut count_hunks = 0; + let mut count_delta = 0; + { + let mut opts = ApplyOptions::new(); + opts.hunk_callback(|_hunk| { + count_hunks += 1; + true + }); + opts.delta_callback(|_delta| { + count_delta += 1; + true + }); + t!(repo.apply(&diff, ApplyLocation::Index, Some(&mut opts))); + } + assert_eq!(count_delta, 1); + assert_eq!(count_hunks, 1); + } +} diff --git a/src/attr.rs b/src/attr.rs new file mode 100644 index 0000000000..33b1d2d4af --- /dev/null +++ b/src/attr.rs @@ -0,0 +1,175 @@ +use crate::raw; +use std::ptr; +use std::str; + +/// All possible states of an attribute. +/// +/// This enum is used to interpret the value returned by +/// [`Repository::get_attr`](crate::Repository::get_attr) and +/// [`Repository::get_attr_bytes`](crate::Repository::get_attr_bytes). +#[derive(Debug, Clone, Copy, Eq)] +pub enum AttrValue<'string> { + /// The attribute is set to true. + True, + /// The attribute is unset (set to false). + False, + /// The attribute is set to a [valid UTF-8 string](prim@str). + String(&'string str), + /// The attribute is set to a string that might not be [valid UTF-8](prim@str). + Bytes(&'string [u8]), + /// The attribute is not specified. + Unspecified, +} + +macro_rules! from_value { + ($value:expr => $string:expr) => { + match unsafe { raw::git_attr_value($value.map_or(ptr::null(), |v| v.as_ptr().cast())) } { + raw::GIT_ATTR_VALUE_TRUE => Self::True, + raw::GIT_ATTR_VALUE_FALSE => Self::False, + raw::GIT_ATTR_VALUE_STRING => $string, + raw::GIT_ATTR_VALUE_UNSPECIFIED => Self::Unspecified, + _ => unreachable!(), + } + }; +} + +impl<'string> AttrValue<'string> { + /// Returns the state of an attribute by inspecting its [value](crate::Repository::get_attr) + /// by a [string](prim@str). + /// + /// This function always returns [`AttrValue::String`] and never returns [`AttrValue::Bytes`] + /// when the attribute is set to a string. + pub fn from_string(value: Option<&'string str>) -> Self { + from_value!(value => Self::String(value.unwrap())) + } + + /// Returns the state of an attribute by inspecting its [value](crate::Repository::get_attr_bytes) + /// by a [byte](u8) [slice]. + /// + /// This function will perform UTF-8 validation when the attribute is set to a string, returns + /// [`AttrValue::String`] if it's valid UTF-8 and [`AttrValue::Bytes`] otherwise. + pub fn from_bytes(value: Option<&'string [u8]>) -> Self { + let mut value = Self::always_bytes(value); + if let Self::Bytes(bytes) = value { + if let Ok(string) = str::from_utf8(bytes) { + value = Self::String(string); + } + } + value + } + + /// Returns the state of an attribute just like [`AttrValue::from_bytes`], but skips UTF-8 + /// validation and always returns [`AttrValue::Bytes`] when it's set to a string. + pub fn always_bytes(value: Option<&'string [u8]>) -> Self { + from_value!(value => Self::Bytes(value.unwrap())) + } +} + +/// Compare two [`AttrValue`]s. +/// +/// Note that this implementation does not differentiate between [`AttrValue::String`] and +/// [`AttrValue::Bytes`]. +impl PartialEq for AttrValue<'_> { + fn eq(&self, other: &AttrValue<'_>) -> bool { + match (self, other) { + (Self::True, AttrValue::True) + | (Self::False, AttrValue::False) + | (Self::Unspecified, AttrValue::Unspecified) => true, + (AttrValue::String(string), AttrValue::Bytes(bytes)) + | (AttrValue::Bytes(bytes), AttrValue::String(string)) => string.as_bytes() == *bytes, + (AttrValue::String(left), AttrValue::String(right)) => left == right, + (AttrValue::Bytes(left), AttrValue::Bytes(right)) => left == right, + _ => false, + } + } +} + +#[cfg(test)] +mod tests { + use super::AttrValue; + + macro_rules! test_attr_value { + ($function:ident, $variant:ident) => { + const ATTR_TRUE: &str = "[internal]__TRUE__"; + const ATTR_FALSE: &str = "[internal]__FALSE__"; + const ATTR_UNSET: &str = "[internal]__UNSET__"; + let as_bytes = AsRef::<[u8]>::as_ref; + // Use `matches!` here since the `PartialEq` implementation does not differentiate + // between `String` and `Bytes`. + assert!(matches!( + AttrValue::$function(Some(ATTR_TRUE.as_ref())), + AttrValue::$variant(s) if as_bytes(s) == ATTR_TRUE.as_bytes() + )); + assert!(matches!( + AttrValue::$function(Some(ATTR_FALSE.as_ref())), + AttrValue::$variant(s) if as_bytes(s) == ATTR_FALSE.as_bytes() + )); + assert!(matches!( + AttrValue::$function(Some(ATTR_UNSET.as_ref())), + AttrValue::$variant(s) if as_bytes(s) == ATTR_UNSET.as_bytes() + )); + assert!(matches!( + AttrValue::$function(Some("foo".as_ref())), + AttrValue::$variant(s) if as_bytes(s) == b"foo" + )); + assert!(matches!( + AttrValue::$function(Some("bar".as_ref())), + AttrValue::$variant(s) if as_bytes(s) == b"bar" + )); + assert_eq!(AttrValue::$function(None), AttrValue::Unspecified); + }; + } + + #[test] + fn attr_value_from_string() { + test_attr_value!(from_string, String); + } + + #[test] + fn attr_value_from_bytes() { + test_attr_value!(from_bytes, String); + assert!(matches!( + AttrValue::from_bytes(Some(&[0xff])), + AttrValue::Bytes(&[0xff]) + )); + assert!(matches!( + AttrValue::from_bytes(Some(b"\xffoobar")), + AttrValue::Bytes(b"\xffoobar") + )); + } + + #[test] + fn attr_value_always_bytes() { + test_attr_value!(always_bytes, Bytes); + assert!(matches!( + AttrValue::always_bytes(Some(&[0xff; 2])), + AttrValue::Bytes(&[0xff, 0xff]) + )); + assert!(matches!( + AttrValue::always_bytes(Some(b"\xffoo")), + AttrValue::Bytes(b"\xffoo") + )); + } + + #[test] + fn attr_value_partial_eq() { + assert_eq!(AttrValue::True, AttrValue::True); + assert_eq!(AttrValue::False, AttrValue::False); + assert_eq!(AttrValue::String("foo"), AttrValue::String("foo")); + assert_eq!(AttrValue::Bytes(b"foo"), AttrValue::Bytes(b"foo")); + assert_eq!(AttrValue::String("bar"), AttrValue::Bytes(b"bar")); + assert_eq!(AttrValue::Bytes(b"bar"), AttrValue::String("bar")); + assert_eq!(AttrValue::Unspecified, AttrValue::Unspecified); + assert_ne!(AttrValue::True, AttrValue::False); + assert_ne!(AttrValue::False, AttrValue::Unspecified); + assert_ne!(AttrValue::Unspecified, AttrValue::True); + assert_ne!(AttrValue::True, AttrValue::String("true")); + assert_ne!(AttrValue::Unspecified, AttrValue::Bytes(b"unspecified")); + assert_ne!(AttrValue::Bytes(b"false"), AttrValue::False); + assert_ne!(AttrValue::String("unspecified"), AttrValue::Unspecified); + assert_ne!(AttrValue::String("foo"), AttrValue::String("bar")); + assert_ne!(AttrValue::Bytes(b"foo"), AttrValue::Bytes(b"bar")); + assert_ne!(AttrValue::String("foo"), AttrValue::Bytes(b"bar")); + assert_ne!(AttrValue::Bytes(b"foo"), AttrValue::String("bar")); + } +} diff --git a/src/blame.rs b/src/blame.rs index c3e1209c5c..4bf41fed1e 100644 --- a/src/blame.rs +++ b/src/blame.rs @@ -1,9 +1,11 @@ -use std::marker; -use {raw, Repository, Oid, signature, Signature}; -use util::{self, Binding}; -use std::path::Path; -use std::ops::Range; +use crate::util::{self, Binding}; +use crate::{raw, signature, Error, Oid, Repository, Signature}; +use libc::c_char; +use std::iter::FusedIterator; use std::mem; +use std::ops::Range; +use std::path::Path; +use std::{marker, ptr}; /// Opaque structure to hold blame results. pub struct Blame<'repo> { @@ -29,14 +31,36 @@ pub struct BlameIter<'blame> { } impl<'repo> Blame<'repo> { + /// Get blame data for a file that has been modified in memory. + /// + /// Lines that differ between the buffer and the committed version are + /// marked as having a zero OID for their final_commit_id. + pub fn blame_buffer(&self, buffer: &[u8]) -> Result, Error> { + let mut raw = ptr::null_mut(); + + unsafe { + try_call!(raw::git_blame_buffer( + &mut raw, + self.raw, + buffer.as_ptr() as *const c_char, + buffer.len() + )); + Ok(Binding::from_raw(raw)) + } + } /// Gets the number of hunks that exist in the blame structure. pub fn len(&self) -> usize { unsafe { raw::git_blame_get_hunk_count(self.raw) as usize } } + /// Return `true` is there is no hunk in the blame structure. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Gets the blame hunk at the given index. - pub fn get_index(&self, index: usize) -> Option { + pub fn get_index(&self, index: usize) -> Option> { unsafe { let ptr = raw::git_blame_get_hunk_byindex(self.raw(), index as u32); if ptr.is_null() { @@ -49,7 +73,7 @@ impl<'repo> Blame<'repo> { /// Gets the hunk that relates to the given line number in the newest /// commit. - pub fn get_line(&self, lineno: usize) -> Option { + pub fn get_line(&self, lineno: usize) -> Option> { unsafe { let ptr = raw::git_blame_get_hunk_byline(self.raw(), lineno); if ptr.is_null() { @@ -61,16 +85,16 @@ impl<'repo> Blame<'repo> { } /// Returns an iterator over the hunks in this blame. - pub fn iter(&self) -> BlameIter { - BlameIter { range: 0..self.len(), blame: self } + pub fn iter(&self) -> BlameIter<'_> { + BlameIter { + range: 0..self.len(), + blame: self, + } } - } impl<'blame> BlameHunk<'blame> { - - unsafe fn from_raw_const(raw: *const raw::git_blame_hunk) - -> BlameHunk<'blame> { + unsafe fn from_raw_const(raw: *const raw::git_blame_hunk) -> BlameHunk<'blame> { BlameHunk { raw: raw as *mut raw::git_blame_hunk, _marker: marker::PhantomData, @@ -83,7 +107,7 @@ impl<'blame> BlameHunk<'blame> { } /// Returns signature of the commit. - pub fn final_signature(&self) -> Signature { + pub fn final_signature(&self) -> Signature<'_> { unsafe { signature::from_raw_const(self, (*self.raw).final_signature) } } @@ -104,7 +128,7 @@ impl<'blame> BlameHunk<'blame> { } /// Returns signature of the commit. - pub fn orig_signature(&self) -> Signature { + pub fn orig_signature(&self) -> Signature<'_> { unsafe { signature::from_raw_const(self, (*self.raw).orig_signature) } } @@ -112,15 +136,15 @@ impl<'blame> BlameHunk<'blame> { /// /// Note that the start line is counting from 1. pub fn orig_start_line(&self) -> usize { - unsafe { (*self.raw).orig_start_line_number} + unsafe { (*self.raw).orig_start_line_number } } /// Returns path to the file where this hunk originated. /// - /// Note: `None` could be returned for non-unicode paths on Widnows. + /// Note: `None` could be returned for non-unicode paths on Windows. pub fn path(&self) -> Option<&Path> { unsafe { - if let Some(bytes) = ::opt_bytes(self, (*self.raw).orig_path) { + if let Some(bytes) = crate::opt_bytes(self, (*self.raw).orig_path) { Some(util::bytes2path(bytes)) } else { None @@ -140,16 +164,21 @@ impl<'blame> BlameHunk<'blame> { } } -impl BlameOptions { +impl Default for BlameOptions { + fn default() -> Self { + Self::new() + } +} +impl BlameOptions { /// Initialize options pub fn new() -> BlameOptions { unsafe { let mut raw: raw::git_blame_options = mem::zeroed(); assert_eq!( - raw::git_blame_init_options(&mut raw, - raw::GIT_BLAME_OPTIONS_VERSION) - , 0); + raw::git_blame_init_options(&mut raw, raw::GIT_BLAME_OPTIONS_VERSION), + 0 + ); Binding::from_raw(&raw as *const _ as *mut _) } @@ -192,28 +221,60 @@ impl BlameOptions { self.flag(raw::GIT_BLAME_FIRST_PARENT, opt) } + /// Use mailmap file to map author and committer names and email addresses + /// to canonical real names and email addresses. The mailmap will be read + /// from the working directory, or HEAD in a bare repository. + pub fn use_mailmap(&mut self, opt: bool) -> &mut BlameOptions { + self.flag(raw::GIT_BLAME_USE_MAILMAP, opt) + } + + /// Ignore whitespace differences. + pub fn ignore_whitespace(&mut self, opt: bool) -> &mut BlameOptions { + self.flag(raw::GIT_BLAME_IGNORE_WHITESPACE, opt) + } + /// Setter for the id of the newest commit to consider. pub fn newest_commit(&mut self, id: Oid) -> &mut BlameOptions { - unsafe { self.raw.newest_commit = *id.raw(); } + unsafe { + self.raw.newest_commit = *id.raw(); + } self } /// Setter for the id of the oldest commit to consider. pub fn oldest_commit(&mut self, id: Oid) -> &mut BlameOptions { - unsafe { self.raw.oldest_commit = *id.raw(); } + unsafe { + self.raw.oldest_commit = *id.raw(); + } self } + /// The first line in the file to blame. + pub fn min_line(&mut self, lineno: usize) -> &mut BlameOptions { + self.raw.min_line = lineno; + self + } + + /// The last line in the file to blame. + pub fn max_line(&mut self, lineno: usize) -> &mut BlameOptions { + self.raw.max_line = lineno; + self + } } impl<'repo> Binding for Blame<'repo> { type Raw = *mut raw::git_blame; unsafe fn from_raw(raw: *mut raw::git_blame) -> Blame<'repo> { - Blame { raw: raw, _marker: marker::PhantomData } + Blame { + raw, + _marker: marker::PhantomData, + } } - fn raw(&self) -> *mut raw::git_blame { self.raw } + fn raw(&self) -> *mut raw::git_blame { + self.raw + } } impl<'repo> Drop for Blame<'repo> { @@ -226,10 +287,15 @@ impl<'blame> Binding for BlameHunk<'blame> { type Raw = *mut raw::git_blame_hunk; unsafe fn from_raw(raw: *mut raw::git_blame_hunk) -> BlameHunk<'blame> { - BlameHunk { raw: raw, _marker: marker::PhantomData } + BlameHunk { + raw, + _marker: marker::PhantomData, + } } - fn raw(&self) -> *mut raw::git_blame_hunk { self.raw } + fn raw(&self) -> *mut raw::git_blame_hunk { + self.raw + } } impl Binding for BlameOptions { @@ -250,7 +316,9 @@ impl<'blame> Iterator for BlameIter<'blame> { self.range.next().and_then(|i| self.blame.get_index(i)) } - fn size_hint(&self) -> (usize, Option) { self.range.size_hint() } + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() + } } impl<'blame> DoubleEndedIterator for BlameIter<'blame> { @@ -259,6 +327,8 @@ impl<'blame> DoubleEndedIterator for BlameIter<'blame> { } } +impl<'blame> FusedIterator for BlameIter<'blame> {} + impl<'blame> ExactSizeIterator for BlameIter<'blame> {} #[cfg(test)] @@ -268,10 +338,10 @@ mod tests { #[test] fn smoke() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let mut index = repo.index().unwrap(); - let root = repo.path().parent().unwrap(); + let root = repo.workdir().unwrap(); fs::create_dir(&root.join("foo")).unwrap(); File::create(&root.join("foo/bar")).unwrap(); index.add_path(Path::new("foo/bar")).unwrap(); @@ -281,8 +351,9 @@ mod tests { let sig = repo.signature().unwrap(); let id = repo.refname_to_id("HEAD").unwrap(); let parent = repo.find_commit(id).unwrap(); - let commit = repo.commit(Some("HEAD"), &sig, &sig, "commit", - &tree, &[&parent]).unwrap(); + let commit = repo + .commit(Some("HEAD"), &sig, &sig, "commit", &tree, &[&parent]) + .unwrap(); let blame = repo.blame_file(Path::new("foo/bar"), None).unwrap(); @@ -296,8 +367,13 @@ mod tests { assert_eq!(hunk.final_start_line(), 1); assert_eq!(hunk.path(), Some(Path::new("foo/bar"))); assert_eq!(hunk.lines_in_hunk(), 0); - assert!(!hunk.is_boundary()) - } + assert!(!hunk.is_boundary()); -} + let blame_buffer = blame.blame_buffer("\n".as_bytes()).unwrap(); + let line = blame_buffer.get_line(1).unwrap(); + assert_eq!(blame_buffer.len(), 2); + assert_eq!(blame_buffer.iter().count(), 2); + assert!(line.final_commit_id().is_zero()); + } +} diff --git a/src/blob.rs b/src/blob.rs index 40a94b5829..5c4a6ce6b8 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -1,9 +1,10 @@ +use std::io; use std::marker; use std::mem; use std::slice; -use {raw, Oid, Object}; -use util::Binding; +use crate::util::Binding; +use crate::{raw, Error, Object, Oid}; /// A structure to represent a git [blob][1] /// @@ -33,19 +34,20 @@ impl<'repo> Blob<'repo> { } } + /// Get the size in bytes of the contents of this blob. + pub fn size(&self) -> usize { + unsafe { raw::git_blob_rawsize(&*self.raw) as usize } + } + /// Casts this Blob to be usable as an `Object` pub fn as_object(&self) -> &Object<'repo> { - unsafe { - &*(self as *const _ as *const Object<'repo>) - } + unsafe { &*(self as *const _ as *const Object<'repo>) } } /// Consumes Blob to be returned as an `Object` pub fn into_object(self) -> Object<'repo> { - assert_eq!(mem::size_of_val(&self), mem::size_of::()); - unsafe { - mem::transmute(self) - } + assert_eq!(mem::size_of_val(&self), mem::size_of::>()); + unsafe { mem::transmute(self) } } } @@ -54,13 +56,26 @@ impl<'repo> Binding for Blob<'repo> { unsafe fn from_raw(raw: *mut raw::git_blob) -> Blob<'repo> { Blob { - raw: raw, + raw, _marker: marker::PhantomData, } } - fn raw(&self) -> *mut raw::git_blob { self.raw } + fn raw(&self) -> *mut raw::git_blob { + self.raw + } +} + +impl<'repo> std::fmt::Debug for Blob<'repo> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_struct("Blob").field("id", &self.id()).finish() + } } +impl<'repo> Clone for Blob<'repo> { + fn clone(&self) -> Self { + self.as_object().clone().into_blob().ok().unwrap() + } +} impl<'repo> Drop for Blob<'repo> { fn drop(&mut self) { @@ -68,31 +83,107 @@ impl<'repo> Drop for Blob<'repo> { } } +/// A structure to represent a git writestream for blobs +pub struct BlobWriter<'repo> { + raw: *mut raw::git_writestream, + need_cleanup: bool, + _marker: marker::PhantomData>, +} + +impl<'repo> BlobWriter<'repo> { + /// Finalize blob writing stream and write the blob to the object db + pub fn commit(mut self) -> Result { + // After commit we already doesn't need cleanup on drop + self.need_cleanup = false; + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_blob_create_fromstream_commit(&mut raw, self.raw)); + Ok(Binding::from_raw(&raw as *const _)) + } + } +} + +impl<'repo> Binding for BlobWriter<'repo> { + type Raw = *mut raw::git_writestream; + + unsafe fn from_raw(raw: *mut raw::git_writestream) -> BlobWriter<'repo> { + BlobWriter { + raw, + need_cleanup: true, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_writestream { + self.raw + } +} + +impl<'repo> Drop for BlobWriter<'repo> { + fn drop(&mut self) { + // We need cleanup in case the stream has not been committed + if self.need_cleanup { + unsafe { + if let Some(f) = (*self.raw).free { + f(self.raw) + } + } + } + } +} + +impl<'repo> io::Write for BlobWriter<'repo> { + fn write(&mut self, buf: &[u8]) -> io::Result { + unsafe { + if let Some(f) = (*self.raw).write { + let res = f(self.raw, buf.as_ptr() as *const _, buf.len()); + if res < 0 { + Err(io::Error::new(io::ErrorKind::Other, "Write error")) + } else { + Ok(buf.len()) + } + } else { + Err(io::Error::new(io::ErrorKind::Other, "no write callback")) + } + } + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + #[cfg(test)] mod tests { - use std::io::prelude::*; + use crate::Repository; use std::fs::File; - use tempdir::TempDir; - use Repository; + use std::io::prelude::*; + use std::path::Path; + use tempfile::TempDir; #[test] fn buffer() { - let td = TempDir::new("test").unwrap(); + let td = TempDir::new().unwrap(); let repo = Repository::init(td.path()).unwrap(); let id = repo.blob(&[5, 4, 6]).unwrap(); let blob = repo.find_blob(id).unwrap(); assert_eq!(blob.id(), id); + assert_eq!(blob.size(), 3); assert_eq!(blob.content(), [5, 4, 6]); assert!(blob.is_binary()); repo.find_object(id, None).unwrap().as_blob().unwrap(); - repo.find_object(id, None).unwrap().into_blob().ok().unwrap(); + repo.find_object(id, None) + .unwrap() + .into_blob() + .ok() + .unwrap(); } #[test] fn path() { - let td = TempDir::new("test").unwrap(); + let td = TempDir::new().unwrap(); let path = td.path().join("foo"); File::create(&path).unwrap().write_all(&[7, 8, 9]).unwrap(); let repo = Repository::init(td.path()).unwrap(); @@ -101,4 +192,17 @@ mod tests { assert_eq!(blob.content(), [7, 8, 9]); blob.into_object(); } + + #[test] + fn stream() { + let td = TempDir::new().unwrap(); + let repo = Repository::init(td.path()).unwrap(); + let mut ws = repo.blob_writer(Some(Path::new("foo"))).unwrap(); + let wl = ws.write(&[10, 11, 12]).unwrap(); + assert_eq!(wl, 3); + let id = ws.commit().unwrap(); + let blob = repo.find_blob(id).unwrap(); + assert_eq!(blob.content(), [10, 11, 12]); + blob.into_object(); + } } diff --git a/src/branch.rs b/src/branch.rs index 34ba296263..e1eba99c2b 100644 --- a/src/branch.rs +++ b/src/branch.rs @@ -1,15 +1,15 @@ use std::ffi::CString; use std::marker; +use std::ptr; use std::str; -use libc; -use {raw, Error, Reference, BranchType, References}; -use util::Binding; +use crate::util::Binding; +use crate::{raw, BranchType, Error, Reference, References}; /// A structure to represent a git [branch][1] /// /// A branch is currently just a wrapper to an underlying `Reference`. The -/// reference can be accessed through the `get` and `unwrap` methods. +/// reference can be accessed through the `get` and `into_reference` methods. /// /// [1]: http://git-scm.com/book/en/Git-Branching-What-a-Branch-Is pub struct Branch<'repo> { @@ -23,18 +23,42 @@ pub struct Branches<'repo> { } impl<'repo> Branch<'repo> { - /// Creates a new branch from a reference - pub fn wrap(reference: Reference) -> Branch { Branch { inner: reference } } + /// Creates Branch type from a Reference + pub fn wrap(reference: Reference<'_>) -> Branch<'_> { + Branch { inner: reference } + } + + /// Ensure the branch name is well-formed. + pub fn name_is_valid(name: &str) -> Result { + crate::init(); + let name = CString::new(name)?; + let mut valid: libc::c_int = 0; + unsafe { + try_call!(raw::git_branch_name_is_valid(&mut valid, name.as_ptr())); + } + Ok(valid == 1) + } /// Gain access to the reference that is this branch - pub fn get(&self) -> &Reference<'repo> { &self.inner } + pub fn get(&self) -> &Reference<'repo> { + &self.inner + } + + /// Gain mutable access to the reference that is this branch + pub fn get_mut(&mut self) -> &mut Reference<'repo> { + &mut self.inner + } /// Take ownership of the underlying reference. - pub fn into_reference(self) -> Reference<'repo> { self.inner } + pub fn into_reference(self) -> Reference<'repo> { + self.inner + } /// Delete an existing branch reference. pub fn delete(&mut self) -> Result<(), Error> { - unsafe { try_call!(raw::git_branch_delete(self.get().raw())); } + unsafe { + try_call!(raw::git_branch_delete(self.get().raw())); + } Ok(()) } @@ -44,13 +68,16 @@ impl<'repo> Branch<'repo> { } /// Move/rename an existing local branch reference. - pub fn rename(&mut self, new_branch_name: &str, force: bool) - -> Result, Error> { - let mut ret = 0 as *mut raw::git_reference; - let new_branch_name = try!(CString::new(new_branch_name)); + pub fn rename(&mut self, new_branch_name: &str, force: bool) -> Result, Error> { + let mut ret = ptr::null_mut(); + let new_branch_name = CString::new(new_branch_name)?; unsafe { - try_call!(raw::git_branch_move(&mut ret, self.get().raw(), - new_branch_name, force)); + try_call!(raw::git_branch_move( + &mut ret, + self.get().raw(), + new_branch_name, + force + )); Ok(Branch::wrap(Binding::from_raw(ret))) } } @@ -64,17 +91,17 @@ impl<'repo> Branch<'repo> { /// Return the name of the given local or remote branch. pub fn name_bytes(&self) -> Result<&[u8], Error> { - let mut ret = 0 as *const libc::c_char; + let mut ret = ptr::null(); unsafe { try_call!(raw::git_branch_name(&mut ret, &*self.get().raw())); - Ok(::opt_bytes(self, ret).unwrap()) + Ok(crate::opt_bytes(self, ret).unwrap()) } } /// Return the reference supporting the remote tracking branch, given a /// local branch reference. - pub fn upstream<'a>(&'a self) -> Result, Error> { - let mut ret = 0 as *mut raw::git_reference; + pub fn upstream(&self) -> Result, Error> { + let mut ret = ptr::null_mut(); unsafe { try_call!(raw::git_branch_upstream(&mut ret, &*self.get().raw())); Ok(Branch::wrap(Binding::from_raw(ret))) @@ -85,12 +112,13 @@ impl<'repo> Branch<'repo> { /// /// If `None` is specified, then the upstream branch is unset. The name /// provided is the name of the branch to set as upstream. - pub fn set_upstream(&mut self, - upstream_name: Option<&str>) -> Result<(), Error> { - let upstream_name = try!(::opt_cstr(upstream_name)); + pub fn set_upstream(&mut self, upstream_name: Option<&str>) -> Result<(), Error> { + let upstream_name = crate::opt_cstr(upstream_name)?; unsafe { - try_call!(raw::git_branch_set_upstream(self.get().raw(), - upstream_name)); + try_call!(raw::git_branch_set_upstream( + self.get().raw(), + upstream_name + )); Ok(()) } } @@ -101,10 +129,9 @@ impl<'repo> Branches<'repo> { /// /// This function is unsafe as it is not guaranteed that `raw` is a valid /// pointer. - pub unsafe fn from_raw(raw: *mut raw::git_branch_iterator) - -> Branches<'repo> { + pub unsafe fn from_raw(raw: *mut raw::git_branch_iterator) -> Branches<'repo> { Branches { - raw: raw, + raw, _marker: marker::PhantomData, } } @@ -113,7 +140,7 @@ impl<'repo> Branches<'repo> { impl<'repo> Iterator for Branches<'repo> { type Item = Result<(Branch<'repo>, BranchType), Error>; fn next(&mut self) -> Option, BranchType), Error>> { - let mut ret = 0 as *mut raw::git_reference; + let mut ret = ptr::null_mut(); let mut typ = raw::GIT_BRANCH_LOCAL; unsafe { try_call_iter!(raw::git_branch_next(&mut ret, &mut typ, self.raw)); @@ -135,11 +162,11 @@ impl<'repo> Drop for Branches<'repo> { #[cfg(test)] mod tests { - use BranchType; + use crate::{Branch, BranchType}; #[test] fn smoke() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let head = repo.head().unwrap(); let target = head.target().unwrap(); let commit = repo.find_commit(target).unwrap(); @@ -153,10 +180,18 @@ mod tests { let mut b1 = b1.rename("bar", false).unwrap(); assert_eq!(b1.name().unwrap(), Some("bar")); assert!(b1.upstream().is_err()); - b1.set_upstream(Some("master")).unwrap(); + b1.set_upstream(Some("main")).unwrap(); b1.upstream().unwrap(); b1.set_upstream(None).unwrap(); b1.delete().unwrap(); } + + #[test] + fn name_is_valid() { + assert!(Branch::name_is_valid("foo").unwrap()); + assert!(!Branch::name_is_valid("").unwrap()); + assert!(!Branch::name_is_valid("with spaces").unwrap()); + assert!(!Branch::name_is_valid("~tilde").unwrap()); + } } diff --git a/src/buf.rs b/src/buf.rs index 330f948bc8..fd2bcbf96f 100644 --- a/src/buf.rs +++ b/src/buf.rs @@ -1,10 +1,10 @@ +use std::ops::{Deref, DerefMut}; +use std::ptr; use std::slice; use std::str; -use std::ops::{Deref, DerefMut}; -use libc; -use raw; -use util::Binding; +use crate::raw; +use crate::util::Binding; /// A structure to wrap an intermediate buffer used by libgit2. /// @@ -14,15 +14,21 @@ pub struct Buf { raw: raw::git_buf, } +impl Default for Buf { + fn default() -> Self { + Self::new() + } +} + impl Buf { /// Creates a new empty buffer. pub fn new() -> Buf { - ::init(); + crate::init(); unsafe { Binding::from_raw(&mut raw::git_buf { - ptr: 0 as *mut libc::c_char, + ptr: ptr::null_mut(), size: 0, - asize: 0, + reserved: 0, } as *mut _) } } @@ -30,25 +36,21 @@ impl Buf { /// Attempt to view this buffer as a string slice. /// /// Returns `None` if the buffer is not valid utf-8. - pub fn as_str(&self) -> Option<&str> { str::from_utf8(&**self).ok() } + pub fn as_str(&self) -> Option<&str> { + str::from_utf8(&**self).ok() + } } impl Deref for Buf { type Target = [u8]; fn deref(&self) -> &[u8] { - unsafe { - slice::from_raw_parts(self.raw.ptr as *const u8, - self.raw.size as usize) - } + unsafe { slice::from_raw_parts(self.raw.ptr as *const u8, self.raw.size as usize) } } } impl DerefMut for Buf { fn deref_mut(&mut self) -> &mut [u8] { - unsafe { - slice::from_raw_parts_mut(self.raw.ptr as *mut u8, - self.raw.size as usize) - } + unsafe { slice::from_raw_parts_mut(self.raw.ptr as *mut u8, self.raw.size as usize) } } } @@ -57,11 +59,13 @@ impl Binding for Buf { unsafe fn from_raw(raw: *mut raw::git_buf) -> Buf { Buf { raw: *raw } } - fn raw(&self) -> *mut raw::git_buf { &self.raw as *const _ as *mut _ } + fn raw(&self) -> *mut raw::git_buf { + &self.raw as *const _ as *mut _ + } } impl Drop for Buf { fn drop(&mut self) { - unsafe { raw::git_buf_free(&mut self.raw) } + unsafe { raw::git_buf_dispose(&mut self.raw) } } } diff --git a/src/build.rs b/src/build.rs index 3ac6bd7b3f..4ac62439b7 100644 --- a/src/build.rs +++ b/src/build.rs @@ -1,16 +1,52 @@ //! Builder-pattern objects for configuration various git operations. +use libc::{c_char, c_int, c_uint, c_void, size_t}; use std::ffi::{CStr, CString}; use std::mem; use std::path::Path; -use libc::{c_char, size_t, c_void, c_uint, c_int}; +use std::ptr; -use {raw, panic, Error, Repository, FetchOptions, IntoCString}; -use {CheckoutNotificationType, DiffFile}; -use util::{self, Binding}; +use crate::util::{self, Binding}; +use crate::{panic, raw, Error, FetchOptions, IntoCString, Oid, Repository, Tree}; +use crate::{CheckoutNotificationType, DiffFile, FileMode, Remote}; /// A builder struct which is used to build configuration for cloning a new git /// repository. +/// +/// # Example +/// +/// Cloning using SSH: +/// +/// ```no_run +/// use git2::{Cred, Error, RemoteCallbacks}; +/// use std::env; +/// use std::path::Path; +/// +/// // Prepare callbacks. +/// let mut callbacks = RemoteCallbacks::new(); +/// callbacks.credentials(|_url, username_from_url, _allowed_types| { +/// Cred::ssh_key( +/// username_from_url.unwrap(), +/// None, +/// Path::new(&format!("{}/.ssh/id_rsa", env::var("HOME").unwrap())), +/// None, +/// ) +/// }); +/// +/// // Prepare fetch options. +/// let mut fo = git2::FetchOptions::new(); +/// fo.remote_callbacks(callbacks); +/// +/// // Prepare builder. +/// let mut builder = git2::build::RepoBuilder::new(); +/// builder.fetch_options(fo); +/// +/// // Clone the project. +/// builder.clone( +/// "git@github.com:rust-lang/git2-rs.git", +/// Path::new("/tmp/git2-rs"), +/// ); +/// ``` pub struct RepoBuilder<'cb> { bare: bool, branch: Option, @@ -18,6 +54,28 @@ pub struct RepoBuilder<'cb> { hardlinks: bool, checkout: Option>, fetch_opts: Option>, + clone_local: Option, + remote_create: Option>>, +} + +/// Type of callback passed to `RepoBuilder::remote_create`. +/// +/// The second and third arguments are the remote's name and the remote's URL. +pub type RemoteCreate<'cb> = + dyn for<'a> FnMut(&'a Repository, &str, &str) -> Result, Error> + 'cb; + +/// A builder struct for git tree updates. +/// +/// Paths passed to `remove` and `upsert` can be multi-component paths, i.e. they +/// may contain slashes. +/// +/// This is a higher-level tree update facility. There is also [`TreeBuilder`] +/// which is lower-level (and operates only on one level of the tree at a time). +/// +/// [`TreeBuilder`]: crate::TreeBuilder +pub struct TreeUpdateBuilder { + updates: Vec, + paths: Vec, } /// A builder struct for configuring checkouts of a repository. @@ -39,9 +97,9 @@ pub struct CheckoutBuilder<'cb> { /// Checkout progress notification callback. /// -/// The first argument is the path for the notification, the next is the numver +/// The first argument is the path for the notification, the next is the number /// of completed steps so far, and the final is the total number of steps. -pub type Progress<'a> = FnMut(Option<&Path>, usize, usize) + 'a; +pub type Progress<'a> = dyn FnMut(Option<&Path>, usize, usize) + 'a; /// Checkout notifications callback. /// @@ -50,8 +108,42 @@ pub type Progress<'a> = FnMut(Option<&Path>, usize, usize) + 'a; /// /// The callback must return a bool specifying whether the checkout should /// continue. -pub type Notify<'a> = FnMut(CheckoutNotificationType, Option<&Path>, DiffFile, - DiffFile, DiffFile) -> bool + 'a; +pub type Notify<'a> = dyn FnMut( + CheckoutNotificationType, + Option<&Path>, + Option>, + Option>, + Option>, + ) -> bool + + 'a; + +impl<'cb> Default for RepoBuilder<'cb> { + fn default() -> Self { + Self::new() + } +} + +/// Options that can be passed to `RepoBuilder::clone_local`. +#[derive(Clone, Copy)] +pub enum CloneLocal { + /// Auto-detect (default) + /// + /// Here libgit2 will bypass the git-aware transport for local paths, but + /// use a normal fetch for `file://` URLs. + Auto = raw::GIT_CLONE_LOCAL_AUTO as isize, + + /// Bypass the git-aware transport even for `file://` URLs. + Local = raw::GIT_CLONE_LOCAL as isize, + + /// Never bypass the git-aware transport + None = raw::GIT_CLONE_NO_LOCAL as isize, + + /// Bypass the git-aware transport, but don't try to use hardlinks. + NoLinks = raw::GIT_CLONE_LOCAL_NO_LINKS as isize, + + #[doc(hidden)] + __Nonexhaustive = 0xff, +} impl<'cb> RepoBuilder<'cb> { /// Creates a new repository builder with all of the default configuration. @@ -59,14 +151,16 @@ impl<'cb> RepoBuilder<'cb> { /// When ready, the `clone()` method can be used to clone a new repository /// using this configuration. pub fn new() -> RepoBuilder<'cb> { - ::init(); + crate::init(); RepoBuilder { bare: false, branch: None, local: true, + clone_local: None, hardlinks: true, checkout: None, fetch_opts: None, + remote_create: None, } } @@ -85,11 +179,23 @@ impl<'cb> RepoBuilder<'cb> { self } + /// Configures options for bypassing the git-aware transport on clone. + /// + /// Bypassing it means that instead of a fetch libgit2 will copy the object + /// database directory instead of figuring out what it needs, which is + /// faster. If possible, it will hardlink the files to save space. + pub fn clone_local(&mut self, clone_local: CloneLocal) -> &mut RepoBuilder<'cb> { + self.clone_local = Some(clone_local); + self + } + /// Set the flag for bypassing the git aware transport mechanism for local /// paths. /// /// If `true`, the git-aware transport will be bypassed for local paths. If /// `false`, the git-aware transport will not be bypassed. + #[deprecated(note = "use `clone_local` instead")] + #[doc(hidden)] pub fn local(&mut self, local: bool) -> &mut RepoBuilder<'cb> { self.local = local; self @@ -97,6 +203,8 @@ impl<'cb> RepoBuilder<'cb> { /// Set the flag for whether hardlinks are used when using a local git-aware /// transport mechanism. + #[deprecated(note = "use `clone_local` instead")] + #[doc(hidden)] pub fn hardlinks(&mut self, links: bool) -> &mut RepoBuilder<'cb> { self.hardlinks = links; self @@ -104,8 +212,7 @@ impl<'cb> RepoBuilder<'cb> { /// Configure the checkout which will be performed by consuming a checkout /// builder. - pub fn with_checkout(&mut self, checkout: CheckoutBuilder<'cb>) - -> &mut RepoBuilder<'cb> { + pub fn with_checkout(&mut self, checkout: CheckoutBuilder<'cb>) -> &mut RepoBuilder<'cb> { self.checkout = Some(checkout); self } @@ -114,50 +221,69 @@ impl<'cb> RepoBuilder<'cb> { /// /// The callbacks are used for reporting fetch progress, and for acquiring /// credentials in the event they are needed. - pub fn fetch_options(&mut self, fetch_opts: FetchOptions<'cb>) - -> &mut RepoBuilder<'cb> { + pub fn fetch_options(&mut self, fetch_opts: FetchOptions<'cb>) -> &mut RepoBuilder<'cb> { self.fetch_opts = Some(fetch_opts); self } + /// Configures a callback used to create the git remote, prior to its being + /// used to perform the clone operation. + pub fn remote_create(&mut self, f: F) -> &mut RepoBuilder<'cb> + where + F: for<'a> FnMut(&'a Repository, &str, &str) -> Result, Error> + 'cb, + { + self.remote_create = Some(Box::new(f)); + self + } + /// Clone a remote repository. /// - /// This will use the options configured so far to clone the specified url + /// This will use the options configured so far to clone the specified URL /// into the specified local path. pub fn clone(&mut self, url: &str, into: &Path) -> Result { let mut opts: raw::git_clone_options = unsafe { mem::zeroed() }; unsafe { - try_call!(raw::git_clone_init_options(&mut opts, - raw::GIT_CLONE_OPTIONS_VERSION)); + try_call!(raw::git_clone_init_options( + &mut opts, + raw::GIT_CLONE_OPTIONS_VERSION + )); } opts.bare = self.bare as c_int; - opts.checkout_branch = self.branch.as_ref().map(|s| { - s.as_ptr() - }).unwrap_or(0 as *const _); - - opts.local = match (self.local, self.hardlinks) { - (true, false) => raw::GIT_CLONE_LOCAL_NO_LINKS, - (false, _) => raw::GIT_CLONE_NO_LOCAL, - (true, _) => raw::GIT_CLONE_LOCAL_AUTO, - }; - opts.checkout_opts.checkout_strategy = - raw::GIT_CHECKOUT_SAFE as c_uint; + opts.checkout_branch = self + .branch + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(ptr::null()); + + if let Some(ref local) = self.clone_local { + opts.local = *local as raw::git_clone_local_t; + } else { + opts.local = match (self.local, self.hardlinks) { + (true, false) => raw::GIT_CLONE_LOCAL_NO_LINKS, + (false, _) => raw::GIT_CLONE_NO_LOCAL, + (true, _) => raw::GIT_CLONE_LOCAL_AUTO, + }; + } - match self.fetch_opts { - Some(ref mut cbs) => { - opts.fetch_opts = cbs.raw(); - }, - None => {} + if let Some(ref mut cbs) = self.fetch_opts { + opts.fetch_opts = cbs.raw(); } - match self.checkout { - Some(ref mut c) => unsafe { c.configure(&mut opts.checkout_opts) }, - None => {} + if let Some(ref mut c) = self.checkout { + unsafe { + c.configure(&mut opts.checkout_opts); + } } - let url = try!(CString::new(url)); - let into = try!(into.into_c_string()); - let mut raw = 0 as *mut raw::git_repository; + if let Some(ref mut callback) = self.remote_create { + opts.remote_cb = Some(remote_create_cb); + opts.remote_cb_payload = callback as *mut _ as *mut _; + } + + let url = CString::new(url)?; + // Normal file path OK (does not need Windows conversion). + let into = into.into_c_string()?; + let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_clone(&mut raw, url, into, &opts)); Ok(Binding::from_raw(raw)) @@ -165,11 +291,43 @@ impl<'cb> RepoBuilder<'cb> { } } +extern "C" fn remote_create_cb( + out: *mut *mut raw::git_remote, + repo: *mut raw::git_repository, + name: *const c_char, + url: *const c_char, + payload: *mut c_void, +) -> c_int { + unsafe { + let repo = Repository::from_raw(repo); + let code = panic::wrap(|| { + let name = CStr::from_ptr(name).to_str().unwrap(); + let url = CStr::from_ptr(url).to_str().unwrap(); + let f = payload as *mut Box>; + match (*f)(&repo, name, url) { + Ok(remote) => { + *out = crate::remote::remote_into_raw(remote); + 0 + } + Err(e) => e.raw_code(), + } + }); + mem::forget(repo); + code.unwrap_or(-1) + } +} + +impl<'cb> Default for CheckoutBuilder<'cb> { + fn default() -> Self { + Self::new() + } +} + impl<'cb> CheckoutBuilder<'cb> { /// Creates a new builder for checkouts with all of its default /// configuration. pub fn new() -> CheckoutBuilder<'cb> { - ::init(); + crate::init(); CheckoutBuilder { disable_filters: false, dir_perm: None, @@ -204,7 +362,7 @@ impl<'cb> CheckoutBuilder<'cb> { } /// Indicate that the checkout should be performed safely, allowing new - /// files to be created but not overwriting extisting files or changes. + /// files to be created but not overwriting existing files or changes. /// /// This is the default. pub fn safe(&mut self) -> &mut CheckoutBuilder<'cb> { @@ -213,8 +371,7 @@ impl<'cb> CheckoutBuilder<'cb> { self } - fn flag(&mut self, bit: raw::git_checkout_strategy_t, - on: bool) -> &mut CheckoutBuilder<'cb> { + fn flag(&mut self, bit: raw::git_checkout_strategy_t, on: bool) -> &mut CheckoutBuilder<'cb> { if on { self.checkout_opts |= bit as u32; } else { @@ -241,8 +398,7 @@ impl<'cb> CheckoutBuilder<'cb> { /// Remove untracked files from the working dir. /// /// Defaults to false. - pub fn remove_untracked(&mut self, remove: bool) - -> &mut CheckoutBuilder<'cb> { + pub fn remove_untracked(&mut self, remove: bool) -> &mut CheckoutBuilder<'cb> { self.flag(raw::GIT_CHECKOUT_REMOVE_UNTRACKED, remove) } @@ -304,16 +460,14 @@ impl<'cb> CheckoutBuilder<'cb> { /// Indicate whether ignored files should be overwritten during the checkout. /// /// Defaults to true. - pub fn overwrite_ignored(&mut self, overwrite: bool) - -> &mut CheckoutBuilder<'cb> { + pub fn overwrite_ignored(&mut self, overwrite: bool) -> &mut CheckoutBuilder<'cb> { self.flag(raw::GIT_CHECKOUT_DONT_OVERWRITE_IGNORED, !overwrite) } /// Indicate whether a normal merge file should be written for conflicts. /// /// Defaults to false. - pub fn conflict_style_merge(&mut self, on: bool) - -> &mut CheckoutBuilder<'cb> { + pub fn conflict_style_merge(&mut self, on: bool) -> &mut CheckoutBuilder<'cb> { self.flag(raw::GIT_CHECKOUT_CONFLICT_STYLE_MERGE, on) } @@ -321,8 +475,10 @@ impl<'cb> CheckoutBuilder<'cb> { /// callback. /// /// Defaults to none. - pub fn notify_on(&mut self, notification_types: CheckoutNotificationType) - -> &mut CheckoutBuilder<'cb> { + pub fn notify_on( + &mut self, + notification_types: CheckoutNotificationType, + ) -> &mut CheckoutBuilder<'cb> { self.notify_flags = notification_types; self } @@ -331,14 +487,18 @@ impl<'cb> CheckoutBuilder<'cb> { /// for conflicts. /// /// Defaults to false. - pub fn conflict_style_diff3(&mut self, on: bool) - -> &mut CheckoutBuilder<'cb> { + pub fn conflict_style_diff3(&mut self, on: bool) -> &mut CheckoutBuilder<'cb> { self.flag(raw::GIT_CHECKOUT_CONFLICT_STYLE_DIFF3, on) } + /// Treat paths specified in [`CheckoutBuilder::path`] as exact file paths + /// instead of as pathspecs. + pub fn disable_pathspec_match(&mut self, on: bool) -> &mut CheckoutBuilder<'cb> { + self.flag(raw::GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH, on) + } + /// Indicate whether to apply filters like CRLF conversion. - pub fn disable_filters(&mut self, disable: bool) - -> &mut CheckoutBuilder<'cb> { + pub fn disable_filters(&mut self, disable: bool) -> &mut CheckoutBuilder<'cb> { self.disable_filters = disable; self } @@ -361,11 +521,15 @@ impl<'cb> CheckoutBuilder<'cb> { /// Add a path to be checked out. /// + /// The path is a [pathspec] pattern, unless + /// [`CheckoutBuilder::disable_pathspec_match`] is set. + /// /// If no paths are specified, then all files are checked out. Otherwise /// only these specified paths are checked out. - pub fn path(&mut self, path: T) - -> &mut CheckoutBuilder<'cb> { - let path = path.into_c_string().unwrap(); + /// + /// [pathspec]: https://git-scm.com/docs/gitglossary.html#Documentation/gitglossary.txt-aiddefpathspecapathspec + pub fn path(&mut self, path: T) -> &mut CheckoutBuilder<'cb> { + let path = util::cstring_to_repo_path(path).unwrap(); self.path_ptrs.push(path.as_ptr()); self.paths.push(path); self @@ -373,6 +537,7 @@ impl<'cb> CheckoutBuilder<'cb> { /// Set the directory to check out to pub fn target_dir(&mut self, dst: &Path) -> &mut CheckoutBuilder<'cb> { + // Normal file path OK (does not need Windows conversion). self.target_dir = Some(dst.into_c_string().unwrap()); self } @@ -397,7 +562,9 @@ impl<'cb> CheckoutBuilder<'cb> { /// Set a callback to receive notifications of checkout progress. pub fn progress(&mut self, cb: F) -> &mut CheckoutBuilder<'cb> - where F: FnMut(Option<&Path>, usize, usize) + 'cb { + where + F: FnMut(Option<&Path>, usize, usize) + 'cb, + { self.progress = Some(Box::new(cb) as Box>); self } @@ -407,8 +574,15 @@ impl<'cb> CheckoutBuilder<'cb> { /// Callbacks are invoked prior to modifying any files on disk. /// Returning `false` from the callback will cancel the checkout. pub fn notify(&mut self, cb: F) -> &mut CheckoutBuilder<'cb> - where F: FnMut(CheckoutNotificationType, Option<&Path>, DiffFile, - DiffFile, DiffFile) -> bool + 'cb + where + F: FnMut( + CheckoutNotificationType, + Option<&Path>, + Option>, + Option>, + Option>, + ) -> bool + + 'cb, { self.notify = Some(Box::new(cb) as Box>); self @@ -424,35 +598,29 @@ impl<'cb> CheckoutBuilder<'cb> { opts.dir_mode = self.dir_perm.unwrap_or(0) as c_uint; opts.file_mode = self.file_perm.unwrap_or(0) as c_uint; - if self.path_ptrs.len() > 0 { + if !self.path_ptrs.is_empty() { opts.paths.strings = self.path_ptrs.as_ptr() as *mut _; opts.paths.count = self.path_ptrs.len() as size_t; } - match self.target_dir { - Some(ref c) => opts.target_directory = c.as_ptr(), - None => {} + if let Some(ref c) = self.target_dir { + opts.target_directory = c.as_ptr(); } - match self.ancestor_label { - Some(ref c) => opts.ancestor_label = c.as_ptr(), - None => {} + if let Some(ref c) = self.ancestor_label { + opts.ancestor_label = c.as_ptr(); } - match self.our_label { - Some(ref c) => opts.our_label = c.as_ptr(), - None => {} + if let Some(ref c) = self.our_label { + opts.our_label = c.as_ptr(); } - match self.their_label { - Some(ref c) => opts.their_label = c.as_ptr(), - None => {} + if let Some(ref c) = self.their_label { + opts.their_label = c.as_ptr(); } if self.progress.is_some() { - let f: raw::git_checkout_progress_cb = progress_cb; - opts.progress_cb = Some(f); + opts.progress_cb = Some(progress_cb); opts.progress_payload = self as *mut _ as *mut _; } if self.notify.is_some() { - let f: raw::git_checkout_notify_cb = notify_cb; - opts.notify_cb = Some(f); + opts.notify_cb = Some(notify_cb); opts.notify_payload = self as *mut _ as *mut _; opts.notify_flags = self.notify_flags.bits() as c_uint; } @@ -460,12 +628,14 @@ impl<'cb> CheckoutBuilder<'cb> { } } -extern fn progress_cb(path: *const c_char, - completed: size_t, - total: size_t, - data: *mut c_void) { +extern "C" fn progress_cb( + path: *const c_char, + completed: size_t, + total: size_t, + data: *mut c_void, +) { panic::wrap(|| unsafe { - let payload = &mut *(data as *mut CheckoutBuilder); + let payload = &mut *(data as *mut CheckoutBuilder<'_>); let callback = match payload.progress { Some(ref mut c) => c, None => return, @@ -479,15 +649,17 @@ extern fn progress_cb(path: *const c_char, }); } -extern fn notify_cb(why: raw::git_checkout_notify_t, - path: *const c_char, - baseline: *const raw::git_diff_file, - target: *const raw::git_diff_file, - workdir: *const raw::git_diff_file, - data: *mut c_void) -> c_int { +extern "C" fn notify_cb( + why: raw::git_checkout_notify_t, + path: *const c_char, + baseline: *const raw::git_diff_file, + target: *const raw::git_diff_file, + workdir: *const raw::git_diff_file, + data: *mut c_void, +) -> c_int { // pack callback etc panic::wrap(|| unsafe { - let payload = &mut *(data as *mut CheckoutBuilder); + let payload = &mut *(data as *mut CheckoutBuilder<'_>); let callback = match payload.notify { Some(ref mut c) => c, None => return 0, @@ -498,23 +670,113 @@ extern fn notify_cb(why: raw::git_checkout_notify_t, Some(util::bytes2path(CStr::from_ptr(path).to_bytes())) }; + let baseline = if baseline.is_null() { + None + } else { + Some(DiffFile::from_raw(baseline)) + }; + + let target = if target.is_null() { + None + } else { + Some(DiffFile::from_raw(target)) + }; + + let workdir = if workdir.is_null() { + None + } else { + Some(DiffFile::from_raw(workdir)) + }; + let why = CheckoutNotificationType::from_bits_truncate(why as u32); - let keep_going = callback(why, - path, - DiffFile::from_raw(baseline), - DiffFile::from_raw(target), - DiffFile::from_raw(workdir)); - if keep_going {0} else {1} - }).unwrap_or(2) + let keep_going = callback(why, path, baseline, target, workdir); + if keep_going { + 0 + } else { + 1 + } + }) + .unwrap_or(2) +} + +unsafe impl Send for TreeUpdateBuilder {} + +impl Default for TreeUpdateBuilder { + fn default() -> Self { + Self::new() + } +} + +impl TreeUpdateBuilder { + /// Create a new empty series of updates. + pub fn new() -> Self { + Self { + updates: Vec::new(), + paths: Vec::new(), + } + } + + /// Add an update removing the specified `path` from a tree. + pub fn remove(&mut self, path: T) -> &mut Self { + let path = util::cstring_to_repo_path(path).unwrap(); + let path_ptr = path.as_ptr(); + self.paths.push(path); + self.updates.push(raw::git_tree_update { + action: raw::GIT_TREE_UPDATE_REMOVE, + id: raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }, + filemode: raw::GIT_FILEMODE_UNREADABLE, + path: path_ptr, + }); + self + } + + /// Add an update setting the specified `path` to a specific Oid, whether it currently exists + /// or not. + /// + /// Note that libgit2 does not support an upsert of a previously removed path, or an upsert + /// that changes the type of an object (such as from tree to blob or vice versa). + pub fn upsert(&mut self, path: T, id: Oid, filemode: FileMode) -> &mut Self { + let path = util::cstring_to_repo_path(path).unwrap(); + let path_ptr = path.as_ptr(); + self.paths.push(path); + self.updates.push(raw::git_tree_update { + action: raw::GIT_TREE_UPDATE_UPSERT, + id: unsafe { *id.raw() }, + filemode: u32::from(filemode) as raw::git_filemode_t, + path: path_ptr, + }); + self + } + + /// Create a new tree from the specified baseline and this series of updates. + /// + /// The baseline tree must exist in the specified repository. + pub fn create_updated(&mut self, repo: &Repository, baseline: &Tree<'_>) -> Result { + let mut ret = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_tree_create_updated( + &mut ret, + repo.raw(), + baseline.raw(), + self.updates.len(), + self.updates.as_ptr() + )); + Ok(Binding::from_raw(&ret as *const _)) + } + } } #[cfg(test)] mod tests { + use super::{CheckoutBuilder, RepoBuilder, TreeUpdateBuilder}; + use crate::{CheckoutNotificationType, FileMode, Repository}; use std::fs; use std::path::Path; - use tempdir::TempDir; - use super::RepoBuilder; - use Repository; + use tempfile::TempDir; #[test] fn smoke() { @@ -524,25 +786,87 @@ mod tests { #[test] fn smoke2() { - let td = TempDir::new("test").unwrap(); + let td = TempDir::new().unwrap(); Repository::init_bare(&td.path().join("bare")).unwrap(); let url = if cfg!(unix) { format!("file://{}/bare", td.path().display()) } else { - format!("file:///{}/bare", td.path().display().to_string() - .replace("\\", "/")) + format!( + "file:///{}/bare", + td.path().display().to_string().replace("\\", "/") + ) }; let dst = td.path().join("foo"); RepoBuilder::new().clone(&url, &dst).unwrap(); fs::remove_dir_all(&dst).unwrap(); - RepoBuilder::new().local(false).clone(&url, &dst).unwrap(); - fs::remove_dir_all(&dst).unwrap(); - RepoBuilder::new().local(false).hardlinks(false).bare(true) - .clone(&url, &dst).unwrap(); - fs::remove_dir_all(&dst).unwrap(); - assert!(RepoBuilder::new().branch("foo") - .clone(&url, &dst).is_err()); + assert!(RepoBuilder::new().branch("foo").clone(&url, &dst).is_err()); } + #[test] + fn smoke_tree_create_updated() { + let (_tempdir, repo) = crate::test::repo_init(); + let (_, tree_id) = crate::test::commit(&repo); + let tree = t!(repo.find_tree(tree_id)); + assert!(tree.get_name("bar").is_none()); + let foo_id = tree.get_name("foo").unwrap().id(); + let tree2_id = t!(TreeUpdateBuilder::new() + .remove("foo") + .upsert("bar/baz", foo_id, FileMode::Blob) + .create_updated(&repo, &tree)); + let tree2 = t!(repo.find_tree(tree2_id)); + assert!(tree2.get_name("foo").is_none()); + let baz_id = tree2.get_path(Path::new("bar/baz")).unwrap().id(); + assert_eq!(foo_id, baz_id); + } + + /// Issue regression test #365 + #[test] + fn notify_callback() { + let td = TempDir::new().unwrap(); + let cd = TempDir::new().unwrap(); + + { + let mut opts = crate::RepositoryInitOptions::new(); + opts.initial_head("main"); + let repo = Repository::init_opts(&td.path(), &opts).unwrap(); + + let mut config = repo.config().unwrap(); + config.set_str("user.name", "name").unwrap(); + config.set_str("user.email", "email").unwrap(); + + let mut index = repo.index().unwrap(); + let p = Path::new(td.path()).join("file"); + println!("using path {:?}", p); + fs::File::create(&p).unwrap(); + index.add_path(&Path::new("file")).unwrap(); + let id = index.write_tree().unwrap(); + + let tree = repo.find_tree(id).unwrap(); + let sig = repo.signature().unwrap(); + repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[]) + .unwrap(); + } + + let repo = Repository::open_bare(&td.path().join(".git")).unwrap(); + let tree = repo + .revparse_single(&"main") + .unwrap() + .peel_to_tree() + .unwrap(); + let mut index = repo.index().unwrap(); + index.read_tree(&tree).unwrap(); + + let mut checkout_opts = CheckoutBuilder::new(); + checkout_opts.target_dir(&cd.path()); + checkout_opts.notify_on(CheckoutNotificationType::all()); + checkout_opts.notify(|_notif, _path, baseline, target, workdir| { + assert!(baseline.is_none()); + assert_eq!(target.unwrap().path(), Some(Path::new("file"))); + assert!(workdir.is_none()); + true + }); + repo.checkout_index(Some(&mut index), Some(&mut checkout_opts)) + .unwrap(); + } } diff --git a/src/call.rs b/src/call.rs index 1107e69f4d..9aa3ae667f 100644 --- a/src/call.rs +++ b/src/call.rs @@ -1,19 +1,18 @@ #![macro_use] -use libc; -use Error; +use crate::Error; macro_rules! call { (raw::$p:ident ($($e:expr),*)) => ( - raw::$p($(::call::convert(&$e)),*) + raw::$p($(crate::call::convert(&$e)),*) ) } macro_rules! try_call { (raw::$p:ident ($($e:expr),*)) => ({ - match ::call::try(raw::$p($(::call::convert(&$e)),*)) { + match crate::call::c_try(raw::$p($(crate::call::convert(&$e)),*)) { Ok(o) => o, - Err(e) => { ::panic::check(); return Err(e) } + Err(e) => { crate::panic::check(); return Err(e) } } }) } @@ -23,7 +22,7 @@ macro_rules! try_call_iter { match call!($($f)*) { 0 => {} raw::GIT_ITEROVER => return None, - e => return Some(Err(::call::last_error(e))) + e => return Some(Err(crate::call::last_error(e))) } } } @@ -33,9 +32,11 @@ pub trait Convert { fn convert(&self) -> T; } -pub fn convert>(u: &U) -> T { u.convert() } +pub fn convert>(u: &U) -> T { + u.convert() +} -pub fn try(ret: libc::c_int) -> Result { +pub fn c_try(ret: libc::c_int) -> Result { match ret { n if n < 0 => Err(last_error(n)), n => Ok(n), @@ -43,51 +44,63 @@ pub fn try(ret: libc::c_int) -> Result { } pub fn last_error(code: libc::c_int) -> Error { - // Apparently libgit2 isn't necessarily guaranteed to set the last error - // whenever a function returns a negative value! - Error::last_error(code).unwrap_or_else(|| { - Error::from_str("an unknown error occurred") - }) + Error::last_error(code) } mod impls { use std::ffi::CString; - use libc; + use std::ptr; - use {raw, ConfigLevel, ResetType, ObjectType, BranchType, Direction}; - use {DiffFormat, FileFavor, SubmoduleIgnore, AutotagOption, FetchPrune}; - use call::Convert; + use crate::call::Convert; + use crate::{raw, BranchType, ConfigLevel, Direction, ObjectType, ResetType}; + use crate::{ + AutotagOption, DiffFormat, FetchPrune, FileFavor, SubmoduleIgnore, SubmoduleUpdate, + }; impl Convert for T { - fn convert(&self) -> T { *self } + fn convert(&self) -> T { + *self + } } impl Convert for bool { - fn convert(&self) -> libc::c_int { *self as libc::c_int } + fn convert(&self) -> libc::c_int { + *self as libc::c_int + } } impl<'a, T> Convert<*const T> for &'a T { - fn convert(&self) -> *const T { *self as *const T } + fn convert(&self) -> *const T { + *self as *const T + } } impl<'a, T> Convert<*mut T> for &'a mut T { - fn convert(&self) -> *mut T { &**self as *const T as *mut T } + fn convert(&self) -> *mut T { + &**self as *const T as *mut T + } } impl Convert<*const T> for *mut T { - fn convert(&self) -> *const T { *self as *const T } + fn convert(&self) -> *const T { + *self as *const T + } } impl Convert<*const libc::c_char> for CString { - fn convert(&self) -> *const libc::c_char { self.as_ptr() } + fn convert(&self) -> *const libc::c_char { + self.as_ptr() + } } impl> Convert<*const T> for Option { fn convert(&self) -> *const T { - self.as_ref().map(|s| s.convert()).unwrap_or(0 as *const _) + self.as_ref().map(|s| s.convert()).unwrap_or(ptr::null()) } } impl> Convert<*mut T> for Option { fn convert(&self) -> *mut T { - self.as_ref().map(|s| s.convert()).unwrap_or(0 as *mut _) + self.as_ref() + .map(|s| s.convert()) + .unwrap_or(ptr::null_mut()) } } @@ -110,20 +123,20 @@ mod impls { } } - impl Convert for ObjectType { - fn convert(&self) -> raw::git_otype { + impl Convert for ObjectType { + fn convert(&self) -> raw::git_object_t { match *self { - ObjectType::Any => raw::GIT_OBJ_ANY, - ObjectType::Commit => raw::GIT_OBJ_COMMIT, - ObjectType::Tree => raw::GIT_OBJ_TREE, - ObjectType::Blob => raw::GIT_OBJ_BLOB, - ObjectType::Tag => raw::GIT_OBJ_TAG, + ObjectType::Any => raw::GIT_OBJECT_ANY, + ObjectType::Commit => raw::GIT_OBJECT_COMMIT, + ObjectType::Tree => raw::GIT_OBJECT_TREE, + ObjectType::Blob => raw::GIT_OBJECT_BLOB, + ObjectType::Tag => raw::GIT_OBJECT_TAG, } } } - impl Convert for Option { - fn convert(&self) -> raw::git_otype { + impl Convert for Option { + fn convert(&self) -> raw::git_object_t { self.unwrap_or(ObjectType::Any).convert() } } @@ -151,6 +164,7 @@ mod impls { ConfigLevel::XDG => raw::GIT_CONFIG_LEVEL_XDG, ConfigLevel::Global => raw::GIT_CONFIG_LEVEL_GLOBAL, ConfigLevel::Local => raw::GIT_CONFIG_LEVEL_LOCAL, + ConfigLevel::Worktree => raw::GIT_CONFIG_LEVEL_WORKTREE, ConfigLevel::App => raw::GIT_CONFIG_LEVEL_APP, ConfigLevel::Highest => raw::GIT_CONFIG_HIGHEST_LEVEL, } @@ -165,6 +179,7 @@ mod impls { DiffFormat::Raw => raw::GIT_DIFF_FORMAT_RAW, DiffFormat::NameOnly => raw::GIT_DIFF_FORMAT_NAME_ONLY, DiffFormat::NameStatus => raw::GIT_DIFF_FORMAT_NAME_STATUS, + DiffFormat::PatchId => raw::GIT_DIFF_FORMAT_PATCH_ID, } } } @@ -183,8 +198,7 @@ mod impls { impl Convert for SubmoduleIgnore { fn convert(&self) -> raw::git_submodule_ignore_t { match *self { - SubmoduleIgnore::Unspecified => - raw::GIT_SUBMODULE_IGNORE_UNSPECIFIED, + SubmoduleIgnore::Unspecified => raw::GIT_SUBMODULE_IGNORE_UNSPECIFIED, SubmoduleIgnore::None => raw::GIT_SUBMODULE_IGNORE_NONE, SubmoduleIgnore::Untracked => raw::GIT_SUBMODULE_IGNORE_UNTRACKED, SubmoduleIgnore::Dirty => raw::GIT_SUBMODULE_IGNORE_DIRTY, @@ -193,11 +207,22 @@ mod impls { } } + impl Convert for SubmoduleUpdate { + fn convert(&self) -> raw::git_submodule_update_t { + match *self { + SubmoduleUpdate::Checkout => raw::GIT_SUBMODULE_UPDATE_CHECKOUT, + SubmoduleUpdate::Rebase => raw::GIT_SUBMODULE_UPDATE_REBASE, + SubmoduleUpdate::Merge => raw::GIT_SUBMODULE_UPDATE_MERGE, + SubmoduleUpdate::None => raw::GIT_SUBMODULE_UPDATE_NONE, + SubmoduleUpdate::Default => raw::GIT_SUBMODULE_UPDATE_DEFAULT, + } + } + } + impl Convert for AutotagOption { fn convert(&self) -> raw::git_remote_autotag_option_t { match *self { - AutotagOption::Unspecified => - raw::GIT_REMOTE_DOWNLOAD_TAGS_UNSPECIFIED, + AutotagOption::Unspecified => raw::GIT_REMOTE_DOWNLOAD_TAGS_UNSPECIFIED, AutotagOption::None => raw::GIT_REMOTE_DOWNLOAD_TAGS_NONE, AutotagOption::Auto => raw::GIT_REMOTE_DOWNLOAD_TAGS_AUTO, AutotagOption::All => raw::GIT_REMOTE_DOWNLOAD_TAGS_ALL, diff --git a/src/cert.rs b/src/cert.rs index 70ab9498f8..b232cc3ce8 100644 --- a/src/cert.rs +++ b/src/cert.rs @@ -5,8 +5,8 @@ use std::marker; use std::mem; use std::slice; -use raw; -use util::Binding; +use crate::raw; +use crate::util::Binding; /// A certificate for a remote connection, viewable as one of `CertHostkey` or /// `CertX509` currently. @@ -27,6 +27,54 @@ pub struct CertX509<'a> { _marker: marker::PhantomData<&'a raw::git_cert>, } +/// The SSH host key type. +#[derive(Copy, Clone, Debug)] +#[non_exhaustive] +pub enum SshHostKeyType { + /// Unknown key type + Unknown = raw::GIT_CERT_SSH_RAW_TYPE_UNKNOWN as isize, + /// RSA key type + Rsa = raw::GIT_CERT_SSH_RAW_TYPE_RSA as isize, + /// DSS key type + Dss = raw::GIT_CERT_SSH_RAW_TYPE_DSS as isize, + /// ECDSA 256 key type + Ecdsa256 = raw::GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_256 as isize, + /// ECDSA 384 key type + Ecdsa384 = raw::GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_384 as isize, + /// ECDSA 521 key type + Ecdsa521 = raw::GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_521 as isize, + /// ED25519 key type + Ed255219 = raw::GIT_CERT_SSH_RAW_TYPE_KEY_ED25519 as isize, +} + +impl SshHostKeyType { + /// The name of the key type as encoded in the known_hosts file. + pub fn name(&self) -> &'static str { + match self { + SshHostKeyType::Unknown => "unknown", + SshHostKeyType::Rsa => "ssh-rsa", + SshHostKeyType::Dss => "ssh-dss", + SshHostKeyType::Ecdsa256 => "ecdsa-sha2-nistp256", + SshHostKeyType::Ecdsa384 => "ecdsa-sha2-nistp384", + SshHostKeyType::Ecdsa521 => "ecdsa-sha2-nistp521", + SshHostKeyType::Ed255219 => "ssh-ed25519", + } + } + + /// A short name of the key type, the colloquial form used as a human-readable description. + pub fn short_name(&self) -> &'static str { + match self { + SshHostKeyType::Unknown => "Unknown", + SshHostKeyType::Rsa => "RSA", + SshHostKeyType::Dss => "DSA", + SshHostKeyType::Ecdsa256 => "ECDSA", + SshHostKeyType::Ecdsa384 => "ECDSA", + SshHostKeyType::Ecdsa521 => "ECDSA", + SshHostKeyType::Ed255219 => "ED25519", + } + } +} + impl<'a> Cert<'a> { /// Attempt to view this certificate as an SSH hostkey. /// @@ -76,22 +124,68 @@ impl<'a> CertHostkey<'a> { } } } + + /// Returns the SHA-256 hash of the hostkey, if available. + pub fn hash_sha256(&self) -> Option<&[u8; 32]> { + unsafe { + if (*self.raw).kind as u32 & raw::GIT_CERT_SSH_SHA256 as u32 == 0 { + None + } else { + Some(&(*self.raw).hash_sha256) + } + } + } + + /// Returns the raw host key. + pub fn hostkey(&self) -> Option<&[u8]> { + unsafe { + if (*self.raw).kind & raw::GIT_CERT_SSH_RAW == 0 { + return None; + } + Some(slice::from_raw_parts( + (*self.raw).hostkey as *const u8, + (*self.raw).hostkey_len as usize, + )) + } + } + + /// Returns the type of the host key. + pub fn hostkey_type(&self) -> Option { + unsafe { + if (*self.raw).kind & raw::GIT_CERT_SSH_RAW == 0 { + return None; + } + let t = match (*self.raw).raw_type { + raw::GIT_CERT_SSH_RAW_TYPE_UNKNOWN => SshHostKeyType::Unknown, + raw::GIT_CERT_SSH_RAW_TYPE_RSA => SshHostKeyType::Rsa, + raw::GIT_CERT_SSH_RAW_TYPE_DSS => SshHostKeyType::Dss, + raw::GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_256 => SshHostKeyType::Ecdsa256, + raw::GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_384 => SshHostKeyType::Ecdsa384, + raw::GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_521 => SshHostKeyType::Ecdsa521, + raw::GIT_CERT_SSH_RAW_TYPE_KEY_ED25519 => SshHostKeyType::Ed255219, + t => panic!("unexpected host key type {:?}", t), + }; + Some(t) + } + } } impl<'a> CertX509<'a> { /// Return the X.509 certificate data as a byte slice pub fn data(&self) -> &[u8] { - unsafe { - slice::from_raw_parts((*self.raw).data as *const u8, - (*self.raw).len as usize) - } + unsafe { slice::from_raw_parts((*self.raw).data as *const u8, (*self.raw).len as usize) } } } impl<'a> Binding for Cert<'a> { type Raw = *mut raw::git_cert; unsafe fn from_raw(raw: *mut raw::git_cert) -> Cert<'a> { - Cert { raw: raw, _marker: marker::PhantomData } + Cert { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_cert { + self.raw } - fn raw(&self) -> *mut raw::git_cert { self.raw } } diff --git a/src/cherrypick.rs b/src/cherrypick.rs new file mode 100644 index 0000000000..659b73089b --- /dev/null +++ b/src/cherrypick.rs @@ -0,0 +1,72 @@ +use std::mem; + +use crate::build::CheckoutBuilder; +use crate::merge::MergeOptions; +use crate::raw; +use std::ptr; + +/// Options to specify when cherry picking +pub struct CherrypickOptions<'cb> { + mainline: u32, + checkout_builder: Option>, + merge_opts: Option, +} + +impl<'cb> CherrypickOptions<'cb> { + /// Creates a default set of cherrypick options + pub fn new() -> CherrypickOptions<'cb> { + CherrypickOptions { + mainline: 0, + checkout_builder: None, + merge_opts: None, + } + } + + /// Set the mainline value + /// + /// For merge commits, the "mainline" is treated as the parent. + pub fn mainline(&mut self, mainline: u32) -> &mut Self { + self.mainline = mainline; + self + } + + /// Set the checkout builder + pub fn checkout_builder(&mut self, cb: CheckoutBuilder<'cb>) -> &mut Self { + self.checkout_builder = Some(cb); + self + } + + /// Set the merge options + pub fn merge_opts(&mut self, merge_opts: MergeOptions) -> &mut Self { + self.merge_opts = Some(merge_opts); + self + } + + /// Obtain the raw struct + pub fn raw(&mut self) -> raw::git_cherrypick_options { + unsafe { + let mut checkout_opts: raw::git_checkout_options = mem::zeroed(); + raw::git_checkout_init_options(&mut checkout_opts, raw::GIT_CHECKOUT_OPTIONS_VERSION); + if let Some(ref mut cb) = self.checkout_builder { + cb.configure(&mut checkout_opts); + } + + let mut merge_opts: raw::git_merge_options = mem::zeroed(); + raw::git_merge_init_options(&mut merge_opts, raw::GIT_MERGE_OPTIONS_VERSION); + if let Some(ref opts) = self.merge_opts { + ptr::copy(opts.raw(), &mut merge_opts, 1); + } + + let mut cherrypick_opts: raw::git_cherrypick_options = mem::zeroed(); + raw::git_cherrypick_init_options( + &mut cherrypick_opts, + raw::GIT_CHERRYPICK_OPTIONS_VERSION, + ); + cherrypick_opts.mainline = self.mainline; + cherrypick_opts.checkout_opts = checkout_opts; + cherrypick_opts.merge_opts = merge_opts; + + cherrypick_opts + } + } +} diff --git a/src/commit.rs b/src/commit.rs index 91929d15e5..7fef508096 100644 --- a/src/commit.rs +++ b/src/commit.rs @@ -1,11 +1,12 @@ +use std::iter::FusedIterator; use std::marker; use std::mem; use std::ops::Range; +use std::ptr; use std::str; -use libc; -use {raw, signature, Oid, Error, Signature, Tree, Time, Object}; -use util::Binding; +use crate::util::Binding; +use crate::{raw, signature, Buf, Error, IntoCString, Mailmap, Object, Oid, Signature, Time, Tree}; /// A structure to represent a git [commit][1] /// @@ -16,12 +17,16 @@ pub struct Commit<'repo> { } /// An iterator over the parent commits of a commit. -pub struct Parents<'commit, 'repo: 'commit> { +/// +/// Aborts iteration when a commit cannot be found +pub struct Parents<'commit, 'repo> { range: Range, commit: &'commit Commit<'repo>, } /// An iterator over the parent commits' ids of a commit. +/// +/// Aborts iteration when a commit cannot be found pub struct ParentIds<'commit> { range: Range, commit: &'commit Commit<'commit>, @@ -42,7 +47,7 @@ impl<'repo> Commit<'repo> { /// Get the tree pointed to by a commit. pub fn tree(&self) -> Result, Error> { - let mut ret = 0 as *mut raw::git_tree; + let mut ret = ptr::null_mut(); unsafe { try_call!(raw::git_commit_tree(&mut ret, &*self.raw)); Ok(Binding::from_raw(ret)) @@ -50,7 +55,9 @@ impl<'repo> Commit<'repo> { } /// Get access to the underlying raw pointer. - pub fn raw(&self) -> *mut raw::git_commit { self.raw } + pub fn raw(&self) -> *mut raw::git_commit { + self.raw + } /// Get the full message of a commit. /// @@ -67,9 +74,7 @@ impl<'repo> Commit<'repo> { /// The returned message will be slightly prettified by removing any /// potential leading newlines. pub fn message_bytes(&self) -> &[u8] { - unsafe { - ::opt_bytes(self, raw::git_commit_message(&*self.raw)).unwrap() - } + unsafe { crate::opt_bytes(self, raw::git_commit_message(&*self.raw)).unwrap() } } /// Get the encoding for the message of a commit, as a string representing a @@ -77,10 +82,8 @@ impl<'repo> Commit<'repo> { /// /// `None` will be returned if the encoding is not known pub fn message_encoding(&self) -> Option<&str> { - let bytes = unsafe { - ::opt_bytes(self, raw::git_commit_message(&*self.raw)) - }; - bytes.map(|b| str::from_utf8(b).unwrap()) + let bytes = unsafe { crate::opt_bytes(self, raw::git_commit_message_encoding(&*self.raw)) }; + bytes.and_then(|b| str::from_utf8(b).ok()) } /// Get the full raw message of a commit. @@ -92,9 +95,7 @@ impl<'repo> Commit<'repo> { /// Get the full raw message of a commit. pub fn message_raw_bytes(&self) -> &[u8] { - unsafe { - ::opt_bytes(self, raw::git_commit_message_raw(&*self.raw)).unwrap() - } + unsafe { crate::opt_bytes(self, raw::git_commit_message_raw(&*self.raw)).unwrap() } } /// Get the full raw text of the commit header. @@ -104,11 +105,23 @@ impl<'repo> Commit<'repo> { str::from_utf8(self.raw_header_bytes()).ok() } - /// Get the full raw text of the commit header. - pub fn raw_header_bytes(&self) -> &[u8] { + /// Get an arbitrary header field. + pub fn header_field_bytes(&self, field: T) -> Result { + let buf = Buf::new(); + let raw_field = field.into_c_string()?; unsafe { - ::opt_bytes(self, raw::git_commit_raw_header(&*self.raw)).unwrap() + try_call!(raw::git_commit_header_field( + buf.raw(), + &*self.raw, + raw_field + )); } + Ok(buf) + } + + /// Get the full raw text of the commit header. + pub fn raw_header_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, raw::git_commit_raw_header(&*self.raw)).unwrap() } } /// Get the short "summary" of the git commit message. @@ -118,7 +131,7 @@ impl<'repo> Commit<'repo> { /// /// `None` may be returned if an error occurs or if the summary is not valid /// utf-8. - pub fn summary(&mut self) -> Option<&str> { + pub fn summary(&self) -> Option<&str> { self.summary_bytes().and_then(|s| str::from_utf8(s).ok()) } @@ -128,8 +141,31 @@ impl<'repo> Commit<'repo> { /// paragraph of the message with whitespace trimmed and squashed. /// /// `None` may be returned if an error occurs - pub fn summary_bytes(&mut self) -> Option<&[u8]> { - unsafe { ::opt_bytes(self, raw::git_commit_summary(self.raw)) } + pub fn summary_bytes(&self) -> Option<&[u8]> { + unsafe { crate::opt_bytes(self, raw::git_commit_summary(self.raw)) } + } + + /// Get the long "body" of the git commit message. + /// + /// The returned message is the body of the commit, comprising everything + /// but the first paragraph of the message. Leading and trailing whitespaces + /// are trimmed. + /// + /// `None` may be returned if an error occurs or if the summary is not valid + /// utf-8. + pub fn body(&self) -> Option<&str> { + self.body_bytes().and_then(|s| str::from_utf8(s).ok()) + } + + /// Get the long "body" of the git commit message. + /// + /// The returned message is the body of the commit, comprising everything + /// but the first paragraph of the message. Leading and trailing whitespaces + /// are trimmed. + /// + /// `None` may be returned if an error occurs. + pub fn body_bytes(&self) -> Option<&[u8]> { + unsafe { crate::opt_bytes(self, raw::git_commit_body(self.raw)) } } /// Get the commit time (i.e. committer time) of a commit. @@ -139,85 +175,137 @@ impl<'repo> Commit<'repo> { /// committer's preferred time zone. pub fn time(&self) -> Time { unsafe { - Time::new(raw::git_commit_time(&*self.raw) as i64, - raw::git_commit_time_offset(&*self.raw) as i32) + Time::new( + raw::git_commit_time(&*self.raw) as i64, + raw::git_commit_time_offset(&*self.raw) as i32, + ) } } /// Creates a new iterator over the parents of this commit. pub fn parents<'a>(&'a self) -> Parents<'a, 'repo> { - let max = unsafe { raw::git_commit_parentcount(&*self.raw) as usize }; - Parents { range: 0..max, commit: self } + Parents { + range: 0..self.parent_count(), + commit: self, + } } /// Creates a new iterator over the parents of this commit. - pub fn parent_ids(&self) -> ParentIds { - let max = unsafe { raw::git_commit_parentcount(&*self.raw) as usize }; - ParentIds { range: 0..max, commit: self } + pub fn parent_ids(&self) -> ParentIds<'_> { + ParentIds { + range: 0..self.parent_count(), + commit: self, + } } /// Get the author of this commit. - pub fn author(&self) -> Signature { + pub fn author(&self) -> Signature<'_> { unsafe { let ptr = raw::git_commit_author(&*self.raw); signature::from_raw_const(self, ptr) } } + /// Get the author of this commit, using the mailmap to map names and email + /// addresses to canonical real names and email addresses. + pub fn author_with_mailmap(&self, mailmap: &Mailmap) -> Result, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_commit_author_with_mailmap( + &mut ret, + &*self.raw, + &*mailmap.raw() + )); + Ok(Binding::from_raw(ret)) + } + } + /// Get the committer of this commit. - pub fn committer(&self) -> Signature { + pub fn committer(&self) -> Signature<'_> { unsafe { let ptr = raw::git_commit_committer(&*self.raw); signature::from_raw_const(self, ptr) } } + /// Get the committer of this commit, using the mailmap to map names and email + /// addresses to canonical real names and email addresses. + pub fn committer_with_mailmap(&self, mailmap: &Mailmap) -> Result, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_commit_committer_with_mailmap( + &mut ret, + &*self.raw, + &*mailmap.raw() + )); + Ok(Binding::from_raw(ret)) + } + } + /// Amend this existing commit with all non-`None` values /// /// This creates a new commit that is exactly the same as the old commit, /// except that any non-`None` values will be updated. The new commit has /// the same parents as the old commit. /// - /// For information about `update_ref`, see `new`. - pub fn amend(&self, - update_ref: Option<&str>, - author: Option<&Signature>, - committer: Option<&Signature>, - message_encoding: Option<&str>, - message: Option<&str>, - tree: Option<&Tree<'repo>>) -> Result { - let mut raw = raw::git_oid { id: [0; raw::GIT_OID_RAWSZ] }; - let update_ref = try!(::opt_cstr(update_ref)); - let encoding = try!(::opt_cstr(message_encoding)); - let message = try!(::opt_cstr(message)); + /// For information about `update_ref`, see [`Repository::commit`]. + /// + /// [`Repository::commit`]: struct.Repository.html#method.commit + pub fn amend( + &self, + update_ref: Option<&str>, + author: Option<&Signature<'_>>, + committer: Option<&Signature<'_>>, + message_encoding: Option<&str>, + message: Option<&str>, + tree: Option<&Tree<'repo>>, + ) -> Result { + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + let update_ref = crate::opt_cstr(update_ref)?; + let encoding = crate::opt_cstr(message_encoding)?; + let message = crate::opt_cstr(message)?; unsafe { - try_call!(raw::git_commit_amend(&mut raw, - self.raw(), - update_ref, - author.map(|s| s.raw()), - committer.map(|s| s.raw()), - encoding, - message, - tree.map(|t| t.raw()))); + try_call!(raw::git_commit_amend( + &mut raw, + self.raw(), + update_ref, + author.map(|s| s.raw()), + committer.map(|s| s.raw()), + encoding, + message, + tree.map(|t| t.raw()) + )); Ok(Binding::from_raw(&raw as *const _)) } } + /// Get the number of parents of this commit. + /// + /// Use the `parents` iterator to return an iterator over all parents. + pub fn parent_count(&self) -> usize { + unsafe { raw::git_commit_parentcount(&*self.raw) as usize } + } + /// Get the specified parent of the commit. /// /// Use the `parents` iterator to return an iterator over all parents. pub fn parent(&self, i: usize) -> Result, Error> { unsafe { - let mut raw = 0 as *mut raw::git_commit; - try_call!(raw::git_commit_parent(&mut raw, &*self.raw, - i as libc::c_uint)); + let mut raw = ptr::null_mut(); + try_call!(raw::git_commit_parent( + &mut raw, + &*self.raw, + i as libc::c_uint + )); Ok(Binding::from_raw(raw)) } } /// Get the specified parent id of the commit. /// - /// This is different from `parent`, which will attemptstempt to load the + /// This is different from `parent`, which will attempt to load the /// parent commit from the ODB. /// /// Use the `parent_ids` iterator to return an iterator over all parents. @@ -234,17 +322,13 @@ impl<'repo> Commit<'repo> { /// Casts this Commit to be usable as an `Object` pub fn as_object(&self) -> &Object<'repo> { - unsafe { - &*(self as *const _ as *const Object<'repo>) - } + unsafe { &*(self as *const _ as *const Object<'repo>) } } /// Consumes Commit to be returned as an `Object` pub fn into_object(self) -> Object<'repo> { - assert_eq!(mem::size_of_val(&self), mem::size_of::()); - unsafe { - mem::transmute(self) - } + assert_eq!(mem::size_of_val(&self), mem::size_of::>()); + unsafe { mem::transmute(self) } } } @@ -252,46 +336,82 @@ impl<'repo> Binding for Commit<'repo> { type Raw = *mut raw::git_commit; unsafe fn from_raw(raw: *mut raw::git_commit) -> Commit<'repo> { Commit { - raw: raw, + raw, _marker: marker::PhantomData, } } - fn raw(&self) -> *mut raw::git_commit { self.raw } + fn raw(&self) -> *mut raw::git_commit { + self.raw + } } +impl<'repo> std::fmt::Debug for Commit<'repo> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut ds = f.debug_struct("Commit"); + ds.field("id", &self.id()); + if let Some(summary) = self.summary() { + ds.field("summary", &summary); + } + ds.finish() + } +} +/// Aborts iteration when a commit cannot be found impl<'repo, 'commit> Iterator for Parents<'commit, 'repo> { type Item = Commit<'repo>; fn next(&mut self) -> Option> { - self.range.next().map(|i| self.commit.parent(i).unwrap()) + self.range.next().and_then(|i| self.commit.parent(i).ok()) + } + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() } - fn size_hint(&self) -> (usize, Option) { self.range.size_hint() } } +/// Aborts iteration when a commit cannot be found impl<'repo, 'commit> DoubleEndedIterator for Parents<'commit, 'repo> { fn next_back(&mut self) -> Option> { - self.range.next_back().map(|i| self.commit.parent(i).unwrap()) + self.range + .next_back() + .and_then(|i| self.commit.parent(i).ok()) } } +impl<'repo, 'commit> FusedIterator for Parents<'commit, 'repo> {} + impl<'repo, 'commit> ExactSizeIterator for Parents<'commit, 'repo> {} +/// Aborts iteration when a commit cannot be found impl<'commit> Iterator for ParentIds<'commit> { type Item = Oid; fn next(&mut self) -> Option { - self.range.next().map(|i| self.commit.parent_id(i).unwrap()) + self.range + .next() + .and_then(|i| self.commit.parent_id(i).ok()) + } + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() } - fn size_hint(&self) -> (usize, Option) { self.range.size_hint() } } +/// Aborts iteration when a commit cannot be found impl<'commit> DoubleEndedIterator for ParentIds<'commit> { fn next_back(&mut self) -> Option { - self.range.next_back().map(|i| self.commit.parent_id(i).unwrap()) + self.range + .next_back() + .and_then(|i| self.commit.parent_id(i).ok()) } } +impl<'commit> FusedIterator for ParentIds<'commit> {} + impl<'commit> ExactSizeIterator for ParentIds<'commit> {} +impl<'repo> Clone for Commit<'repo> { + fn clone(&self) -> Self { + self.as_object().clone().into_commit().ok().unwrap() + } +} + impl<'repo> Drop for Commit<'repo> { fn drop(&mut self) { unsafe { raw::git_commit_free(self.raw) } @@ -302,20 +422,27 @@ impl<'repo> Drop for Commit<'repo> { mod tests { #[test] fn smoke() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let head = repo.head().unwrap(); let target = head.target().unwrap(); - let mut commit = repo.find_commit(target).unwrap(); - assert_eq!(commit.message(), Some("initial")); + let commit = repo.find_commit(target).unwrap(); + assert_eq!(commit.message(), Some("initial\n\nbody")); + assert_eq!(commit.body(), Some("body")); assert_eq!(commit.id(), target); commit.message_raw().unwrap(); commit.raw_header().unwrap(); commit.message_encoding(); commit.summary().unwrap(); + commit.body().unwrap(); commit.tree_id(); commit.tree().unwrap(); assert_eq!(commit.parents().count(), 0); + let tree_header_bytes = commit.header_field_bytes("tree").unwrap(); + assert_eq!( + crate::Oid::from_str(tree_header_bytes.as_str().unwrap()).unwrap(), + commit.tree_id() + ); assert_eq!(commit.author().name(), Some("name")); assert_eq!(commit.author().email(), Some("email")); assert_eq!(commit.committer().name(), Some("name")); @@ -323,18 +450,23 @@ mod tests { let sig = repo.signature().unwrap(); let tree = repo.find_tree(commit.tree_id()).unwrap(); - let id = repo.commit(Some("HEAD"), &sig, &sig, "bar", &tree, - &[&commit]).unwrap(); + let id = repo + .commit(Some("HEAD"), &sig, &sig, "bar", &tree, &[&commit]) + .unwrap(); let head = repo.find_commit(id).unwrap(); - let new_head = head.amend(Some("HEAD"), None, None, None, - Some("new message"), None).unwrap(); + let new_head = head + .amend(Some("HEAD"), None, None, None, Some("new message"), None) + .unwrap(); let new_head = repo.find_commit(new_head).unwrap(); assert_eq!(new_head.message(), Some("new message")); new_head.into_object(); repo.find_object(target, None).unwrap().as_commit().unwrap(); - repo.find_object(target, None).unwrap().into_commit().ok().unwrap(); + repo.find_object(target, None) + .unwrap() + .into_commit() + .ok() + .unwrap(); } } - diff --git a/src/config.rs b/src/config.rs index 36d0f26596..9ba0a6da64 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,11 +1,11 @@ use std::ffi::CString; use std::marker; use std::path::{Path, PathBuf}; +use std::ptr; use std::str; -use libc; -use {raw, Error, ConfigLevel, Buf, IntoCString}; -use util::{self, Binding}; +use crate::util::{self, Binding}; +use crate::{raw, Buf, ConfigLevel, Error, IntoCString}; /// A structure representing a git configuration key/value store pub struct Config { @@ -22,8 +22,39 @@ pub struct ConfigEntry<'cfg> { } /// An iterator over the `ConfigEntry` values of a `Config` structure. +/// +/// Due to lifetime restrictions, `ConfigEntries` does not implement the +/// standard [`Iterator`] trait. It provides a [`next`] function which only +/// allows access to one entry at a time. [`for_each`] is available as a +/// convenience function. +/// +/// [`next`]: ConfigEntries::next +/// [`for_each`]: ConfigEntries::for_each +/// +/// # Example +/// +/// ``` +/// // Example of how to collect all entries. +/// use git2::Config; +/// +/// let config = Config::new()?; +/// let iter = config.entries(None)?; +/// let mut entries = Vec::new(); +/// iter +/// .for_each(|entry| { +/// let name = entry.name().unwrap().to_string(); +/// let value = entry.value().unwrap_or("").to_string(); +/// entries.push((name, value)) +/// })?; +/// for entry in &entries { +/// println!("{} = {}", entry.0, entry.1); +/// } +/// # Ok::<(), git2::Error>(()) +/// +/// ``` pub struct ConfigEntries<'cfg> { raw: *mut raw::git_config_iterator, + current: Option>, _marker: marker::PhantomData<&'cfg Config>, } @@ -33,8 +64,8 @@ impl Config { /// This object is empty, so you have to add a file to it before you can do /// anything with it. pub fn new() -> Result { - ::init(); - let mut raw = 0 as *mut raw::git_config; + crate::init(); + let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_config_new(&mut raw)); Ok(Binding::from_raw(raw)) @@ -43,9 +74,10 @@ impl Config { /// Create a new config instance containing a single on-disk file pub fn open(path: &Path) -> Result { - ::init(); - let mut raw = 0 as *mut raw::git_config; - let path = try!(path.into_c_string()); + crate::init(); + let mut raw = ptr::null_mut(); + // Normal file path OK (does not need Windows conversion). + let path = path.into_c_string()?; unsafe { try_call!(raw::git_config_open_ondisk(&mut raw, path)); Ok(Binding::from_raw(raw)) @@ -58,8 +90,8 @@ impl Config { /// files and opens them into a single prioritized config object that can /// be used when accessing default config data outside a repository. pub fn open_default() -> Result { - ::init(); - let mut raw = 0 as *mut raw::git_config; + crate::init(); + let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_config_open_default(&mut raw)); Ok(Binding::from_raw(raw)) @@ -75,33 +107,39 @@ impl Config { /// exists. The returned path may be used on any method call to load /// the global configuration file. /// - /// This method will not guess the path to the xdg compatible config file + /// This method will not guess the path to the XDG compatible config file /// (`.config/git/config`). pub fn find_global() -> Result { - ::init(); + crate::init(); let buf = Buf::new(); - unsafe { try_call!(raw::git_config_find_global(buf.raw())); } + unsafe { + try_call!(raw::git_config_find_global(buf.raw())); + } Ok(util::bytes2path(&buf).to_path_buf()) } /// Locate the path to the system configuration file /// - /// If /etc/gitconfig doesn't exist, it will look for %PROGRAMFILES% + /// If /etc/gitconfig doesn't exist, it will look for `%PROGRAMFILES%` pub fn find_system() -> Result { - ::init(); + crate::init(); let buf = Buf::new(); - unsafe { try_call!(raw::git_config_find_system(buf.raw())); } + unsafe { + try_call!(raw::git_config_find_system(buf.raw())); + } Ok(util::bytes2path(&buf).to_path_buf()) } - /// Locate the path to the global xdg compatible configuration file + /// Locate the path to the global XDG compatible configuration file /// - /// The xdg compatible configuration file is usually located in + /// The XDG compatible configuration file is usually located in /// `$HOME/.config/git/config`. pub fn find_xdg() -> Result { - ::init(); + crate::init(); let buf = Buf::new(); - unsafe { try_call!(raw::git_config_find_xdg(buf.raw())); } + unsafe { + try_call!(raw::git_config_find_xdg(buf.raw())); + } Ok(util::bytes2path(&buf).to_path_buf()) } @@ -114,12 +152,17 @@ impl Config { /// Further queries on this config object will access each of the config /// file instances in order (instances with a higher priority level will be /// accessed first). - pub fn add_file(&mut self, path: &Path, level: ConfigLevel, - force: bool) -> Result<(), Error> { - let path = try!(path.into_c_string()); + pub fn add_file(&mut self, path: &Path, level: ConfigLevel, force: bool) -> Result<(), Error> { + // Normal file path OK (does not need Windows conversion). + let path = path.into_c_string()?; unsafe { - try_call!(raw::git_config_add_file_ondisk(self.raw, path, level, - force)); + try_call!(raw::git_config_add_file_ondisk( + self.raw, + path, + level, + ptr::null(), + force + )); Ok(()) } } @@ -127,13 +170,26 @@ impl Config { /// Delete a config variable from the config file with the highest level /// (usually the local one). pub fn remove(&mut self, name: &str) -> Result<(), Error> { - let name = try!(CString::new(name)); + let name = CString::new(name)?; unsafe { try_call!(raw::git_config_delete_entry(self.raw, name)); Ok(()) } } + /// Remove multivar config variables in the config file with the highest level (usually the + /// local one). + /// + /// The regular expression is applied case-sensitively on the value. + pub fn remove_multivar(&mut self, name: &str, regexp: &str) -> Result<(), Error> { + let name = CString::new(name)?; + let regexp = CString::new(regexp)?; + unsafe { + try_call!(raw::git_config_delete_multivar(self.raw, name, regexp)); + } + Ok(()) + } + /// Get the value of a boolean config variable. /// /// All config files will be looked into, in the order of their defined @@ -141,12 +197,11 @@ impl Config { /// the variable will be returned here. pub fn get_bool(&self, name: &str) -> Result { let mut out = 0 as libc::c_int; - let name = try!(CString::new(name)); + let name = CString::new(name)?; unsafe { try_call!(raw::git_config_get_bool(&mut out, &*self.raw, name)); - } - Ok(if out == 0 {false} else {true}) + Ok(out != 0) } /// Get the value of an integer config variable. @@ -156,10 +211,9 @@ impl Config { /// the variable will be returned here. pub fn get_i32(&self, name: &str) -> Result { let mut out = 0i32; - let name = try!(CString::new(name)); + let name = CString::new(name)?; unsafe { try_call!(raw::git_config_get_int32(&mut out, &*self.raw, name)); - } Ok(out) } @@ -171,7 +225,7 @@ impl Config { /// the variable will be returned here. pub fn get_i64(&self, name: &str) -> Result { let mut out = 0i64; - let name = try!(CString::new(name)); + let name = CString::new(name)?; unsafe { try_call!(raw::git_config_get_int64(&mut out, &*self.raw, name)); } @@ -182,52 +236,71 @@ impl Config { /// /// This is the same as `get_bytes` except that it may return `Err` if /// the bytes are not valid utf-8. + /// + /// For consistency reasons, this method can only be called on a [`snapshot`]. + /// An error will be returned otherwise. + /// + /// [`snapshot`]: `crate::Config::snapshot` pub fn get_str(&self, name: &str) -> Result<&str, Error> { - str::from_utf8(try!(self.get_bytes(name))).map_err(|_| { - Error::from_str("configuration value is not valid utf8") - }) + str::from_utf8(self.get_bytes(name)?) + .map_err(|_| Error::from_str("configuration value is not valid utf8")) } /// Get the value of a string config variable as a byte slice. /// - /// This method will return an error if this `Config` is not a snapshot. + /// For consistency reasons, this method can only be called on a [`snapshot`]. + /// An error will be returned otherwise. + /// + /// [`snapshot`]: `crate::Config::snapshot` pub fn get_bytes(&self, name: &str) -> Result<&[u8], Error> { - let mut ret = 0 as *const libc::c_char; - let name = try!(CString::new(name)); + let mut ret = ptr::null(); + let name = CString::new(name)?; unsafe { try_call!(raw::git_config_get_string(&mut ret, &*self.raw, name)); - Ok(::opt_bytes(self, ret).unwrap()) + Ok(crate::opt_bytes(self, ret).unwrap()) } } /// Get the value of a string config variable as an owned string. /// + /// All config files will be looked into, in the order of their + /// defined level. A higher level means a higher priority. The + /// first occurrence of the variable will be returned here. + /// /// An error will be returned if the config value is not valid utf-8. pub fn get_string(&self, name: &str) -> Result { let ret = Buf::new(); - let name = try!(CString::new(name)); + let name = CString::new(name)?; unsafe { try_call!(raw::git_config_get_string_buf(ret.raw(), self.raw, name)); } - str::from_utf8(&ret).map(|s| s.to_string()).map_err(|_| { - Error::from_str("configuration value is not valid utf8") - }) + str::from_utf8(&ret) + .map(|s| s.to_string()) + .map_err(|_| Error::from_str("configuration value is not valid utf8")) } - /// Get the value of a path config variable as an owned . + /// Get the value of a path config variable as an owned `PathBuf`. + /// + /// A leading '~' will be expanded to the global search path (which + /// defaults to the user's home directory but can be overridden via + /// [`raw::git_libgit2_opts`]. + /// + /// All config files will be looked into, in the order of their + /// defined level. A higher level means a higher priority. The + /// first occurrence of the variable will be returned here. pub fn get_path(&self, name: &str) -> Result { let ret = Buf::new(); - let name = try!(CString::new(name)); + let name = CString::new(name)?; unsafe { try_call!(raw::git_config_get_path(ret.raw(), self.raw, name)); } - Ok(::util::bytes2path(&ret).to_path_buf()) + Ok(crate::util::bytes2path(&ret).to_path_buf()) } /// Get the ConfigEntry for a config variable. - pub fn get_entry(&self, name: &str) -> Result { - let mut ret = 0 as *mut raw::git_config_entry; - let name = try!(CString::new(name)); + pub fn get_entry(&self, name: &str) -> Result, Error> { + let mut ret = ptr::null_mut(); + let name = CString::new(name)?; unsafe { try_call!(raw::git_config_get_entry(&mut ret, self.raw, name)); Ok(Binding::from_raw(ret)) @@ -239,28 +312,33 @@ impl Config { /// If `glob` is `Some`, then the iterator will only iterate over all /// variables whose name matches the pattern. /// + /// The regular expression is applied case-sensitively on the normalized form of + /// the variable name: the section and variable parts are lower-cased. The + /// subsection is left unchanged. + /// + /// Due to lifetime restrictions, the returned value does not implement + /// the standard [`Iterator`] trait. See [`ConfigEntries`] for more. + /// /// # Example /// /// ``` - /// # #![allow(unstable)] /// use git2::Config; /// /// let cfg = Config::new().unwrap(); /// - /// for entry in &cfg.entries(None).unwrap() { + /// let mut entries = cfg.entries(None).unwrap(); + /// while let Some(entry) = entries.next() { /// let entry = entry.unwrap(); /// println!("{} => {}", entry.name().unwrap(), entry.value().unwrap()); /// } /// ``` - pub fn entries(&self, glob: Option<&str>) -> Result { - let mut ret = 0 as *mut raw::git_config_iterator; + pub fn entries(&self, glob: Option<&str>) -> Result, Error> { + let mut ret = ptr::null_mut(); unsafe { match glob { Some(s) => { - let s = try!(CString::new(s)); - try_call!(raw::git_config_iterator_glob_new(&mut ret, - &*self.raw, - s)); + let s = CString::new(s)?; + try_call!(raw::git_config_iterator_glob_new(&mut ret, &*self.raw, s)); } None => { try_call!(raw::git_config_iterator_new(&mut ret, &*self.raw)); @@ -270,14 +348,37 @@ impl Config { } } + /// Iterate over the values of a multivar + /// + /// If `regexp` is `Some`, then the iterator will only iterate over all + /// values which match the pattern. + /// + /// The regular expression is applied case-sensitively on the normalized form of + /// the variable name: the section and variable parts are lower-cased. The + /// subsection is left unchanged. + /// + /// Due to lifetime restrictions, the returned value does not implement + /// the standard [`Iterator`] trait. See [`ConfigEntries`] for more. + pub fn multivar(&self, name: &str, regexp: Option<&str>) -> Result, Error> { + let mut ret = ptr::null_mut(); + let name = CString::new(name)?; + let regexp = regexp.map(CString::new).transpose()?; + unsafe { + try_call!(raw::git_config_multivar_iterator_new( + &mut ret, &*self.raw, name, regexp + )); + Ok(Binding::from_raw(ret)) + } + } + /// Open the global/XDG configuration file according to git's rules /// /// Git allows you to store your global configuration at `$HOME/.config` or - /// `$XDG_CONFIG_HOME/git/config`. For backwards compatability, the XDG file + /// `$XDG_CONFIG_HOME/git/config`. For backwards compatibility, the XDG file /// shouldn't be used unless the use has created it explicitly. With this /// function you'll open the correct one to write to. pub fn open_global(&mut self) -> Result { - let mut raw = 0 as *mut raw::git_config; + let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_config_open_global(&mut raw, self.raw)); Ok(Binding::from_raw(raw)) @@ -289,7 +390,7 @@ impl Config { /// The returned config object can be used to perform get/set/delete /// operations on a single specific level. pub fn open_level(&self, level: ConfigLevel) -> Result { - let mut raw = 0 as *mut raw::git_config; + let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_config_open_level(&mut raw, &*self.raw, level)); Ok(Binding::from_raw(raw)) @@ -299,7 +400,7 @@ impl Config { /// Set the value of a boolean config variable in the config file with the /// highest level (usually the local one). pub fn set_bool(&mut self, name: &str, value: bool) -> Result<(), Error> { - let name = try!(CString::new(name)); + let name = CString::new(name)?; unsafe { try_call!(raw::git_config_set_bool(self.raw, name, value)); } @@ -309,7 +410,7 @@ impl Config { /// Set the value of an integer config variable in the config file with the /// highest level (usually the local one). pub fn set_i32(&mut self, name: &str, value: i32) -> Result<(), Error> { - let name = try!(CString::new(name)); + let name = CString::new(name)?; unsafe { try_call!(raw::git_config_set_int32(self.raw, name, value)); } @@ -319,18 +420,32 @@ impl Config { /// Set the value of an integer config variable in the config file with the /// highest level (usually the local one). pub fn set_i64(&mut self, name: &str, value: i64) -> Result<(), Error> { - let name = try!(CString::new(name)); + let name = CString::new(name)?; unsafe { try_call!(raw::git_config_set_int64(self.raw, name, value)); } Ok(()) } + /// Set the value of an multivar config variable in the config file with the + /// highest level (usually the local one). + /// + /// The regular expression is applied case-sensitively on the value. + pub fn set_multivar(&mut self, name: &str, regexp: &str, value: &str) -> Result<(), Error> { + let name = CString::new(name)?; + let regexp = CString::new(regexp)?; + let value = CString::new(value)?; + unsafe { + try_call!(raw::git_config_set_multivar(self.raw, name, regexp, value)); + } + Ok(()) + } + /// Set the value of a string config variable in the config file with the /// highest level (usually the local one). pub fn set_str(&mut self, name: &str, value: &str) -> Result<(), Error> { - let name = try!(CString::new(name)); - let value = try!(CString::new(value)); + let name = CString::new(name)?; + let value = CString::new(value)?; unsafe { try_call!(raw::git_config_set_string(self.raw, name, value)); } @@ -343,20 +458,60 @@ impl Config { /// you to look into a consistent view of the configuration for looking up /// complex values (e.g. a remote, submodule). pub fn snapshot(&mut self) -> Result { - let mut ret = 0 as *mut raw::git_config; + let mut ret = ptr::null_mut(); unsafe { try_call!(raw::git_config_snapshot(&mut ret, self.raw)); Ok(Binding::from_raw(ret)) } } + + /// Parse a string as a bool. + /// + /// Interprets "true", "yes", "on", 1, or any non-zero number as true. + /// Interprets "false", "no", "off", 0, or an empty string as false. + pub fn parse_bool(s: S) -> Result { + let s = s.into_c_string()?; + let mut out = 0; + crate::init(); + unsafe { + try_call!(raw::git_config_parse_bool(&mut out, s)); + } + Ok(out != 0) + } + + /// Parse a string as an i32; handles suffixes like k, M, or G, and + /// multiplies by the appropriate power of 1024. + pub fn parse_i32(s: S) -> Result { + let s = s.into_c_string()?; + let mut out = 0; + crate::init(); + unsafe { + try_call!(raw::git_config_parse_int32(&mut out, s)); + } + Ok(out) + } + + /// Parse a string as an i64; handles suffixes like k, M, or G, and + /// multiplies by the appropriate power of 1024. + pub fn parse_i64(s: S) -> Result { + let s = s.into_c_string()?; + let mut out = 0; + crate::init(); + unsafe { + try_call!(raw::git_config_parse_int64(&mut out, s)); + } + Ok(out) + } } impl Binding for Config { type Raw = *mut raw::git_config; unsafe fn from_raw(raw: *mut raw::git_config) -> Config { - Config { raw: raw } + Config { raw } + } + fn raw(&self) -> *mut raw::git_config { + self.raw } - fn raw(&self) -> *mut raw::git_config { self.raw } } impl Drop for Config { @@ -369,74 +524,110 @@ impl<'cfg> ConfigEntry<'cfg> { /// Gets the name of this entry. /// /// May return `None` if the name is not valid utf-8 - pub fn name(&self) -> Option<&str> { str::from_utf8(self.name_bytes()).ok() } + pub fn name(&self) -> Option<&str> { + str::from_utf8(self.name_bytes()).ok() + } /// Gets the name of this entry as a byte slice. pub fn name_bytes(&self) -> &[u8] { - unsafe { ::opt_bytes(self, (*self.raw).name).unwrap() } + unsafe { crate::opt_bytes(self, (*self.raw).name).unwrap() } } /// Gets the value of this entry. /// /// May return `None` if the value is not valid utf-8 - pub fn value(&self) -> Option<&str> { str::from_utf8(self.value_bytes()).ok() } + /// + /// # Panics + /// + /// Panics when no value is defined. + pub fn value(&self) -> Option<&str> { + str::from_utf8(self.value_bytes()).ok() + } /// Gets the value of this entry as a byte slice. + /// + /// # Panics + /// + /// Panics when no value is defined. pub fn value_bytes(&self) -> &[u8] { - unsafe { ::opt_bytes(self, (*self.raw).value).unwrap() } + unsafe { crate::opt_bytes(self, (*self.raw).value).unwrap() } + } + + /// Returns `true` when a value is defined otherwise `false`. + /// + /// No value defined is a short-hand to represent a Boolean `true`. + pub fn has_value(&self) -> bool { + unsafe { !(*self.raw).value.is_null() } } /// Gets the configuration level of this entry. pub fn level(&self) -> ConfigLevel { unsafe { ConfigLevel::from_raw((*self.raw).level) } } + + /// Depth of includes where this variable was found + pub fn include_depth(&self) -> u32 { + unsafe { (*self.raw).include_depth as u32 } + } } impl<'cfg> Binding for ConfigEntry<'cfg> { type Raw = *mut raw::git_config_entry; - unsafe fn from_raw(raw: *mut raw::git_config_entry) - -> ConfigEntry<'cfg> { + unsafe fn from_raw(raw: *mut raw::git_config_entry) -> ConfigEntry<'cfg> { ConfigEntry { - raw: raw, + raw, _marker: marker::PhantomData, owned: true, } } - fn raw(&self) -> *mut raw::git_config_entry { self.raw } + fn raw(&self) -> *mut raw::git_config_entry { + self.raw + } } impl<'cfg> Binding for ConfigEntries<'cfg> { type Raw = *mut raw::git_config_iterator; - unsafe fn from_raw(raw: *mut raw::git_config_iterator) - -> ConfigEntries<'cfg> { + unsafe fn from_raw(raw: *mut raw::git_config_iterator) -> ConfigEntries<'cfg> { ConfigEntries { - raw: raw, + raw, + current: None, _marker: marker::PhantomData, } } - fn raw(&self) -> *mut raw::git_config_iterator { self.raw } + fn raw(&self) -> *mut raw::git_config_iterator { + self.raw + } } -// entries are only valid until the iterator is freed, so this impl is for -// `&'b T` instead of `T` to have a lifetime to tie them to. -// -// It's also not implemented for `&'b mut T` so we can have multiple entries -// (ok). -impl<'cfg, 'b> Iterator for &'b ConfigEntries<'cfg> { - type Item = Result, Error>; - fn next(&mut self) -> Option, Error>> { - let mut raw = 0 as *mut raw::git_config_entry; +impl<'cfg> ConfigEntries<'cfg> { + /// Advances the iterator and returns the next value. + /// + /// Returns `None` when iteration is finished. + pub fn next(&mut self) -> Option, Error>> { + let mut raw = ptr::null_mut(); + drop(self.current.take()); unsafe { try_call_iter!(raw::git_config_next(&mut raw, self.raw)); - Some(Ok(ConfigEntry { + let entry = ConfigEntry { owned: false, - raw: raw, + raw, _marker: marker::PhantomData, - })) + }; + self.current = Some(entry); + Some(Ok(self.current.as_ref().unwrap())) } } + + /// Calls the given closure for each remaining entry in the iterator. + pub fn for_each)>(mut self, mut f: F) -> Result<(), Error> { + while let Some(entry) = self.next() { + let entry = entry?; + f(entry); + } + Ok(()) + } } impl<'cfg> Drop for ConfigEntries<'cfg> { @@ -456,9 +647,9 @@ impl<'cfg> Drop for ConfigEntry<'cfg> { #[cfg(test)] mod tests { use std::fs::File; - use tempdir::TempDir; + use tempfile::TempDir; - use Config; + use crate::Config; #[test] fn smoke() { @@ -470,7 +661,7 @@ mod tests { #[test] fn persisted() { - let td = TempDir::new("test").unwrap(); + let td = TempDir::new().unwrap(); let path = td.path().join("foo"); File::create(&path).unwrap(); @@ -489,11 +680,103 @@ mod tests { assert_eq!(cfg.get_i64("foo.k3").unwrap(), 2); assert_eq!(cfg.get_str("foo.k4").unwrap(), "bar"); - for entry in &cfg.entries(None).unwrap() { + let mut entries = cfg.entries(None).unwrap(); + while let Some(entry) = entries.next() { let entry = entry.unwrap(); entry.name(); entry.value(); entry.level(); } } + + #[test] + fn multivar() { + let td = TempDir::new().unwrap(); + let path = td.path().join("foo"); + File::create(&path).unwrap(); + + let mut cfg = Config::open(&path).unwrap(); + cfg.set_multivar("foo.bar", "^$", "baz").unwrap(); + cfg.set_multivar("foo.bar", "^$", "qux").unwrap(); + cfg.set_multivar("foo.bar", "^$", "quux").unwrap(); + cfg.set_multivar("foo.baz", "^$", "oki").unwrap(); + + // `entries` filters by name + let mut entries: Vec = Vec::new(); + cfg.entries(Some("foo.bar")) + .unwrap() + .for_each(|entry| entries.push(entry.value().unwrap().to_string())) + .unwrap(); + entries.sort(); + assert_eq!(entries, ["baz", "quux", "qux"]); + + // which is the same as `multivar` without a regex + let mut multivals = Vec::new(); + cfg.multivar("foo.bar", None) + .unwrap() + .for_each(|entry| multivals.push(entry.value().unwrap().to_string())) + .unwrap(); + multivals.sort(); + assert_eq!(multivals, entries); + + // yet _with_ a regex, `multivar` filters by value + let mut quxish = Vec::new(); + cfg.multivar("foo.bar", Some("qu.*x")) + .unwrap() + .for_each(|entry| quxish.push(entry.value().unwrap().to_string())) + .unwrap(); + quxish.sort(); + assert_eq!(quxish, ["quux", "qux"]); + + cfg.remove_multivar("foo.bar", ".*").unwrap(); + + let count = |entries: super::ConfigEntries<'_>| -> usize { + let mut c = 0; + entries.for_each(|_| c += 1).unwrap(); + c + }; + + assert_eq!(count(cfg.entries(Some("foo.bar")).unwrap()), 0); + assert_eq!(count(cfg.multivar("foo.bar", None).unwrap()), 0); + } + + #[test] + fn parse() { + assert_eq!(Config::parse_bool("").unwrap(), false); + assert_eq!(Config::parse_bool("false").unwrap(), false); + assert_eq!(Config::parse_bool("no").unwrap(), false); + assert_eq!(Config::parse_bool("off").unwrap(), false); + assert_eq!(Config::parse_bool("0").unwrap(), false); + + assert_eq!(Config::parse_bool("true").unwrap(), true); + assert_eq!(Config::parse_bool("yes").unwrap(), true); + assert_eq!(Config::parse_bool("on").unwrap(), true); + assert_eq!(Config::parse_bool("1").unwrap(), true); + assert_eq!(Config::parse_bool("42").unwrap(), true); + + assert!(Config::parse_bool(" ").is_err()); + assert!(Config::parse_bool("some-string").is_err()); + assert!(Config::parse_bool("-").is_err()); + + assert_eq!(Config::parse_i32("0").unwrap(), 0); + assert_eq!(Config::parse_i32("1").unwrap(), 1); + assert_eq!(Config::parse_i32("100").unwrap(), 100); + assert_eq!(Config::parse_i32("-1").unwrap(), -1); + assert_eq!(Config::parse_i32("-100").unwrap(), -100); + assert_eq!(Config::parse_i32("1k").unwrap(), 1024); + assert_eq!(Config::parse_i32("4k").unwrap(), 4096); + assert_eq!(Config::parse_i32("1M").unwrap(), 1048576); + assert_eq!(Config::parse_i32("1G").unwrap(), 1024 * 1024 * 1024); + + assert_eq!(Config::parse_i64("0").unwrap(), 0); + assert_eq!(Config::parse_i64("1").unwrap(), 1); + assert_eq!(Config::parse_i64("100").unwrap(), 100); + assert_eq!(Config::parse_i64("-1").unwrap(), -1); + assert_eq!(Config::parse_i64("-100").unwrap(), -100); + assert_eq!(Config::parse_i64("1k").unwrap(), 1024); + assert_eq!(Config::parse_i64("4k").unwrap(), 4096); + assert_eq!(Config::parse_i64("1M").unwrap(), 1048576); + assert_eq!(Config::parse_i64("1G").unwrap(), 1024 * 1024 * 1024); + assert_eq!(Config::parse_i64("100G").unwrap(), 100 * 1024 * 1024 * 1024); + } } diff --git a/src/cred.rs b/src/cred.rs index 94113b739f..adf621b5d8 100644 --- a/src/cred.rs +++ b/src/cred.rs @@ -1,12 +1,14 @@ +#[cfg(feature = "cred")] +use log::{debug, trace}; use std::ffi::CString; -use std::io::Write; use std::mem; use std::path::Path; -use std::process::{Command, Stdio}; -use url; +use std::ptr; -use {raw, Error, Config, IntoCString}; -use util::Binding; +use crate::util::Binding; +#[cfg(feature = "cred")] +use crate::Config; +use crate::{raw, Error, IntoCString}; /// A structure to represent git credentials in libgit2. pub struct Cred { @@ -14,12 +16,15 @@ pub struct Cred { } /// Management of the gitcredentials(7) interface. +#[cfg(feature = "cred")] pub struct CredentialHelper { /// A public field representing the currently discovered username from /// configuration. pub username: Option, protocol: Option, host: Option, + port: Option, + path: Option, url: String, commands: Vec, } @@ -28,8 +33,8 @@ impl Cred { /// Create a "default" credential usable for Negotiate mechanisms like NTLM /// or Kerberos authentication. pub fn default() -> Result { - ::init(); - let mut out = 0 as *mut raw::git_cred; + crate::init(); + let mut out = ptr::null_mut(); unsafe { try_call!(raw::git_cred_default_new(&mut out)); Ok(Binding::from_raw(out)) @@ -40,9 +45,9 @@ impl Cred { /// /// The username specified is the username to authenticate. pub fn ssh_key_from_agent(username: &str) -> Result { - ::init(); - let mut out = 0 as *mut raw::git_cred; - let username = try!(CString::new(username)); + crate::init(); + let mut out = ptr::null_mut(); + let username = CString::new(username)?; unsafe { try_call!(raw::git_cred_ssh_key_from_agent(&mut out, username)); Ok(Binding::from_raw(out)) @@ -50,33 +55,57 @@ impl Cred { } /// Create a new passphrase-protected ssh key credential object. - pub fn ssh_key(username: &str, - publickey: Option<&Path>, - privatekey: &Path, - passphrase: Option<&str>) -> Result { - ::init(); - let username = try!(CString::new(username)); - let publickey = try!(::opt_cstr(publickey)); - let privatekey = try!(privatekey.into_c_string()); - let passphrase = try!(::opt_cstr(passphrase)); - let mut out = 0 as *mut raw::git_cred; + pub fn ssh_key( + username: &str, + publickey: Option<&Path>, + privatekey: &Path, + passphrase: Option<&str>, + ) -> Result { + crate::init(); + let username = CString::new(username)?; + let publickey = crate::opt_cstr(publickey)?; + let privatekey = privatekey.into_c_string()?; + let passphrase = crate::opt_cstr(passphrase)?; + let mut out = ptr::null_mut(); unsafe { - try_call!(raw::git_cred_ssh_key_new(&mut out, username, publickey, - privatekey, passphrase)); + try_call!(raw::git_cred_ssh_key_new( + &mut out, username, publickey, privatekey, passphrase + )); + Ok(Binding::from_raw(out)) + } + } + + /// Create a new ssh key credential object reading the keys from memory. + pub fn ssh_key_from_memory( + username: &str, + publickey: Option<&str>, + privatekey: &str, + passphrase: Option<&str>, + ) -> Result { + crate::init(); + let username = CString::new(username)?; + let publickey = crate::opt_cstr(publickey)?; + let privatekey = CString::new(privatekey)?; + let passphrase = crate::opt_cstr(passphrase)?; + let mut out = ptr::null_mut(); + unsafe { + try_call!(raw::git_cred_ssh_key_memory_new( + &mut out, username, publickey, privatekey, passphrase + )); Ok(Binding::from_raw(out)) } } /// Create a new plain-text username and password credential object. - pub fn userpass_plaintext(username: &str, - password: &str) -> Result { - ::init(); - let username = try!(CString::new(username)); - let password = try!(CString::new(password)); - let mut out = 0 as *mut raw::git_cred; + pub fn userpass_plaintext(username: &str, password: &str) -> Result { + crate::init(); + let username = CString::new(username)?; + let password = CString::new(password)?; + let mut out = ptr::null_mut(); unsafe { - try_call!(raw::git_cred_userpass_plaintext_new(&mut out, username, - password)); + try_call!(raw::git_cred_userpass_plaintext_new( + &mut out, username, password + )); Ok(Binding::from_raw(out)) } } @@ -85,34 +114,39 @@ impl Cred { /// /// This function will attempt to parse the user's `credential.helper` /// configuration, invoke the necessary processes, and read off what the - /// username/password should be for a particular url. + /// username/password should be for a particular URL. /// /// The returned credential type will be a username/password credential if /// successful. /// /// [1]: https://www.kernel.org/pub/software/scm/git/docs/gitcredentials.html - pub fn credential_helper(config: &Config, - url: &str, - username: Option<&str>) - -> Result { - match CredentialHelper::new(url).config(config).username(username) - .execute() { - Some((username, password)) => { - Cred::userpass_plaintext(&username, &password) - } - None => Err(Error::from_str("failed to acquire username/password \ - from local configuration")) + #[cfg(feature = "cred")] + pub fn credential_helper( + config: &Config, + url: &str, + username: Option<&str>, + ) -> Result { + match CredentialHelper::new(url) + .config(config) + .username(username) + .execute() + { + Some((username, password)) => Cred::userpass_plaintext(&username, &password), + None => Err(Error::from_str( + "failed to acquire username/password \ + from local configuration", + )), } } /// Create a credential to specify a username. /// - /// THis is used with ssh authentication to query for the username if non is - /// specified in the url. + /// This is used with ssh authentication to query for the username if none is + /// specified in the URL. pub fn username(username: &str) -> Result { - ::init(); - let username = try!(CString::new(username)); - let mut out = 0 as *mut raw::git_cred; + crate::init(); + let username = CString::new(username)?; + let mut out = ptr::null_mut(); unsafe { try_call!(raw::git_cred_username_new(&mut out, username)); Ok(Binding::from_raw(out)) @@ -131,7 +165,7 @@ impl Cred { /// Unwrap access to the underlying raw pointer, canceling the destructor pub unsafe fn unwrap(mut self) -> *mut raw::git_cred { - mem::replace(&mut self.raw, 0 as *mut raw::git_cred) + mem::replace(&mut self.raw, ptr::null_mut()) } } @@ -139,29 +173,38 @@ impl Binding for Cred { type Raw = *mut raw::git_cred; unsafe fn from_raw(raw: *mut raw::git_cred) -> Cred { - Cred { raw: raw } + Cred { raw } + } + fn raw(&self) -> *mut raw::git_cred { + self.raw } - fn raw(&self) -> *mut raw::git_cred { self.raw } } impl Drop for Cred { fn drop(&mut self) { if !self.raw.is_null() { - unsafe { ((*self.raw).free)(self.raw) } + unsafe { + if let Some(f) = (*self.raw).free { + f(self.raw) + } + } } } } +#[cfg(feature = "cred")] impl CredentialHelper { /// Create a new credential helper object which will be used to probe git's /// local credential configuration. /// - /// The url specified is the namespace on which this will query credentials. - /// Invalid urls are currently ignored. + /// The URL specified is the namespace on which this will query credentials. + /// Invalid URLs are currently ignored. pub fn new(url: &str) -> CredentialHelper { let mut ret = CredentialHelper { protocol: None, host: None, + port: None, + path: None, username: None, url: url.to_string(), commands: Vec::new(), @@ -172,9 +215,10 @@ impl CredentialHelper { if let Some(url::Host::Domain(s)) = url.host() { ret.host = Some(s.to_string()); } - ret.protocol = Some(url.scheme().to_string()) + ret.port = url.port(); + ret.protocol = Some(url.scheme().to_string()); } - return ret; + ret } /// Set the username that this credential helper will query with. @@ -191,53 +235,82 @@ impl CredentialHelper { // Figure out the configured username/helper program. // // see http://git-scm.com/docs/gitcredentials.html#_configuration_options - // - // TODO: implement useHttpPath if self.username.is_none() { self.config_username(config); } self.config_helper(config); + self.config_use_http_path(config); self } // Configure the queried username from `config` fn config_username(&mut self, config: &Config) { let key = self.exact_key("username"); - self.username = config.get_string(&key).ok().or_else(|| { - self.url_key("username").and_then(|s| { - config.get_string(&s).ok() + self.username = config + .get_string(&key) + .ok() + .or_else(|| { + self.url_key("username") + .and_then(|s| config.get_string(&s).ok()) }) - }).or_else(|| { - config.get_string("credential.username").ok() - }) + .or_else(|| config.get_string("credential.username").ok()) } // Discover all `helper` directives from `config` fn config_helper(&mut self, config: &Config) { let exact = config.get_string(&self.exact_key("helper")); self.add_command(exact.as_ref().ok().map(|s| &s[..])); - match self.url_key("helper") { - Some(key) => { - let url = config.get_string(&key); - self.add_command(url.as_ref().ok().map(|s| &s[..])); - } - None => {} + if let Some(key) = self.url_key("helper") { + let url = config.get_string(&key); + self.add_command(url.as_ref().ok().map(|s| &s[..])); } let global = config.get_string("credential.helper"); self.add_command(global.as_ref().ok().map(|s| &s[..])); } + // Discover `useHttpPath` from `config` + fn config_use_http_path(&mut self, config: &Config) { + let mut use_http_path = false; + if let Some(value) = config.get_bool(&self.exact_key("useHttpPath")).ok() { + use_http_path = value; + } else if let Some(value) = self + .url_key("useHttpPath") + .and_then(|key| config.get_bool(&key).ok()) + { + use_http_path = value; + } else if let Some(value) = config.get_bool("credential.useHttpPath").ok() { + use_http_path = value; + } + + if use_http_path { + if let Ok(url) = url::Url::parse(&self.url) { + let path = url.path(); + // Url::parse always includes a leading slash for rooted URLs, while git does not. + self.path = Some(path.strip_prefix('/').unwrap_or(path).to_string()); + } + } + } + // Add a `helper` configured command to the list of commands to execute. // // see https://www.kernel.org/pub/software/scm/git/docs/technical // /api-credentials.html#_credential_helpers fn add_command(&mut self, cmd: Option<&str>) { - let cmd = match cmd { Some(s) => s, None => return }; - if cmd.starts_with("!") { + fn is_absolute_path(path: &str) -> bool { + path.starts_with('/') + || path.starts_with('\\') + || cfg!(windows) && path.chars().nth(1).is_some_and(|x| x == ':') + } + + let cmd = match cmd { + Some("") | None => return, + Some(s) => s, + }; + + if cmd.starts_with('!') { self.commands.push(cmd[1..].to_string()); - } else if cmd.starts_with("/") || cmd.starts_with("\\") || - cmd[1..].starts_with(":\\") { - self.commands.push(format!("\"{}\"", cmd)); + } else if is_absolute_path(cmd) { + self.commands.push(cmd.to_string()); } else { self.commands.push(format!("git credential-{}", cmd)); } @@ -252,7 +325,7 @@ impl CredentialHelper { (&Some(ref host), &Some(ref protocol)) => { Some(format!("credential.{}://{}.{}", protocol, host, name)) } - _ => None + _ => None, } } @@ -263,15 +336,17 @@ impl CredentialHelper { pub fn execute(&self) -> Option<(String, String)> { let mut username = self.username.clone(); let mut password = None; - for cmd in self.commands.iter() { - let (u, p) = self.execute_cmd(&cmd, &username); + for cmd in &self.commands { + let (u, p) = self.execute_cmd(cmd, &username); if u.is_some() && username.is_none() { username = u; } if p.is_some() && password.is_none() { password = p; } - if username.is_some() && password.is_some() { break } + if username.is_some() && password.is_some() { + break; + } } match (username, password) { @@ -282,38 +357,112 @@ impl CredentialHelper { // Execute the given `cmd`, providing the appropriate variables on stdin and // then afterwards parsing the output into the username/password on stdout. - fn execute_cmd(&self, cmd: &str, username: &Option) - -> (Option, Option) { + fn execute_cmd( + &self, + cmd: &str, + username: &Option, + ) -> (Option, Option) { + use std::io::Write; + use std::process::{Command, Stdio}; + macro_rules! my_try( ($e:expr) => ( - match $e { Ok(e) => e, Err(..) => return (None, None) } + match $e { + Ok(e) => e, + Err(e) => { + debug!("{} failed with {}", stringify!($e), e); + return (None, None) + } + } ) ); - let mut p = my_try!(Command::new("sh").arg("-c") - .arg(&format!("{} get", cmd)) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn()); + // It looks like the `cmd` specification is typically bourne-shell-like + // syntax, so try that first. If that fails, though, we may be on a + // Windows machine for example where `sh` isn't actually available by + // default. Most credential helper configurations though are pretty + // simple (aka one or two space-separated strings) so also try to invoke + // the process directly. + // + // If that fails then it's up to the user to put `sh` in path and make + // sure it works. + let mut c = Command::new("sh"); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x08000000; + c.creation_flags(CREATE_NO_WINDOW); + } + c.arg("-c") + .arg(&format!("{} get", cmd)) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + debug!("executing credential helper {:?}", c); + let mut p = match c.spawn() { + Ok(p) => p, + Err(e) => { + debug!("`sh` failed to spawn: {}", e); + let mut parts = cmd.split_whitespace(); + let mut c = Command::new(parts.next().unwrap()); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x08000000; + c.creation_flags(CREATE_NO_WINDOW); + } + for arg in parts { + c.arg(arg); + } + c.arg("get") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + debug!("executing credential helper {:?}", c); + match c.spawn() { + Ok(p) => p, + Err(e) => { + debug!("fallback of {:?} failed with {}", cmd, e); + return (None, None); + } + } + } + }; + // Ignore write errors as the command may not actually be listening for // stdin { let stdin = p.stdin.as_mut().unwrap(); - match self.protocol { - Some(ref p) => { let _ = writeln!(stdin, "protocol={}", p); } - None => {} + if let Some(ref p) = self.protocol { + let _ = writeln!(stdin, "protocol={}", p); } - match self.host { - Some(ref p) => { let _ = writeln!(stdin, "host={}", p); } - None => {} + if let Some(ref p) = self.host { + if let Some(ref p2) = self.port { + let _ = writeln!(stdin, "host={}:{}", p, p2); + } else { + let _ = writeln!(stdin, "host={}", p); + } } - match *username { - Some(ref p) => { let _ = writeln!(stdin, "username={}", p); } - None => {} + if let Some(ref p) = self.path { + let _ = writeln!(stdin, "path={}", p); + } + if let Some(ref p) = *username { + let _ = writeln!(stdin, "username={}", p); } } let output = my_try!(p.wait_with_output()); - if !output.status.success() { return (None, None) } - return self.parse_output(output.stdout) + if !output.status.success() { + debug!( + "credential helper failed: {}\nstdout ---\n{}\nstderr ---\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + return (None, None); + } + trace!( + "credential helper stderr ---\n{}", + String::from_utf8_lossy(&output.stderr) + ); + self.parse_output(output.stdout) } // Parse the output of a command into the username/password found @@ -324,7 +473,13 @@ impl CredentialHelper { for line in output.split(|t| *t == b'\n') { let mut parts = line.splitn(2, |t| *t == b'='); let key = parts.next().unwrap(); - let value = match parts.next() { Some(s) => s, None => continue }; + let value = match parts.next() { + Some(s) => s, + None => { + trace!("ignoring output line: {}", String::from_utf8_lossy(line)); + continue; + } + }; let value = match String::from_utf8(value.to_vec()) { Ok(s) => s, Err(..) => continue, @@ -339,20 +494,21 @@ impl CredentialHelper { } } -#[cfg(all(test, feature = "unstable"))] +#[cfg(test)] +#[cfg(feature = "cred")] mod test { use std::env; use std::fs::File; use std::io::prelude::*; use std::path::Path; - use tempdir::TempDir; + use tempfile::TempDir; - use {Cred, Config, CredentialHelper, ConfigLevel}; + use crate::{Config, ConfigLevel, Cred, CredentialHelper}; - macro_rules! cfg( ($($k:expr => $v:expr),*) => ({ - let td = TempDir::new("git2-rs").unwrap(); + macro_rules! test_cfg( ($($k:expr => $v:expr),*) => ({ + let td = TempDir::new().unwrap(); let mut cfg = Config::new().unwrap(); - cfg.add_file(&td.path().join("cfg"), ConfigLevel::Highest, false).unwrap(); + cfg.add_file(&td.path().join("cfg"), ConfigLevel::App, false).unwrap(); $(cfg.set_str($k, $v).unwrap();)* cfg }) ); @@ -364,89 +520,216 @@ mod test { #[test] fn credential_helper1() { - let cfg = cfg! { + let cfg = test_cfg! { "credential.helper" => "!f() { echo username=a; echo password=b; }; f" }; let (u, p) = CredentialHelper::new("https://example.com/foo/bar") - .config(&cfg) - .execute().unwrap(); + .config(&cfg) + .execute() + .unwrap(); assert_eq!(u, "a"); assert_eq!(p, "b"); } #[test] fn credential_helper2() { - let cfg = cfg! {}; + let cfg = test_cfg! {}; assert!(CredentialHelper::new("https://example.com/foo/bar") - .config(&cfg) - .execute().is_none()); + .config(&cfg) + .execute() + .is_none()); } #[test] fn credential_helper3() { - let cfg = cfg! { + let cfg = test_cfg! { "credential.https://example.com.helper" => "!f() { echo username=c; }; f", "credential.helper" => "!f() { echo username=a; echo password=b; }; f" }; let (u, p) = CredentialHelper::new("https://example.com/foo/bar") - .config(&cfg) - .execute().unwrap(); + .config(&cfg) + .execute() + .unwrap(); assert_eq!(u, "c"); assert_eq!(p, "b"); } #[test] fn credential_helper4() { - let td = TempDir::new("git2-rs").unwrap(); + if cfg!(windows) { + return; + } // shell scripts don't work on Windows + + let td = TempDir::new().unwrap(); let path = td.path().join("script"); - File::create(&path).unwrap().write(br"\ + File::create(&path) + .unwrap() + .write( + br"\ #!/bin/sh echo username=c -").unwrap(); +", + ) + .unwrap(); chmod(&path); - let cfg = cfg! { + let cfg = test_cfg! { "credential.https://example.com.helper" => &path.display().to_string()[..], "credential.helper" => "!f() { echo username=a; echo password=b; }; f" }; let (u, p) = CredentialHelper::new("https://example.com/foo/bar") - .config(&cfg) - .execute().unwrap(); + .config(&cfg) + .execute() + .unwrap(); assert_eq!(u, "c"); assert_eq!(p, "b"); } #[test] fn credential_helper5() { - let td = TempDir::new("git2-rs").unwrap(); - let path = td.path().join("git-credential-script"); - File::create(&path).unwrap().write(br"\ + if cfg!(windows) { + return; + } // shell scripts don't work on Windows + let td = TempDir::new().unwrap(); + let path = td.path().join("git-credential-some-script"); + File::create(&path) + .unwrap() + .write( + br"\ #!/bin/sh -echo username=c -").unwrap(); +echo username=$1 +", + ) + .unwrap(); chmod(&path); let paths = env::var("PATH").unwrap(); - let paths = env::split_paths(&paths) - .chain(path.parent().map(|p| p.to_path_buf()).into_iter()); + let paths = + env::split_paths(&paths).chain(path.parent().map(|p| p.to_path_buf()).into_iter()); env::set_var("PATH", &env::join_paths(paths).unwrap()); - let cfg = cfg! { - "credential.https://example.com.helper" => "script", + let cfg = test_cfg! { + "credential.https://example.com.helper" => "some-script \"value/with\\slashes\"", "credential.helper" => "!f() { echo username=a; echo password=b; }; f" }; let (u, p) = CredentialHelper::new("https://example.com/foo/bar") - .config(&cfg) - .execute().unwrap(); - assert_eq!(u, "c"); + .config(&cfg) + .execute() + .unwrap(); + assert_eq!(u, "value/with\\slashes"); + assert_eq!(p, "b"); + } + + #[test] + fn credential_helper6() { + let cfg = test_cfg! { + "credential.helper" => "" + }; + assert!(CredentialHelper::new("https://example.com/foo/bar") + .config(&cfg) + .execute() + .is_none()); + } + + #[test] + fn credential_helper7() { + if cfg!(windows) { + return; + } // shell scripts don't work on Windows + let td = TempDir::new().unwrap(); + let path = td.path().join("script"); + File::create(&path) + .unwrap() + .write( + br"\ +#!/bin/sh +echo username=$1 +echo password=$2 +", + ) + .unwrap(); + chmod(&path); + let cfg = test_cfg! { + "credential.helper" => &format!("{} a b", path.display()) + }; + let (u, p) = CredentialHelper::new("https://example.com/foo/bar") + .config(&cfg) + .execute() + .unwrap(); + assert_eq!(u, "a"); assert_eq!(p, "b"); } + #[test] + fn credential_helper8() { + let cfg = test_cfg! { + "credential.useHttpPath" => "true" + }; + let mut helper = CredentialHelper::new("https://example.com/foo/bar"); + helper.config(&cfg); + assert_eq!(helper.path.as_deref(), Some("foo/bar")); + } + + #[test] + fn credential_helper9() { + let cfg = test_cfg! { + "credential.helper" => "!f() { while read line; do eval $line; done; if [ \"$host\" = example.com:3000 ]; then echo username=a; echo password=b; fi; }; f" + }; + let (u, p) = CredentialHelper::new("https://example.com:3000/foo/bar") + .config(&cfg) + .execute() + .unwrap(); + assert_eq!(u, "a"); + assert_eq!(p, "b"); + } + + #[test] + #[cfg(feature = "ssh")] + fn ssh_key_from_memory() { + let cred = Cred::ssh_key_from_memory( + "test", + Some("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDByAO8uj+kXicj6C2ODMspgmUoVyl5eaw8vR6a1yEnFuJFzevabNlN6Ut+CPT3TRnYk5BW73pyXBtnSL2X95BOnbjMDXc4YIkgs3YYHWnxbqsD4Pj/RoGqhf+gwhOBtL0poh8tT8WqXZYxdJQKLQC7oBqf3ykCEYulE4oeRUmNh4IzEE+skD/zDkaJ+S1HRD8D8YCiTO01qQnSmoDFdmIZTi8MS8Cw+O/Qhym1271ThMlhD6PubSYJXfE6rVbE7A9RzH73A6MmKBlzK8VTb4SlNSrr/DOk+L0uq+wPkv+pm+D9WtxoqQ9yl6FaK1cPawa3+7yRNle3m+72KCtyMkQv"), + r#" + -----BEGIN RSA PRIVATE KEY----- + Proc-Type: 4,ENCRYPTED + DEK-Info: AES-128-CBC,818C7722D3B01F2161C2ACF6A5BBAAE8 + + 3Cht4QB3PcoQ0I55j1B3m2ZzIC/mrh+K5nQeA1Vy2GBTMyM7yqGHqTOv7qLhJscd + H+cB0Pm6yCr3lYuNrcKWOCUto+91P7ikyARruHVwyIxKdNx15uNulOzQJHQWNbA4 + RQHlhjON4atVo2FyJ6n+ujK6QiBg2PR5Vbbw/AtV6zBCFW3PhzDn+qqmHjpBFqj2 + vZUUe+MkDQcaF5J45XMHahhSdo/uKCDhfbylExp/+ACWkvxdPpsvcARM6X434ucD + aPY+4i0/JyLkdbm0GFN9/q3i53qf4kCBhojFl4AYJdGI0AzAgbdTXZ7EJHbAGZHS + os5K0oTwDVXMI0sSE2I/qHxaZZsDP1dOKq6di6SFPUp8liYimm7rNintRX88Gl2L + g1ko9abp/NlgD0YY/3mad+NNAISDL/YfXq2fklH3En3/7ZrOVZFKfZXwQwas5g+p + VQPKi3+ae74iOjLyuPDSc1ePmhUNYeP+9rLSc0wiaiHqls+2blPPDxAGMEo63kbz + YPVjdmuVX4VWnyEsfTxxJdFDYGSNh6rlrrO1RFrex7kJvpg5gTX4M/FT8TfCd7Hn + M6adXsLMqwu5tz8FuDmAtVdq8zdSrgZeAbpJ9D3EDOmZ70xz4XBL19ImxDp+Qqs2 + kQX7kobRzeeP2URfRoGr7XZikQWyQ2UASfPcQULY8R58QoZWWsQ4w51GZHg7TDnw + 1DRo/0OgkK7Gqf215nFmMpB4uyi58cq3WFwWQa1IqslkObpVgBQZcNZb/hKUYPGk + g4zehfIgAfCdnQHwZvQ6Fdzhcs3SZeO+zVyuiZN3Gsi9HU0/1vpAKiuuOzcG02vF + b6Y6hwsAA9yphF3atI+ARD4ZwXdDfzuGb3yJglMT3Fr/xuLwAvdchRo1spANKA0E + tT5okLrK0H4wnHvf2SniVVWRhmJis0lQo9LjGGwRIdsPpVnJSDvaISIVF+fHT90r + HvxN8zXI93x9jcPtwp7puQ1C7ehKJK10sZ71OLIZeuUgwt+5DRunqg6evPco9Go7 + UOGwcVhLY200KT+1k7zWzCS0yVQp2HRm6cxsZXAp4ClBSwIx15eIoLIrjZdJRjCq + COp6pZx1fnvJ9ERIvl5hon+Ty+renMcFKz2HmchC7egpcqIxW9Dsv6zjhHle6pxb + 37GaEKHF2KA3RN+dSV/K8n+C9Yent5tx5Y9a/pMcgRGtgu+G+nyFmkPKn5Zt39yX + qDpyM0LtbRVZPs+MgiqoGIwYc/ujoCq7GL38gezsBQoHaTt79yYBqCp6UR0LMuZ5 + f/7CtWqffgySfJ/0wjGidDAumDv8CK45AURpL/Z+tbFG3M9ar/LZz/Y6EyBcLtGY + Wwb4zs8zXIA0qHrjNTnPqHDvezziArYfgPjxCIHMZzms9Yn8+N02p39uIytqg434 + BAlCqZ7GYdDFfTpWIwX+segTK9ux0KdBqcQv+9Fwwjkq9KySnRKqNl7ZJcefFZJq + c6PA1iinZWBjuaO1HKx3PFulrl0bcpR9Kud1ZIyfnh5rwYN8UQkkcR/wZPla04TY + 8l5dq/LI/3G5sZXwUHKOcuQWTj7Saq7Q6gkKoMfqt0wC5bpZ1m17GHPoMz6GtX9O + -----END RSA PRIVATE KEY----- + "#, + Some("test123")); + assert!(cred.is_ok()); + } + #[cfg(unix)] fn chmod(path: &Path) { - use std::os::unix::prelude::*; use std::fs; + use std::os::unix::prelude::*; let mut perms = fs::metadata(path).unwrap().permissions(); perms.set_mode(0o755); fs::set_permissions(path, perms).unwrap(); diff --git a/src/describe.rs b/src/describe.rs index 55aaca6cb0..cbaa1893b6 100644 --- a/src/describe.rs +++ b/src/describe.rs @@ -1,11 +1,12 @@ +use std::ffi::CString; use std::marker; use std::mem; -use std::ffi::CString; +use std::ptr; -use libc::{c_uint, c_int}; +use libc::{c_int, c_uint}; -use {raw, Repository, Error, Buf}; -use util::Binding; +use crate::util::Binding; +use crate::{raw, Buf, Error, Repository}; /// The result of a `describe` operation on either an `Describe` or a /// `Repository`. @@ -28,10 +29,9 @@ pub struct DescribeFormatOptions { impl<'repo> Describe<'repo> { /// Prints this describe result, returning the result as a string. - pub fn format(&self, opts: Option<&DescribeFormatOptions>) - -> Result { + pub fn format(&self, opts: Option<&DescribeFormatOptions>) -> Result { let buf = Buf::new(); - let opts = opts.map(|o| &o.raw as *const _).unwrap_or(0 as *const _); + let opts = opts.map(|o| &o.raw as *const _).unwrap_or(ptr::null()); unsafe { try_call!(raw::git_describe_format(buf.raw(), self.raw, opts)); } @@ -43,9 +43,14 @@ impl<'repo> Binding for Describe<'repo> { type Raw = *mut raw::git_describe_result; unsafe fn from_raw(raw: *mut raw::git_describe_result) -> Describe<'repo> { - Describe { raw: raw, _marker: marker::PhantomData, } + Describe { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_describe_result { + self.raw } - fn raw(&self) -> *mut raw::git_describe_result { self.raw } } impl<'repo> Drop for Describe<'repo> { @@ -54,6 +59,12 @@ impl<'repo> Drop for Describe<'repo> { } } +impl Default for DescribeFormatOptions { + fn default() -> Self { + Self::new() + } +} + impl DescribeFormatOptions { /// Creates a new blank set of formatting options for a description. pub fn new() -> DescribeFormatOptions { @@ -63,7 +74,7 @@ impl DescribeFormatOptions { }; opts.raw.version = 1; opts.raw.abbreviated_size = 7; - return opts + opts } /// Sets the size of the abbreviated commit id to use. @@ -91,6 +102,12 @@ impl DescribeFormatOptions { } } +impl Default for DescribeOptions { + fn default() -> Self { + Self::new() + } +} + impl DescribeOptions { /// Creates a new blank set of formatting options for a description. pub fn new() -> DescribeOptions { @@ -100,7 +117,7 @@ impl DescribeOptions { }; opts.raw.version = 1; opts.raw.max_candidates_tags = 10; - return opts + opts } #[allow(missing_docs)] @@ -111,7 +128,7 @@ impl DescribeOptions { /// Sets the reference lookup strategy /// - /// This behaves like the `--tags` option to git-decribe. + /// This behaves like the `--tags` option to git-describe. pub fn describe_tags(&mut self) -> &mut Self { self.raw.describe_strategy = raw::GIT_DESCRIBE_TAGS as c_uint; self @@ -119,7 +136,7 @@ impl DescribeOptions { /// Sets the reference lookup strategy /// - /// This behaves like the `--all` option to git-decribe. + /// This behaves like the `--all` option to git-describe. pub fn describe_all(&mut self) -> &mut Self { self.raw.describe_strategy = raw::GIT_DESCRIBE_ALL as c_uint; self @@ -151,8 +168,7 @@ impl DescribeOptions { impl Binding for DescribeOptions { type Raw = *mut raw::git_describe_options; - unsafe fn from_raw(_raw: *mut raw::git_describe_options) - -> DescribeOptions { + unsafe fn from_raw(_raw: *mut raw::git_describe_options) -> DescribeOptions { panic!("unimplemened") } fn raw(&self) -> *mut raw::git_describe_options { @@ -162,15 +178,14 @@ impl Binding for DescribeOptions { #[cfg(test)] mod tests { - use DescribeOptions; + use crate::DescribeOptions; #[test] fn smoke() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let head = t!(repo.head()).target().unwrap(); - let d = t!(repo.describe(DescribeOptions::new() - .show_commit_oid_as_fallback(true))); + let d = t!(repo.describe(DescribeOptions::new().show_commit_oid_as_fallback(true))); let id = head.to_string(); assert_eq!(t!(d.format(None)), &id[..7]); diff --git a/src/diff.rs b/src/diff.rs index 602ce2fe02..3070550390 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -1,14 +1,16 @@ +use libc::{c_char, c_int, c_void, size_t}; use std::ffi::CString; +use std::iter::FusedIterator; use std::marker; use std::mem; use std::ops::Range; use std::path::Path; +use std::ptr; use std::slice; -use libc::{c_char, size_t, c_void, c_int}; -use {raw, panic, Buf, Delta, Oid, Repository, Error, DiffFormat}; -use {DiffStatsFormat, IntoCString}; -use util::{self, Binding}; +use crate::util::{self, Binding}; +use crate::{panic, raw, Buf, Delta, DiffFormat, Error, FileMode, Oid, Repository}; +use crate::{DiffFlags, DiffStatsFormat, IntoCString}; /// The diff object that contains all individual file deltas. /// @@ -52,6 +54,16 @@ pub struct DiffFindOptions { raw: raw::git_diff_find_options, } +/// Control behavior of formatting emails +pub struct DiffFormatEmailOptions { + raw: raw::git_diff_format_email_options, +} + +/// Control behavior of formatting emails +pub struct DiffPatchidOptions { + raw: raw::git_diff_patchid_options, +} + /// An iterator over the diffs in a delta pub struct Deltas<'diff> { range: Range, @@ -101,18 +113,18 @@ pub enum DiffBinaryKind { Delta, } -type PrintCb<'a> = FnMut(DiffDelta, Option, DiffLine) -> bool + 'a; +type PrintCb<'a> = dyn FnMut(DiffDelta<'_>, Option>, DiffLine<'_>) -> bool + 'a; -pub type FileCb<'a> = FnMut(DiffDelta, f32) -> bool + 'a; -pub type BinaryCb<'a> = FnMut(DiffDelta, DiffBinary) -> bool + 'a; -pub type HunkCb<'a> = FnMut(DiffDelta, DiffHunk) -> bool + 'a; -pub type LineCb<'a> = FnMut(DiffDelta, Option, DiffLine) -> bool + 'a; +pub type FileCb<'a> = dyn FnMut(DiffDelta<'_>, f32) -> bool + 'a; +pub type BinaryCb<'a> = dyn FnMut(DiffDelta<'_>, DiffBinary<'_>) -> bool + 'a; +pub type HunkCb<'a> = dyn FnMut(DiffDelta<'_>, DiffHunk<'_>) -> bool + 'a; +pub type LineCb<'a> = dyn FnMut(DiffDelta<'_>, Option>, DiffLine<'_>) -> bool + 'a; -struct ForeachCallbacks<'a, 'b: 'a, 'c, 'd: 'c, 'e, 'f: 'e, 'g, 'h: 'g> { - file: &'a mut FileCb<'b>, - binary: Option<&'c mut BinaryCb<'d>>, - hunk: Option<&'e mut HunkCb<'f>>, - line: Option<&'g mut LineCb<'h>>, +pub struct DiffCallbacks<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> { + pub file: Option<&'a mut FileCb<'b>>, + pub binary: Option<&'c mut BinaryCb<'d>>, + pub hunk: Option<&'e mut HunkCb<'f>>, + pub line: Option<&'g mut LineCb<'h>>, } impl<'repo> Diff<'repo> { @@ -125,18 +137,23 @@ impl<'repo> Diff<'repo> { /// is from the "from" list (with the exception that if the item has a /// pending DELETE in the middle, then it will show as deleted). pub fn merge(&mut self, from: &Diff<'repo>) -> Result<(), Error> { - unsafe { try_call!(raw::git_diff_merge(self.raw, &*from.raw)); } + unsafe { + try_call!(raw::git_diff_merge(self.raw, &*from.raw)); + } Ok(()) } /// Returns an iterator over the deltas in this diff. - pub fn deltas(&self) -> Deltas { + pub fn deltas(&self) -> Deltas<'_> { let num_deltas = unsafe { raw::git_diff_num_deltas(&*self.raw) }; - Deltas { range: 0..(num_deltas as usize), diff: self } + Deltas { + range: 0..(num_deltas as usize), + diff: self, + } } /// Return the diff delta for an entry in the diff list. - pub fn get_delta(&self, i: usize) -> Option { + pub fn get_delta(&self, i: usize) -> Option> { unsafe { let ptr = raw::git_diff_get_delta(&*self.raw, i as size_t); Binding::from_raw_opt(ptr as *mut _) @@ -153,15 +170,15 @@ impl<'repo> Diff<'repo> { /// Returning `false` from the callback will terminate the iteration and /// return an error from this function. pub fn print(&self, format: DiffFormat, mut cb: F) -> Result<(), Error> - where F: FnMut(DiffDelta, - Option, - DiffLine) -> bool { - let mut cb: &mut PrintCb = &mut cb; + where + F: FnMut(DiffDelta<'_>, Option>, DiffLine<'_>) -> bool, + { + let mut cb: &mut PrintCb<'_> = &mut cb; let ptr = &mut cb as *mut _; + let print: raw::git_diff_line_cb = Some(print_cb); unsafe { - try_call!(raw::git_diff_print(self.raw, format, print_cb, - ptr as *mut _)); - return Ok(()) + try_call!(raw::git_diff_print(self.raw, format, print, ptr as *mut _)); + Ok(()) } } @@ -169,44 +186,52 @@ impl<'repo> Diff<'repo> { /// /// Returning `false` from any callback will terminate the iteration and /// return an error from this function. - pub fn foreach(&self, - file_cb: &mut FileCb, - binary_cb: Option<&mut BinaryCb>, - hunk_cb: Option<&mut HunkCb>, - line_cb: Option<&mut LineCb>) -> Result<(), Error> { - let mut cbs = ForeachCallbacks { - file: file_cb, + pub fn foreach( + &self, + file_cb: &mut FileCb<'_>, + binary_cb: Option<&mut BinaryCb<'_>>, + hunk_cb: Option<&mut HunkCb<'_>>, + line_cb: Option<&mut LineCb<'_>>, + ) -> Result<(), Error> { + let mut cbs = DiffCallbacks { + file: Some(file_cb), binary: binary_cb, hunk: hunk_cb, line: line_cb, }; let ptr = &mut cbs as *mut _; unsafe { - let binary_cb_c = if cbs.binary.is_some() { - Some(binary_cb_c as raw::git_diff_binary_cb) + let binary_cb_c: raw::git_diff_binary_cb = if cbs.binary.is_some() { + Some(binary_cb_c) } else { None }; - let hunk_cb_c = if cbs.hunk.is_some() { - Some(hunk_cb_c as raw::git_diff_hunk_cb) + let hunk_cb_c: raw::git_diff_hunk_cb = if cbs.hunk.is_some() { + Some(hunk_cb_c) } else { None }; - let line_cb_c = if cbs.line.is_some() { - Some(line_cb_c as raw::git_diff_line_cb) + let line_cb_c: raw::git_diff_line_cb = if cbs.line.is_some() { + Some(line_cb_c) } else { None }; - try_call!(raw::git_diff_foreach(self.raw, file_cb_c, binary_cb_c, - hunk_cb_c, line_cb_c, - ptr as *mut _)); - return Ok(()) + let file_cb: raw::git_diff_file_cb = Some(file_cb_c); + try_call!(raw::git_diff_foreach( + self.raw, + file_cb, + binary_cb_c, + hunk_cb_c, + line_cb_c, + ptr as *mut _ + )); + Ok(()) } } /// Accumulate diff statistics for all patches. pub fn stats(&self) -> Result { - let mut ret = 0 as *mut raw::git_diff_stats; + let mut ret = ptr::null_mut(); unsafe { try_call!(raw::git_diff_get_stats(&mut ret, self.raw)); Ok(Binding::from_raw(ret)) @@ -219,113 +244,220 @@ impl<'repo> Diff<'repo> { /// renames or copies with new entries reflecting those changes. This also /// will, if requested, break modified files into add/remove pairs if the /// amount of change is above a threshold. - pub fn find_similar(&mut self, opts: Option<&mut DiffFindOptions>) - -> Result<(), Error> { + pub fn find_similar(&mut self, opts: Option<&mut DiffFindOptions>) -> Result<(), Error> { let opts = opts.map(|opts| &opts.raw); - unsafe { try_call!(raw::git_diff_find_similar(self.raw, opts)); } + unsafe { + try_call!(raw::git_diff_find_similar(self.raw, opts)); + } Ok(()) } - // TODO: num_deltas_of_type, format_email, find_similar + /// Create an e-mail ready patch from a diff. + /// + /// Matches the format created by `git format-patch` + #[doc(hidden)] + #[deprecated(note = "refactored to `Email::from_diff` to match upstream")] + pub fn format_email( + &mut self, + patch_no: usize, + total_patches: usize, + commit: &crate::Commit<'repo>, + opts: Option<&mut DiffFormatEmailOptions>, + ) -> Result { + assert!(patch_no > 0); + assert!(patch_no <= total_patches); + let mut default = DiffFormatEmailOptions::default(); + let raw_opts = opts.map_or(&mut default.raw, |opts| &mut opts.raw); + let summary = commit.summary_bytes().unwrap(); + let mut message = commit.message_bytes(); + assert!(message.starts_with(summary)); + message = &message[summary.len()..]; + raw_opts.patch_no = patch_no; + raw_opts.total_patches = total_patches; + let id = commit.id(); + raw_opts.id = id.raw(); + raw_opts.summary = summary.as_ptr() as *const _; + raw_opts.body = message.as_ptr() as *const _; + raw_opts.author = commit.author().raw(); + let buf = Buf::new(); + #[allow(deprecated)] + unsafe { + try_call!(raw::git_diff_format_email(buf.raw(), self.raw, &*raw_opts)); + } + Ok(buf) + } + + /// Create a patch ID from a diff. + pub fn patchid(&self, opts: Option<&mut DiffPatchidOptions>) -> Result { + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_diff_patchid( + &mut raw, + self.raw, + opts.map(|o| &mut o.raw) + )); + Ok(Binding::from_raw(&raw as *const _)) + } + } + + // TODO: num_deltas_of_type, find_similar +} +impl Diff<'static> { + /// Read the contents of a git patch file into a `git_diff` object. + /// + /// The diff object produced is similar to the one that would be + /// produced if you actually produced it computationally by comparing + /// two trees, however there may be subtle differences. For example, + /// a patch file likely contains abbreviated object IDs, so the + /// object IDs parsed by this function will also be abbreviated. + pub fn from_buffer(buffer: &[u8]) -> Result, Error> { + crate::init(); + let mut diff: *mut raw::git_diff = std::ptr::null_mut(); + unsafe { + // NOTE: Doesn't depend on repo, so lifetime can be 'static + try_call!(raw::git_diff_from_buffer( + &mut diff, + buffer.as_ptr() as *const c_char, + buffer.len() + )); + Ok(Diff::from_raw(diff)) + } + } } -extern fn print_cb(delta: *const raw::git_diff_delta, - hunk: *const raw::git_diff_hunk, - line: *const raw::git_diff_line, - data: *mut c_void) -> c_int { +pub extern "C" fn print_cb( + delta: *const raw::git_diff_delta, + hunk: *const raw::git_diff_hunk, + line: *const raw::git_diff_line, + data: *mut c_void, +) -> c_int { unsafe { let delta = Binding::from_raw(delta as *mut _); let hunk = Binding::from_raw_opt(hunk); let line = Binding::from_raw(line); let r = panic::wrap(|| { - let data = data as *mut &mut PrintCb; + let data = data as *mut &mut PrintCb<'_>; (*data)(delta, hunk, line) }); - if r == Some(true) {0} else {-1} + if r == Some(true) { + raw::GIT_OK + } else { + raw::GIT_EUSER + } } } -extern fn file_cb_c(delta: *const raw::git_diff_delta, - progress: f32, - data: *mut c_void) -> c_int { +pub extern "C" fn file_cb_c( + delta: *const raw::git_diff_delta, + progress: f32, + data: *mut c_void, +) -> c_int { unsafe { let delta = Binding::from_raw(delta as *mut _); let r = panic::wrap(|| { - let cbs = data as *mut ForeachCallbacks; - ((*cbs).file)(delta, progress) + let cbs = data as *mut DiffCallbacks<'_, '_, '_, '_, '_, '_, '_, '_>; + match (*cbs).file { + Some(ref mut cb) => cb(delta, progress), + None => false, + } }); - if r == Some(true) {0} else {-1} + if r == Some(true) { + raw::GIT_OK + } else { + raw::GIT_EUSER + } } } -extern fn binary_cb_c(delta: *const raw::git_diff_delta, - binary: *const raw::git_diff_binary, - data: *mut c_void) -> c_int { +pub extern "C" fn binary_cb_c( + delta: *const raw::git_diff_delta, + binary: *const raw::git_diff_binary, + data: *mut c_void, +) -> c_int { unsafe { let delta = Binding::from_raw(delta as *mut _); let binary = Binding::from_raw(binary); let r = panic::wrap(|| { - let cbs = data as *mut ForeachCallbacks; + let cbs = data as *mut DiffCallbacks<'_, '_, '_, '_, '_, '_, '_, '_>; match (*cbs).binary { Some(ref mut cb) => cb(delta, binary), None => false, } }); - if r == Some(true) {0} else {-1} + if r == Some(true) { + raw::GIT_OK + } else { + raw::GIT_EUSER + } } } -extern fn hunk_cb_c(delta: *const raw::git_diff_delta, - hunk: *const raw::git_diff_hunk, - data: *mut c_void) -> c_int { +pub extern "C" fn hunk_cb_c( + delta: *const raw::git_diff_delta, + hunk: *const raw::git_diff_hunk, + data: *mut c_void, +) -> c_int { unsafe { let delta = Binding::from_raw(delta as *mut _); let hunk = Binding::from_raw(hunk); let r = panic::wrap(|| { - let cbs = data as *mut ForeachCallbacks; + let cbs = data as *mut DiffCallbacks<'_, '_, '_, '_, '_, '_, '_, '_>; match (*cbs).hunk { Some(ref mut cb) => cb(delta, hunk), None => false, } }); - if r == Some(true) {0} else {-1} + if r == Some(true) { + raw::GIT_OK + } else { + raw::GIT_EUSER + } } } -extern fn line_cb_c(delta: *const raw::git_diff_delta, - hunk: *const raw::git_diff_hunk, - line: *const raw::git_diff_line, - data: *mut c_void) -> c_int { +pub extern "C" fn line_cb_c( + delta: *const raw::git_diff_delta, + hunk: *const raw::git_diff_hunk, + line: *const raw::git_diff_line, + data: *mut c_void, +) -> c_int { unsafe { let delta = Binding::from_raw(delta as *mut _); let hunk = Binding::from_raw_opt(hunk); let line = Binding::from_raw(line); let r = panic::wrap(|| { - let cbs = data as *mut ForeachCallbacks; + let cbs = data as *mut DiffCallbacks<'_, '_, '_, '_, '_, '_, '_, '_>; match (*cbs).line { Some(ref mut cb) => cb(delta, hunk, line), None => false, } }); - if r == Some(true) {0} else {-1} + if r == Some(true) { + raw::GIT_OK + } else { + raw::GIT_EUSER + } } } - impl<'repo> Binding for Diff<'repo> { type Raw = *mut raw::git_diff; unsafe fn from_raw(raw: *mut raw::git_diff) -> Diff<'repo> { Diff { - raw: raw, - _marker: marker::PhantomData, + raw, + _marker: marker::PhantomData, } } - fn raw(&self) -> *mut raw::git_diff { self.raw } + fn raw(&self) -> *mut raw::git_diff { + self.raw + } } impl<'repo> Drop for Diff<'repo> { @@ -335,6 +467,37 @@ impl<'repo> Drop for Diff<'repo> { } impl<'a> DiffDelta<'a> { + /// Returns the flags on the delta. + /// + /// For more information, see `DiffFlags`'s documentation. + pub fn flags(&self) -> DiffFlags { + let flags = unsafe { (*self.raw).flags }; + let mut result = DiffFlags::empty(); + + #[cfg(target_env = "msvc")] + fn as_u32(flag: i32) -> u32 { + flag as u32 + } + #[cfg(not(target_env = "msvc"))] + fn as_u32(flag: u32) -> u32 { + flag + } + + if (flags & as_u32(raw::GIT_DIFF_FLAG_BINARY)) != 0 { + result |= DiffFlags::BINARY; + } + if (flags & as_u32(raw::GIT_DIFF_FLAG_NOT_BINARY)) != 0 { + result |= DiffFlags::NOT_BINARY; + } + if (flags & as_u32(raw::GIT_DIFF_FLAG_VALID_ID)) != 0 { + result |= DiffFlags::VALID_ID; + } + if (flags & as_u32(raw::GIT_DIFF_FLAG_EXISTS)) != 0 { + result |= DiffFlags::EXISTS; + } + result + } + // TODO: expose when diffs are more exposed // pub fn similarity(&self) -> u16 { // unsafe { (*self.raw).similarity } @@ -386,11 +549,24 @@ impl<'a> Binding for DiffDelta<'a> { type Raw = *mut raw::git_diff_delta; unsafe fn from_raw(raw: *mut raw::git_diff_delta) -> DiffDelta<'a> { DiffDelta { - raw: raw, + raw, _marker: marker::PhantomData, } } - fn raw(&self) -> *mut raw::git_diff_delta { self.raw } + fn raw(&self) -> *mut raw::git_diff_delta { + self.raw + } +} + +impl<'a> std::fmt::Debug for DiffDelta<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_struct("DiffDelta") + .field("nfiles", &self.nfiles()) + .field("status", &self.status()) + .field("old_file", &self.old_file()) + .field("new_file", &self.new_file()) + .finish() + } } impl<'a> DiffFile<'a> { @@ -406,7 +582,7 @@ impl<'a> DiffFile<'a> { /// directory of the repository. pub fn path_bytes(&self) -> Option<&'a [u8]> { static FOO: () = (); - unsafe { ::opt_bytes(&FOO, (*self.raw).path) } + unsafe { crate::opt_bytes(&FOO, (*self.raw).path) } } /// Returns the path of the entry relative to the working directory of the @@ -416,20 +592,76 @@ impl<'a> DiffFile<'a> { } /// Returns the size of this entry, in bytes - pub fn size(&self) -> u64 { unsafe { (*self.raw).size as u64 } } + pub fn size(&self) -> u64 { + unsafe { (*self.raw).size as u64 } + } + + /// Returns `true` if file(s) are treated as binary data. + pub fn is_binary(&self) -> bool { + unsafe { (*self.raw).flags & raw::GIT_DIFF_FLAG_BINARY as u32 != 0 } + } + + /// Returns `true` if file(s) are treated as text data. + pub fn is_not_binary(&self) -> bool { + unsafe { (*self.raw).flags & raw::GIT_DIFF_FLAG_NOT_BINARY as u32 != 0 } + } + + /// Returns `true` if `id` value is known correct. + pub fn is_valid_id(&self) -> bool { + unsafe { (*self.raw).flags & raw::GIT_DIFF_FLAG_VALID_ID as u32 != 0 } + } + + /// Returns `true` if file exists at this side of the delta. + pub fn exists(&self) -> bool { + unsafe { (*self.raw).flags & raw::GIT_DIFF_FLAG_EXISTS as u32 != 0 } + } - // TODO: expose flags/mode + /// Returns file mode. + pub fn mode(&self) -> FileMode { + match unsafe { (*self.raw).mode.into() } { + raw::GIT_FILEMODE_UNREADABLE => FileMode::Unreadable, + raw::GIT_FILEMODE_TREE => FileMode::Tree, + raw::GIT_FILEMODE_BLOB => FileMode::Blob, + raw::GIT_FILEMODE_BLOB_GROUP_WRITABLE => FileMode::BlobGroupWritable, + raw::GIT_FILEMODE_BLOB_EXECUTABLE => FileMode::BlobExecutable, + raw::GIT_FILEMODE_LINK => FileMode::Link, + raw::GIT_FILEMODE_COMMIT => FileMode::Commit, + mode => panic!("unknown mode: {}", mode), + } + } } impl<'a> Binding for DiffFile<'a> { type Raw = *const raw::git_diff_file; unsafe fn from_raw(raw: *const raw::git_diff_file) -> DiffFile<'a> { DiffFile { - raw: raw, + raw, _marker: marker::PhantomData, } } - fn raw(&self) -> *const raw::git_diff_file { self.raw } + fn raw(&self) -> *const raw::git_diff_file { + self.raw + } +} + +impl<'a> std::fmt::Debug for DiffFile<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut ds = f.debug_struct("DiffFile"); + ds.field("id", &self.id()); + if let Some(path_bytes) = &self.path_bytes() { + ds.field("path_bytes", path_bytes); + } + if let Some(path) = &self.path() { + ds.field("path", path); + } + ds.field("size", &self.size()).finish() + } +} + +impl Default for DiffOptions { + fn default() -> Self { + Self::new() + } } impl DiffOptions { @@ -445,13 +677,12 @@ impl DiffOptions { old_prefix: None, new_prefix: None, }; - assert_eq!(unsafe { - raw::git_diff_init_options(&mut opts.raw, 1) - }, 0); + assert_eq!(unsafe { raw::git_diff_init_options(&mut opts.raw, 1) }, 0); opts } - fn flag(&mut self, opt: u32, val: bool) -> &mut DiffOptions { + fn flag(&mut self, opt: raw::git_diff_option_t, val: bool) -> &mut DiffOptions { + let opt = opt as u32; if val { self.raw.flags |= opt; } else { @@ -480,7 +711,7 @@ impl DiffOptions { self.flag(raw::GIT_DIFF_INCLUDE_UNTRACKED, include) } - /// Flag indicating whether untracked directories are deeply traversed or + /// Flag indicating whether untracked directories are traversed deeply or /// not. pub fn recurse_untracked_dirs(&mut self, recurse: bool) -> &mut DiffOptions { self.flag(raw::GIT_DIFF_RECURSE_UNTRACKED_DIRS, recurse) @@ -491,13 +722,13 @@ impl DiffOptions { self.flag(raw::GIT_DIFF_INCLUDE_UNMODIFIED, include) } - /// If entrabled, then Typechange delta records are generated. + /// If enabled, then Typechange delta records are generated. pub fn include_typechange(&mut self, include: bool) -> &mut DiffOptions { self.flag(raw::GIT_DIFF_INCLUDE_TYPECHANGE, include) } - /// Event with `include_typechange`, the tree treturned generally shows a - /// deleted blow. This flag correctly labels the tree transitions as a + /// Event with `include_typechange`, the tree returned generally shows a + /// deleted blob. This flag correctly labels the tree transitions as a /// typechange record with the `new_file`'s mode set to tree. /// /// Note that the tree SHA will not be available. @@ -541,8 +772,7 @@ impl DiffOptions { /// /// This flag turns off that scan and immediately labels an untracked /// directory as untracked (changing the behavior to not match core git). - pub fn enable_fast_untracked_dirs(&mut self, enable: bool) - -> &mut DiffOptions { + pub fn enable_fast_untracked_dirs(&mut self, enable: bool) -> &mut DiffOptions { self.flag(raw::GIT_DIFF_ENABLE_FAST_UNTRACKED_DIRS, enable) } @@ -559,9 +789,8 @@ impl DiffOptions { self.flag(raw::GIT_DIFF_INCLUDE_UNREADABLE, include) } - /// Include unreadable files in the diff - pub fn include_unreadable_as_untracked(&mut self, include: bool) - -> &mut DiffOptions { + /// Include unreadable files in the diff as untracked files + pub fn include_unreadable_as_untracked(&mut self, include: bool) -> &mut DiffOptions { self.flag(raw::GIT_DIFF_INCLUDE_UNREADABLE_AS_UNTRACKED, include) } @@ -572,7 +801,7 @@ impl DiffOptions { /// Treat all files as binary, disabling text diffs pub fn force_binary(&mut self, force: bool) -> &mut DiffOptions { - self.flag(raw::GIT_DIFF_FORCE_TEXT, force) + self.flag(raw::GIT_DIFF_FORCE_BINARY, force) } /// Ignore all whitespace @@ -585,11 +814,16 @@ impl DiffOptions { self.flag(raw::GIT_DIFF_IGNORE_WHITESPACE_CHANGE, ignore) } - /// Ignore whitespace at tend of line + /// Ignore whitespace at the end of line pub fn ignore_whitespace_eol(&mut self, ignore: bool) -> &mut DiffOptions { self.flag(raw::GIT_DIFF_IGNORE_WHITESPACE_EOL, ignore) } + /// Ignore blank lines + pub fn ignore_blank_lines(&mut self, ignore: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_IGNORE_BLANK_LINES, ignore) + } + /// When generating patch text, include the content of untracked files. /// /// This automatically turns on `include_untracked` but it does not turn on @@ -623,6 +857,13 @@ impl DiffOptions { self.flag(raw::GIT_DIFF_SHOW_BINARY, show) } + /// Use a heuristic that takes indentation and whitespace into account + /// which generally can produce better diffs when dealing with ambiguous + /// diff hunks. + pub fn indent_heuristic(&mut self, heuristic: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_INDENT_HEURISTIC, heuristic) + } + /// Set the number of unchanged lines that define the boundary of a hunk /// (and to display before and after). /// @@ -675,9 +916,8 @@ impl DiffOptions { } /// Add to the array of paths/fnmatch patterns to constrain the diff. - pub fn pathspec(&mut self, pathspec: T) - -> &mut DiffOptions { - let s = pathspec.into_c_string().unwrap(); + pub fn pathspec(&mut self, pathspec: T) -> &mut DiffOptions { + let s = util::cstring_to_repo_path(pathspec).unwrap(); self.pathspec_ptrs.push(s.as_ptr()); self.pathspec.push(s); self @@ -688,10 +928,16 @@ impl DiffOptions { /// This function is unsafe as the pointer is only valid so long as this /// structure is not moved, modified, or used elsewhere. pub unsafe fn raw(&mut self) -> *const raw::git_diff_options { - self.raw.old_prefix = self.old_prefix.as_ref().map(|s| s.as_ptr()) - .unwrap_or(0 as *const _); - self.raw.new_prefix = self.new_prefix.as_ref().map(|s| s.as_ptr()) - .unwrap_or(0 as *const _); + self.raw.old_prefix = self + .old_prefix + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(ptr::null()); + self.raw.new_prefix = self + .new_prefix + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(ptr::null()); self.raw.pathspec.count = self.pathspec_ptrs.len() as size_t; self.raw.pathspec.strings = self.pathspec_ptrs.as_ptr() as *mut _; &self.raw as *const _ @@ -705,15 +951,74 @@ impl<'diff> Iterator for Deltas<'diff> { fn next(&mut self) -> Option> { self.range.next().and_then(|i| self.diff.get_delta(i)) } - fn size_hint(&self) -> (usize, Option) { self.range.size_hint() } + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() + } } impl<'diff> DoubleEndedIterator for Deltas<'diff> { fn next_back(&mut self) -> Option> { self.range.next_back().and_then(|i| self.diff.get_delta(i)) } } +impl<'diff> FusedIterator for Deltas<'diff> {} + impl<'diff> ExactSizeIterator for Deltas<'diff> {} +/// Line origin constants. +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum DiffLineType { + /// These values will be sent to `git_diff_line_cb` along with the line + Context, + /// + Addition, + /// + Deletion, + /// Both files have no LF at end + ContextEOFNL, + /// Old has no LF at end, new does + AddEOFNL, + /// Old has LF at end, new does not + DeleteEOFNL, + /// The following values will only be sent to a `git_diff_line_cb` when + /// the content of a diff is being formatted through `git_diff_print`. + FileHeader, + /// + HunkHeader, + /// For "Binary files x and y differ" + Binary, +} + +impl Binding for DiffLineType { + type Raw = raw::git_diff_line_t; + unsafe fn from_raw(raw: raw::git_diff_line_t) -> Self { + match raw { + raw::GIT_DIFF_LINE_CONTEXT => DiffLineType::Context, + raw::GIT_DIFF_LINE_ADDITION => DiffLineType::Addition, + raw::GIT_DIFF_LINE_DELETION => DiffLineType::Deletion, + raw::GIT_DIFF_LINE_CONTEXT_EOFNL => DiffLineType::ContextEOFNL, + raw::GIT_DIFF_LINE_ADD_EOFNL => DiffLineType::AddEOFNL, + raw::GIT_DIFF_LINE_DEL_EOFNL => DiffLineType::DeleteEOFNL, + raw::GIT_DIFF_LINE_FILE_HDR => DiffLineType::FileHeader, + raw::GIT_DIFF_LINE_HUNK_HDR => DiffLineType::HunkHeader, + raw::GIT_DIFF_LINE_BINARY => DiffLineType::Binary, + _ => panic!("Unknown git diff line type"), + } + } + fn raw(&self) -> raw::git_diff_line_t { + match *self { + DiffLineType::Context => raw::GIT_DIFF_LINE_CONTEXT, + DiffLineType::Addition => raw::GIT_DIFF_LINE_ADDITION, + DiffLineType::Deletion => raw::GIT_DIFF_LINE_DELETION, + DiffLineType::ContextEOFNL => raw::GIT_DIFF_LINE_CONTEXT_EOFNL, + DiffLineType::AddEOFNL => raw::GIT_DIFF_LINE_ADD_EOFNL, + DiffLineType::DeleteEOFNL => raw::GIT_DIFF_LINE_DEL_EOFNL, + DiffLineType::FileHeader => raw::GIT_DIFF_LINE_FILE_HDR, + DiffLineType::HunkHeader => raw::GIT_DIFF_LINE_HUNK_HDR, + DiffLineType::Binary => raw::GIT_DIFF_LINE_BINARY, + } + } +} + impl<'a> DiffLine<'a> { /// Line number in old file or `None` for added line pub fn old_lineno(&self) -> Option { @@ -742,13 +1047,21 @@ impl<'a> DiffLine<'a> { } /// Content of this line as bytes. - pub fn content(&self) -> &[u8] { + pub fn content(&self) -> &'a [u8] { unsafe { - slice::from_raw_parts((*self.raw).content as *const u8, - (*self.raw).content_len as usize) + slice::from_raw_parts( + (*self.raw).content as *const u8, + (*self.raw).content_len as usize, + ) } } + /// origin of this `DiffLine`. + /// + pub fn origin_value(&self) -> DiffLineType { + unsafe { Binding::from_raw((*self.raw).origin as raw::git_diff_line_t) } + } + /// Sigil showing the origin of this `DiffLine`. /// /// * ` ` - Line context @@ -780,11 +1093,30 @@ impl<'a> Binding for DiffLine<'a> { type Raw = *const raw::git_diff_line; unsafe fn from_raw(raw: *const raw::git_diff_line) -> DiffLine<'a> { DiffLine { - raw: raw, + raw, _marker: marker::PhantomData, } } - fn raw(&self) -> *const raw::git_diff_line { self.raw } + fn raw(&self) -> *const raw::git_diff_line { + self.raw + } +} + +impl<'a> std::fmt::Debug for DiffLine<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut ds = f.debug_struct("DiffLine"); + if let Some(old_lineno) = &self.old_lineno() { + ds.field("old_lineno", old_lineno); + } + if let Some(new_lineno) = &self.new_lineno() { + ds.field("new_lineno", new_lineno); + } + ds.field("num_lines", &self.num_lines()) + .field("content_offset", &self.content_offset()) + .field("content", &self.content()) + .field("origin", &self.origin()) + .finish() + } } impl<'a> DiffHunk<'a> { @@ -809,10 +1141,12 @@ impl<'a> DiffHunk<'a> { } /// Header text - pub fn header(&self) -> &[u8] { + pub fn header(&self) -> &'a [u8] { unsafe { - slice::from_raw_parts((*self.raw).header.as_ptr() as *const u8, - (*self.raw).header_len as usize) + slice::from_raw_parts( + (*self.raw).header.as_ptr() as *const u8, + (*self.raw).header_len as usize, + ) } } } @@ -821,15 +1155,29 @@ impl<'a> Binding for DiffHunk<'a> { type Raw = *const raw::git_diff_hunk; unsafe fn from_raw(raw: *const raw::git_diff_hunk) -> DiffHunk<'a> { DiffHunk { - raw: raw, + raw, _marker: marker::PhantomData, } } - fn raw(&self) -> *const raw::git_diff_hunk { self.raw } + fn raw(&self) -> *const raw::git_diff_hunk { + self.raw + } +} + +impl<'a> std::fmt::Debug for DiffHunk<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_struct("DiffHunk") + .field("old_start", &self.old_start()) + .field("old_lines", &self.old_lines()) + .field("new_start", &self.new_start()) + .field("new_lines", &self.new_lines()) + .field("header", &self.header()) + .finish() + } } impl DiffStats { - /// Get the total number of files chaned in a diff. + /// Get the total number of files changed in a diff. pub fn files_changed(&self) -> usize { unsafe { raw::git_diff_stats_files_changed(&*self.raw) as usize } } @@ -845,13 +1193,15 @@ impl DiffStats { } /// Print diff statistics to a Buf - pub fn to_buf(&self, format: DiffStatsFormat, width: usize) - -> Result { + pub fn to_buf(&self, format: DiffStatsFormat, width: usize) -> Result { let buf = Buf::new(); unsafe { - try_call!(raw::git_diff_stats_to_buf(buf.raw(), self.raw, - format.bits(), - width as size_t)); + try_call!(raw::git_diff_stats_to_buf( + buf.raw(), + self.raw, + format.bits(), + width as size_t + )); } Ok(buf) } @@ -861,9 +1211,11 @@ impl Binding for DiffStats { type Raw = *mut raw::git_diff_stats; unsafe fn from_raw(raw: *mut raw::git_diff_stats) -> DiffStats { - DiffStats { raw: raw } + DiffStats { raw } + } + fn raw(&self) -> *mut raw::git_diff_stats { + self.raw } - fn raw(&self) -> *mut raw::git_diff_stats { self.raw } } impl Drop for DiffStats { @@ -872,7 +1224,27 @@ impl Drop for DiffStats { } } +impl std::fmt::Debug for DiffStats { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_struct("DiffStats") + .field("files_changed", &self.files_changed()) + .field("insertions", &self.insertions()) + .field("deletions", &self.deletions()) + .finish() + } +} + impl<'a> DiffBinary<'a> { + /// Returns whether there is data in this binary structure or not. + /// + /// If this is `true`, then this was produced and included binary content. + /// If this is `false` then this was generated knowing only that a binary + /// file changed but without providing the data, probably from a patch that + /// said `Binary files a/file.txt and b/file.txt differ`. + pub fn contains_data(&self) -> bool { + unsafe { (*self.raw).contains_data == 1 } + } + /// The contents of the old file. pub fn old_file(&self) -> DiffBinaryFile<'a> { unsafe { Binding::from_raw(&(*self.raw).old_file as *const _) } @@ -888,11 +1260,13 @@ impl<'a> Binding for DiffBinary<'a> { type Raw = *const raw::git_diff_binary; unsafe fn from_raw(raw: *const raw::git_diff_binary) -> DiffBinary<'a> { DiffBinary { - raw: raw, + raw, _marker: marker::PhantomData, } } - fn raw(&self) -> *const raw::git_diff_binary { self.raw } + fn raw(&self) -> *const raw::git_diff_binary { + self.raw + } } impl<'a> DiffBinaryFile<'a> { @@ -904,8 +1278,7 @@ impl<'a> DiffBinaryFile<'a> { /// The binary data, deflated pub fn data(&self) -> &[u8] { unsafe { - slice::from_raw_parts((*self.raw).data as *const u8, - (*self.raw).datalen as usize) + slice::from_raw_parts((*self.raw).data as *const u8, (*self.raw).datalen as usize) } } @@ -913,18 +1286,19 @@ impl<'a> DiffBinaryFile<'a> { pub fn inflated_len(&self) -> usize { unsafe { (*self.raw).inflatedlen as usize } } - } impl<'a> Binding for DiffBinaryFile<'a> { type Raw = *const raw::git_diff_binary_file; unsafe fn from_raw(raw: *const raw::git_diff_binary_file) -> DiffBinaryFile<'a> { DiffBinaryFile { - raw: raw, + raw, _marker: marker::PhantomData, } } - fn raw(&self) -> *const raw::git_diff_binary_file { self.raw } + fn raw(&self) -> *const raw::git_diff_binary_file { + self.raw + } } impl Binding for DiffBinaryKind { @@ -946,6 +1320,12 @@ impl Binding for DiffBinaryKind { } } +impl Default for DiffFindOptions { + fn default() -> Self { + Self::new() + } +} + impl DiffFindOptions { /// Creates a new set of empty diff find options. /// @@ -955,9 +1335,10 @@ impl DiffFindOptions { let mut opts = DiffFindOptions { raw: unsafe { mem::zeroed() }, }; - assert_eq!(unsafe { - raw::git_diff_find_init_options(&mut opts.raw, 1) - }, 0); + assert_eq!( + unsafe { raw::git_diff_find_init_options(&mut opts.raw, 1) }, + 0 + ); opts } @@ -996,8 +1377,7 @@ impl DiffFindOptions { /// /// For this to work correctly, use `include_unmodified` when the initial /// diff is being generated. - pub fn copies_from_unmodified(&mut self, find: bool) - -> &mut DiffFindOptions { + pub fn copies_from_unmodified(&mut self, find: bool) -> &mut DiffFindOptions { self.flag(raw::GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED, find) } @@ -1007,10 +1387,15 @@ impl DiffFindOptions { } /// Actually split large rewrites into delete/add pairs - pub fn break_rewries(&mut self, find: bool) -> &mut DiffFindOptions { + pub fn break_rewrites(&mut self, find: bool) -> &mut DiffFindOptions { self.flag(raw::GIT_DIFF_BREAK_REWRITES, find) } + #[doc(hidden)] + pub fn break_rewries(&mut self, find: bool) -> &mut DiffFindOptions { + self.break_rewrites(find) + } + /// Find renames/copies for untracked items in working directory. /// /// For this to work correctly use the `include_untracked` option when the @@ -1025,8 +1410,7 @@ impl DiffFindOptions { } /// Measure similarity ignoring leading whitespace (default) - pub fn ignore_leading_whitespace(&mut self, ignore: bool) - -> &mut DiffFindOptions { + pub fn ignore_leading_whitespace(&mut self, ignore: bool) -> &mut DiffFindOptions { self.flag(raw::GIT_DIFF_FIND_IGNORE_LEADING_WHITESPACE, ignore) } @@ -1055,8 +1439,7 @@ impl DiffFindOptions { /// If you add this flag in and the split pair is not used for an actual /// rename or copy, then the modified record will be restored to a regular /// modified record instead of being split. - pub fn break_rewrites_for_renames_only(&mut self, b: bool) - -> &mut DiffFindOptions { + pub fn break_rewrites_for_renames_only(&mut self, b: bool) -> &mut DiffFindOptions { self.flag(raw::GIT_DIFF_BREAK_REWRITES_FOR_RENAMES_ONLY, b) } @@ -1076,9 +1459,8 @@ impl DiffFindOptions { self } - /// Similarity of modified to be glegible rename source (default 50) - pub fn rename_from_rewrite_threshold(&mut self, thresh: u16) - -> &mut DiffFindOptions { + /// Similarity of modified to be eligible rename source (default 50) + pub fn rename_from_rewrite_threshold(&mut self, thresh: u16) -> &mut DiffFindOptions { self.raw.rename_from_rewrite_threshold = thresh; self } @@ -1090,8 +1472,7 @@ impl DiffFindOptions { } /// Similarity to split modify into delete/add pair (default 60) - pub fn break_rewrite_threshold(&mut self, thresh: u16) - -> &mut DiffFindOptions { + pub fn break_rewrite_threshold(&mut self, thresh: u16) -> &mut DiffFindOptions { self.raw.break_rewrite_threshold = thresh; self } @@ -1106,52 +1487,129 @@ impl DiffFindOptions { } // TODO: expose git_diff_similarity_metric + + /// Acquire a pointer to the underlying raw options. + pub unsafe fn raw(&mut self) -> *const raw::git_diff_find_options { + &self.raw + } +} + +impl Default for DiffFormatEmailOptions { + fn default() -> Self { + Self::new() + } +} + +impl DiffFormatEmailOptions { + /// Creates a new set of email options, + /// initialized to the default values + pub fn new() -> Self { + let mut opts = DiffFormatEmailOptions { + raw: unsafe { mem::zeroed() }, + }; + assert_eq!( + unsafe { raw::git_diff_format_email_options_init(&mut opts.raw, 1) }, + 0 + ); + opts + } + + fn flag(&mut self, opt: u32, val: bool) -> &mut Self { + if val { + self.raw.flags |= opt; + } else { + self.raw.flags &= !opt; + } + self + } + + /// Exclude `[PATCH]` from the subject header + pub fn exclude_subject_patch_header(&mut self, should_exclude: bool) -> &mut Self { + self.flag( + raw::GIT_DIFF_FORMAT_EMAIL_EXCLUDE_SUBJECT_PATCH_MARKER, + should_exclude, + ) + } +} + +impl DiffPatchidOptions { + /// Creates a new set of patchid options, + /// initialized to the default values + pub fn new() -> Self { + let mut opts = DiffPatchidOptions { + raw: unsafe { mem::zeroed() }, + }; + assert_eq!( + unsafe { + raw::git_diff_patchid_options_init( + &mut opts.raw, + raw::GIT_DIFF_PATCHID_OPTIONS_VERSION, + ) + }, + 0 + ); + opts + } } #[cfg(test)] mod tests { - use DiffOptions; - use std::fs::File; - use std::path::Path; + use crate::{DiffLineType, DiffOptions, Oid, Signature, Time}; use std::borrow::Borrow; + use std::fs::File; use std::io::Write; + use std::path::Path; #[test] fn smoke() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let diff = repo.diff_tree_to_workdir(None, None).unwrap(); assert_eq!(diff.deltas().len(), 0); let stats = diff.stats().unwrap(); assert_eq!(stats.insertions(), 0); assert_eq!(stats.deletions(), 0); assert_eq!(stats.files_changed(), 0); + let patchid = diff.patchid(None).unwrap(); + assert_ne!(patchid, Oid::zero()); } #[test] fn foreach_smoke() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let diff = t!(repo.diff_tree_to_workdir(None, None)); let mut count = 0; - t!(diff.foreach(&mut |_file, _progress| { count = count + 1; true }, - None, None, None)); + t!(diff.foreach( + &mut |_file, _progress| { + count = count + 1; + true + }, + None, + None, + None + )); assert_eq!(count, 0); } #[test] fn foreach_file_only() { let path = Path::new("foo"); - let (td, repo) = ::test::repo_init(); + let (td, repo) = crate::test::repo_init(); t!(t!(File::create(&td.path().join(path))).write_all(b"bar")); let mut opts = DiffOptions::new(); opts.include_untracked(true); let diff = t!(repo.diff_tree_to_workdir(None, Some(&mut opts))); let mut count = 0; let mut result = None; - t!(diff.foreach(&mut |file, _progress| { - count = count + 1; - result = file.new_file().path().map(ToOwned::to_owned); - true - }, None, None, None)); + t!(diff.foreach( + &mut |file, _progress| { + count = count + 1; + result = file.new_file().path().map(ToOwned::to_owned); + true + }, + None, + None, + None + )); assert_eq!(result.as_ref().map(Borrow::borrow), Some(path)); assert_eq!(count, 1); } @@ -1159,14 +1617,13 @@ mod tests { #[test] fn foreach_file_and_hunk() { let path = Path::new("foo"); - let (td, repo) = ::test::repo_init(); + let (td, repo) = crate::test::repo_init(); t!(t!(File::create(&td.path().join(path))).write_all(b"bar")); let mut index = t!(repo.index()); t!(index.add_path(path)); let mut opts = DiffOptions::new(); opts.include_untracked(true); - let diff = t!(repo.diff_tree_to_index(None, Some(&index), - Some(&mut opts))); + let diff = t!(repo.diff_tree_to_index(None, Some(&index), Some(&mut opts))); let mut new_lines = 0; t!(diff.foreach( &mut |_file, _progress| { true }, @@ -1175,7 +1632,8 @@ mod tests { new_lines = hunk.new_lines(); true }), - None)); + None + )); assert_eq!(new_lines, 1); } @@ -1184,20 +1642,18 @@ mod tests { let fib = vec![0, 1, 1, 2, 3, 5, 8]; // Verified with a node implementation of deflate, might be worth // adding a deflate lib to do this inline here. - let deflated_fib = vec![120, 156, 99, 96, 100, 100, 98, 102, 229, 0, 0, - 0, 53, 0, 21]; + let deflated_fib = vec![120, 156, 99, 96, 100, 100, 98, 102, 229, 0, 0, 0, 53, 0, 21]; let foo_path = Path::new("foo"); let bin_path = Path::new("bin"); - let (td, repo) = ::test::repo_init(); + let (td, repo) = crate::test::repo_init(); t!(t!(File::create(&td.path().join(foo_path))).write_all(b"bar\n")); t!(t!(File::create(&td.path().join(bin_path))).write_all(&fib)); let mut index = t!(repo.index()); t!(index.add_path(foo_path)); t!(index.add_path(bin_path)); let mut opts = DiffOptions::new(); - opts.include_untracked(true); - let diff = t!(repo.diff_tree_to_index(None, Some(&index), - Some(&mut opts))); + opts.include_untracked(true).show_binary(true); + let diff = t!(repo.diff_tree_to_index(None, Some(&index), Some(&mut opts))); let mut bin_content = None; let mut new_lines = 0; let mut line_content = None; @@ -1214,9 +1670,194 @@ mod tests { Some(&mut |_file, _hunk, line| { line_content = String::from_utf8(line.content().into()).ok(); true - }))); + }) + )); assert_eq!(bin_content, Some(deflated_fib)); assert_eq!(new_lines, 1); assert_eq!(line_content, Some("bar\n".to_string())); } + + #[test] + fn format_email_simple() { + let (_td, repo) = crate::test::repo_init(); + const COMMIT_MESSAGE: &str = "Modify some content"; + const EXPECTED_EMAIL_START: &str = concat!( + "From f1234fb0588b6ed670779a34ba5c51ef962f285f Mon Sep 17 00:00:00 2001\n", + "From: Techcable \n", + "Date: Tue, 11 Jan 1972 17:46:40 +0000\n", + "Subject: [PATCH] Modify some content\n", + "\n", + "---\n", + " file1.txt | 8 +++++---\n", + " 1 file changed, 5 insertions(+), 3 deletions(-)\n", + "\n", + "diff --git a/file1.txt b/file1.txt\n", + "index 94aaae8..af8f41d 100644\n", + "--- a/file1.txt\n", + "+++ b/file1.txt\n", + "@@ -1,15 +1,17 @@\n", + " file1.txt\n", + " file1.txt\n", + "+_file1.txt_\n", + " file1.txt\n", + " file1.txt\n", + " file1.txt\n", + " file1.txt\n", + "+\n", + "+\n", + " file1.txt\n", + " file1.txt\n", + " file1.txt\n", + " file1.txt\n", + " file1.txt\n", + "-file1.txt\n", + "-file1.txt\n", + "-file1.txt\n", + "+_file1.txt_\n", + "+_file1.txt_\n", + " file1.txt\n", + "--\n" + ); + const ORIGINAL_FILE: &str = concat!( + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n" + ); + const UPDATED_FILE: &str = concat!( + "file1.txt\n", + "file1.txt\n", + "_file1.txt_\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "\n", + "\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "_file1.txt_\n", + "_file1.txt_\n", + "file1.txt\n" + ); + const FILE_MODE: i32 = 0o100644; + let original_file = repo.blob(ORIGINAL_FILE.as_bytes()).unwrap(); + let updated_file = repo.blob(UPDATED_FILE.as_bytes()).unwrap(); + let mut original_tree = repo.treebuilder(None).unwrap(); + original_tree + .insert("file1.txt", original_file, FILE_MODE) + .unwrap(); + let original_tree = original_tree.write().unwrap(); + let mut updated_tree = repo.treebuilder(None).unwrap(); + updated_tree + .insert("file1.txt", updated_file, FILE_MODE) + .unwrap(); + let updated_tree = updated_tree.write().unwrap(); + let time = Time::new(64_000_000, 0); + let author = Signature::new("Techcable", "dummy@dummy.org", &time).unwrap(); + let updated_commit = repo + .commit( + None, + &author, + &author, + COMMIT_MESSAGE, + &repo.find_tree(updated_tree).unwrap(), + &[], // NOTE: Have no parents to ensure stable hash + ) + .unwrap(); + let updated_commit = repo.find_commit(updated_commit).unwrap(); + let mut diff = repo + .diff_tree_to_tree( + Some(&repo.find_tree(original_tree).unwrap()), + Some(&repo.find_tree(updated_tree).unwrap()), + None, + ) + .unwrap(); + #[allow(deprecated)] + let actual_email = diff.format_email(1, 1, &updated_commit, None).unwrap(); + let actual_email = actual_email.as_str().unwrap(); + assert!( + actual_email.starts_with(EXPECTED_EMAIL_START), + "Unexpected email:\n{}", + actual_email + ); + let mut remaining_lines = actual_email[EXPECTED_EMAIL_START.len()..].lines(); + let version_line = remaining_lines.next(); + assert!( + version_line.unwrap().starts_with("libgit2"), + "Invalid version line: {:?}", + version_line + ); + while let Some(line) = remaining_lines.next() { + assert_eq!(line.trim(), "") + } + } + + #[test] + fn foreach_diff_line_origin_value() { + let foo_path = Path::new("foo"); + let (td, repo) = crate::test::repo_init(); + t!(t!(File::create(&td.path().join(foo_path))).write_all(b"bar\n")); + let mut index = t!(repo.index()); + t!(index.add_path(foo_path)); + let mut opts = DiffOptions::new(); + opts.include_untracked(true); + let diff = t!(repo.diff_tree_to_index(None, Some(&index), Some(&mut opts))); + let mut origin_values: Vec = Vec::new(); + t!(diff.foreach( + &mut |_file, _progress| { true }, + None, + None, + Some(&mut |_file, _hunk, line| { + origin_values.push(line.origin_value()); + true + }) + )); + assert_eq!(origin_values.len(), 1); + assert_eq!(origin_values[0], DiffLineType::Addition); + } + + #[test] + fn foreach_exits_with_euser() { + let foo_path = Path::new("foo"); + let bar_path = Path::new("foo"); + + let (td, repo) = crate::test::repo_init(); + t!(t!(File::create(&td.path().join(foo_path))).write_all(b"bar\n")); + + let mut index = t!(repo.index()); + t!(index.add_path(foo_path)); + t!(index.add_path(bar_path)); + + let mut opts = DiffOptions::new(); + opts.include_untracked(true); + let diff = t!(repo.diff_tree_to_index(None, Some(&index), Some(&mut opts))); + + let mut calls = 0; + let result = diff.foreach( + &mut |_file, _progress| { + calls += 1; + false + }, + None, + None, + None, + ); + + assert_eq!(result.unwrap_err().code(), crate::ErrorCode::User); + } } diff --git a/src/email.rs b/src/email.rs new file mode 100644 index 0000000000..d3ebc03842 --- /dev/null +++ b/src/email.rs @@ -0,0 +1,183 @@ +use std::ffi::CString; +use std::{mem, ptr}; + +use crate::util::Binding; +use crate::{raw, Buf, Commit, DiffFindOptions, DiffOptions, Error, IntoCString}; +use crate::{Diff, Oid, Signature}; + +/// A structure to represent patch in mbox format for sending via email +pub struct Email { + buf: Buf, +} + +/// Options for controlling the formatting of the generated e-mail. +pub struct EmailCreateOptions { + diff_options: DiffOptions, + diff_find_options: DiffFindOptions, + subject_prefix: Option, + raw: raw::git_email_create_options, +} + +impl Default for EmailCreateOptions { + fn default() -> Self { + // Defaults options created in corresponding to `GIT_EMAIL_CREATE_OPTIONS_INIT` + let default_options = raw::git_email_create_options { + version: raw::GIT_EMAIL_CREATE_OPTIONS_VERSION, + flags: raw::GIT_EMAIL_CREATE_DEFAULT as u32, + diff_opts: unsafe { mem::zeroed() }, + diff_find_opts: unsafe { mem::zeroed() }, + subject_prefix: ptr::null(), + start_number: 1, + reroll_number: 0, + }; + let mut diff_options = DiffOptions::new(); + diff_options.show_binary(true).context_lines(3); + Self { + diff_options, + diff_find_options: DiffFindOptions::new(), + subject_prefix: None, + raw: default_options, + } + } +} + +impl EmailCreateOptions { + /// Creates a new set of email create options + /// + /// By default, options include rename detection and binary + /// diffs to match `git format-patch`. + pub fn new() -> Self { + Self::default() + } + + fn flag(&mut self, opt: raw::git_email_create_flags_t, val: bool) -> &mut Self { + let opt = opt as u32; + if val { + self.raw.flags |= opt; + } else { + self.raw.flags &= !opt; + } + self + } + + /// Flag indicating whether patch numbers are included in the subject prefix. + pub fn omit_numbers(&mut self, omit: bool) -> &mut Self { + self.flag(raw::GIT_EMAIL_CREATE_OMIT_NUMBERS, omit) + } + + /// Flag indicating whether numbers included in the subject prefix even when + /// the patch is for a single commit (1/1). + pub fn always_number(&mut self, always: bool) -> &mut Self { + self.flag(raw::GIT_EMAIL_CREATE_ALWAYS_NUMBER, always) + } + + /// Flag indicating whether rename or similarity detection are ignored. + pub fn ignore_renames(&mut self, ignore: bool) -> &mut Self { + self.flag(raw::GIT_EMAIL_CREATE_NO_RENAMES, ignore) + } + + /// Get mutable access to `DiffOptions` that are used for creating diffs. + pub fn diff_options(&mut self) -> &mut DiffOptions { + &mut self.diff_options + } + + /// Get mutable access to `DiffFindOptions` that are used for finding + /// similarities within diffs. + pub fn diff_find_options(&mut self) -> &mut DiffFindOptions { + &mut self.diff_find_options + } + + /// Set the subject prefix + /// + /// The default value for this is "PATCH". If set to an empty string ("") + /// then only the patch numbers will be shown in the prefix. + /// If the subject_prefix is empty and patch numbers are not being shown, + /// the prefix will be omitted entirely. + pub fn subject_prefix(&mut self, t: T) -> &mut Self { + self.subject_prefix = Some(t.into_c_string().unwrap()); + self + } + + /// Set the starting patch number; this cannot be 0. + /// + /// The default value for this is 1. + pub fn start_number(&mut self, number: usize) -> &mut Self { + self.raw.start_number = number; + self + } + + /// Set the "re-roll" number. + /// + /// The default value for this is 0 (no re-roll). + pub fn reroll_number(&mut self, number: usize) -> &mut Self { + self.raw.reroll_number = number; + self + } + + /// Acquire a pointer to the underlying raw options. + /// + /// This function is unsafe as the pointer is only valid so long as this + /// structure is not moved, modified, or used elsewhere. + unsafe fn raw(&mut self) -> *const raw::git_email_create_options { + self.raw.subject_prefix = self + .subject_prefix + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(ptr::null()); + self.raw.diff_opts = ptr::read(self.diff_options.raw()); + self.raw.diff_find_opts = ptr::read(self.diff_find_options.raw()); + &self.raw as *const _ + } +} + +impl Email { + /// Returns a byte slice with stored e-mail patch in. `Email` could be + /// created by one of the `from_*` functions. + pub fn as_slice(&self) -> &[u8] { + &self.buf + } + + /// Create a diff for a commit in mbox format for sending via email. + pub fn from_diff( + diff: &Diff<'_>, + patch_idx: usize, + patch_count: usize, + commit_id: &Oid, + summary: T, + body: T, + author: &Signature<'_>, + opts: &mut EmailCreateOptions, + ) -> Result { + let buf = Buf::new(); + let summary = summary.into_c_string()?; + let body = body.into_c_string()?; + unsafe { + try_call!(raw::git_email_create_from_diff( + buf.raw(), + Binding::raw(diff), + patch_idx, + patch_count, + Binding::raw(commit_id), + summary.as_ptr(), + body.as_ptr(), + Binding::raw(author), + opts.raw() + )); + Ok(Self { buf }) + } + } + + /// Create a diff for a commit in mbox format for sending via email. + /// The commit must not be a merge commit. + pub fn from_commit(commit: &Commit<'_>, opts: &mut EmailCreateOptions) -> Result { + let buf = Buf::new(); + unsafe { + try_call!(raw::git_email_create_from_commit( + buf.raw(), + commit.raw(), + opts.raw() + )); + Ok(Self { buf }) + } + } +} diff --git a/src/error.rs b/src/error.rs index ab4173a075..ecc7f4f776 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,50 +1,96 @@ +use libc::c_int; use std::env::JoinPathsError; -use std::ffi::{CStr, NulError}; use std::error; +use std::ffi::{CStr, CString, NulError}; use std::fmt; use std::str; -use libc::c_int; -use {raw, ErrorClass, ErrorCode}; +use crate::{raw, ErrorClass, ErrorCode}; /// A structure to represent errors coming out of libgit2. -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct Error { code: c_int, klass: c_int, - message: String, + message: Box, } impl Error { - /// Returns the last error, or `None` if one is not available. - pub fn last_error(code: c_int) -> Option { - ::init(); + /// Creates a new error. + /// + /// This is mainly intended for implementers of custom transports or + /// database backends, where it is desirable to propagate an [`Error`] + /// through `libgit2`. + pub fn new>(code: ErrorCode, class: ErrorClass, message: S) -> Self { + let mut err = Error::from_str(message.as_ref()); + err.set_code(code); + err.set_class(class); + err + } + + /// Returns the last error that happened with the code specified by `code`. + /// + /// The `code` argument typically comes from the return value of a function + /// call. This code will later be returned from the `code` function. + pub fn last_error(code: c_int) -> Error { + crate::init(); unsafe { - let ptr = raw::giterr_last(); - if ptr.is_null() { - None + // Note that whenever libgit2 returns an error any negative value + // indicates that an error happened. Auxiliary information is + // *usually* in `git_error_last` but unfortunately that's not always + // the case. Sometimes a negative error code is returned from + // libgit2 *without* calling `git_error_set` internally to configure + // the error. + // + // To handle this case and hopefully provide better error messages + // on our end we unconditionally call `git_error_clear` when we're done + // with an error. This is an attempt to clear it as aggressively as + // possible when we can to ensure that error information from one + // api invocation doesn't leak over to the next api invocation. + // + // Additionally if `git_error_last` returns null then we returned a + // canned error out. + let ptr = raw::git_error_last(); + let err = if ptr.is_null() { + let mut error = Error::from_str("an unknown git error occurred"); + error.code = code; + error } else { - Some(Error::from_raw(code, ptr)) - } + Error::from_raw(code, ptr) + }; + raw::git_error_clear(); + err } } unsafe fn from_raw(code: c_int, ptr: *const raw::git_error) -> Error { - let msg = CStr::from_ptr((*ptr).message as *const _).to_bytes(); - let msg = str::from_utf8(msg).unwrap(); - Error { code: code, klass: (*ptr).klass, message: msg.to_string() } + let message = CStr::from_ptr((*ptr).message as *const _).to_bytes(); + let message = String::from_utf8_lossy(message).into_owned().into(); + Error { + code, + klass: (*ptr).klass, + message, + } } /// Creates a new error from the given string as the error. + /// + /// The error returned will have the code `GIT_ERROR` and the class + /// `GIT_ERROR_NONE`. pub fn from_str(s: &str) -> Error { Error { code: raw::GIT_ERROR as c_int, - klass: raw::GITERR_NONE as c_int, - message: s.to_string(), + klass: raw::GIT_ERROR_NONE as c_int, + message: s.into(), } } /// Return the error code associated with this error. + /// + /// An error code is intended to be programmatically actionable most of the + /// time. For example the code `GIT_EAGAIN` indicates that an error could be + /// fixed by trying again, while the code `GIT_ERROR` is more bland and + /// doesn't convey anything in particular. pub fn code(&self) -> ErrorCode { match self.raw_code() { raw::GIT_OK => super::ErrorCode::GenericError, @@ -72,48 +118,143 @@ impl Error { raw::GIT_EINVALID => super::ErrorCode::Invalid, raw::GIT_EUNCOMMITTED => super::ErrorCode::Uncommitted, raw::GIT_EDIRECTORY => super::ErrorCode::Directory, + raw::GIT_EMERGECONFLICT => super::ErrorCode::MergeConflict, + raw::GIT_EMISMATCH => super::ErrorCode::HashsumMismatch, + raw::GIT_EINDEXDIRTY => super::ErrorCode::IndexDirty, + raw::GIT_EAPPLYFAIL => super::ErrorCode::ApplyFail, + raw::GIT_EOWNER => super::ErrorCode::Owner, + raw::GIT_TIMEOUT => super::ErrorCode::Timeout, _ => super::ErrorCode::GenericError, } } + /// Modify the error code associated with this error. + /// + /// This is mainly intended to be used by implementers of custom transports + /// or database backends, and should be used with care. + pub fn set_code(&mut self, code: ErrorCode) { + self.code = match code { + ErrorCode::GenericError => raw::GIT_ERROR, + ErrorCode::NotFound => raw::GIT_ENOTFOUND, + ErrorCode::Exists => raw::GIT_EEXISTS, + ErrorCode::Ambiguous => raw::GIT_EAMBIGUOUS, + ErrorCode::BufSize => raw::GIT_EBUFS, + ErrorCode::User => raw::GIT_EUSER, + ErrorCode::BareRepo => raw::GIT_EBAREREPO, + ErrorCode::UnbornBranch => raw::GIT_EUNBORNBRANCH, + ErrorCode::Unmerged => raw::GIT_EUNMERGED, + ErrorCode::NotFastForward => raw::GIT_ENONFASTFORWARD, + ErrorCode::InvalidSpec => raw::GIT_EINVALIDSPEC, + ErrorCode::Conflict => raw::GIT_ECONFLICT, + ErrorCode::Locked => raw::GIT_ELOCKED, + ErrorCode::Modified => raw::GIT_EMODIFIED, + ErrorCode::Auth => raw::GIT_EAUTH, + ErrorCode::Certificate => raw::GIT_ECERTIFICATE, + ErrorCode::Applied => raw::GIT_EAPPLIED, + ErrorCode::Peel => raw::GIT_EPEEL, + ErrorCode::Eof => raw::GIT_EEOF, + ErrorCode::Invalid => raw::GIT_EINVALID, + ErrorCode::Uncommitted => raw::GIT_EUNCOMMITTED, + ErrorCode::Directory => raw::GIT_EDIRECTORY, + ErrorCode::MergeConflict => raw::GIT_EMERGECONFLICT, + ErrorCode::HashsumMismatch => raw::GIT_EMISMATCH, + ErrorCode::IndexDirty => raw::GIT_EINDEXDIRTY, + ErrorCode::ApplyFail => raw::GIT_EAPPLYFAIL, + ErrorCode::Owner => raw::GIT_EOWNER, + ErrorCode::Timeout => raw::GIT_TIMEOUT, + }; + } + /// Return the error class associated with this error. + /// + /// Error classes are in general mostly just informative. For example the + /// class will show up in the error message but otherwise an error class is + /// typically not directly actionable. pub fn class(&self) -> ErrorClass { match self.raw_class() { - raw::GITERR_NONE => super::ErrorClass::None, - raw::GITERR_NOMEMORY => super::ErrorClass::NoMemory, - raw::GITERR_OS => super::ErrorClass::Os, - raw::GITERR_INVALID => super::ErrorClass::Invalid, - raw::GITERR_REFERENCE => super::ErrorClass::Reference, - raw::GITERR_ZLIB => super::ErrorClass::Zlib, - raw::GITERR_REPOSITORY => super::ErrorClass::Repository, - raw::GITERR_CONFIG => super::ErrorClass::Config, - raw::GITERR_REGEX => super::ErrorClass::Regex, - raw::GITERR_ODB => super::ErrorClass::Odb, - raw::GITERR_INDEX => super::ErrorClass::Index, - raw::GITERR_OBJECT => super::ErrorClass::Object, - raw::GITERR_NET => super::ErrorClass::Net, - raw::GITERR_TAG => super::ErrorClass::Tag, - raw::GITERR_TREE => super::ErrorClass::Tree, - raw::GITERR_INDEXER => super::ErrorClass::Indexer, - raw::GITERR_SSL => super::ErrorClass::Ssl, - raw::GITERR_SUBMODULE => super::ErrorClass::Submodule, - raw::GITERR_THREAD => super::ErrorClass::Thread, - raw::GITERR_STASH => super::ErrorClass::Stash, - raw::GITERR_CHECKOUT => super::ErrorClass::Checkout, - raw::GITERR_FETCHHEAD => super::ErrorClass::FetchHead, - raw::GITERR_MERGE => super::ErrorClass::Merge, - raw::GITERR_SSH => super::ErrorClass::Ssh, - raw::GITERR_FILTER => super::ErrorClass::Filter, - raw::GITERR_REVERT => super::ErrorClass::Revert, - raw::GITERR_CALLBACK => super::ErrorClass::Callback, - raw::GITERR_CHERRYPICK => super::ErrorClass::CherryPick, - raw::GITERR_DESCRIBE => super::ErrorClass::Describe, - raw::GITERR_REBASE => super::ErrorClass::Rebase, - raw::GITERR_FILESYSTEM => super::ErrorClass::Filesystem, + raw::GIT_ERROR_NONE => super::ErrorClass::None, + raw::GIT_ERROR_NOMEMORY => super::ErrorClass::NoMemory, + raw::GIT_ERROR_OS => super::ErrorClass::Os, + raw::GIT_ERROR_INVALID => super::ErrorClass::Invalid, + raw::GIT_ERROR_REFERENCE => super::ErrorClass::Reference, + raw::GIT_ERROR_ZLIB => super::ErrorClass::Zlib, + raw::GIT_ERROR_REPOSITORY => super::ErrorClass::Repository, + raw::GIT_ERROR_CONFIG => super::ErrorClass::Config, + raw::GIT_ERROR_REGEX => super::ErrorClass::Regex, + raw::GIT_ERROR_ODB => super::ErrorClass::Odb, + raw::GIT_ERROR_INDEX => super::ErrorClass::Index, + raw::GIT_ERROR_OBJECT => super::ErrorClass::Object, + raw::GIT_ERROR_NET => super::ErrorClass::Net, + raw::GIT_ERROR_TAG => super::ErrorClass::Tag, + raw::GIT_ERROR_TREE => super::ErrorClass::Tree, + raw::GIT_ERROR_INDEXER => super::ErrorClass::Indexer, + raw::GIT_ERROR_SSL => super::ErrorClass::Ssl, + raw::GIT_ERROR_SUBMODULE => super::ErrorClass::Submodule, + raw::GIT_ERROR_THREAD => super::ErrorClass::Thread, + raw::GIT_ERROR_STASH => super::ErrorClass::Stash, + raw::GIT_ERROR_CHECKOUT => super::ErrorClass::Checkout, + raw::GIT_ERROR_FETCHHEAD => super::ErrorClass::FetchHead, + raw::GIT_ERROR_MERGE => super::ErrorClass::Merge, + raw::GIT_ERROR_SSH => super::ErrorClass::Ssh, + raw::GIT_ERROR_FILTER => super::ErrorClass::Filter, + raw::GIT_ERROR_REVERT => super::ErrorClass::Revert, + raw::GIT_ERROR_CALLBACK => super::ErrorClass::Callback, + raw::GIT_ERROR_CHERRYPICK => super::ErrorClass::CherryPick, + raw::GIT_ERROR_DESCRIBE => super::ErrorClass::Describe, + raw::GIT_ERROR_REBASE => super::ErrorClass::Rebase, + raw::GIT_ERROR_FILESYSTEM => super::ErrorClass::Filesystem, + raw::GIT_ERROR_PATCH => super::ErrorClass::Patch, + raw::GIT_ERROR_WORKTREE => super::ErrorClass::Worktree, + raw::GIT_ERROR_SHA1 => super::ErrorClass::Sha1, + raw::GIT_ERROR_HTTP => super::ErrorClass::Http, _ => super::ErrorClass::None, } } + /// Modify the error class associated with this error. + /// + /// This is mainly intended to be used by implementers of custom transports + /// or database backends, and should be used with care. + pub fn set_class(&mut self, class: ErrorClass) { + self.klass = match class { + ErrorClass::None => raw::GIT_ERROR_NONE, + ErrorClass::NoMemory => raw::GIT_ERROR_NOMEMORY, + ErrorClass::Os => raw::GIT_ERROR_OS, + ErrorClass::Invalid => raw::GIT_ERROR_INVALID, + ErrorClass::Reference => raw::GIT_ERROR_REFERENCE, + ErrorClass::Zlib => raw::GIT_ERROR_ZLIB, + ErrorClass::Repository => raw::GIT_ERROR_REPOSITORY, + ErrorClass::Config => raw::GIT_ERROR_CONFIG, + ErrorClass::Regex => raw::GIT_ERROR_REGEX, + ErrorClass::Odb => raw::GIT_ERROR_ODB, + ErrorClass::Index => raw::GIT_ERROR_INDEX, + ErrorClass::Object => raw::GIT_ERROR_OBJECT, + ErrorClass::Net => raw::GIT_ERROR_NET, + ErrorClass::Tag => raw::GIT_ERROR_TAG, + ErrorClass::Tree => raw::GIT_ERROR_TREE, + ErrorClass::Indexer => raw::GIT_ERROR_INDEXER, + ErrorClass::Ssl => raw::GIT_ERROR_SSL, + ErrorClass::Submodule => raw::GIT_ERROR_SUBMODULE, + ErrorClass::Thread => raw::GIT_ERROR_THREAD, + ErrorClass::Stash => raw::GIT_ERROR_STASH, + ErrorClass::Checkout => raw::GIT_ERROR_CHECKOUT, + ErrorClass::FetchHead => raw::GIT_ERROR_FETCHHEAD, + ErrorClass::Merge => raw::GIT_ERROR_MERGE, + ErrorClass::Ssh => raw::GIT_ERROR_SSH, + ErrorClass::Filter => raw::GIT_ERROR_FILTER, + ErrorClass::Revert => raw::GIT_ERROR_REVERT, + ErrorClass::Callback => raw::GIT_ERROR_CALLBACK, + ErrorClass::CherryPick => raw::GIT_ERROR_CHERRYPICK, + ErrorClass::Describe => raw::GIT_ERROR_DESCRIBE, + ErrorClass::Rebase => raw::GIT_ERROR_REBASE, + ErrorClass::Filesystem => raw::GIT_ERROR_FILESYSTEM, + ErrorClass::Patch => raw::GIT_ERROR_PATCH, + ErrorClass::Worktree => raw::GIT_ERROR_WORKTREE, + ErrorClass::Sha1 => raw::GIT_ERROR_SHA1, + ErrorClass::Http => raw::GIT_ERROR_HTTP, + } as c_int; + } + /// Return the raw error code associated with this error. pub fn raw_code(&self) -> raw::git_error_code { macro_rules! check( ($($e:ident,)*) => ( @@ -145,8 +286,19 @@ impl Error { GIT_EEOF, GIT_EINVALID, GIT_EUNCOMMITTED, + GIT_EDIRECTORY, + GIT_EMERGECONFLICT, GIT_PASSTHROUGH, GIT_ITEROVER, + GIT_RETRY, + GIT_EMISMATCH, + GIT_EINDEXDIRTY, + GIT_EAPPLYFAIL, + GIT_EOWNER, + GIT_TIMEOUT, + GIT_EUNCHANGED, + GIT_ENOTSUPPORTED, + GIT_EREADONLY, ) } @@ -155,80 +307,104 @@ impl Error { macro_rules! check( ($($e:ident,)*) => ( $(if self.klass == raw::$e as c_int { raw::$e }) else * else { - raw::GITERR_NONE + raw::GIT_ERROR_NONE } ) ); check!( - GITERR_NONE, - GITERR_NOMEMORY, - GITERR_OS, - GITERR_INVALID, - GITERR_REFERENCE, - GITERR_ZLIB, - GITERR_REPOSITORY, - GITERR_CONFIG, - GITERR_REGEX, - GITERR_ODB, - GITERR_INDEX, - GITERR_OBJECT, - GITERR_NET, - GITERR_TAG, - GITERR_TREE, - GITERR_INDEXER, - GITERR_SSL, - GITERR_SUBMODULE, - GITERR_THREAD, - GITERR_STASH, - GITERR_CHECKOUT, - GITERR_FETCHHEAD, - GITERR_MERGE, - GITERR_SSH, - GITERR_FILTER, - GITERR_REVERT, - GITERR_CALLBACK, - GITERR_CHERRYPICK, - GITERR_DESCRIBE, - GITERR_REBASE, - GITERR_FILESYSTEM, + GIT_ERROR_NONE, + GIT_ERROR_NOMEMORY, + GIT_ERROR_OS, + GIT_ERROR_INVALID, + GIT_ERROR_REFERENCE, + GIT_ERROR_ZLIB, + GIT_ERROR_REPOSITORY, + GIT_ERROR_CONFIG, + GIT_ERROR_REGEX, + GIT_ERROR_ODB, + GIT_ERROR_INDEX, + GIT_ERROR_OBJECT, + GIT_ERROR_NET, + GIT_ERROR_TAG, + GIT_ERROR_TREE, + GIT_ERROR_INDEXER, + GIT_ERROR_SSL, + GIT_ERROR_SUBMODULE, + GIT_ERROR_THREAD, + GIT_ERROR_STASH, + GIT_ERROR_CHECKOUT, + GIT_ERROR_FETCHHEAD, + GIT_ERROR_MERGE, + GIT_ERROR_SSH, + GIT_ERROR_FILTER, + GIT_ERROR_REVERT, + GIT_ERROR_CALLBACK, + GIT_ERROR_CHERRYPICK, + GIT_ERROR_DESCRIBE, + GIT_ERROR_REBASE, + GIT_ERROR_FILESYSTEM, + GIT_ERROR_PATCH, + GIT_ERROR_WORKTREE, + GIT_ERROR_SHA1, + GIT_ERROR_HTTP, ) } /// Return the message associated with this error - pub fn message(&self) -> &str { &self.message } -} + pub fn message(&self) -> &str { + &self.message + } -impl error::Error for Error { - fn description(&self) -> &str { &self.message } + /// A low-level convenience to call [`raw::git_error_set_str`] with the + /// information from this error. + /// + /// Returns the [`Error::raw_code`] value of this error, which is often + /// needed from a C callback. + pub(crate) unsafe fn raw_set_git_error(&self) -> raw::git_error_code { + let s = CString::new(self.message()).unwrap(); + raw::git_error_set_str(self.class() as c_int, s.as_ptr()); + self.raw_code() + } } +impl error::Error for Error {} + impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - try!(write!(f, "[{}/{}] ", self.klass, self.code)); - f.write_str(&self.message) + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.message)?; + match self.class() { + ErrorClass::None => {} + other => write!(f, "; class={:?} ({})", other, self.klass)?, + } + match self.code() { + ErrorCode::GenericError => {} + other => write!(f, "; code={:?} ({})", other, self.code)?, + } + Ok(()) } } impl From for Error { fn from(_: NulError) -> Error { - Error::from_str("data contained a nul byte that could not be \ - represented as a string") + Error::from_str( + "data contained a nul byte that could not be \ + represented as a string", + ) } } impl From for Error { fn from(e: JoinPathsError) -> Error { - Error::from_str(error::Error::description(&e)) + Error::from_str(&e.to_string()) } } - #[cfg(test)] mod tests { - use {ErrorClass, ErrorCode}; + use crate::{ErrorClass, ErrorCode}; #[test] fn smoke() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let err = repo.find_submodule("does_not_exist").err().unwrap(); assert_eq!(err.code(), ErrorCode::NotFound); diff --git a/src/index.rs b/src/index.rs index d9629cb808..c0d9294520 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1,13 +1,15 @@ -use std::ffi::{CStr, OsString, CString}; +use std::ffi::{CStr, CString}; +use std::marker; use std::ops::Range; use std::path::Path; +use std::ptr; use std::slice; -use libc::{c_int, c_uint, size_t, c_void, c_char}; +use libc::{c_char, c_int, c_uint, c_void, size_t}; -use {raw, panic, Repository, Error, Tree, Oid, IndexAddOption, IndexTime}; -use IntoCString; -use util::{self, Binding}; +use crate::util::{self, path_to_repo_path, Binding}; +use crate::IntoCString; +use crate::{panic, raw, Error, IndexAddOption, IndexTime, Oid, Repository, Tree}; /// A structure to represent a git [index][1] /// @@ -22,18 +24,37 @@ pub struct IndexEntries<'index> { index: &'index Index, } +/// An iterator over the conflicting entries in an index +pub struct IndexConflicts<'index> { + conflict_iter: *mut raw::git_index_conflict_iterator, + _marker: marker::PhantomData<&'index Index>, +} + +/// A structure to represent the information returned when a conflict is detected in an index entry +pub struct IndexConflict { + /// The ancestor index entry of the two conflicting index entries + pub ancestor: Option, + /// The index entry originating from the user's copy of the repository. + /// Its contents conflict with 'their' index entry + pub our: Option, + /// The index entry originating from the external repository. + /// Its contents conflict with 'our' index entry + pub their: Option, +} + /// A callback function to filter index matches. /// /// Used by `Index::{add_all,remove_all,update_all}`. The first argument is the -/// path, and the second is the patchspec that matched it. Return 0 to confirm +/// path, and the second is the pathspec that matched it. Return 0 to confirm /// the operation on the item, > 0 to skip the item, and < 0 to abort the scan. -pub type IndexMatchedPath<'a> = FnMut(&Path, &[u8]) -> i32 + 'a; +pub type IndexMatchedPath<'a> = dyn FnMut(&Path, &[u8]) -> i32 + 'a; /// A structure to represent an entry or a file inside of an index. /// /// All fields of an entry are public for modification and inspection. This is /// also how a new index entry is created. #[allow(missing_docs)] +#[derive(Debug)] pub struct IndexEntry { pub ctime: IndexTime, pub mtime: IndexTime, @@ -46,6 +67,21 @@ pub struct IndexEntry { pub id: Oid, pub flags: u16, pub flags_extended: u16, + + /// The path of this index entry as a byte vector. Regardless of the + /// current platform, the directory separator is an ASCII forward slash + /// (`0x2F`). There are no terminating or internal NUL characters, and no + /// trailing slashes. Most of the time, paths will be valid utf-8 — but + /// not always. For more information on the path storage format, see + /// [these git docs][git-index-docs]. Note that libgit2 will take care of + /// handling the prefix compression mentioned there. + /// + /// [git-index-docs]: https://github.com/git/git/blob/a08a83db2bf27f015bec9a435f6d73e223c21c5e/Documentation/technical/index-format.txt#L107-L124 + /// + /// You can turn this value into a `std::ffi::CString` with + /// `CString::new(&entry.path[..]).unwrap()`. To turn a reference into a + /// `&std::path::Path`, see the `bytes2path()` function in the private, + /// internal `util` module in this crate’s source code. pub path: Vec, } @@ -55,8 +91,8 @@ impl Index { /// This index object cannot be read/written to the filesystem, but may be /// used to perform in-memory index operations. pub fn new() -> Result { - ::init(); - let mut raw = 0 as *mut raw::git_index; + crate::init(); + let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_index_new(&mut raw)); Ok(Binding::from_raw(raw)) @@ -72,32 +108,54 @@ impl Index { /// If you need an index attached to a repository, use the `index()` method /// on `Repository`. pub fn open(index_path: &Path) -> Result { - ::init(); - let mut raw = 0 as *mut raw::git_index; - let index_path = try!(index_path.into_c_string()); + crate::init(); + let mut raw = ptr::null_mut(); + // Normal file path OK (does not need Windows conversion). + let index_path = index_path.into_c_string()?; unsafe { try_call!(raw::git_index_open(&mut raw, index_path)); Ok(Binding::from_raw(raw)) } } + /// Get index on-disk version. + /// + /// Valid return values are 2, 3, or 4. If 3 is returned, an index + /// with version 2 may be written instead, if the extension data in + /// version 3 is not necessary. + pub fn version(&self) -> u32 { + unsafe { raw::git_index_version(self.raw) } + } + + /// Set index on-disk version. + /// + /// Valid values are 2, 3, or 4. If 2 is given, git_index_write may + /// write an index with version 3 instead, if necessary to accurately + /// represent the index. + pub fn set_version(&mut self, version: u32) -> Result<(), Error> { + unsafe { + try_call!(raw::git_index_set_version(self.raw, version)); + } + Ok(()) + } + /// Add or update an index entry from an in-memory struct /// /// If a previous index entry exists that has the same path and stage as the /// given 'source_entry', it will be replaced. Otherwise, the 'source_entry' /// will be added. pub fn add(&mut self, entry: &IndexEntry) -> Result<(), Error> { - let path = try!(CString::new(&entry.path[..])); + let path = CString::new(&entry.path[..])?; // libgit2 encodes the length of the path in the lower bits of the // `flags` entry, so mask those out and recalculate here to ensure we // don't corrupt anything. - let mut flags = entry.flags & !raw::GIT_IDXENTRY_NAMEMASK; + let mut flags = entry.flags & !raw::GIT_INDEX_ENTRY_NAMEMASK; - if entry.path.len() < raw::GIT_IDXENTRY_NAMEMASK as usize { + if entry.path.len() < raw::GIT_INDEX_ENTRY_NAMEMASK as usize { flags |= entry.path.len() as u16; } else { - flags |= raw::GIT_IDXENTRY_NAMEMASK; + flags |= raw::GIT_INDEX_ENTRY_NAMEMASK; } unsafe { @@ -109,7 +167,7 @@ impl Index { gid: entry.gid, file_size: entry.file_size, id: *entry.id.raw(), - flags: flags, + flags, flags_extended: entry.flags_extended, path: path.as_ptr(), mtime: raw::git_index_time { @@ -126,6 +184,66 @@ impl Index { } } + /// Add or update an index entry from a buffer in memory + /// + /// This method will create a blob in the repository that owns the index and + /// then add the index entry to the index. The path of the entry represents + /// the position of the blob relative to the repository's root folder. + /// + /// If a previous index entry exists that has the same path as the given + /// 'entry', it will be replaced. Otherwise, the 'entry' will be added. + /// The id and the file_size of the 'entry' are updated with the real value + /// of the blob. + /// + /// This forces the file to be added to the index, not looking at gitignore + /// rules. + /// + /// If this file currently is the result of a merge conflict, this file will + /// no longer be marked as conflicting. The data about the conflict will be + /// moved to the "resolve undo" (REUC) section. + pub fn add_frombuffer(&mut self, entry: &IndexEntry, data: &[u8]) -> Result<(), Error> { + let path = CString::new(&entry.path[..])?; + + // libgit2 encodes the length of the path in the lower bits of the + // `flags` entry, so mask those out and recalculate here to ensure we + // don't corrupt anything. + let mut flags = entry.flags & !raw::GIT_INDEX_ENTRY_NAMEMASK; + + if entry.path.len() < raw::GIT_INDEX_ENTRY_NAMEMASK as usize { + flags |= entry.path.len() as u16; + } else { + flags |= raw::GIT_INDEX_ENTRY_NAMEMASK; + } + + unsafe { + let raw = raw::git_index_entry { + dev: entry.dev, + ino: entry.ino, + mode: entry.mode, + uid: entry.uid, + gid: entry.gid, + file_size: entry.file_size, + id: *entry.id.raw(), + flags, + flags_extended: entry.flags_extended, + path: path.as_ptr(), + mtime: raw::git_index_time { + seconds: entry.mtime.seconds(), + nanoseconds: entry.mtime.nanoseconds(), + }, + ctime: raw::git_index_time { + seconds: entry.ctime.seconds(), + nanoseconds: entry.ctime.nanoseconds(), + }, + }; + + let ptr = data.as_ptr() as *const c_void; + let len = data.len() as size_t; + try_call!(raw::git_index_add_frombuffer(self.raw, &raw, ptr, len)); + Ok(()) + } + } + /// Add or update an index entry from a file on disk /// /// The file path must be relative to the repository's working folder and @@ -140,13 +258,7 @@ impl Index { /// no longer be marked as conflicting. The data about the conflict will be /// moved to the "resolve undo" (REUC) section. pub fn add_path(&mut self, path: &Path) -> Result<(), Error> { - // Git apparently expects '/' to be separators for paths - let mut posix_path = OsString::new(); - for (i, comp) in path.components().enumerate() { - if i != 0 { posix_path.push("/"); } - posix_path.push(comp.as_os_str()); - } - let posix_path = try!(posix_path.into_c_string()); + let posix_path = path_to_repo_path(path)?; unsafe { try_call!(raw::git_index_add_bypath(self.raw, posix_path)); Ok(()) @@ -186,28 +298,44 @@ impl Index { /// updated in the index. Returning zero will add the item to the index, /// greater than zero will skip the item, and less than zero will abort the /// scan an return an error to the caller. - pub fn add_all(&mut self, - pathspecs: I, - flag: IndexAddOption, - mut cb: Option<&mut IndexMatchedPath>) - -> Result<(), Error> - where T: IntoCString, I: IntoIterator, + /// + /// # Example + /// + /// Emulate `git add *`: + /// + /// ```no_run + /// use git2::{Index, IndexAddOption, Repository}; + /// + /// let repo = Repository::open("/path/to/a/repo").expect("failed to open"); + /// let mut index = repo.index().expect("cannot get the Index file"); + /// index.add_all(["*"].iter(), IndexAddOption::DEFAULT, None); + /// index.write(); + /// ``` + pub fn add_all( + &mut self, + pathspecs: I, + flag: IndexAddOption, + mut cb: Option<&mut IndexMatchedPath<'_>>, + ) -> Result<(), Error> + where + T: IntoCString, + I: IntoIterator, { - let (_a, _b, raw_strarray) = try!(::util::iter2cstrs(pathspecs)); + let (_a, _b, raw_strarray) = crate::util::iter2cstrs_paths(pathspecs)?; let ptr = cb.as_mut(); - let callback = ptr.as_ref().map(|_| { - index_matched_path_cb as raw::git_index_matched_path_cb - }); + let callback = ptr + .as_ref() + .map(|_| index_matched_path_cb as extern "C" fn(_, _, _) -> _); unsafe { - try_call!(raw::git_index_add_all(self.raw, - &raw_strarray, - flag.bits() as c_uint, - callback, - ptr.map(|p| p as *mut _) - .unwrap_or(0 as *mut _) - as *mut c_void)); + try_call!(raw::git_index_add_all( + self.raw, + &raw_strarray, + flag.bits() as c_uint, + callback, + ptr.map(|p| p as *mut _).unwrap_or(ptr::null_mut()) as *mut c_void + )); } - return Ok(()); + Ok(()) } /// Clear the contents (all the entries) of an index object. @@ -215,7 +343,9 @@ impl Index { /// This clears the index object in memory; changes must be explicitly /// written to disk for them to take effect persistently via `write_*`. pub fn clear(&mut self) -> Result<(), Error> { - unsafe { try_call!(raw::git_index_clear(self.raw)); } + unsafe { + try_call!(raw::git_index_clear(self.raw)); + } Ok(()) } @@ -224,26 +354,94 @@ impl Index { unsafe { raw::git_index_entrycount(&*self.raw) as usize } } + /// Return `true` is there is no entry in the index + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Get one of the entries in the index by its position. pub fn get(&self, n: usize) -> Option { unsafe { let ptr = raw::git_index_get_byindex(self.raw, n as size_t); - if ptr.is_null() {None} else {Some(Binding::from_raw(*ptr))} + if ptr.is_null() { + None + } else { + Some(Binding::from_raw(*ptr)) + } } } /// Get an iterator over the entries in this index. - pub fn iter(&self) -> IndexEntries { - IndexEntries { range: 0..self.len(), index: self } + pub fn iter(&self) -> IndexEntries<'_> { + IndexEntries { + range: 0..self.len(), + index: self, + } + } + + /// Get an iterator over the index entries that have conflicts + pub fn conflicts(&self) -> Result, Error> { + crate::init(); + let mut conflict_iter = ptr::null_mut(); + unsafe { + try_call!(raw::git_index_conflict_iterator_new( + &mut conflict_iter, + self.raw + )); + Ok(Binding::from_raw(conflict_iter)) + } } /// Get one of the entries in the index by its path. pub fn get_path(&self, path: &Path, stage: i32) -> Option { - let path = path.into_c_string().unwrap(); + let path = path_to_repo_path(path).unwrap(); unsafe { - let ptr = call!(raw::git_index_get_bypath(self.raw, path, - stage as c_int)); - if ptr.is_null() {None} else {Some(Binding::from_raw(*ptr))} + let ptr = call!(raw::git_index_get_bypath(self.raw, path, stage as c_int)); + if ptr.is_null() { + None + } else { + Some(Binding::from_raw(*ptr)) + } + } + } + + /// Does this index have conflicts? + /// + /// Returns `true` if the index contains conflicts, `false` if it does not. + pub fn has_conflicts(&self) -> bool { + unsafe { raw::git_index_has_conflicts(self.raw) == 1 } + } + + /// Get the index entries that represent a conflict of a single file. + pub fn conflict_get(&self, path: &Path) -> Result { + let path = path_to_repo_path(path)?; + let mut ancestor = ptr::null(); + let mut our = ptr::null(); + let mut their = ptr::null(); + + unsafe { + try_call!(raw::git_index_conflict_get( + &mut ancestor, + &mut our, + &mut their, + self.raw, + path + )); + + Ok(IndexConflict { + ancestor: match ancestor.is_null() { + false => Some(IndexEntry::from_raw(*ancestor)), + true => None, + }, + our: match our.is_null() { + false => Some(IndexEntry::from_raw(*our)), + true => None, + }, + their: match their.is_null() { + false => Some(IndexEntry::from_raw(*their)), + true => None, + }, + }) } } @@ -251,9 +449,7 @@ impl Index { /// /// Returns `None` if this is an in-memory index. pub fn path(&self) -> Option<&Path> { - unsafe { - ::opt_bytes(self, raw::git_index_path(&*self.raw)).map(util::bytes2path) - } + unsafe { crate::opt_bytes(self, raw::git_index_path(&*self.raw)).map(util::bytes2path) } } /// Update the contents of an existing index object in memory by reading @@ -268,21 +464,25 @@ impl Index { /// Purely in-memory index data will be untouched. Be aware: if there are /// changes on disk, unwritten in-memory changes are discarded. pub fn read(&mut self, force: bool) -> Result<(), Error> { - unsafe { try_call!(raw::git_index_read(self.raw, force)); } + unsafe { + try_call!(raw::git_index_read(self.raw, force)); + } Ok(()) } /// Read a tree into the index file with stats /// /// The current index contents will be replaced by the specified tree. - pub fn read_tree(&mut self, tree: &Tree) -> Result<(), Error> { - unsafe { try_call!(raw::git_index_read_tree(self.raw, &*tree.raw())); } + pub fn read_tree(&mut self, tree: &Tree<'_>) -> Result<(), Error> { + unsafe { + try_call!(raw::git_index_read_tree(self.raw, &*tree.raw())); + } Ok(()) } /// Remove an entry from the index pub fn remove(&mut self, path: &Path, stage: i32) -> Result<(), Error> { - let path = try!(path.into_c_string()); + let path = path_to_repo_path(path)?; unsafe { try_call!(raw::git_index_remove(self.raw, path, stage as c_int)); } @@ -298,7 +498,7 @@ impl Index { /// no longer be marked as conflicting. The data about the conflict will be /// moved to the "resolve undo" (REUC) section. pub fn remove_path(&mut self, path: &Path) -> Result<(), Error> { - let path = try!(path.into_c_string()); + let path = path_to_repo_path(path)?; unsafe { try_call!(raw::git_index_remove_bypath(self.raw, path)); } @@ -307,10 +507,22 @@ impl Index { /// Remove all entries from the index under a given directory. pub fn remove_dir(&mut self, path: &Path, stage: i32) -> Result<(), Error> { - let path = try!(path.into_c_string()); + let path = path_to_repo_path(path)?; unsafe { - try_call!(raw::git_index_remove_directory(self.raw, path, - stage as c_int)); + try_call!(raw::git_index_remove_directory( + self.raw, + path, + stage as c_int + )); + } + Ok(()) + } + + /// Removes the index entries that represent a conflict of a single file. + pub fn conflict_remove(&mut self, path: &Path) -> Result<(), Error> { + let path = path_to_repo_path(path)?; + unsafe { + try_call!(raw::git_index_conflict_remove(self.raw, path)); } Ok(()) } @@ -320,26 +532,29 @@ impl Index { /// If you provide a callback function, it will be invoked on each matching /// item in the index immediately before it is removed. Return 0 to remove /// the item, > 0 to skip the item, and < 0 to abort the scan. - pub fn remove_all(&mut self, - pathspecs: I, - mut cb: Option<&mut IndexMatchedPath>) - -> Result<(), Error> - where T: IntoCString, I: IntoIterator, + pub fn remove_all( + &mut self, + pathspecs: I, + mut cb: Option<&mut IndexMatchedPath<'_>>, + ) -> Result<(), Error> + where + T: IntoCString, + I: IntoIterator, { - let (_a, _b, raw_strarray) = try!(::util::iter2cstrs(pathspecs)); + let (_a, _b, raw_strarray) = crate::util::iter2cstrs_paths(pathspecs)?; let ptr = cb.as_mut(); - let callback = ptr.as_ref().map(|_| { - index_matched_path_cb as raw::git_index_matched_path_cb - }); + let callback = ptr + .as_ref() + .map(|_| index_matched_path_cb as extern "C" fn(_, _, _) -> _); unsafe { - try_call!(raw::git_index_remove_all(self.raw, - &raw_strarray, - callback, - ptr.map(|p| p as *mut _) - .unwrap_or(0 as *mut _) - as *mut c_void)); + try_call!(raw::git_index_remove_all( + self.raw, + &raw_strarray, + callback, + ptr.map(|p| p as *mut _).unwrap_or(ptr::null_mut()) as *mut c_void + )); } - return Ok(()); + Ok(()) } /// Update all index entries to match the working directory @@ -355,32 +570,37 @@ impl Index { /// item in the index immediately before it is updated (either refreshed or /// removed depending on working directory state). Return 0 to proceed with /// updating the item, > 0 to skip the item, and < 0 to abort the scan. - pub fn update_all(&mut self, - pathspecs: I, - mut cb: Option<&mut IndexMatchedPath>) - -> Result<(), Error> - where T: IntoCString, I: IntoIterator, + pub fn update_all( + &mut self, + pathspecs: I, + mut cb: Option<&mut IndexMatchedPath<'_>>, + ) -> Result<(), Error> + where + T: IntoCString, + I: IntoIterator, { - let (_a, _b, raw_strarray) = try!(::util::iter2cstrs(pathspecs)); + let (_a, _b, raw_strarray) = crate::util::iter2cstrs_paths(pathspecs)?; let ptr = cb.as_mut(); - let callback = ptr.as_ref().map(|_| { - index_matched_path_cb as raw::git_index_matched_path_cb - }); + let callback = ptr + .as_ref() + .map(|_| index_matched_path_cb as extern "C" fn(_, _, _) -> _); unsafe { - try_call!(raw::git_index_update_all(self.raw, - &raw_strarray, - callback, - ptr.map(|p| p as *mut _) - .unwrap_or(0 as *mut _) - as *mut c_void)); + try_call!(raw::git_index_update_all( + self.raw, + &raw_strarray, + callback, + ptr.map(|p| p as *mut _).unwrap_or(ptr::null_mut()) as *mut c_void + )); } - return Ok(()); + Ok(()) } /// Write an existing index object from memory back to disk using an atomic /// file lock. pub fn write(&mut self) -> Result<(), Error> { - unsafe { try_call!(raw::git_index_write(self.raw)); } + unsafe { + try_call!(raw::git_index_write(self.raw)); + } Ok(()) } @@ -396,7 +616,9 @@ impl Index { /// /// The index must not contain any file in conflict. pub fn write_tree(&mut self) -> Result { - let mut raw = raw::git_oid { id: [0; raw::GIT_OID_RAWSZ] }; + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; unsafe { try_call!(raw::git_index_write_tree(&mut raw, self.raw)); Ok(Binding::from_raw(&raw as *const _)) @@ -408,34 +630,116 @@ impl Index { /// This is the same as `write_tree` except that the destination repository /// can be chosen. pub fn write_tree_to(&mut self, repo: &Repository) -> Result { - let mut raw = raw::git_oid { id: [0; raw::GIT_OID_RAWSZ] }; + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; unsafe { - try_call!(raw::git_index_write_tree_to(&mut raw, self.raw, - repo.raw())); + try_call!(raw::git_index_write_tree_to(&mut raw, self.raw, repo.raw())); Ok(Binding::from_raw(&raw as *const _)) } } + + /// Find the first position of any entries matching a prefix. + /// + /// To find the first position of a path inside a given folder, suffix the prefix with a '/'. + pub fn find_prefix(&self, prefix: T) -> Result { + let mut at_pos: size_t = 0; + let entry_path = prefix.into_c_string()?; + unsafe { + try_call!(raw::git_index_find_prefix( + &mut at_pos, + self.raw, + entry_path + )); + Ok(at_pos) + } + } +} + +impl IndexEntry { + /// Create a raw index entry. + /// + /// The returned `raw::git_index_entry` contains a pointer to a `CString` path, which is also + /// returned because it's lifetime must exceed the lifetime of the `raw::git_index_entry`. + pub(crate) unsafe fn to_raw(&self) -> Result<(raw::git_index_entry, CString), Error> { + let path = CString::new(&self.path[..])?; + + // libgit2 encodes the length of the path in the lower bits of the + // `flags` entry, so mask those out and recalculate here to ensure we + // don't corrupt anything. + let mut flags = self.flags & !raw::GIT_INDEX_ENTRY_NAMEMASK; + + if self.path.len() < raw::GIT_INDEX_ENTRY_NAMEMASK as usize { + flags |= self.path.len() as u16; + } else { + flags |= raw::GIT_INDEX_ENTRY_NAMEMASK; + } + + unsafe { + let raw = raw::git_index_entry { + dev: self.dev, + ino: self.ino, + mode: self.mode, + uid: self.uid, + gid: self.gid, + file_size: self.file_size, + id: *self.id.raw(), + flags, + flags_extended: self.flags_extended, + path: path.as_ptr(), + mtime: raw::git_index_time { + seconds: self.mtime.seconds(), + nanoseconds: self.mtime.nanoseconds(), + }, + ctime: raw::git_index_time { + seconds: self.ctime.seconds(), + nanoseconds: self.ctime.nanoseconds(), + }, + }; + + Ok((raw, path)) + } + } } impl Binding for Index { type Raw = *mut raw::git_index; unsafe fn from_raw(raw: *mut raw::git_index) -> Index { - Index { raw: raw } + Index { raw } + } + fn raw(&self) -> *mut raw::git_index { + self.raw } - fn raw(&self) -> *mut raw::git_index { self.raw } } -extern fn index_matched_path_cb(path: *const c_char, - matched_pathspec: *const c_char, - payload: *mut c_void) -> c_int { +impl<'index> Binding for IndexConflicts<'index> { + type Raw = *mut raw::git_index_conflict_iterator; + + unsafe fn from_raw(raw: *mut raw::git_index_conflict_iterator) -> IndexConflicts<'index> { + IndexConflicts { + conflict_iter: raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_index_conflict_iterator { + self.conflict_iter + } +} + +extern "C" fn index_matched_path_cb( + path: *const c_char, + matched_pathspec: *const c_char, + payload: *mut c_void, +) -> c_int { unsafe { let path = CStr::from_ptr(path).to_bytes(); let matched_pathspec = CStr::from_ptr(matched_pathspec).to_bytes(); panic::wrap(|| { - let payload = payload as *mut &mut IndexMatchedPath; + let payload = payload as *mut &mut IndexMatchedPath<'_>; (*payload)(util::bytes2path(path), matched_pathspec) as c_int - }).unwrap_or(-1) + }) + .unwrap_or(-1) } } @@ -445,6 +749,12 @@ impl Drop for Index { } } +impl<'index> Drop for IndexConflicts<'index> { + fn drop(&mut self) { + unsafe { raw::git_index_conflict_iterator_free(self.conflict_iter) } + } +} + impl<'index> Iterator for IndexEntries<'index> { type Item = IndexEntry; fn next(&mut self) -> Option { @@ -452,35 +762,76 @@ impl<'index> Iterator for IndexEntries<'index> { } } +impl<'index> Iterator for IndexConflicts<'index> { + type Item = Result; + fn next(&mut self) -> Option> { + let mut ancestor = ptr::null(); + let mut our = ptr::null(); + let mut their = ptr::null(); + unsafe { + try_call_iter!(raw::git_index_conflict_next( + &mut ancestor, + &mut our, + &mut their, + self.conflict_iter + )); + Some(Ok(IndexConflict { + ancestor: match ancestor.is_null() { + false => Some(IndexEntry::from_raw(*ancestor)), + true => None, + }, + our: match our.is_null() { + false => Some(IndexEntry::from_raw(*our)), + true => None, + }, + their: match their.is_null() { + false => Some(IndexEntry::from_raw(*their)), + true => None, + }, + })) + } + } +} + impl Binding for IndexEntry { type Raw = raw::git_index_entry; unsafe fn from_raw(raw: raw::git_index_entry) -> IndexEntry { let raw::git_index_entry { - ctime, mtime, dev, ino, mode, uid, gid, file_size, id, flags, - flags_extended, path + ctime, + mtime, + dev, + ino, + mode, + uid, + gid, + file_size, + id, + flags, + flags_extended, + path, } = raw; // libgit2 encodes the length of the path in the lower bits of `flags`, // but if the length exceeds the number of bits then the path is // nul-terminated. - let mut pathlen = (flags & raw::GIT_IDXENTRY_NAMEMASK) as usize; - if pathlen == raw::GIT_IDXENTRY_NAMEMASK as usize { + let mut pathlen = (flags & raw::GIT_INDEX_ENTRY_NAMEMASK) as usize; + if pathlen == raw::GIT_INDEX_ENTRY_NAMEMASK as usize { pathlen = CStr::from_ptr(path).to_bytes().len(); } let path = slice::from_raw_parts(path as *const u8, pathlen); IndexEntry { - dev: dev, - ino: ino, - mode: mode, - uid: uid, - gid: gid, - file_size: file_size, + dev, + ino, + mode, + uid, + gid, + file_size, id: Binding::from_raw(&id as *const _), - flags: flags, - flags_extended: flags_extended, + flags, + flags_extended, path: path.to_vec(), mtime: Binding::from_raw(mtime), ctime: Binding::from_raw(ctime), @@ -497,9 +848,9 @@ impl Binding for IndexEntry { mod tests { use std::fs::{self, File}; use std::path::Path; - use tempdir::TempDir; + use tempfile::TempDir; - use {Index, IndexEntry, Repository, ResetType, Oid, IndexTime}; + use crate::{ErrorCode, Index, IndexEntry, IndexTime, Oid, Repository, ResetType}; #[test] fn smoke() { @@ -514,10 +865,12 @@ mod tests { #[test] fn smoke_from_repo() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let mut index = repo.index().unwrap(); - assert_eq!(index.path().map(|s| s.to_path_buf()), - Some(repo.path().join("index"))); + assert_eq!( + index.path().map(|s| s.to_path_buf()), + Some(repo.path().join("index")) + ); Index::open(&repo.path().join("index")).unwrap(); index.clear().unwrap(); @@ -529,37 +882,47 @@ mod tests { #[test] fn add_all() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let mut index = repo.index().unwrap(); let root = repo.path().parent().unwrap(); fs::create_dir(&root.join("foo")).unwrap(); File::create(&root.join("foo/bar")).unwrap(); let mut called = false; - index.add_all(["foo"].iter(), ::ADD_DEFAULT, - Some(&mut |a: &Path, b: &[u8]| { - assert!(!called); - called = true; - assert_eq!(b, b"foo"); - assert_eq!(a, Path::new("foo/bar")); - 0 - })).unwrap(); + index + .add_all( + ["foo"].iter(), + crate::IndexAddOption::DEFAULT, + Some(&mut |a: &Path, b: &[u8]| { + assert!(!called); + called = true; + assert_eq!(b, b"foo"); + assert_eq!(a, Path::new("foo/bar")); + 0 + }), + ) + .unwrap(); assert!(called); called = false; - index.remove_all(["."].iter(), Some(&mut |a: &Path, b: &[u8]| { - assert!(!called); - called = true; - assert_eq!(b, b"."); - assert_eq!(a, Path::new("foo/bar")); - 0 - })).unwrap(); + index + .remove_all( + ["."].iter(), + Some(&mut |a: &Path, b: &[u8]| { + assert!(!called); + called = true; + assert_eq!(b, b"."); + assert_eq!(a, Path::new("foo/bar")); + 0 + }), + ) + .unwrap(); assert!(called); } #[test] fn smoke_add() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let mut index = repo.index().unwrap(); let root = repo.path().parent().unwrap(); @@ -575,13 +938,14 @@ mod tests { let sig = repo.signature().unwrap(); let id = repo.refname_to_id("HEAD").unwrap(); let parent = repo.find_commit(id).unwrap(); - let commit = repo.commit(Some("HEAD"), &sig, &sig, "commit", - &tree, &[&parent]).unwrap(); + let commit = repo + .commit(Some("HEAD"), &sig, &sig, "commit", &tree, &[&parent]) + .unwrap(); let obj = repo.find_object(commit, None).unwrap(); repo.reset(&obj, ResetType::Hard, None).unwrap(); - let td2 = TempDir::new("git").unwrap(); - let url = ::test::path2url(https://github.com/&root); + let td2 = TempDir::new().unwrap(); + let url = crate::test::path2url(https://github.com/&root); let repo = Repository::clone(&url, td2.path()).unwrap(); let obj = repo.find_object(commit, None).unwrap(); repo.reset(&obj, ResetType::Hard, None).unwrap(); @@ -590,14 +954,48 @@ mod tests { #[test] fn add_then_read() { let mut index = Index::new().unwrap(); - assert!(index.add(&entry()).is_err()); + let mut e = entry(); + e.path = b"foobar".to_vec(); + index.add(&e).unwrap(); + let e = index.get(0).unwrap(); + assert_eq!(e.path.len(), 6); + } + #[test] + fn add_then_find() { let mut index = Index::new().unwrap(); let mut e = entry(); - e.path = b"foobar".to_vec(); + e.path = b"foo/bar".to_vec(); + index.add(&e).unwrap(); + let mut e = entry(); + e.path = b"foo2/bar".to_vec(); index.add(&e).unwrap(); + assert_eq!(index.get(0).unwrap().path, b"foo/bar"); + assert_eq!( + index.get_path(Path::new("foo/bar"), 0).unwrap().path, + b"foo/bar" + ); + assert_eq!(index.find_prefix(Path::new("foo2/")), Ok(1)); + assert_eq!( + index.find_prefix(Path::new("empty/")).unwrap_err().code(), + ErrorCode::NotFound + ); + } + + #[test] + fn add_frombuffer_then_read() { + let (_td, repo) = crate::test::repo_init(); + let mut index = repo.index().unwrap(); + + let mut e = entry(); + e.path = b"foobar".to_vec(); + let content = b"the contents"; + index.add_frombuffer(&e, content).unwrap(); let e = index.get(0).unwrap(); assert_eq!(e.path.len(), 6); + + let b = repo.find_blob(e.id).unwrap(); + assert_eq!(b.content(), content); } fn entry() -> IndexEntry { @@ -617,4 +1015,3 @@ mod tests { } } } - diff --git a/src/indexer.rs b/src/indexer.rs new file mode 100644 index 0000000000..3a3ff62a5a --- /dev/null +++ b/src/indexer.rs @@ -0,0 +1,255 @@ +use std::ffi::CStr; +use std::path::Path; +use std::{io, marker, mem, ptr}; + +use libc::c_void; + +use crate::odb::{write_pack_progress_cb, OdbPackwriterCb}; +use crate::util::Binding; +use crate::{raw, Error, IntoCString, Odb}; + +/// Struct representing the progress by an in-flight transfer. +pub struct Progress<'a> { + pub(crate) raw: ProgressState, + pub(crate) _marker: marker::PhantomData<&'a raw::git_indexer_progress>, +} + +pub(crate) enum ProgressState { + Borrowed(*const raw::git_indexer_progress), + Owned(raw::git_indexer_progress), +} + +/// Callback to be invoked while indexing is in progress. +/// +/// This callback will be periodically called with updates to the progress of +/// the indexing so far. The return value indicates whether the indexing or +/// transfer should continue. A return value of `false` will cancel the +/// indexing or transfer. +/// +/// * `progress` - the progress being made so far. +pub type IndexerProgress<'a> = dyn FnMut(Progress<'_>) -> bool + 'a; + +impl<'a> Progress<'a> { + /// Number of objects in the packfile being downloaded + pub fn total_objects(&self) -> usize { + unsafe { (*self.raw()).total_objects as usize } + } + /// Received objects that have been hashed + pub fn indexed_objects(&self) -> usize { + unsafe { (*self.raw()).indexed_objects as usize } + } + /// Objects which have been downloaded + pub fn received_objects(&self) -> usize { + unsafe { (*self.raw()).received_objects as usize } + } + /// Locally-available objects that have been injected in order to fix a thin + /// pack. + pub fn local_objects(&self) -> usize { + unsafe { (*self.raw()).local_objects as usize } + } + /// Number of deltas in the packfile being downloaded + pub fn total_deltas(&self) -> usize { + unsafe { (*self.raw()).total_deltas as usize } + } + /// Received deltas that have been hashed. + pub fn indexed_deltas(&self) -> usize { + unsafe { (*self.raw()).indexed_deltas as usize } + } + /// Size of the packfile received up to now + pub fn received_bytes(&self) -> usize { + unsafe { (*self.raw()).received_bytes as usize } + } + + /// Convert this to an owned version of `Progress`. + pub fn to_owned(&self) -> Progress<'static> { + Progress { + raw: ProgressState::Owned(unsafe { *self.raw() }), + _marker: marker::PhantomData, + } + } +} + +impl<'a> Binding for Progress<'a> { + type Raw = *const raw::git_indexer_progress; + unsafe fn from_raw(raw: *const raw::git_indexer_progress) -> Progress<'a> { + Progress { + raw: ProgressState::Borrowed(raw), + _marker: marker::PhantomData, + } + } + + fn raw(&self) -> *const raw::git_indexer_progress { + match self.raw { + ProgressState::Borrowed(raw) => raw, + ProgressState::Owned(ref raw) => raw as *const _, + } + } +} + +/// Callback to be invoked while a transfer is in progress. +/// +/// This callback will be periodically called with updates to the progress of +/// the transfer so far. The return value indicates whether the transfer should +/// continue. A return value of `false` will cancel the transfer. +/// +/// * `progress` - the progress being made so far. +#[deprecated( + since = "0.11.0", + note = "renamed to `IndexerProgress` to match upstream" +)] +#[allow(dead_code)] +pub type TransportProgress<'a> = IndexerProgress<'a>; + +/// A stream to write and index a packfile +/// +/// This is equivalent to [`crate::OdbPackwriter`], but allows to store the pack +/// and index at an arbitrary path. It also does not require access to an object +/// database if, and only if, the pack file is self-contained (i.e. not "thin"). +pub struct Indexer<'odb> { + raw: *mut raw::git_indexer, + progress: raw::git_indexer_progress, + progress_payload_ptr: *mut OdbPackwriterCb<'odb>, +} + +impl<'a> Indexer<'a> { + /// Create a new indexer + /// + /// The [`Odb`] is used to resolve base objects when fixing thin packs. It + /// can be `None` if no thin pack is expected, in which case missing bases + /// will result in an error. + /// + /// `path` is the directory where the packfile should be stored. + /// + /// `mode` is the permissions to use for the output files, use `0` for defaults. + /// + /// If `verify` is `false`, the indexer will bypass object connectivity checks. + pub fn new(odb: Option<&Odb<'a>>, path: &Path, mode: u32, verify: bool) -> Result { + crate::init(); + let path = path.into_c_string()?; + + let odb = odb.map(Binding::raw).unwrap_or_else(ptr::null_mut); + + let mut out = ptr::null_mut(); + let progress_cb: raw::git_indexer_progress_cb = Some(write_pack_progress_cb); + let progress_payload = Box::new(OdbPackwriterCb { cb: None }); + let progress_payload_ptr = Box::into_raw(progress_payload); + + unsafe { + let mut opts = mem::zeroed(); + try_call!(raw::git_indexer_options_init( + &mut opts, + raw::GIT_INDEXER_OPTIONS_VERSION + )); + opts.progress_cb = progress_cb; + opts.progress_cb_payload = progress_payload_ptr as *mut c_void; + opts.verify = verify.into(); + + try_call!(raw::git_indexer_new(&mut out, path, mode, odb, &mut opts)); + } + + Ok(Self { + raw: out, + progress: Default::default(), + progress_payload_ptr, + }) + } + + /// Finalize the pack and index + /// + /// Resolves any pending deltas and writes out the index file. The returned + /// string is the hexadecimal checksum of the packfile, which is also used + /// to name the pack and index files (`pack-.pack` and + /// `pack-.idx` respectively). + pub fn commit(mut self) -> Result { + unsafe { + try_call!(raw::git_indexer_commit(self.raw, &mut self.progress)); + + let name = CStr::from_ptr(raw::git_indexer_name(self.raw)); + Ok(name.to_str().expect("pack name not utf8").to_owned()) + } + } + + /// The callback through which progress is monitored. Be aware that this is + /// called inline, so performance may be affected. + pub fn progress(&mut self, cb: F) -> &mut Self + where + F: FnMut(Progress<'_>) -> bool + 'a, + { + let progress_payload = + unsafe { &mut *(self.progress_payload_ptr as *mut OdbPackwriterCb<'_>) }; + progress_payload.cb = Some(Box::new(cb) as Box>); + + self + } +} + +impl io::Write for Indexer<'_> { + fn write(&mut self, buf: &[u8]) -> io::Result { + unsafe { + let ptr = buf.as_ptr() as *mut c_void; + let len = buf.len(); + + let res = raw::git_indexer_append(self.raw, ptr, len, &mut self.progress); + if res < 0 { + Err(io::Error::new(io::ErrorKind::Other, Error::last_error(res))) + } else { + Ok(buf.len()) + } + } + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl Drop for Indexer<'_> { + fn drop(&mut self) { + unsafe { + raw::git_indexer_free(self.raw); + drop(Box::from_raw(self.progress_payload_ptr)) + } + } +} + +#[cfg(test)] +mod tests { + use crate::{Buf, Indexer}; + use std::io::prelude::*; + + #[test] + fn indexer() { + let (_td, repo_source) = crate::test::repo_init(); + let (_td, repo_target) = crate::test::repo_init(); + + let mut progress_called = false; + + // Create an in-memory packfile + let mut builder = t!(repo_source.packbuilder()); + let mut buf = Buf::new(); + let (commit_source_id, _tree) = crate::test::commit(&repo_source); + t!(builder.insert_object(commit_source_id, None)); + t!(builder.write_buf(&mut buf)); + + // Write it to the standard location in the target repo, but via indexer + let odb = repo_source.odb().unwrap(); + let mut indexer = Indexer::new( + Some(&odb), + repo_target.path().join("objects").join("pack").as_path(), + 0o644, + true, + ) + .unwrap(); + indexer.progress(|_| { + progress_called = true; + true + }); + indexer.write(&buf).unwrap(); + indexer.commit().unwrap(); + + // Assert that target repo picks it up as valid + let commit_target = repo_target.find_commit(commit_source_id).unwrap(); + assert_eq!(commit_target.id(), commit_source_id); + assert!(progress_called); + } +} diff --git a/src/lib.rs b/src/lib.rs index e68fcb45d0..675680a8e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ //! to manage git repositories. The library itself is a work in progress and is //! likely lacking some bindings here and there, so be warned. //! -//! [1]: https://libgit2.github.com/ +//! [1]: https://libgit2.org/ //! //! The git2-rs library strives to be as close to libgit2 as possible, but also //! strives to make using libgit2 as safe as possible. All resource management @@ -57,69 +57,113 @@ //! }; //! ``` //! +//! To clone using SSH, refer to [RepoBuilder](./build/struct.RepoBuilder.html). +//! //! ## Working with a `Repository` //! -//! All deriviative objects, references, etc are attached to the lifetime of the +//! All derivative objects, references, etc are attached to the lifetime of the //! source `Repository`, to ensure that they do not outlive the repository //! itself. -#![doc(html_root_url = "http://alexcrichton.com/git2-rs")] +#![doc(html_root_url = "https://docs.rs/git2/0.21")] #![allow(trivial_numeric_casts, trivial_casts)] #![deny(missing_docs)] +#![warn(rust_2018_idioms)] #![cfg_attr(test, deny(warnings))] -extern crate libc; -extern crate url; -extern crate libgit2_sys as raw; -#[macro_use] extern crate bitflags; -#[cfg(test)] extern crate tempdir; +use bitflags::bitflags; +use libgit2_sys as raw; use std::ffi::{CStr, CString}; use std::fmt; use std::str; -use std::sync::{Once, ONCE_INIT}; - -pub use blame::{Blame, BlameHunk, BlameIter, BlameOptions}; -pub use blob::Blob; -pub use branch::{Branch, Branches}; -pub use buf::Buf; -pub use commit::{Commit, Parents}; -pub use config::{Config, ConfigEntry, ConfigEntries}; -pub use cred::{Cred, CredentialHelper}; -pub use describe::{Describe, DescribeFormatOptions, DescribeOptions}; -pub use diff::{Diff, DiffDelta, DiffFile, DiffOptions, Deltas}; -pub use diff::{DiffLine, DiffHunk, DiffStats, DiffFindOptions}; -pub use diff::{DiffBinary, DiffBinaryFile, DiffBinaryKind}; -pub use merge::{AnnotatedCommit, MergeOptions}; -pub use error::Error; -pub use index::{Index, IndexEntry, IndexEntries, IndexMatchedPath}; -pub use message::{message_prettify, DEFAULT_COMMENT_CHAR}; -pub use note::{Note, Notes}; -pub use object::Object; -pub use oid::Oid; -pub use packbuilder::{PackBuilder, PackBuilderStage}; -pub use pathspec::{Pathspec, PathspecMatchList, PathspecFailedEntries}; -pub use pathspec::{PathspecDiffEntries, PathspecEntries}; -pub use reference::{Reference, References, ReferenceNames}; -pub use reflog::{Reflog, ReflogEntry, ReflogIter}; -pub use refspec::Refspec; -pub use remote::{Remote, Refspecs, RemoteHead, FetchOptions, PushOptions}; -pub use remote_callbacks::{RemoteCallbacks, Credentials, TransferProgress}; -pub use remote_callbacks::{TransportMessage, Progress, UpdateTips}; -pub use repo::{Repository, RepositoryInitOptions}; -pub use revspec::Revspec; -pub use revwalk::Revwalk; -pub use signature::Signature; -pub use status::{StatusOptions, Statuses, StatusIter, StatusEntry, StatusShow}; -pub use submodule::Submodule; -pub use tag::Tag; -pub use time::{Time, IndexTime}; -pub use tree::{Tree, TreeEntry, TreeIter}; -pub use treebuilder::TreeBuilder; -pub use util::IntoCString; +use std::sync::Once; + +pub use crate::apply::{ApplyLocation, ApplyOptions}; +pub use crate::attr::AttrValue; +pub use crate::blame::{Blame, BlameHunk, BlameIter, BlameOptions}; +pub use crate::blob::{Blob, BlobWriter}; +pub use crate::branch::{Branch, Branches}; +pub use crate::buf::Buf; +pub use crate::cherrypick::CherrypickOptions; +pub use crate::commit::{Commit, Parents}; +pub use crate::config::{Config, ConfigEntries, ConfigEntry}; +pub use crate::cred::Cred; +#[cfg(feature = "cred")] +pub use crate::cred::CredentialHelper; +pub use crate::describe::{Describe, DescribeFormatOptions, DescribeOptions}; +pub use crate::diff::{Deltas, Diff, DiffDelta, DiffFile, DiffOptions}; +pub use crate::diff::{DiffBinary, DiffBinaryFile, DiffBinaryKind, DiffPatchidOptions}; +pub use crate::diff::{DiffFindOptions, DiffHunk, DiffLine, DiffLineType, DiffStats}; +pub use crate::email::{Email, EmailCreateOptions}; +pub use crate::error::Error; +pub use crate::index::{ + Index, IndexConflict, IndexConflicts, IndexEntries, IndexEntry, IndexMatchedPath, +}; +pub use crate::indexer::{Indexer, IndexerProgress, Progress}; +pub use crate::mailmap::Mailmap; +pub use crate::mempack::Mempack; +pub use crate::merge::{AnnotatedCommit, MergeFileOptions, MergeFileResult, MergeOptions}; +pub use crate::message::{ + message_prettify, message_trailers_bytes, message_trailers_strs, MessageTrailersBytes, + MessageTrailersBytesIterator, MessageTrailersStrs, MessageTrailersStrsIterator, + DEFAULT_COMMENT_CHAR, +}; +pub use crate::note::{Note, Notes}; +pub use crate::object::Object; +pub use crate::odb::{Odb, OdbObject, OdbPackwriter, OdbReader, OdbWriter}; +pub use crate::oid::Oid; +pub use crate::packbuilder::{PackBuilder, PackBuilderStage}; +pub use crate::patch::Patch; +pub use crate::pathspec::{Pathspec, PathspecFailedEntries, PathspecMatchList}; +pub use crate::pathspec::{PathspecDiffEntries, PathspecEntries}; +pub use crate::proxy_options::ProxyOptions; +pub use crate::push_update::PushUpdate; +pub use crate::rebase::{Rebase, RebaseOperation, RebaseOperationType, RebaseOptions}; +pub use crate::reference::{Reference, ReferenceNames, References}; +pub use crate::reflog::{Reflog, ReflogEntry, ReflogIter}; +pub use crate::refspec::Refspec; +pub use crate::remote::{ + FetchOptions, PushOptions, Refspecs, Remote, RemoteConnection, RemoteHead, RemoteRedirect, +}; +pub use crate::remote_callbacks::{CertificateCheckStatus, Credentials, RemoteCallbacks}; +pub use crate::remote_callbacks::{TransportMessage, UpdateTips}; +pub use crate::repo::{Repository, RepositoryInitOptions}; +pub use crate::revert::RevertOptions; +pub use crate::revspec::Revspec; +pub use crate::revwalk::Revwalk; +pub use crate::signature::Signature; +pub use crate::stash::{StashApplyOptions, StashApplyProgressCb, StashCb, StashSaveOptions}; +pub use crate::status::{StatusEntry, StatusIter, StatusOptions, StatusShow, Statuses}; +pub use crate::submodule::{Submodule, SubmoduleUpdateOptions}; +pub use crate::tag::Tag; +pub use crate::time::{IndexTime, Time}; +pub use crate::tracing::{trace_set, TraceLevel}; +pub use crate::transaction::Transaction; +pub use crate::tree::{Tree, TreeEntry, TreeIter, TreeWalkMode, TreeWalkResult}; +pub use crate::treebuilder::TreeBuilder; +pub use crate::util::{Binding, IntoCString}; +pub use crate::version::Version; +pub use crate::worktree::{Worktree, WorktreeAddOptions, WorktreeLockStatus, WorktreePruneOptions}; + +// Create a convinience method on bitflag struct which checks the given flag +macro_rules! is_bit_set { + ($name:ident, $flag:expr) => { + #[allow(missing_docs)] + pub fn $name(&self) -> bool { + self.intersects($flag) + } + }; +} /// An enumeration of possible errors that can happen when working with a git /// repository. +// Note: We omit a few native error codes, as they are unlikely to be propagated +// to the library user. Currently: +// +// * GIT_EPASSTHROUGH +// * GIT_ITEROVER +// * GIT_RETRY #[derive(PartialEq, Eq, Clone, Debug, Copy)] pub enum ErrorCode { /// Generic error @@ -164,8 +208,20 @@ pub enum ErrorCode { Invalid, /// Uncommitted changes in index prevented operation Uncommitted, - /// Operation was not valid for a directory, + /// Operation was not valid for a directory Directory, + /// A merge conflict exists and cannot continue + MergeConflict, + /// Hashsum mismatch in object + HashsumMismatch, + /// Unsaved changes in the index would be overwritten + IndexDirty, + /// Patch application failed + ApplyFail, + /// The object is not owned by the current user + Owner, + /// Timeout + Timeout, } /// An enumeration of possible categories of things that can have @@ -198,7 +254,7 @@ pub enum ErrorClass { Object, /// Network error Net, - /// Error manpulating a tag + /// Error manipulating a tag Tag, /// Invalid value in tree Tree, @@ -206,7 +262,7 @@ pub enum ErrorClass { Indexer, /// Error from SSL Ssl, - /// Error involing submodules + /// Error involving submodules Submodule, /// Threading error Thread, @@ -234,6 +290,14 @@ pub enum ErrorClass { Rebase, /// Filesystem-related error Filesystem, + /// Invalid patch data + Patch, + /// Error involving worktrees + Worktree, + /// Hash library error or SHA-1 collision + Sha1, + /// HTTP error + Http, } /// A listing of the possible states that a repository can be in. @@ -255,7 +319,7 @@ pub enum RepositoryState { } /// An enumeration of the possible directions for a remote. -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Direction { /// Data will be fetched (read) from this remote. Fetch, @@ -265,7 +329,7 @@ pub enum Direction { /// An enumeration of the operations that can be performed for the `reset` /// method on a `Repository`. -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ResetType { /// Move the head to the given commit. Soft, @@ -290,6 +354,16 @@ pub enum ObjectType { Tag, } +/// An enumeration of all possible kinds of references. +#[derive(PartialEq, Eq, Copy, Clone, Debug)] +pub enum ReferenceType { + /// A reference which points at an object id. + Direct, + + /// A reference which points at another reference. + Symbolic, +} + /// An enumeration for the possible types of branches #[derive(PartialEq, Eq, Debug, Copy, Clone)] pub enum BranchType { @@ -306,7 +380,7 @@ pub enum BranchType { #[derive(PartialEq, Eq, Debug, Copy, Clone)] pub enum ConfigLevel { /// System-wide on Windows, for compatibility with portable git - ProgramData, + ProgramData = 1, /// System-wide configuration file, e.g. /etc/gitconfig System, /// XDG-compatible configuration file, e.g. ~/.config/git/config @@ -315,10 +389,12 @@ pub enum ConfigLevel { Global, /// Repository specific config, e.g. $PWD/.git/config Local, + /// Worktree specific configuration file, e.g. $GIT_DIR/config.worktree + Worktree, /// Application specific configuration file App, /// Highest level available - Highest, + Highest = -1, } /// Merge file favor options for `MergeOptions` instruct the file-level @@ -345,196 +421,473 @@ pub enum FileFavor { bitflags! { /// Orderings that may be specified for Revwalk iteration. - flags Sort: u32 { + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct Sort: u32 { /// Sort the repository contents in no particular ordering. /// /// This sorting is arbitrary, implementation-specific, and subject to /// change at any time. This is the default sorting for new walkers. - const SORT_NONE = raw::GIT_SORT_NONE as u32, + const NONE = raw::GIT_SORT_NONE as u32; - /// Sort the repository contents in topological order (parents before - /// children). + /// Sort the repository contents in topological order (children before + /// parents). /// /// This sorting mode can be combined with time sorting. - const SORT_TOPOLOGICAL = raw::GIT_SORT_TOPOLOGICAL as u32, + const TOPOLOGICAL = raw::GIT_SORT_TOPOLOGICAL as u32; /// Sort the repository contents by commit time. /// /// This sorting mode can be combined with topological sorting. - const SORT_TIME = raw::GIT_SORT_TIME as u32, + const TIME = raw::GIT_SORT_TIME as u32; /// Iterate through the repository contents in reverse order. /// /// This sorting mode can be combined with any others. - const SORT_REVERSE = raw::GIT_SORT_REVERSE as u32, + const REVERSE = raw::GIT_SORT_REVERSE as u32; } } +impl Sort { + is_bit_set!(is_none, Sort::NONE); + is_bit_set!(is_topological, Sort::TOPOLOGICAL); + is_bit_set!(is_time, Sort::TIME); + is_bit_set!(is_reverse, Sort::REVERSE); +} + bitflags! { /// Types of credentials that can be requested by a credential callback. - flags CredentialType: u32 { + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct CredentialType: u32 { #[allow(missing_docs)] - const USER_PASS_PLAINTEXT = raw::GIT_CREDTYPE_USERPASS_PLAINTEXT as u32, + const USER_PASS_PLAINTEXT = raw::GIT_CREDTYPE_USERPASS_PLAINTEXT as u32; #[allow(missing_docs)] - const SSH_KEY = raw::GIT_CREDTYPE_SSH_KEY as u32, + const SSH_KEY = raw::GIT_CREDTYPE_SSH_KEY as u32; #[allow(missing_docs)] - const SSH_MEMORY = raw::GIT_CREDTYPE_SSH_MEMORY as u32, + const SSH_MEMORY = raw::GIT_CREDTYPE_SSH_MEMORY as u32; #[allow(missing_docs)] - const SSH_CUSTOM = raw::GIT_CREDTYPE_SSH_CUSTOM as u32, + const SSH_CUSTOM = raw::GIT_CREDTYPE_SSH_CUSTOM as u32; #[allow(missing_docs)] - const DEFAULT = raw::GIT_CREDTYPE_DEFAULT as u32, + const DEFAULT = raw::GIT_CREDTYPE_DEFAULT as u32; #[allow(missing_docs)] - const SSH_INTERACTIVE = raw::GIT_CREDTYPE_SSH_INTERACTIVE as u32, + const SSH_INTERACTIVE = raw::GIT_CREDTYPE_SSH_INTERACTIVE as u32; #[allow(missing_docs)] - const USERNAME = raw::GIT_CREDTYPE_USERNAME as u32, + const USERNAME = raw::GIT_CREDTYPE_USERNAME as u32; + } +} + +impl CredentialType { + is_bit_set!(is_user_pass_plaintext, CredentialType::USER_PASS_PLAINTEXT); + is_bit_set!(is_ssh_key, CredentialType::SSH_KEY); + is_bit_set!(is_ssh_memory, CredentialType::SSH_MEMORY); + is_bit_set!(is_ssh_custom, CredentialType::SSH_CUSTOM); + is_bit_set!(is_default, CredentialType::DEFAULT); + is_bit_set!(is_ssh_interactive, CredentialType::SSH_INTERACTIVE); + is_bit_set!(is_username, CredentialType::USERNAME); +} + +impl Default for CredentialType { + fn default() -> Self { + CredentialType::DEFAULT } } bitflags! { /// Flags for the `flags` field of an IndexEntry. - flags IndexEntryFlag: u16 { + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct IndexEntryFlag: u16 { /// Set when the `extended_flags` field is valid. - const IDXENTRY_EXTENDED = raw::GIT_IDXENTRY_EXTENDED as u16, + const EXTENDED = raw::GIT_INDEX_ENTRY_EXTENDED as u16; /// "Assume valid" flag - const IDXENTRY_VALID = raw::GIT_IDXENTRY_VALID as u16, + const VALID = raw::GIT_INDEX_ENTRY_VALID as u16; } } +impl IndexEntryFlag { + is_bit_set!(is_extended, IndexEntryFlag::EXTENDED); + is_bit_set!(is_valid, IndexEntryFlag::VALID); +} + bitflags! { /// Flags for the `extended_flags` field of an IndexEntry. - flags IndexEntryExtendedFlag: u16 { + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct IndexEntryExtendedFlag: u16 { /// An "intent to add" entry from "git add -N" - const IDXENTRY_INTENT_TO_ADD = raw::GIT_IDXENTRY_INTENT_TO_ADD as u16, + const INTENT_TO_ADD = raw::GIT_INDEX_ENTRY_INTENT_TO_ADD as u16; /// Skip the associated worktree file, for sparse checkouts - const IDXENTRY_SKIP_WORKTREE = raw::GIT_IDXENTRY_SKIP_WORKTREE as u16, - /// Reserved for a future on-disk extended flag - const IDXENTRY_EXTENDED2 = raw::GIT_IDXENTRY_EXTENDED2 as u16, - - #[allow(missing_docs)] - const IDXENTRY_UPDATE = raw::GIT_IDXENTRY_UPDATE as u16, - #[allow(missing_docs)] - const IDXENTRY_REMOVE = raw::GIT_IDXENTRY_REMOVE as u16, - #[allow(missing_docs)] - const IDXENTRY_UPTODATE = raw::GIT_IDXENTRY_UPTODATE as u16, - #[allow(missing_docs)] - const IDXENTRY_ADDED = raw::GIT_IDXENTRY_ADDED as u16, - - #[allow(missing_docs)] - const IDXENTRY_HASHED = raw::GIT_IDXENTRY_HASHED as u16, - #[allow(missing_docs)] - const IDXENTRY_UNHASHED = raw::GIT_IDXENTRY_UNHASHED as u16, - #[allow(missing_docs)] - const IDXENTRY_WT_REMOVE = raw::GIT_IDXENTRY_WT_REMOVE as u16, - #[allow(missing_docs)] - const IDXENTRY_CONFLICTED = raw::GIT_IDXENTRY_CONFLICTED as u16, + const SKIP_WORKTREE = raw::GIT_INDEX_ENTRY_SKIP_WORKTREE as u16; #[allow(missing_docs)] - const IDXENTRY_UNPACKED = raw::GIT_IDXENTRY_UNPACKED as u16, - #[allow(missing_docs)] - const IDXENTRY_NEW_SKIP_WORKTREE = raw::GIT_IDXENTRY_NEW_SKIP_WORKTREE as u16, + const UPTODATE = raw::GIT_INDEX_ENTRY_UPTODATE as u16; } } +impl IndexEntryExtendedFlag { + is_bit_set!(is_intent_to_add, IndexEntryExtendedFlag::INTENT_TO_ADD); + is_bit_set!(is_skip_worktree, IndexEntryExtendedFlag::SKIP_WORKTREE); + is_bit_set!(is_up_to_date, IndexEntryExtendedFlag::UPTODATE); +} + bitflags! { /// Flags for APIs that add files matching pathspec - flags IndexAddOption: u32 { + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct IndexAddOption: u32 { + /// Adds files that are not ignored to the index + const DEFAULT = raw::GIT_INDEX_ADD_DEFAULT as u32; + /// Allows adding otherwise ignored files to the index + const FORCE = raw::GIT_INDEX_ADD_FORCE as u32; #[allow(missing_docs)] - const ADD_DEFAULT = raw::GIT_INDEX_ADD_DEFAULT as u32, + const DISABLE_PATHSPEC_MATCH = + raw::GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH as u32; #[allow(missing_docs)] - const ADD_FORCE = raw::GIT_INDEX_ADD_FORCE as u32, - #[allow(missing_docs)] - const ADD_DISABLE_PATHSPEC_MATCH = - raw::GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH as u32, - #[allow(missing_docs)] - const ADD_CHECK_PATHSPEC = raw::GIT_INDEX_ADD_CHECK_PATHSPEC as u32, + const CHECK_PATHSPEC = raw::GIT_INDEX_ADD_CHECK_PATHSPEC as u32; + } +} + +impl IndexAddOption { + is_bit_set!(is_default, IndexAddOption::DEFAULT); + is_bit_set!(is_force, IndexAddOption::FORCE); + is_bit_set!( + is_disable_pathspec_match, + IndexAddOption::DISABLE_PATHSPEC_MATCH + ); + is_bit_set!(is_check_pathspec, IndexAddOption::CHECK_PATHSPEC); +} + +impl Default for IndexAddOption { + fn default() -> Self { + IndexAddOption::DEFAULT } } bitflags! { /// Flags for `Repository::open_ext` - flags RepositoryOpenFlags: u32 { + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct RepositoryOpenFlags: u32 { /// Only open the specified path; don't walk upward searching. - const REPOSITORY_OPEN_NO_SEARCH = raw::GIT_REPOSITORY_OPEN_NO_SEARCH as u32, + const NO_SEARCH = raw::GIT_REPOSITORY_OPEN_NO_SEARCH as u32; /// Search across filesystem boundaries. - const REPOSITORY_OPEN_CROSS_FS = raw::GIT_REPOSITORY_OPEN_CROSS_FS as u32, + const CROSS_FS = raw::GIT_REPOSITORY_OPEN_CROSS_FS as u32; /// Force opening as bare repository, and defer loading its config. - const REPOSITORY_OPEN_BARE = raw::GIT_REPOSITORY_OPEN_BARE as u32, + const BARE = raw::GIT_REPOSITORY_OPEN_BARE as u32; + /// Don't try appending `/.git` to the specified repository path. + const NO_DOTGIT = raw::GIT_REPOSITORY_OPEN_NO_DOTGIT as u32; + /// Respect environment variables like `$GIT_DIR`. + const FROM_ENV = raw::GIT_REPOSITORY_OPEN_FROM_ENV as u32; } } +impl RepositoryOpenFlags { + is_bit_set!(is_no_search, RepositoryOpenFlags::NO_SEARCH); + is_bit_set!(is_cross_fs, RepositoryOpenFlags::CROSS_FS); + is_bit_set!(is_bare, RepositoryOpenFlags::BARE); + is_bit_set!(is_no_dotgit, RepositoryOpenFlags::NO_DOTGIT); + is_bit_set!(is_from_env, RepositoryOpenFlags::FROM_ENV); +} + bitflags! { /// Flags for the return value of `Repository::revparse` - flags RevparseMode: u32 { + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct RevparseMode: u32 { /// The spec targeted a single object - const REVPARSE_SINGLE = raw::GIT_REVPARSE_SINGLE as u32, + const SINGLE = raw::GIT_REVPARSE_SINGLE as u32; /// The spec targeted a range of commits - const REVPARSE_RANGE = raw::GIT_REVPARSE_RANGE as u32, + const RANGE = raw::GIT_REVPARSE_RANGE as u32; /// The spec used the `...` operator, which invokes special semantics. - const REVPARSE_MERGE_BASE = raw::GIT_REVPARSE_MERGE_BASE as u32, + const MERGE_BASE = raw::GIT_REVPARSE_MERGE_BASE as u32; + } +} + +impl RevparseMode { + is_bit_set!(is_no_single, RevparseMode::SINGLE); + is_bit_set!(is_range, RevparseMode::RANGE); + is_bit_set!(is_merge_base, RevparseMode::MERGE_BASE); +} + +bitflags! { + /// The results of `merge_analysis` indicating the merge opportunities. + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct MergeAnalysis: u32 { + /// No merge is possible. + const ANALYSIS_NONE = raw::GIT_MERGE_ANALYSIS_NONE as u32; + /// A "normal" merge; both HEAD and the given merge input have diverged + /// from their common ancestor. The divergent commits must be merged. + const ANALYSIS_NORMAL = raw::GIT_MERGE_ANALYSIS_NORMAL as u32; + /// All given merge inputs are reachable from HEAD, meaning the + /// repository is up-to-date and no merge needs to be performed. + const ANALYSIS_UP_TO_DATE = raw::GIT_MERGE_ANALYSIS_UP_TO_DATE as u32; + /// The given merge input is a fast-forward from HEAD and no merge + /// needs to be performed. Instead, the client can check out the + /// given merge input. + const ANALYSIS_FASTFORWARD = raw::GIT_MERGE_ANALYSIS_FASTFORWARD as u32; + /// The HEAD of the current repository is "unborn" and does not point to + /// a valid commit. No merge can be performed, but the caller may wish + /// to simply set HEAD to the target commit(s). + const ANALYSIS_UNBORN = raw::GIT_MERGE_ANALYSIS_UNBORN as u32; } } -#[cfg(test)] #[macro_use] mod test; -#[macro_use] mod panic; +impl MergeAnalysis { + is_bit_set!(is_none, MergeAnalysis::ANALYSIS_NONE); + is_bit_set!(is_normal, MergeAnalysis::ANALYSIS_NORMAL); + is_bit_set!(is_up_to_date, MergeAnalysis::ANALYSIS_UP_TO_DATE); + is_bit_set!(is_fast_forward, MergeAnalysis::ANALYSIS_FASTFORWARD); + is_bit_set!(is_unborn, MergeAnalysis::ANALYSIS_UNBORN); +} + +bitflags! { + /// The user's stated preference for merges. + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct MergePreference: u32 { + /// No configuration was found that suggests a preferred behavior for + /// merge. + const NONE = raw::GIT_MERGE_PREFERENCE_NONE as u32; + /// There is a `merge.ff=false` configuration setting, suggesting that + /// the user does not want to allow a fast-forward merge. + const NO_FAST_FORWARD = raw::GIT_MERGE_PREFERENCE_NO_FASTFORWARD as u32; + /// There is a `merge.ff=only` configuration setting, suggesting that + /// the user only wants fast-forward merges. + const FASTFORWARD_ONLY = raw::GIT_MERGE_PREFERENCE_FASTFORWARD_ONLY as u32; + } +} + +impl MergePreference { + is_bit_set!(is_none, MergePreference::NONE); + is_bit_set!(is_no_fast_forward, MergePreference::NO_FAST_FORWARD); + is_bit_set!(is_fastforward_only, MergePreference::FASTFORWARD_ONLY); +} + +bitflags! { + /// Flags controlling the behavior of ODB lookup operations + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct OdbLookupFlags: u32 { + /// Don't call `git_odb_refresh` if the lookup fails. Useful when doing + /// a batch of lookup operations for objects that may legitimately not + /// exist. When using this flag, you may wish to manually call + /// `git_odb_refresh` before processing a batch of objects. + const NO_REFRESH = raw::GIT_ODB_LOOKUP_NO_REFRESH as u32; + } +} + +bitflags! { + /// How to handle reference updates. + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct RemoteUpdateFlags: u32 { + /// Write the fetch results to FETCH_HEAD. + const UPDATE_FETCHHEAD = raw::GIT_REMOTE_UPDATE_FETCHHEAD as u32; + /// Report unchanged tips in the update_tips callback. + const REPORT_UNCHANGED = raw::GIT_REMOTE_UPDATE_REPORT_UNCHANGED as u32; + } +} + +#[cfg(test)] +#[macro_use] +mod test; +#[macro_use] +mod panic; +mod attr; mod call; mod util; pub mod build; pub mod cert; -pub mod string_array; pub mod oid_array; +pub mod opts; +pub mod string_array; pub mod transport; +mod apply; mod blame; mod blob; mod branch; mod buf; +mod cherrypick; mod commit; mod config; mod cred; mod describe; mod diff; -mod merge; +mod email; mod error; mod index; +mod indexer; +mod mailmap; +mod mempack; +mod merge; mod message; mod note; mod object; +mod odb; mod oid; mod packbuilder; +mod patch; mod pathspec; +mod proxy_options; +mod push_update; +mod rebase; mod reference; mod reflog; mod refspec; mod remote; mod remote_callbacks; mod repo; +mod revert; mod revspec; mod revwalk; mod signature; +mod stash; mod status; mod submodule; mod tag; +mod tagforeach; mod time; +mod tracing; +mod transaction; mod tree; mod treebuilder; +mod version; +mod worktree; fn init() { - static INIT: Once = ONCE_INIT; - INIT.call_once(|| unsafe { - raw::openssl_init(); - let r = raw::git_libgit2_init(); - assert!(r >= 0, - "couldn't initialize the libgit2 library: {}", r); - assert_eq!(libc::atexit(shutdown), 0); + static INIT: Once = Once::new(); + + INIT.call_once(|| { + openssl_env_init(); }); - extern fn shutdown() { - unsafe { raw::git_libgit2_shutdown(); } - } + + raw::init(); } -unsafe fn opt_bytes<'a, T>(_anchor: &'a T, - c: *const libc::c_char) -> Option<&'a [u8]> { +#[cfg(all( + unix, + not(target_os = "macos"), + not(target_os = "ios"), + feature = "https" +))] +fn openssl_env_init() { + // Currently, libgit2 leverages OpenSSL for SSL support when cloning + // repositories over HTTPS. This means that we're picking up an OpenSSL + // dependency on non-Windows platforms (where it has its own HTTPS + // subsystem). As a result, we need to link to OpenSSL. + // + // Now actually *linking* to OpenSSL isn't so hard. We just need to make + // sure to use pkg-config to discover any relevant system dependencies for + // differences between distributions like CentOS and Ubuntu. The actual + // trickiness comes about when we start *distributing* the resulting + // binaries. Currently Cargo is distributed in binary form as nightlies, + // which means we're distributing a binary with OpenSSL linked in. + // + // For historical reasons, the Linux nightly builder is running a CentOS + // distribution in order to have as much ABI compatibility with other + // distributions as possible. Sadly, however, this compatibility does not + // extend to OpenSSL. Currently OpenSSL has two major versions, 0.9 and 1.0, + // which are incompatible (many ABI differences). The CentOS builder we + // build on has version 1.0, as do most distributions today. Some still have + // 0.9, however. This means that if we are to distribute the binaries built + // by the CentOS machine, we would only be compatible with OpenSSL 1.0 and + // we would fail to run (a dynamic linker error at runtime) on systems with + // only 9.8 installed (hopefully). + // + // But wait, the plot thickens! Apparently CentOS has dubbed their OpenSSL + // library as `libssl.so.10`, notably the `10` is included at the end. On + // the other hand Ubuntu, for example, only distributes `libssl.so`. This + // means that the binaries created at CentOS are hard-wired to probe for a + // file called `libssl.so.10` at runtime (using the LD_LIBRARY_PATH), which + // will not be found on ubuntu. The conclusion of this is that binaries + // built on CentOS cannot be distributed to Ubuntu and run successfully. + // + // There are a number of sneaky things we could do, including, but not + // limited to: + // + // 1. Create a shim program which runs "just before" cargo runs. The + // responsibility of this shim program would be to locate `libssl.so`, + // whatever it's called, on the current system, make sure there's a + // symlink *somewhere* called `libssl.so.10`, and then set up + // LD_LIBRARY_PATH and run the actual cargo. + // + // This approach definitely seems unconventional, and is borderline + // overkill for this problem. It's also dubious if we can find a + // libssl.so reliably on the target system. + // + // 2. Somehow re-work the CentOS installation so that the linked-against + // library is called libssl.so instead of libssl.so.10 + // + // The problem with this approach is that systems with 0.9 installed will + // start to silently fail, due to also having libraries called libssl.so + // (probably symlinked under a more appropriate version). + // + // 3. Compile Cargo against both OpenSSL 1.0 *and* OpenSSL 0.9, and + // distribute both. Also make sure that the linked-against name of the + // library is `libssl.so`. At runtime we determine which version is + // installed, and we then the appropriate binary. + // + // This approach clearly has drawbacks in terms of infrastructure and + // feasibility. + // + // 4. Build a nightly of Cargo for each distribution we'd like to support. + // You would then pick the appropriate Cargo nightly to install locally. + // + // So, with all this in mind, the decision was made to *statically* link + // OpenSSL. This solves any problem of relying on a downstream OpenSSL + // version being available. This does, however, open a can of worms related + // to security issues. It's generally a good idea to dynamically link + // OpenSSL as you'll get security updates over time without having to do + // anything (the system administrator will update the local openssl + // package). By statically linking, we're forfeiting this feature. + // + // The conclusion was made it is likely appropriate for the Cargo nightlies + // to statically link OpenSSL, but highly encourage distributions and + // packagers of Cargo to dynamically link OpenSSL. Packagers are targeting + // one system and are distributing to only that system, so none of the + // problems mentioned above would arise. + // + // In order to support this, a new package was made: openssl-static-sys. + // This package currently performs a fairly simple task: + // + // 1. Run pkg-config to discover where openssl is installed. + // 2. If openssl is installed in a nonstandard location, *and* static copies + // of the libraries are available, copy them to $OUT_DIR. + // + // This library will bring in libssl.a and libcrypto.a into the local build, + // allowing them to be picked up by this crate. This allows us to configure + // our own buildbots to have pkg-config point to these local pre-built + // copies of a static OpenSSL (with very few dependencies) while allowing + // most other builds of Cargo to naturally dynamically link OpenSSL. + // + // So in summary, if you're with me so far, we've statically linked OpenSSL + // to the Cargo binary (or any binary, for that matter) and we're ready to + // distribute it to *all* linux distributions. Remember that our original + // intent for openssl was for HTTPS support, which implies that we need some + // for of CA certificate store to validate certificates. This is normally + // installed in a standard system location. + // + // Unfortunately, as one might imagine, OpenSSL is configured for where this + // standard location is at *build time*, but it often varies widely + // per-system. Consequently, it was discovered that OpenSSL will respect the + // SSL_CERT_FILE and SSL_CERT_DIR environment variables in order to assist + // in discovering the location of this file (hurray!). + // + // So, finally getting to the point, this function solely exists to support + // our static builds of OpenSSL by probing for the "standard system + // location" of certificates and setting relevant environment variable to + // point to them. + // + // Ah, and as a final note, this is only a problem on Linux, not on OS X. On + // OS X the OpenSSL binaries are stable enough that we can just rely on + // dynamic linkage (plus they have some weird modifications to OpenSSL which + // means we wouldn't want to link statically). + #[allow(deprecated)] // FIXME: use init_openssl_env_vars instead + openssl_probe::init_ssl_cert_env_vars(); +} + +#[cfg(any( + windows, + target_os = "macos", + target_os = "ios", + not(feature = "https") +))] +fn openssl_env_init() {} + +unsafe fn opt_bytes<'a, T>(_anchor: &'a T, c: *const libc::c_char) -> Option<&'a [u8]> { if c.is_null() { None } else { @@ -545,7 +898,7 @@ unsafe fn opt_bytes<'a, T>(_anchor: &'a T, fn opt_cstr(o: Option) -> Result, Error> { match o { Some(s) => s.into_c_string().map(Some), - None => Ok(None) + None => Ok(None), } } @@ -559,25 +912,25 @@ impl ObjectType { } } - /// Determine if the given git_otype is a valid loose object type. + /// Determine if the given git_object_t is a valid loose object type. pub fn is_loose(&self) -> bool { - unsafe { (call!(raw::git_object_typeisloose(*self)) == 1) } + unsafe { call!(raw::git_object_typeisloose(*self)) == 1 } } - /// Convert a raw git_otype to an ObjectType - pub fn from_raw(raw: raw::git_otype) -> Option { + /// Convert a raw git_object_t to an ObjectType + pub fn from_raw(raw: raw::git_object_t) -> Option { match raw { - raw::GIT_OBJ_ANY => Some(ObjectType::Any), - raw::GIT_OBJ_COMMIT => Some(ObjectType::Commit), - raw::GIT_OBJ_TREE => Some(ObjectType::Tree), - raw::GIT_OBJ_BLOB => Some(ObjectType::Blob), - raw::GIT_OBJ_TAG => Some(ObjectType::Tag), + raw::GIT_OBJECT_ANY => Some(ObjectType::Any), + raw::GIT_OBJECT_COMMIT => Some(ObjectType::Commit), + raw::GIT_OBJECT_TREE => Some(ObjectType::Tree), + raw::GIT_OBJECT_BLOB => Some(ObjectType::Blob), + raw::GIT_OBJECT_TAG => Some(ObjectType::Tag), _ => None, } } /// Convert this kind into its raw representation - pub fn raw(&self) -> raw::git_otype { + pub fn raw(&self) -> raw::git_object_t { call::convert(self) } @@ -589,7 +942,32 @@ impl ObjectType { } impl fmt::Display for ObjectType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.str().fmt(f) + } +} + +impl ReferenceType { + /// Convert an object type to its string representation. + pub fn str(&self) -> &'static str { + match self { + ReferenceType::Direct => "direct", + ReferenceType::Symbolic => "symbolic", + } + } + + /// Convert a raw git_reference_t to a ReferenceType. + pub fn from_raw(raw: raw::git_reference_t) -> Option { + match raw { + raw::GIT_REFERENCE_DIRECT => Some(ReferenceType::Direct), + raw::GIT_REFERENCE_SYMBOLIC => Some(ReferenceType::Symbolic), + _ => None, + } + } +} + +impl fmt::Display for ReferenceType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.str().fmt(f) } } @@ -603,6 +981,7 @@ impl ConfigLevel { raw::GIT_CONFIG_LEVEL_XDG => ConfigLevel::XDG, raw::GIT_CONFIG_LEVEL_GLOBAL => ConfigLevel::Global, raw::GIT_CONFIG_LEVEL_LOCAL => ConfigLevel::Local, + raw::GIT_CONFIG_LEVEL_WORKTREE => ConfigLevel::Worktree, raw::GIT_CONFIG_LEVEL_APP => ConfigLevel::App, raw::GIT_CONFIG_HIGHEST_LEVEL => ConfigLevel::Highest, n => panic!("unknown config level: {}", n), @@ -610,6 +989,34 @@ impl ConfigLevel { } } +impl SubmoduleIgnore { + /// Converts a [`raw::git_submodule_ignore_t`] to a [`SubmoduleIgnore`] + pub fn from_raw(raw: raw::git_submodule_ignore_t) -> Self { + match raw { + raw::GIT_SUBMODULE_IGNORE_UNSPECIFIED => SubmoduleIgnore::Unspecified, + raw::GIT_SUBMODULE_IGNORE_NONE => SubmoduleIgnore::None, + raw::GIT_SUBMODULE_IGNORE_UNTRACKED => SubmoduleIgnore::Untracked, + raw::GIT_SUBMODULE_IGNORE_DIRTY => SubmoduleIgnore::Dirty, + raw::GIT_SUBMODULE_IGNORE_ALL => SubmoduleIgnore::All, + n => panic!("unknown submodule ignore rule: {}", n), + } + } +} + +impl SubmoduleUpdate { + /// Converts a [`raw::git_submodule_update_t`] to a [`SubmoduleUpdate`] + pub fn from_raw(raw: raw::git_submodule_update_t) -> Self { + match raw { + raw::GIT_SUBMODULE_UPDATE_CHECKOUT => SubmoduleUpdate::Checkout, + raw::GIT_SUBMODULE_UPDATE_REBASE => SubmoduleUpdate::Rebase, + raw::GIT_SUBMODULE_UPDATE_MERGE => SubmoduleUpdate::Merge, + raw::GIT_SUBMODULE_UPDATE_NONE => SubmoduleUpdate::None, + raw::GIT_SUBMODULE_UPDATE_DEFAULT => SubmoduleUpdate::Default, + n => panic!("unknown submodule update strategy: {}", n), + } + } +} + bitflags! { /// Status flags for a single file /// @@ -619,57 +1026,79 @@ bitflags! { /// represents the status of file in the index relative to the HEAD, and the /// `STATUS_WT_*` set of flags represent the status of the file in the /// working directory relative to the index. - flags Status: u32 { + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct Status: u32 { #[allow(missing_docs)] - const STATUS_CURRENT = raw::GIT_STATUS_CURRENT as u32, + const CURRENT = raw::GIT_STATUS_CURRENT as u32; #[allow(missing_docs)] - const STATUS_INDEX_NEW = raw::GIT_STATUS_INDEX_NEW as u32, + const INDEX_NEW = raw::GIT_STATUS_INDEX_NEW as u32; #[allow(missing_docs)] - const STATUS_INDEX_MODIFIED = raw::GIT_STATUS_INDEX_MODIFIED as u32, + const INDEX_MODIFIED = raw::GIT_STATUS_INDEX_MODIFIED as u32; #[allow(missing_docs)] - const STATUS_INDEX_DELETED = raw::GIT_STATUS_INDEX_DELETED as u32, + const INDEX_DELETED = raw::GIT_STATUS_INDEX_DELETED as u32; #[allow(missing_docs)] - const STATUS_INDEX_RENAMED = raw::GIT_STATUS_INDEX_RENAMED as u32, + const INDEX_RENAMED = raw::GIT_STATUS_INDEX_RENAMED as u32; #[allow(missing_docs)] - const STATUS_INDEX_TYPECHANGE = raw::GIT_STATUS_INDEX_TYPECHANGE as u32, + const INDEX_TYPECHANGE = raw::GIT_STATUS_INDEX_TYPECHANGE as u32; #[allow(missing_docs)] - const STATUS_WT_NEW = raw::GIT_STATUS_WT_NEW as u32, + const WT_NEW = raw::GIT_STATUS_WT_NEW as u32; + #[allow(missing_docs)] + const WT_MODIFIED = raw::GIT_STATUS_WT_MODIFIED as u32; #[allow(missing_docs)] - const STATUS_WT_MODIFIED = raw::GIT_STATUS_WT_MODIFIED as u32, + const WT_DELETED = raw::GIT_STATUS_WT_DELETED as u32; #[allow(missing_docs)] - const STATUS_WT_DELETED = raw::GIT_STATUS_WT_DELETED as u32, + const WT_TYPECHANGE = raw::GIT_STATUS_WT_TYPECHANGE as u32; #[allow(missing_docs)] - const STATUS_WT_TYPECHANGE = raw::GIT_STATUS_WT_TYPECHANGE as u32, + const WT_RENAMED = raw::GIT_STATUS_WT_RENAMED as u32; #[allow(missing_docs)] - const STATUS_WT_RENAMED = raw::GIT_STATUS_WT_RENAMED as u32, + const WT_UNREADABLE = raw::GIT_STATUS_WT_UNREADABLE as u32; #[allow(missing_docs)] - const STATUS_IGNORED = raw::GIT_STATUS_IGNORED as u32, + const IGNORED = raw::GIT_STATUS_IGNORED as u32; #[allow(missing_docs)] - const STATUS_CONFLICTED = raw::GIT_STATUS_CONFLICTED as u32, + const CONFLICTED = raw::GIT_STATUS_CONFLICTED as u32; } } +impl Status { + is_bit_set!(is_index_new, Status::INDEX_NEW); + is_bit_set!(is_index_modified, Status::INDEX_MODIFIED); + is_bit_set!(is_index_deleted, Status::INDEX_DELETED); + is_bit_set!(is_index_renamed, Status::INDEX_RENAMED); + is_bit_set!(is_index_typechange, Status::INDEX_TYPECHANGE); + is_bit_set!(is_wt_new, Status::WT_NEW); + is_bit_set!(is_wt_modified, Status::WT_MODIFIED); + is_bit_set!(is_wt_deleted, Status::WT_DELETED); + is_bit_set!(is_wt_typechange, Status::WT_TYPECHANGE); + is_bit_set!(is_wt_renamed, Status::WT_RENAMED); + is_bit_set!(is_ignored, Status::IGNORED); + is_bit_set!(is_conflicted, Status::CONFLICTED); +} + bitflags! { /// Mode options for RepositoryInitOptions - flags RepositoryInitMode: u32 { + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct RepositoryInitMode: u32 { /// Use permissions configured by umask - the default - const REPOSITORY_INIT_SHARED_UMASK = - raw::GIT_REPOSITORY_INIT_SHARED_UMASK as u32, + const SHARED_UMASK = raw::GIT_REPOSITORY_INIT_SHARED_UMASK as u32; /// Use `--shared=group` behavior, chmod'ing the new repo to be /// group writable and \"g+sx\" for sticky group assignment - const REPOSITORY_INIT_SHARED_GROUP = - raw::GIT_REPOSITORY_INIT_SHARED_GROUP as u32, + const SHARED_GROUP = raw::GIT_REPOSITORY_INIT_SHARED_GROUP as u32; /// Use `--shared=all` behavior, adding world readability. - const REPOSITORY_INIT_SHARED_ALL = - raw::GIT_REPOSITORY_INIT_SHARED_ALL as u32, + const SHARED_ALL = raw::GIT_REPOSITORY_INIT_SHARED_ALL as u32; } } +impl RepositoryInitMode { + is_bit_set!(is_shared_umask, RepositoryInitMode::SHARED_UMASK); + is_bit_set!(is_shared_group, RepositoryInitMode::SHARED_GROUP); + is_bit_set!(is_shared_all, RepositoryInitMode::SHARED_ALL); +} + /// What type of change is described by a `DiffDelta`? -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Delta { /// No changes Unmodified, @@ -679,7 +1108,7 @@ pub enum Delta { Deleted, /// Entry content changed between old and new Modified, - /// Entry was renamed wbetween old and new + /// Entry was renamed between old and new Renamed, /// Entry was copied from another old entry Copied, @@ -695,6 +1124,53 @@ pub enum Delta { Conflicted, } +/// Valid modes for index and tree entries. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum FileMode { + /// Unreadable + Unreadable, + /// Tree + Tree, + /// Blob + Blob, + /// Group writable blob. Obsolete mode kept for compatibility reasons + BlobGroupWritable, + /// Blob executable + BlobExecutable, + /// Link + Link, + /// Commit + Commit, +} + +impl From for i32 { + fn from(mode: FileMode) -> i32 { + match mode { + FileMode::Unreadable => raw::GIT_FILEMODE_UNREADABLE as i32, + FileMode::Tree => raw::GIT_FILEMODE_TREE as i32, + FileMode::Blob => raw::GIT_FILEMODE_BLOB as i32, + FileMode::BlobGroupWritable => raw::GIT_FILEMODE_BLOB_GROUP_WRITABLE as i32, + FileMode::BlobExecutable => raw::GIT_FILEMODE_BLOB_EXECUTABLE as i32, + FileMode::Link => raw::GIT_FILEMODE_LINK as i32, + FileMode::Commit => raw::GIT_FILEMODE_COMMIT as i32, + } + } +} + +impl From for u32 { + fn from(mode: FileMode) -> u32 { + match mode { + FileMode::Unreadable => raw::GIT_FILEMODE_UNREADABLE as u32, + FileMode::Tree => raw::GIT_FILEMODE_TREE as u32, + FileMode::Blob => raw::GIT_FILEMODE_BLOB as u32, + FileMode::BlobGroupWritable => raw::GIT_FILEMODE_BLOB_GROUP_WRITABLE as u32, + FileMode::BlobExecutable => raw::GIT_FILEMODE_BLOB_EXECUTABLE as u32, + FileMode::Link => raw::GIT_FILEMODE_LINK as u32, + FileMode::Commit => raw::GIT_FILEMODE_COMMIT as u32, + } + } +} + bitflags! { /// Return codes for submodule status. /// @@ -735,52 +1211,56 @@ bitflags! { /// /// Lastly, the following will only be returned for ignore "NONE". /// - /// * WD_UNTRACKED - wd contains untracked files - flags SubmoduleStatus: u32 { + /// * WD_UNTRACKED - workdir contains untracked files + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct SubmoduleStatus: u32 { #[allow(missing_docs)] - const SUBMODULE_STATUS_IN_HEAD = - raw::GIT_SUBMODULE_STATUS_IN_HEAD as u32, + const IN_HEAD = raw::GIT_SUBMODULE_STATUS_IN_HEAD as u32; #[allow(missing_docs)] - const SUBMODULE_STATUS_IN_INDEX = - raw::GIT_SUBMODULE_STATUS_IN_INDEX as u32, + const IN_INDEX = raw::GIT_SUBMODULE_STATUS_IN_INDEX as u32; #[allow(missing_docs)] - const SUBMODULE_STATUS_IN_CONFIG = - raw::GIT_SUBMODULE_STATUS_IN_CONFIG as u32, + const IN_CONFIG = raw::GIT_SUBMODULE_STATUS_IN_CONFIG as u32; #[allow(missing_docs)] - const SUBMODULE_STATUS_IN_WD = - raw::GIT_SUBMODULE_STATUS_IN_WD as u32, + const IN_WD = raw::GIT_SUBMODULE_STATUS_IN_WD as u32; #[allow(missing_docs)] - const SUBMODULE_STATUS_INDEX_ADDED = - raw::GIT_SUBMODULE_STATUS_INDEX_ADDED as u32, + const INDEX_ADDED = raw::GIT_SUBMODULE_STATUS_INDEX_ADDED as u32; #[allow(missing_docs)] - const SUBMODULE_STATUS_INDEX_DELETED = - raw::GIT_SUBMODULE_STATUS_INDEX_DELETED as u32, + const INDEX_DELETED = raw::GIT_SUBMODULE_STATUS_INDEX_DELETED as u32; #[allow(missing_docs)] - const SUBMODULE_STATUS_INDEX_MODIFIED = - raw::GIT_SUBMODULE_STATUS_INDEX_MODIFIED as u32, + const INDEX_MODIFIED = raw::GIT_SUBMODULE_STATUS_INDEX_MODIFIED as u32; #[allow(missing_docs)] - const SUBMODULE_STATUS_WD_UNINITIALIZED = - raw::GIT_SUBMODULE_STATUS_WD_UNINITIALIZED as u32, + const WD_UNINITIALIZED = + raw::GIT_SUBMODULE_STATUS_WD_UNINITIALIZED as u32; #[allow(missing_docs)] - const SUBMODULE_STATUS_WD_ADDED = - raw::GIT_SUBMODULE_STATUS_WD_ADDED as u32, + const WD_ADDED = raw::GIT_SUBMODULE_STATUS_WD_ADDED as u32; #[allow(missing_docs)] - const SUBMODULE_STATUS_WD_DELETED = - raw::GIT_SUBMODULE_STATUS_WD_DELETED as u32, + const WD_DELETED = raw::GIT_SUBMODULE_STATUS_WD_DELETED as u32; #[allow(missing_docs)] - const SUBMODULE_STATUS_WD_MODIFIED = - raw::GIT_SUBMODULE_STATUS_WD_MODIFIED as u32, + const WD_MODIFIED = raw::GIT_SUBMODULE_STATUS_WD_MODIFIED as u32; #[allow(missing_docs)] - const SUBMODULE_STATUS_WD_INDEX_MODIFIED = - raw::GIT_SUBMODULE_STATUS_WD_INDEX_MODIFIED as u32, + const WD_INDEX_MODIFIED = + raw::GIT_SUBMODULE_STATUS_WD_INDEX_MODIFIED as u32; #[allow(missing_docs)] - const SUBMODULE_STATUS_WD_WD_MODIFIED = - raw::GIT_SUBMODULE_STATUS_WD_WD_MODIFIED as u32, + const WD_WD_MODIFIED = raw::GIT_SUBMODULE_STATUS_WD_WD_MODIFIED as u32; #[allow(missing_docs)] - const SUBMODULE_STATUS_WD_UNTRACKED = - raw::GIT_SUBMODULE_STATUS_WD_UNTRACKED as u32, + const WD_UNTRACKED = raw::GIT_SUBMODULE_STATUS_WD_UNTRACKED as u32; } +} +impl SubmoduleStatus { + is_bit_set!(is_in_head, SubmoduleStatus::IN_HEAD); + is_bit_set!(is_in_index, SubmoduleStatus::IN_INDEX); + is_bit_set!(is_in_config, SubmoduleStatus::IN_CONFIG); + is_bit_set!(is_in_wd, SubmoduleStatus::IN_WD); + is_bit_set!(is_index_added, SubmoduleStatus::INDEX_ADDED); + is_bit_set!(is_index_deleted, SubmoduleStatus::INDEX_DELETED); + is_bit_set!(is_index_modified, SubmoduleStatus::INDEX_MODIFIED); + is_bit_set!(is_wd_uninitialized, SubmoduleStatus::WD_UNINITIALIZED); + is_bit_set!(is_wd_added, SubmoduleStatus::WD_ADDED); + is_bit_set!(is_wd_deleted, SubmoduleStatus::WD_DELETED); + is_bit_set!(is_wd_modified, SubmoduleStatus::WD_MODIFIED); + is_bit_set!(is_wd_wd_modified, SubmoduleStatus::WD_WD_MODIFIED); + is_bit_set!(is_wd_untracked, SubmoduleStatus::WD_UNTRACKED); } /// Submodule ignore values @@ -788,6 +1268,7 @@ bitflags! { /// These values represent settings for the `submodule.$name.ignore` /// configuration value which says how deeply to look at the working /// directory when getting the submodule status. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum SubmoduleIgnore { /// Use the submodule's configuration Unspecified, @@ -801,53 +1282,104 @@ pub enum SubmoduleIgnore { All, } +/// Submodule update values +/// +/// These values represent settings for the `submodule.$name.update` +/// configuration value which says how to handle `git submodule update` +/// for this submodule. The value is usually set in the ".gitmodules" +/// file and copied to ".git/config" when the submodule is initialized. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum SubmoduleUpdate { + /// The default; when a submodule is updated, checkout the new detached + /// HEAD to the submodule directory. + Checkout, + /// Update by rebasing the current checked out branch onto the commit from + /// the superproject. + Rebase, + /// Update by merging the commit in the superproject into the current + /// checkout out branch of the submodule. + Merge, + /// Do not update this submodule even when the commit in the superproject + /// is updated. + None, + /// Not used except as static initializer when we don't want any particular + /// update rule to be specified. + Default, +} + bitflags! { /// ... - flags PathspecFlags: u32 { + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct PathspecFlags: u32 { /// Use the default pathspec matching configuration. - const PATHSPEC_DEFAULT = raw::GIT_PATHSPEC_DEFAULT as u32, + const DEFAULT = raw::GIT_PATHSPEC_DEFAULT as u32; /// Force matching to ignore case, otherwise matching will use native - /// case sensitivity fo the platform filesystem. - const PATHSPEC_IGNORE_CASE = raw::GIT_PATHSPEC_IGNORE_CASE as u32, + /// case sensitivity of the platform filesystem. + const IGNORE_CASE = raw::GIT_PATHSPEC_IGNORE_CASE as u32; /// Force case sensitive matches, otherwise match will use the native /// case sensitivity of the platform filesystem. - const PATHSPEC_USE_CASE = raw::GIT_PATHSPEC_USE_CASE as u32, + const USE_CASE = raw::GIT_PATHSPEC_USE_CASE as u32; /// Disable glob patterns and just use simple string comparison for /// matching. - const PATHSPEC_NO_GLOB = raw::GIT_PATHSPEC_NO_GLOB as u32, + const NO_GLOB = raw::GIT_PATHSPEC_NO_GLOB as u32; /// Means that match functions return the error code `NotFound` if no /// matches are found. By default no matches is a success. - const PATHSPEC_NO_MATCH_ERROR = raw::GIT_PATHSPEC_NO_MATCH_ERROR as u32, + const NO_MATCH_ERROR = raw::GIT_PATHSPEC_NO_MATCH_ERROR as u32; /// Means that the list returned should track which patterns matched /// which files so that at the end of the match we can identify patterns /// that did not match any files. - const PATHSPEC_FIND_FAILURES = raw::GIT_PATHSPEC_FIND_FAILURES as u32, + const FIND_FAILURES = raw::GIT_PATHSPEC_FIND_FAILURES as u32; /// Means that the list returned does not need to keep the actual /// matching filenames. Use this to just test if there were any matches /// at all or in combination with `PATHSPEC_FAILURES` to validate a /// pathspec. - const PATHSPEC_FAILURES_ONLY = raw::GIT_PATHSPEC_FAILURES_ONLY as u32, + const FAILURES_ONLY = raw::GIT_PATHSPEC_FAILURES_ONLY as u32; + } +} + +impl PathspecFlags { + is_bit_set!(is_default, PathspecFlags::DEFAULT); + is_bit_set!(is_ignore_case, PathspecFlags::IGNORE_CASE); + is_bit_set!(is_use_case, PathspecFlags::USE_CASE); + is_bit_set!(is_no_glob, PathspecFlags::NO_GLOB); + is_bit_set!(is_no_match_error, PathspecFlags::NO_MATCH_ERROR); + is_bit_set!(is_find_failures, PathspecFlags::FIND_FAILURES); + is_bit_set!(is_failures_only, PathspecFlags::FAILURES_ONLY); +} + +impl Default for PathspecFlags { + fn default() -> Self { + PathspecFlags::DEFAULT } } bitflags! { /// Types of notifications emitted from checkouts. - flags CheckoutNotificationType: u32 { + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct CheckoutNotificationType: u32 { /// Notification about a conflict. - const CHECKOUT_NOTIFICATION_CONFLICT = raw::GIT_CHECKOUT_NOTIFY_CONFLICT as u32, + const CONFLICT = raw::GIT_CHECKOUT_NOTIFY_CONFLICT as u32; /// Notification about a dirty file. - const CHECKOUT_NOTIFICATION_DIRTY = raw::GIT_CHECKOUT_NOTIFY_DIRTY as u32, + const DIRTY = raw::GIT_CHECKOUT_NOTIFY_DIRTY as u32; /// Notification about an updated file. - const CHECKOUT_NOTIFICATION_UPDATED = raw::GIT_CHECKOUT_NOTIFY_UPDATED as u32, + const UPDATED = raw::GIT_CHECKOUT_NOTIFY_UPDATED as u32; /// Notification about an untracked file. - const CHECKOUT_NOTIFICATION_UNTRACKED = raw::GIT_CHECKOUT_NOTIFY_UNTRACKED as u32, + const UNTRACKED = raw::GIT_CHECKOUT_NOTIFY_UNTRACKED as u32; /// Notification about an ignored file. - const CHECKOUT_NOTIFICATION_IGNORED = raw::GIT_CHECKOUT_NOTIFY_IGNORED as u32, + const IGNORED = raw::GIT_CHECKOUT_NOTIFY_IGNORED as u32; } } +impl CheckoutNotificationType { + is_bit_set!(is_conflict, CheckoutNotificationType::CONFLICT); + is_bit_set!(is_dirty, CheckoutNotificationType::DIRTY); + is_bit_set!(is_updated, CheckoutNotificationType::UPDATED); + is_bit_set!(is_untracked, CheckoutNotificationType::UNTRACKED); + is_bit_set!(is_ignored, CheckoutNotificationType::IGNORED); +} + /// Possible output formats for diff data -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum DiffFormat { /// full git diff Patch, @@ -859,27 +1391,38 @@ pub enum DiffFormat { NameOnly, /// like git diff --name-status NameStatus, + /// git diff as used by git patch-id + PatchId, } bitflags! { /// Formatting options for diff stats - flags DiffStatsFormat: raw::git_diff_stats_format_t { + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct DiffStatsFormat: raw::git_diff_stats_format_t { /// Don't generate any stats - const DIFF_STATS_NONE = raw::GIT_DIFF_STATS_NONE, + const NONE = raw::GIT_DIFF_STATS_NONE; /// Equivalent of `--stat` in git - const DIFF_STATS_FULL = raw::GIT_DIFF_STATS_FULL, + const FULL = raw::GIT_DIFF_STATS_FULL; /// Equivalent of `--shortstat` in git - const DIFF_STATS_SHORT = raw::GIT_DIFF_STATS_SHORT, + const SHORT = raw::GIT_DIFF_STATS_SHORT; /// Equivalent of `--numstat` in git - const DIFF_STATS_NUMBER = raw::GIT_DIFF_STATS_NUMBER, + const NUMBER = raw::GIT_DIFF_STATS_NUMBER; /// Extended header information such as creations, renames and mode /// changes, equivalent of `--summary` in git - const DIFF_STATS_INCLUDE_SUMMARY = - raw::GIT_DIFF_STATS_INCLUDE_SUMMARY, + const INCLUDE_SUMMARY = raw::GIT_DIFF_STATS_INCLUDE_SUMMARY; } } +impl DiffStatsFormat { + is_bit_set!(is_none, DiffStatsFormat::NONE); + is_bit_set!(is_full, DiffStatsFormat::FULL); + is_bit_set!(is_short, DiffStatsFormat::SHORT); + is_bit_set!(is_number, DiffStatsFormat::NUMBER); + is_bit_set!(is_include_summary, DiffStatsFormat::INCLUDE_SUMMARY); +} + /// Automatic tag following options. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum AutotagOption { /// Use the setting from the remote's configuration Unspecified, @@ -892,6 +1435,7 @@ pub enum AutotagOption { } /// Configuration for how pruning is done on a fetch +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum FetchPrune { /// Use the setting from the configuration Unspecified, @@ -901,9 +1445,164 @@ pub enum FetchPrune { Off, } +#[allow(missing_docs)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum StashApplyProgress { + /// None + None, + /// Loading the stashed data from the object database + LoadingStash, + /// The stored index is being analyzed + AnalyzeIndex, + /// The modified files are being analyzed + AnalyzeModified, + /// The untracked and ignored files are being analyzed + AnalyzeUntracked, + /// The untracked files are being written to disk + CheckoutUntracked, + /// The modified files are being written to disk + CheckoutModified, + /// The stash was applied successfully + Done, +} + +bitflags! { + #[allow(missing_docs)] + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct StashApplyFlags: u32 { + #[allow(missing_docs)] + const DEFAULT = raw::GIT_STASH_APPLY_DEFAULT as u32; + /// Try to reinstate not only the working tree's changes, + /// but also the index's changes. + const REINSTATE_INDEX = raw::GIT_STASH_APPLY_REINSTATE_INDEX as u32; + } +} + +impl StashApplyFlags { + is_bit_set!(is_default, StashApplyFlags::DEFAULT); + is_bit_set!(is_reinstate_index, StashApplyFlags::REINSTATE_INDEX); +} + +impl Default for StashApplyFlags { + fn default() -> Self { + StashApplyFlags::DEFAULT + } +} + +bitflags! { + #[allow(missing_docs)] + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct StashFlags: u32 { + #[allow(missing_docs)] + const DEFAULT = raw::GIT_STASH_DEFAULT as u32; + /// All changes already added to the index are left intact in + /// the working directory + const KEEP_INDEX = raw::GIT_STASH_KEEP_INDEX as u32; + /// All untracked files are also stashed and then cleaned up + /// from the working directory + const INCLUDE_UNTRACKED = raw::GIT_STASH_INCLUDE_UNTRACKED as u32; + /// All ignored files are also stashed and then cleaned up from + /// the working directory + const INCLUDE_IGNORED = raw::GIT_STASH_INCLUDE_IGNORED as u32; + /// All changes in the index and working directory are left intact + const KEEP_ALL = raw::GIT_STASH_KEEP_ALL as u32; + } +} + +impl StashFlags { + is_bit_set!(is_default, StashFlags::DEFAULT); + is_bit_set!(is_keep_index, StashFlags::KEEP_INDEX); + is_bit_set!(is_include_untracked, StashFlags::INCLUDE_UNTRACKED); + is_bit_set!(is_include_ignored, StashFlags::INCLUDE_IGNORED); +} + +impl Default for StashFlags { + fn default() -> Self { + StashFlags::DEFAULT + } +} + +bitflags! { + #[allow(missing_docs)] + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct AttrCheckFlags: u32 { + /// Check the working directory, then the index. + const FILE_THEN_INDEX = raw::GIT_ATTR_CHECK_FILE_THEN_INDEX as u32; + /// Check the index, then the working directory. + const INDEX_THEN_FILE = raw::GIT_ATTR_CHECK_INDEX_THEN_FILE as u32; + /// Check the index only. + const INDEX_ONLY = raw::GIT_ATTR_CHECK_INDEX_ONLY as u32; + /// Do not use the system gitattributes file. + const NO_SYSTEM = raw::GIT_ATTR_CHECK_NO_SYSTEM as u32; + } +} + +impl Default for AttrCheckFlags { + fn default() -> Self { + AttrCheckFlags::FILE_THEN_INDEX + } +} + +bitflags! { + #[allow(missing_docs)] + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct DiffFlags: u32 { + /// File(s) treated as binary data. + const BINARY = raw::GIT_DIFF_FLAG_BINARY as u32; + /// File(s) treated as text data. + const NOT_BINARY = raw::GIT_DIFF_FLAG_NOT_BINARY as u32; + /// `id` value is known correct. + const VALID_ID = raw::GIT_DIFF_FLAG_VALID_ID as u32; + /// File exists at this side of the delta. + const EXISTS = raw::GIT_DIFF_FLAG_EXISTS as u32; + } +} + +impl DiffFlags { + is_bit_set!(is_binary, DiffFlags::BINARY); + is_bit_set!(is_not_binary, DiffFlags::NOT_BINARY); + is_bit_set!(has_valid_id, DiffFlags::VALID_ID); + is_bit_set!(exists, DiffFlags::EXISTS); +} + +bitflags! { + /// Options for [`Reference::normalize_name`]. + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] + pub struct ReferenceFormat: u32 { + /// No particular normalization. + const NORMAL = raw::GIT_REFERENCE_FORMAT_NORMAL as u32; + /// Control whether one-level refname are accepted (i.e., refnames that + /// do not contain multiple `/`-separated components). Those are + /// expected to be written only using uppercase letters and underscore + /// (e.g. `HEAD`, `FETCH_HEAD`). + const ALLOW_ONELEVEL = raw::GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL as u32; + /// Interpret the provided name as a reference pattern for a refspec (as + /// used with remote repositories). If this option is enabled, the name + /// is allowed to contain a single `*` in place of a full pathname + /// components (e.g., `foo/*/bar` but not `foo/bar*`). + const REFSPEC_PATTERN = raw::GIT_REFERENCE_FORMAT_REFSPEC_PATTERN as u32; + /// Interpret the name as part of a refspec in shorthand form so the + /// `ALLOW_ONELEVEL` naming rules aren't enforced and `main` becomes a + /// valid name. + const REFSPEC_SHORTHAND = raw::GIT_REFERENCE_FORMAT_REFSPEC_SHORTHAND as u32; + } +} + +impl ReferenceFormat { + is_bit_set!(is_allow_onelevel, ReferenceFormat::ALLOW_ONELEVEL); + is_bit_set!(is_refspec_pattern, ReferenceFormat::REFSPEC_PATTERN); + is_bit_set!(is_refspec_shorthand, ReferenceFormat::REFSPEC_SHORTHAND); +} + +impl Default for ReferenceFormat { + fn default() -> Self { + ReferenceFormat::NORMAL + } +} + #[cfg(test)] mod tests { - use super::ObjectType; + use super::{FileMode, ObjectType}; #[test] fn convert() { @@ -912,4 +1611,79 @@ mod tests { assert!(ObjectType::Blob.is_loose()); } + #[test] + fn convert_filemode() { + assert_eq!(i32::from(FileMode::Blob), 0o100644); + assert_eq!(i32::from(FileMode::BlobGroupWritable), 0o100664); + assert_eq!(i32::from(FileMode::BlobExecutable), 0o100755); + assert_eq!(u32::from(FileMode::Blob), 0o100644); + assert_eq!(u32::from(FileMode::BlobGroupWritable), 0o100664); + assert_eq!(u32::from(FileMode::BlobExecutable), 0o100755); + } + + #[test] + fn bitflags_partial_eq() { + use super::{ + AttrCheckFlags, CheckoutNotificationType, CredentialType, DiffFlags, DiffStatsFormat, + IndexAddOption, IndexEntryExtendedFlag, IndexEntryFlag, MergeAnalysis, MergePreference, + OdbLookupFlags, PathspecFlags, ReferenceFormat, RepositoryInitMode, + RepositoryOpenFlags, RevparseMode, Sort, StashApplyFlags, StashFlags, Status, + SubmoduleStatus, + }; + + assert_eq!( + AttrCheckFlags::FILE_THEN_INDEX, + AttrCheckFlags::FILE_THEN_INDEX + ); + assert_eq!( + CheckoutNotificationType::CONFLICT, + CheckoutNotificationType::CONFLICT + ); + assert_eq!( + CredentialType::USER_PASS_PLAINTEXT, + CredentialType::USER_PASS_PLAINTEXT + ); + assert_eq!(DiffFlags::BINARY, DiffFlags::BINARY); + assert_eq!( + DiffStatsFormat::INCLUDE_SUMMARY, + DiffStatsFormat::INCLUDE_SUMMARY + ); + assert_eq!( + IndexAddOption::CHECK_PATHSPEC, + IndexAddOption::CHECK_PATHSPEC + ); + assert_eq!( + IndexEntryExtendedFlag::INTENT_TO_ADD, + IndexEntryExtendedFlag::INTENT_TO_ADD + ); + assert_eq!(IndexEntryFlag::EXTENDED, IndexEntryFlag::EXTENDED); + assert_eq!( + MergeAnalysis::ANALYSIS_FASTFORWARD, + MergeAnalysis::ANALYSIS_FASTFORWARD + ); + assert_eq!( + MergePreference::FASTFORWARD_ONLY, + MergePreference::FASTFORWARD_ONLY + ); + assert_eq!(OdbLookupFlags::NO_REFRESH, OdbLookupFlags::NO_REFRESH); + assert_eq!(PathspecFlags::FAILURES_ONLY, PathspecFlags::FAILURES_ONLY); + assert_eq!( + ReferenceFormat::ALLOW_ONELEVEL, + ReferenceFormat::ALLOW_ONELEVEL + ); + assert_eq!( + RepositoryInitMode::SHARED_ALL, + RepositoryInitMode::SHARED_ALL + ); + assert_eq!(RepositoryOpenFlags::CROSS_FS, RepositoryOpenFlags::CROSS_FS); + assert_eq!(RevparseMode::RANGE, RevparseMode::RANGE); + assert_eq!(Sort::REVERSE, Sort::REVERSE); + assert_eq!( + StashApplyFlags::REINSTATE_INDEX, + StashApplyFlags::REINSTATE_INDEX + ); + assert_eq!(StashFlags::INCLUDE_IGNORED, StashFlags::INCLUDE_IGNORED); + assert_eq!(Status::WT_MODIFIED, Status::WT_MODIFIED); + assert_eq!(SubmoduleStatus::WD_ADDED, SubmoduleStatus::WD_ADDED); + } } diff --git a/src/mailmap.rs b/src/mailmap.rs new file mode 100644 index 0000000000..096b3227c7 --- /dev/null +++ b/src/mailmap.rs @@ -0,0 +1,134 @@ +use std::ffi::CString; +use std::ptr; + +use crate::util::Binding; +use crate::{raw, Error, Signature}; + +/// A structure to represent a repository's .mailmap file. +/// +/// The representation cannot be written to disk. +pub struct Mailmap { + raw: *mut raw::git_mailmap, +} + +impl Binding for Mailmap { + type Raw = *mut raw::git_mailmap; + + unsafe fn from_raw(ptr: *mut raw::git_mailmap) -> Mailmap { + Mailmap { raw: ptr } + } + + fn raw(&self) -> *mut raw::git_mailmap { + self.raw + } +} + +impl Drop for Mailmap { + fn drop(&mut self) { + unsafe { + raw::git_mailmap_free(self.raw); + } + } +} + +impl Mailmap { + /// Creates an empty, in-memory mailmap object. + pub fn new() -> Result { + crate::init(); + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_mailmap_new(&mut ret)); + Ok(Binding::from_raw(ret)) + } + } + + /// Creates an in-memory mailmap object representing the given buffer. + pub fn from_buffer(buf: &str) -> Result { + crate::init(); + let mut ret = ptr::null_mut(); + let len = buf.len(); + let buf = CString::new(buf)?; + unsafe { + try_call!(raw::git_mailmap_from_buffer(&mut ret, buf, len)); + Ok(Binding::from_raw(ret)) + } + } + + /// Adds a new entry to this in-memory mailmap object. + pub fn add_entry( + &mut self, + real_name: Option<&str>, + real_email: Option<&str>, + replace_name: Option<&str>, + replace_email: &str, + ) -> Result<(), Error> { + let real_name = crate::opt_cstr(real_name)?; + let real_email = crate::opt_cstr(real_email)?; + let replace_name = crate::opt_cstr(replace_name)?; + let replace_email = CString::new(replace_email)?; + unsafe { + try_call!(raw::git_mailmap_add_entry( + self.raw, + real_name, + real_email, + replace_name, + replace_email + )); + Ok(()) + } + } + + /// Resolves a signature to its real name and email address. + pub fn resolve_signature(&self, sig: &Signature<'_>) -> Result, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_mailmap_resolve_signature( + &mut ret, + &*self.raw, + sig.raw() + )); + Ok(Binding::from_raw(ret)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn smoke() { + let sig_name = "name"; + let sig_email = "email"; + let sig = t!(Signature::now(sig_name, sig_email)); + + let mut mm = t!(Mailmap::new()); + + let mailmapped_sig = t!(mm.resolve_signature(&sig)); + assert_eq!(mailmapped_sig.name(), Some(sig_name)); + assert_eq!(mailmapped_sig.email(), Some(sig_email)); + + t!(mm.add_entry(None, None, None, sig_email)); + t!(mm.add_entry( + Some("real name"), + Some("real@email"), + Some(sig_name), + sig_email, + )); + + let mailmapped_sig = t!(mm.resolve_signature(&sig)); + assert_eq!(mailmapped_sig.name(), Some("real name")); + assert_eq!(mailmapped_sig.email(), Some("real@email")); + } + + #[test] + fn from_buffer() { + let buf = " "; + let mm = t!(Mailmap::from_buffer(&buf)); + + let sig = t!(Signature::now("name", "email")); + let mailmapped_sig = t!(mm.resolve_signature(&sig)); + assert_eq!(mailmapped_sig.name(), Some("name")); + assert_eq!(mailmapped_sig.email(), Some("prøper@emæil")); + } +} diff --git a/src/mempack.rs b/src/mempack.rs new file mode 100644 index 0000000000..a780707913 --- /dev/null +++ b/src/mempack.rs @@ -0,0 +1,49 @@ +use std::marker; + +use crate::util::Binding; +use crate::{raw, Buf, Error, Odb, Repository}; + +/// A structure to represent a mempack backend for the object database. The +/// Mempack is bound to the Odb that it was created from, and cannot outlive +/// that Odb. +pub struct Mempack<'odb> { + raw: *mut raw::git_odb_backend, + _marker: marker::PhantomData<&'odb Odb<'odb>>, +} + +impl<'odb> Binding for Mempack<'odb> { + type Raw = *mut raw::git_odb_backend; + + unsafe fn from_raw(raw: *mut raw::git_odb_backend) -> Mempack<'odb> { + Mempack { + raw, + _marker: marker::PhantomData, + } + } + + fn raw(&self) -> *mut raw::git_odb_backend { + self.raw + } +} + +// We don't need to implement `Drop` for Mempack because it is owned by the +// odb to which it is attached, and that will take care of freeing the mempack +// and associated memory. + +impl<'odb> Mempack<'odb> { + /// Dumps the contents of the mempack into the provided buffer. + pub fn dump(&self, repo: &Repository, buf: &mut Buf) -> Result<(), Error> { + unsafe { + try_call!(raw::git_mempack_dump(buf.raw(), repo.raw(), self.raw)); + } + Ok(()) + } + + /// Clears all data in the mempack. + pub fn reset(&self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_mempack_reset(self.raw)); + } + Ok(()) + } +} diff --git a/src/merge.rs b/src/merge.rs index 045e9faf86..bdb32970a9 100644 --- a/src/merge.rs +++ b/src/merge.rs @@ -1,10 +1,14 @@ +use libc::{c_uint, c_ushort}; +use std::ffi::CString; use std::marker; use std::mem; -use libc::c_uint; +use std::ptr; +use std::str; -use {raw, Oid, Commit, FileFavor}; -use util::Binding; -use call::Convert; +use crate::call::Convert; +use crate::util::Binding; +use crate::IntoCString; +use crate::{raw, Commit, FileFavor, Oid}; /// A structure to represent an annotated commit, the input to merge and rebase. /// @@ -21,11 +25,42 @@ pub struct MergeOptions { raw: raw::git_merge_options, } +/// Options for merging a file. +pub struct MergeFileOptions { + ancestor_label: Option, + our_label: Option, + their_label: Option, + raw: raw::git_merge_file_options, +} + +/// Information about file-level merging. +pub struct MergeFileResult { + raw: raw::git_merge_file_result, +} + impl<'repo> AnnotatedCommit<'repo> { /// Gets the commit ID that the given git_annotated_commit refers to pub fn id(&self) -> Oid { unsafe { Binding::from_raw(raw::git_annotated_commit_id(self.raw)) } } + + /// Get the refname that the given git_annotated_commit refers to + /// + /// Returns None if it is not valid utf8 + pub fn refname(&self) -> Option<&str> { + str::from_utf8(self.refname_bytes()).ok() + } + + /// Get the refname that the given git_annotated_commit refers to. + pub fn refname_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, raw::git_annotated_commit_ref(&*self.raw)).unwrap() } + } +} + +impl Default for MergeOptions { + fn default() -> Self { + Self::new() + } } impl MergeOptions { @@ -34,22 +69,42 @@ impl MergeOptions { let mut opts = MergeOptions { raw: unsafe { mem::zeroed() }, }; - assert_eq!(unsafe { - raw::git_merge_init_options(&mut opts.raw, 1) - }, 0); + assert_eq!(unsafe { raw::git_merge_init_options(&mut opts.raw, 1) }, 0); opts } - /// Detect file renames - pub fn find_renames(&mut self, find: bool) -> &mut MergeOptions { - if find { - self.raw.flags |= raw::GIT_MERGE_FIND_RENAMES; + fn flag(&mut self, opt: u32, val: bool) -> &mut MergeOptions { + if val { + self.raw.flags |= opt; } else { - self.raw.flags &= !raw::GIT_MERGE_FIND_RENAMES; + self.raw.flags &= !opt; } self } + /// Detect file renames + pub fn find_renames(&mut self, find: bool) -> &mut MergeOptions { + self.flag(raw::GIT_MERGE_FIND_RENAMES as u32, find) + } + + /// If a conflict occurs, exit immediately instead of attempting to continue + /// resolving conflicts + pub fn fail_on_conflict(&mut self, fail: bool) -> &mut MergeOptions { + self.flag(raw::GIT_MERGE_FAIL_ON_CONFLICT as u32, fail) + } + + /// Do not write the REUC extension on the generated index + pub fn skip_reuc(&mut self, skip: bool) -> &mut MergeOptions { + self.flag(raw::GIT_MERGE_FAIL_ON_CONFLICT as u32, skip) + } + + /// If the commits being merged have multiple merge bases, do not build a + /// recursive merge base (by merging the multiple merge bases), instead + /// simply use the first base. + pub fn no_recursive(&mut self, disable: bool) -> &mut MergeOptions { + self.flag(raw::GIT_MERGE_NO_RECURSIVE as u32, disable) + } + /// Similarity to consider a file renamed (default 50) pub fn rename_threshold(&mut self, thresh: u32) -> &mut MergeOptions { self.raw.rename_threshold = thresh; @@ -80,7 +135,7 @@ impl MergeOptions { self } - fn flag(&mut self, opt: raw::git_merge_file_flag_t, val: bool) -> &mut MergeOptions { + fn file_flag(&mut self, opt: u32, val: bool) -> &mut MergeOptions { if val { self.raw.file_flags |= opt; } else { @@ -91,42 +146,42 @@ impl MergeOptions { /// Create standard conflicted merge files pub fn standard_style(&mut self, standard: bool) -> &mut MergeOptions { - self.flag(raw::GIT_MERGE_FILE_STYLE_MERGE, standard) + self.file_flag(raw::GIT_MERGE_FILE_STYLE_MERGE as u32, standard) } /// Create diff3-style file pub fn diff3_style(&mut self, diff3: bool) -> &mut MergeOptions { - self.flag(raw::GIT_MERGE_FILE_STYLE_DIFF3, diff3) + self.file_flag(raw::GIT_MERGE_FILE_STYLE_DIFF3 as u32, diff3) } /// Condense non-alphanumeric regions for simplified diff file pub fn simplify_alnum(&mut self, simplify: bool) -> &mut MergeOptions { - self.flag(raw::GIT_MERGE_FILE_SIMPLIFY_ALNUM, simplify) + self.file_flag(raw::GIT_MERGE_FILE_SIMPLIFY_ALNUM as u32, simplify) } /// Ignore all whitespace pub fn ignore_whitespace(&mut self, ignore: bool) -> &mut MergeOptions { - self.flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE, ignore) + self.file_flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE as u32, ignore) } /// Ignore changes in amount of whitespace pub fn ignore_whitespace_change(&mut self, ignore: bool) -> &mut MergeOptions { - self.flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE_CHANGE, ignore) + self.file_flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE_CHANGE as u32, ignore) } /// Ignore whitespace at end of line pub fn ignore_whitespace_eol(&mut self, ignore: bool) -> &mut MergeOptions { - self.flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE_EOL, ignore) + self.file_flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE_EOL as u32, ignore) } /// Use the "patience diff" algorithm pub fn patience(&mut self, patience: bool) -> &mut MergeOptions { - self.flag(raw::GIT_MERGE_FILE_DIFF_PATIENCE, patience) + self.file_flag(raw::GIT_MERGE_FILE_DIFF_PATIENCE as u32, patience) } /// Take extra time to find minimal diff pub fn minimal(&mut self, minimal: bool) -> &mut MergeOptions { - self.flag(raw::GIT_MERGE_FILE_DIFF_MINIMAL, minimal) + self.file_flag(raw::GIT_MERGE_FILE_DIFF_MINIMAL as u32, minimal) } /// Acquire a pointer to the underlying raw options. @@ -137,14 +192,15 @@ impl MergeOptions { impl<'repo> Binding for AnnotatedCommit<'repo> { type Raw = *mut raw::git_annotated_commit; - unsafe fn from_raw(raw: *mut raw::git_annotated_commit) - -> AnnotatedCommit<'repo> { + unsafe fn from_raw(raw: *mut raw::git_annotated_commit) -> AnnotatedCommit<'repo> { AnnotatedCommit { - raw: raw, + raw, _marker: marker::PhantomData, } } - fn raw(&self) -> *mut raw::git_annotated_commit { self.raw } + fn raw(&self) -> *mut raw::git_annotated_commit { + self.raw + } } impl<'repo> Drop for AnnotatedCommit<'repo> { @@ -152,3 +208,207 @@ impl<'repo> Drop for AnnotatedCommit<'repo> { unsafe { raw::git_annotated_commit_free(self.raw) } } } + +impl Default for MergeFileOptions { + fn default() -> Self { + Self::new() + } +} + +impl MergeFileOptions { + /// Creates a default set of merge file options. + pub fn new() -> MergeFileOptions { + let mut opts = MergeFileOptions { + ancestor_label: None, + our_label: None, + their_label: None, + raw: unsafe { mem::zeroed() }, + }; + assert_eq!( + unsafe { raw::git_merge_file_options_init(&mut opts.raw, 1) }, + 0 + ); + opts + } + + /// Label for the ancestor file side of the conflict which will be prepended + /// to labels in diff3-format merge files. + pub fn ancestor_label(&mut self, t: T) -> &mut MergeFileOptions { + self.ancestor_label = Some(t.into_c_string().unwrap()); + + self.raw.ancestor_label = self + .ancestor_label + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(ptr::null()); + + self + } + + /// Label for our file side of the conflict which will be prepended to labels + /// in merge files. + pub fn our_label(&mut self, t: T) -> &mut MergeFileOptions { + self.our_label = Some(t.into_c_string().unwrap()); + + self.raw.our_label = self + .our_label + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(ptr::null()); + + self + } + + /// Label for their file side of the conflict which will be prepended to labels + /// in merge files. + pub fn their_label(&mut self, t: T) -> &mut MergeFileOptions { + self.their_label = Some(t.into_c_string().unwrap()); + + self.raw.their_label = self + .their_label + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(ptr::null()); + + self + } + + /// Specify a side to favor for resolving conflicts + pub fn favor(&mut self, favor: FileFavor) -> &mut MergeFileOptions { + self.raw.favor = favor.convert(); + self + } + + fn flag(&mut self, opt: raw::git_merge_file_flag_t, val: bool) -> &mut MergeFileOptions { + if val { + self.raw.flags |= opt as u32; + } else { + self.raw.flags &= !opt as u32; + } + self + } + + /// Create standard conflicted merge files + pub fn style_standard(&mut self, standard: bool) -> &mut MergeFileOptions { + self.flag(raw::GIT_MERGE_FILE_STYLE_MERGE, standard) + } + + /// Create diff3-style file + pub fn style_diff3(&mut self, diff3: bool) -> &mut MergeFileOptions { + self.flag(raw::GIT_MERGE_FILE_STYLE_DIFF3, diff3) + } + + /// Condense non-alphanumeric regions for simplified diff file + pub fn simplify_alnum(&mut self, simplify: bool) -> &mut MergeFileOptions { + self.flag(raw::GIT_MERGE_FILE_SIMPLIFY_ALNUM, simplify) + } + + /// Ignore all whitespace + pub fn ignore_whitespace(&mut self, ignore: bool) -> &mut MergeFileOptions { + self.flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE, ignore) + } + + /// Ignore changes in amount of whitespace + pub fn ignore_whitespace_change(&mut self, ignore: bool) -> &mut MergeFileOptions { + self.flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE_CHANGE, ignore) + } + + /// Ignore whitespace at end of line + pub fn ignore_whitespace_eol(&mut self, ignore: bool) -> &mut MergeFileOptions { + self.flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE_EOL, ignore) + } + + /// Use the "patience diff" algorithm + pub fn patience(&mut self, patience: bool) -> &mut MergeFileOptions { + self.flag(raw::GIT_MERGE_FILE_DIFF_PATIENCE, patience) + } + + /// Take extra time to find minimal diff + pub fn minimal(&mut self, minimal: bool) -> &mut MergeFileOptions { + self.flag(raw::GIT_MERGE_FILE_DIFF_MINIMAL, minimal) + } + + /// Create zdiff3 ("zealous diff3")-style files + pub fn style_zdiff3(&mut self, zdiff3: bool) -> &mut MergeFileOptions { + self.flag(raw::GIT_MERGE_FILE_STYLE_ZDIFF3, zdiff3) + } + + /// Do not produce file conflicts when common regions have changed + pub fn accept_conflicts(&mut self, accept: bool) -> &mut MergeFileOptions { + self.flag(raw::GIT_MERGE_FILE_ACCEPT_CONFLICTS, accept) + } + + /// The size of conflict markers (eg, "<<<<<<<"). Default is 7. + pub fn marker_size(&mut self, size: u16) -> &mut MergeFileOptions { + self.raw.marker_size = size as c_ushort; + self + } + + /// Acquire a pointer to the underlying raw options. + /// + /// # Safety + /// The pointer used here (or its contents) should not outlive self. + pub(crate) unsafe fn raw(&mut self) -> *const raw::git_merge_file_options { + &self.raw + } +} + +impl MergeFileResult { + /// True if the output was automerged, false if the output contains + /// conflict markers. + pub fn is_automergeable(&self) -> bool { + self.raw.automergeable > 0 + } + + /// The path that the resultant merge file should use. + /// + /// returns `None` if a filename conflict would occur, + /// or if the path is not valid utf-8 + pub fn path(&self) -> Option<&str> { + self.path_bytes() + .and_then(|bytes| str::from_utf8(bytes).ok()) + } + + /// Gets the path as a byte slice. + pub fn path_bytes(&self) -> Option<&[u8]> { + unsafe { crate::opt_bytes(self, self.raw.path) } + } + + /// The mode that the resultant merge file should use. + pub fn mode(&self) -> u32 { + self.raw.mode as u32 + } + + /// The contents of the merge. + pub fn content(&self) -> &[u8] { + unsafe { std::slice::from_raw_parts(self.raw.ptr as *const u8, self.raw.len as usize) } + } +} + +impl Binding for MergeFileResult { + type Raw = raw::git_merge_file_result; + unsafe fn from_raw(raw: raw::git_merge_file_result) -> MergeFileResult { + MergeFileResult { raw } + } + fn raw(&self) -> raw::git_merge_file_result { + unimplemented!() + } +} + +impl Drop for MergeFileResult { + fn drop(&mut self) { + unsafe { raw::git_merge_file_result_free(&mut self.raw) } + } +} + +impl std::fmt::Debug for MergeFileResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut ds = f.debug_struct("MergeFileResult"); + if let Some(path) = &self.path() { + ds.field("path", path); + } + ds.field("automergeable", &self.is_automergeable()); + ds.field("mode", &self.mode()); + ds.finish() + } +} diff --git a/src/message.rs b/src/message.rs index bdfccf6515..a7041da3ae 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,52 +1,349 @@ +use core::ops::Range; +use std::ffi::CStr; use std::ffi::CString; +use std::iter::FusedIterator; +use std::ptr; use libc::{c_char, c_int}; -use {raw, Buf, Error, IntoCString}; -use util::Binding; +use crate::util::Binding; +use crate::{raw, Buf, Error, IntoCString}; /// Clean up a message, removing extraneous whitespace, and ensure that the -/// message ends with a newline. If comment_char is Some, also remove comment +/// message ends with a newline. If `comment_char` is `Some`, also remove comment /// lines starting with that character. -pub fn message_prettify(message: T, comment_char: Option) - -> Result { - _message_prettify(try!(message.into_c_string()), comment_char) +pub fn message_prettify( + message: T, + comment_char: Option, +) -> Result { + _message_prettify(message.into_c_string()?, comment_char) } -fn _message_prettify(message: CString, comment_char: Option) - -> Result { +fn _message_prettify(message: CString, comment_char: Option) -> Result { let ret = Buf::new(); unsafe { - try_call!(raw::git_message_prettify(ret.raw(), message, - comment_char.is_some() as c_int, - comment_char.unwrap_or(0) as c_char)); + try_call!(raw::git_message_prettify( + ret.raw(), + message, + comment_char.is_some() as c_int, + comment_char.unwrap_or(0) as c_char + )); } Ok(ret.as_str().unwrap().to_string()) } -/// The default comment character for message_prettify ('#') -pub const DEFAULT_COMMENT_CHAR: Option = Some('#' as u8); +/// The default comment character for `message_prettify` ('#') +pub const DEFAULT_COMMENT_CHAR: Option = Some(b'#'); + +/// Get the trailers for the given message. +/// +/// Use this function when you are dealing with a UTF-8-encoded message. +pub fn message_trailers_strs(message: &str) -> Result { + _message_trailers(message.into_c_string()?).map(|res| MessageTrailersStrs(res)) +} + +/// Get the trailers for the given message. +/// +/// Use this function when the message might not be UTF-8-encoded, +/// or if you want to handle the returned trailer key–value pairs +/// as bytes. +pub fn message_trailers_bytes(message: S) -> Result { + _message_trailers(message.into_c_string()?).map(|res| MessageTrailersBytes(res)) +} + +fn _message_trailers(message: CString) -> Result { + let ret = MessageTrailers::new(); + unsafe { + try_call!(raw::git_message_trailers(ret.raw(), message)); + } + Ok(ret) +} + +/// Collection of UTF-8-encoded trailers. +/// +/// Use `iter()` to get access to the values. +pub struct MessageTrailersStrs(MessageTrailers); + +impl MessageTrailersStrs { + /// Create a borrowed iterator. + pub fn iter(&self) -> MessageTrailersStrsIterator<'_> { + MessageTrailersStrsIterator(self.0.iter()) + } + /// The number of trailer key–value pairs. + pub fn len(&self) -> usize { + self.0.len() + } + /// Convert to the “bytes” variant. + pub fn to_bytes(self) -> MessageTrailersBytes { + MessageTrailersBytes(self.0) + } +} + +/// Collection of unencoded (bytes) trailers. +/// +/// Use `iter()` to get access to the values. +pub struct MessageTrailersBytes(MessageTrailers); + +impl MessageTrailersBytes { + /// Create a borrowed iterator. + pub fn iter(&self) -> MessageTrailersBytesIterator<'_> { + MessageTrailersBytesIterator(self.0.iter()) + } + /// The number of trailer key–value pairs. + pub fn len(&self) -> usize { + self.0.len() + } +} + +struct MessageTrailers { + raw: raw::git_message_trailer_array, +} + +impl MessageTrailers { + fn new() -> MessageTrailers { + crate::init(); + unsafe { + Binding::from_raw(&mut raw::git_message_trailer_array { + trailers: ptr::null_mut(), + count: 0, + _trailer_block: ptr::null_mut(), + } as *mut _) + } + } + fn iter(&self) -> MessageTrailersIterator<'_> { + MessageTrailersIterator { + trailers: self, + range: Range { + start: 0, + end: self.raw.count, + }, + } + } + fn len(&self) -> usize { + self.raw.count + } +} + +impl Drop for MessageTrailers { + fn drop(&mut self) { + unsafe { + raw::git_message_trailer_array_free(&mut self.raw); + } + } +} + +impl Binding for MessageTrailers { + type Raw = *mut raw::git_message_trailer_array; + unsafe fn from_raw(raw: *mut raw::git_message_trailer_array) -> MessageTrailers { + MessageTrailers { raw: *raw } + } + fn raw(&self) -> *mut raw::git_message_trailer_array { + &self.raw as *const _ as *mut _ + } +} + +struct MessageTrailersIterator<'a> { + trailers: &'a MessageTrailers, + range: Range, +} + +fn to_raw_tuple(trailers: &MessageTrailers, index: usize) -> (*const c_char, *const c_char) { + unsafe { + let addr = trailers.raw.trailers.wrapping_add(index); + ((*addr).key, (*addr).value) + } +} + +/// Borrowed iterator over the UTF-8-encoded trailers. +pub struct MessageTrailersStrsIterator<'a>(MessageTrailersIterator<'a>); + +impl<'pair> Iterator for MessageTrailersStrsIterator<'pair> { + type Item = (&'pair str, &'pair str); + + fn next(&mut self) -> Option { + self.0 + .range + .next() + .map(|index| to_str_tuple(&self.0.trailers, index)) + } + + fn size_hint(&self) -> (usize, Option) { + self.0.range.size_hint() + } +} + +impl FusedIterator for MessageTrailersStrsIterator<'_> {} + +impl ExactSizeIterator for MessageTrailersStrsIterator<'_> { + fn len(&self) -> usize { + self.0.range.len() + } +} + +impl DoubleEndedIterator for MessageTrailersStrsIterator<'_> { + fn next_back(&mut self) -> Option { + self.0 + .range + .next_back() + .map(|index| to_str_tuple(&self.0.trailers, index)) + } +} + +fn to_str_tuple(trailers: &MessageTrailers, index: usize) -> (&str, &str) { + unsafe { + let (rkey, rvalue) = to_raw_tuple(&trailers, index); + let key = CStr::from_ptr(rkey).to_str().unwrap(); + let value = CStr::from_ptr(rvalue).to_str().unwrap(); + (key, value) + } +} + +/// Borrowed iterator over the raw (bytes) trailers. +pub struct MessageTrailersBytesIterator<'a>(MessageTrailersIterator<'a>); + +impl<'pair> Iterator for MessageTrailersBytesIterator<'pair> { + type Item = (&'pair [u8], &'pair [u8]); + + fn next(&mut self) -> Option { + self.0 + .range + .next() + .map(|index| to_bytes_tuple(&self.0.trailers, index)) + } + + fn size_hint(&self) -> (usize, Option) { + self.0.range.size_hint() + } +} + +impl FusedIterator for MessageTrailersBytesIterator<'_> {} + +impl ExactSizeIterator for MessageTrailersBytesIterator<'_> { + fn len(&self) -> usize { + self.0.range.len() + } +} + +impl DoubleEndedIterator for MessageTrailersBytesIterator<'_> { + fn next_back(&mut self) -> Option { + self.0 + .range + .next_back() + .map(|index| to_bytes_tuple(&self.0.trailers, index)) + } +} + +fn to_bytes_tuple(trailers: &MessageTrailers, index: usize) -> (&[u8], &[u8]) { + unsafe { + let (rkey, rvalue) = to_raw_tuple(&trailers, index); + let key = CStr::from_ptr(rkey).to_bytes(); + let value = CStr::from_ptr(rvalue).to_bytes(); + (key, value) + } +} #[cfg(test)] mod tests { - use {message_prettify, DEFAULT_COMMENT_CHAR}; #[test] fn prettify() { + use crate::{message_prettify, DEFAULT_COMMENT_CHAR}; + // This does not attempt to duplicate the extensive tests for // git_message_prettify in libgit2, just a few representative values to // make sure the interface works as expected. - assert_eq!(message_prettify("1\n\n\n2", None).unwrap(), - "1\n\n2\n"); - assert_eq!(message_prettify("1\n\n\n2\n\n\n3", None).unwrap(), - "1\n\n2\n\n3\n"); - assert_eq!(message_prettify("1\n# comment\n# more", None).unwrap(), - "1\n# comment\n# more\n"); - assert_eq!(message_prettify("1\n# comment\n# more", - DEFAULT_COMMENT_CHAR).unwrap(), - "1\n"); - assert_eq!(message_prettify("1\n; comment\n; more", - Some(';' as u8)).unwrap(), - "1\n"); + assert_eq!(message_prettify("1\n\n\n2", None).unwrap(), "1\n\n2\n"); + assert_eq!( + message_prettify("1\n\n\n2\n\n\n3", None).unwrap(), + "1\n\n2\n\n3\n" + ); + assert_eq!( + message_prettify("1\n# comment\n# more", None).unwrap(), + "1\n# comment\n# more\n" + ); + assert_eq!( + message_prettify("1\n# comment\n# more", DEFAULT_COMMENT_CHAR).unwrap(), + "1\n" + ); + assert_eq!( + message_prettify("1\n; comment\n; more", Some(';' as u8)).unwrap(), + "1\n" + ); + } + + #[test] + fn trailers() { + use crate::{message_trailers_bytes, message_trailers_strs, MessageTrailersStrs}; + use std::collections::HashMap; + + // no trailers + let message1 = " +WHAT ARE WE HERE FOR + +What are we here for? + +Just to be eaten? +"; + let expected: HashMap<&str, &str> = HashMap::new(); + assert_eq!(expected, to_map(&message_trailers_strs(message1).unwrap())); + + // standard PSA + let message2 = " +Attention all + +We are out of tomatoes. + +Spoken-by: Major Turnips +Transcribed-by: Seargant Persimmons +Signed-off-by: Colonel Kale +"; + let expected: HashMap<&str, &str> = vec![ + ("Spoken-by", "Major Turnips"), + ("Transcribed-by", "Seargant Persimmons"), + ("Signed-off-by", "Colonel Kale"), + ] + .into_iter() + .collect(); + assert_eq!(expected, to_map(&message_trailers_strs(message2).unwrap())); + + // ignore everything after `---` + let message3 = " +The fate of Seargant Green-Peppers + +Seargant Green-Peppers was killed by Caterpillar Battalion 44. + +Signed-off-by: Colonel Kale +--- +I never liked that guy, anyway. + +Opined-by: Corporal Garlic +"; + let expected: HashMap<&str, &str> = vec![("Signed-off-by", "Colonel Kale")] + .into_iter() + .collect(); + assert_eq!(expected, to_map(&message_trailers_strs(message3).unwrap())); + + // Raw bytes message; not valid UTF-8 + // Source: https://stackoverflow.com/a/3886015/1725151 + let message4 = b" +Be honest guys + +Am I a malformed brussels sprout? + +Signed-off-by: Lieutenant \xe2\x28\xa1prout +"; + + let trailer = message_trailers_bytes(&message4[..]).unwrap(); + let expected = (&b"Signed-off-by"[..], &b"Lieutenant \xe2\x28\xa1prout"[..]); + let actual = trailer.iter().next().unwrap(); + assert_eq!(expected, actual); + + fn to_map(trailers: &MessageTrailersStrs) -> HashMap<&str, &str> { + let mut map = HashMap::with_capacity(trailers.len()); + for (key, value) in trailers.iter() { + map.insert(key, value); + } + map + } } } diff --git a/src/note.rs b/src/note.rs index bbd94a517b..50e5800fe7 100644 --- a/src/note.rs +++ b/src/note.rs @@ -1,12 +1,12 @@ use std::marker; use std::str; -use {raw, signature, Signature, Oid, Repository, Error}; -use util::Binding; +use crate::util::Binding; +use crate::{raw, signature, Error, Oid, Repository, Signature}; /// A structure representing a [note][note] in git. /// -/// [note]: http://git-scm.com/blog/2010/08/25/notes.html +/// [note]: http://alblue.bandlem.com/2011/11/git-tip-of-week-git-notes.html pub struct Note<'repo> { raw: *mut raw::git_note, @@ -24,22 +24,18 @@ pub struct Notes<'repo> { impl<'repo> Note<'repo> { /// Get the note author - pub fn author(&self) -> Signature { - unsafe { - signature::from_raw_const(self, raw::git_note_author(&*self.raw)) - } + pub fn author(&self) -> Signature<'_> { + unsafe { signature::from_raw_const(self, raw::git_note_author(&*self.raw)) } } /// Get the note committer - pub fn committer(&self) -> Signature { - unsafe { - signature::from_raw_const(self, raw::git_note_committer(&*self.raw)) - } + pub fn committer(&self) -> Signature<'_> { + unsafe { signature::from_raw_const(self, raw::git_note_committer(&*self.raw)) } } /// Get the note message, in bytes. pub fn message_bytes(&self) -> &[u8] { - unsafe { ::opt_bytes(self, raw::git_note_message(&*self.raw)).unwrap() } + unsafe { crate::opt_bytes(self, raw::git_note_message(&*self.raw)).unwrap() } } /// Get the note message as a string, returning `None` if it is not UTF-8. @@ -56,43 +52,69 @@ impl<'repo> Note<'repo> { impl<'repo> Binding for Note<'repo> { type Raw = *mut raw::git_note; unsafe fn from_raw(raw: *mut raw::git_note) -> Note<'repo> { - Note { raw: raw, _marker: marker::PhantomData, } + Note { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_note { + self.raw } - fn raw(&self) -> *mut raw::git_note { self.raw } } +impl<'repo> std::fmt::Debug for Note<'repo> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_struct("Note").field("id", &self.id()).finish() + } +} impl<'repo> Drop for Note<'repo> { fn drop(&mut self) { - unsafe { raw::git_note_free(self.raw); } + unsafe { + raw::git_note_free(self.raw); + } } } impl<'repo> Binding for Notes<'repo> { type Raw = *mut raw::git_note_iterator; unsafe fn from_raw(raw: *mut raw::git_note_iterator) -> Notes<'repo> { - Notes { raw: raw, _marker: marker::PhantomData, } + Notes { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_note_iterator { + self.raw } - fn raw(&self) -> *mut raw::git_note_iterator { self.raw } } impl<'repo> Iterator for Notes<'repo> { type Item = Result<(Oid, Oid), Error>; fn next(&mut self) -> Option> { - let mut note_id = raw::git_oid { id: [0; raw::GIT_OID_RAWSZ] }; + let mut note_id = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; let mut annotated_id = note_id; unsafe { - try_call_iter!(raw::git_note_next(&mut note_id, &mut annotated_id, - self.raw)); - Some(Ok((Binding::from_raw(¬e_id as *const _), - Binding::from_raw(&annotated_id as *const _)))) + try_call_iter!(raw::git_note_next( + &mut note_id, + &mut annotated_id, + self.raw + )); + Some(Ok(( + Binding::from_raw(¬e_id as *const _), + Binding::from_raw(&annotated_id as *const _), + ))) } } } impl<'repo> Drop for Notes<'repo> { fn drop(&mut self) { - unsafe { raw::git_note_iterator_free(self.raw); } + unsafe { + raw::git_note_iterator_free(self.raw); + } } } @@ -100,7 +122,7 @@ impl<'repo> Drop for Notes<'repo> { mod tests { #[test] fn smoke() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); assert!(repo.notes(None).is_err()); let sig = repo.signature().unwrap(); diff --git a/src/object.rs b/src/object.rs index c7b75e7d47..fcae0066cb 100644 --- a/src/object.rs +++ b/src/object.rs @@ -2,9 +2,9 @@ use std::marker; use std::mem; use std::ptr; -use {raw, Oid, ObjectType, Error, Buf, Commit, Tag, Blob, Tree, Repository}; -use {Describe, DescribeOptions}; -use util::Binding; +use crate::util::Binding; +use crate::{raw, Blob, Buf, Commit, Error, ObjectType, Oid, Repository, Tag, Tree}; +use crate::{Describe, DescribeOptions}; /// A structure to represent a git [object][1] /// @@ -17,9 +17,7 @@ pub struct Object<'repo> { impl<'repo> Object<'repo> { /// Get the id (SHA1) of a repository object pub fn id(&self) -> Oid { - unsafe { - Binding::from_raw(raw::git_object_id(&*self.raw)) - } + unsafe { Binding::from_raw(raw::git_object_id(&*self.raw)) } } /// Get the object type of an object. @@ -35,13 +33,37 @@ impl<'repo> Object<'repo> { /// peeled until the type changes (e.g. a tag will be chased until the /// referenced object is no longer a tag). pub fn peel(&self, kind: ObjectType) -> Result, Error> { - let mut raw = 0 as *mut raw::git_object; + let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_object_peel(&mut raw, &*self.raw(), kind)); Ok(Binding::from_raw(raw)) } } + /// Recursively peel an object until a blob is found + pub fn peel_to_blob(&self) -> Result, Error> { + self.peel(ObjectType::Blob) + .map(|o| o.cast_or_panic(ObjectType::Blob)) + } + + /// Recursively peel an object until a commit is found + pub fn peel_to_commit(&self) -> Result, Error> { + self.peel(ObjectType::Commit) + .map(|o| o.cast_or_panic(ObjectType::Commit)) + } + + /// Recursively peel an object until a tag is found + pub fn peel_to_tag(&self) -> Result, Error> { + self.peel(ObjectType::Tag) + .map(|o| o.cast_or_panic(ObjectType::Tag)) + } + + /// Recursively peel an object until a tree is found + pub fn peel_to_tree(&self) -> Result, Error> { + self.peel(ObjectType::Tree) + .map(|o| o.cast_or_panic(ObjectType::Tree)) + } + /// Get a short abbreviated OID string for the object /// /// This starts at the "core.abbrev" length (default 7 characters) and @@ -115,9 +137,8 @@ impl<'repo> Object<'repo> { /// Describes a commit /// /// Performs a describe operation on this commitish object. - pub fn describe(&self, opts: &DescribeOptions) - -> Result { - let mut ret = 0 as *mut _; + pub fn describe(&self, opts: &DescribeOptions) -> Result, Error> { + let mut ret = ptr::null_mut(); unsafe { try_call!(raw::git_describe_commit(&mut ret, self.raw, opts.raw())); Ok(Binding::from_raw(ret)) @@ -125,7 +146,7 @@ impl<'repo> Object<'repo> { } fn cast(&self, kind: ObjectType) -> Option<&T> { - assert_eq!(mem::size_of::(), mem::size_of::()); + assert_eq!(mem::size_of::>(), mem::size_of::()); if self.kind() == Some(kind) { unsafe { Some(&*(self as *const _ as *const T)) } } else { @@ -147,9 +168,42 @@ impl<'repo> Object<'repo> { } } +/// This trait is useful to export cast_or_panic into crate but not outside +pub trait CastOrPanic { + fn cast_or_panic(self, kind: ObjectType) -> T; +} + +impl<'repo> CastOrPanic for Object<'repo> { + fn cast_or_panic(self, kind: ObjectType) -> T { + assert_eq!(mem::size_of_val(&self), mem::size_of::()); + if self.kind() == Some(kind) { + unsafe { + let other = ptr::read(&self as *const _ as *const T); + mem::forget(self); + other + } + } else { + let buf; + let akind = match self.kind() { + Some(akind) => akind.str(), + None => { + buf = format!("unknown ({})", unsafe { raw::git_object_type(&*self.raw) }); + &buf + } + }; + panic!( + "Expected object {} to be {} but it is {}", + self.id(), + kind.str(), + akind + ) + } + } +} + impl<'repo> Clone for Object<'repo> { fn clone(&self) -> Object<'repo> { - let mut raw = 0 as *mut raw::git_object; + let mut raw = ptr::null_mut(); unsafe { let rc = raw::git_object_dup(&mut raw, self.raw); assert_eq!(rc, 0); @@ -158,13 +212,33 @@ impl<'repo> Clone for Object<'repo> { } } +impl<'repo> std::fmt::Debug for Object<'repo> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut ds = f.debug_struct("Object"); + match self.kind() { + Some(kind) => ds.field("kind", &kind), + None => ds.field( + "kind", + &format!("Unknow ({})", unsafe { raw::git_object_type(&*self.raw) }), + ), + }; + ds.field("id", &self.id()); + ds.finish() + } +} + impl<'repo> Binding for Object<'repo> { type Raw = *mut raw::git_object; unsafe fn from_raw(raw: *mut raw::git_object) -> Object<'repo> { - Object { raw: raw, _marker: marker::PhantomData, } + Object { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_object { + self.raw } - fn raw(&self) -> *mut raw::git_object { self.raw } } impl<'repo> Drop for Object<'repo> { diff --git a/src/odb.rs b/src/odb.rs new file mode 100644 index 0000000000..2019908c48 --- /dev/null +++ b/src/odb.rs @@ -0,0 +1,770 @@ +use std::io; +use std::marker; +use std::ptr; +use std::slice; + +use std::ffi::CString; + +use libc::{c_char, c_int, c_uint, c_void, size_t}; + +use crate::panic; +use crate::util::Binding; +use crate::{ + raw, Error, IndexerProgress, Mempack, Object, ObjectType, OdbLookupFlags, Oid, Progress, +}; + +/// A structure to represent a git object database +pub struct Odb<'repo> { + raw: *mut raw::git_odb, + _marker: marker::PhantomData>, +} + +// `git_odb` uses locking and atomics internally. +unsafe impl<'repo> Send for Odb<'repo> {} +unsafe impl<'repo> Sync for Odb<'repo> {} + +impl<'repo> Binding for Odb<'repo> { + type Raw = *mut raw::git_odb; + + unsafe fn from_raw(raw: *mut raw::git_odb) -> Odb<'repo> { + Odb { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_odb { + self.raw + } +} + +impl<'repo> Drop for Odb<'repo> { + fn drop(&mut self) { + unsafe { raw::git_odb_free(self.raw) } + } +} + +impl<'repo> Odb<'repo> { + /// Creates an object database without any backends. + pub fn new<'a>() -> Result, Error> { + crate::init(); + unsafe { + let mut out = ptr::null_mut(); + try_call!(raw::git_odb_new(&mut out)); + Ok(Odb::from_raw(out)) + } + } + + /// Create object database reading stream. + /// + /// Note that most backends do not support streaming reads because they store their objects as compressed/delta'ed blobs. + /// If the backend does not support streaming reads, use the `read` method instead. + pub fn reader(&self, oid: Oid) -> Result<(OdbReader<'_>, usize, ObjectType), Error> { + let mut out = ptr::null_mut(); + let mut size = 0usize; + let mut otype: raw::git_object_t = ObjectType::Any.raw(); + unsafe { + try_call!(raw::git_odb_open_rstream( + &mut out, + &mut size, + &mut otype, + self.raw, + oid.raw() + )); + Ok(( + OdbReader::from_raw(out), + size, + ObjectType::from_raw(otype).unwrap(), + )) + } + } + + /// Create object database writing stream. + /// + /// The type and final length of the object must be specified when opening the stream. + /// If the backend does not support streaming writes, use the `write` method instead. + pub fn writer(&self, size: usize, obj_type: ObjectType) -> Result, Error> { + let mut out = ptr::null_mut(); + unsafe { + try_call!(raw::git_odb_open_wstream( + &mut out, + self.raw, + size as raw::git_object_size_t, + obj_type.raw() + )); + Ok(OdbWriter::from_raw(out)) + } + } + + /// Iterate over all objects in the object database.s + pub fn foreach(&self, mut callback: C) -> Result<(), Error> + where + C: FnMut(&Oid) -> bool, + { + unsafe { + let mut data = ForeachCbData { + callback: &mut callback, + }; + let cb: raw::git_odb_foreach_cb = Some(foreach_cb); + try_call!(raw::git_odb_foreach( + self.raw(), + cb, + &mut data as *mut _ as *mut _ + )); + Ok(()) + } + } + + /// Read an object from the database. + pub fn read(&self, oid: Oid) -> Result, Error> { + let mut out = ptr::null_mut(); + unsafe { + try_call!(raw::git_odb_read(&mut out, self.raw, oid.raw())); + Ok(OdbObject::from_raw(out)) + } + } + + /// Reads the header of an object from the database + /// without reading the full content. + pub fn read_header(&self, oid: Oid) -> Result<(usize, ObjectType), Error> { + let mut size: usize = 0; + let mut kind_id: i32 = ObjectType::Any.raw(); + + unsafe { + try_call!(raw::git_odb_read_header( + &mut size as *mut size_t, + &mut kind_id as *mut raw::git_object_t, + self.raw, + oid.raw() + )); + + Ok((size, ObjectType::from_raw(kind_id).unwrap())) + } + } + + /// Write an object to the database. + pub fn write(&self, kind: ObjectType, data: &[u8]) -> Result { + unsafe { + let mut out = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + try_call!(raw::git_odb_write( + &mut out, + self.raw, + data.as_ptr() as *const c_void, + data.len(), + kind.raw() + )); + Ok(Oid::from_raw(&mut out)) + } + } + + /// Create stream for writing a pack file to the ODB + pub fn packwriter(&self) -> Result, Error> { + let mut out = ptr::null_mut(); + let progress_cb: raw::git_indexer_progress_cb = Some(write_pack_progress_cb); + let progress_payload = Box::new(OdbPackwriterCb { cb: None }); + let progress_payload_ptr = Box::into_raw(progress_payload); + + unsafe { + try_call!(raw::git_odb_write_pack( + &mut out, + self.raw, + progress_cb, + progress_payload_ptr as *mut c_void + )); + } + + Ok(OdbPackwriter { + raw: out, + progress: Default::default(), + progress_payload_ptr, + }) + } + + /// Checks if the object database has an object. + pub fn exists(&self, oid: Oid) -> bool { + unsafe { raw::git_odb_exists(self.raw, oid.raw()) != 0 } + } + + /// Checks if the object database has an object, with extended flags. + pub fn exists_ext(&self, oid: Oid, flags: OdbLookupFlags) -> bool { + unsafe { raw::git_odb_exists_ext(self.raw, oid.raw(), flags.bits() as c_uint) != 0 } + } + + /// Potentially finds an object that starts with the given prefix. + pub fn exists_prefix(&self, short_oid: Oid, len: usize) -> Result { + unsafe { + let mut out = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + try_call!(raw::git_odb_exists_prefix( + &mut out, + self.raw, + short_oid.raw(), + len + )); + Ok(Oid::from_raw(&out)) + } + } + + /// Refresh the object database. + /// This should never be needed, and is + /// provided purely for convenience. + /// The object database will automatically + /// refresh when an object is not found when + /// requested. + pub fn refresh(&self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_odb_refresh(self.raw)); + Ok(()) + } + } + + /// Adds an alternate disk backend to the object database. + pub fn add_disk_alternate(&self, path: &str) -> Result<(), Error> { + unsafe { + let path = CString::new(path)?; + try_call!(raw::git_odb_add_disk_alternate(self.raw, path)); + Ok(()) + } + } + + /// Create a new mempack backend, and add it to this odb with the given + /// priority. Higher values give the backend higher precedence. The default + /// loose and pack backends have priorities 1 and 2 respectively (hard-coded + /// in libgit2). A reference to the new mempack backend is returned on + /// success. The lifetime of the backend must be contained within the + /// lifetime of this odb, since deletion of the odb will also result in + /// deletion of the mempack backend. + /// + /// Here is an example that fails to compile because it tries to hold the + /// mempack reference beyond the Odb's lifetime: + /// + /// ```compile_fail + /// use git2::Odb; + /// let mempack = { + /// let odb = Odb::new().unwrap(); + /// odb.add_new_mempack_backend(1000).unwrap() + /// }; + /// ``` + pub fn add_new_mempack_backend<'odb>( + &'odb self, + priority: i32, + ) -> Result, Error> { + unsafe { + let mut mempack = ptr::null_mut(); + // The mempack backend object in libgit2 is only ever freed by an + // odb that has the backend in its list. So to avoid potentially + // leaking the mempack backend, this API ensures that the backend + // is added to the odb before returning it. The lifetime of the + // mempack is also bound to the lifetime of the odb, so that users + // can't end up with a dangling reference to a mempack object that + // was actually freed when the odb was destroyed. + try_call!(raw::git_mempack_new(&mut mempack)); + try_call!(raw::git_odb_add_backend( + self.raw, + mempack, + priority as c_int + )); + Ok(Mempack::from_raw(mempack)) + } + } +} + +/// An object from the Object Database. +pub struct OdbObject<'a> { + raw: *mut raw::git_odb_object, + _marker: marker::PhantomData>, +} + +impl<'a> Binding for OdbObject<'a> { + type Raw = *mut raw::git_odb_object; + + unsafe fn from_raw(raw: *mut raw::git_odb_object) -> OdbObject<'a> { + OdbObject { + raw, + _marker: marker::PhantomData, + } + } + + fn raw(&self) -> *mut raw::git_odb_object { + self.raw + } +} + +impl<'a> Drop for OdbObject<'a> { + fn drop(&mut self) { + unsafe { raw::git_odb_object_free(self.raw) } + } +} + +impl<'a> OdbObject<'a> { + /// Get the object type. + pub fn kind(&self) -> ObjectType { + unsafe { ObjectType::from_raw(raw::git_odb_object_type(self.raw)).unwrap() } + } + + /// Get the object size. + pub fn len(&self) -> usize { + unsafe { raw::git_odb_object_size(self.raw) } + } + + /// Get the object data. + pub fn data(&self) -> &[u8] { + unsafe { + let size = self.len(); + let ptr: *const u8 = raw::git_odb_object_data(self.raw) as *const u8; + let buffer = slice::from_raw_parts(ptr, size); + return buffer; + } + } + + /// Get the object id. + pub fn id(&self) -> Oid { + unsafe { Oid::from_raw(raw::git_odb_object_id(self.raw)) } + } +} + +/// A structure to represent a git ODB rstream +pub struct OdbReader<'repo> { + raw: *mut raw::git_odb_stream, + _marker: marker::PhantomData>, +} + +// `git_odb_stream` is not thread-safe internally, so it can't use `Sync`, but moving it to another +// thread and continuing to read will work. +unsafe impl<'repo> Send for OdbReader<'repo> {} + +impl<'repo> Binding for OdbReader<'repo> { + type Raw = *mut raw::git_odb_stream; + + unsafe fn from_raw(raw: *mut raw::git_odb_stream) -> OdbReader<'repo> { + OdbReader { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_odb_stream { + self.raw + } +} + +impl<'repo> Drop for OdbReader<'repo> { + fn drop(&mut self) { + unsafe { raw::git_odb_stream_free(self.raw) } + } +} + +impl<'repo> io::Read for OdbReader<'repo> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + unsafe { + let ptr = buf.as_ptr() as *mut c_char; + let len = buf.len(); + let res = raw::git_odb_stream_read(self.raw, ptr, len); + if res < 0 { + Err(io::Error::new(io::ErrorKind::Other, "Read error")) + } else { + Ok(res as _) + } + } + } +} + +/// A structure to represent a git ODB wstream +pub struct OdbWriter<'repo> { + raw: *mut raw::git_odb_stream, + _marker: marker::PhantomData>, +} + +// `git_odb_stream` is not thread-safe internally, so it can't use `Sync`, but moving it to another +// thread and continuing to write will work. +unsafe impl<'repo> Send for OdbWriter<'repo> {} + +impl<'repo> OdbWriter<'repo> { + /// Finish writing to an ODB stream + /// + /// This method can be used to finalize writing object to the database and get an identifier. + /// The object will take its final name and will be available to the odb. + /// This method will fail if the total number of received bytes differs from the size declared with odb_writer() + /// Attempting write after finishing will be ignored. + pub fn finalize(&mut self) -> Result { + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_odb_stream_finalize_write(&mut raw, self.raw)); + Ok(Binding::from_raw(&raw as *const _)) + } + } +} + +impl<'repo> Binding for OdbWriter<'repo> { + type Raw = *mut raw::git_odb_stream; + + unsafe fn from_raw(raw: *mut raw::git_odb_stream) -> OdbWriter<'repo> { + OdbWriter { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_odb_stream { + self.raw + } +} + +impl<'repo> Drop for OdbWriter<'repo> { + fn drop(&mut self) { + unsafe { raw::git_odb_stream_free(self.raw) } + } +} + +impl<'repo> io::Write for OdbWriter<'repo> { + fn write(&mut self, buf: &[u8]) -> io::Result { + unsafe { + let ptr = buf.as_ptr() as *const c_char; + let len = buf.len(); + let res = raw::git_odb_stream_write(self.raw, ptr, len); + if res < 0 { + Err(io::Error::new(io::ErrorKind::Other, "Write error")) + } else { + Ok(buf.len()) + } + } + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +pub(crate) struct OdbPackwriterCb<'repo> { + pub(crate) cb: Option>>, +} + +/// A stream to write a packfile to the ODB +pub struct OdbPackwriter<'repo> { + raw: *mut raw::git_odb_writepack, + progress: raw::git_indexer_progress, + progress_payload_ptr: *mut OdbPackwriterCb<'repo>, +} + +impl<'repo> OdbPackwriter<'repo> { + /// Finish writing the packfile + pub fn commit(&mut self) -> Result { + unsafe { + let writepack = &*self.raw; + let res = match writepack.commit { + Some(commit) => commit(self.raw, &mut self.progress), + None => -1, + }; + + if res < 0 { + Err(Error::last_error(res)) + } else { + Ok(res) + } + } + } + + /// The callback through which progress is monitored. Be aware that this is + /// called inline, so performance may be affected. + pub fn progress(&mut self, cb: F) -> &mut OdbPackwriter<'repo> + where + F: FnMut(Progress<'_>) -> bool + 'repo, + { + let progress_payload = + unsafe { &mut *(self.progress_payload_ptr as *mut OdbPackwriterCb<'_>) }; + + progress_payload.cb = Some(Box::new(cb) as Box>); + self + } +} + +impl<'repo> io::Write for OdbPackwriter<'repo> { + fn write(&mut self, buf: &[u8]) -> io::Result { + unsafe { + let ptr = buf.as_ptr() as *mut c_void; + let len = buf.len(); + + let writepack = &*self.raw; + let res = match writepack.append { + Some(append) => append(self.raw, ptr, len, &mut self.progress), + None => -1, + }; + + if res < 0 { + Err(io::Error::new(io::ErrorKind::Other, "Write error")) + } else { + Ok(buf.len()) + } + } + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl<'repo> Drop for OdbPackwriter<'repo> { + fn drop(&mut self) { + unsafe { + let writepack = &*self.raw; + match writepack.free { + Some(free) => free(self.raw), + None => (), + }; + + drop(Box::from_raw(self.progress_payload_ptr)); + } + } +} + +pub type ForeachCb<'a> = dyn FnMut(&Oid) -> bool + 'a; + +struct ForeachCbData<'a> { + pub callback: &'a mut ForeachCb<'a>, +} + +extern "C" fn foreach_cb(id: *const raw::git_oid, payload: *mut c_void) -> c_int { + panic::wrap(|| unsafe { + let data = &mut *(payload as *mut ForeachCbData<'_>); + let res = { + let callback = &mut data.callback; + callback(&Binding::from_raw(id)) + }; + + if res { + 0 + } else { + 1 + } + }) + .unwrap_or(1) +} + +pub(crate) extern "C" fn write_pack_progress_cb( + stats: *const raw::git_indexer_progress, + payload: *mut c_void, +) -> c_int { + let ok = panic::wrap(|| unsafe { + let payload = &mut *(payload as *mut OdbPackwriterCb<'_>); + + let callback = match payload.cb { + Some(ref mut cb) => cb, + None => return true, + }; + + let progress: Progress<'_> = Binding::from_raw(stats); + callback(progress) + }); + if ok == Some(true) { + 0 + } else { + -1 + } +} + +#[cfg(test)] +mod tests { + use crate::{Buf, ObjectType, Oid, Repository}; + use std::io::prelude::*; + use tempfile::TempDir; + + #[test] + fn read() { + let td = TempDir::new().unwrap(); + let repo = Repository::init(td.path()).unwrap(); + let dat = [4, 3, 5, 6, 9]; + let id = repo.blob(&dat).unwrap(); + let db = repo.odb().unwrap(); + let obj = db.read(id).unwrap(); + let data = obj.data(); + let size = obj.len(); + assert_eq!(size, 5); + assert_eq!(dat, data); + assert_eq!(id, obj.id()); + } + + #[test] + fn read_header() { + let td = TempDir::new().unwrap(); + let repo = Repository::init(td.path()).unwrap(); + let dat = [4, 3, 5, 6, 9]; + let id = repo.blob(&dat).unwrap(); + let db = repo.odb().unwrap(); + let (size, kind) = db.read_header(id).unwrap(); + + assert_eq!(size, 5); + assert_eq!(kind, ObjectType::Blob); + } + + #[test] + fn write() { + let td = TempDir::new().unwrap(); + let repo = Repository::init(td.path()).unwrap(); + let dat = [4, 3, 5, 6, 9]; + let db = repo.odb().unwrap(); + let id = db.write(ObjectType::Blob, &dat).unwrap(); + let blob = repo.find_blob(id).unwrap(); + assert_eq!(blob.content(), dat); + } + + #[test] + fn writer() { + let td = TempDir::new().unwrap(); + let repo = Repository::init(td.path()).unwrap(); + let dat = [4, 3, 5, 6, 9]; + let db = repo.odb().unwrap(); + let mut ws = db.writer(dat.len(), ObjectType::Blob).unwrap(); + let wl = ws.write(&dat[0..3]).unwrap(); + assert_eq!(wl, 3); + let wl = ws.write(&dat[3..5]).unwrap(); + assert_eq!(wl, 2); + let id = ws.finalize().unwrap(); + let blob = repo.find_blob(id).unwrap(); + assert_eq!(blob.content(), dat); + } + + #[test] + fn exists() { + let td = TempDir::new().unwrap(); + let repo = Repository::init(td.path()).unwrap(); + let dat = [4, 3, 5, 6, 9]; + let db = repo.odb().unwrap(); + let id = db.write(ObjectType::Blob, &dat).unwrap(); + assert!(db.exists(id)); + } + + #[test] + fn exists_prefix() { + let td = TempDir::new().unwrap(); + let repo = Repository::init(td.path()).unwrap(); + let dat = [4, 3, 5, 6, 9]; + let db = repo.odb().unwrap(); + let id = db.write(ObjectType::Blob, &dat).unwrap(); + let id_prefix_str = &id.to_string()[0..10]; + let id_prefix = Oid::from_str(id_prefix_str).unwrap(); + let found_oid = db.exists_prefix(id_prefix, 10).unwrap(); + assert_eq!(found_oid, id); + } + + #[test] + fn packwriter() { + let (_td, repo_source) = crate::test::repo_init(); + let (_td, repo_target) = crate::test::repo_init(); + let mut builder = t!(repo_source.packbuilder()); + let mut buf = Buf::new(); + let (commit_source_id, _tree) = crate::test::commit(&repo_source); + t!(builder.insert_object(commit_source_id, None)); + t!(builder.write_buf(&mut buf)); + let db = repo_target.odb().unwrap(); + let mut packwriter = db.packwriter().unwrap(); + packwriter.write(&buf).unwrap(); + packwriter.commit().unwrap(); + let commit_target = repo_target.find_commit(commit_source_id).unwrap(); + assert_eq!(commit_target.id(), commit_source_id); + } + + #[test] + fn packwriter_progress() { + let mut progress_called = false; + { + let (_td, repo_source) = crate::test::repo_init(); + let (_td, repo_target) = crate::test::repo_init(); + let mut builder = t!(repo_source.packbuilder()); + let mut buf = Buf::new(); + let (commit_source_id, _tree) = crate::test::commit(&repo_source); + t!(builder.insert_object(commit_source_id, None)); + t!(builder.write_buf(&mut buf)); + let db = repo_target.odb().unwrap(); + let mut packwriter = db.packwriter().unwrap(); + packwriter.progress(|_| { + progress_called = true; + true + }); + packwriter.write(&buf).unwrap(); + packwriter.commit().unwrap(); + } + assert_eq!(progress_called, true); + } + + #[test] + fn write_with_mempack() { + use crate::{Buf, ResetType}; + use std::io::Write; + use std::path::Path; + + // Create a repo, add a mempack backend + let (_td, repo) = crate::test::repo_init(); + let odb = repo.odb().unwrap(); + let mempack = odb.add_new_mempack_backend(1000).unwrap(); + + // Sanity check that foo doesn't exist initially + let foo_file = Path::new(repo.workdir().unwrap()).join("foo"); + assert!(!foo_file.exists()); + + // Make a commit that adds foo. This writes new stuff into the mempack + // backend. + let (oid1, _id) = crate::test::commit(&repo); + let commit1 = repo.find_commit(oid1).unwrap(); + t!(repo.reset(commit1.as_object(), ResetType::Hard, None)); + assert!(foo_file.exists()); + + // Dump the mempack modifications into a buf, and reset it. This "erases" + // commit-related objects from the repository. Ensure the commit appears + // to have become invalid, by checking for failure in `reset --hard`. + let mut buf = Buf::new(); + mempack.dump(&repo, &mut buf).unwrap(); + mempack.reset().unwrap(); + assert!(repo + .reset(commit1.as_object(), ResetType::Hard, None) + .is_err()); + + // Write the buf into a packfile in the repo. This brings back the + // missing objects, and we verify everything is good again. + let mut packwriter = odb.packwriter().unwrap(); + packwriter.write(&buf).unwrap(); + packwriter.commit().unwrap(); + t!(repo.reset(commit1.as_object(), ResetType::Hard, None)); + assert!(foo_file.exists()); + } + + #[test] + fn stream_read() { + // Test for read impl of OdbReader. + const FOO_TEXT: &[u8] = b"this is a test"; + let (_td, repo) = crate::test::repo_init(); + let p = repo.path().parent().unwrap().join("foo"); + std::fs::write(&p, FOO_TEXT).unwrap(); + let mut index = repo.index().unwrap(); + index.add_path(std::path::Path::new("foo")).unwrap(); + let tree_id = index.write_tree().unwrap(); + let tree = repo.find_tree(tree_id).unwrap(); + let sig = repo.signature().unwrap(); + let head_id = repo.refname_to_id("HEAD").unwrap(); + let parent = repo.find_commit(head_id).unwrap(); + let _commit = repo + .commit(Some("HEAD"), &sig, &sig, "commit", &tree, &[&parent]) + .unwrap(); + + // Try reading from a commit object. + let odb = repo.odb().unwrap(); + let oid = repo.refname_to_id("HEAD").unwrap(); + let (mut reader, size, ty) = odb.reader(oid).unwrap(); + assert!(ty == ObjectType::Commit); + let mut x = [0; 10000]; + let r = reader.read(&mut x).unwrap(); + assert!(r == size); + + // Try reading from a blob. This assumes it is a loose object (packed + // objects can't read). + let commit = repo.find_commit(oid).unwrap(); + let tree = commit.tree().unwrap(); + let entry = tree.get_name("foo").unwrap(); + let (mut reader, size, ty) = odb.reader(entry.id()).unwrap(); + assert_eq!(size, FOO_TEXT.len()); + assert!(ty == ObjectType::Blob); + let mut x = [0; 10000]; + let r = reader.read(&mut x).unwrap(); + assert_eq!(r, 14); + assert_eq!(&x[..FOO_TEXT.len()], FOO_TEXT); + } +} diff --git a/src/oid.rs b/src/oid.rs index 4d08b88ca4..35516cb181 100644 --- a/src/oid.rs +++ b/src/oid.rs @@ -1,14 +1,16 @@ -use std::fmt; use std::cmp::Ordering; -use std::hash::{Hasher, Hash}; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::path::Path; use std::str; -use libc; -use {raw, Error}; -use util::Binding; +use crate::{raw, Error, IntoCString, ObjectType}; + +use crate::util::{c_cmp_to_ordering, Binding}; /// Unique identity of any object (commit, tree, blob, tag). -#[derive(Copy)] +#[derive(Copy, Clone)] +#[repr(C)] pub struct Oid { raw: raw::git_oid, } @@ -16,36 +18,95 @@ pub struct Oid { impl Oid { /// Parse a hex-formatted object id into an Oid structure. /// - /// If the string is not a valid 40-character hex string, an error is - /// returned. + /// # Errors + /// + /// Returns an error if the string is empty, is longer than 40 hex + /// characters, or contains any non-hex characters. pub fn from_str(s: &str) -> Result { - ::init(); - let mut raw = raw::git_oid { id: [0; raw::GIT_OID_RAWSZ] }; + crate::init(); + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; unsafe { - try_call!(raw::git_oid_fromstrn(&mut raw, - s.as_bytes().as_ptr() - as *const libc::c_char, - s.len() as libc::size_t)); + try_call!(raw::git_oid_fromstrn( + &mut raw, + s.as_bytes().as_ptr() as *const libc::c_char, + s.len() as libc::size_t + )); } - Ok(Oid { raw: raw }) + Ok(Oid { raw }) } /// Parse a raw object id into an Oid structure. /// /// If the array given is not 20 bytes in length, an error is returned. pub fn from_bytes(bytes: &[u8]) -> Result { - ::init(); - let mut raw = raw::git_oid { id: [0; raw::GIT_OID_RAWSZ] }; + crate::init(); + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; if bytes.len() != raw::GIT_OID_RAWSZ { Err(Error::from_str("raw byte array must be 20 bytes")) } else { - unsafe { raw::git_oid_fromraw(&mut raw, bytes.as_ptr()) } - Ok(Oid { raw: raw }) + unsafe { + try_call!(raw::git_oid_fromraw(&mut raw, bytes.as_ptr())); + } + Ok(Oid { raw }) + } + } + + /// Creates an all zero Oid structure. + pub fn zero() -> Oid { + let out = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + Oid { raw: out } + } + + /// Hashes the provided data as an object of the provided type, and returns + /// an Oid corresponding to the result. This does not store the object + /// inside any object database or repository. + pub fn hash_object(kind: ObjectType, bytes: &[u8]) -> Result { + crate::init(); + + let mut out = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_odb_hash( + &mut out, + bytes.as_ptr() as *const libc::c_void, + bytes.len(), + kind.raw() + )); + } + + Ok(Oid { raw: out }) + } + + /// Hashes the content of the provided file as an object of the provided type, + /// and returns an Oid corresponding to the result. This does not store the object + /// inside any object database or repository. + pub fn hash_file>(kind: ObjectType, path: P) -> Result { + crate::init(); + + // Normal file path OK (does not need Windows conversion). + let rpath = path.as_ref().into_c_string()?; + + let mut out = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_odb_hashfile(&mut out, rpath, kind.raw())); } + + Ok(Oid { raw: out }) } /// View this OID as a byte-slice 20 bytes in length. - pub fn as_bytes(&self) -> &[u8] { &self.raw.id } + pub fn as_bytes(&self) -> &[u8] { + &self.raw.id + } /// Test if this OID is all zeros. pub fn is_zero(&self) -> bool { @@ -59,22 +120,27 @@ impl Binding for Oid { unsafe fn from_raw(oid: *const raw::git_oid) -> Oid { Oid { raw: *oid } } - fn raw(&self) -> *const raw::git_oid { &self.raw as *const _ } + fn raw(&self) -> *const raw::git_oid { + &self.raw as *const _ + } } impl fmt::Debug for Oid { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(self, f) } } impl fmt::Display for Oid { /// Hex-encode this Oid into a formatter. - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut dst = [0u8; raw::GIT_OID_HEXSZ + 1]; unsafe { - raw::git_oid_tostr(dst.as_mut_ptr() as *mut libc::c_char, - dst.len() as libc::size_t, &self.raw); + raw::git_oid_tostr( + dst.as_mut_ptr() as *mut libc::c_char, + dst.len() as libc::size_t, + &self.raw, + ); } let s = &dst[..dst.iter().position(|&a| a == 0).unwrap()]; str::from_utf8(s).unwrap().fmt(f) @@ -86,8 +152,10 @@ impl str::FromStr for Oid { /// Parse a hex-formatted object id into an Oid structure. /// - /// If the string is not a valid 40-character hex string, an error is - /// returned. + /// # Errors + /// + /// Returns an error if the string is empty, is longer than 40 hex + /// characters, or contains any non-hex characters. fn from_str(s: &str) -> Result { Oid::from_str(s) } @@ -108,18 +176,10 @@ impl PartialOrd for Oid { impl Ord for Oid { fn cmp(&self, other: &Oid) -> Ordering { - match unsafe { raw::git_oid_cmp(&self.raw, &other.raw) } { - 0 => Ordering::Equal, - n if n < 0 => Ordering::Less, - _ => Ordering::Greater, - } + c_cmp_to_ordering(unsafe { raw::git_oid_cmp(&self.raw, &other.raw) }) } } -impl Clone for Oid { - fn clone(&self) -> Oid { *self } -} - impl Hash for Oid { fn hash(&self, into: &mut H) { self.raw.id.hash(into) @@ -127,12 +187,20 @@ impl Hash for Oid { } impl AsRef<[u8]> for Oid { - fn as_ref(&self) -> &[u8] { self.as_bytes() } + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } } #[cfg(test)] mod tests { + use std::fs::File; + use std::io::prelude::*; + + use super::Error; use super::Oid; + use crate::ObjectType; + use tempfile::TempDir; #[test] fn conversions() { @@ -141,4 +209,50 @@ mod tests { assert!(Oid::from_bytes(b"foo").is_err()); assert!(Oid::from_bytes(b"00000000000000000000").is_ok()); } + + #[test] + fn comparisons() -> Result<(), Error> { + assert_eq!(Oid::from_str("decbf2b")?, Oid::from_str("decbf2b")?); + assert!(Oid::from_str("decbf2b")? <= Oid::from_str("decbf2b")?); + assert!(Oid::from_str("decbf2b")? >= Oid::from_str("decbf2b")?); + { + let o = Oid::from_str("decbf2b")?; + assert_eq!(o, o); + assert!(o <= o); + assert!(o >= o); + } + assert_eq!( + Oid::from_str("decbf2b")?, + Oid::from_str("decbf2b000000000000000000000000000000000")? + ); + assert!( + Oid::from_bytes(b"00000000000000000000")? < Oid::from_bytes(b"00000000000000000001")? + ); + assert!(Oid::from_bytes(b"00000000000000000000")? < Oid::from_str("decbf2b")?); + assert_eq!( + Oid::from_bytes(b"00000000000000000000")?, + Oid::from_str("3030303030303030303030303030303030303030")? + ); + Ok(()) + } + + #[test] + fn zero_is_zero() { + assert!(Oid::zero().is_zero()); + } + + #[test] + fn hash_object() { + let bytes = "Hello".as_bytes(); + assert!(Oid::hash_object(ObjectType::Blob, bytes).is_ok()); + } + + #[test] + fn hash_file() { + let td = TempDir::new().unwrap(); + let path = td.path().join("hello.txt"); + let mut file = File::create(&path).unwrap(); + file.write_all("Hello".as_bytes()).unwrap(); + assert!(Oid::hash_file(ObjectType::Blob, &path).is_ok()); + } } diff --git a/src/oid_array.rs b/src/oid_array.rs index 314753883c..0d87ce9954 100644 --- a/src/oid_array.rs +++ b/src/oid_array.rs @@ -1,16 +1,16 @@ -//! Bindings to libgit2's raw git_strarray type +//! Bindings to libgit2's raw `git_oidarray` type use std::ops::Deref; -use oid::Oid; -use raw; -use util::Binding; -use std::slice; +use crate::oid::Oid; +use crate::raw; +use crate::util::Binding; use std::mem; +use std::slice; /// An oid array structure used by libgit2 /// -/// Some apis return arrays of oids which originate from libgit2. This +/// Some APIs return arrays of OIDs which originate from libgit2. This /// wrapper type behaves a little like `Vec<&Oid>` but does so without copying /// the underlying Oids until necessary. pub struct OidArray { @@ -23,7 +23,7 @@ impl Deref for OidArray { fn deref(&self) -> &[Oid] { unsafe { debug_assert_eq!(mem::size_of::(), mem::size_of_val(&*self.raw.ids)); - + slice::from_raw_parts(self.raw.ids as *const Oid, self.raw.count as usize) } } @@ -32,9 +32,17 @@ impl Deref for OidArray { impl Binding for OidArray { type Raw = raw::git_oidarray; unsafe fn from_raw(raw: raw::git_oidarray) -> OidArray { - OidArray { raw: raw } + OidArray { raw } + } + fn raw(&self) -> raw::git_oidarray { + self.raw + } +} + +impl<'repo> std::fmt::Debug for OidArray { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_tuple("OidArray").field(&self.deref()).finish() } - fn raw(&self) -> raw::git_oidarray { self.raw } } impl Drop for OidArray { diff --git a/src/opts.rs b/src/opts.rs new file mode 100644 index 0000000000..232d81e996 --- /dev/null +++ b/src/opts.rs @@ -0,0 +1,530 @@ +//! Bindings to libgit2's git_libgit2_opts function. + +use std::ffi::CString; +use std::ptr; + +use crate::string_array::StringArray; +use crate::util::Binding; +use crate::{raw, Buf, ConfigLevel, Error, IntoCString, ObjectType}; + +/// Set the search path for a level of config data. The search path applied to +/// shared attributes and ignore files, too. +/// +/// `level` must be one of [`ConfigLevel::System`], [`ConfigLevel::Global`], +/// [`ConfigLevel::XDG`], [`ConfigLevel::ProgramData`]. +/// +/// `path` lists directories delimited by `GIT_PATH_LIST_SEPARATOR`. +/// Use magic path `$PATH` to include the old value of the path +/// (if you want to prepend or append, for instance). +/// +/// This function is unsafe as it mutates the global state but cannot guarantee +/// thread-safety. It needs to be externally synchronized with calls to access +/// the global state. +pub unsafe fn set_search_path

(level: ConfigLevel, path: P) -> Result<(), Error> +where + P: IntoCString, +{ + crate::init(); + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_SET_SEARCH_PATH as libc::c_int, + level as libc::c_int, + path.into_c_string()?.as_ptr() + )); + Ok(()) +} + +/// Reset the search path for a given level of config data to the default +/// (generally based on environment variables). +/// +/// `level` must be one of [`ConfigLevel::System`], [`ConfigLevel::Global`], +/// [`ConfigLevel::XDG`], [`ConfigLevel::ProgramData`]. +/// +/// This function is unsafe as it mutates the global state but cannot guarantee +/// thread-safety. It needs to be externally synchronized with calls to access +/// the global state. +pub unsafe fn reset_search_path(level: ConfigLevel) -> Result<(), Error> { + crate::init(); + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_SET_SEARCH_PATH as libc::c_int, + level as libc::c_int, + core::ptr::null::() + )); + Ok(()) +} + +/// Get the search path for a given level of config data. +/// +/// `level` must be one of [`ConfigLevel::System`], [`ConfigLevel::Global`], +/// [`ConfigLevel::XDG`], [`ConfigLevel::ProgramData`]. +/// +/// This function is unsafe as it mutates the global state but cannot guarantee +/// thread-safety. It needs to be externally synchronized with calls to access +/// the global state. +pub unsafe fn get_search_path(level: ConfigLevel) -> Result { + crate::init(); + let buf = Buf::new(); + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_GET_SEARCH_PATH as libc::c_int, + level as libc::c_int, + buf.raw() as *const _ + )); + buf.into_c_string() +} + +/// Controls whether or not libgit2 will cache loaded objects. Enabled by +/// default, but disabling this can improve performance and memory usage if +/// loading a large number of objects that will not be referenced again. +/// Disabling this will cause repository objects to clear their caches when next +/// accessed. +pub fn enable_caching(enabled: bool) { + crate::init(); + let error = unsafe { + raw::git_libgit2_opts( + raw::GIT_OPT_ENABLE_CACHING as libc::c_int, + enabled as libc::c_int, + ) + }; + // This function cannot actually fail, but the function has an error return + // for other options that can. + debug_assert!(error >= 0); +} + +/// Set the maximum data size for the given type of object to be considered +/// eligible for caching in memory. Setting to value to zero means that that +/// type of object will not be cached. Defaults to 0 for [`ObjectType::Blob`] +/// (i.e. won't cache blobs) and 4k for [`ObjectType::Commit`], +/// [`ObjectType::Tree`], and [`ObjectType::Tag`]. +/// +/// `kind` must be one of [`ObjectType::Blob`], [`ObjectType::Commit`], +/// [`ObjectType::Tree`], and [`ObjectType::Tag`]. +/// +/// # Safety +/// This function is modifying a C global without synchronization, so it is not +/// thread safe, and should only be called before any thread is spawned. +pub unsafe fn set_cache_object_limit(kind: ObjectType, size: libc::size_t) -> Result<(), Error> { + crate::init(); + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_SET_CACHE_OBJECT_LIMIT as libc::c_int, + kind as libc::c_int, + size + )); + Ok(()) +} + +/// Set the maximum total data size that will be cached in memory across all +/// repositories before libgit2 starts evicting objects from the cache. This +/// is a soft limit, in that the library might briefly exceed it, but will start +/// aggressively evicting objects from cache when that happens. The default +/// cache size is 256MB. +/// +/// # Safety +/// This function is modifying a C global without synchronization, so it is not +/// thread safe, and should only be called before any thread is spawned. +pub unsafe fn set_cache_max_size(size: libc::ssize_t) -> Result<(), Error> { + crate::init(); + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_SET_CACHE_MAX_SIZE as libc::c_int, + size + )); + Ok(()) +} + +/// Get the current bytes in cache and the maximum that would be allowed in the cache. +/// +/// # Safety +/// This function is reading a C global without synchronization, so it is not +/// thread safe, and should only be called before any thread is spawned. +pub unsafe fn get_cached_memory() -> Result<(libc::ssize_t, libc::ssize_t), Error> { + crate::init(); + let mut current = 0; + let mut allowed = 0; + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_GET_CACHED_MEMORY as libc::c_int, + &mut current, + &mut allowed + )); + Ok((current, allowed)) +} + +/// Controls whether or not libgit2 will verify when writing an object that all +/// objects it references are valid. Enabled by default, but disabling this can +/// significantly improve performance, at the cost of potentially allowing the +/// creation of objects that reference invalid objects (due to programming +/// error or repository corruption). +pub fn strict_object_creation(enabled: bool) { + crate::init(); + let error = unsafe { + raw::git_libgit2_opts( + raw::GIT_OPT_ENABLE_STRICT_OBJECT_CREATION as libc::c_int, + enabled as libc::c_int, + ) + }; + // This function cannot actually fail, but the function has an error return + // for other options that can. + debug_assert!(error >= 0); +} + +/// Controls whether or not libgit2 will verify that objects loaded have the +/// expected hash. Enabled by default, but disabling this can significantly +/// improve performance, at the cost of relying on repository integrity +/// without checking it. +pub fn strict_hash_verification(enabled: bool) { + crate::init(); + let error = unsafe { + raw::git_libgit2_opts( + raw::GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION as libc::c_int, + enabled as libc::c_int, + ) + }; + // This function cannot actually fail, but the function has an error return + // for other options that can. + debug_assert!(error >= 0); +} + +/// Returns the list of git extensions that are supported. This is the list of +/// built-in extensions supported by libgit2 and custom extensions that have +/// been added with [`set_extensions`]. Extensions that have been negated will +/// not be returned. +/// +/// # Safety +/// +/// libgit2 stores user extensions in a static variable. +/// This function is effectively reading a `static mut` and should be treated as such +pub unsafe fn get_extensions() -> Result { + crate::init(); + + let mut extensions = raw::git_strarray { + strings: ptr::null_mut(), + count: 0, + }; + + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_GET_EXTENSIONS as libc::c_int, + &mut extensions + )); + + Ok(StringArray::from_raw(extensions)) +} + +/// Set that the given git extensions are supported by the caller. Extensions +/// supported by libgit2 may be negated by prefixing them with a `!`. +/// For example: setting extensions to `[ "!noop", "newext" ]` indicates that +/// the caller does not want to support repositories with the `noop` extension +/// but does want to support repositories with the `newext` extension. +/// +/// # Safety +/// +/// libgit2 stores user extensions in a static variable. +/// This function is effectively modifying a `static mut` and should be treated as such +pub unsafe fn set_extensions(extensions: &[E]) -> Result<(), Error> +where + for<'x> &'x E: IntoCString, +{ + crate::init(); + + let extensions = extensions + .iter() + .map(|e| e.into_c_string()) + .collect::, _>>()?; + + let extension_ptrs = extensions.iter().map(|e| e.as_ptr()).collect::>(); + + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_SET_EXTENSIONS as libc::c_int, + extension_ptrs.as_ptr(), + extension_ptrs.len() as libc::size_t + )); + + Ok(()) +} + +/// Set whether or not to verify ownership before performing a repository. +/// Enabled by default, but disabling this can lead to code execution vulnerabilities. +pub unsafe fn set_verify_owner_validation(enabled: bool) -> Result<(), Error> { + crate::init(); + let error = raw::git_libgit2_opts( + raw::GIT_OPT_SET_OWNER_VALIDATION as libc::c_int, + enabled as libc::c_int, + ); + // This function cannot actually fail, but the function has an error return + // for other options that can. + debug_assert!(error >= 0); + Ok(()) +} + +/// Set the SSL certificate-authority location to `file`. `file` is the location +/// of a file containing several certificates concatenated together. +pub unsafe fn set_ssl_cert_file

(file: P) -> Result<(), Error> +where + P: IntoCString, +{ + crate::init(); + + unsafe { + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_SET_SSL_CERT_LOCATIONS as libc::c_int, + file.into_c_string()?.as_ptr(), + core::ptr::null::() + )); + } + + Ok(()) +} + +/// Set the SSL certificate-authority location to `path`. `path` is the location +/// of a directory holding several certificates, one per file. +pub unsafe fn set_ssl_cert_dir

(path: P) -> Result<(), Error> +where + P: IntoCString, +{ + crate::init(); + + unsafe { + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_SET_SSL_CERT_LOCATIONS as libc::c_int, + core::ptr::null::(), + path.into_c_string()?.as_ptr() + )); + } + + Ok(()) +} + +/// Get the maximum mmap window size +/// +/// # Safety +/// This function is reading a C global without synchronization, so it is not +/// thread safe, and should only be called before any thread is spawned. +pub unsafe fn get_mwindow_size() -> Result { + crate::init(); + + let mut size = 0; + + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_GET_MWINDOW_SIZE as libc::c_int, + &mut size + )); + + Ok(size) +} + +/// Set the maximum mmap window size +/// +/// # Safety +/// This function is modifying a C global without synchronization, so it is not +/// thread safe, and should only be called before any thread is spawned. +pub unsafe fn set_mwindow_size(size: libc::size_t) -> Result<(), Error> { + crate::init(); + + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_SET_MWINDOW_SIZE as libc::c_int, + size + )); + + Ok(()) +} + +/// Get the maximum memory that will be mapped in total by the library +/// +/// # Safety +/// This function is reading a C global without synchronization, so it is not +/// thread safe, and should only be called before any thread is spawned. +pub unsafe fn get_mwindow_mapped_limit() -> Result { + crate::init(); + + let mut limit = 0; + + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_GET_MWINDOW_MAPPED_LIMIT as libc::c_int, + &mut limit + )); + + Ok(limit) +} + +/// Set the maximum amount of memory that can be mapped at any time +/// by the library. +/// +/// # Safety +/// This function is modifying a C global without synchronization, so it is not +/// thread safe, and should only be called before any thread is spawned. +pub unsafe fn set_mwindow_mapped_limit(limit: libc::size_t) -> Result<(), Error> { + crate::init(); + + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_SET_MWINDOW_MAPPED_LIMIT as libc::c_int, + limit + )); + + Ok(()) +} + +/// Get the maximum number of files that will be mapped at any time by the +/// library. +/// +/// # Safety +/// This function is reading a C global without synchronization, so it is not +/// thread safe, and should only be called before any thread is spawned. +pub unsafe fn get_mwindow_file_limit() -> Result { + crate::init(); + + let mut limit = 0; + + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_GET_MWINDOW_FILE_LIMIT as libc::c_int, + &mut limit + )); + + Ok(limit) +} + +/// Set the maximum number of files that can be mapped at any time +/// by the library. The default (0) is unlimited. +/// +/// # Safety +/// This function is modifying a C global without synchronization, so it is not +/// thread safe, and should only be called before any thread is spawned. +pub unsafe fn set_mwindow_file_limit(limit: libc::size_t) -> Result<(), Error> { + crate::init(); + + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_SET_MWINDOW_FILE_LIMIT as libc::c_int, + limit + )); + + Ok(()) +} + +/// Get server connect timeout in milliseconds +/// +/// # Safety +/// This function is modifying a C global without synchronization, so it is not +/// thread safe, and should only be called before any thread is spawned. +pub unsafe fn get_server_connect_timeout_in_milliseconds() -> Result { + crate::init(); + + let mut server_connect_timeout = 0; + + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_GET_SERVER_CONNECT_TIMEOUT as libc::c_int, + &mut server_connect_timeout + )); + + Ok(server_connect_timeout) +} + +/// Set server connect timeout in milliseconds +/// +/// # Safety +/// This function is modifying a C global without synchronization, so it is not +/// thread safe, and should only be called before any thread is spawned. +pub unsafe fn set_server_connect_timeout_in_milliseconds( + timeout: libc::c_int, +) -> Result<(), Error> { + crate::init(); + + let error = raw::git_libgit2_opts( + raw::GIT_OPT_SET_SERVER_CONNECT_TIMEOUT as libc::c_int, + timeout, + ); + // This function cannot actually fail, but the function has an error return + // for other options that can. + debug_assert!(error >= 0); + + Ok(()) +} + +/// Get server timeout in milliseconds +/// +/// # Safety +/// This function is modifying a C global without synchronization, so it is not +/// thread safe, and should only be called before any thread is spawned. +pub unsafe fn get_server_timeout_in_milliseconds() -> Result { + crate::init(); + + let mut server_timeout = 0; + + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_GET_SERVER_TIMEOUT as libc::c_int, + &mut server_timeout + )); + + Ok(server_timeout) +} + +/// Set server timeout in milliseconds +/// +/// # Safety +/// This function is modifying a C global without synchronization, so it is not +/// thread safe, and should only be called before any thread is spawned. +pub unsafe fn set_server_timeout_in_milliseconds(timeout: libc::c_int) -> Result<(), Error> { + crate::init(); + + let error = raw::git_libgit2_opts( + raw::GIT_OPT_SET_SERVER_TIMEOUT as libc::c_int, + timeout as libc::c_int, + ); + // This function cannot actually fail, but the function has an error return + // for other options that can. + debug_assert!(error >= 0); + + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn smoke() { + strict_hash_verification(false); + } + + #[test] + fn mwindow_size() { + unsafe { + assert!(set_mwindow_size(1024).is_ok()); + assert!(get_mwindow_size().unwrap() == 1024); + } + } + + #[test] + fn mwindow_mapped_limit() { + unsafe { + assert!(set_mwindow_mapped_limit(1024).is_ok()); + assert!(get_mwindow_mapped_limit().unwrap() == 1024); + } + } + + #[test] + fn mwindow_file_limit() { + unsafe { + assert!(set_mwindow_file_limit(1024).is_ok()); + assert!(get_mwindow_file_limit().unwrap() == 1024); + } + } + + #[test] + fn server_connect_timeout() { + unsafe { + assert!(set_server_connect_timeout_in_milliseconds(5000).is_ok()); + assert!(get_server_connect_timeout_in_milliseconds().unwrap() == 5000); + } + } + + #[test] + fn server_timeout() { + unsafe { + assert!(set_server_timeout_in_milliseconds(10_000).is_ok()); + assert!(get_server_timeout_in_milliseconds().unwrap() == 10_000); + } + } + + #[test] + fn cache_size() { + unsafe { + assert!(set_cache_max_size(20 * 1024 * 1024).is_ok()); + assert!(get_cached_memory().is_ok_and(|m| m.1 == 20 * 1024 * 1024)); + } + } +} diff --git a/src/packbuilder.rs b/src/packbuilder.rs index ccb3aaefa5..de47bbce32 100644 --- a/src/packbuilder.rs +++ b/src/packbuilder.rs @@ -1,12 +1,17 @@ +use libc::{c_int, c_uint, c_void, size_t}; use std::marker; +use std::path::Path; use std::ptr; use std::slice; -use libc::{c_int, c_uint, c_void, size_t}; +use std::str; -use {raw, panic, Repository, Error, Oid, Revwalk, Buf}; -use util::Binding; +use crate::odb::{write_pack_progress_cb, OdbPackwriterCb}; +use crate::util::Binding; +use crate::IntoCString; +use crate::{panic, raw, Buf, Error, Oid, Repository, Revwalk}; -/// Stages that are reported by the PackBuilder progress callback. +#[derive(PartialEq, Eq, Clone, Debug, Copy)] +/// Stages that are reported by the `PackBuilder` progress callback. pub enum PackBuilderStage { /// Adding objects to the pack AddingObjects, @@ -14,22 +19,21 @@ pub enum PackBuilderStage { Deltafication, } -pub type ProgressCb<'a> = FnMut(PackBuilderStage, u32, u32) -> bool + 'a; -pub type ForEachCb<'a> = FnMut(&[u8]) -> bool + 'a; +pub type ProgressCb<'a> = dyn FnMut(PackBuilderStage, u32, u32) -> bool + 'a; +pub type ForEachCb<'a> = dyn FnMut(&[u8]) -> bool + 'a; /// A builder for creating a packfile pub struct PackBuilder<'repo> { raw: *mut raw::git_packbuilder, - progress: Option>>>, + _progress: Option>>>, _marker: marker::PhantomData<&'repo Repository>, } impl<'repo> PackBuilder<'repo> { /// Insert a single object. For an optimal pack it's mandatory to insert /// objects in recency order, commits followed by trees and blobs. - pub fn insert_object(&mut self, id: Oid, name: Option<&str>) - -> Result<(), Error> { - let name = try!(::opt_cstr(name)); + pub fn insert_object(&mut self, id: Oid, name: Option<&str>) -> Result<(), Error> { + let name = crate::opt_cstr(name)?; unsafe { try_call!(raw::git_packbuilder_insert(self.raw, id.raw(), name)); } @@ -56,7 +60,7 @@ impl<'repo> PackBuilder<'repo> { /// Insert objects as given by the walk. Those commits and all objects they /// reference will be inserted into the packbuilder. - pub fn insert_walk(&mut self, walk: &mut Revwalk) -> Result<(), Error> { + pub fn insert_walk(&mut self, walk: &mut Revwalk<'_>) -> Result<(), Error> { unsafe { try_call!(raw::git_packbuilder_insert_walk(self.raw, walk.raw())); } @@ -65,13 +69,10 @@ impl<'repo> PackBuilder<'repo> { /// Recursively insert an object and its referenced objects. Insert the /// object as well as any object it references. - pub fn insert_recursive(&mut self, id: Oid, name: Option<&str>) - -> Result<(), Error> { - let name = try!(::opt_cstr(name)); + pub fn insert_recursive(&mut self, id: Oid, name: Option<&str>) -> Result<(), Error> { + let name = crate::opt_cstr(name)?; unsafe { - try_call!(raw::git_packbuilder_insert_recur(self.raw, - id.raw(), - name)); + try_call!(raw::git_packbuilder_insert_recur(self.raw, id.raw(), name)); } Ok(()) } @@ -86,16 +87,40 @@ impl<'repo> PackBuilder<'repo> { Ok(()) } + /// Write the new pack and corresponding index file to path. + /// To set a progress callback, use `set_progress_callback` before calling this method. + pub fn write(&mut self, path: &Path, mode: u32) -> Result<(), Error> { + let path = path.into_c_string()?; + let progress_cb: raw::git_indexer_progress_cb = Some(write_pack_progress_cb); + let progress_payload = Box::new(OdbPackwriterCb { cb: None }); + let progress_payload_ptr = Box::into_raw(progress_payload); + + unsafe { + try_call!(raw::git_packbuilder_write( + self.raw, + path, + mode, + progress_cb, + progress_payload_ptr as *mut _ + )); + } + Ok(()) + } + /// Create the new pack and pass each object to the callback. pub fn foreach(&mut self, mut cb: F) -> Result<(), Error> - where F: FnMut(&[u8]) -> bool + where + F: FnMut(&[u8]) -> bool, { - let mut cb = &mut cb as &mut ForEachCb; + let mut cb = &mut cb as &mut ForEachCb<'_>; let ptr = &mut cb as *mut _; + let foreach: raw::git_packbuilder_foreach_cb = Some(foreach_c); unsafe { - try_call!(raw::git_packbuilder_foreach(self.raw, - foreach_c, - ptr as *mut _)); + try_call!(raw::git_packbuilder_foreach( + self.raw, + foreach, + ptr as *mut _ + )); } Ok(()) } @@ -108,17 +133,20 @@ impl<'repo> PackBuilder<'repo> { /// existing one. See `unset_progress_callback` to remove the current /// progress callback without attaching a new one. pub fn set_progress_callback(&mut self, progress: F) -> Result<(), Error> - where F: FnMut(PackBuilderStage, u32, u32) -> bool + 'repo + where + F: FnMut(PackBuilderStage, u32, u32) -> bool + 'repo, { - let mut progress = Box::new(Box::new(progress) as Box); + let mut progress = Box::new(Box::new(progress) as Box>); let ptr = &mut *progress as *mut _; - let progress_c = Some(progress_c as raw::git_packbuilder_progress); + let progress_c: raw::git_packbuilder_progress = Some(progress_c); unsafe { - try_call!(raw::git_packbuilder_set_callbacks(self.raw, - progress_c, - ptr as *mut _)); + try_call!(raw::git_packbuilder_set_callbacks( + self.raw, + progress_c, + ptr as *mut _ + )); } - self.progress = Some(progress); + self._progress = Some(progress); Ok(()) } @@ -126,36 +154,64 @@ impl<'repo> PackBuilder<'repo> { /// set the progress callback. pub fn unset_progress_callback(&mut self) -> Result<(), Error> { unsafe { - try_call!(raw::git_packbuilder_set_callbacks(self.raw, - None, - ptr::null_mut())); - self.progress = None; + try_call!(raw::git_packbuilder_set_callbacks( + self.raw, + None, + ptr::null_mut() + )); + self._progress = None; } Ok(()) } + /// Set the number of threads to be used. + /// + /// Returns the number of threads to be used. + pub fn set_threads(&mut self, threads: u32) -> u32 { + unsafe { raw::git_packbuilder_set_threads(self.raw, threads) } + } + /// Get the total number of objects the packbuilder will write out. pub fn object_count(&self) -> usize { - unsafe { raw::git_packbuilder_object_count(self.raw) as usize } + unsafe { raw::git_packbuilder_object_count(self.raw) } } /// Get the number of objects the packbuilder has already written out. pub fn written(&self) -> usize { - unsafe { raw::git_packbuilder_written(self.raw) as usize } + unsafe { raw::git_packbuilder_written(self.raw) } } /// Get the packfile's hash. A packfile's name is derived from the sorted /// hashing of all object names. This is only correct after the packfile /// has been written. + #[deprecated = "use `name()` to retrieve the filename"] + #[allow(deprecated)] pub fn hash(&self) -> Option { if self.object_count() == 0 { - unsafe { - Some(Binding::from_raw(raw::git_packbuilder_hash(self.raw))) - } + unsafe { Some(Binding::from_raw(raw::git_packbuilder_hash(self.raw))) } } else { None } } + + /// Get the unique name for the resulting packfile. + /// + /// The packfile's name is derived from the packfile's content. This is only + /// correct after the packfile has been written. + /// + /// Returns `None` if the packfile has not been written or if the name is + /// not valid utf-8. + pub fn name(&self) -> Option<&str> { + self.name_bytes().and_then(|s| str::from_utf8(s).ok()) + } + + /// Get the unique name for the resulting packfile, in bytes. + /// + /// The packfile's name is derived from the packfile's content. This is only + /// correct after the packfile has been written. + pub fn name_bytes(&self) -> Option<&[u8]> { + unsafe { crate::opt_bytes(self, raw::git_packbuilder_name(self.raw)) } + } } impl<'repo> Binding for PackBuilder<'repo> { @@ -163,7 +219,7 @@ impl<'repo> Binding for PackBuilder<'repo> { unsafe fn from_raw(ptr: *mut raw::git_packbuilder) -> PackBuilder<'repo> { PackBuilder { raw: ptr, - progress: None, + _progress: None, _marker: marker::PhantomData, } } @@ -198,15 +254,12 @@ impl Binding for PackBuilderStage { } } -extern fn foreach_c(buf: *const c_void, - size: size_t, - data: *mut c_void) - -> c_int { +extern "C" fn foreach_c(buf: *const c_void, size: size_t, data: *mut c_void) -> c_int { unsafe { let buf = slice::from_raw_parts(buf as *const u8, size as usize); let r = panic::wrap(|| { - let data = data as *mut &mut ForEachCb; + let data = data as *mut &mut ForEachCb<'_>; (*data)(buf) }); if r == Some(true) { @@ -217,16 +270,17 @@ extern fn foreach_c(buf: *const c_void, } } -extern fn progress_c(stage: raw::git_packbuilder_stage_t, - current: c_uint, - total: c_uint, - data: *mut c_void) - -> c_int { +extern "C" fn progress_c( + stage: raw::git_packbuilder_stage_t, + current: c_uint, + total: c_uint, + data: *mut c_void, +) -> c_int { unsafe { let stage = Binding::from_raw(stage); let r = panic::wrap(|| { - let data = data as *mut Box; + let data = data as *mut Box>; (*data)(stage, current, total) }); if r == Some(true) { @@ -239,66 +293,68 @@ extern fn progress_c(stage: raw::git_packbuilder_stage_t, #[cfg(test)] mod tests { - use std::fs::File; - use std::path::Path; - use {Buf, Repository, Oid}; - - fn commit(repo: &Repository) -> (Oid, Oid) { - let mut index = t!(repo.index()); - let root = repo.path().parent().unwrap(); - t!(File::create(&root.join("foo"))); - t!(index.add_path(Path::new("foo"))); - - let tree_id = t!(index.write_tree()); - let tree = t!(repo.find_tree(tree_id)); - let sig = t!(repo.signature()); - let head_id = t!(repo.refname_to_id("HEAD")); - let parent = t!(repo.find_commit(head_id)); - let commit = t!(repo.commit(Some("HEAD"), - &sig, - &sig, - "commit", - &tree, - &[&parent])); - (commit, tree_id) - } + use crate::{Buf, Oid}; + + // hash of a packfile constructed without any objects in it + const EMPTY_PACKFILE_OID: &str = "029d08823bd8a8eab510ad6ac75c823cfd3ed31e"; fn pack_header(len: u8) -> Vec { - [].into_iter() - .chain(b"PACK") // signature - .chain(&[0, 0, 0, 2]) // version number - .chain(&[0, 0, 0, len]) // number of objects - .cloned().collect::>() + [].iter() + .chain(b"PACK") // signature + .chain(&[0, 0, 0, 2]) // version number + .chain(&[0, 0, 0, len]) // number of objects + .cloned() + .collect::>() } fn empty_pack_header() -> Vec { - pack_header(0).iter() - .chain(&[0x02, 0x9d, 0x08, 0x82, 0x3b, // ^ - 0xd8, 0xa8, 0xea, 0xb5, 0x10, // | SHA-1 of the zero - 0xad, 0x6a, 0xc7, 0x5c, 0x82, // | object pack header - 0x3c, 0xfd, 0x3e, 0xd3, 0x1e]) // v - .cloned().collect::>() + pack_header(0) + .iter() + .chain(&[ + 0x02, 0x9d, 0x08, 0x82, 0x3b, // ^ + 0xd8, 0xa8, 0xea, 0xb5, 0x10, // | SHA-1 of the zero + 0xad, 0x6a, 0xc7, 0x5c, 0x82, // | object pack header + 0x3c, 0xfd, 0x3e, 0xd3, 0x1e, + ]) // v + .cloned() + .collect::>() } #[test] fn smoke() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let _builder = t!(repo.packbuilder()); } #[test] fn smoke_write_buf() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let mut builder = t!(repo.packbuilder()); let mut buf = Buf::new(); t!(builder.write_buf(&mut buf)); - assert!(builder.hash().unwrap().is_zero()); + #[allow(deprecated)] + { + assert!(builder.hash().unwrap().is_zero()); + } + assert!(builder.name().is_none()); assert_eq!(&*buf, &*empty_pack_header()); } + #[test] + fn smoke_write() { + let (_td, repo) = crate::test::repo_init(); + let mut builder = t!(repo.packbuilder()); + t!(builder.write(repo.path(), 0)); + #[allow(deprecated)] + { + assert!(builder.hash().unwrap() == Oid::from_str(EMPTY_PACKFILE_OID).unwrap()); + } + assert!(builder.name().unwrap() == EMPTY_PACKFILE_OID); + } + #[test] fn smoke_foreach() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let mut builder = t!(repo.packbuilder()); let mut buf = Vec::::new(); t!(builder.foreach(|bytes| { @@ -310,10 +366,10 @@ mod tests { #[test] fn insert_write_buf() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let mut builder = t!(repo.packbuilder()); let mut buf = Buf::new(); - let (commit, _tree) = commit(&repo); + let (commit, _tree) = crate::test::commit(&repo); t!(builder.insert_object(commit, None)); assert_eq!(builder.object_count(), 1); t!(builder.write_buf(&mut buf)); @@ -323,10 +379,10 @@ mod tests { #[test] fn insert_tree_write_buf() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let mut builder = t!(repo.packbuilder()); let mut buf = Buf::new(); - let (_commit, tree) = commit(&repo); + let (_commit, tree) = crate::test::commit(&repo); // will insert the tree itself and the blob, 2 objects t!(builder.insert_tree(tree)); assert_eq!(builder.object_count(), 2); @@ -337,10 +393,10 @@ mod tests { #[test] fn insert_commit_write_buf() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let mut builder = t!(repo.packbuilder()); let mut buf = Buf::new(); - let (commit, _tree) = commit(&repo); + let (commit, _tree) = crate::test::commit(&repo); // will insert the commit, its tree and the blob, 3 objects t!(builder.insert_commit(commit)); assert_eq!(builder.object_count(), 3); @@ -349,13 +405,48 @@ mod tests { assert_eq!(&buf[0..12], &*pack_header(3)); } + #[test] + fn insert_write() { + let (_td, repo) = crate::test::repo_init(); + let mut builder = t!(repo.packbuilder()); + let (commit, _tree) = crate::test::commit(&repo); + t!(builder.insert_object(commit, None)); + assert_eq!(builder.object_count(), 1); + t!(builder.write(repo.path(), 0)); + t!(repo.find_commit(commit)); + } + + #[test] + fn insert_tree_write() { + let (_td, repo) = crate::test::repo_init(); + let mut builder = t!(repo.packbuilder()); + let (_commit, tree) = crate::test::commit(&repo); + // will insert the tree itself and the blob, 2 objects + t!(builder.insert_tree(tree)); + assert_eq!(builder.object_count(), 2); + t!(builder.write(repo.path(), 0)); + t!(repo.find_tree(tree)); + } + + #[test] + fn insert_commit_write() { + let (_td, repo) = crate::test::repo_init(); + let mut builder = t!(repo.packbuilder()); + let (commit, _tree) = crate::test::commit(&repo); + // will insert the commit, its tree and the blob, 3 objects + t!(builder.insert_commit(commit)); + assert_eq!(builder.object_count(), 3); + t!(builder.write(repo.path(), 0)); + t!(repo.find_commit(commit)); + } + #[test] fn progress_callback() { let mut progress_called = false; { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let mut builder = t!(repo.packbuilder()); - let (commit, _tree) = commit(&repo); + let (commit, _tree) = crate::test::commit(&repo); t!(builder.set_progress_callback(|_, _, _| { progress_called = true; true @@ -370,9 +461,9 @@ mod tests { fn clear_progress_callback() { let mut progress_called = false; { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let mut builder = t!(repo.packbuilder()); - let (commit, _tree) = commit(&repo); + let (commit, _tree) = crate::test::commit(&repo); t!(builder.set_progress_callback(|_, _, _| { progress_called = true; true @@ -383,4 +474,30 @@ mod tests { } assert_eq!(progress_called, false); } + + #[test] + fn progress_callback_with_write() { + let mut progress_called = false; + { + let (_td, repo) = crate::test::repo_init(); + let mut builder = t!(repo.packbuilder()); + let (commit, _tree) = crate::test::commit(&repo); + t!(builder.set_progress_callback(|_, _, _| { + progress_called = true; + true + })); + t!(builder.insert_commit(commit)); + t!(builder.write(repo.path(), 0)); + } + assert_eq!(progress_called, true); + } + + #[test] + fn set_threads() { + let (_td, repo) = crate::test::repo_init(); + let mut builder = t!(repo.packbuilder()); + let used = builder.set_threads(4); + // Will be 1 if not compiled with threading. + assert!(used == 1 || used == 4); + } } diff --git a/src/panic.rs b/src/panic.rs index ad4f3759fa..3e1b208bc7 100644 --- a/src/panic.rs +++ b/src/panic.rs @@ -1,15 +1,14 @@ use std::any::Any; use std::cell::RefCell; -thread_local!(static LAST_ERROR: RefCell>> = { +thread_local!(static LAST_ERROR: RefCell>> = { RefCell::new(None) }); -#[cfg(feature = "unstable")] -pub fn wrap T + ::std::panic::UnwindSafe>(f: F) -> Option { +pub fn wrap T + std::panic::UnwindSafe>(f: F) -> Option { use std::panic; if LAST_ERROR.with(|slot| slot.borrow().is_some()) { - return None + return None; } match panic::catch_unwind(f) { Ok(ret) => Some(ret), @@ -22,31 +21,10 @@ pub fn wrap T + ::std::panic::UnwindSafe>(f: F) -> Option { } } -#[cfg(not(feature = "unstable"))] -pub fn wrap T>(f: F) -> Option { - struct Bomb { - enabled: bool, - } - impl Drop for Bomb { - fn drop(&mut self) { - if !self.enabled { - return - } - panic!("callback has panicked, and continuing to unwind into C \ - is not safe, so aborting the process"); - - } - } - let mut bomb = Bomb { enabled: true }; - let ret = Some(f()); - bomb.enabled = false; - return ret; -} - pub fn check() { let err = LAST_ERROR.with(|slot| slot.borrow_mut().take()); if let Some(err) = err { - panic!(err) + std::panic::resume_unwind(err); } } diff --git a/src/patch.rs b/src/patch.rs new file mode 100644 index 0000000000..d44e87e925 --- /dev/null +++ b/src/patch.rs @@ -0,0 +1,235 @@ +use libc::{c_int, c_void}; +use std::marker::PhantomData; +use std::path::Path; +use std::ptr; + +use crate::diff::{print_cb, LineCb}; +use crate::util::{into_opt_c_string, Binding}; +use crate::{raw, Blob, Buf, Diff, DiffDelta, DiffHunk, DiffLine, DiffOptions, Error}; + +/// A structure representing the text changes in a single diff delta. +/// +/// This is an opaque structure. +pub struct Patch<'buffers> { + raw: *mut raw::git_patch, + buffers: PhantomData<&'buffers ()>, +} + +unsafe impl<'buffers> Send for Patch<'buffers> {} + +impl<'buffers> Binding for Patch<'buffers> { + type Raw = *mut raw::git_patch; + unsafe fn from_raw(raw: Self::Raw) -> Self { + Patch { + raw, + buffers: PhantomData, + } + } + fn raw(&self) -> Self::Raw { + self.raw + } +} + +impl<'buffers> Drop for Patch<'buffers> { + fn drop(&mut self) { + unsafe { raw::git_patch_free(self.raw) } + } +} + +impl<'buffers> Patch<'buffers> { + /// Return a Patch for one file in a Diff. + /// + /// Returns Ok(None) for an unchanged or binary file. + pub fn from_diff(diff: &Diff<'buffers>, idx: usize) -> Result, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_patch_from_diff(&mut ret, diff.raw(), idx)); + Ok(Binding::from_raw_opt(ret)) + } + } + + /// Generate a Patch by diffing two blobs. + pub fn from_blobs( + old_blob: &Blob<'buffers>, + old_path: Option<&Path>, + new_blob: &Blob<'buffers>, + new_path: Option<&Path>, + opts: Option<&mut DiffOptions>, + ) -> Result { + let mut ret = ptr::null_mut(); + let old_path = into_opt_c_string(old_path)?; + let new_path = into_opt_c_string(new_path)?; + unsafe { + try_call!(raw::git_patch_from_blobs( + &mut ret, + old_blob.raw(), + old_path, + new_blob.raw(), + new_path, + opts.map(|s| s.raw()) + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Generate a Patch by diffing a blob and a buffer. + pub fn from_blob_and_buffer( + old_blob: &Blob<'buffers>, + old_path: Option<&Path>, + new_buffer: &'buffers [u8], + new_path: Option<&Path>, + opts: Option<&mut DiffOptions>, + ) -> Result { + let mut ret = ptr::null_mut(); + let old_path = into_opt_c_string(old_path)?; + let new_path = into_opt_c_string(new_path)?; + unsafe { + try_call!(raw::git_patch_from_blob_and_buffer( + &mut ret, + old_blob.raw(), + old_path, + new_buffer.as_ptr() as *const c_void, + new_buffer.len(), + new_path, + opts.map(|s| s.raw()) + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Generate a Patch by diffing two buffers. + pub fn from_buffers( + old_buffer: &'buffers [u8], + old_path: Option<&Path>, + new_buffer: &'buffers [u8], + new_path: Option<&Path>, + opts: Option<&mut DiffOptions>, + ) -> Result { + crate::init(); + let mut ret = ptr::null_mut(); + let old_path = into_opt_c_string(old_path)?; + let new_path = into_opt_c_string(new_path)?; + unsafe { + try_call!(raw::git_patch_from_buffers( + &mut ret, + old_buffer.as_ptr() as *const c_void, + old_buffer.len(), + old_path, + new_buffer.as_ptr() as *const c_void, + new_buffer.len(), + new_path, + opts.map(|s| s.raw()) + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Get the DiffDelta associated with the Patch. + pub fn delta<'a>(&'a self) -> DiffDelta<'a> { + unsafe { Binding::from_raw(raw::git_patch_get_delta(self.raw) as *mut _) } + } + + /// Get the number of hunks in the Patch. + pub fn num_hunks(&self) -> usize { + unsafe { raw::git_patch_num_hunks(self.raw) } + } + + /// Get the number of lines of context, additions, and deletions in the Patch. + pub fn line_stats(&self) -> Result<(usize, usize, usize), Error> { + let mut context = 0; + let mut additions = 0; + let mut deletions = 0; + unsafe { + try_call!(raw::git_patch_line_stats( + &mut context, + &mut additions, + &mut deletions, + self.raw + )); + } + Ok((context, additions, deletions)) + } + + /// Get a DiffHunk and its total line count from the Patch. + pub fn hunk<'a>(&'a self, hunk_idx: usize) -> Result<(DiffHunk<'a>, usize), Error> { + let mut ret = ptr::null(); + let mut lines = 0; + unsafe { + try_call!(raw::git_patch_get_hunk( + &mut ret, &mut lines, self.raw, hunk_idx + )); + Ok((Binding::from_raw(ret), lines)) + } + } + + /// Get the number of lines in a hunk. + pub fn num_lines_in_hunk(&self, hunk_idx: usize) -> Result { + unsafe { Ok(try_call!(raw::git_patch_num_lines_in_hunk(self.raw, hunk_idx)) as usize) } + } + + /// Get a DiffLine from a hunk of the Patch. + pub fn line_in_hunk<'a>( + &'a self, + hunk_idx: usize, + line_of_hunk: usize, + ) -> Result, Error> { + let mut ret = ptr::null(); + unsafe { + try_call!(raw::git_patch_get_line_in_hunk( + &mut ret, + self.raw, + hunk_idx, + line_of_hunk + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Get the size of a Patch's diff data in bytes. + pub fn size( + &self, + include_context: bool, + include_hunk_headers: bool, + include_file_headers: bool, + ) -> usize { + unsafe { + raw::git_patch_size( + self.raw, + include_context as c_int, + include_hunk_headers as c_int, + include_file_headers as c_int, + ) + } + } + + /// Print the Patch to text via a callback. + pub fn print(&mut self, mut line_cb: &mut LineCb<'_>) -> Result<(), Error> { + let ptr = &mut line_cb as *mut _ as *mut c_void; + unsafe { + let cb: raw::git_diff_line_cb = Some(print_cb); + try_call!(raw::git_patch_print(self.raw, cb, ptr)); + Ok(()) + } + } + + /// Get the Patch text as a Buf. + pub fn to_buf(&mut self) -> Result { + let buf = Buf::new(); + unsafe { + try_call!(raw::git_patch_to_buf(buf.raw(), self.raw)); + } + Ok(buf) + } +} + +impl<'buffers> std::fmt::Debug for Patch<'buffers> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut ds = f.debug_struct("Patch"); + ds.field("delta", &self.delta()) + .field("num_hunks", &self.num_hunks()); + if let Ok(line_stats) = &self.line_stats() { + ds.field("line_stats", line_stats); + } + ds.finish() + } +} diff --git a/src/pathspec.rs b/src/pathspec.rs index 888e8eacff..16850dc210 100644 --- a/src/pathspec.rs +++ b/src/pathspec.rs @@ -1,11 +1,12 @@ -use std::iter::IntoIterator; +use libc::size_t; +use std::iter::FusedIterator; use std::marker; use std::ops::Range; use std::path::Path; -use libc::size_t; +use std::ptr; -use {raw, Error, Diff, Tree, PathspecFlags, Index, Repository, DiffDelta, IntoCString}; -use util::Binding; +use crate::util::{path_to_repo_path, Binding}; +use crate::{raw, Diff, DiffDelta, Error, Index, IntoCString, PathspecFlags, Repository, Tree}; /// Structure representing a compiled pathspec used for matching against various /// structures. @@ -40,10 +41,14 @@ pub struct PathspecFailedEntries<'list> { impl Pathspec { /// Creates a new pathspec from a list of specs to match against. pub fn new(specs: I) -> Result - where T: IntoCString, I: IntoIterator { - let (_a, _b, arr) = try!(::util::iter2cstrs(specs)); + where + T: IntoCString, + I: IntoIterator, + { + crate::init(); + let (_a, _b, arr) = crate::util::iter2cstrs_paths(specs)?; unsafe { - let mut ret = 0 as *mut raw::git_pathspec; + let mut ret = ptr::null_mut(); try_call!(raw::git_pathspec_new(&mut ret, &arr)); Ok(Binding::from_raw(ret)) } @@ -55,12 +60,19 @@ impl Pathspec { /// pass `PATHSPEC_FAILURES_ONLY` in the flags) and may also contain the /// list of pathspecs with no match if the `PATHSPEC_FIND_FAILURES` flag is /// specified. - pub fn match_diff(&self, diff: &Diff, flags: PathspecFlags) - -> Result { - let mut ret = 0 as *mut raw::git_pathspec_match_list; + pub fn match_diff( + &self, + diff: &Diff<'_>, + flags: PathspecFlags, + ) -> Result, Error> { + let mut ret = ptr::null_mut(); unsafe { - try_call!(raw::git_pathspec_match_diff(&mut ret, diff.raw(), - flags.bits(), self.raw)); + try_call!(raw::git_pathspec_match_diff( + &mut ret, + diff.raw(), + flags.bits(), + self.raw + )); Ok(Binding::from_raw(ret)) } } @@ -71,12 +83,19 @@ impl Pathspec { /// pass `PATHSPEC_FAILURES_ONLY` in the flags) and may also contain the /// list of pathspecs with no match if the `PATHSPEC_FIND_FAILURES` flag is /// specified. - pub fn match_tree(&self, tree: &Tree, flags: PathspecFlags) - -> Result { - let mut ret = 0 as *mut raw::git_pathspec_match_list; + pub fn match_tree( + &self, + tree: &Tree<'_>, + flags: PathspecFlags, + ) -> Result, Error> { + let mut ret = ptr::null_mut(); unsafe { - try_call!(raw::git_pathspec_match_tree(&mut ret, tree.raw(), - flags.bits(), self.raw)); + try_call!(raw::git_pathspec_match_tree( + &mut ret, + tree.raw(), + flags.bits(), + self.raw + )); Ok(Binding::from_raw(ret)) } } @@ -87,12 +106,19 @@ impl Pathspec { /// pass `PATHSPEC_FAILURES_ONLY` in the flags) and may also contain the /// list of pathspecs with no match if the `PATHSPEC_FIND_FAILURES` flag is /// specified. - pub fn match_index(&self, index: &Index, flags: PathspecFlags) - -> Result { - let mut ret = 0 as *mut raw::git_pathspec_match_list; + pub fn match_index( + &self, + index: &Index, + flags: PathspecFlags, + ) -> Result, Error> { + let mut ret = ptr::null_mut(); unsafe { - try_call!(raw::git_pathspec_match_index(&mut ret, index.raw(), - flags.bits(), self.raw)); + try_call!(raw::git_pathspec_match_index( + &mut ret, + index.raw(), + flags.bits(), + self.raw + )); Ok(Binding::from_raw(ret)) } } @@ -109,12 +135,19 @@ impl Pathspec { /// pass `PATHSPEC_FAILURES_ONLY` in the flags) and may also contain the /// list of pathspecs with no match if the `PATHSPEC_FIND_FAILURES` flag is /// specified. - pub fn match_workdir(&self, repo: &Repository, flags: PathspecFlags) - -> Result { - let mut ret = 0 as *mut raw::git_pathspec_match_list; + pub fn match_workdir( + &self, + repo: &Repository, + flags: PathspecFlags, + ) -> Result, Error> { + let mut ret = ptr::null_mut(); unsafe { - try_call!(raw::git_pathspec_match_workdir(&mut ret, repo.raw(), - flags.bits(), self.raw)); + try_call!(raw::git_pathspec_match_workdir( + &mut ret, + repo.raw(), + flags.bits(), + self.raw + )); Ok(Binding::from_raw(ret)) } } @@ -126,11 +159,8 @@ impl Pathspec { /// explicitly pass flags to control case sensitivity or else this will fall /// back on being case sensitive. pub fn matches_path(&self, path: &Path, flags: PathspecFlags) -> bool { - let path = path.into_c_string().unwrap(); - unsafe { - raw::git_pathspec_matches_path(&*self.raw, flags.bits(), - path.as_ptr()) == 1 - } + let path = path_to_repo_path(path).unwrap(); + unsafe { raw::git_pathspec_matches_path(&*self.raw, flags.bits(), path.as_ptr()) == 1 } } } @@ -138,9 +168,11 @@ impl Binding for Pathspec { type Raw = *mut raw::git_pathspec; unsafe fn from_raw(raw: *mut raw::git_pathspec) -> Pathspec { - Pathspec { raw: raw } + Pathspec { raw } + } + fn raw(&self) -> *mut raw::git_pathspec { + self.raw } - fn raw(&self) -> *mut raw::git_pathspec { self.raw } } impl Drop for Pathspec { @@ -159,10 +191,17 @@ impl<'ps> PathspecMatchList<'ps> { } /// Returns an iterator over the matching filenames in this list. - pub fn entries(&self) -> PathspecEntries { + pub fn entries(&self) -> PathspecEntries<'_> { let n = self.entrycount(); - let n = if n > 0 && self.entry(0).is_none() {0} else {n}; - PathspecEntries { range: 0..n, list: self } + let n = if n > 0 && self.entry(0).is_none() { + 0 + } else { + n + }; + PathspecEntries { + range: 0..n, + list: self, + } } /// Get a matching filename by position. @@ -172,42 +211,54 @@ impl<'ps> PathspecMatchList<'ps> { pub fn entry(&self, i: usize) -> Option<&[u8]> { unsafe { let ptr = raw::git_pathspec_match_list_entry(&*self.raw, i as size_t); - ::opt_bytes(self, ptr) + crate::opt_bytes(self, ptr) } } /// Returns an iterator over the matching diff entries in this list. - pub fn diff_entries(&self) -> PathspecDiffEntries { + pub fn diff_entries(&self) -> PathspecDiffEntries<'_> { let n = self.entrycount(); - let n = if n > 0 && self.diff_entry(0).is_none() {0} else {n}; - PathspecDiffEntries { range: 0..n, list: self } + let n = if n > 0 && self.diff_entry(0).is_none() { + 0 + } else { + n + }; + PathspecDiffEntries { + range: 0..n, + list: self, + } } /// Get a matching diff delta by position. /// /// If the list was not generated from a diff, then the return value will /// always be `None`. - pub fn diff_entry(&self, i: usize) -> Option { + pub fn diff_entry(&self, i: usize) -> Option> { unsafe { - let ptr = raw::git_pathspec_match_list_diff_entry(&*self.raw, - i as size_t); + let ptr = raw::git_pathspec_match_list_diff_entry(&*self.raw, i as size_t); Binding::from_raw_opt(ptr as *mut _) } } /// Returns an iterator over the non-matching entries in this list. - pub fn failed_entries(&self) -> PathspecFailedEntries { + pub fn failed_entries(&self) -> PathspecFailedEntries<'_> { let n = self.failed_entrycount(); - let n = if n > 0 && self.failed_entry(0).is_none() {0} else {n}; - PathspecFailedEntries { range: 0..n, list: self } + let n = if n > 0 && self.failed_entry(0).is_none() { + 0 + } else { + n + }; + PathspecFailedEntries { + range: 0..n, + list: self, + } } /// Get an original pathspec string that had no matches. pub fn failed_entry(&self, i: usize) -> Option<&[u8]> { unsafe { - let ptr = raw::git_pathspec_match_list_failed_entry(&*self.raw, - i as size_t); - ::opt_bytes(self, ptr) + let ptr = raw::git_pathspec_match_list_failed_entry(&*self.raw, i as size_t); + crate::opt_bytes(self, ptr) } } } @@ -215,11 +266,15 @@ impl<'ps> PathspecMatchList<'ps> { impl<'ps> Binding for PathspecMatchList<'ps> { type Raw = *mut raw::git_pathspec_match_list; - unsafe fn from_raw(raw: *mut raw::git_pathspec_match_list) - -> PathspecMatchList<'ps> { - PathspecMatchList { raw: raw, _marker: marker::PhantomData } + unsafe fn from_raw(raw: *mut raw::git_pathspec_match_list) -> PathspecMatchList<'ps> { + PathspecMatchList { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_pathspec_match_list { + self.raw } - fn raw(&self) -> *mut raw::git_pathspec_match_list { self.raw } } impl<'ps> Drop for PathspecMatchList<'ps> { @@ -233,13 +288,16 @@ impl<'list> Iterator for PathspecEntries<'list> { fn next(&mut self) -> Option<&'list [u8]> { self.range.next().and_then(|i| self.list.entry(i)) } - fn size_hint(&self) -> (usize, Option) { self.range.size_hint() } + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() + } } impl<'list> DoubleEndedIterator for PathspecEntries<'list> { fn next_back(&mut self) -> Option<&'list [u8]> { self.range.next_back().and_then(|i| self.list.entry(i)) } } +impl<'list> FusedIterator for PathspecEntries<'list> {} impl<'list> ExactSizeIterator for PathspecEntries<'list> {} impl<'list> Iterator for PathspecDiffEntries<'list> { @@ -247,13 +305,16 @@ impl<'list> Iterator for PathspecDiffEntries<'list> { fn next(&mut self) -> Option> { self.range.next().and_then(|i| self.list.diff_entry(i)) } - fn size_hint(&self) -> (usize, Option) { self.range.size_hint() } + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() + } } impl<'list> DoubleEndedIterator for PathspecDiffEntries<'list> { fn next_back(&mut self) -> Option> { self.range.next_back().and_then(|i| self.list.diff_entry(i)) } } +impl<'list> FusedIterator for PathspecDiffEntries<'list> {} impl<'list> ExactSizeIterator for PathspecDiffEntries<'list> {} impl<'list> Iterator for PathspecFailedEntries<'list> { @@ -261,39 +322,46 @@ impl<'list> Iterator for PathspecFailedEntries<'list> { fn next(&mut self) -> Option<&'list [u8]> { self.range.next().and_then(|i| self.list.failed_entry(i)) } - fn size_hint(&self) -> (usize, Option) { self.range.size_hint() } + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() + } } impl<'list> DoubleEndedIterator for PathspecFailedEntries<'list> { fn next_back(&mut self) -> Option<&'list [u8]> { - self.range.next_back().and_then(|i| self.list.failed_entry(i)) + self.range + .next_back() + .and_then(|i| self.list.failed_entry(i)) } } +impl<'list> FusedIterator for PathspecFailedEntries<'list> {} impl<'list> ExactSizeIterator for PathspecFailedEntries<'list> {} #[cfg(test)] mod tests { - use PATHSPEC_DEFAULT; use super::Pathspec; + use crate::PathspecFlags; use std::fs::File; use std::path::Path; #[test] fn smoke() { let ps = Pathspec::new(["a"].iter()).unwrap(); - assert!(ps.matches_path(Path::new("a"), PATHSPEC_DEFAULT)); - assert!(ps.matches_path(Path::new("a/b"), PATHSPEC_DEFAULT)); - assert!(!ps.matches_path(Path::new("b"), PATHSPEC_DEFAULT)); - assert!(!ps.matches_path(Path::new("ab/c"), PATHSPEC_DEFAULT)); + assert!(ps.matches_path(Path::new("a"), PathspecFlags::DEFAULT)); + assert!(ps.matches_path(Path::new("a/b"), PathspecFlags::DEFAULT)); + assert!(!ps.matches_path(Path::new("b"), PathspecFlags::DEFAULT)); + assert!(!ps.matches_path(Path::new("ab/c"), PathspecFlags::DEFAULT)); - let (td, repo) = ::test::repo_init(); - let list = ps.match_workdir(&repo, PATHSPEC_DEFAULT).unwrap(); + let (td, repo) = crate::test::repo_init(); + let list = ps.match_workdir(&repo, PathspecFlags::DEFAULT).unwrap(); assert_eq!(list.entries().len(), 0); assert_eq!(list.diff_entries().len(), 0); assert_eq!(list.failed_entries().len(), 0); File::create(&td.path().join("a")).unwrap(); - let list = ps.match_workdir(&repo, ::PATHSPEC_FIND_FAILURES).unwrap(); + let list = ps + .match_workdir(&repo, crate::PathspecFlags::FIND_FAILURES) + .unwrap(); assert_eq!(list.entries().len(), 1); assert_eq!(list.entries().next(), Some("a".as_bytes())); } diff --git a/src/proxy_options.rs b/src/proxy_options.rs new file mode 100644 index 0000000000..b19ba3a527 --- /dev/null +++ b/src/proxy_options.rs @@ -0,0 +1,56 @@ +use std::ffi::CString; +use std::marker; +use std::ptr; + +use crate::raw; +use crate::util::Binding; + +/// Options which can be specified to various fetch operations. +#[derive(Default)] +pub struct ProxyOptions<'a> { + url: Option, + proxy_kind: raw::git_proxy_t, + _marker: marker::PhantomData<&'a i32>, +} + +impl<'a> ProxyOptions<'a> { + /// Creates a new set of proxy options ready to be configured. + pub fn new() -> ProxyOptions<'a> { + Default::default() + } + + /// Try to auto-detect the proxy from the git configuration. + /// + /// Note that this will override `url` specified before. + pub fn auto(&mut self) -> &mut Self { + self.proxy_kind = raw::GIT_PROXY_AUTO; + self + } + + /// Specify the exact URL of the proxy to use. + /// + /// Note that this will override `auto` specified before. + pub fn url(https://github.com/&mut%20self,%20url:%20&str) -> &mut Self { + self.proxy_kind = raw::GIT_PROXY_SPECIFIED; + self.url = Some(CString::new(url).unwrap()); + self + } +} + +impl<'a> Binding for ProxyOptions<'a> { + type Raw = raw::git_proxy_options; + unsafe fn from_raw(_raw: raw::git_proxy_options) -> ProxyOptions<'a> { + panic!("can't create proxy from raw options") + } + + fn raw(&self) -> raw::git_proxy_options { + raw::git_proxy_options { + version: raw::GIT_PROXY_OPTIONS_VERSION, + kind: self.proxy_kind, + url: self.url.as_ref().map(|s| s.as_ptr()).unwrap_or(ptr::null()), + credentials: None, + certificate_check: None, + payload: ptr::null_mut(), + } + } +} diff --git a/src/push_update.rs b/src/push_update.rs new file mode 100644 index 0000000000..97bebb1921 --- /dev/null +++ b/src/push_update.rs @@ -0,0 +1,55 @@ +use crate::util::Binding; +use crate::{raw, Oid}; +use std::marker; +use std::str; + +/// Represents an update which will be performed on the remote during push. +pub struct PushUpdate<'a> { + raw: *const raw::git_push_update, + _marker: marker::PhantomData<&'a raw::git_push_update>, +} + +impl<'a> Binding for PushUpdate<'a> { + type Raw = *const raw::git_push_update; + unsafe fn from_raw(raw: *const raw::git_push_update) -> PushUpdate<'a> { + PushUpdate { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> Self::Raw { + self.raw + } +} + +impl PushUpdate<'_> { + /// Returns the source name of the reference as a byte slice. + pub fn src_refname_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, (*self.raw).src_refname).unwrap() } + } + + /// Returns the source name of the reference, or None if it is not valid UTF-8. + pub fn src_refname(&self) -> Option<&str> { + str::from_utf8(self.src_refname_bytes()).ok() + } + + /// Returns the name of the reference to update on the server as a byte slice. + pub fn dst_refname_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, (*self.raw).dst_refname).unwrap() } + } + + /// Returns the name of the reference to update on the server, or None if it is not valid UTF-8. + pub fn dst_refname(&self) -> Option<&str> { + str::from_utf8(self.dst_refname_bytes()).ok() + } + + /// Returns the current target of the reference. + pub fn src(&self) -> Oid { + unsafe { Binding::from_raw(&(*self.raw).src as *const _) } + } + + /// Returns the new target for the reference. + pub fn dst(&self) -> Oid { + unsafe { Binding::from_raw(&(*self.raw).dst as *const _) } + } +} diff --git a/src/rebase.rs b/src/rebase.rs new file mode 100644 index 0000000000..2bf8fe3e8a --- /dev/null +++ b/src/rebase.rs @@ -0,0 +1,441 @@ +use std::ffi::CString; +use std::{marker, mem, ptr, str}; + +use crate::build::CheckoutBuilder; +use crate::util::Binding; +use crate::{raw, Error, Index, MergeOptions, Oid, Signature}; + +/// Rebase options +/// +/// Use to tell the rebase machinery how to operate. +pub struct RebaseOptions<'cb> { + raw: raw::git_rebase_options, + rewrite_notes_ref: Option, + merge_options: Option, + checkout_options: Option>, +} + +impl<'cb> Default for RebaseOptions<'cb> { + fn default() -> Self { + Self::new() + } +} + +impl<'cb> RebaseOptions<'cb> { + /// Creates a new default set of rebase options. + pub fn new() -> RebaseOptions<'cb> { + let mut opts = RebaseOptions { + raw: unsafe { mem::zeroed() }, + rewrite_notes_ref: None, + merge_options: None, + checkout_options: None, + }; + assert_eq!(unsafe { raw::git_rebase_init_options(&mut opts.raw, 1) }, 0); + opts + } + + /// Used by `Repository::rebase`, this will instruct other clients working on this + /// rebase that you want a quiet rebase experience, which they may choose to + /// provide in an application-specific manner. This has no effect upon + /// libgit2 directly, but is provided for interoperability between Git + /// tools. + pub fn quiet(&mut self, quiet: bool) -> &mut RebaseOptions<'cb> { + self.raw.quiet = quiet as i32; + self + } + + /// Used by `Repository::rebase`, this will begin an in-memory rebase, + /// which will allow callers to step through the rebase operations and + /// commit the rebased changes, but will not rewind HEAD or update the + /// repository to be in a rebasing state. This will not interfere with + /// the working directory (if there is one). + pub fn inmemory(&mut self, inmemory: bool) -> &mut RebaseOptions<'cb> { + self.raw.inmemory = inmemory as i32; + self + } + + /// Used by `finish()`, this is the name of the notes reference + /// used to rewrite notes for rebased commits when finishing the rebase; + /// if NULL, the contents of the configuration option `notes.rewriteRef` + /// is examined, unless the configuration option `notes.rewrite.rebase` + /// is set to false. If `notes.rewriteRef` is also NULL, notes will + /// not be rewritten. + pub fn rewrite_notes_ref(&mut self, rewrite_notes_ref: &str) -> &mut RebaseOptions<'cb> { + self.rewrite_notes_ref = Some(CString::new(rewrite_notes_ref).unwrap()); + self + } + + /// Options to control how trees are merged during `next()`. + pub fn merge_options(&mut self, opts: MergeOptions) -> &mut RebaseOptions<'cb> { + self.merge_options = Some(opts); + self + } + + /// Options to control how files are written during `Repository::rebase`, + /// `next()` and `abort()`. Note that a minimum strategy of + /// `GIT_CHECKOUT_SAFE` is defaulted in `init` and `next`, and a minimum + /// strategy of `GIT_CHECKOUT_FORCE` is defaulted in `abort` to match git + /// semantics. + pub fn checkout_options(&mut self, opts: CheckoutBuilder<'cb>) -> &mut RebaseOptions<'cb> { + self.checkout_options = Some(opts); + self + } + + /// Acquire a pointer to the underlying raw options. + pub fn raw(&mut self) -> *const raw::git_rebase_options { + unsafe { + if let Some(opts) = self.merge_options.as_mut().take() { + ptr::copy_nonoverlapping(opts.raw(), &mut self.raw.merge_options, 1); + } + if let Some(opts) = self.checkout_options.as_mut() { + opts.configure(&mut self.raw.checkout_options); + } + self.raw.rewrite_notes_ref = self + .rewrite_notes_ref + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(ptr::null()); + } + &self.raw + } +} + +/// Representation of a rebase +pub struct Rebase<'repo> { + raw: *mut raw::git_rebase, + _marker: marker::PhantomData<&'repo raw::git_rebase>, +} + +impl<'repo> Rebase<'repo> { + /// Gets the count of rebase operations that are to be applied. + pub fn len(&self) -> usize { + unsafe { raw::git_rebase_operation_entrycount(self.raw) } + } + + /// Gets the original `HEAD` ref name for merge rebases. + pub fn orig_head_name(&self) -> Option<&str> { + let name_bytes = + unsafe { crate::opt_bytes(self, raw::git_rebase_orig_head_name(self.raw)) }; + name_bytes.and_then(|s| str::from_utf8(s).ok()) + } + + /// Gets the original HEAD id for merge rebases. + pub fn orig_head_id(&self) -> Option { + unsafe { Oid::from_raw_opt(raw::git_rebase_orig_head_id(self.raw)) } + } + + /// Gets the rebase operation specified by the given index. + pub fn nth(&mut self, n: usize) -> Option> { + unsafe { + let op = raw::git_rebase_operation_byindex(self.raw, n); + if op.is_null() { + None + } else { + Some(RebaseOperation::from_raw(op)) + } + } + } + + /// Gets the index of the rebase operation that is currently being applied. + /// If the first operation has not yet been applied (because you have called + /// `init` but not yet `next`) then this returns None. + pub fn operation_current(&mut self) -> Option { + let cur = unsafe { raw::git_rebase_operation_current(self.raw) }; + if cur == raw::GIT_REBASE_NO_OPERATION { + None + } else { + Some(cur) + } + } + + /// Gets the index produced by the last operation, which is the result of + /// `next()` and which will be committed by the next invocation of + /// `commit()`. This is useful for resolving conflicts in an in-memory + /// rebase before committing them. + /// + /// This is only applicable for in-memory rebases; for rebases within a + /// working directory, the changes were applied to the repository's index. + pub fn inmemory_index(&mut self) -> Result { + let mut idx = ptr::null_mut(); + unsafe { + try_call!(raw::git_rebase_inmemory_index(&mut idx, self.raw)); + Ok(Binding::from_raw(idx)) + } + } + + /// Commits the current patch. You must have resolved any conflicts that + /// were introduced during the patch application from the `git_rebase_next` + /// invocation. To keep the author and message from the original commit leave + /// them as None + pub fn commit( + &mut self, + author: Option<&Signature<'_>>, + committer: &Signature<'_>, + message: Option<&str>, + ) -> Result { + let mut id: raw::git_oid = unsafe { mem::zeroed() }; + let message = crate::opt_cstr(message)?; + unsafe { + try_call!(raw::git_rebase_commit( + &mut id, + self.raw, + author.map(|a| a.raw()), + committer.raw(), + ptr::null(), + message + )); + Ok(Binding::from_raw(&id as *const _)) + } + } + + /// Aborts a rebase that is currently in progress, resetting the repository + /// and working directory to their state before rebase began. + pub fn abort(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_rebase_abort(self.raw)); + } + + Ok(()) + } + + /// Finishes a rebase that is currently in progress once all patches have + /// been applied. + pub fn finish(&mut self, signature: Option<&Signature<'_>>) -> Result<(), Error> { + unsafe { + try_call!(raw::git_rebase_finish(self.raw, signature.map(|s| s.raw()))); + } + + Ok(()) + } +} + +impl<'rebase> Iterator for Rebase<'rebase> { + type Item = Result, Error>; + + /// Performs the next rebase operation and returns the information about it. + /// If the operation is one that applies a patch (which is any operation except + /// GitRebaseOperation::Exec) then the patch will be applied and the index and + /// working directory will be updated with the changes. If there are conflicts, + /// you will need to address those before committing the changes. + fn next(&mut self) -> Option, Error>> { + let mut out = ptr::null_mut(); + unsafe { + try_call_iter!(raw::git_rebase_next(&mut out, self.raw)); + Some(Ok(RebaseOperation::from_raw(out))) + } + } +} + +impl<'repo> Binding for Rebase<'repo> { + type Raw = *mut raw::git_rebase; + unsafe fn from_raw(raw: *mut raw::git_rebase) -> Rebase<'repo> { + Rebase { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_rebase { + self.raw + } +} + +impl<'repo> Drop for Rebase<'repo> { + fn drop(&mut self) { + unsafe { raw::git_rebase_free(self.raw) } + } +} + +/// A rebase operation +/// +/// Describes a single instruction/operation to be performed during the +/// rebase. +#[derive(Debug, PartialEq)] +pub enum RebaseOperationType { + /// The given commit is to be cherry-picked. The client should commit the + /// changes and continue if there are no conflicts. + Pick, + + /// The given commit is to be cherry-picked, but the client should prompt + /// the user to provide an updated commit message. + Reword, + + /// The given commit is to be cherry-picked, but the client should stop to + /// allow the user to edit the changes before committing them. + Edit, + + /// The given commit is to be squashed into the previous commit. The commit + /// message will be merged with the previous message. + Squash, + + /// The given commit is to be squashed into the previous commit. The commit + /// message from this commit will be discarded. + Fixup, + + /// No commit will be cherry-picked. The client should run the given command + /// and (if successful) continue. + Exec, +} + +impl RebaseOperationType { + /// Convert from the int into an enum. Returns None if invalid. + pub fn from_raw(raw: raw::git_rebase_operation_t) -> Option { + match raw { + raw::GIT_REBASE_OPERATION_PICK => Some(RebaseOperationType::Pick), + raw::GIT_REBASE_OPERATION_REWORD => Some(RebaseOperationType::Reword), + raw::GIT_REBASE_OPERATION_EDIT => Some(RebaseOperationType::Edit), + raw::GIT_REBASE_OPERATION_SQUASH => Some(RebaseOperationType::Squash), + raw::GIT_REBASE_OPERATION_FIXUP => Some(RebaseOperationType::Fixup), + raw::GIT_REBASE_OPERATION_EXEC => Some(RebaseOperationType::Exec), + _ => None, + } + } +} + +/// A rebase operation +/// +/// Describes a single instruction/operation to be performed during the +/// rebase. +#[derive(Debug)] +pub struct RebaseOperation<'rebase> { + raw: *const raw::git_rebase_operation, + _marker: marker::PhantomData>, +} + +impl<'rebase> RebaseOperation<'rebase> { + /// The type of rebase operation + pub fn kind(&self) -> Option { + unsafe { RebaseOperationType::from_raw((*self.raw).kind) } + } + + /// The commit ID being cherry-picked. This will be populated for all + /// operations except those of type `GIT_REBASE_OPERATION_EXEC`. + pub fn id(&self) -> Oid { + unsafe { Binding::from_raw(&(*self.raw).id as *const _) } + } + + ///The executable the user has requested be run. This will only + /// be populated for operations of type RebaseOperationType::Exec + pub fn exec(&self) -> Option<&str> { + unsafe { str::from_utf8(crate::opt_bytes(self, (*self.raw).exec).unwrap()).ok() } + } +} + +impl<'rebase> Binding for RebaseOperation<'rebase> { + type Raw = *const raw::git_rebase_operation; + unsafe fn from_raw(raw: *const raw::git_rebase_operation) -> RebaseOperation<'rebase> { + RebaseOperation { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *const raw::git_rebase_operation { + self.raw + } +} + +#[cfg(test)] +mod tests { + use crate::{RebaseOperationType, RebaseOptions, Signature}; + use std::{fs, path}; + + #[test] + fn smoke() { + let (_td, repo) = crate::test::repo_init(); + let head_target = repo.head().unwrap().target().unwrap(); + let tip = repo.find_commit(head_target).unwrap(); + let sig = tip.author(); + let tree = tip.tree().unwrap(); + + // We just want to see the iteration work so we can create commits with + // no changes + let c1 = repo + .commit(Some("refs/heads/main"), &sig, &sig, "foo", &tree, &[&tip]) + .unwrap(); + let c1 = repo.find_commit(c1).unwrap(); + let c2 = repo + .commit(Some("refs/heads/main"), &sig, &sig, "foo", &tree, &[&c1]) + .unwrap(); + + let head = repo.find_reference("refs/heads/main").unwrap(); + let branch = repo.reference_to_annotated_commit(&head).unwrap(); + let upstream = repo.find_annotated_commit(tip.id()).unwrap(); + let mut rebase = repo + .rebase(Some(&branch), Some(&upstream), None, None) + .unwrap(); + + assert_eq!(Some("refs/heads/main"), rebase.orig_head_name()); + assert_eq!(Some(c2), rebase.orig_head_id()); + + assert_eq!(rebase.len(), 2); + { + let op = rebase.next().unwrap().unwrap(); + assert_eq!(op.kind(), Some(RebaseOperationType::Pick)); + assert_eq!(op.id(), c1.id()); + } + { + let op = rebase.next().unwrap().unwrap(); + assert_eq!(op.kind(), Some(RebaseOperationType::Pick)); + assert_eq!(op.id(), c2); + } + { + let op = rebase.next(); + assert!(op.is_none()); + } + } + + #[test] + fn keeping_original_author_msg() { + let (td, repo) = crate::test::repo_init(); + let head_target = repo.head().unwrap().target().unwrap(); + let tip = repo.find_commit(head_target).unwrap(); + let sig = Signature::now("testname", "testemail").unwrap(); + let mut index = repo.index().unwrap(); + + fs::File::create(td.path().join("file_a")).unwrap(); + index.add_path(path::Path::new("file_a")).unwrap(); + index.write().unwrap(); + let tree_id_a = index.write_tree().unwrap(); + let tree_a = repo.find_tree(tree_id_a).unwrap(); + let c1 = repo + .commit(Some("refs/heads/main"), &sig, &sig, "A", &tree_a, &[&tip]) + .unwrap(); + let c1 = repo.find_commit(c1).unwrap(); + + fs::File::create(td.path().join("file_b")).unwrap(); + index.add_path(path::Path::new("file_b")).unwrap(); + index.write().unwrap(); + let tree_id_b = index.write_tree().unwrap(); + let tree_b = repo.find_tree(tree_id_b).unwrap(); + let c2 = repo + .commit(Some("refs/heads/main"), &sig, &sig, "B", &tree_b, &[&c1]) + .unwrap(); + + let branch = repo.find_annotated_commit(c2).unwrap(); + let upstream = repo.find_annotated_commit(tip.id()).unwrap(); + let mut opts: RebaseOptions<'_> = Default::default(); + let mut rebase = repo + .rebase(Some(&branch), Some(&upstream), None, Some(&mut opts)) + .unwrap(); + + assert_eq!(rebase.len(), 2); + + { + rebase.next().unwrap().unwrap(); + let id = rebase.commit(None, &sig, None).unwrap(); + let commit = repo.find_commit(id).unwrap(); + assert_eq!(commit.message(), Some("A")); + assert_eq!(commit.author().name(), Some("testname")); + assert_eq!(commit.author().email(), Some("testemail")); + } + + { + rebase.next().unwrap().unwrap(); + let id = rebase.commit(None, &sig, None).unwrap(); + let commit = repo.find_commit(id).unwrap(); + assert_eq!(commit.message(), Some("B")); + assert_eq!(commit.author().name(), Some("testname")); + assert_eq!(commit.author().email(), Some("testemail")); + } + rebase.finish(None).unwrap(); + } +} diff --git a/src/reference.rs b/src/reference.rs index e7af200df5..0af845d7c5 100644 --- a/src/reference.rs +++ b/src/reference.rs @@ -2,13 +2,27 @@ use std::cmp::Ordering; use std::ffi::CString; use std::marker; use std::mem; +use std::ptr; use std::str; -use libc; -use {raw, Error, Oid, Repository, Object, ObjectType}; -use util::Binding; - -struct Refdb<'repo>(&'repo Repository); +use crate::object::CastOrPanic; +use crate::util::{c_cmp_to_ordering, Binding}; +use crate::{ + call, raw, Blob, Commit, Error, Object, ObjectType, Oid, ReferenceFormat, ReferenceType, + Repository, Tag, Tree, +}; + +// Not in the public header files (yet?), but a hard limit used by libgit2 +// internally +const GIT_REFNAME_MAX: usize = 1024; + +/// This is used to logically indicate that a [`raw::git_reference`] or +/// [`raw::git_reference_iterator`] holds a reference to [`raw::git_refdb`]. +/// It is not necessary to have a wrapper like this in the +/// [`marker::PhantomData`], since all that matters is that it is tied to the +/// lifetime of the [`Repository`], but this helps distinguish the actual +/// references involved. +struct Refdb<'repo>(#[allow(dead_code)] &'repo Repository); /// A structure to represent a git [reference][1]. /// @@ -25,20 +39,138 @@ pub struct References<'repo> { } /// An iterator over the names of references in a repository. -pub struct ReferenceNames<'repo> { - inner: References<'repo>, +pub struct ReferenceNames<'repo, 'references> { + inner: &'references mut References<'repo>, } impl<'repo> Reference<'repo> { /// Ensure the reference name is well-formed. + /// + /// Validation is performed as if [`ReferenceFormat::ALLOW_ONELEVEL`] + /// was given to [`Reference::normalize_name`]. No normalization is + /// performed, however. + /// + /// ```rust + /// use git2::Reference; + /// + /// assert!(Reference::is_valid_name("HEAD")); + /// assert!(Reference::is_valid_name("refs/heads/main")); + /// + /// // But: + /// assert!(!Reference::is_valid_name("main")); + /// assert!(!Reference::is_valid_name("refs/heads/*")); + /// assert!(!Reference::is_valid_name("foo//bar")); + /// ``` + /// + /// [`ReferenceFormat::ALLOW_ONELEVEL`]: + /// struct.ReferenceFormat#associatedconstant.ALLOW_ONELEVEL + /// [`Reference::normalize_name`]: struct.Reference#method.normalize_name pub fn is_valid_name(refname: &str) -> bool { - ::init(); + crate::init(); let refname = CString::new(refname).unwrap(); - unsafe { raw::git_reference_is_valid_name(refname.as_ptr()) == 1 } + let mut valid: libc::c_int = 0; + unsafe { + call::c_try(raw::git_reference_name_is_valid( + &mut valid, + refname.as_ptr(), + )) + .unwrap(); + } + valid == 1 + } + + /// Normalize reference name and check validity. + /// + /// This will normalize the reference name by collapsing runs of adjacent + /// slashes between name components into a single slash. It also validates + /// the name according to the following rules: + /// + /// 1. If [`ReferenceFormat::ALLOW_ONELEVEL`] is given, the name may + /// contain only capital letters and underscores, and must begin and end + /// with a letter. (e.g. "HEAD", "ORIG_HEAD"). + /// 2. The flag [`ReferenceFormat::REFSPEC_SHORTHAND`] has an effect + /// only when combined with [`ReferenceFormat::ALLOW_ONELEVEL`]. If + /// it is given, "shorthand" branch names (i.e. those not prefixed by + /// `refs/`, but consisting of a single word without `/` separators) + /// become valid. For example, "main" would be accepted. + /// 3. If [`ReferenceFormat::REFSPEC_PATTERN`] is given, the name may + /// contain a single `*` in place of a full pathname component (e.g. + /// `foo/*/bar`, `foo/bar*`). + /// 4. Names prefixed with "refs/" can be almost anything. You must avoid + /// the characters '~', '^', ':', '\\', '?', '[', and '*', and the + /// sequences ".." and "@{" which have special meaning to revparse. + /// + /// If the reference passes validation, it is returned in normalized form, + /// otherwise an [`Error`] with [`ErrorCode::InvalidSpec`] is returned. + /// + /// ```rust + /// use git2::{Reference, ReferenceFormat}; + /// + /// assert_eq!( + /// Reference::normalize_name( + /// "foo//bar", + /// ReferenceFormat::NORMAL + /// ) + /// .unwrap(), + /// "foo/bar".to_owned() + /// ); + /// + /// assert_eq!( + /// Reference::normalize_name( + /// "HEAD", + /// ReferenceFormat::ALLOW_ONELEVEL + /// ) + /// .unwrap(), + /// "HEAD".to_owned() + /// ); + /// + /// assert_eq!( + /// Reference::normalize_name( + /// "refs/heads/*", + /// ReferenceFormat::REFSPEC_PATTERN + /// ) + /// .unwrap(), + /// "refs/heads/*".to_owned() + /// ); + /// + /// assert_eq!( + /// Reference::normalize_name( + /// "main", + /// ReferenceFormat::ALLOW_ONELEVEL | ReferenceFormat::REFSPEC_SHORTHAND + /// ) + /// .unwrap(), + /// "main".to_owned() + /// ); + /// ``` + /// + /// [`ReferenceFormat::ALLOW_ONELEVEL`]: + /// struct.ReferenceFormat#associatedconstant.ALLOW_ONELEVEL + /// [`ReferenceFormat::REFSPEC_SHORTHAND`]: + /// struct.ReferenceFormat#associatedconstant.REFSPEC_SHORTHAND + /// [`ReferenceFormat::REFSPEC_PATTERN`]: + /// struct.ReferenceFormat#associatedconstant.REFSPEC_PATTERN + /// [`Error`]: struct.Error + /// [`ErrorCode::InvalidSpec`]: enum.ErrorCode#variant.InvalidSpec + pub fn normalize_name(refname: &str, flags: ReferenceFormat) -> Result { + crate::init(); + let mut dst = [0u8; GIT_REFNAME_MAX]; + let refname = CString::new(refname)?; + unsafe { + try_call!(raw::git_reference_normalize_name( + dst.as_mut_ptr() as *mut libc::c_char, + dst.len() as libc::size_t, + refname, + flags.bits() + )); + let s = &dst[..dst.iter().position(|&a| a == 0).unwrap()]; + Ok(str::from_utf8(s).unwrap().to_owned()) + } } /// Get access to the underlying raw pointer. - pub fn raw(&self) -> *mut raw::git_reference { self.raw } + pub fn raw(&self) -> *mut raw::git_reference { + self.raw + } /// Delete an existing reference. /// @@ -48,7 +180,9 @@ impl<'repo> Reference<'repo> { /// This function will return an error if the reference has changed from the /// time it was looked up. pub fn delete(&mut self) -> Result<(), Error> { - unsafe { try_call!(raw::git_reference_delete(self.raw)); } + unsafe { + try_call!(raw::git_reference_delete(self.raw)); + } Ok(()) } @@ -72,14 +206,23 @@ impl<'repo> Reference<'repo> { unsafe { raw::git_reference_is_tag(&*self.raw) == 1 } } + /// Get the reference type of a reference. + /// + /// If the type is unknown, then `None` is returned. + pub fn kind(&self) -> Option { + ReferenceType::from_raw(unsafe { raw::git_reference_type(&*self.raw) }) + } + /// Get the full name of a reference. /// /// Returns `None` if the name is not valid utf-8. - pub fn name(&self) -> Option<&str> { str::from_utf8(self.name_bytes()).ok() } + pub fn name(&self) -> Option<&str> { + str::from_utf8(self.name_bytes()).ok() + } /// Get the full name of a reference. pub fn name_bytes(&self) -> &[u8] { - unsafe { ::opt_bytes(self, raw::git_reference_name(&*self.raw)).unwrap() } + unsafe { crate::opt_bytes(self, raw::git_reference_name(&*self.raw)).unwrap() } } /// Get the full shorthand of a reference. @@ -94,9 +237,7 @@ impl<'repo> Reference<'repo> { /// Get the full shorthand of a reference. pub fn shorthand_bytes(&self) -> &[u8] { - unsafe { - ::opt_bytes(self, raw::git_reference_shorthand(&*self.raw)).unwrap() - } + unsafe { crate::opt_bytes(self, raw::git_reference_shorthand(&*self.raw)).unwrap() } } /// Get the OID pointed to by a direct reference. @@ -104,9 +245,7 @@ impl<'repo> Reference<'repo> { /// Only available if the reference is direct (i.e. an object id reference, /// not a symbolic one). pub fn target(&self) -> Option { - unsafe { - Binding::from_raw_opt(raw::git_reference_target(&*self.raw)) - } + unsafe { Binding::from_raw_opt(raw::git_reference_target(&*self.raw)) } } /// Return the peeled OID target of this reference. @@ -114,9 +253,7 @@ impl<'repo> Reference<'repo> { /// This peeled OID only applies to direct references that point to a hard /// Tag object: it is the result of peeling such Tag. pub fn target_peel(&self) -> Option { - unsafe { - Binding::from_raw_opt(raw::git_reference_target_peel(&*self.raw)) - } + unsafe { Binding::from_raw_opt(raw::git_reference_target_peel(&*self.raw)) } } /// Get full name to the reference pointed to by a symbolic reference. @@ -124,14 +261,15 @@ impl<'repo> Reference<'repo> { /// May return `None` if the reference is either not symbolic or not a /// valid utf-8 string. pub fn symbolic_target(&self) -> Option<&str> { - self.symbolic_target_bytes().and_then(|s| str::from_utf8(s).ok()) + self.symbolic_target_bytes() + .and_then(|s| str::from_utf8(s).ok()) } /// Get full name to the reference pointed to by a symbolic reference. /// /// Only available if the reference is symbolic. pub fn symbolic_target_bytes(&self) -> Option<&[u8]> { - unsafe { ::opt_bytes(self, raw::git_reference_symbolic_target(&*self.raw)) } + unsafe { crate::opt_bytes(self, raw::git_reference_symbolic_target(&*self.raw)) } } /// Resolve a symbolic reference to a direct reference. @@ -142,7 +280,7 @@ impl<'repo> Reference<'repo> { /// If a direct reference is passed as an argument, a copy of that /// reference is returned. pub fn resolve(&self) -> Result, Error> { - let mut raw = 0 as *mut raw::git_reference; + let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_reference_resolve(&mut raw, &*self.raw)); Ok(Binding::from_raw(raw)) @@ -154,27 +292,66 @@ impl<'repo> Reference<'repo> { /// This method recursively peels the reference until it reaches /// an object of the specified type. pub fn peel(&self, kind: ObjectType) -> Result, Error> { - let mut raw = 0 as *mut raw::git_object; + let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_reference_peel(&mut raw, self.raw, kind)); Ok(Binding::from_raw(raw)) } } + /// Peel a reference to a blob + /// + /// This method recursively peels the reference until it reaches + /// a blob. + pub fn peel_to_blob(&self) -> Result, Error> { + Ok(self.peel(ObjectType::Blob)?.cast_or_panic(ObjectType::Blob)) + } + + /// Peel a reference to a commit + /// + /// This method recursively peels the reference until it reaches + /// a commit. + pub fn peel_to_commit(&self) -> Result, Error> { + Ok(self + .peel(ObjectType::Commit)? + .cast_or_panic(ObjectType::Commit)) + } + + /// Peel a reference to a tree + /// + /// This method recursively peels the reference until it reaches + /// a tree. + pub fn peel_to_tree(&self) -> Result, Error> { + Ok(self.peel(ObjectType::Tree)?.cast_or_panic(ObjectType::Tree)) + } + + /// Peel a reference to a tag + /// + /// This method recursively peels the reference until it reaches + /// a tag. + pub fn peel_to_tag(&self) -> Result, Error> { + Ok(self.peel(ObjectType::Tag)?.cast_or_panic(ObjectType::Tag)) + } + /// Rename an existing reference. /// /// This method works for both direct and symbolic references. /// /// If the force flag is not enabled, and there's already a reference with /// the given name, the renaming will fail. - pub fn rename(&mut self, new_name: &str, force: bool, - msg: &str) -> Result, Error> { - let mut raw = 0 as *mut raw::git_reference; - let new_name = try!(CString::new(new_name)); - let msg = try!(CString::new(msg)); + pub fn rename( + &mut self, + new_name: &str, + force: bool, + msg: &str, + ) -> Result, Error> { + let mut raw = ptr::null_mut(); + let new_name = CString::new(new_name)?; + let msg = CString::new(msg)?; unsafe { - try_call!(raw::git_reference_rename(&mut raw, self.raw, new_name, - force, msg)); + try_call!(raw::git_reference_rename( + &mut raw, self.raw, new_name, force, msg + )); Ok(Binding::from_raw(raw)) } } @@ -185,17 +362,48 @@ impl<'repo> Reference<'repo> { /// /// The new reference will be written to disk, overwriting the given /// reference. - pub fn set_target(&mut self, id: Oid, reflog_msg: &str) - -> Result, Error> { - let mut raw = 0 as *mut raw::git_reference; - let msg = try!(CString::new(reflog_msg)); + pub fn set_target(&mut self, id: Oid, reflog_msg: &str) -> Result, Error> { + let mut raw = ptr::null_mut(); + let msg = CString::new(reflog_msg)?; unsafe { - try_call!(raw::git_reference_set_target(&mut raw, self.raw, - id.raw(), msg)); + try_call!(raw::git_reference_set_target( + &mut raw, + self.raw, + id.raw(), + msg + )); Ok(Binding::from_raw(raw)) } } + /// Create a new reference with the same name as the given reference but a + /// different symbolic target. The reference must be a symbolic reference, + /// otherwise this will fail. + /// + /// The new reference will be written to disk, overwriting the given + /// reference. + /// + /// The target name will be checked for validity. See + /// [`Repository::reference_symbolic`] for rules about valid names. + /// + /// The message for the reflog will be ignored if the reference does not + /// belong in the standard set (HEAD, branches and remote-tracking + /// branches) and it does not have a reflog. + pub fn symbolic_set_target( + &mut self, + target: &str, + reflog_msg: &str, + ) -> Result, Error> { + let mut raw = ptr::null_mut(); + let target = CString::new(target)?; + let msg = CString::new(reflog_msg)?; + unsafe { + try_call!(raw::git_reference_symbolic_set_target( + &mut raw, self.raw, target, msg + )); + Ok(Binding::from_raw(raw)) + } + } } impl<'repo> PartialOrd for Reference<'repo> { @@ -206,11 +414,7 @@ impl<'repo> PartialOrd for Reference<'repo> { impl<'repo> Ord for Reference<'repo> { fn cmp(&self, other: &Reference<'repo>) -> Ordering { - match unsafe { raw::git_reference_cmp(&*self.raw, &*other.raw) } { - 0 => Ordering::Equal, - n if n < 0 => Ordering::Less, - _ => Ordering::Greater, - } + c_cmp_to_ordering(unsafe { raw::git_reference_cmp(&*self.raw, &*other.raw) }) } } @@ -225,9 +429,14 @@ impl<'repo> Eq for Reference<'repo> {} impl<'repo> Binding for Reference<'repo> { type Raw = *mut raw::git_reference; unsafe fn from_raw(raw: *mut raw::git_reference) -> Reference<'repo> { - Reference { raw: raw, _marker: marker::PhantomData } + Reference { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_reference { + self.raw } - fn raw(&self) -> *mut raw::git_reference { self.raw } } impl<'repo> Drop for Reference<'repo> { @@ -244,24 +453,28 @@ impl<'repo> References<'repo> { /// the references themselves don't have to be allocated and deallocated. /// /// The returned iterator will yield strings as opposed to a `Reference`. - pub fn names(self) -> ReferenceNames<'repo> { + pub fn names<'a>(&'a mut self) -> ReferenceNames<'repo, 'a> { ReferenceNames { inner: self } } } impl<'repo> Binding for References<'repo> { type Raw = *mut raw::git_reference_iterator; - unsafe fn from_raw(raw: *mut raw::git_reference_iterator) - -> References<'repo> { - References { raw: raw, _marker: marker::PhantomData } + unsafe fn from_raw(raw: *mut raw::git_reference_iterator) -> References<'repo> { + References { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_reference_iterator { + self.raw } - fn raw(&self) -> *mut raw::git_reference_iterator { self.raw } } impl<'repo> Iterator for References<'repo> { type Item = Result, Error>; fn next(&mut self) -> Option, Error>> { - let mut out = 0 as *mut raw::git_reference; + let mut out = ptr::null_mut(); unsafe { try_call_iter!(raw::git_reference_next(&mut out, self.raw)); Some(Ok(Binding::from_raw(out))) @@ -275,56 +488,72 @@ impl<'repo> Drop for References<'repo> { } } -impl<'repo> Iterator for ReferenceNames<'repo> { - type Item = Result<&'repo str, Error>; - fn next(&mut self) -> Option> { - let mut out = 0 as *const libc::c_char; +impl<'repo, 'references> Iterator for ReferenceNames<'repo, 'references> { + type Item = Result<&'references str, Error>; + fn next(&mut self) -> Option> { + let mut out = ptr::null(); unsafe { - try_call_iter!(raw::git_reference_next_name(&mut out, - self.inner.raw)); - let bytes = ::opt_bytes(self, out).unwrap(); + try_call_iter!(raw::git_reference_next_name(&mut out, self.inner.raw)); + let bytes = crate::opt_bytes(self, out).unwrap(); let s = str::from_utf8(bytes).unwrap(); - Some(Ok(mem::transmute::<&str, &'repo str>(s))) + Some(Ok(mem::transmute::<&str, &'references str>(s))) } } } #[cfg(test)] mod tests { - use {Reference, ObjectType}; + use crate::{ObjectType, Reference, ReferenceType}; #[test] - fn smoke() { + fn is_valid_name() { assert!(Reference::is_valid_name("refs/foo")); assert!(!Reference::is_valid_name("foo")); + assert!(Reference::is_valid_name("FOO_BAR")); + + assert!(!Reference::is_valid_name("foo")); + assert!(!Reference::is_valid_name("_FOO_BAR")); } #[test] - fn smoke2() { - let (_td, repo) = ::test::repo_init(); + #[should_panic] + fn is_valid_name_for_invalid_ref() { + Reference::is_valid_name("ab\012"); + } + + #[test] + fn smoke() { + let (_td, repo) = crate::test::repo_init(); let mut head = repo.head().unwrap(); assert!(head.is_branch()); assert!(!head.is_remote()); assert!(!head.is_tag()); assert!(!head.is_note()); + // HEAD is a symbolic reference but git_repository_head resolves it + // so it is a GIT_REFERENCE_DIRECT. + assert_eq!(head.kind().unwrap(), ReferenceType::Direct); + assert!(head == repo.head().unwrap()); - assert_eq!(head.name(), Some("refs/heads/master")); + assert_eq!(head.name(), Some("refs/heads/main")); - assert!(head == repo.find_reference("refs/heads/master").unwrap()); - assert_eq!(repo.refname_to_id("refs/heads/master").unwrap(), - head.target().unwrap()); + assert!(head == repo.find_reference("refs/heads/main").unwrap()); + assert_eq!( + repo.refname_to_id("refs/heads/main").unwrap(), + head.target().unwrap() + ); assert!(head.symbolic_target().is_none()); assert!(head.target_peel().is_none()); - assert_eq!(head.shorthand(), Some("master")); + assert_eq!(head.shorthand(), Some("main")); assert!(head.resolve().unwrap() == head); - let mut tag1 = repo.reference("refs/tags/tag1", - head.target().unwrap(), - false, "test").unwrap(); + let mut tag1 = repo + .reference("refs/tags/tag1", head.target().unwrap(), false, "test") + .unwrap(); assert!(tag1.is_tag()); + assert_eq!(tag1.kind().unwrap(), ReferenceType::Direct); let peeled_commit = tag1.peel(ObjectType::Commit).unwrap(); assert_eq!(ObjectType::Commit, peeled_commit.kind().unwrap()); @@ -332,16 +561,26 @@ mod tests { tag1.delete().unwrap(); - let mut sym1 = repo.reference_symbolic("refs/tags/tag1", - "refs/heads/master", false, - "test").unwrap(); + let mut sym1 = repo + .reference_symbolic("refs/tags/tag1", "refs/heads/main", false, "test") + .unwrap(); + assert_eq!(sym1.kind().unwrap(), ReferenceType::Symbolic); + let mut sym2 = repo + .reference_symbolic("refs/tags/tag2", "refs/heads/main", false, "test") + .unwrap() + .symbolic_set_target("refs/tags/tag1", "test") + .unwrap(); + assert_eq!(sym2.kind().unwrap(), ReferenceType::Symbolic); + assert_eq!(sym2.symbolic_target().unwrap(), "refs/tags/tag1"); + sym2.delete().unwrap(); sym1.delete().unwrap(); { assert!(repo.references().unwrap().count() == 1); assert!(repo.references().unwrap().next().unwrap().unwrap() == head); - let mut names = repo.references().unwrap().names(); - assert_eq!(names.next().unwrap().unwrap(), "refs/heads/master"); + let mut names = repo.references().unwrap(); + let mut names = names.names(); + assert_eq!(names.next().unwrap().unwrap(), "refs/heads/main"); assert!(names.next().is_none()); assert!(repo.references_glob("foo").unwrap().count() == 0); assert!(repo.references_glob("refs/heads/*").unwrap().count() == 1); @@ -349,6 +588,5 @@ mod tests { let mut head = head.rename("refs/foo", true, "test").unwrap(); head.delete().unwrap(); - } } diff --git a/src/reflog.rs b/src/reflog.rs index 69e40f8e30..bbd2140ab2 100644 --- a/src/reflog.rs +++ b/src/reflog.rs @@ -1,10 +1,11 @@ -use std::ops::Range; +use libc::size_t; +use std::iter::FusedIterator; use std::marker; +use std::ops::Range; use std::str; -use libc::size_t; -use {raw, signature, Oid, Error, Signature}; -use util::Binding; +use crate::util::Binding; +use crate::{raw, signature, Error, Oid, Signature}; /// A reference log of a git repository. pub struct Reflog { @@ -25,12 +26,20 @@ pub struct ReflogIter<'reflog> { impl Reflog { /// Add a new entry to the in-memory reflog. - pub fn append(&mut self, new_oid: Oid, committer: &Signature, - msg: Option<&str>) -> Result<(), Error> { - let msg = try!(::opt_cstr(msg)); + pub fn append( + &mut self, + new_oid: Oid, + committer: &Signature<'_>, + msg: Option<&str>, + ) -> Result<(), Error> { + let msg = crate::opt_cstr(msg)?; unsafe { - try_call!(raw::git_reflog_append(self.raw, new_oid.raw(), - committer.raw(), msg)); + try_call!(raw::git_reflog_append( + self.raw, + new_oid.raw(), + committer.raw(), + msg + )); } Ok(()) } @@ -41,11 +50,13 @@ impl Reflog { /// param value to `true`. When deleting entry n, member old_oid of entry /// n-1 (if any) will be updated with the value of member new_oid of entry /// n+1. - pub fn remove(&mut self, i: usize, rewrite_previous_entry: bool) - -> Result<(), Error> { + pub fn remove(&mut self, i: usize, rewrite_previous_entry: bool) -> Result<(), Error> { unsafe { - try_call!(raw::git_reflog_drop(self.raw, i as size_t, - rewrite_previous_entry)); + try_call!(raw::git_reflog_drop( + self.raw, + i as size_t, + rewrite_previous_entry + )); } Ok(()) } @@ -54,7 +65,7 @@ impl Reflog { /// /// Requesting the reflog entry with an index of 0 (zero) will return the /// most recently created entry. - pub fn get(&self, i: usize) -> Option { + pub fn get(&self, i: usize) -> Option> { unsafe { let ptr = raw::git_reflog_entry_byindex(self.raw, i as size_t); Binding::from_raw_opt(ptr) @@ -66,15 +77,25 @@ impl Reflog { unsafe { raw::git_reflog_entrycount(self.raw) as usize } } + /// Return `true ` is there is no log entry in a reflog + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Get an iterator to all entries inside of this reflog - pub fn iter(&self) -> ReflogIter { - ReflogIter { range: 0..self.len(), reflog: self } + pub fn iter(&self) -> ReflogIter<'_> { + ReflogIter { + range: 0..self.len(), + reflog: self, + } } /// Write an existing in-memory reflog object back to disk using an atomic /// file lock. pub fn write(&mut self) -> Result<(), Error> { - unsafe { try_call!(raw::git_reflog_write(self.raw)); } + unsafe { + try_call!(raw::git_reflog_write(self.raw)); + } Ok(()) } } @@ -83,9 +104,11 @@ impl Binding for Reflog { type Raw = *mut raw::git_reflog; unsafe fn from_raw(raw: *mut raw::git_reflog) -> Reflog { - Reflog { raw: raw } + Reflog { raw } + } + fn raw(&self) -> *mut raw::git_reflog { + self.raw } - fn raw(&self) -> *mut raw::git_reflog { self.raw } } impl Drop for Reflog { @@ -96,7 +119,7 @@ impl Drop for Reflog { impl<'reflog> ReflogEntry<'reflog> { /// Get the committer of this entry - pub fn committer(&self) -> Signature { + pub fn committer(&self) -> Signature<'_> { unsafe { let ptr = raw::git_reflog_entry_committer(self.raw); signature::from_raw_const(self, ptr) @@ -110,7 +133,7 @@ impl<'reflog> ReflogEntry<'reflog> { /// Get the old oid pub fn id_old(&self) -> Oid { - unsafe { Binding::from_raw(raw::git_reflog_entry_id_new(self.raw)) } + unsafe { Binding::from_raw(raw::git_reflog_entry_id_old(self.raw)) } } /// Get the log message, returning `None` on invalid UTF-8. @@ -120,9 +143,7 @@ impl<'reflog> ReflogEntry<'reflog> { /// Get the log message as a byte array. pub fn message_bytes(&self) -> Option<&[u8]> { - unsafe { - ::opt_bytes(self, raw::git_reflog_entry_message(self.raw)) - } + unsafe { crate::opt_bytes(self, raw::git_reflog_entry_message(self.raw)) } } } @@ -130,9 +151,14 @@ impl<'reflog> Binding for ReflogEntry<'reflog> { type Raw = *const raw::git_reflog_entry; unsafe fn from_raw(raw: *const raw::git_reflog_entry) -> ReflogEntry<'reflog> { - ReflogEntry { raw: raw, _marker: marker::PhantomData } + ReflogEntry { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *const raw::git_reflog_entry { + self.raw } - fn raw(&self) -> *const raw::git_reflog_entry { self.raw } } impl<'reflog> Iterator for ReflogIter<'reflog> { @@ -140,20 +166,23 @@ impl<'reflog> Iterator for ReflogIter<'reflog> { fn next(&mut self) -> Option> { self.range.next().and_then(|i| self.reflog.get(i)) } - fn size_hint(&self) -> (usize, Option) { self.range.size_hint() } + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() + } } impl<'reflog> DoubleEndedIterator for ReflogIter<'reflog> { fn next_back(&mut self) -> Option> { self.range.next_back().and_then(|i| self.reflog.get(i)) } } +impl<'reflog> FusedIterator for ReflogIter<'reflog> {} impl<'reflog> ExactSizeIterator for ReflogIter<'reflog> {} #[cfg(test)] mod tests { #[test] fn smoke() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let mut reflog = repo.reflog("HEAD").unwrap(); assert_eq!(reflog.iter().len(), 1); reflog.write().unwrap(); diff --git a/src/refspec.rs b/src/refspec.rs index c814d23e5b..3f62e991c7 100644 --- a/src/refspec.rs +++ b/src/refspec.rs @@ -2,8 +2,8 @@ use std::ffi::CString; use std::marker; use std::str; -use {raw, Direction}; -use util::Binding; +use crate::util::Binding; +use crate::{raw, Buf, Direction, Error}; /// A structure to represent a git [refspec][1]. /// @@ -34,7 +34,7 @@ impl<'remote> Refspec<'remote> { /// Get the destination specifier, in bytes. pub fn dst_bytes(&self) -> &[u8] { - unsafe { ::opt_bytes(self, raw::git_refspec_dst(self.raw)).unwrap() } + unsafe { crate::opt_bytes(self, raw::git_refspec_dst(self.raw)).unwrap() } } /// Check if a refspec's destination descriptor matches a reference @@ -52,7 +52,7 @@ impl<'remote> Refspec<'remote> { /// Get the source specifier, in bytes. pub fn src_bytes(&self) -> &[u8] { - unsafe { ::opt_bytes(self, raw::git_refspec_src(self.raw)).unwrap() } + unsafe { crate::opt_bytes(self, raw::git_refspec_src(self.raw)).unwrap() } } /// Check if a refspec's source descriptor matches a reference @@ -75,7 +75,35 @@ impl<'remote> Refspec<'remote> { /// Get the refspec's string as a byte array pub fn bytes(&self) -> &[u8] { - unsafe { ::opt_bytes(self, raw::git_refspec_string(self.raw)).unwrap() } + unsafe { crate::opt_bytes(self, raw::git_refspec_string(self.raw)).unwrap() } + } + + /// Transform a reference to its target following the refspec's rules + pub fn transform(&self, name: &str) -> Result { + let name = CString::new(name).unwrap(); + unsafe { + let buf = Buf::new(); + try_call!(raw::git_refspec_transform( + buf.raw(), + self.raw, + name.as_ptr() + )); + Ok(buf) + } + } + + /// Transform a target reference to its source reference following the refspec's rules + pub fn rtransform(&self, name: &str) -> Result { + let name = CString::new(name).unwrap(); + unsafe { + let buf = Buf::new(); + try_call!(raw::git_refspec_rtransform( + buf.raw(), + self.raw, + name.as_ptr() + )); + Ok(buf) + } } } @@ -83,7 +111,12 @@ impl<'remote> Binding for Refspec<'remote> { type Raw = *const raw::git_refspec; unsafe fn from_raw(raw: *const raw::git_refspec) -> Refspec<'remote> { - Refspec { raw: raw, _marker: marker::PhantomData } + Refspec { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *const raw::git_refspec { + self.raw } - fn raw(&self) -> *const raw::git_refspec { self.raw } } diff --git a/src/remote.rs b/src/remote.rs index 9b51d7350d..0c13a53fcf 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -1,14 +1,18 @@ -use std::ffi::CString; -use std::ops::Range; +use raw::git_strarray; +use std::iter::FusedIterator; use std::marker; use std::mem; +use std::ops::Range; +use std::os::raw::c_uint; +use std::ptr; use std::slice; use std::str; -use libc; +use std::{ffi::CString, os::raw::c_char}; -use {raw, Direction, Error, Refspec, Oid, FetchPrune}; -use {RemoteCallbacks, Progress, Repository, AutotagOption}; -use util::Binding; +use crate::string_array::StringArray; +use crate::util::Binding; +use crate::{call, raw, Buf, Direction, Error, FetchPrune, Oid, ProxyOptions, Refspec}; +use crate::{AutotagOption, Progress, RemoteCallbacks, RemoteUpdateFlags, Repository}; /// A structure representing a [remote][1] of a git repository. /// @@ -27,7 +31,7 @@ pub struct Refspecs<'remote> { remote: &'remote Remote<'remote>, } -/// Description of a reference advertised bya remote server, given out on calls +/// Description of a reference advertised by a remote server, given out on calls /// to `list`. pub struct RemoteHead<'remote> { raw: *const raw::git_remote_head, @@ -37,23 +41,86 @@ pub struct RemoteHead<'remote> { /// Options which can be specified to various fetch operations. pub struct FetchOptions<'cb> { callbacks: Option>, + depth: i32, + proxy: Option>, prune: FetchPrune, - update_fetchhead: bool, + update_flags: RemoteUpdateFlags, download_tags: AutotagOption, + follow_redirects: RemoteRedirect, + custom_headers: Vec, + custom_headers_ptrs: Vec<*const c_char>, } /// Options to control the behavior of a git push. pub struct PushOptions<'cb> { callbacks: Option>, + proxy: Option>, pb_parallelism: u32, + follow_redirects: RemoteRedirect, + custom_headers: Vec, + custom_headers_ptrs: Vec<*const c_char>, + remote_push_options: Vec, + remote_push_options_ptrs: Vec<*const c_char>, +} + +/// Holds callbacks for a connection to a `Remote`. Disconnects when dropped +pub struct RemoteConnection<'repo, 'connection, 'cb> { + _callbacks: Box>, + _proxy: ProxyOptions<'cb>, + remote: &'connection mut Remote<'repo>, +} + +/// Remote redirection settings; whether redirects to another host are +/// permitted. +/// +/// By default, git will follow a redirect on the initial request +/// (`/info/refs`), but not subsequent requests. +pub enum RemoteRedirect { + /// Do not follow any off-site redirects at any stage of the fetch or push. + None, + /// Allow off-site redirects only upon the initial request. This is the + /// default. + Initial, + /// Allow redirects at any stage in the fetch or push. + All, +} + +pub fn remote_into_raw(remote: Remote<'_>) -> *mut raw::git_remote { + let ret = remote.raw; + mem::forget(remote); + ret } impl<'repo> Remote<'repo> { /// Ensure the remote name is well-formed. pub fn is_valid_name(remote_name: &str) -> bool { - ::init(); + crate::init(); let remote_name = CString::new(remote_name).unwrap(); - unsafe { raw::git_remote_is_valid_name(remote_name.as_ptr()) == 1 } + let mut valid: libc::c_int = 0; + unsafe { + call::c_try(raw::git_remote_name_is_valid( + &mut valid, + remote_name.as_ptr(), + )) + .unwrap(); + } + valid == 1 + } + + /// Create a detached remote + /// + /// Create a remote with the given URL in-memory. You can use this + /// when you have a URL instead of a remote's name. + /// Contrasted with an anonymous remote, a detached remote will not + /// consider any repo configuration values. + pub fn create_detached>>(url: S) -> Result, Error> { + crate::init(); + let mut ret = ptr::null_mut(); + let url = CString::new(url)?; + unsafe { + try_call!(raw::git_remote_create_detached(&mut ret, url)); + Ok(Binding::from_raw(ret)) + } } /// Get the remote's name. @@ -68,52 +135,103 @@ impl<'repo> Remote<'repo> { /// /// Returns `None` if this remote has not yet been named pub fn name_bytes(&self) -> Option<&[u8]> { - unsafe { ::opt_bytes(self, raw::git_remote_name(&*self.raw)) } + unsafe { crate::opt_bytes(self, raw::git_remote_name(&*self.raw)) } } - /// Get the remote's url. + /// Get the remote's URL. /// - /// Returns `None` if the url is not valid utf-8 + /// Returns `None` if the URL is not valid utf-8 pub fn url(https://github.com/&self) -> Option<&str> { str::from_utf8(self.url_bytes()).ok() } - /// Get the remote's url as a byte array. + /// Get the remote's URL as a byte array. pub fn url_bytes(&self) -> &[u8] { - unsafe { ::opt_bytes(self, raw::git_remote_url(https://github.com/&*self.raw)).unwrap() } + unsafe { crate::opt_bytes(self, raw::git_remote_url(https://github.com/&*self.raw)).unwrap_or(&[]) } } /// Get the remote's pushurl. /// - /// Returns `None` if the pushurl is not valid utf-8 + /// Returns `None` if the pushurl is not valid utf-8 or no special url for pushing is set. pub fn pushurl(https://github.com/&self) -> Option<&str> { self.pushurl_bytes().and_then(|s| str::from_utf8(s).ok()) } /// Get the remote's pushurl as a byte array. + /// + /// Returns `None` if no special url for pushing is set. pub fn pushurl_bytes(&self) -> Option<&[u8]> { - unsafe { ::opt_bytes(self, raw::git_remote_pushurl(https://github.com/&*self.raw)) } + unsafe { crate::opt_bytes(self, raw::git_remote_pushurl(https://github.com/&*self.raw)) } + } + + /// Get the remote's default branch. + /// + /// The remote (or more exactly its transport) must have connected to the + /// remote repository. This default branch is available as soon as the + /// connection to the remote is initiated and it remains available after + /// disconnecting. + pub fn default_branch(&self) -> Result { + unsafe { + let buf = Buf::new(); + try_call!(raw::git_remote_default_branch(buf.raw(), self.raw)); + Ok(buf) + } } /// Open a connection to a remote. pub fn connect(&mut self, dir: Direction) -> Result<(), Error> { // TODO: can callbacks be exposed safely? unsafe { - try_call!(raw::git_remote_connect(self.raw, dir, - 0 as *const _, - 0 as *const _)); + try_call!(raw::git_remote_connect( + self.raw, + dir, + ptr::null(), + ptr::null(), + ptr::null() + )); } Ok(()) } + /// Open a connection to a remote with callbacks and proxy settings + /// + /// Returns a `RemoteConnection` that will disconnect once dropped + pub fn connect_auth<'connection, 'cb>( + &'connection mut self, + dir: Direction, + cb: Option>, + proxy_options: Option>, + ) -> Result, Error> { + let cb = Box::new(cb.unwrap_or_else(RemoteCallbacks::new)); + let proxy_options = proxy_options.unwrap_or_else(ProxyOptions::new); + unsafe { + try_call!(raw::git_remote_connect( + self.raw, + dir, + &cb.raw(), + &proxy_options.raw(), + ptr::null() + )); + } + + Ok(RemoteConnection { + _callbacks: cb, + _proxy: proxy_options, + remote: self, + }) + } + /// Check whether the remote is connected pub fn connected(&mut self) -> bool { unsafe { raw::git_remote_connected(self.raw) == 1 } } /// Disconnect from the remote - pub fn disconnect(&mut self) { - unsafe { raw::git_remote_disconnect(self.raw) } + pub fn disconnect(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_remote_disconnect(self.raw)); + } + Ok(()) } /// Download and index the packfile @@ -126,9 +244,12 @@ impl<'repo> Remote<'repo> { /// /// The `specs` argument is a list of refspecs to use for this negotiation /// and download. Use an empty array to use the base refspecs. - pub fn download(&mut self, specs: &[&str], opts: Option<&mut FetchOptions>) - -> Result<(), Error> { - let (_a, _b, arr) = try!(::util::iter2cstrs(specs.iter())); + pub fn download + crate::IntoCString + Clone>( + &mut self, + specs: &[Str], + opts: Option<&mut FetchOptions<'_>>, + ) -> Result<(), Error> { + let (_a, _b, arr) = crate::util::iter2cstrs(specs.iter())?; let raw = opts.map(|o| o.raw()); unsafe { try_call!(raw::git_remote_download(self.raw, &arr, raw.as_ref())); @@ -136,10 +257,24 @@ impl<'repo> Remote<'repo> { Ok(()) } + /// Cancel the operation + /// + /// At certain points in its operation, the network code checks whether the + /// operation has been canceled and if so stops the operation. + pub fn stop(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_remote_stop(self.raw)); + } + Ok(()) + } + /// Get the number of refspecs for a remote - pub fn refspecs<'a>(&'a self) -> Refspecs<'a> { + pub fn refspecs(&self) -> Refspecs<'_> { let cnt = unsafe { raw::git_remote_refspec_count(&*self.raw) as usize }; - Refspecs { range: 0..cnt, remote: self } + Refspecs { + range: 0..cnt, + remote: self, + } } /// Get the `nth` refspec from this remote. @@ -147,8 +282,7 @@ impl<'repo> Remote<'repo> { /// The `refspecs` iterator can be used to iterate over all refspecs. pub fn get_refspec(&self, i: usize) -> Option> { unsafe { - let ptr = raw::git_remote_get_refspec(&*self.raw, - i as libc::size_t); + let ptr = raw::git_remote_get_refspec(&*self.raw, i as libc::size_t); Binding::from_raw_opt(ptr) } } @@ -157,12 +291,27 @@ impl<'repo> Remote<'repo> { /// /// Convenience function to connect to a remote, download the data, /// disconnect and update the remote-tracking branches. - pub fn fetch(&mut self, - refspecs: &[&str], - opts: Option<&mut FetchOptions>, - reflog_msg: Option<&str>) -> Result<(), Error> { - let (_a, _b, arr) = try!(::util::iter2cstrs(refspecs.iter())); - let msg = try!(::opt_cstr(reflog_msg)); + /// + /// # Examples + /// + /// Example of functionality similar to `git fetch origin main`: + /// + /// ```no_run + /// fn fetch_origin_main(repo: git2::Repository) -> Result<(), git2::Error> { + /// repo.find_remote("origin")?.fetch(&["main"], None, None) + /// } + /// + /// let repo = git2::Repository::discover("rust").unwrap(); + /// fetch_origin_main(repo).unwrap(); + /// ``` + pub fn fetch + crate::IntoCString + Clone>( + &mut self, + refspecs: &[Str], + opts: Option<&mut FetchOptions<'_>>, + reflog_msg: Option<&str>, + ) -> Result<(), Error> { + let (_a, _b, arr) = crate::util::iter2cstrs(refspecs.iter())?; + let msg = crate::opt_cstr(reflog_msg)?; let raw = opts.map(|o| o.raw()); unsafe { try_call!(raw::git_remote_fetch(self.raw, &arr, raw.as_ref(), msg)); @@ -171,17 +320,23 @@ impl<'repo> Remote<'repo> { } /// Update the tips to the new state - pub fn update_tips(&mut self, - callbacks: Option<&mut RemoteCallbacks>, - update_fetchhead: bool, - download_tags: AutotagOption, - msg: Option<&str>) -> Result<(), Error> { - let msg = try!(::opt_cstr(msg)); + pub fn update_tips( + &mut self, + callbacks: Option<&mut RemoteCallbacks<'_>>, + update_flags: RemoteUpdateFlags, + download_tags: AutotagOption, + msg: Option<&str>, + ) -> Result<(), Error> { + let msg = crate::opt_cstr(msg)?; let cbs = callbacks.map(|cb| cb.raw()); unsafe { - try_call!(raw::git_remote_update_tips(self.raw, cbs.as_ref(), - update_fetchhead, - download_tags, msg)); + try_call!(raw::git_remote_update_tips( + self.raw, + cbs.as_ref(), + update_flags.bits() as c_uint, + download_tags, + msg + )); } Ok(()) } @@ -190,10 +345,16 @@ impl<'repo> Remote<'repo> { /// /// Perform all the steps for a push. If no refspecs are passed then the /// configured refspecs will be used. - pub fn push(&mut self, - refspecs: &[&str], - opts: Option<&mut PushOptions>) -> Result<(), Error> { - let (_a, _b, arr) = try!(::util::iter2cstrs(refspecs.iter())); + /// + /// Note that you'll likely want to use `RemoteCallbacks` and set + /// `push_update_reference` to test whether all the references were pushed + /// successfully. + pub fn push + crate::IntoCString + Clone>( + &mut self, + refspecs: &[Str], + opts: Option<&mut PushOptions<'_>>, + ) -> Result<(), Error> { + let (_a, _b, arr) = crate::util::iter2cstrs(refspecs.iter())?; let raw = opts.map(|o| o.raw()); unsafe { try_call!(raw::git_remote_push(self.raw, &arr, raw.as_ref())); @@ -202,10 +363,8 @@ impl<'repo> Remote<'repo> { } /// Get the statistics structure that is filled in by the fetch operation. - pub fn stats(&self) -> Progress { - unsafe { - Binding::from_raw(raw::git_remote_stats(self.raw)) - } + pub fn stats(&self) -> Progress<'_> { + unsafe { Binding::from_raw(raw::git_remote_stats(self.raw)) } } /// Get the remote repository's reference advertisement list. @@ -216,23 +375,54 @@ impl<'repo> Remote<'repo> { /// The remote (or more exactly its transport) must have connected to the /// remote repository. This list is available as soon as the connection to /// the remote is initiated and it remains available after disconnecting. - pub fn list(&self) -> Result<&[RemoteHead], Error> { + pub fn list(&self) -> Result<&[RemoteHead<'_>], Error> { let mut size = 0; - let mut base = 0 as *mut _; + let mut base = ptr::null_mut(); unsafe { try_call!(raw::git_remote_ls(&mut base, &mut size, self.raw)); - assert_eq!(mem::size_of::(), - mem::size_of::<*const raw::git_remote_head>()); + assert_eq!( + mem::size_of::>(), + mem::size_of::<*const raw::git_remote_head>() + ); let slice = slice::from_raw_parts(base as *const _, size as usize); - Ok(mem::transmute::<&[*const raw::git_remote_head], - &[RemoteHead]>(slice)) + Ok(mem::transmute::< + &[*const raw::git_remote_head], + &[RemoteHead<'_>], + >(slice)) + } + } + + /// Prune tracking refs that are no longer present on remote + pub fn prune(&mut self, callbacks: Option>) -> Result<(), Error> { + let cbs = Box::new(callbacks.unwrap_or_else(RemoteCallbacks::new)); + unsafe { + try_call!(raw::git_remote_prune(self.raw, &cbs.raw())); + } + Ok(()) + } + + /// Get the remote's list of fetch refspecs + pub fn fetch_refspecs(&self) -> Result { + unsafe { + let mut raw: raw::git_strarray = mem::zeroed(); + try_call!(raw::git_remote_get_fetch_refspecs(&mut raw, self.raw)); + Ok(StringArray::from_raw(raw)) + } + } + + /// Get the remote's list of push refspecs + pub fn push_refspecs(&self) -> Result { + unsafe { + let mut raw: raw::git_strarray = mem::zeroed(); + try_call!(raw::git_remote_get_push_refspecs(&mut raw, self.raw)); + Ok(StringArray::from_raw(raw)) } } } impl<'repo> Clone for Remote<'repo> { fn clone(&self) -> Remote<'repo> { - let mut ret = 0 as *mut raw::git_remote; + let mut ret = ptr::null_mut(); let rc = unsafe { call!(raw::git_remote_dup(&mut ret, self.raw)) }; assert_eq!(rc, 0); Remote { @@ -247,11 +437,13 @@ impl<'repo> Binding for Remote<'repo> { unsafe fn from_raw(raw: *mut raw::git_remote) -> Remote<'repo> { Remote { - raw: raw, + raw, _marker: marker::PhantomData, } } - fn raw(&self) -> *mut raw::git_remote { self.raw } + fn raw(&self) -> *mut raw::git_remote { + self.raw + } } impl<'repo> Drop for Remote<'repo> { @@ -265,13 +457,18 @@ impl<'repo> Iterator for Refspecs<'repo> { fn next(&mut self) -> Option> { self.range.next().and_then(|i| self.remote.get_refspec(i)) } - fn size_hint(&self) -> (usize, Option) { self.range.size_hint() } + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() + } } impl<'repo> DoubleEndedIterator for Refspecs<'repo> { fn next_back(&mut self) -> Option> { - self.range.next_back().and_then(|i| self.remote.get_refspec(i)) + self.range + .next_back() + .and_then(|i| self.remote.get_refspec(i)) } } +impl<'repo> FusedIterator for Refspecs<'repo> {} impl<'repo> ExactSizeIterator for Refspecs<'repo> {} #[allow(missing_docs)] // not documented in libgit2 :( @@ -289,24 +486,35 @@ impl<'remote> RemoteHead<'remote> { } pub fn name(&self) -> &str { - let b = unsafe { ::opt_bytes(self, (*self.raw).name).unwrap() }; + let b = unsafe { crate::opt_bytes(self, (*self.raw).name).unwrap() }; str::from_utf8(b).unwrap() } pub fn symref_target(&self) -> Option<&str> { - let b = unsafe { ::opt_bytes(self, (*self.raw).symref_target) }; + let b = unsafe { crate::opt_bytes(self, (*self.raw).symref_target) }; b.map(|b| str::from_utf8(b).unwrap()) } } +impl<'cb> Default for FetchOptions<'cb> { + fn default() -> Self { + Self::new() + } +} + impl<'cb> FetchOptions<'cb> { /// Creates a new blank set of fetch options pub fn new() -> FetchOptions<'cb> { FetchOptions { callbacks: None, + proxy: None, prune: FetchPrune::Unspecified, - update_fetchhead: true, + update_flags: RemoteUpdateFlags::UPDATE_FETCHHEAD, download_tags: AutotagOption::Unspecified, + follow_redirects: RemoteRedirect::Initial, + custom_headers: Vec::new(), + custom_headers_ptrs: Vec::new(), + depth: 0, // Not limited depth } } @@ -316,6 +524,12 @@ impl<'cb> FetchOptions<'cb> { self } + /// Set the proxy options to use for the fetch operation. + pub fn proxy_options(&mut self, opts: ProxyOptions<'cb>) -> &mut Self { + self.proxy = Some(opts); + self + } + /// Set whether to perform a prune after the fetch. pub fn prune(&mut self, prune: FetchPrune) -> &mut Self { self.prune = prune; @@ -326,7 +540,28 @@ impl<'cb> FetchOptions<'cb> { /// /// Defaults to `true`. pub fn update_fetchhead(&mut self, update: bool) -> &mut Self { - self.update_fetchhead = update; + self.update_flags + .set(RemoteUpdateFlags::UPDATE_FETCHHEAD, update); + self + } + + /// Set whether to report unchanged tips in the update_tips callback. + /// + /// Defaults to `false`. + pub fn report_unchanged(&mut self, update: bool) -> &mut Self { + self.update_flags + .set(RemoteUpdateFlags::REPORT_UNCHANGED, update); + self + } + + /// Set fetch depth, a value less or equal to 0 is interpreted as pull + /// everything (effectively the same as not declaring a limit depth). + + // FIXME(blyxyas): We currently don't have a test for shallow functions + // because libgit2 doesn't support local shallow clones. + // https://github.com/rust-lang/git2-rs/pull/979#issuecomment-1716299900 + pub fn depth(&mut self, depth: i32) -> &mut Self { + self.depth = depth.max(0); self } @@ -338,6 +573,26 @@ impl<'cb> FetchOptions<'cb> { self.download_tags = opt; self } + + /// Set remote redirection settings; whether redirects to another host are + /// permitted. + /// + /// By default, git will follow a redirect on the initial request + /// (`/info/refs`), but not subsequent requests. + pub fn follow_redirects(&mut self, redirect: RemoteRedirect) -> &mut Self { + self.follow_redirects = redirect; + self + } + + /// Set extra headers for this fetch operation. + pub fn custom_headers(&mut self, custom_headers: &[&str]) -> &mut Self { + self.custom_headers = custom_headers + .iter() + .map(|&s| CString::new(s).unwrap()) + .collect(); + self.custom_headers_ptrs = self.custom_headers.iter().map(|s| s.as_ptr()).collect(); + self + } } impl<'cb> Binding for FetchOptions<'cb> { @@ -349,35 +604,65 @@ impl<'cb> Binding for FetchOptions<'cb> { fn raw(&self) -> raw::git_fetch_options { raw::git_fetch_options { version: 1, - callbacks: self.callbacks.as_ref().map(|m| m.raw()) - .unwrap_or_else(|| RemoteCallbacks::new().raw()), - prune: ::call::convert(&self.prune), - update_fetchhead: ::call::convert(&self.update_fetchhead), - download_tags: ::call::convert(&self.download_tags), - // TODO: expose this as a builder option - custom_headers: raw::git_strarray { - count: 0, - strings: 0 as *mut _, + callbacks: self + .callbacks + .as_ref() + .map(|m| m.raw()) + .unwrap_or_else(|| RemoteCallbacks::new().raw()), + proxy_opts: self + .proxy + .as_ref() + .map(|m| m.raw()) + .unwrap_or_else(|| ProxyOptions::new().raw()), + prune: crate::call::convert(&self.prune), + // `update_fetchhead` is an incorrectly named option which contains both + // the `UPDATE_FETCHHEAD` and `REPORT_UNCHANGED` flags. + // See https://github.com/libgit2/libgit2/pull/6806 + update_fetchhead: self.update_flags.bits() as c_uint, + download_tags: crate::call::convert(&self.download_tags), + depth: self.depth, + follow_redirects: self.follow_redirects.raw(), + custom_headers: git_strarray { + count: self.custom_headers_ptrs.len(), + strings: self.custom_headers_ptrs.as_ptr() as *mut _, }, } } } +impl<'cb> Default for PushOptions<'cb> { + fn default() -> Self { + Self::new() + } +} + impl<'cb> PushOptions<'cb> { /// Creates a new blank set of push options pub fn new() -> PushOptions<'cb> { PushOptions { callbacks: None, + proxy: None, pb_parallelism: 1, + follow_redirects: RemoteRedirect::Initial, + custom_headers: Vec::new(), + custom_headers_ptrs: Vec::new(), + remote_push_options: Vec::new(), + remote_push_options_ptrs: Vec::new(), } } - /// Set the callbacks to use for the fetch operation. + /// Set the callbacks to use for the push operation. pub fn remote_callbacks(&mut self, cbs: RemoteCallbacks<'cb>) -> &mut Self { self.callbacks = Some(cbs); self } + /// Set the proxy options to use for the push operation. + pub fn proxy_options(&mut self, opts: ProxyOptions<'cb>) -> &mut Self { + self.proxy = Some(opts); + self + } + /// If the transport being used to push to the remote requires the creation /// of a pack file, this controls the number of worker threads used by the /// packbuilder when creating that pack file to be sent to the remote. @@ -388,6 +673,40 @@ impl<'cb> PushOptions<'cb> { self.pb_parallelism = parallel; self } + + /// Set remote redirection settings; whether redirects to another host are + /// permitted. + /// + /// By default, git will follow a redirect on the initial request + /// (`/info/refs`), but not subsequent requests. + pub fn follow_redirects(&mut self, redirect: RemoteRedirect) -> &mut Self { + self.follow_redirects = redirect; + self + } + + /// Set extra headers for this push operation. + pub fn custom_headers(&mut self, custom_headers: &[&str]) -> &mut Self { + self.custom_headers = custom_headers + .iter() + .map(|&s| CString::new(s).unwrap()) + .collect(); + self.custom_headers_ptrs = self.custom_headers.iter().map(|s| s.as_ptr()).collect(); + self + } + + /// Set "push options" to deliver to the remote. + pub fn remote_push_options(&mut self, remote_push_options: &[&str]) -> &mut Self { + self.remote_push_options = remote_push_options + .iter() + .map(|&s| CString::new(s).unwrap()) + .collect(); + self.remote_push_options_ptrs = self + .remote_push_options + .iter() + .map(|s| s.as_ptr()) + .collect(); + self + } } impl<'cb> Binding for PushOptions<'cb> { @@ -399,33 +718,95 @@ impl<'cb> Binding for PushOptions<'cb> { fn raw(&self) -> raw::git_push_options { raw::git_push_options { version: 1, - callbacks: self.callbacks.as_ref().map(|m| m.raw()) - .unwrap_or(RemoteCallbacks::new().raw()), + callbacks: self + .callbacks + .as_ref() + .map(|m| m.raw()) + .unwrap_or_else(|| RemoteCallbacks::new().raw()), + proxy_opts: self + .proxy + .as_ref() + .map(|m| m.raw()) + .unwrap_or_else(|| ProxyOptions::new().raw()), pb_parallelism: self.pb_parallelism as libc::c_uint, - // TODO: expose this as a builder option - custom_headers: raw::git_strarray { - count: 0, - strings: 0 as *mut _, + follow_redirects: self.follow_redirects.raw(), + custom_headers: git_strarray { + count: self.custom_headers_ptrs.len(), + strings: self.custom_headers_ptrs.as_ptr() as *mut _, + }, + remote_push_options: git_strarray { + count: self.remote_push_options.len(), + strings: self.remote_push_options_ptrs.as_ptr() as *mut _, }, } } } +impl<'repo, 'connection, 'cb> RemoteConnection<'repo, 'connection, 'cb> { + /// Check whether the remote is (still) connected + pub fn connected(&mut self) -> bool { + self.remote.connected() + } + + /// Get the remote repository's reference advertisement list. + /// + /// This list is available as soon as the connection to + /// the remote is initiated and it remains available after disconnecting. + pub fn list(&self) -> Result<&[RemoteHead<'_>], Error> { + self.remote.list() + } + + /// Get the remote's default branch. + /// + /// This default branch is available as soon as the connection to the remote + /// is initiated and it remains available after disconnecting. + pub fn default_branch(&self) -> Result { + self.remote.default_branch() + } + + /// access remote bound to this connection + pub fn remote(&mut self) -> &mut Remote<'repo> { + self.remote + } +} + +impl<'repo, 'connection, 'cb> Drop for RemoteConnection<'repo, 'connection, 'cb> { + fn drop(&mut self) { + drop(self.remote.disconnect()); + } +} + +impl Default for RemoteRedirect { + fn default() -> Self { + RemoteRedirect::Initial + } +} + +impl RemoteRedirect { + fn raw(&self) -> raw::git_remote_redirect_t { + match self { + RemoteRedirect::None => raw::GIT_REMOTE_REDIRECT_NONE, + RemoteRedirect::Initial => raw::GIT_REMOTE_REDIRECT_INITIAL, + RemoteRedirect::All => raw::GIT_REMOTE_REDIRECT_ALL, + } + } +} + #[cfg(test)] mod tests { + use crate::{AutotagOption, PushOptions, RemoteUpdateFlags}; + use crate::{Direction, FetchOptions, Remote, RemoteCallbacks, Repository}; use std::cell::Cell; - use tempdir::TempDir; - use {Repository, Remote, RemoteCallbacks, Direction, FetchOptions}; - use {AutotagOption}; + use tempfile::TempDir; #[test] fn smoke() { - let (td, repo) = ::test::repo_init(); + let (td, repo) = crate::test::repo_init(); t!(repo.remote("origin", "/path/to/nowhere")); drop(repo); let repo = t!(Repository::init(td.path())); - let origin = t!(repo.find_remote("origin")); + let mut origin = t!(repo.find_remote("origin")); assert_eq!(origin.name(), Some("origin")); assert_eq!(origin.url(), Some("/path/to/nowhere")); assert_eq!(origin.pushurl(), None); @@ -435,20 +816,24 @@ mod tests { let stats = origin.stats(); assert_eq!(stats.total_objects(), 0); + + t!(origin.stop()); } #[test] fn create_remote() { - let td = TempDir::new("test").unwrap(); + let td = TempDir::new().unwrap(); let remote = td.path().join("remote"); Repository::init_bare(&remote).unwrap(); - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let url = if cfg!(unix) { format!("file://{}", remote.display()) } else { - format!("file:///{}", remote.display().to_string() - .replace("\\", "/")) + format!( + "file:///{}", + remote.display().to_string().replace("\\", "/") + ) }; let mut origin = repo.remote("origin", &url).unwrap(); @@ -476,17 +861,43 @@ mod tests { origin.connect(Direction::Push).unwrap(); assert!(origin.connected()); - origin.disconnect(); + origin.disconnect().unwrap(); origin.connect(Direction::Fetch).unwrap(); assert!(origin.connected()); - origin.download(&[], None).unwrap(); - origin.disconnect(); + origin.download(&[] as &[&str], None).unwrap(); + origin.disconnect().unwrap(); - origin.fetch(&[], None, None).unwrap(); - origin.fetch(&[], None, Some("foo")).unwrap(); - origin.update_tips(None, true, AutotagOption::Unspecified, None).unwrap(); - origin.update_tips(None, true, AutotagOption::All, Some("foo")).unwrap(); + { + let mut connection = origin.connect_auth(Direction::Push, None, None).unwrap(); + assert!(connection.connected()); + } + assert!(!origin.connected()); + + { + let mut connection = origin.connect_auth(Direction::Fetch, None, None).unwrap(); + assert!(connection.connected()); + } + assert!(!origin.connected()); + + origin.fetch(&[] as &[&str], None, None).unwrap(); + origin.fetch(&[] as &[&str], None, Some("foo")).unwrap(); + origin + .update_tips( + None, + RemoteUpdateFlags::UPDATE_FETCHHEAD, + AutotagOption::Unspecified, + None, + ) + .unwrap(); + origin + .update_tips( + None, + RemoteUpdateFlags::UPDATE_FETCHHEAD, + AutotagOption::All, + Some("foo"), + ) + .unwrap(); t!(repo.remote_add_fetch("https://github.com/origin", "foo")); t!(repo.remote_add_fetch("https://github.com/origin", "bar")); @@ -494,15 +905,15 @@ mod tests { #[test] fn rename_remote() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); repo.remote("origin", "foo").unwrap(); - repo.remote_rename("origin", "foo").unwrap(); - repo.remote_delete("foo").unwrap(); + drop(repo.remote_rename("origin", "foo")); + drop(repo.remote_delete("foo")); } #[test] fn create_remote_anonymous() { - let td = TempDir::new("test").unwrap(); + let td = TempDir::new().unwrap(); let repo = Repository::init(td.path()).unwrap(); let origin = repo.remote_anonymous("/path/to/nowhere").unwrap(); @@ -511,16 +922,22 @@ mod tests { } #[test] - fn is_valid() { + fn is_valid_name() { assert!(Remote::is_valid_name("foobar")); assert!(!Remote::is_valid_name("\x01")); } + #[test] + #[should_panic] + fn is_valid_name_for_invalid_remote() { + Remote::is_valid_name("ab\012"); + } + #[test] fn transfer_cb() { - let (td, _repo) = ::test::repo_init(); - let td2 = TempDir::new("git").unwrap(); - let url = ::test::path2url(https://github.com/&td.path()); + let (td, _repo) = crate::test::repo_init(); + let td2 = TempDir::new().unwrap(); + let url = crate::test::path2url(https://github.com/&td.path()); let repo = Repository::init(td2.path()).unwrap(); let progress_hit = Cell::new(false); @@ -532,35 +949,222 @@ mod tests { progress_hit.set(true); true }); - origin.fetch(&[], - Some(FetchOptions::new().remote_callbacks(callbacks)), - None).unwrap(); + origin + .fetch( + &[] as &[&str], + Some(FetchOptions::new().remote_callbacks(callbacks)), + None, + ) + .unwrap(); let list = t!(origin.list()); assert_eq!(list.len(), 2); assert_eq!(list[0].name(), "HEAD"); assert!(!list[0].is_local()); - assert_eq!(list[1].name(), "refs/heads/master"); + assert_eq!(list[1].name(), "refs/heads/main"); assert!(!list[1].is_local()); } assert!(progress_hit.get()); } + /// This test is meant to assure that the callbacks provided to connect will not cause + /// segfaults #[test] - fn push() { - let (_td, repo) = ::test::repo_init(); - let td2 = TempDir::new("git1").unwrap(); - let td3 = TempDir::new("git2").unwrap(); - let url = ::test::path2url(https://github.com/&td2.path()); + fn connect_list() { + let (td, _repo) = crate::test::repo_init(); + let td2 = TempDir::new().unwrap(); + let url = crate::test::path2url(https://github.com/&td.path()); - Repository::init_bare(td2.path()).unwrap(); + let repo = Repository::init(td2.path()).unwrap(); + let mut callbacks = RemoteCallbacks::new(); + callbacks.sideband_progress(|_progress| { + // no-op + true + }); + + let mut origin = repo.remote("origin", &url).unwrap(); + + { + let mut connection = origin + .connect_auth(Direction::Fetch, Some(callbacks), None) + .unwrap(); + assert!(connection.connected()); + + let list = t!(connection.list()); + assert_eq!(list.len(), 2); + assert_eq!(list[0].name(), "HEAD"); + assert!(!list[0].is_local()); + assert_eq!(list[1].name(), "refs/heads/main"); + assert!(!list[1].is_local()); + } + assert!(!origin.connected()); + } + + #[test] + fn push() { + let (_td, repo) = crate::test::repo_init(); + let td2 = TempDir::new().unwrap(); + let td3 = TempDir::new().unwrap(); + let url = crate::test::path2url(https://github.com/&td2.path()); + + let mut opts = crate::RepositoryInitOptions::new(); + opts.bare(true); + opts.initial_head("main"); + Repository::init_opts(td2.path(), &opts).unwrap(); // git push let mut remote = repo.remote("origin", &url).unwrap(); - remote.push(&["refs/heads/master"], None).unwrap(); + let mut updated = false; + { + let mut callbacks = RemoteCallbacks::new(); + callbacks.push_update_reference(|refname, status| { + updated = true; + assert_eq!(refname, "refs/heads/main"); + assert_eq!(status, None); + Ok(()) + }); + let mut options = PushOptions::new(); + options.remote_callbacks(callbacks); + remote + .push(&["refs/heads/main"], Some(&mut options)) + .unwrap(); + } + assert!(updated); let repo = Repository::clone(&url, td3.path()).unwrap(); let commit = repo.head().unwrap().target().unwrap(); let commit = repo.find_commit(commit).unwrap(); - assert_eq!(commit.message(), Some("initial")); + assert_eq!(commit.message(), Some("initial\n\nbody")); + } + + #[test] + fn prune() { + let (td, remote_repo) = crate::test::repo_init(); + let oid = remote_repo.head().unwrap().target().unwrap(); + let commit = remote_repo.find_commit(oid).unwrap(); + remote_repo.branch("stale", &commit, true).unwrap(); + + let td2 = TempDir::new().unwrap(); + let url = crate::test::path2url(https://github.com/&td.path()); + let repo = Repository::clone(&url, &td2).unwrap(); + + fn assert_branch_count(repo: &Repository, count: usize) { + assert_eq!( + repo.branches(Some(crate::BranchType::Remote)) + .unwrap() + .filter(|b| b.as_ref().unwrap().0.name().unwrap() == Some("origin/stale")) + .count(), + count, + ); + } + + assert_branch_count(&repo, 1); + + // delete `stale` branch on remote repo + let mut stale_branch = remote_repo + .find_branch("stale", crate::BranchType::Local) + .unwrap(); + stale_branch.delete().unwrap(); + + // prune + let mut remote = repo.find_remote("origin").unwrap(); + remote.connect(Direction::Push).unwrap(); + let mut callbacks = RemoteCallbacks::new(); + callbacks.update_tips(|refname, _a, b| { + assert_eq!(refname, "refs/remotes/origin/stale"); + assert!(b.is_zero()); + true + }); + remote.prune(Some(callbacks)).unwrap(); + assert_branch_count(&repo, 0); + } + + #[test] + fn push_negotiation() { + let (_td, repo) = crate::test::repo_init(); + let oid = repo.head().unwrap().target().unwrap(); + + let td2 = TempDir::new().unwrap(); + let url = crate::test::path2url(https://github.com/td2.path()); + let mut opts = crate::RepositoryInitOptions::new(); + opts.bare(true); + opts.initial_head("main"); + let remote_repo = Repository::init_opts(td2.path(), &opts).unwrap(); + + // reject pushing a branch + let mut remote = repo.remote("origin", &url).unwrap(); + let mut updated = false; + { + let mut callbacks = RemoteCallbacks::new(); + callbacks.push_negotiation(|updates| { + assert!(!updated); + updated = true; + assert_eq!(updates.len(), 1); + let u = &updates[0]; + assert_eq!(u.src_refname().unwrap(), "refs/heads/main"); + assert!(u.src().is_zero()); + assert_eq!(u.dst_refname().unwrap(), "refs/heads/main"); + assert_eq!(u.dst(), oid); + Err(crate::Error::from_str("rejected")) + }); + let mut options = PushOptions::new(); + options.remote_callbacks(callbacks); + assert!(remote + .push(&["refs/heads/main"], Some(&mut options)) + .is_err()); + } + assert!(updated); + assert_eq!(remote_repo.branches(None).unwrap().count(), 0); + + // push 3 branches + let commit = repo.find_commit(oid).unwrap(); + repo.branch("new1", &commit, true).unwrap(); + repo.branch("new2", &commit, true).unwrap(); + let mut flag = 0; + updated = false; + { + let mut callbacks = RemoteCallbacks::new(); + callbacks.push_negotiation(|updates| { + assert!(!updated); + updated = true; + assert_eq!(updates.len(), 3); + for u in updates { + assert!(u.src().is_zero()); + assert_eq!(u.dst(), oid); + let src_name = u.src_refname().unwrap(); + let dst_name = u.dst_refname().unwrap(); + match src_name { + "refs/heads/main" => { + assert_eq!(dst_name, src_name); + flag |= 1; + } + "refs/heads/new1" => { + assert_eq!(dst_name, "refs/heads/dev1"); + flag |= 2; + } + "refs/heads/new2" => { + assert_eq!(dst_name, "refs/heads/dev2"); + flag |= 4; + } + _ => panic!("unexpected refname: {}", src_name), + } + } + Ok(()) + }); + let mut options = PushOptions::new(); + options.remote_callbacks(callbacks); + remote + .push( + &[ + "refs/heads/main", + "refs/heads/new1:refs/heads/dev1", + "refs/heads/new2:refs/heads/dev2", + ], + Some(&mut options), + ) + .unwrap(); + } + assert!(updated); + assert_eq!(flag, 7); + assert_eq!(remote_repo.branches(None).unwrap().count(), 3); } } diff --git a/src/remote_callbacks.rs b/src/remote_callbacks.rs index 6035f3cda2..2df2e7b015 100644 --- a/src/remote_callbacks.rs +++ b/src/remote_callbacks.rs @@ -1,13 +1,16 @@ +use libc::{c_char, c_int, c_uint, c_void, size_t}; use std::ffi::CStr; -use std::marker; use std::mem; +use std::ptr; use std::slice; use std::str; -use libc::{c_void, c_int, c_char, c_uint}; -use {raw, panic, Error, Cred, CredentialType, Oid}; -use cert::Cert; -use util::Binding; +use crate::cert::Cert; +use crate::util::Binding; +use crate::{ + panic, raw, Cred, CredentialType, Error, IndexerProgress, Oid, PackBuilderStage, Progress, + PushUpdate, +}; /// A structure to contain the callbacks which are invoked when a repository is /// being updated or downloaded. @@ -15,58 +18,93 @@ use util::Binding; /// These callbacks are used to manage facilities such as authentication, /// transfer progress, etc. pub struct RemoteCallbacks<'a> { - progress: Option>>, + push_progress: Option>>, + progress: Option>>, + pack_progress: Option>>, credentials: Option>>, sideband_progress: Option>>, update_tips: Option>>, certificate_check: Option>>, -} - -/// Struct representing the progress by an in-flight transfer. -pub struct Progress<'a> { - raw: ProgressState, - _marker: marker::PhantomData<&'a raw::git_transfer_progress>, -} - -enum ProgressState { - Borrowed(*const raw::git_transfer_progress), - Owned(raw::git_transfer_progress), + push_update_reference: Option>>, + push_negotiation: Option>>, } /// Callback used to acquire credentials for when a remote is fetched. /// /// * `url` - the resource for which the credentials are required. -/// * `username_from_url` - the username that was embedded in the url, or `None` +/// * `username_from_url` - the username that was embedded in the URL, or `None` /// if it was not included. -/// * `allowed_types` - a bitmask stating which cred types are ok to return. -pub type Credentials<'a> = FnMut(&str, Option<&str>, CredentialType) - -> Result + 'a; - -/// Callback to be invoked while a transfer is in progress. -/// -/// This callback will be periodically called with updates to the progress of -/// the transfer so far. The return value indicates whether the transfer should -/// continue. A return value of `false` will cancel the transfer. -/// -/// * `progress` - the progress being made so far. -pub type TransferProgress<'a> = FnMut(Progress) -> bool + 'a; +/// * `allowed_types` - a bitmask stating which cred types are OK to return. +pub type Credentials<'a> = + dyn FnMut(&str, Option<&str>, CredentialType) -> Result + 'a; /// Callback for receiving messages delivered by the transport. /// /// The return value indicates whether the network operation should continue. -pub type TransportMessage<'a> = FnMut(&[u8]) -> bool + 'a; +pub type TransportMessage<'a> = dyn FnMut(&[u8]) -> bool + 'a; /// Callback for whenever a reference is updated locally. -pub type UpdateTips<'a> = FnMut(&str, Oid, Oid) -> bool + 'a; +pub type UpdateTips<'a> = dyn FnMut(&str, Oid, Oid) -> bool + 'a; /// Callback for a custom certificate check. /// -/// The first argument is the certificate receved on the connection. +/// The first argument is the certificate received on the connection. /// Certificates are typically either an SSH or X509 certificate. /// /// The second argument is the hostname for the connection is passed as the last /// argument. -pub type CertificateCheck<'a> = FnMut(&Cert, &str) -> bool + 'a; +pub type CertificateCheck<'a> = + dyn FnMut(&Cert<'_>, &str) -> Result + 'a; + +/// The return value for the [`RemoteCallbacks::certificate_check`] callback. +pub enum CertificateCheckStatus { + /// Indicates that the certificate should be accepted. + CertificateOk, + /// Indicates that the certificate callback is neither accepting nor + /// rejecting the certificate. The result of the certificate checks + /// built-in to libgit2 will be used instead. + CertificatePassthrough, +} + +/// Callback for each updated reference on push. +/// +/// The first argument here is the `refname` of the reference, and the second is +/// the status message sent by a server. If the status is `Some` then the update +/// was rejected by the remote server with a reason why. +pub type PushUpdateReference<'a> = dyn FnMut(&str, Option<&str>) -> Result<(), Error> + 'a; + +/// Callback for push transfer progress +/// +/// Parameters: +/// * current +/// * total +/// * bytes +pub type PushTransferProgress<'a> = dyn FnMut(usize, usize, usize) + 'a; + +/// Callback for pack progress +/// +/// Be aware that this is called inline with pack building operations, +/// so performance may be affected. +/// +/// Parameters: +/// * stage +/// * current +/// * total +pub type PackProgress<'a> = dyn FnMut(PackBuilderStage, usize, usize) + 'a; + +/// The callback is called once between the negotiation step and the upload. +/// +/// The argument is a slice containing the updates which will be sent as +/// commands to the destination. +/// +/// The push is cancelled if an error is returned. +pub type PushNegotiation<'a> = dyn FnMut(&[PushUpdate<'_>]) -> Result<(), Error> + 'a; + +impl<'a> Default for RemoteCallbacks<'a> { + fn default() -> Self { + Self::new() + } +} impl<'a> RemoteCallbacks<'a> { /// Creates a new set of empty callbacks @@ -74,16 +112,40 @@ impl<'a> RemoteCallbacks<'a> { RemoteCallbacks { credentials: None, progress: None, + pack_progress: None, sideband_progress: None, update_tips: None, certificate_check: None, + push_update_reference: None, + push_progress: None, + push_negotiation: None, } } /// The callback through which to fetch credentials if required. + /// + /// # Example + /// + /// Prepare a callback to authenticate using the `$HOME/.ssh/id_rsa` SSH key, and + /// extracting the username from the URL (i.e. git@github.com:rust-lang/git2-rs.git): + /// + /// ```no_run + /// use git2::{Cred, RemoteCallbacks}; + /// use std::env; + /// + /// let mut callbacks = RemoteCallbacks::new(); + /// callbacks.credentials(|_url, username_from_url, _allowed_types| { + /// Cred::ssh_key( + /// username_from_url.unwrap(), + /// None, + /// std::path::Path::new(&format!("{}/.ssh/id_rsa", env::var("HOME").unwrap())), + /// None, + /// ) + /// }); + /// ``` pub fn credentials(&mut self, cb: F) -> &mut RemoteCallbacks<'a> - where F: FnMut(&str, Option<&str>, CredentialType) - -> Result + 'a + where + F: FnMut(&str, Option<&str>, CredentialType) -> Result + 'a, { self.credentials = Some(Box::new(cb) as Box>); self @@ -91,17 +153,21 @@ impl<'a> RemoteCallbacks<'a> { /// The callback through which progress is monitored. pub fn transfer_progress(&mut self, cb: F) -> &mut RemoteCallbacks<'a> - where F: FnMut(Progress) -> bool + 'a { - self.progress = Some(Box::new(cb) as Box>); + where + F: FnMut(Progress<'_>) -> bool + 'a, + { + self.progress = Some(Box::new(cb) as Box>); self } /// Textual progress from the remote. /// /// Text sent over the progress side-band will be passed to this function - /// (this is the 'counting objects' output. + /// (this is the 'counting objects' output). pub fn sideband_progress(&mut self, cb: F) -> &mut RemoteCallbacks<'a> - where F: FnMut(&[u8]) -> bool + 'a { + where + F: FnMut(&[u8]) -> bool + 'a, + { self.sideband_progress = Some(Box::new(cb) as Box>); self } @@ -109,7 +175,9 @@ impl<'a> RemoteCallbacks<'a> { /// Each time a reference is updated locally, the callback will be called /// with information about it. pub fn update_tips(&mut self, cb: F) -> &mut RemoteCallbacks<'a> - where F: FnMut(&str, Oid, Oid) -> bool + 'a { + where + F: FnMut(&str, Oid, Oid) -> bool + 'a, + { self.update_tips = Some(Box::new(cb) as Box>); self } @@ -118,11 +186,70 @@ impl<'a> RemoteCallbacks<'a> { /// let the caller make the final decision of whether to allow the /// connection to proceed. pub fn certificate_check(&mut self, cb: F) -> &mut RemoteCallbacks<'a> - where F: FnMut(&Cert, &str) -> bool + 'a + where + F: FnMut(&Cert<'_>, &str) -> Result + 'a, { self.certificate_check = Some(Box::new(cb) as Box>); self } + + /// Set a callback to get invoked for each updated reference on a push. + /// + /// The first argument to the callback is the name of the reference and the + /// second is a status message sent by the server. If the status is `Some` + /// then the push was rejected. + pub fn push_update_reference(&mut self, cb: F) -> &mut RemoteCallbacks<'a> + where + F: FnMut(&str, Option<&str>) -> Result<(), Error> + 'a, + { + self.push_update_reference = Some(Box::new(cb) as Box>); + self + } + + /// The callback through which progress of push transfer is monitored + /// + /// Parameters: + /// * current + /// * total + /// * bytes + pub fn push_transfer_progress(&mut self, cb: F) -> &mut RemoteCallbacks<'a> + where + F: FnMut(usize, usize, usize) + 'a, + { + self.push_progress = Some(Box::new(cb) as Box>); + self + } + + /// Function to call with progress information during pack building. + /// + /// Be aware that this is called inline with pack building operations, + /// so performance may be affected. + /// + /// Parameters: + /// * stage + /// * current + /// * total + pub fn pack_progress(&mut self, cb: F) -> &mut RemoteCallbacks<'a> + where + F: FnMut(PackBuilderStage, usize, usize) + 'a, + { + self.pack_progress = Some(Box::new(cb) as Box>); + self + } + + /// The callback is called once between the negotiation step and the upload. + /// + /// The argument to the callback is a slice containing the updates which + /// will be sent as commands to the destination. + /// + /// The push is cancelled if the callback returns an error. + pub fn push_negotiation(&mut self, cb: F) -> &mut RemoteCallbacks<'a> + where + F: FnMut(&[PushUpdate<'_>]) -> Result<(), Error> + 'a, + { + self.push_negotiation = Some(Box::new(cb) as Box>); + self + } } impl<'a> Binding for RemoteCallbacks<'a> { @@ -134,121 +261,76 @@ impl<'a> Binding for RemoteCallbacks<'a> { fn raw(&self) -> raw::git_remote_callbacks { unsafe { let mut callbacks: raw::git_remote_callbacks = mem::zeroed(); - assert_eq!(raw::git_remote_init_callbacks(&mut callbacks, - raw::GIT_REMOTE_CALLBACKS_VERSION), 0); + assert_eq!( + raw::git_remote_init_callbacks(&mut callbacks, raw::GIT_REMOTE_CALLBACKS_VERSION), + 0 + ); if self.progress.is_some() { - let f: raw::git_transfer_progress_cb = transfer_progress_cb; - callbacks.transfer_progress = Some(f); + callbacks.transfer_progress = Some(transfer_progress_cb); } if self.credentials.is_some() { - let f: raw::git_cred_acquire_cb = credentials_cb; - callbacks.credentials = Some(f); + callbacks.credentials = Some(credentials_cb); } if self.sideband_progress.is_some() { - let f: raw::git_transport_message_cb = sideband_progress_cb; - callbacks.sideband_progress = Some(f); + callbacks.sideband_progress = Some(sideband_progress_cb); } if self.certificate_check.is_some() { - let f: raw::git_transport_certificate_check_cb = - certificate_check_cb; - callbacks.certificate_check = Some(f); + callbacks.certificate_check = Some(certificate_check_cb); + } + if self.push_update_reference.is_some() { + callbacks.push_update_reference = Some(push_update_reference_cb); + } + if self.push_progress.is_some() { + callbacks.push_transfer_progress = Some(push_transfer_progress_cb); + } + if self.pack_progress.is_some() { + callbacks.pack_progress = Some(pack_progress_cb); } if self.update_tips.is_some() { - let f: extern fn(*const c_char, *const raw::git_oid, - *const raw::git_oid, *mut c_void) -> c_int - = update_tips_cb; + let f: extern "C" fn( + *const c_char, + *const raw::git_oid, + *const raw::git_oid, + *mut c_void, + ) -> c_int = update_tips_cb; callbacks.update_tips = Some(f); } + if self.push_negotiation.is_some() { + callbacks.push_negotiation = Some(push_negotiation_cb); + } callbacks.payload = self as *const _ as *mut _; - return callbacks; - } - } -} - -impl<'a> Progress<'a> { - /// Number of objects in the packfile being downloaded - pub fn total_objects(&self) -> usize { - unsafe { (*self.raw()).total_objects as usize } - } - /// Received objects that have been hashed - pub fn indexed_objects(&self) -> usize { - unsafe { (*self.raw()).indexed_objects as usize } - } - /// Objects which have been downloaded - pub fn received_objects(&self) -> usize { - unsafe { (*self.raw()).received_objects as usize } - } - /// Locally-available objects that have been injected in order to fix a thin - /// pack. - pub fn local_objects(&self) -> usize { - unsafe { (*self.raw()).local_objects as usize } - } - /// Number of deltas in the packfile being downloaded - pub fn total_deltas(&self) -> usize { - unsafe { (*self.raw()).total_deltas as usize } - } - /// Received deltas that have been hashed. - pub fn indexed_deltas(&self) -> usize { - unsafe { (*self.raw()).indexed_deltas as usize } - } - /// Size of the packfile received up to now - pub fn received_bytes(&self) -> usize { - unsafe { (*self.raw()).received_bytes as usize } - } - - /// Convert this to an owned version of `Progress`. - pub fn to_owned(&self) -> Progress<'static> { - Progress { - raw: ProgressState::Owned(unsafe { *self.raw() }), - _marker: marker::PhantomData, + callbacks } } } -impl<'a> Binding for Progress<'a> { - type Raw = *const raw::git_transfer_progress; - unsafe fn from_raw(raw: *const raw::git_transfer_progress) - -> Progress<'a> { - Progress { - raw: ProgressState::Borrowed(raw), - _marker: marker::PhantomData, - } - } - - fn raw(&self) -> *const raw::git_transfer_progress { - match self.raw { - ProgressState::Borrowed(raw) => raw, - ProgressState::Owned(ref raw) => raw as *const _, - } - } -} - -extern fn credentials_cb(ret: *mut *mut raw::git_cred, - url: *const c_char, - username_from_url: *const c_char, - allowed_types: c_uint, - payload: *mut c_void) -> c_int { +extern "C" fn credentials_cb( + ret: *mut *mut raw::git_cred, + url: *const c_char, + username_from_url: *const c_char, + allowed_types: c_uint, + payload: *mut c_void, +) -> c_int { unsafe { let ok = panic::wrap(|| { - let payload = &mut *(payload as *mut RemoteCallbacks); - let callback = try!(payload.credentials.as_mut() - .ok_or(raw::GIT_PASSTHROUGH as c_int)); - *ret = 0 as *mut raw::git_cred; - let url = try!(str::from_utf8(CStr::from_ptr(url).to_bytes()) - .map_err(|_| raw::GIT_PASSTHROUGH as c_int)); - let username_from_url = match ::opt_bytes(&url, username_from_url) { + let payload = &mut *(payload as *mut RemoteCallbacks<'_>); + let callback = payload + .credentials + .as_mut() + .ok_or(raw::GIT_PASSTHROUGH as c_int)?; + *ret = ptr::null_mut(); + let url = str::from_utf8(CStr::from_ptr(url).to_bytes()) + .map_err(|_| raw::GIT_PASSTHROUGH as c_int)?; + let username_from_url = match crate::opt_bytes(&url, username_from_url) { Some(username) => { - Some(try!(str::from_utf8(username) - .map_err(|_| raw::GIT_PASSTHROUGH as c_int))) + Some(str::from_utf8(username).map_err(|_| raw::GIT_PASSTHROUGH as c_int)?) } None => None, }; let cred_type = CredentialType::from_bits_truncate(allowed_types as u32); - callback(url, username_from_url, cred_type).map_err(|e| { - e.raw_code() as c_int - }) + callback(url, username_from_url, cred_type).map_err(|e| e.raw_set_git_error()) }); match ok { Some(Ok(cred)) => { @@ -267,10 +349,12 @@ extern fn credentials_cb(ret: *mut *mut raw::git_cred, } } -extern fn transfer_progress_cb(stats: *const raw::git_transfer_progress, - payload: *mut c_void) -> c_int { +extern "C" fn transfer_progress_cb( + stats: *const raw::git_indexer_progress, + payload: *mut c_void, +) -> c_int { let ok = panic::wrap(|| unsafe { - let payload = &mut *(payload as *mut RemoteCallbacks); + let payload = &mut *(payload as *mut RemoteCallbacks<'_>); let callback = match payload.progress { Some(ref mut c) => c, None => return true, @@ -278,14 +362,16 @@ extern fn transfer_progress_cb(stats: *const raw::git_transfer_progress, let progress = Binding::from_raw(stats); callback(progress) }); - if ok == Some(true) {0} else {-1} + if ok == Some(true) { + 0 + } else { + -1 + } } -extern fn sideband_progress_cb(str: *const c_char, - len: c_int, - payload: *mut c_void) -> c_int { +extern "C" fn sideband_progress_cb(str: *const c_char, len: c_int, payload: *mut c_void) -> c_int { let ok = panic::wrap(|| unsafe { - let payload = &mut *(payload as *mut RemoteCallbacks); + let payload = &mut *(payload as *mut RemoteCallbacks<'_>); let callback = match payload.sideband_progress { Some(ref mut c) => c, None => return true, @@ -293,42 +379,148 @@ extern fn sideband_progress_cb(str: *const c_char, let buf = slice::from_raw_parts(str as *const u8, len as usize); callback(buf) }); - if ok == Some(true) {0} else {-1} + if ok == Some(true) { + 0 + } else { + -1 + } } -extern fn update_tips_cb(refname: *const c_char, - a: *const raw::git_oid, - b: *const raw::git_oid, - data: *mut c_void) -> c_int { +extern "C" fn update_tips_cb( + refname: *const c_char, + a: *const raw::git_oid, + b: *const raw::git_oid, + data: *mut c_void, +) -> c_int { let ok = panic::wrap(|| unsafe { - let payload = &mut *(data as *mut RemoteCallbacks); + let payload = &mut *(data as *mut RemoteCallbacks<'_>); let callback = match payload.update_tips { Some(ref mut c) => c, None => return true, }; - let refname = str::from_utf8(CStr::from_ptr(refname).to_bytes()) - .unwrap(); + let refname = str::from_utf8(CStr::from_ptr(refname).to_bytes()).unwrap(); let a = Binding::from_raw(a); let b = Binding::from_raw(b); callback(refname, a, b) }); - if ok == Some(true) {0} else {-1} + if ok == Some(true) { + 0 + } else { + -1 + } } -extern fn certificate_check_cb(cert: *mut raw::git_cert, - _valid: c_int, - hostname: *const c_char, - data: *mut c_void) -> c_int { +extern "C" fn certificate_check_cb( + cert: *mut raw::git_cert, + _valid: c_int, + hostname: *const c_char, + data: *mut c_void, +) -> c_int { let ok = panic::wrap(|| unsafe { - let payload = &mut *(data as *mut RemoteCallbacks); + let payload = &mut *(data as *mut RemoteCallbacks<'_>); let callback = match payload.certificate_check { Some(ref mut c) => c, - None => return true, + None => return Ok(CertificateCheckStatus::CertificatePassthrough), }; let cert = Binding::from_raw(cert); - let hostname = str::from_utf8(CStr::from_ptr(hostname).to_bytes()) - .unwrap(); + let hostname = str::from_utf8(CStr::from_ptr(hostname).to_bytes()).unwrap(); callback(&cert, hostname) }); - if ok == Some(true) {0} else {-1} + match ok { + Some(Ok(CertificateCheckStatus::CertificateOk)) => 0, + Some(Ok(CertificateCheckStatus::CertificatePassthrough)) => raw::GIT_PASSTHROUGH as c_int, + Some(Err(e)) => unsafe { e.raw_set_git_error() }, + None => { + // Panic. The *should* get resumed by some future call to check(). + -1 + } + } +} + +extern "C" fn push_update_reference_cb( + refname: *const c_char, + status: *const c_char, + data: *mut c_void, +) -> c_int { + panic::wrap(|| unsafe { + let payload = &mut *(data as *mut RemoteCallbacks<'_>); + let callback = match payload.push_update_reference { + Some(ref mut c) => c, + None => return 0, + }; + let refname = str::from_utf8(CStr::from_ptr(refname).to_bytes()).unwrap(); + let status = if status.is_null() { + None + } else { + Some(str::from_utf8(CStr::from_ptr(status).to_bytes()).unwrap()) + }; + match callback(refname, status) { + Ok(()) => 0, + Err(e) => e.raw_set_git_error(), + } + }) + .unwrap_or(-1) +} + +extern "C" fn push_transfer_progress_cb( + progress: c_uint, + total: c_uint, + bytes: size_t, + data: *mut c_void, +) -> c_int { + panic::wrap(|| unsafe { + let payload = &mut *(data as *mut RemoteCallbacks<'_>); + let callback = match payload.push_progress { + Some(ref mut c) => c, + None => return 0, + }; + + callback(progress as usize, total as usize, bytes as usize); + + 0 + }) + .unwrap_or(-1) +} + +extern "C" fn pack_progress_cb( + stage: raw::git_packbuilder_stage_t, + current: c_uint, + total: c_uint, + data: *mut c_void, +) -> c_int { + panic::wrap(|| unsafe { + let payload = &mut *(data as *mut RemoteCallbacks<'_>); + let callback = match payload.pack_progress { + Some(ref mut c) => c, + None => return 0, + }; + + let stage = Binding::from_raw(stage); + + callback(stage, current as usize, total as usize); + + 0 + }) + .unwrap_or(-1) +} + +extern "C" fn push_negotiation_cb( + updates: *mut *const raw::git_push_update, + len: size_t, + payload: *mut c_void, +) -> c_int { + panic::wrap(|| unsafe { + let payload = &mut *(payload as *mut RemoteCallbacks<'_>); + let callback = match payload.push_negotiation { + Some(ref mut c) => c, + None => return 0, + }; + + let updates = slice::from_raw_parts(updates as *mut PushUpdate<'_>, len); + match callback(updates) { + Ok(()) => 0, + Err(e) => e.raw_set_git_error(), + } + }) + .unwrap_or(-1) } diff --git a/src/repo.rs b/src/repo.rs index 3b51613e95..07d3a7c55f 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -1,22 +1,101 @@ +use libc::{c_char, c_int, c_uint, c_void, size_t}; use std::env; use std::ffi::{CStr, CString, OsStr}; -use std::iter::IntoIterator; use std::mem; -use std::path::Path; +use std::path::{Path, PathBuf}; +use std::ptr; use std::str; -use libc::{c_int, c_char, size_t, c_void, c_uint}; - -use {raw, Revspec, Error, init, Object, RepositoryOpenFlags, RepositoryState, Remote, Buf}; -use {ResetType, Signature, Reference, References, Submodule, Blame, BlameOptions}; -use {Branches, BranchType, Index, Config, Oid, Blob, Branch, Commit, Tree}; -use {AnnotatedCommit, MergeOptions, SubmoduleIgnore, SubmoduleStatus}; -use {ObjectType, Tag, Note, Notes, StatusOptions, Statuses, Status, Revwalk}; -use {RevparseMode, RepositoryInitMode, Reflog, IntoCString, Describe}; -use {DescribeOptions, TreeBuilder, Diff, DiffOptions, PackBuilder}; -use build::{RepoBuilder, CheckoutBuilder}; -use string_array::StringArray; -use oid_array::OidArray; -use util::{self, Binding}; + +use crate::build::{CheckoutBuilder, RepoBuilder}; +use crate::diff::{ + binary_cb_c, file_cb_c, hunk_cb_c, line_cb_c, BinaryCb, DiffCallbacks, FileCb, HunkCb, LineCb, +}; +use crate::oid_array::OidArray; +use crate::stash::{stash_cb, StashApplyOptions, StashCbData, StashSaveOptions}; +use crate::string_array::StringArray; +use crate::tagforeach::{tag_foreach_cb, TagForeachCB, TagForeachData}; +use crate::util::{self, path_to_repo_path, Binding}; +use crate::worktree::{Worktree, WorktreeAddOptions}; +use crate::CherrypickOptions; +use crate::RevertOptions; +use crate::{mailmap::Mailmap, panic}; +use crate::{ + raw, AttrCheckFlags, Buf, Error, Object, Remote, RepositoryOpenFlags, RepositoryState, Revspec, + StashFlags, +}; +use crate::{ + AnnotatedCommit, MergeAnalysis, MergeFileOptions, MergeFileResult, MergeOptions, + MergePreference, SubmoduleIgnore, SubmoduleStatus, SubmoduleUpdate, +}; +use crate::{ApplyLocation, ApplyOptions, Rebase, RebaseOptions}; +use crate::{Blame, BlameOptions, Reference, References, ResetType, Signature, Submodule}; +use crate::{ + Blob, BlobWriter, Branch, BranchType, Branches, Commit, Config, Index, IndexEntry, Oid, Tree, +}; +use crate::{Describe, IntoCString, Reflog, RepositoryInitMode, RevparseMode}; +use crate::{DescribeOptions, Diff, DiffOptions, Odb, PackBuilder, TreeBuilder}; +use crate::{Note, Notes, ObjectType, Revwalk, Status, StatusOptions, Statuses, Tag, Transaction}; + +type MergeheadForeachCb<'a> = dyn FnMut(&Oid) -> bool + 'a; +type FetchheadForeachCb<'a> = dyn FnMut(&str, &[u8], &Oid, bool) -> bool + 'a; + +struct FetchheadForeachCbData<'a> { + callback: &'a mut FetchheadForeachCb<'a>, +} + +struct MergeheadForeachCbData<'a> { + callback: &'a mut MergeheadForeachCb<'a>, +} + +extern "C" fn mergehead_foreach_cb(oid: *const raw::git_oid, payload: *mut c_void) -> c_int { + panic::wrap(|| unsafe { + let data = &mut *(payload as *mut MergeheadForeachCbData<'_>); + let res = { + let callback = &mut data.callback; + callback(&Binding::from_raw(oid)) + }; + + if res { + 0 + } else { + 1 + } + }) + .unwrap_or(1) +} + +extern "C" fn fetchhead_foreach_cb( + ref_name: *const c_char, + remote_url: *const c_char, + oid: *const raw::git_oid, + is_merge: c_uint, + payload: *mut c_void, +) -> c_int { + panic::wrap(|| unsafe { + let data = &mut *(payload as *mut FetchheadForeachCbData<'_>); + let res = { + let callback = &mut data.callback; + + assert!(!ref_name.is_null()); + assert!(!remote_url.is_null()); + assert!(!oid.is_null()); + + let ref_name = str::from_utf8(CStr::from_ptr(ref_name).to_bytes()).unwrap(); + let remote_url = CStr::from_ptr(remote_url).to_bytes(); + let oid = Binding::from_raw(oid); + let is_merge = is_merge == 1; + + callback(&ref_name, remote_url, &oid, is_merge) + }; + + if res { + 0 + } else { + 1 + } + }) + .unwrap_or(1) +} /// An owned git repository, representing all state associated with the /// underlying filesystem. @@ -51,48 +130,113 @@ impl Repository { /// /// The path can point to either a normal or bare repository. pub fn open>(path: P) -> Result { - init(); - let path = try!(path.as_ref().into_c_string()); - let mut ret = 0 as *mut raw::git_repository; + crate::init(); + // Normal file path OK (does not need Windows conversion). + let path = path.as_ref().into_c_string()?; + let mut ret = ptr::null_mut(); unsafe { try_call!(raw::git_repository_open(&mut ret, path)); Ok(Binding::from_raw(ret)) } } + /// Attempt to open an already-existing bare repository at `path`. + /// + /// The path can point to only a bare repository. + pub fn open_bare>(path: P) -> Result { + crate::init(); + // Normal file path OK (does not need Windows conversion). + let path = path.as_ref().into_c_string()?; + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_repository_open_bare(&mut ret, path)); + Ok(Binding::from_raw(ret)) + } + } + + /// Find and open an existing repository, respecting git environment + /// variables. This acts like `open_ext` with the + /// [FROM_ENV](RepositoryOpenFlags::FROM_ENV) flag, but additionally respects `$GIT_DIR`. + /// With `$GIT_DIR` unset, this will search for a repository starting in + /// the current directory. + pub fn open_from_env() -> Result { + crate::init(); + let mut ret = ptr::null_mut(); + let flags = raw::GIT_REPOSITORY_OPEN_FROM_ENV; + unsafe { + try_call!(raw::git_repository_open_ext( + &mut ret, + ptr::null(), + flags as c_uint, + ptr::null() + )); + Ok(Binding::from_raw(ret)) + } + } + /// Find and open an existing repository, with additional options. /// - /// If flags contains REPOSITORY_OPEN_NO_SEARCH, the path must point + /// If flags contains [NO_SEARCH](RepositoryOpenFlags::NO_SEARCH), the path must point /// directly to a repository; otherwise, this may point to a subdirectory /// of a repository, and `open_ext` will search up through parent /// directories. /// - /// If flags contains REPOSITORY_OPEN_CROSS_FS, the search through parent + /// If flags contains [CROSS_FS](RepositoryOpenFlags::CROSS_FS), the search through parent /// directories will not cross a filesystem boundary (detected when the /// stat st_dev field changes). /// - /// If flags contains REPOSITORY_OPEN_BARE, force opening the repository as + /// If flags contains [BARE](RepositoryOpenFlags::BARE), force opening the repository as /// bare even if it isn't, ignoring any working directory, and defer /// loading the repository configuration for performance. /// + /// If flags contains [NO_DOTGIT](RepositoryOpenFlags::NO_DOTGIT), don't try appending + /// `/.git` to `path`. + /// + /// If flags contains [FROM_ENV](RepositoryOpenFlags::FROM_ENV), `open_ext` will ignore + /// other flags and `ceiling_dirs`, and respect the same environment + /// variables git does. Note, however, that `path` overrides `$GIT_DIR`; to + /// respect `$GIT_DIR` as well, use `open_from_env`. + /// /// ceiling_dirs specifies a list of paths that the search through parent /// directories will stop before entering. Use the functions in std::env - /// to construct or manipulate such a path list. - pub fn open_ext(path: P, - flags: RepositoryOpenFlags, - ceiling_dirs: I) - -> Result - where P: AsRef, O: AsRef, I: IntoIterator { - init(); - let path = try!(path.as_ref().into_c_string()); - let ceiling_dirs_os = try!(env::join_paths(ceiling_dirs)); - let ceiling_dirs = try!(ceiling_dirs_os.into_c_string()); - let mut ret = 0 as *mut raw::git_repository; - unsafe { - try_call!(raw::git_repository_open_ext(&mut ret, - path, - flags.bits() as c_uint, - ceiling_dirs)); + /// to construct or manipulate such a path list. (You can use `&[] as + /// &[&std::ffi::OsStr]` as an argument if there are no ceiling + /// directories.) + pub fn open_ext( + path: P, + flags: RepositoryOpenFlags, + ceiling_dirs: I, + ) -> Result + where + P: AsRef, + O: AsRef, + I: IntoIterator, + { + crate::init(); + // Normal file path OK (does not need Windows conversion). + let path = path.as_ref().into_c_string()?; + let ceiling_dirs_os = env::join_paths(ceiling_dirs)?; + let ceiling_dirs = ceiling_dirs_os.into_c_string()?; + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_repository_open_ext( + &mut ret, + path, + flags.bits() as c_uint, + ceiling_dirs + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Attempt to open an already-existing repository from a worktree. + pub fn open_from_worktree(worktree: &Worktree) -> Result { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_repository_open_from_worktree( + &mut ret, + worktree.raw() + )); Ok(Binding::from_raw(ret)) } } @@ -103,16 +247,48 @@ impl Repository { /// until it finds a repository. pub fn discover>(path: P) -> Result { // TODO: this diverges significantly from the libgit2 API - init(); + crate::init(); let buf = Buf::new(); - let path = try!(path.as_ref().into_c_string()); + // Normal file path OK (does not need Windows conversion). + let path = path.as_ref().into_c_string()?; unsafe { - try_call!(raw::git_repository_discover(buf.raw(), path, 1, - 0 as *const _)); + try_call!(raw::git_repository_discover( + buf.raw(), + path, + 1, + ptr::null() + )); } Repository::open(util::bytes2path(&*buf)) } + /// Attempt to find the path to a git repo for a given path + /// + /// This starts at `path` and looks up the filesystem hierarchy + /// until it finds a repository, stopping if it finds a member of ceiling_dirs + pub fn discover_path, I, O>(path: P, ceiling_dirs: I) -> Result + where + O: AsRef, + I: IntoIterator, + { + crate::init(); + let buf = Buf::new(); + // Normal file path OK (does not need Windows conversion). + let path = path.as_ref().into_c_string()?; + let ceiling_dirs_os = env::join_paths(ceiling_dirs)?; + let ceiling_dirs = ceiling_dirs_os.into_c_string()?; + unsafe { + try_call!(raw::git_repository_discover( + buf.raw(), + path, + 1, + ceiling_dirs + )); + } + + Ok(util::bytes2path(&*buf).to_path_buf()) + } + /// Creates a new repository in the specified folder. /// /// This by default will create any necessary directories to create the @@ -129,14 +305,17 @@ impl Repository { Repository::init_opts(path, RepositoryInitOptions::new().bare(true)) } - /// Creates a new `--bare` repository in the specified folder. + /// Creates a new repository in the specified folder with the given options. /// - /// The folder must exist prior to invoking this function. - pub fn init_opts>(path: P, opts: &RepositoryInitOptions) - -> Result { - init(); - let path = try!(path.as_ref().into_c_string()); - let mut ret = 0 as *mut raw::git_repository; + /// See `RepositoryInitOptions` struct for more information. + pub fn init_opts>( + path: P, + opts: &RepositoryInitOptions, + ) -> Result { + crate::init(); + // Normal file path OK (does not need Windows conversion). + let path = path.as_ref().into_c_string()?; + let mut ret = ptr::null_mut(); unsafe { let mut opts = opts.raw(); try_call!(raw::git_repository_init_ext(&mut ret, path, &mut opts)); @@ -148,23 +327,62 @@ impl Repository { /// /// See the `RepoBuilder` struct for more information. This function will /// delegate to a fresh `RepoBuilder` - pub fn clone>(url: &str, into: P) - -> Result { - ::init(); + pub fn clone>(url: &str, into: P) -> Result { + crate::init(); RepoBuilder::new().clone(url, into.as_ref()) } + /// Clone a remote repository, initialize and update its submodules + /// recursively. + /// + /// This is similar to `git clone --recursive`. + pub fn clone_recurse>(url: &str, into: P) -> Result { + let repo = Repository::clone(url, into)?; + repo.update_submodules()?; + Ok(repo) + } + + /// Attempt to wrap an object database as a repository. + pub fn from_odb(odb: Odb<'_>) -> Result { + crate::init(); + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_repository_wrap_odb(&mut ret, odb.raw())); + Ok(Binding::from_raw(ret)) + } + } + + /// Update submodules recursively. + /// + /// Uninitialized submodules will be initialized. + fn update_submodules(&self) -> Result<(), Error> { + fn add_subrepos(repo: &Repository, list: &mut Vec) -> Result<(), Error> { + for mut subm in repo.submodules()? { + subm.update(true, None)?; + list.push(subm.open()?); + } + Ok(()) + } + + let mut repos = Vec::new(); + add_subrepos(self, &mut repos)?; + while let Some(repo) = repos.pop() { + add_subrepos(&repo, &mut repos)?; + } + Ok(()) + } + /// Execute a rev-parse operation against the `spec` listed. /// /// The resulting revision specification is returned, or an error is /// returned if one occurs. - pub fn revparse(&self, spec: &str) -> Result { + pub fn revparse(&self, spec: &str) -> Result, Error> { let mut raw = raw::git_revspec { - from: 0 as *mut _, - to: 0 as *mut _, + from: ptr::null_mut(), + to: ptr::null_mut(), flags: 0, }; - let spec = try!(CString::new(spec)); + let spec = CString::new(spec)?; unsafe { try_call!(raw::git_revparse(&mut raw, self.raw, spec)); let to = Binding::from_raw_opt(raw.to); @@ -175,9 +393,9 @@ impl Repository { } /// Find a single object, as specified by a revision string. - pub fn revparse_single(&self, spec: &str) -> Result { - let spec = try!(CString::new(spec)); - let mut obj = 0 as *mut raw::git_object; + pub fn revparse_single(&self, spec: &str) -> Result, Error> { + let spec = CString::new(spec)?; + let mut obj = ptr::null_mut(); unsafe { try_call!(raw::git_revparse_single(&mut obj, self.raw, spec)); assert!(!obj.is_null()); @@ -188,20 +406,23 @@ impl Repository { /// Find a single object and intermediate reference by a revision string. /// /// See `man gitrevisions`, or - /// http://git-scm.com/docs/git-rev-parse.html#_specifying_revisions for + /// for /// information on the syntax accepted. /// /// In some cases (`@{<-n>}` or `@{upstream}`), the expression /// may point to an intermediate reference. When such expressions are being /// passed in, this intermediate reference is returned. - pub fn revparse_ext(&self, spec: &str) - -> Result<(Object, Option), Error> { - let spec = try!(CString::new(spec)); - let mut git_obj = 0 as *mut raw::git_object; - let mut git_ref = 0 as *mut raw::git_reference; - unsafe { - try_call!(raw::git_revparse_ext(&mut git_obj, &mut git_ref, - self.raw, spec)); + pub fn revparse_ext(&self, spec: &str) -> Result<(Object<'_>, Option>), Error> { + let spec = CString::new(spec)?; + let mut git_obj = ptr::null_mut(); + let mut git_ref = ptr::null_mut(); + unsafe { + try_call!(raw::git_revparse_ext( + &mut git_obj, + &mut git_ref, + self.raw, + spec + )); assert!(!git_obj.is_null()); Ok((Binding::from_raw(git_obj), Binding::from_raw_opt(git_ref))) } @@ -217,11 +438,14 @@ impl Repository { unsafe { raw::git_repository_is_shallow(self.raw) == 1 } } + /// Tests whether this repository is a worktree. + pub fn is_worktree(&self) -> bool { + unsafe { raw::git_repository_is_worktree(self.raw) == 1 } + } + /// Tests whether this repository is empty. pub fn is_empty(&self) -> Result { - let empty = unsafe { - try_call!(raw::git_repository_is_empty(self.raw)) - }; + let empty = unsafe { try_call!(raw::git_repository_is_empty(self.raw)) }; Ok(empty == 1) } @@ -230,7 +454,19 @@ impl Repository { pub fn path(&self) -> &Path { unsafe { let ptr = raw::git_repository_path(self.raw); - util::bytes2path(::opt_bytes(self, ptr).unwrap()) + util::bytes2path(crate::opt_bytes(self, ptr).unwrap()) + } + } + + /// Returns the path of the shared common directory for this repository. + /// + /// If the repository is bare, it is the root directory for the repository. + /// If the repository is a worktree, it is the parent repo's gitdir. + /// Otherwise, it is the gitdir. + pub fn commondir(&self) -> &Path { + unsafe { + let ptr = raw::git_repository_commondir(self.raw); + util::bytes2path(crate::opt_bytes(self, ptr).unwrap()) } } @@ -281,12 +517,15 @@ impl Repository { /// If `update_link` is true, create/update the gitlink file in the workdir /// and set config "core.worktree" (if workdir is not the parent of the .git /// directory). - pub fn set_workdir(&self, path: &Path, update_gitlink: bool) - -> Result<(), Error> { - let path = try!(path.into_c_string()); + pub fn set_workdir(&self, path: &Path, update_gitlink: bool) -> Result<(), Error> { + // Normal file path OK (does not need Windows conversion). + let path = path.into_c_string()?; unsafe { - try_call!(raw::git_repository_set_workdir(self.raw(), path, - update_gitlink)); + try_call!(raw::git_repository_set_workdir( + self.raw(), + path, + update_gitlink + )); } Ok(()) } @@ -303,13 +542,53 @@ impl Repository { /// /// If there is no namespace, `None` is returned. pub fn namespace_bytes(&self) -> Option<&[u8]> { - unsafe { ::opt_bytes(self, raw::git_repository_get_namespace(self.raw)) } + unsafe { crate::opt_bytes(self, raw::git_repository_get_namespace(self.raw)) } + } + + /// Set the active namespace for this repository. + pub fn set_namespace(&self, namespace: &str) -> Result<(), Error> { + self.set_namespace_bytes(namespace.as_bytes()) + } + + /// Set the active namespace for this repository as a byte array. + pub fn set_namespace_bytes(&self, namespace: &[u8]) -> Result<(), Error> { + unsafe { + let namespace = CString::new(namespace)?; + try_call!(raw::git_repository_set_namespace(self.raw, namespace)); + Ok(()) + } + } + + /// Remove the active namespace for this repository. + pub fn remove_namespace(&self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_repository_set_namespace(self.raw, ptr::null())); + Ok(()) + } + } + + /// Retrieves the Git merge message. + /// Remember to remove the message when finished. + pub fn message(&self) -> Result { + unsafe { + let buf = Buf::new(); + try_call!(raw::git_repository_message(buf.raw(), self.raw)); + Ok(str::from_utf8(&buf).unwrap().to_string()) + } + } + + /// Remove the Git merge message. + pub fn remove_message(&self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_repository_message_remove(self.raw)); + Ok(()) + } } /// List all remotes for a given repository pub fn remotes(&self) -> Result { let mut arr = raw::git_strarray { - strings: 0 as *mut *mut c_char, + strings: ptr::null_mut(), count: 0, }; unsafe { @@ -319,9 +598,9 @@ impl Repository { } /// Get the information for a particular remote - pub fn find_remote(&self, name: &str) -> Result { - let mut ret = 0 as *mut raw::git_remote; - let name = try!(CString::new(name)); + pub fn find_remote(&self, name: &str) -> Result, Error> { + let mut ret = ptr::null_mut(); + let name = CString::new(name)?; unsafe { try_call!(raw::git_remote_lookup(&mut ret, self.raw, name)); Ok(Binding::from_raw(ret)) @@ -330,24 +609,44 @@ impl Repository { /// Add a remote with the default fetch refspec to the repository's /// configuration. - pub fn remote(&self, name: &str, url: &str) -> Result { - let mut ret = 0 as *mut raw::git_remote; - let name = try!(CString::new(name)); - let url = try!(CString::new(url)); + pub fn remote(&self, name: &str, url: &str) -> Result, Error> { + let mut ret = ptr::null_mut(); + let name = CString::new(name)?; + let url = CString::new(url)?; unsafe { try_call!(raw::git_remote_create(&mut ret, self.raw, name, url)); Ok(Binding::from_raw(ret)) } } + /// Add a remote with the provided fetch refspec to the repository's + /// configuration. + pub fn remote_with_fetch( + &self, + name: &str, + url: &str, + fetch: &str, + ) -> Result, Error> { + let mut ret = ptr::null_mut(); + let name = CString::new(name)?; + let url = CString::new(url)?; + let fetch = CString::new(fetch)?; + unsafe { + try_call!(raw::git_remote_create_with_fetchspec( + &mut ret, self.raw, name, url, fetch + )); + Ok(Binding::from_raw(ret)) + } + } + /// Create an anonymous remote /// - /// Create a remote with the given url and refspec in memory. You can use + /// Create a remote with the given URL and refspec in memory. You can use /// this when you have a URL instead of a remote's name. Note that anonymous /// remotes cannot be converted to persisted remotes. - pub fn remote_anonymous(&self, url: &str) -> Result { - let mut ret = 0 as *mut raw::git_remote; - let url = try!(CString::new(url)); + pub fn remote_anonymous(&self, url: &str) -> Result, Error> { + let mut ret = ptr::null_mut(); + let url = CString::new(url)?; unsafe { try_call!(raw::git_remote_create_anonymous(&mut ret, self.raw, url)); Ok(Binding::from_raw(ret)) @@ -367,17 +666,20 @@ impl Repository { /// The returned array of strings is a list of the non-default refspecs /// which cannot be renamed and are returned for further processing by the /// caller. - pub fn remote_rename(&self, name: &str, - new_name: &str) -> Result { - let name = try!(CString::new(name)); - let new_name = try!(CString::new(new_name)); + pub fn remote_rename(&self, name: &str, new_name: &str) -> Result { + let name = CString::new(name)?; + let new_name = CString::new(new_name)?; let mut problems = raw::git_strarray { count: 0, - strings: 0 as *mut *mut c_char, + strings: ptr::null_mut(), }; unsafe { - try_call!(raw::git_remote_rename(&mut problems, self.raw, name, - new_name)); + try_call!(raw::git_remote_rename( + &mut problems, + self.raw, + name, + new_name + )); Ok(Binding::from_raw(problems)) } } @@ -387,8 +689,10 @@ impl Repository { /// All remote-tracking branches and configuration settings for the remote /// will be removed. pub fn remote_delete(&self, name: &str) -> Result<(), Error> { - let name = try!(CString::new(name)); - unsafe { try_call!(raw::git_remote_delete(self.raw, name)); } + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_remote_delete(self.raw, name)); + } Ok(()) } @@ -396,10 +700,9 @@ impl Repository { /// /// Add the given refspec to the fetch list in the configuration. No loaded /// remote instances will be affected. - pub fn remote_add_fetch(&self, name: &str, spec: &str) - -> Result<(), Error> { - let name = try!(CString::new(name)); - let spec = try!(CString::new(spec)); + pub fn remote_add_fetch(&self, name: &str, spec: &str) -> Result<(), Error> { + let name = CString::new(name)?; + let spec = CString::new(spec)?; unsafe { try_call!(raw::git_remote_add_fetch(self.raw, name, spec)); } @@ -410,39 +713,39 @@ impl Repository { /// /// Add the given refspec to the push list in the configuration. No /// loaded remote instances will be affected. - pub fn remote_add_push(&self, name: &str, spec: &str) - -> Result<(), Error> { - let name = try!(CString::new(name)); - let spec = try!(CString::new(spec)); + pub fn remote_add_push(&self, name: &str, spec: &str) -> Result<(), Error> { + let name = CString::new(name)?; + let spec = CString::new(spec)?; unsafe { try_call!(raw::git_remote_add_push(self.raw, name, spec)); } Ok(()) } - /// Set the remote's url in the configuration + /// Set the remote's URL in the configuration /// /// Remote objects already in memory will not be affected. This assumes /// the common case of a single-url remote and will otherwise return an /// error. pub fn remote_set_url(https://github.com/&self,%20name:%20&str,%20url:%20&str) -> Result<(), Error> { - let name = try!(CString::new(name)); - let url = try!(CString::new(url)); - unsafe { try_call!(raw::git_remote_set_url(https://github.com/self.raw,%20name,%20url)); } + let name = CString::new(name)?; + let url = CString::new(url)?; + unsafe { + try_call!(raw::git_remote_set_url(https://github.com/self.raw,%20name,%20url)); + } Ok(()) } - /// Set the remote's url for pushing in the configuration. + /// Set the remote's URL for pushing in the configuration. /// /// Remote objects already in memory will not be affected. This assumes /// the common case of a single-url remote and will otherwise return an /// error. /// /// `None` indicates that it should be cleared. - pub fn remote_set_pushurl(https://github.com/&self,%20name:%20&str,%20pushurl:%20Option%3C&str%3E) - -> Result<(), Error> { - let name = try!(CString::new(name)); - let pushurl = try!(::opt_cstr(pushurl)); + pub fn remote_set_pushurl(https://github.com/&self,%20name:%20&str,%20pushurl:%20Option%3C&str%3E) -> Result<(), Error> { + let name = CString::new(name)?; + let pushurl = crate::opt_cstr(pushurl)?; unsafe { try_call!(raw::git_remote_set_pushurl(https://github.com/self.raw,%20name,%20pushurl)); } @@ -462,21 +765,25 @@ impl Repository { /// will be left alone, however.) /// /// The `target` is a commit-ish to which the head should be moved to. The - /// object can either be a commit or a tag, but tags must be derefernceable + /// object can either be a commit or a tag, but tags must be dereferenceable /// to a commit. /// /// The `checkout` options will only be used for a hard reset. - pub fn reset(&self, - target: &Object, - kind: ResetType, - checkout: Option<&mut CheckoutBuilder>) - -> Result<(), Error> { + pub fn reset( + &self, + target: &Object<'_>, + kind: ResetType, + checkout: Option<&mut CheckoutBuilder<'_>>, + ) -> Result<(), Error> { unsafe { let mut opts: raw::git_checkout_options = mem::zeroed(); - try_call!(raw::git_checkout_init_options(&mut opts, - raw::GIT_CHECKOUT_OPTIONS_VERSION)); + try_call!(raw::git_checkout_init_options( + &mut opts, + raw::GIT_CHECKOUT_OPTIONS_VERSION + )); let opts = checkout.map(|c| { - c.configure(&mut opts); &mut opts + c.configure(&mut opts); + &mut opts }); try_call!(raw::git_reset(self.raw, target.raw(), kind, opts)); } @@ -490,12 +797,12 @@ impl Repository { /// /// Passing a `None` target will result in removing entries in the index /// matching the provided pathspecs. - pub fn reset_default(&self, - target: Option<&Object>, - paths: I) -> Result<(), Error> - where T: IntoCString, I: IntoIterator, + pub fn reset_default(&self, target: Option<&Object<'_>>, paths: I) -> Result<(), Error> + where + T: IntoCString, + I: IntoIterator, { - let (_a, _b, mut arr) = try!(::util::iter2cstrs(paths)); + let (_a, _b, mut arr) = crate::util::iter2cstrs_paths(paths)?; let target = target.map(|t| t.raw()); unsafe { try_call!(raw::git_reset_default(self.raw, target, &mut arr)); @@ -504,8 +811,8 @@ impl Repository { } /// Retrieve and resolve the reference pointed at by HEAD. - pub fn head(&self) -> Result { - let mut ret = 0 as *mut raw::git_reference; + pub fn head(&self) -> Result, Error> { + let mut ret = ptr::null_mut(); unsafe { try_call!(raw::git_repository_head(&mut ret, self.raw)); Ok(Binding::from_raw(ret)) @@ -525,16 +832,44 @@ impl Repository { /// Otherwise, the HEAD will be detached and will directly point to the /// commit. pub fn set_head(&self, refname: &str) -> Result<(), Error> { - let refname = try!(CString::new(refname)); + self.set_head_bytes(refname.as_bytes()) + } + + /// Make the repository HEAD point to the specified reference as a byte array. + /// + /// If the provided reference points to a tree or a blob, the HEAD is + /// unaltered and an error is returned. + /// + /// If the provided reference points to a branch, the HEAD will point to + /// that branch, staying attached, or become attached if it isn't yet. If + /// the branch doesn't exist yet, no error will be returned. The HEAD will + /// then be attached to an unborn branch. + /// + /// Otherwise, the HEAD will be detached and will directly point to the + /// commit. + pub fn set_head_bytes(&self, refname: &[u8]) -> Result<(), Error> { + let refname = CString::new(refname)?; unsafe { try_call!(raw::git_repository_set_head(self.raw, refname)); } Ok(()) } + /// Determines whether the repository HEAD is detached. + pub fn head_detached(&self) -> Result { + unsafe { + let value = raw::git_repository_head_detached(self.raw); + match value { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(Error::last_error(value)), + } + } + } + /// Make the repository HEAD directly point to the commit. /// - /// If the provided committish cannot be found in the repository, the HEAD + /// If the provided commitish cannot be found in the repository, the HEAD /// is unaltered and an error is returned. /// /// If the provided commitish cannot be peeled into a commit, the HEAD is @@ -544,15 +879,38 @@ impl Repository { /// to the peeled commit. pub fn set_head_detached(&self, commitish: Oid) -> Result<(), Error> { unsafe { - try_call!(raw::git_repository_set_head_detached(self.raw, - commitish.raw())); + try_call!(raw::git_repository_set_head_detached( + self.raw, + commitish.raw() + )); + } + Ok(()) + } + + /// Make the repository HEAD directly point to the commit. + /// + /// If the provided commitish cannot be found in the repository, the HEAD + /// is unaltered and an error is returned. + /// If the provided commitish cannot be peeled into a commit, the HEAD is + /// unaltered and an error is returned. + /// Otherwise, the HEAD will eventually be detached and will directly point + /// to the peeled commit. + pub fn set_head_detached_from_annotated( + &self, + commitish: AnnotatedCommit<'_>, + ) -> Result<(), Error> { + unsafe { + try_call!(raw::git_repository_set_head_detached_from_annotated( + self.raw, + commitish.raw() + )); } Ok(()) } /// Create an iterator for the repo's references - pub fn references(&self) -> Result { - let mut ret = 0 as *mut raw::git_reference_iterator; + pub fn references(&self) -> Result, Error> { + let mut ret = ptr::null_mut(); unsafe { try_call!(raw::git_reference_iterator_new(&mut ret, self.raw)); Ok(Binding::from_raw(ret)) @@ -561,20 +919,21 @@ impl Repository { /// Create an iterator for the repo's references that match the specified /// glob - pub fn references_glob(&self, glob: &str) -> Result { - let mut ret = 0 as *mut raw::git_reference_iterator; - let glob = try!(CString::new(glob)); + pub fn references_glob(&self, glob: &str) -> Result, Error> { + let mut ret = ptr::null_mut(); + let glob = CString::new(glob)?; unsafe { - try_call!(raw::git_reference_iterator_glob_new(&mut ret, self.raw, - glob)); + try_call!(raw::git_reference_iterator_glob_new( + &mut ret, self.raw, glob + )); Ok(Binding::from_raw(ret)) } } /// Load all submodules for this repository and return them. - pub fn submodules(&self) -> Result, Error> { - struct Data<'a, 'b:'a> { + pub fn submodules(&self) -> Result>, Error> { + struct Data<'a, 'b> { repo: &'b Repository, ret: &'a mut Vec>, } @@ -585,21 +944,25 @@ impl Repository { repo: self, ret: &mut ret, }; - try_call!(raw::git_submodule_foreach(self.raw, append, - &mut data as *mut _ - as *mut c_void)); + let cb: raw::git_submodule_cb = Some(append); + try_call!(raw::git_submodule_foreach( + self.raw, + cb, + &mut data as *mut _ as *mut c_void + )); } return Ok(ret); - extern fn append(_repo: *mut raw::git_submodule, - name: *const c_char, - data: *mut c_void) -> c_int { + extern "C" fn append( + _repo: *mut raw::git_submodule, + name: *const c_char, + data: *mut c_void, + ) -> c_int { unsafe { - let data = &mut *(data as *mut Data); - let mut raw = 0 as *mut raw::git_submodule; - let rc = raw::git_submodule_lookup(&mut raw, data.repo.raw(), - name); + let data = &mut *(data as *mut Data<'_, '_>); + let mut raw = ptr::null_mut(); + let rc = raw::git_submodule_lookup(&mut raw, data.repo.raw(), name); assert_eq!(rc, 0); data.ret.push(Binding::from_raw(raw)); } @@ -613,13 +976,14 @@ impl Repository { /// status, then the results from rename detection (if you enable it) may /// not be accurate. To do rename detection properly, this must be called /// with no pathspec so that all files can be considered. - pub fn statuses(&self, options: Option<&mut StatusOptions>) - -> Result { - let mut ret = 0 as *mut raw::git_status_list; + pub fn statuses(&self, options: Option<&mut StatusOptions>) -> Result, Error> { + let mut ret = ptr::null_mut(); unsafe { - try_call!(raw::git_status_list_new(&mut ret, self.raw, - options.map(|s| s.raw()) - .unwrap_or(0 as *const _))); + try_call!(raw::git_status_list_new( + &mut ret, + self.raw, + options.map(|s| s.raw()).unwrap_or(ptr::null()) + )); Ok(Binding::from_raw(ret)) } } @@ -634,10 +998,9 @@ impl Repository { /// directory containing the file, would it be added or not? pub fn status_should_ignore(&self, path: &Path) -> Result { let mut ret = 0 as c_int; - let path = try!(path.into_c_string()); + let path = util::cstring_to_repo_path(path)?; unsafe { - try_call!(raw::git_status_should_ignore(&mut ret, self.raw, - path)); + try_call!(raw::git_status_should_ignore(&mut ret, self.raw, path)); } Ok(ret != 0) } @@ -660,18 +1023,16 @@ impl Repository { /// through looking for the path that you are interested in. pub fn status_file(&self, path: &Path) -> Result { let mut ret = 0 as c_uint; - let path = try!(path.into_c_string()); + let path = path_to_repo_path(path)?; unsafe { - try_call!(raw::git_status_file(&mut ret, self.raw, - path)); + try_call!(raw::git_status_file(&mut ret, self.raw, path)); } Ok(Status::from_bits_truncate(ret as u32)) } /// Create an iterator which loops over the requested branches. - pub fn branches(&self, filter: Option) - -> Result { - let mut raw = 0 as *mut raw::git_branch_iterator; + pub fn branches(&self, filter: Option) -> Result, Error> { + let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_branch_iterator_new(&mut raw, self.raw(), filter)); Ok(Branches::from_raw(raw)) @@ -682,8 +1043,13 @@ impl Repository { /// /// If a custom index has not been set, the default index for the repository /// will be returned (the one located in .git/index). + /// + /// **Caution**: If the [`Repository`] of this index is dropped, then this + /// [`Index`] will become detached, and most methods on it will fail. See + /// [`Index::open`]. Be sure the repository has a binding such as a local + /// variable to keep it alive at least as long as the index. pub fn index(&self) -> Result { - let mut raw = 0 as *mut raw::git_index; + let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_repository_index(&mut raw, self.raw())); Ok(Binding::from_raw(raw)) @@ -691,10 +1057,11 @@ impl Repository { } /// Set the Index file for this repository. - pub fn set_index(&self, index: &mut Index) { + pub fn set_index(&self, index: &mut Index) -> Result<(), Error> { unsafe { - raw::git_repository_set_index(self.raw(), index.raw()); + try_call!(raw::git_repository_set_index(self.raw(), index.raw())); } + Ok(()) } /// Get the configuration file for this repository. @@ -703,24 +1070,80 @@ impl Repository { /// repository will be returned, including global and system configurations /// (if they are available). pub fn config(&self) -> Result { - let mut raw = 0 as *mut raw::git_config; + let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_repository_config(&mut raw, self.raw())); Ok(Binding::from_raw(raw)) } } + /// Get the value of a git attribute for a path as a string. + /// + /// This function will return a special string if the attribute is set to a special value. + /// Interpreting the special string is discouraged. You should always use + /// [`AttrValue::from_string`](crate::AttrValue::from_string) to interpret the return value + /// and avoid the special string. + /// + /// As such, the return type of this function will probably be changed in the next major version + /// to prevent interpreting the returned string without checking whether it's special. + pub fn get_attr( + &self, + path: &Path, + name: &str, + flags: AttrCheckFlags, + ) -> Result, Error> { + Ok(self + .get_attr_bytes(path, name, flags)? + .and_then(|a| str::from_utf8(a).ok())) + } + + /// Get the value of a git attribute for a path as a byte slice. + /// + /// This function will return a special byte slice if the attribute is set to a special value. + /// Interpreting the special byte slice is discouraged. You should always use + /// [`AttrValue::from_bytes`](crate::AttrValue::from_bytes) to interpret the return value and + /// avoid the special string. + /// + /// As such, the return type of this function will probably be changed in the next major version + /// to prevent interpreting the returned byte slice without checking whether it's special. + pub fn get_attr_bytes( + &self, + path: &Path, + name: &str, + flags: AttrCheckFlags, + ) -> Result, Error> { + let mut ret = ptr::null(); + let path = util::cstring_to_repo_path(path)?; + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_attr_get( + &mut ret, + self.raw(), + flags.bits(), + path, + name + )); + Ok(crate::opt_bytes(self, ret)) + } + } + /// Write an in-memory buffer to the ODB as a blob. /// /// The Oid returned can in turn be passed to `find_blob` to get a handle to /// the blob. pub fn blob(&self, data: &[u8]) -> Result { - let mut raw = raw::git_oid { id: [0; raw::GIT_OID_RAWSZ] }; + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; unsafe { let ptr = data.as_ptr() as *const c_void; let len = data.len() as size_t; - try_call!(raw::git_blob_create_frombuffer(&mut raw, self.raw(), - ptr, len)); + try_call!(raw::git_blob_create_frombuffer( + &mut raw, + self.raw(), + ptr, + len + )); Ok(Binding::from_raw(&raw as *const _)) } } @@ -731,53 +1154,134 @@ impl Repository { /// The Oid returned can in turn be passed to `find_blob` to get a handle to /// the blob. pub fn blob_path(&self, path: &Path) -> Result { - let path = try!(path.into_c_string()); - let mut raw = raw::git_oid { id: [0; raw::GIT_OID_RAWSZ] }; + // Normal file path OK (does not need Windows conversion). + let path = path.into_c_string()?; + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; unsafe { - try_call!(raw::git_blob_create_fromdisk(&mut raw, self.raw(), - path)); + try_call!(raw::git_blob_create_fromdisk(&mut raw, self.raw(), path)); Ok(Binding::from_raw(&raw as *const _)) } } + /// Create a stream to write blob + /// + /// This function may need to buffer the data on disk and will in general + /// not be the right choice if you know the size of the data to write. + /// + /// Use `BlobWriter::commit()` to commit the write to the object db + /// and get the object id. + /// + /// If the `hintpath` parameter is filled, it will be used to determine + /// what git filters should be applied to the object before it is written + /// to the object database. + pub fn blob_writer(&self, hintpath: Option<&Path>) -> Result, Error> { + let path_str = match hintpath { + Some(path) => Some(path.into_c_string()?), + None => None, + }; + let path = match path_str { + Some(ref path) => path.as_ptr(), + None => ptr::null(), + }; + let mut out = ptr::null_mut(); + unsafe { + try_call!(raw::git_blob_create_fromstream(&mut out, self.raw(), path)); + Ok(BlobWriter::from_raw(out)) + } + } + /// Lookup a reference to one of the objects in a repository. - pub fn find_blob(&self, oid: Oid) -> Result { - let mut raw = 0 as *mut raw::git_blob; + pub fn find_blob(&self, oid: Oid) -> Result, Error> { + let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_blob_lookup(&mut raw, self.raw(), oid.raw())); Ok(Binding::from_raw(raw)) } } + /// Get the object database for this repository + pub fn odb(&self) -> Result, Error> { + let mut odb = ptr::null_mut(); + unsafe { + try_call!(raw::git_repository_odb(&mut odb, self.raw())); + Ok(Odb::from_raw(odb)) + } + } + + /// Override the object database for this repository + pub fn set_odb(&self, odb: &Odb<'_>) -> Result<(), Error> { + unsafe { + try_call!(raw::git_repository_set_odb(self.raw(), odb.raw())); + } + Ok(()) + } + /// Create a new branch pointing at a target commit /// /// A new direct reference will be created pointing to this target commit. /// If `force` is true and a reference already exists with the given name, /// it'll be replaced. - pub fn branch(&self, - branch_name: &str, - target: &Commit, - force: bool) -> Result { - let branch_name = try!(CString::new(branch_name)); - let mut raw = 0 as *mut raw::git_reference; - unsafe { - try_call!(raw::git_branch_create(&mut raw, - self.raw(), - branch_name, - target.raw(), - force)); + pub fn branch( + &self, + branch_name: &str, + target: &Commit<'_>, + force: bool, + ) -> Result, Error> { + let branch_name = CString::new(branch_name)?; + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_branch_create( + &mut raw, + self.raw(), + branch_name, + target.raw(), + force + )); + Ok(Branch::wrap(Binding::from_raw(raw))) + } + } + + /// Create a new branch pointing at a target commit + /// + /// This behaves like `Repository::branch()` but takes + /// an annotated commit, which lets you specify which + /// extended SHA syntax string was specified by a user, + /// allowing for more exact reflog messages. + /// + /// See the documentation for `Repository::branch()` + pub fn branch_from_annotated_commit( + &self, + branch_name: &str, + target: &AnnotatedCommit<'_>, + force: bool, + ) -> Result, Error> { + let branch_name = CString::new(branch_name)?; + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_branch_create_from_annotated( + &mut raw, + self.raw(), + branch_name, + target.raw(), + force + )); Ok(Branch::wrap(Binding::from_raw(raw))) } } /// Lookup a branch by its name in a repository. - pub fn find_branch(&self, name: &str, branch_type: BranchType) - -> Result { - let name = try!(CString::new(name)); - let mut ret = 0 as *mut raw::git_reference; + pub fn find_branch(&self, name: &str, branch_type: BranchType) -> Result, Error> { + let name = CString::new(name)?; + let mut ret = ptr::null_mut(); unsafe { - try_call!(raw::git_branch_lookup(&mut ret, self.raw(), name, - branch_type)); + try_call!(raw::git_branch_lookup( + &mut ret, + self.raw(), + name, + branch_type + )); Ok(Branch::wrap(Binding::from_raw(ret))) } } @@ -790,51 +1294,199 @@ impl Repository { /// current branch and make it point to this commit. If the reference /// doesn't exist yet, it will be created. If it does exist, the first /// parent must be the tip of this branch. - pub fn commit(&self, - update_ref: Option<&str>, - author: &Signature, - committer: &Signature, - message: &str, - tree: &Tree, - parents: &[&Commit]) -> Result { - let update_ref = try!(::opt_cstr(update_ref)); - let mut parent_ptrs = parents.iter().map(|p| { - p.raw() as *const raw::git_commit - }).collect::>(); - let message = try!(CString::new(message)); - let mut raw = raw::git_oid { id: [0; raw::GIT_OID_RAWSZ] }; - unsafe { - try_call!(raw::git_commit_create(&mut raw, - self.raw(), - update_ref, - author.raw(), - committer.raw(), - 0 as *const c_char, - message, - tree.raw(), - parents.len() as size_t, - parent_ptrs.as_mut_ptr())); + pub fn commit( + &self, + update_ref: Option<&str>, + author: &Signature<'_>, + committer: &Signature<'_>, + message: &str, + tree: &Tree<'_>, + parents: &[&Commit<'_>], + ) -> Result { + let update_ref = crate::opt_cstr(update_ref)?; + let mut parent_ptrs = parents + .iter() + .map(|p| p.raw() as *const raw::git_commit) + .collect::>(); + let message = CString::new(message)?; + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_commit_create( + &mut raw, + self.raw(), + update_ref, + author.raw(), + committer.raw(), + ptr::null(), + message, + tree.raw(), + parents.len() as size_t, + parent_ptrs.as_mut_ptr() + )); + Ok(Binding::from_raw(&raw as *const _)) + } + } + + /// Create a commit object and return that as a Buf. + /// + /// That can be converted to a string like this `str::from_utf8(&buf).unwrap().to_string()`. + /// And that string can be passed to the `commit_signed` function, + /// the arguments behave the same as in the `commit` function. + pub fn commit_create_buffer( + &self, + author: &Signature<'_>, + committer: &Signature<'_>, + message: &str, + tree: &Tree<'_>, + parents: &[&Commit<'_>], + ) -> Result { + let mut parent_ptrs = parents + .iter() + .map(|p| p.raw() as *const raw::git_commit) + .collect::>(); + let message = CString::new(message)?; + let buf = Buf::new(); + unsafe { + try_call!(raw::git_commit_create_buffer( + buf.raw(), + self.raw(), + author.raw(), + committer.raw(), + ptr::null(), + message, + tree.raw(), + parents.len() as size_t, + parent_ptrs.as_mut_ptr() + )); + Ok(buf) + } + } + + /// Create a commit object from the given buffer and signature + /// + /// Given the unsigned commit object's contents, its signature and the + /// header field in which to store the signature, attach the signature to + /// the commit and write it into the given repository. + /// + /// Use `None` in `signature_field` to use the default of `gpgsig`, which is + /// almost certainly what you want. + /// + /// Returns the resulting (signed) commit id. + pub fn commit_signed( + &self, + commit_content: &str, + signature: &str, + signature_field: Option<&str>, + ) -> Result { + let commit_content = CString::new(commit_content)?; + let signature = CString::new(signature)?; + let signature_field = crate::opt_cstr(signature_field)?; + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_commit_create_with_signature( + &mut raw, + self.raw(), + commit_content, + signature, + signature_field + )); Ok(Binding::from_raw(&raw as *const _)) } } + /// Extract the signature from a commit + /// + /// Returns a tuple containing the signature in the first value and the + /// signed data in the second. + pub fn extract_signature( + &self, + commit_id: &Oid, + signature_field: Option<&str>, + ) -> Result<(Buf, Buf), Error> { + let signature_field = crate::opt_cstr(signature_field)?; + let signature = Buf::new(); + let content = Buf::new(); + unsafe { + try_call!(raw::git_commit_extract_signature( + signature.raw(), + content.raw(), + self.raw(), + commit_id.raw() as *mut _, + signature_field + )); + Ok((signature, content)) + } + } /// Lookup a reference to one of the commits in a repository. - pub fn find_commit(&self, oid: Oid) -> Result { - let mut raw = 0 as *mut raw::git_commit; + pub fn find_commit(&self, oid: Oid) -> Result, Error> { + let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_commit_lookup(&mut raw, self.raw(), oid.raw())); Ok(Binding::from_raw(raw)) } } + /// Lookup a reference to one of the commits in a repository by short hash. + pub fn find_commit_by_prefix(&self, prefix_hash: &str) -> Result, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_commit_lookup_prefix( + &mut raw, + self.raw(), + Oid::from_str(prefix_hash)?.raw(), + prefix_hash.len() + )); + Ok(Binding::from_raw(raw)) + } + } + + /// Creates an `AnnotatedCommit` from the given commit id. + pub fn find_annotated_commit(&self, id: Oid) -> Result, Error> { + unsafe { + let mut raw = ptr::null_mut(); + try_call!(raw::git_annotated_commit_lookup( + &mut raw, + self.raw(), + id.raw() + )); + Ok(Binding::from_raw(raw)) + } + } + /// Lookup a reference to one of the objects in a repository. - pub fn find_object(&self, oid: Oid, - kind: Option) -> Result { - let mut raw = 0 as *mut raw::git_object; + pub fn find_object(&self, oid: Oid, kind: Option) -> Result, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_object_lookup( + &mut raw, + self.raw(), + oid.raw(), + kind + )); + Ok(Binding::from_raw(raw)) + } + } + + /// Lookup a reference to one of the objects by id prefix in a repository. + pub fn find_object_by_prefix( + &self, + prefix_hash: &str, + kind: Option, + ) -> Result, Error> { + let mut raw = ptr::null_mut(); unsafe { - try_call!(raw::git_object_lookup(&mut raw, self.raw(), oid.raw(), - kind)); + try_call!(raw::git_object_lookup_prefix( + &mut raw, + self.raw(), + Oid::from_str(prefix_hash)?.raw(), + prefix_hash.len(), + kind + )); Ok(Binding::from_raw(raw)) } } @@ -844,15 +1496,25 @@ impl Repository { /// This function will return an error if a reference already exists with /// the given name unless force is true, in which case it will be /// overwritten. - pub fn reference(&self, name: &str, id: Oid, force: bool, - log_message: &str) -> Result { - let name = try!(CString::new(name)); - let log_message = try!(CString::new(log_message)); - let mut raw = 0 as *mut raw::git_reference; - unsafe { - try_call!(raw::git_reference_create(&mut raw, self.raw(), name, - id.raw(), force, - log_message)); + pub fn reference( + &self, + name: &str, + id: Oid, + force: bool, + log_message: &str, + ) -> Result, Error> { + let name = CString::new(name)?; + let log_message = CString::new(log_message)?; + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_reference_create( + &mut raw, + self.raw(), + name, + id.raw(), + force, + log_message + )); Ok(Binding::from_raw(raw)) } } @@ -887,44 +1549,69 @@ impl Repository { /// It will return GIT_EMODIFIED if the reference's value at the time of /// updating does not match the one passed through `current_id` (i.e. if the /// ref has changed since the user read it). - pub fn reference_matching(&self, - name: &str, - id: Oid, - force: bool, - current_id: Oid, - log_message: &str) -> Result { - let name = try!(CString::new(name)); - let log_message = try!(CString::new(log_message)); - let mut raw = 0 as *mut raw::git_reference; - unsafe { - try_call!(raw::git_reference_create_matching(&mut raw, - self.raw(), - name, - id.raw(), - force, - current_id.raw(), - log_message)); + pub fn reference_matching( + &self, + name: &str, + id: Oid, + force: bool, + current_id: Oid, + log_message: &str, + ) -> Result, Error> { + let name = CString::new(name)?; + let log_message = CString::new(log_message)?; + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_reference_create_matching( + &mut raw, + self.raw(), + name, + id.raw(), + force, + current_id.raw(), + log_message + )); Ok(Binding::from_raw(raw)) } } /// Create a new symbolic reference. /// + /// A symbolic reference is a reference name that refers to another + /// reference name. If the other name moves, the symbolic name will move, + /// too. As a simple example, the "HEAD" reference might refer to + /// "refs/heads/master" while on the "master" branch of a repository. + /// + /// Valid reference names must follow one of two patterns: + /// + /// 1. Top-level names must contain only capital letters and underscores, + /// and must begin and end with a letter. (e.g. "HEAD", "ORIG_HEAD"). + /// 2. Names prefixed with "refs/" can be almost anything. You must avoid + /// the characters '~', '^', ':', '\\', '?', '[', and '*', and the + /// sequences ".." and "@{" which have special meaning to revparse. + /// /// This function will return an error if a reference already exists with /// the given name unless force is true, in which case it will be /// overwritten. - pub fn reference_symbolic(&self, name: &str, target: &str, - force: bool, - log_message: &str) - -> Result { - let name = try!(CString::new(name)); - let target = try!(CString::new(target)); - let log_message = try!(CString::new(log_message)); - let mut raw = 0 as *mut raw::git_reference; - unsafe { - try_call!(raw::git_reference_symbolic_create(&mut raw, self.raw(), - name, target, force, - log_message)); + pub fn reference_symbolic( + &self, + name: &str, + target: &str, + force: bool, + log_message: &str, + ) -> Result, Error> { + let name = CString::new(name)?; + let target = CString::new(target)?; + let log_message = CString::new(log_message)?; + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_reference_symbolic_create( + &mut raw, + self.raw(), + name, + target, + force, + log_message + )); Ok(Binding::from_raw(raw)) } } @@ -938,48 +1625,66 @@ impl Repository { /// It will return GIT_EMODIFIED if the reference's value at the time of /// updating does not match the one passed through current_value (i.e. if /// the ref has changed since the user read it). - pub fn reference_symbolic_matching(&self, - name: &str, - target: &str, - force: bool, - current_value: &str, - log_message: &str) - -> Result { - let name = try!(CString::new(name)); - let target = try!(CString::new(target)); - let current_value = try!(CString::new(current_value)); - let log_message = try!(CString::new(log_message)); - let mut raw = 0 as *mut raw::git_reference; - unsafe { - try_call!(raw::git_reference_symbolic_create_matching(&mut raw, - self.raw(), - name, - target, - force, - current_value, - log_message)); + pub fn reference_symbolic_matching( + &self, + name: &str, + target: &str, + force: bool, + current_value: &str, + log_message: &str, + ) -> Result, Error> { + let name = CString::new(name)?; + let target = CString::new(target)?; + let current_value = CString::new(current_value)?; + let log_message = CString::new(log_message)?; + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_reference_symbolic_create_matching( + &mut raw, + self.raw(), + name, + target, + force, + current_value, + log_message + )); Ok(Binding::from_raw(raw)) } } /// Lookup a reference to one of the objects in a repository. - pub fn find_reference(&self, name: &str) -> Result { - let name = try!(CString::new(name)); - let mut raw = 0 as *mut raw::git_reference; + pub fn find_reference(&self, name: &str) -> Result, Error> { + let name = CString::new(name)?; + let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_reference_lookup(&mut raw, self.raw(), name)); Ok(Binding::from_raw(raw)) } } + /// Lookup a reference to one of the objects in a repository. + /// `Repository::find_reference` with teeth; give the method your reference in + /// human-readable format e.g. 'main' instead of 'refs/heads/main', and it + /// will do-what-you-mean, returning the `Reference`. + pub fn resolve_reference_from_short_name(&self, refname: &str) -> Result, Error> { + let refname = CString::new(refname)?; + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_reference_dwim(&mut raw, self.raw(), refname)); + Ok(Binding::from_raw(raw)) + } + } + /// Lookup a reference by name and resolve immediately to OID. /// /// This function provides a quick way to resolve a reference name straight /// through to the object id that it refers to. This avoids having to /// allocate or free any `Reference` objects for simple situations. pub fn refname_to_id(&self, name: &str) -> Result { - let name = try!(CString::new(name)); - let mut ret = raw::git_oid { id: [0; raw::GIT_OID_RAWSZ] }; + let name = CString::new(name)?; + let mut ret = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; unsafe { try_call!(raw::git_reference_name_to_id(&mut ret, self.raw(), name)); Ok(Binding::from_raw(&ret as *const _)) @@ -987,13 +1692,40 @@ impl Repository { } /// Creates a git_annotated_commit from the given reference. - pub fn reference_to_annotated_commit(&self, reference: &Reference) - -> Result { - let mut ret = 0 as *mut raw::git_annotated_commit; + pub fn reference_to_annotated_commit( + &self, + reference: &Reference<'_>, + ) -> Result, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_annotated_commit_from_ref( + &mut ret, + self.raw(), + reference.raw() + )); + Ok(AnnotatedCommit::from_raw(ret)) + } + } + + /// Creates a git_annotated_commit from FETCH_HEAD. + pub fn annotated_commit_from_fetchhead( + &self, + branch_name: &str, + remote_url: &str, + id: &Oid, + ) -> Result, Error> { + let branch_name = CString::new(branch_name)?; + let remote_url = CString::new(remote_url)?; + + let mut ret = ptr::null_mut(); unsafe { - try_call!(raw::git_annotated_commit_from_ref(&mut ret, - self.raw(), - reference.raw())); + try_call!(raw::git_annotated_commit_from_fetchhead( + &mut ret, + self.raw(), + branch_name, + remote_url, + id.raw() + )); Ok(AnnotatedCommit::from_raw(ret)) } } @@ -1005,7 +1737,7 @@ impl Repository { /// based on that information. It will return `NotFound` if either the /// user.name or user.email are not set. pub fn signature(&self) -> Result, Error> { - let mut ret = 0 as *mut raw::git_signature; + let mut ret = ptr::null_mut(); unsafe { try_call!(raw::git_signature_default(&mut ret, self.raw())); Ok(Binding::from_raw(ret)) @@ -1022,16 +1754,25 @@ impl Repository { /// /// To fully emulate "git submodule add" call this function, then `open()` /// the submodule repo and perform the clone step as needed. Lastly, call - /// `finalize()` to wrap up adding the new submodule and `.gitmodules` to - /// the index to be ready to commit. - pub fn submodule(&self, url: &str, path: &Path, - use_gitlink: bool) -> Result { - let url = try!(CString::new(url)); - let path = try!(path.into_c_string()); - let mut raw = 0 as *mut raw::git_submodule; - unsafe { - try_call!(raw::git_submodule_add_setup(&mut raw, self.raw(), - url, path, use_gitlink)); + /// `add_finalize()` to wrap up adding the new submodule and `.gitmodules` + /// to the index to be ready to commit. + pub fn submodule( + &self, + url: &str, + path: &Path, + use_gitlink: bool, + ) -> Result, Error> { + let url = CString::new(url)?; + let path = path_to_repo_path(path)?; + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_submodule_add_setup( + &mut raw, + self.raw(), + url, + path, + use_gitlink + )); Ok(Binding::from_raw(raw)) } } @@ -1040,9 +1781,9 @@ impl Repository { /// /// Given either the submodule name or path (they are usually the same), /// this returns a structure describing the submodule. - pub fn find_submodule(&self, name: &str) -> Result { - let name = try!(CString::new(name)); - let mut raw = 0 as *mut raw::git_submodule; + pub fn find_submodule(&self, name: &str) -> Result, Error> { + let name = CString::new(name)?; + let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_submodule_lookup(&mut raw, self.raw(), name)); Ok(Binding::from_raw(raw)) @@ -1053,102 +1794,227 @@ impl Repository { /// /// This looks at a submodule and tries to determine the status. It /// will return a combination of the `SubmoduleStatus` values. - pub fn submodule_status(&self, name: &str, ignore: SubmoduleIgnore) - -> Result { + pub fn submodule_status( + &self, + name: &str, + ignore: SubmoduleIgnore, + ) -> Result { let mut ret = 0; - let name = try!(CString::new(name)); + let name = CString::new(name)?; unsafe { - try_call!(raw::git_submodule_status(&mut ret, self.raw, name, - ignore)); + try_call!(raw::git_submodule_status(&mut ret, self.raw, name, ignore)); } Ok(SubmoduleStatus::from_bits_truncate(ret as u32)) } - /// Lookup a reference to one of the objects in a repository. - pub fn find_tree(&self, oid: Oid) -> Result { - let mut raw = 0 as *mut raw::git_tree; + /// Set the ignore rule for the submodule in the configuration + /// + /// This does not affect any currently-loaded instances. + pub fn submodule_set_ignore( + &mut self, + name: &str, + ignore: SubmoduleIgnore, + ) -> Result<(), Error> { + let name = CString::new(name)?; unsafe { - try_call!(raw::git_tree_lookup(&mut raw, self.raw(), oid.raw())); - Ok(Binding::from_raw(raw)) + try_call!(raw::git_submodule_set_ignore(self.raw(), name, ignore)); } + Ok(()) } - /// Create a new TreeBuilder, optionally initialized with the - /// entries of the given Tree. + /// Set the update rule for the submodule in the configuration /// - /// The tree builder can be used to create or modify trees in memory and - /// write them as tree objects to the database. - pub fn treebuilder(&self, tree: Option<&Tree>) -> Result { + /// This setting won't affect any existing instances. + pub fn submodule_set_update( + &mut self, + name: &str, + update: SubmoduleUpdate, + ) -> Result<(), Error> { + let name = CString::new(name)?; unsafe { - let mut ret = 0 as *mut raw::git_treebuilder; - let tree = match tree { - Some(tree) => tree.raw(), - None => 0 as *mut raw::git_tree, - }; - try_call!(raw::git_treebuilder_new(&mut ret, self.raw, tree)); - Ok(Binding::from_raw(ret)) + try_call!(raw::git_submodule_set_update(self.raw(), name, update)); } + Ok(()) } - - /// Create a new tag in the repository from an object + /// Set the URL for the submodule in the configuration /// - /// A new reference will also be created pointing to this tag object. If - /// `force` is true and a reference already exists with the given name, - /// it'll be replaced. + /// After calling this, you may wish to call [`Submodule::sync`] to write + /// the changes to the checked out submodule repository. + pub fn submodule_set_url(https://github.com/&mut%20self,%20name:%20&str,%20url:%20&str) -> Result<(), Error> { + let name = CString::new(name)?; + let url = CString::new(url)?; + unsafe { + try_call!(raw::git_submodule_set_url(https://github.com/self.raw(), name, url)); + } + Ok(()) + } + + /// Set the branch for the submodule in the configuration + /// + /// After calling this, you may wish to call [`Submodule::sync`] to write + /// the changes to the checked out submodule repository. + pub fn submodule_set_branch(&mut self, name: &str, branch_name: &str) -> Result<(), Error> { + let name = CString::new(name)?; + let branch_name = CString::new(branch_name)?; + unsafe { + try_call!(raw::git_submodule_set_branch(self.raw(), name, branch_name)); + } + Ok(()) + } + + /// Lookup a reference to one of the objects in a repository. + pub fn find_tree(&self, oid: Oid) -> Result, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_tree_lookup(&mut raw, self.raw(), oid.raw())); + Ok(Binding::from_raw(raw)) + } + } + + /// Create a new TreeBuilder, optionally initialized with the + /// entries of the given Tree. + /// + /// The tree builder can be used to create or modify trees in memory and + /// write them as tree objects to the database. + pub fn treebuilder(&self, tree: Option<&Tree<'_>>) -> Result, Error> { + unsafe { + let mut ret = ptr::null_mut(); + let tree = match tree { + Some(tree) => tree.raw(), + None => ptr::null_mut(), + }; + try_call!(raw::git_treebuilder_new(&mut ret, self.raw, tree)); + Ok(Binding::from_raw(ret)) + } + } + + /// Create a new tag in the repository from an object + /// + /// A new reference will also be created pointing to this tag object. If + /// `force` is true and a reference already exists with the given name, + /// it'll be replaced. /// /// The message will not be cleaned up. /// /// The tag name will be checked for validity. You must avoid the characters /// '~', '^', ':', ' \ ', '?', '[', and '*', and the sequences ".." and " @ /// {" which have special meaning to revparse. - pub fn tag(&self, name: &str, target: &Object, - tagger: &Signature, message: &str, - force: bool) -> Result { - let name = try!(CString::new(name)); - let message = try!(CString::new(message)); - let mut raw = raw::git_oid { id: [0; raw::GIT_OID_RAWSZ] }; - unsafe { - try_call!(raw::git_tag_create(&mut raw, self.raw, name, - target.raw(), tagger.raw(), - message, force)); + pub fn tag( + &self, + name: &str, + target: &Object<'_>, + tagger: &Signature<'_>, + message: &str, + force: bool, + ) -> Result { + let name = CString::new(name)?; + let message = CString::new(message)?; + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_tag_create( + &mut raw, + self.raw, + name, + target.raw(), + tagger.raw(), + message, + force + )); Ok(Binding::from_raw(&raw as *const _)) } } + /// Create a new tag in the repository from an object without creating a reference. + /// + /// The message will not be cleaned up. + /// + /// The tag name will be checked for validity. You must avoid the characters + /// '~', '^', ':', ' \ ', '?', '[', and '*', and the sequences ".." and " @ + /// {" which have special meaning to revparse. + pub fn tag_annotation_create( + &self, + name: &str, + target: &Object<'_>, + tagger: &Signature<'_>, + message: &str, + ) -> Result { + let name = CString::new(name)?; + let message = CString::new(message)?; + let mut raw_oid = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_tag_annotation_create( + &mut raw_oid, + self.raw, + name, + target.raw(), + tagger.raw(), + message + )); + Ok(Binding::from_raw(&raw_oid as *const _)) + } + } + /// Create a new lightweight tag pointing at a target object /// /// A new direct reference will be created pointing to this target object. /// If force is true and a reference already exists with the given name, /// it'll be replaced. - pub fn tag_lightweight(&self, - name: &str, - target: &Object, - force: bool) -> Result { - let name = try!(CString::new(name)); - let mut raw = raw::git_oid { id: [0; raw::GIT_OID_RAWSZ] }; - unsafe { - try_call!(raw::git_tag_create_lightweight(&mut raw, self.raw, name, - target.raw(), force)); + pub fn tag_lightweight( + &self, + name: &str, + target: &Object<'_>, + force: bool, + ) -> Result { + let name = CString::new(name)?; + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_tag_create_lightweight( + &mut raw, + self.raw, + name, + target.raw(), + force + )); Ok(Binding::from_raw(&raw as *const _)) } } /// Lookup a tag object from the repository. - pub fn find_tag(&self, id: Oid) -> Result { - let mut raw = 0 as *mut raw::git_tag; + pub fn find_tag(&self, id: Oid) -> Result, Error> { + let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_tag_lookup(&mut raw, self.raw, id.raw())); Ok(Binding::from_raw(raw)) } } + /// Lookup a tag object by prefix hash from the repository. + pub fn find_tag_by_prefix(&self, prefix_hash: &str) -> Result, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_tag_lookup_prefix( + &mut raw, + self.raw, + Oid::from_str(prefix_hash)?.raw(), + prefix_hash.len() + )); + Ok(Binding::from_raw(raw)) + } + } + /// Delete an existing tag reference. /// /// The tag name will be checked for validity, see `tag` for some rules /// about valid names. pub fn tag_delete(&self, name: &str) -> Result<(), Error> { - let name = try!(CString::new(name)); + let name = CString::new(name)?; unsafe { try_call!(raw::git_tag_delete(self.raw, name)); Ok(()) @@ -1160,29 +2026,61 @@ impl Repository { /// An optional fnmatch pattern can also be specified. pub fn tag_names(&self, pattern: Option<&str>) -> Result { let mut arr = raw::git_strarray { - strings: 0 as *mut *mut c_char, + strings: ptr::null_mut(), count: 0, }; unsafe { match pattern { Some(s) => { - let s = try!(CString::new(s)); + let s = CString::new(s)?; try_call!(raw::git_tag_list_match(&mut arr, s, self.raw)); } - None => { try_call!(raw::git_tag_list(&mut arr, self.raw)); } + None => { + try_call!(raw::git_tag_list(&mut arr, self.raw)); + } } Ok(Binding::from_raw(arr)) } } + /// Iterate over all tags, calling the callback `cb` on each. + /// The arguments of `cb` are the tag id and name, in this order. + /// + /// Returning `false` from `cb` causes the iteration to break early. + pub fn tag_foreach(&self, cb: T) -> Result<(), Error> + where + T: FnMut(Oid, &[u8]) -> bool, + { + let mut data = TagForeachData { + cb: Box::new(cb) as TagForeachCB<'_>, + }; + + unsafe { + raw::git_tag_foreach( + self.raw, + Some(tag_foreach_cb), + (&mut data) as *mut _ as *mut _, + ); + } + Ok(()) + } + /// Updates files in the index and the working tree to match the content of /// the commit pointed at by HEAD. - pub fn checkout_head(&self, opts: Option<&mut CheckoutBuilder>) - -> Result<(), Error> { + /// + /// Note that this is _not_ the correct mechanism used to switch branches; + /// do not change your `HEAD` and then call this method, that would leave + /// you with checkout conflicts since your working directory would then + /// appear to be dirty. Instead, checkout the target of the branch and + /// then update `HEAD` using [`Repository::set_head`] to point to the + /// branch you checked out. + pub fn checkout_head(&self, opts: Option<&mut CheckoutBuilder<'_>>) -> Result<(), Error> { unsafe { let mut raw_opts = mem::zeroed(); - try_call!(raw::git_checkout_init_options(&mut raw_opts, - raw::GIT_CHECKOUT_OPTIONS_VERSION)); + try_call!(raw::git_checkout_init_options( + &mut raw_opts, + raw::GIT_CHECKOUT_OPTIONS_VERSION + )); if let Some(c) = opts { c.configure(&mut raw_opts); } @@ -1195,41 +2093,48 @@ impl Repository { /// Updates files in the working tree to match the content of the index. /// /// If the index is `None`, the repository's index will be used. - pub fn checkout_index(&self, - index: Option<&mut Index>, - opts: Option<&mut CheckoutBuilder>) -> Result<(), Error> { + pub fn checkout_index( + &self, + index: Option<&mut Index>, + opts: Option<&mut CheckoutBuilder<'_>>, + ) -> Result<(), Error> { unsafe { let mut raw_opts = mem::zeroed(); - try_call!(raw::git_checkout_init_options(&mut raw_opts, - raw::GIT_CHECKOUT_OPTIONS_VERSION)); - match opts { - Some(c) => c.configure(&mut raw_opts), - None => {} + try_call!(raw::git_checkout_init_options( + &mut raw_opts, + raw::GIT_CHECKOUT_OPTIONS_VERSION + )); + if let Some(c) = opts { + c.configure(&mut raw_opts); } - try_call!(raw::git_checkout_index(self.raw, - index.map(|i| &mut *i.raw()), - &raw_opts)); + try_call!(raw::git_checkout_index( + self.raw, + index.map(|i| &mut *i.raw()), + &raw_opts + )); } Ok(()) } /// Updates files in the index and working tree to match the content of the /// tree pointed at by the treeish. - pub fn checkout_tree(&self, - treeish: &Object, - opts: Option<&mut CheckoutBuilder>) -> Result<(), Error> { + pub fn checkout_tree( + &self, + treeish: &Object<'_>, + opts: Option<&mut CheckoutBuilder<'_>>, + ) -> Result<(), Error> { unsafe { let mut raw_opts = mem::zeroed(); - try_call!(raw::git_checkout_init_options(&mut raw_opts, - raw::GIT_CHECKOUT_OPTIONS_VERSION)); - match opts { - Some(c) => c.configure(&mut raw_opts), - None => {} + try_call!(raw::git_checkout_init_options( + &mut raw_opts, + raw::GIT_CHECKOUT_OPTIONS_VERSION + )); + if let Some(c) = opts { + c.configure(&mut raw_opts); } - try_call!(raw::git_checkout_tree(self.raw, &*treeish.raw(), - &raw_opts)); + try_call!(raw::git_checkout_tree(self.raw, &*treeish.raw(), &raw_opts)); } Ok(()) } @@ -1240,32 +2145,36 @@ impl Repository { /// after this completes, resolve any conflicts and prepare a commit. /// /// For compatibility with git, the repository is put into a merging state. - /// Once the commit is done (or if the uses wishes to abort), you should - /// clear this state by calling git_repository_state_cleanup(). - pub fn merge(&self, - annotated_commits: &[&AnnotatedCommit], - merge_opts: Option<&mut MergeOptions>, - checkout_opts: Option<&mut CheckoutBuilder>) - -> Result<(), Error> - { + /// Once the commit is done (or if the user wishes to abort), you should + /// clear this state by calling [`cleanup_state()`][Repository::cleanup_state]. + pub fn merge( + &self, + annotated_commits: &[&AnnotatedCommit<'_>], + merge_opts: Option<&mut MergeOptions>, + checkout_opts: Option<&mut CheckoutBuilder<'_>>, + ) -> Result<(), Error> { unsafe { let mut raw_checkout_opts = mem::zeroed(); - try_call!(raw::git_checkout_init_options(&mut raw_checkout_opts, - raw::GIT_CHECKOUT_OPTIONS_VERSION)); + try_call!(raw::git_checkout_init_options( + &mut raw_checkout_opts, + raw::GIT_CHECKOUT_OPTIONS_VERSION + )); if let Some(c) = checkout_opts { c.configure(&mut raw_checkout_opts); } - let mut commit_ptrs = annotated_commits.iter().map(|c| { - c.raw() as *const raw::git_annotated_commit - }).collect::>(); - - try_call!(raw::git_merge(self.raw, - commit_ptrs.as_mut_ptr(), - annotated_commits.len() as size_t, - merge_opts.map(|o| o.raw()) - .unwrap_or(0 as *const _), - &raw_checkout_opts)); + let mut commit_ptrs = annotated_commits + .iter() + .map(|c| c.raw() as *const raw::git_annotated_commit) + .collect::>(); + + try_call!(raw::git_merge( + self.raw, + commit_ptrs.as_mut_ptr(), + annotated_commits.len() as size_t, + merge_opts.map(|o| o.raw()).unwrap_or(ptr::null()), + &raw_checkout_opts + )); } Ok(()) } @@ -1274,14 +2183,46 @@ impl Repository { /// the merge. The index may be written as-is to the working directory or /// checked out. If the index is to be converted to a tree, the caller /// should resolve any conflicts that arose as part of the merge. - pub fn merge_commits(&self, our_commit: &Commit, their_commit: &Commit, - opts: Option<&MergeOptions>) -> Result { - let mut raw = 0 as *mut raw::git_index; - unsafe { - try_call!(raw::git_merge_commits(&mut raw, self.raw, - our_commit.raw(), - their_commit.raw(), - opts.map(|o| o.raw()))); + pub fn merge_commits( + &self, + our_commit: &Commit<'_>, + their_commit: &Commit<'_>, + opts: Option<&MergeOptions>, + ) -> Result { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_merge_commits( + &mut raw, + self.raw, + our_commit.raw(), + their_commit.raw(), + opts.map(|o| o.raw()) + )); + Ok(Binding::from_raw(raw)) + } + } + + /// Merge two trees, producing an index that reflects the result of + /// the merge. The index may be written as-is to the working directory or + /// checked out. If the index is to be converted to a tree, the caller + /// should resolve any conflicts that arose as part of the merge. + pub fn merge_trees( + &self, + ancestor_tree: &Tree<'_>, + our_tree: &Tree<'_>, + their_tree: &Tree<'_>, + opts: Option<&MergeOptions>, + ) -> Result { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_merge_trees( + &mut raw, + self.raw, + ancestor_tree.raw(), + our_tree.raw(), + their_tree.raw(), + opts.map(|o| o.raw()) + )); Ok(Binding::from_raw(raw)) } } @@ -1295,30 +2236,131 @@ impl Repository { Ok(()) } + /// Analyzes the given branch(es) and determines the opportunities for + /// merging them into the HEAD of the repository. + pub fn merge_analysis( + &self, + their_heads: &[&AnnotatedCommit<'_>], + ) -> Result<(MergeAnalysis, MergePreference), Error> { + unsafe { + let mut raw_merge_analysis = 0 as raw::git_merge_analysis_t; + let mut raw_merge_preference = 0 as raw::git_merge_preference_t; + let mut their_heads = their_heads + .iter() + .map(|v| v.raw() as *const _) + .collect::>(); + try_call!(raw::git_merge_analysis( + &mut raw_merge_analysis, + &mut raw_merge_preference, + self.raw, + their_heads.as_mut_ptr() as *mut _, + their_heads.len() + )); + Ok(( + MergeAnalysis::from_bits_truncate(raw_merge_analysis as u32), + MergePreference::from_bits_truncate(raw_merge_preference as u32), + )) + } + } + + /// Analyzes the given branch(es) and determines the opportunities for + /// merging them into a reference. + pub fn merge_analysis_for_ref( + &self, + our_ref: &Reference<'_>, + their_heads: &[&AnnotatedCommit<'_>], + ) -> Result<(MergeAnalysis, MergePreference), Error> { + unsafe { + let mut raw_merge_analysis = 0 as raw::git_merge_analysis_t; + let mut raw_merge_preference = 0 as raw::git_merge_preference_t; + let mut their_heads = their_heads + .iter() + .map(|v| v.raw() as *const _) + .collect::>(); + try_call!(raw::git_merge_analysis_for_ref( + &mut raw_merge_analysis, + &mut raw_merge_preference, + self.raw, + our_ref.raw(), + their_heads.as_mut_ptr() as *mut _, + their_heads.len() + )); + Ok(( + MergeAnalysis::from_bits_truncate(raw_merge_analysis as u32), + MergePreference::from_bits_truncate(raw_merge_preference as u32), + )) + } + } + + /// Initializes a rebase operation to rebase the changes in `branch` + /// relative to `upstream` onto another branch. To begin the rebase process, + /// call `next()`. + pub fn rebase( + &self, + branch: Option<&AnnotatedCommit<'_>>, + upstream: Option<&AnnotatedCommit<'_>>, + onto: Option<&AnnotatedCommit<'_>>, + opts: Option<&mut RebaseOptions<'_>>, + ) -> Result, Error> { + let mut rebase: *mut raw::git_rebase = ptr::null_mut(); + unsafe { + try_call!(raw::git_rebase_init( + &mut rebase, + self.raw(), + branch.map(|c| c.raw()), + upstream.map(|c| c.raw()), + onto.map(|c| c.raw()), + opts.map(|o| o.raw()).unwrap_or(ptr::null()) + )); + + Ok(Rebase::from_raw(rebase)) + } + } + + /// Opens an existing rebase that was previously started by either an + /// invocation of `rebase()` or by another client. + pub fn open_rebase(&self, opts: Option<&mut RebaseOptions<'_>>) -> Result, Error> { + let mut rebase: *mut raw::git_rebase = ptr::null_mut(); + unsafe { + try_call!(raw::git_rebase_open( + &mut rebase, + self.raw(), + opts.map(|o| o.raw()).unwrap_or(ptr::null()) + )); + Ok(Rebase::from_raw(rebase)) + } + } + /// Add a note for an object /// /// The `notes_ref` argument is the canonical name of the reference to use, /// defaulting to "refs/notes/commits". If `force` is specified then /// previous notes are overwritten. - pub fn note(&self, - author: &Signature, - committer: &Signature, - notes_ref: Option<&str>, - oid: Oid, - note: &str, - force: bool) -> Result { - let notes_ref = try!(::opt_cstr(notes_ref)); - let note = try!(CString::new(note)); - let mut ret = raw::git_oid { id: [0; raw::GIT_OID_RAWSZ] }; - unsafe { - try_call!(raw::git_note_create(&mut ret, - self.raw, - notes_ref, - author.raw(), - committer.raw(), - oid.raw(), - note, - force)); + pub fn note( + &self, + author: &Signature<'_>, + committer: &Signature<'_>, + notes_ref: Option<&str>, + oid: Oid, + note: &str, + force: bool, + ) -> Result { + let notes_ref = crate::opt_cstr(notes_ref)?; + let note = CString::new(note)?; + let mut ret = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_note_create( + &mut ret, + self.raw, + notes_ref, + author.raw(), + committer.raw(), + oid.raw(), + note, + force + )); Ok(Binding::from_raw(&ret as *const _)) } } @@ -1340,9 +2382,9 @@ impl Repository { /// The iterator returned yields pairs of (Oid, Oid) where the first element /// is the id of the note and the second id is the id the note is /// annotating. - pub fn notes(&self, notes_ref: Option<&str>) -> Result { - let notes_ref = try!(::opt_cstr(notes_ref)); - let mut ret = 0 as *mut raw::git_note_iterator; + pub fn notes(&self, notes_ref: Option<&str>) -> Result, Error> { + let notes_ref = crate::opt_cstr(notes_ref)?; + let mut ret = ptr::null_mut(); unsafe { try_call!(raw::git_note_iterator_new(&mut ret, self.raw, notes_ref)); Ok(Binding::from_raw(ret)) @@ -1355,13 +2397,11 @@ impl Repository { /// defaulting to "refs/notes/commits". /// /// The id specified is the Oid of the git object to read the note from. - pub fn find_note(&self, notes_ref: Option<&str>, id: Oid) - -> Result { - let notes_ref = try!(::opt_cstr(notes_ref)); - let mut ret = 0 as *mut raw::git_note; + pub fn find_note(&self, notes_ref: Option<&str>, id: Oid) -> Result, Error> { + let notes_ref = crate::opt_cstr(notes_ref)?; + let mut ret = ptr::null_mut(); unsafe { - try_call!(raw::git_note_read(&mut ret, self.raw, notes_ref, - id.raw())); + try_call!(raw::git_note_read(&mut ret, self.raw, notes_ref, id.raw())); Ok(Binding::from_raw(ret)) } } @@ -1372,22 +2412,29 @@ impl Repository { /// defaulting to "refs/notes/commits". /// /// The id specified is the Oid of the git object to remove the note from. - pub fn note_delete(&self, - id: Oid, - notes_ref: Option<&str>, - author: &Signature, - committer: &Signature) -> Result<(), Error> { - let notes_ref = try!(::opt_cstr(notes_ref)); - unsafe { - try_call!(raw::git_note_remove(self.raw, notes_ref, author.raw(), - committer.raw(), id.raw())); + pub fn note_delete( + &self, + id: Oid, + notes_ref: Option<&str>, + author: &Signature<'_>, + committer: &Signature<'_>, + ) -> Result<(), Error> { + let notes_ref = crate::opt_cstr(notes_ref)?; + unsafe { + try_call!(raw::git_note_remove( + self.raw, + notes_ref, + author.raw(), + committer.raw(), + id.raw() + )); Ok(()) } } /// Create a revwalk that can be used to traverse the commit graph. - pub fn revwalk(&self) -> Result { - let mut raw = 0 as *mut raw::git_revwalk; + pub fn revwalk(&self) -> Result, Error> { + let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_revwalk_new(&mut raw, self.raw())); Ok(Binding::from_raw(raw)) @@ -1395,26 +2442,103 @@ impl Repository { } /// Get the blame for a single file. - pub fn blame_file(&self, path: &Path, opts: Option<&mut BlameOptions>) - -> Result { - let path = try!(path.into_c_string()); - let mut raw = 0 as *mut raw::git_blame; + pub fn blame_file( + &self, + path: &Path, + opts: Option<&mut BlameOptions>, + ) -> Result, Error> { + let path = path_to_repo_path(path)?; + let mut raw = ptr::null_mut(); unsafe { - try_call!(raw::git_blame_file(&mut raw, - self.raw(), - path, - opts.map(|s| s.raw()))); + try_call!(raw::git_blame_file( + &mut raw, + self.raw(), + path, + opts.map(|s| s.raw()) + )); Ok(Binding::from_raw(raw)) } } /// Find a merge base between two commits pub fn merge_base(&self, one: Oid, two: Oid) -> Result { - let mut raw = raw::git_oid { id: [0; raw::GIT_OID_RAWSZ] }; + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_merge_base( + &mut raw, + self.raw, + one.raw(), + two.raw() + )); + Ok(Binding::from_raw(&raw as *const _)) + } + } + + /// Find a merge base given a list of commits + /// + /// This behaves similar to [`git merge-base`](https://git-scm.com/docs/git-merge-base#_discussion). + /// Given three commits `a`, `b`, and `c`, `merge_base_many(&[a, b, c])` + /// will compute a hypothetical commit `m`, which is a merge between `b` + /// and `c`. + /// + /// For example, with the following topology: + /// ```text + /// o---o---o---o---C + /// / + /// / o---o---o---B + /// / / + /// ---2---1---o---o---o---A + /// ``` + /// + /// the result of `merge_base_many(&[a, b, c])` is 1. This is because the + /// equivalent topology with a merge commit `m` between `b` and `c` would + /// is: + /// ```text + /// o---o---o---o---o + /// / \ + /// / o---o---o---o---M + /// / / + /// ---2---1---o---o---o---A + /// ``` + /// + /// and the result of `merge_base_many(&[a, m])` is 1. + /// + /// --- + /// + /// If you're looking to recieve the common merge base between all the + /// given commits, use [`Self::merge_base_octopus`]. + pub fn merge_base_many(&self, oids: &[Oid]) -> Result { + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + + unsafe { + try_call!(raw::git_merge_base_many( + &mut raw, + self.raw, + oids.len() as size_t, + oids.as_ptr() as *const raw::git_oid + )); + Ok(Binding::from_raw(&raw as *const _)) + } + } + + /// Find a common merge base between all given a list of commits + pub fn merge_base_octopus(&self, oids: &[Oid]) -> Result { + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { - try_call!(raw::git_merge_base(&mut raw, self.raw, - one.raw(), two.raw())); + try_call!(raw::git_merge_base_octopus( + &mut raw, + self.raw, + oids.len() as size_t, + oids.as_ptr() as *const raw::git_oid + )); Ok(Binding::from_raw(&raw as *const _)) } } @@ -1422,16 +2546,63 @@ impl Repository { /// Find all merge bases between two commits pub fn merge_bases(&self, one: Oid, two: Oid) -> Result { let mut arr = raw::git_oidarray { - ids: 0 as *mut raw::git_oid, + ids: ptr::null_mut(), count: 0, }; unsafe { - try_call!(raw::git_merge_bases(&mut arr, self.raw, - one.raw(), two.raw())); + try_call!(raw::git_merge_bases( + &mut arr, + self.raw, + one.raw(), + two.raw() + )); Ok(Binding::from_raw(arr)) } } + /// Find all merge bases given a list of commits + pub fn merge_bases_many(&self, oids: &[Oid]) -> Result { + let mut arr = raw::git_oidarray { + ids: ptr::null_mut(), + count: 0, + }; + unsafe { + try_call!(raw::git_merge_bases_many( + &mut arr, + self.raw, + oids.len() as size_t, + oids.as_ptr() as *const raw::git_oid + )); + Ok(Binding::from_raw(arr)) + } + } + + /// Merge two files as they exist in the index, using the given common ancestor + /// as the baseline. + pub fn merge_file_from_index( + &self, + ancestor: &IndexEntry, + ours: &IndexEntry, + theirs: &IndexEntry, + opts: Option<&mut MergeFileOptions>, + ) -> Result { + unsafe { + let (ancestor, _ancestor_path) = ancestor.to_raw()?; + let (ours, _ours_path) = ours.to_raw()?; + let (theirs, _theirs_path) = theirs.to_raw()?; + + let mut ret = mem::zeroed(); + try_call!(raw::git_merge_file_from_index( + &mut ret, + self.raw(), + &ancestor, + &ours, + &theirs, + opts.map(|o| o.raw()).unwrap_or(ptr::null()) + )); + Ok(Binding::from_raw(ret)) + } + } /// Count the number of unique commits between two commit objects /// @@ -1439,25 +2610,32 @@ impl Repository { /// upstream relationship, but it helps to think of one as a branch and the /// other as its upstream, the ahead and behind values will be what git /// would report for the branches. - pub fn graph_ahead_behind(&self, local: Oid, upstream: Oid) - -> Result<(usize, usize), Error> { + pub fn graph_ahead_behind(&self, local: Oid, upstream: Oid) -> Result<(usize, usize), Error> { unsafe { let mut ahead: size_t = 0; let mut behind: size_t = 0; - try_call!(raw::git_graph_ahead_behind(&mut ahead, &mut behind, - self.raw(), local.raw(), - upstream.raw())); + try_call!(raw::git_graph_ahead_behind( + &mut ahead, + &mut behind, + self.raw(), + local.raw(), + upstream.raw() + )); Ok((ahead as usize, behind as usize)) } } /// Determine if a commit is the descendant of another commit - pub fn graph_descendant_of(&self, commit: Oid, ancestor: Oid) - -> Result { + /// + /// Note that a commit is not considered a descendant of itself, in contrast + /// to `git merge-base --is-ancestor`. + pub fn graph_descendant_of(&self, commit: Oid, ancestor: Oid) -> Result { unsafe { - let rv = try_call!(raw::git_graph_descendant_of(self.raw(), - commit.raw(), - ancestor.raw())); + let rv = try_call!(raw::git_graph_descendant_of( + self.raw(), + commit.raw(), + ancestor.raw() + )); Ok(rv != 0) } } @@ -1467,8 +2645,8 @@ impl Repository { /// If there is no reflog file for the given reference yet, an empty reflog /// object will be returned. pub fn reflog(&self, name: &str) -> Result { - let name = try!(CString::new(name)); - let mut ret = 0 as *mut raw::git_reflog; + let name = CString::new(name)?; + let mut ret = ptr::null_mut(); unsafe { try_call!(raw::git_reflog_read(&mut ret, self.raw, name)); Ok(Binding::from_raw(ret)) @@ -1477,18 +2655,19 @@ impl Repository { /// Delete the reflog for the given reference pub fn reflog_delete(&self, name: &str) -> Result<(), Error> { - let name = try!(CString::new(name)); - unsafe { try_call!(raw::git_reflog_delete(self.raw, name)); } + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_reflog_delete(self.raw, name)); + } Ok(()) } /// Rename a reflog /// /// The reflog to be renamed is expected to already exist. - pub fn reflog_rename(&self, old_name: &str, new_name: &str) - -> Result<(), Error> { - let old_name = try!(CString::new(old_name)); - let new_name = try!(CString::new(new_name)); + pub fn reflog_rename(&self, old_name: &str, new_name: &str) -> Result<(), Error> { + let old_name = CString::new(old_name)?; + let new_name = CString::new(new_name)?; unsafe { try_call!(raw::git_reflog_rename(self.raw, old_name, new_name)); } @@ -1497,16 +2676,14 @@ impl Repository { /// Check if the given reference has a reflog. pub fn reference_has_log(&self, name: &str) -> Result { - let name = try!(CString::new(name)); - let ret = unsafe { - try_call!(raw::git_reference_has_log(self.raw, name)) - }; + let name = CString::new(name)?; + let ret = unsafe { try_call!(raw::git_reference_has_log(self.raw, name)) }; Ok(ret != 0) } /// Ensure that the given reference has a reflog. pub fn reference_ensure_log(&self, name: &str) -> Result<(), Error> { - let name = try!(CString::new(name)); + let name = CString::new(name)?; unsafe { try_call!(raw::git_reference_ensure_log(self.raw, name)); } @@ -1518,14 +2695,86 @@ impl Repository { /// Performs a describe operation on the current commit and the worktree. /// After performing a describe on HEAD, a status is run and description is /// considered to be dirty if there are. - pub fn describe(&self, opts: &DescribeOptions) -> Result { - let mut ret = 0 as *mut _; + pub fn describe(&self, opts: &DescribeOptions) -> Result, Error> { + let mut ret = ptr::null_mut(); unsafe { try_call!(raw::git_describe_workdir(&mut ret, self.raw, opts.raw())); Ok(Binding::from_raw(ret)) } } + /// Directly run a diff on two blobs. + /// + /// Compared to a file, a blob lacks some contextual information. As such, the + /// `DiffFile` given to the callback will have some fake data; i.e. mode will be + /// 0 and path will be `None`. + /// + /// `None` is allowed for either `old_blob` or `new_blob` and will be treated + /// as an empty blob, with the oid set to zero in the `DiffFile`. Passing `None` + /// for both blobs is a noop; no callbacks will be made at all. + /// + /// We do run a binary content check on the blob content and if either blob looks + /// like binary data, the `DiffFile` binary attribute will be set to 1 and no call to + /// the `hunk_cb` nor `line_cb` will be made (unless you set the `force_text` + /// option). + pub fn diff_blobs( + &self, + old_blob: Option<&Blob<'_>>, + old_as_path: Option<&str>, + new_blob: Option<&Blob<'_>>, + new_as_path: Option<&str>, + opts: Option<&mut DiffOptions>, + file_cb: Option<&mut FileCb<'_>>, + binary_cb: Option<&mut BinaryCb<'_>>, + hunk_cb: Option<&mut HunkCb<'_>>, + line_cb: Option<&mut LineCb<'_>>, + ) -> Result<(), Error> { + let old_as_path = crate::opt_cstr(old_as_path)?; + let new_as_path = crate::opt_cstr(new_as_path)?; + let mut cbs = DiffCallbacks { + file: file_cb, + binary: binary_cb, + hunk: hunk_cb, + line: line_cb, + }; + let ptr = &mut cbs as *mut _; + unsafe { + let file_cb_c: raw::git_diff_file_cb = if cbs.file.is_some() { + Some(file_cb_c) + } else { + None + }; + let binary_cb_c: raw::git_diff_binary_cb = if cbs.binary.is_some() { + Some(binary_cb_c) + } else { + None + }; + let hunk_cb_c: raw::git_diff_hunk_cb = if cbs.hunk.is_some() { + Some(hunk_cb_c) + } else { + None + }; + let line_cb_c: raw::git_diff_line_cb = if cbs.line.is_some() { + Some(line_cb_c) + } else { + None + }; + try_call!(raw::git_diff_blobs( + old_blob.map(|s| s.raw()), + old_as_path, + new_blob.map(|s| s.raw()), + new_as_path, + opts.map(|s| s.raw()), + file_cb_c, + binary_cb_c, + hunk_cb_c, + line_cb_c, + ptr as *mut _ + )); + Ok(()) + } + } + /// Create a diff with the difference between two tree objects. /// /// This is equivalent to `git diff ` @@ -1534,18 +2783,21 @@ impl Repository { /// second tree will be used for the "new_file" side of the delta. You can /// pass `None` to indicate an empty tree, although it is an error to pass /// `None` for both the `old_tree` and `new_tree`. - pub fn diff_tree_to_tree(&self, - old_tree: Option<&Tree>, - new_tree: Option<&Tree>, - opts: Option<&mut DiffOptions>) - -> Result { - let mut ret = 0 as *mut raw::git_diff; - unsafe { - try_call!(raw::git_diff_tree_to_tree(&mut ret, - self.raw(), - old_tree.map(|s| s.raw()), - new_tree.map(|s| s.raw()), - opts.map(|s| s.raw()))); + pub fn diff_tree_to_tree( + &self, + old_tree: Option<&Tree<'_>>, + new_tree: Option<&Tree<'_>>, + opts: Option<&mut DiffOptions>, + ) -> Result, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_diff_tree_to_tree( + &mut ret, + self.raw(), + old_tree.map(|s| s.raw()), + new_tree.map(|s| s.raw()), + opts.map(|s| s.raw()) + )); Ok(Binding::from_raw(ret)) } } @@ -1563,18 +2815,21 @@ impl Repository { /// (if it has changed) before the diff is generated. /// /// If the tree is `None`, then it is considered an empty tree. - pub fn diff_tree_to_index(&self, - old_tree: Option<&Tree>, - index: Option<&Index>, - opts: Option<&mut DiffOptions>) - -> Result { - let mut ret = 0 as *mut raw::git_diff; - unsafe { - try_call!(raw::git_diff_tree_to_index(&mut ret, - self.raw(), - old_tree.map(|s| s.raw()), - index.map(|s| s.raw()), - opts.map(|s| s.raw()))); + pub fn diff_tree_to_index( + &self, + old_tree: Option<&Tree<'_>>, + index: Option<&Index>, + opts: Option<&mut DiffOptions>, + ) -> Result, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_diff_tree_to_index( + &mut ret, + self.raw(), + old_tree.map(|s| s.raw()), + index.map(|s| s.raw()), + opts.map(|s| s.raw()) + )); Ok(Binding::from_raw(ret)) } } @@ -1583,18 +2838,21 @@ impl Repository { /// /// The first index will be used for the "old_file" side of the delta, and /// the second index will be used for the "new_file" side of the delta. - pub fn diff_index_to_index(&self, - old_index: &Index, - new_index: &Index, - opts: Option<&mut DiffOptions>) - -> Result { - let mut ret = 0 as *mut raw::git_diff; - unsafe { - try_call!(raw::git_diff_index_to_index(&mut ret, - self.raw(), - old_index.raw(), - new_index.raw(), - opts.map(|s| s.raw()))); + pub fn diff_index_to_index( + &self, + old_index: &Index, + new_index: &Index, + opts: Option<&mut DiffOptions>, + ) -> Result, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_diff_index_to_index( + &mut ret, + self.raw(), + old_index.raw(), + new_index.raw(), + opts.map(|s| s.raw()) + )); Ok(Binding::from_raw(ret)) } } @@ -1612,16 +2870,19 @@ impl Repository { /// If you pass `None` for the index, then the existing index of the `repo` /// will be used. In this case, the index will be refreshed from disk /// (if it has changed) before the diff is generated. - pub fn diff_index_to_workdir(&self, - index: Option<&Index>, - opts: Option<&mut DiffOptions>) - -> Result { - let mut ret = 0 as *mut raw::git_diff; - unsafe { - try_call!(raw::git_diff_index_to_workdir(&mut ret, - self.raw(), - index.map(|s| s.raw()), - opts.map(|s| s.raw()))); + pub fn diff_index_to_workdir( + &self, + index: Option<&Index>, + opts: Option<&mut DiffOptions>, + ) -> Result, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_diff_index_to_workdir( + &mut ret, + self.raw(), + index.map(|s| s.raw()), + opts.map(|s| s.raw()) + )); Ok(Binding::from_raw(ret)) } } @@ -1644,16 +2905,19 @@ impl Repository { /// show status 'deleted' since there is a staged delete. /// /// If `None` is passed for `tree`, then an empty tree is used. - pub fn diff_tree_to_workdir(&self, - old_tree: Option<&Tree>, - opts: Option<&mut DiffOptions>) - -> Result { - let mut ret = 0 as *mut raw::git_diff; - unsafe { - try_call!(raw::git_diff_tree_to_workdir(&mut ret, - self.raw(), - old_tree.map(|s| s.raw()), - opts.map(|s| s.raw()))); + pub fn diff_tree_to_workdir( + &self, + old_tree: Option<&Tree<'_>>, + opts: Option<&mut DiffOptions>, + ) -> Result, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_diff_tree_to_workdir( + &mut ret, + self.raw(), + old_tree.map(|s| s.raw()), + opts.map(|s| s.raw()) + )); Ok(Binding::from_raw(ret)) } } @@ -1664,27 +2928,454 @@ impl Repository { /// This emulates `git diff ` by diffing the tree to the index and /// the index to the working directory and blending the results into a /// single diff that includes staged deleted, etc. - pub fn diff_tree_to_workdir_with_index(&self, - old_tree: Option<&Tree>, - opts: Option<&mut DiffOptions>) - -> Result { - let mut ret = 0 as *mut raw::git_diff; - unsafe { - try_call!(raw::git_diff_tree_to_workdir_with_index(&mut ret, - self.raw(), old_tree.map(|s| s.raw()), opts.map(|s| s.raw()))); + pub fn diff_tree_to_workdir_with_index( + &self, + old_tree: Option<&Tree<'_>>, + opts: Option<&mut DiffOptions>, + ) -> Result, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_diff_tree_to_workdir_with_index( + &mut ret, + self.raw(), + old_tree.map(|s| s.raw()), + opts.map(|s| s.raw()) + )); Ok(Binding::from_raw(ret)) } } /// Create a PackBuilder - pub fn packbuilder(&self) -> Result { - let mut ret = 0 as *mut raw::git_packbuilder; + pub fn packbuilder(&self) -> Result, Error> { + let mut ret = ptr::null_mut(); unsafe { try_call!(raw::git_packbuilder_new(&mut ret, self.raw())); Ok(Binding::from_raw(ret)) } } + /// Save the local modifications to a new stash. + pub fn stash_save( + &mut self, + stasher: &Signature<'_>, + message: &str, + flags: Option, + ) -> Result { + self.stash_save2(stasher, Some(message), flags) + } + + /// Save the local modifications to a new stash. + /// unlike `stash_save` it allows to pass a null `message` + pub fn stash_save2( + &mut self, + stasher: &Signature<'_>, + message: Option<&str>, + flags: Option, + ) -> Result { + unsafe { + let mut raw_oid = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + let message = crate::opt_cstr(message)?; + let flags = flags.unwrap_or_else(StashFlags::empty); + try_call!(raw::git_stash_save( + &mut raw_oid, + self.raw(), + stasher.raw(), + message, + flags.bits() as c_uint + )); + Ok(Binding::from_raw(&raw_oid as *const _)) + } + } + + /// Like `stash_save` but with more options like selective statshing via path patterns. + pub fn stash_save_ext( + &mut self, + opts: Option<&mut StashSaveOptions<'_>>, + ) -> Result { + unsafe { + let mut raw_oid = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + let opts = opts.map(|opts| opts.raw()); + try_call!(raw::git_stash_save_with_opts( + &mut raw_oid, + self.raw(), + opts + )); + Ok(Binding::from_raw(&raw_oid as *const _)) + } + } + + /// Apply a single stashed state from the stash list. + pub fn stash_apply( + &mut self, + index: usize, + opts: Option<&mut StashApplyOptions<'_>>, + ) -> Result<(), Error> { + unsafe { + let opts = opts.map(|opts| opts.raw()); + try_call!(raw::git_stash_apply(self.raw(), index, opts)); + Ok(()) + } + } + + /// Loop over all the stashed states and issue a callback for each one. + /// + /// Return `true` to continue iterating or `false` to stop. + pub fn stash_foreach(&mut self, mut callback: C) -> Result<(), Error> + where + C: FnMut(usize, &str, &Oid) -> bool, + { + unsafe { + let mut data = StashCbData { + callback: &mut callback, + }; + let cb: raw::git_stash_cb = Some(stash_cb); + try_call!(raw::git_stash_foreach( + self.raw(), + cb, + &mut data as *mut _ as *mut _ + )); + Ok(()) + } + } + + /// Remove a single stashed state from the stash list. + pub fn stash_drop(&mut self, index: usize) -> Result<(), Error> { + unsafe { + try_call!(raw::git_stash_drop(self.raw(), index)); + Ok(()) + } + } + + /// Apply a single stashed state from the stash list and remove it from the list if successful. + pub fn stash_pop( + &mut self, + index: usize, + opts: Option<&mut StashApplyOptions<'_>>, + ) -> Result<(), Error> { + unsafe { + let opts = opts.map(|opts| opts.raw()); + try_call!(raw::git_stash_pop(self.raw(), index, opts)); + Ok(()) + } + } + + /// Add ignore rules for a repository. + /// + /// The format of the rules is the same one of the .gitignore file. + pub fn add_ignore_rule(&self, rules: &str) -> Result<(), Error> { + let rules = CString::new(rules)?; + unsafe { + try_call!(raw::git_ignore_add_rule(self.raw, rules)); + } + Ok(()) + } + + /// Clear ignore rules that were explicitly added. + pub fn clear_ignore_rules(&self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_ignore_clear_internal_rules(self.raw)); + } + Ok(()) + } + + /// Test if the ignore rules apply to a given path. + pub fn is_path_ignored>(&self, path: P) -> Result { + let path = util::cstring_to_repo_path(path.as_ref())?; + let mut ignored: c_int = 0; + unsafe { + try_call!(raw::git_ignore_path_is_ignored( + &mut ignored, + self.raw, + path + )); + } + Ok(ignored == 1) + } + + /// Perform a cherrypick + pub fn cherrypick( + &self, + commit: &Commit<'_>, + options: Option<&mut CherrypickOptions<'_>>, + ) -> Result<(), Error> { + let raw_opts = options.map(|o| o.raw()); + let ptr_raw_opts = match raw_opts.as_ref() { + Some(v) => v, + None => std::ptr::null(), + }; + unsafe { + try_call!(raw::git_cherrypick(self.raw(), commit.raw(), ptr_raw_opts)); + + Ok(()) + } + } + + /// Create an index of uncommitted changes, representing the result of + /// cherry-picking. + pub fn cherrypick_commit( + &self, + cherrypick_commit: &Commit<'_>, + our_commit: &Commit<'_>, + mainline: u32, + options: Option<&MergeOptions>, + ) -> Result { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_cherrypick_commit( + &mut ret, + self.raw(), + cherrypick_commit.raw(), + our_commit.raw(), + mainline, + options.map(|o| o.raw()) + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Find the remote name of a remote-tracking branch + pub fn branch_remote_name(&self, refname: &str) -> Result { + let refname = CString::new(refname)?; + unsafe { + let buf = Buf::new(); + try_call!(raw::git_branch_remote_name(buf.raw(), self.raw, refname)); + Ok(buf) + } + } + + /// Retrieves the name of the reference supporting the remote tracking branch, + /// given the name of a local branch reference. + pub fn branch_upstream_name(&self, refname: &str) -> Result { + let refname = CString::new(refname)?; + unsafe { + let buf = Buf::new(); + try_call!(raw::git_branch_upstream_name(buf.raw(), self.raw, refname)); + Ok(buf) + } + } + + /// Retrieve the name of the upstream remote of a local branch. + /// + /// `refname` must be in the form `refs/heads/{branch_name}` + pub fn branch_upstream_remote(&self, refname: &str) -> Result { + let refname = CString::new(refname)?; + unsafe { + let buf = Buf::new(); + try_call!(raw::git_branch_upstream_remote( + buf.raw(), + self.raw, + refname + )); + Ok(buf) + } + } + + /// Retrieve the upstream merge of a local branch, + /// configured in "branch.*.merge" + /// + /// `refname` must be in the form `refs/heads/{branch_name}` + pub fn branch_upstream_merge(&self, refname: &str) -> Result { + let refname = CString::new(refname)?; + unsafe { + let buf = Buf::new(); + try_call!(raw::git_branch_upstream_merge(buf.raw(), self.raw, refname)); + Ok(buf) + } + } + + /// Apply a Diff to the given repo, making changes directly in the working directory, the index, or both. + pub fn apply( + &self, + diff: &Diff<'_>, + location: ApplyLocation, + options: Option<&mut ApplyOptions<'_>>, + ) -> Result<(), Error> { + unsafe { + try_call!(raw::git_apply( + self.raw, + diff.raw(), + location.raw(), + options.map(|s| s.raw()).unwrap_or(ptr::null()) + )); + + Ok(()) + } + } + + /// Apply a Diff to the provided tree, and return the resulting Index. + pub fn apply_to_tree( + &self, + tree: &Tree<'_>, + diff: &Diff<'_>, + options: Option<&mut ApplyOptions<'_>>, + ) -> Result { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_apply_to_tree( + &mut ret, + self.raw, + tree.raw(), + diff.raw(), + options.map(|s| s.raw()).unwrap_or(ptr::null()) + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Reverts the given commit, producing changes in the index and working directory. + pub fn revert( + &self, + commit: &Commit<'_>, + options: Option<&mut RevertOptions<'_>>, + ) -> Result<(), Error> { + let raw_opts = options.map(|o| o.raw()); + let ptr_raw_opts = match raw_opts.as_ref() { + Some(v) => v, + None => 0 as *const _, + }; + unsafe { + try_call!(raw::git_revert(self.raw(), commit.raw(), ptr_raw_opts)); + Ok(()) + } + } + + /// Reverts the given commit against the given "our" commit, + /// producing an index that reflects the result of the revert. + pub fn revert_commit( + &self, + revert_commit: &Commit<'_>, + our_commit: &Commit<'_>, + mainline: u32, + options: Option<&MergeOptions>, + ) -> Result { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_revert_commit( + &mut ret, + self.raw(), + revert_commit.raw(), + our_commit.raw(), + mainline, + options.map(|o| o.raw()) + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Lists all the worktrees for the repository + pub fn worktrees(&self) -> Result { + let mut arr = raw::git_strarray { + strings: ptr::null_mut(), + count: 0, + }; + unsafe { + try_call!(raw::git_worktree_list(&mut arr, self.raw)); + Ok(Binding::from_raw(arr)) + } + } + + /// Opens a worktree by name for the given repository + /// + /// This can open any worktree that the worktrees method returns. + pub fn find_worktree(&self, name: &str) -> Result { + let mut raw = ptr::null_mut(); + let raw_name = CString::new(name)?; + unsafe { + try_call!(raw::git_worktree_lookup(&mut raw, self.raw, raw_name)); + Ok(Binding::from_raw(raw)) + } + } + + /// Creates a new worktree for the repository + pub fn worktree<'a>( + &'a self, + name: &str, + path: &Path, + opts: Option<&WorktreeAddOptions<'a>>, + ) -> Result { + let mut raw = ptr::null_mut(); + let raw_name = CString::new(name)?; + let raw_path = path.into_c_string()?; + + unsafe { + try_call!(raw::git_worktree_add( + &mut raw, + self.raw, + raw_name, + raw_path, + opts.map(|o| o.raw()) + )); + Ok(Binding::from_raw(raw)) + } + } + + /// Create a new transaction + pub fn transaction<'a>(&'a self) -> Result, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_transaction_new(&mut raw, self.raw)); + Ok(Binding::from_raw(raw)) + } + } + + /// Gets this repository's mailmap. + pub fn mailmap(&self) -> Result { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_mailmap_from_repository(&mut ret, self.raw)); + Ok(Binding::from_raw(ret)) + } + } + + /// If a merge is in progress, invoke 'callback' for each commit ID in the + /// MERGE_HEAD file. + pub fn mergehead_foreach(&mut self, mut callback: C) -> Result<(), Error> + where + C: FnMut(&Oid) -> bool, + { + unsafe { + let mut data = MergeheadForeachCbData { + callback: &mut callback, + }; + let cb: raw::git_repository_mergehead_foreach_cb = Some(mergehead_foreach_cb); + try_call!(raw::git_repository_mergehead_foreach( + self.raw(), + cb, + &mut data as *mut _ as *mut _ + )); + Ok(()) + } + } + + /// Invoke 'callback' for each entry in the given FETCH_HEAD file. + /// + /// `callback` will be called with with following arguments: + /// + /// - `&str`: the reference name + /// - `&[u8]`: the remote URL + /// - `&Oid`: the reference target OID + /// - `bool`: was the reference the result of a merge + pub fn fetchhead_foreach(&self, mut callback: C) -> Result<(), Error> + where + C: FnMut(&str, &[u8], &Oid, bool) -> bool, + { + unsafe { + let mut data = FetchheadForeachCbData { + callback: &mut callback, + }; + let cb: raw::git_repository_fetchhead_foreach_cb = Some(fetchhead_foreach_cb); + try_call!(raw::git_repository_fetchhead_foreach( + self.raw(), + cb, + &mut data as *mut _ as *mut _ + )); + Ok(()) + } + } } impl Binding for Repository { @@ -1692,7 +3383,9 @@ impl Binding for Repository { unsafe fn from_raw(ptr: *mut raw::git_repository) -> Repository { Repository { raw: ptr } } - fn raw(&self) -> *mut raw::git_repository { self.raw } + fn raw(&self) -> *mut raw::git_repository { + self.raw + } } impl Drop for Repository { @@ -1708,9 +3401,9 @@ impl RepositoryInitOptions { /// and initializing a directory from the user-configured templates path. pub fn new() -> RepositoryInitOptions { RepositoryInitOptions { - flags: raw::GIT_REPOSITORY_INIT_MKDIR as u32 | - raw::GIT_REPOSITORY_INIT_MKPATH as u32 | - raw::GIT_REPOSITORY_INIT_EXTERNAL_TEMPLATE as u32, + flags: raw::GIT_REPOSITORY_INIT_MKDIR as u32 + | raw::GIT_REPOSITORY_INIT_MKPATH as u32 + | raw::GIT_REPOSITORY_INIT_EXTERNAL_TEMPLATE as u32, mode: 0, workdir_path: None, description: None, @@ -1735,7 +3428,7 @@ impl RepositoryInitOptions { self.flag(raw::GIT_REPOSITORY_INIT_NO_REINIT, enabled) } - /// Normally a '/.git/' will be appended to the repo apth for non-bare repos + /// Normally a '/.git/' will be appended to the repo path for non-bare repos /// (if it is not already there), but passing this flag prevents that /// behavior. /// @@ -1752,7 +3445,7 @@ impl RepositoryInitOptions { self.flag(raw::GIT_REPOSITORY_INIT_MKDIR, enabled) } - /// Recursively make all components of the repo and workdir path sas + /// Recursively make all components of the repo and workdir path as /// necessary. /// /// Defaults to true. @@ -1761,8 +3454,7 @@ impl RepositoryInitOptions { } /// Set to one of the `RepositoryInit` constants, or a custom value. - pub fn mode(&mut self, mode: RepositoryInitMode) - -> &mut RepositoryInitOptions { + pub fn mode(&mut self, mode: RepositoryInitMode) -> &mut RepositoryInitOptions { self.mode = mode.bits(); self } @@ -1774,13 +3466,15 @@ impl RepositoryInitOptions { /// `/usr/share/git-core-templates` will be used (if it exists). /// /// Defaults to true. - pub fn external_template(&mut self, enabled: bool) - -> &mut RepositoryInitOptions { + pub fn external_template(&mut self, enabled: bool) -> &mut RepositoryInitOptions { self.flag(raw::GIT_REPOSITORY_INIT_EXTERNAL_TEMPLATE, enabled) } - fn flag(&mut self, flag: raw::git_repository_init_flag_t, on: bool) - -> &mut RepositoryInitOptions { + fn flag( + &mut self, + flag: raw::git_repository_init_flag_t, + on: bool, + ) -> &mut RepositoryInitOptions { if on { self.flags |= flag as u32; } else { @@ -1789,12 +3483,13 @@ impl RepositoryInitOptions { self } - /// The path do the working directory. + /// The path to the working directory. /// - /// If this is a relative path it will be evaulated relative to the repo + /// If this is a relative path it will be evaluated relative to the repo /// path. If this is not the "natural" working directory, a .git gitlink /// file will be created here linking to the repo path. pub fn workdir_path(&mut self, path: &Path) -> &mut RepositoryInitOptions { + // Normal file path OK (does not need Windows conversion). self.workdir_path = Some(path.into_c_string().unwrap()); self } @@ -1812,15 +3507,16 @@ impl RepositoryInitOptions { /// If this is not configured, then the default locations will be searched /// instead. pub fn template_path(&mut self, path: &Path) -> &mut RepositoryInitOptions { + // Normal file path OK (does not need Windows conversion). self.template_path = Some(path.into_c_string().unwrap()); self } /// The name of the head to point HEAD at. /// - /// If not configured, this will be treated as `master` and the HEAD ref - /// will be set to `refs/heads/master`. If this begins with `refs/` it will - /// be used verbatim; otherwise `refs/heads/` will be prefixed + /// If not configured, this will be taken from your git configuration. + /// If this begins with `refs/` it will be used verbatim; + /// otherwise `refs/heads/` will be prefixed pub fn initial_head(&mut self, head: &str) -> &mut RepositoryInitOptions { self.initial_head = Some(CString::new(head).unwrap()); self @@ -1840,31 +3536,39 @@ impl RepositoryInitOptions { /// interior of this structure. pub unsafe fn raw(&self) -> raw::git_repository_init_options { let mut opts = mem::zeroed(); - assert_eq!(raw::git_repository_init_init_options(&mut opts, - raw::GIT_REPOSITORY_INIT_OPTIONS_VERSION), 0); + assert_eq!( + raw::git_repository_init_init_options( + &mut opts, + raw::GIT_REPOSITORY_INIT_OPTIONS_VERSION + ), + 0 + ); opts.flags = self.flags; opts.mode = self.mode; - opts.workdir_path = ::call::convert(&self.workdir_path); - opts.description = ::call::convert(&self.description); - opts.template_path = ::call::convert(&self.template_path); - opts.initial_head = ::call::convert(&self.initial_head); - opts.origin_url = ::call::convert(&self.origin_url); - return opts; + opts.workdir_path = crate::call::convert(&self.workdir_path); + opts.description = crate::call::convert(&self.description); + opts.template_path = crate::call::convert(&self.template_path); + opts.initial_head = crate::call::convert(&self.initial_head); + opts.origin_url = crate::call::convert(&self.origin_url); + opts } } #[cfg(test)] mod tests { + use crate::build::CheckoutBuilder; + use crate::{CherrypickOptions, MergeFileOptions}; + use crate::{ + ObjectType, Oid, Repository, ResetType, Signature, SubmoduleIgnore, SubmoduleUpdate, + }; use std::ffi::OsStr; use std::fs; use std::path::Path; - use tempdir::TempDir; - use {Repository, Oid, ObjectType, ResetType}; - use build::CheckoutBuilder; + use tempfile::TempDir; #[test] fn smoke_init() { - let td = TempDir::new("test").unwrap(); + let td = TempDir::new().unwrap(); let path = td.path(); let repo = Repository::init(path).unwrap(); @@ -1873,7 +3577,7 @@ mod tests { #[test] fn smoke_init_bare() { - let td = TempDir::new("test").unwrap(); + let td = TempDir::new().unwrap(); let path = td.path(); let repo = Repository::init_bare(path).unwrap(); @@ -1883,39 +3587,43 @@ mod tests { #[test] fn smoke_open() { - let td = TempDir::new("test").unwrap(); + let td = TempDir::new().unwrap(); let path = td.path(); Repository::init(td.path()).unwrap(); let repo = Repository::open(path).unwrap(); assert!(!repo.is_bare()); assert!(!repo.is_shallow()); assert!(repo.is_empty().unwrap()); - assert_eq!(::test::realpath(&repo.path()).unwrap(), - ::test::realpath(&td.path().join(".git/")).unwrap()); - assert_eq!(repo.state(), ::RepositoryState::Clean); + assert_eq!( + crate::test::realpath(&repo.path()).unwrap(), + crate::test::realpath(&td.path().join(".git/")).unwrap() + ); + assert_eq!(repo.state(), crate::RepositoryState::Clean); } #[test] fn smoke_open_bare() { - let td = TempDir::new("test").unwrap(); + let td = TempDir::new().unwrap(); let path = td.path(); Repository::init_bare(td.path()).unwrap(); let repo = Repository::open(path).unwrap(); assert!(repo.is_bare()); - assert_eq!(::test::realpath(&repo.path()).unwrap(), - ::test::realpath(&td.path().join("")).unwrap()); + assert_eq!( + crate::test::realpath(&repo.path()).unwrap(), + crate::test::realpath(&td.path().join("")).unwrap() + ); } #[test] fn smoke_checkout() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); repo.checkout_head(None).unwrap(); } #[test] fn smoke_revparse() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let rev = repo.revparse("HEAD").unwrap(); assert!(rev.to().is_none()); let from = rev.from().unwrap(); @@ -1932,47 +3640,95 @@ mod tests { #[test] fn makes_dirs() { - let td = TempDir::new("foo").unwrap(); + let td = TempDir::new().unwrap(); Repository::init(&td.path().join("a/b/c/d")).unwrap(); } #[test] fn smoke_discover() { - let td = TempDir::new("test").unwrap(); + let td = TempDir::new().unwrap(); let subdir = td.path().join("subdi"); fs::create_dir(&subdir).unwrap(); Repository::init_bare(td.path()).unwrap(); let repo = Repository::discover(&subdir).unwrap(); - assert_eq!(::test::realpath(&repo.path()).unwrap(), - ::test::realpath(&td.path().join("")).unwrap()); + assert_eq!( + crate::test::realpath(&repo.path()).unwrap(), + crate::test::realpath(&td.path().join("")).unwrap() + ); + } + + #[test] + fn smoke_discover_path() { + let td = TempDir::new().unwrap(); + let subdir = td.path().join("subdi"); + fs::create_dir(&subdir).unwrap(); + Repository::init_bare(td.path()).unwrap(); + let path = Repository::discover_path(&subdir, &[] as &[&OsStr]).unwrap(); + assert_eq!( + crate::test::realpath(&path).unwrap(), + crate::test::realpath(&td.path().join("")).unwrap() + ); + } + + #[test] + fn smoke_discover_path_ceiling_dir() { + let td = TempDir::new().unwrap(); + let subdir = td.path().join("subdi"); + fs::create_dir(&subdir).unwrap(); + let ceilingdir = subdir.join("ceiling"); + fs::create_dir(&ceilingdir).unwrap(); + let testdir = ceilingdir.join("testdi"); + fs::create_dir(&testdir).unwrap(); + Repository::init_bare(td.path()).unwrap(); + let path = Repository::discover_path(&testdir, &[ceilingdir.as_os_str()]); + + assert!(path.is_err()); } #[test] fn smoke_open_ext() { - let td = TempDir::new("test").unwrap(); + let td = TempDir::new().unwrap(); let subdir = td.path().join("subdir"); fs::create_dir(&subdir).unwrap(); Repository::init(td.path()).unwrap(); - let repo = Repository::open_ext(&subdir, ::RepositoryOpenFlags::empty(), &[] as &[&OsStr]).unwrap(); + let repo = Repository::open_ext( + &subdir, + crate::RepositoryOpenFlags::empty(), + &[] as &[&OsStr], + ) + .unwrap(); assert!(!repo.is_bare()); - assert_eq!(::test::realpath(&repo.path()).unwrap(), - ::test::realpath(&td.path().join(".git")).unwrap()); - - let repo = Repository::open_ext(&subdir, ::REPOSITORY_OPEN_BARE, &[] as &[&OsStr]).unwrap(); + assert_eq!( + crate::test::realpath(&repo.path()).unwrap(), + crate::test::realpath(&td.path().join(".git")).unwrap() + ); + + let repo = + Repository::open_ext(&subdir, crate::RepositoryOpenFlags::BARE, &[] as &[&OsStr]) + .unwrap(); assert!(repo.is_bare()); - assert_eq!(::test::realpath(&repo.path()).unwrap(), - ::test::realpath(&td.path().join(".git")).unwrap()); - - let err = Repository::open_ext(&subdir, ::REPOSITORY_OPEN_NO_SEARCH, &[] as &[&OsStr]).err().unwrap(); - assert_eq!(err.code(), ::ErrorCode::NotFound); + assert_eq!( + crate::test::realpath(&repo.path()).unwrap(), + crate::test::realpath(&td.path().join(".git")).unwrap() + ); + + let err = Repository::open_ext( + &subdir, + crate::RepositoryOpenFlags::NO_SEARCH, + &[] as &[&OsStr], + ) + .err() + .unwrap(); + assert_eq!(err.code(), crate::ErrorCode::NotFound); - let err = Repository::open_ext(&subdir, ::RepositoryOpenFlags::empty(), &[&subdir]).err().unwrap(); - assert_eq!(err.code(), ::ErrorCode::NotFound); + assert!( + Repository::open_ext(&subdir, crate::RepositoryOpenFlags::empty(), &[&subdir]).is_ok() + ); } fn graph_repo_init() -> (TempDir, Repository) { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); { let head = repo.head().unwrap().target().unwrap(); let head = repo.find_commit(head).unwrap(); @@ -1982,8 +3738,8 @@ mod tests { let tree = repo.find_tree(id).unwrap(); let sig = repo.signature().unwrap(); - repo.commit(Some("HEAD"), &sig, &sig, "second", - &tree, &[&head]).unwrap(); + repo.commit(Some("HEAD"), &sig, &sig, "second", &tree, &[&head]) + .unwrap(); } (_td, repo) } @@ -1995,12 +3751,10 @@ mod tests { let head = repo.find_commit(head).unwrap(); let head_id = head.id(); let head_parent_id = head.parent(0).unwrap().id(); - let (ahead, behind) = repo.graph_ahead_behind(head_id, - head_parent_id).unwrap(); + let (ahead, behind) = repo.graph_ahead_behind(head_id, head_parent_id).unwrap(); assert_eq!(ahead, 1); assert_eq!(behind, 0); - let (ahead, behind) = repo.graph_ahead_behind(head_parent_id, - head_id).unwrap(); + let (ahead, behind) = repo.graph_ahead_behind(head_parent_id, head_id).unwrap(); assert_eq!(ahead, 0); assert_eq!(behind, 1); } @@ -2018,13 +3772,15 @@ mod tests { #[test] fn smoke_reference_has_log_ensure_log() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); assert_eq!(repo.reference_has_log("HEAD").unwrap(), true); - assert_eq!(repo.reference_has_log("refs/heads/master").unwrap(), true); + assert_eq!(repo.reference_has_log("refs/heads/main").unwrap(), true); assert_eq!(repo.reference_has_log("NOT_HEAD").unwrap(), false); - let master_oid = repo.revparse_single("master").unwrap().id(); - assert!(repo.reference("NOT_HEAD", master_oid, false, "creating a new branch").is_ok()); + let main_oid = repo.revparse_single("main").unwrap().id(); + assert!(repo + .reference("NOT_HEAD", main_oid, false, "creating a new branch") + .is_ok()); assert_eq!(repo.reference_has_log("NOT_HEAD").unwrap(), false); assert!(repo.reference_ensure_log("NOT_HEAD").is_ok()); assert_eq!(repo.reference_has_log("NOT_HEAD").unwrap(), true); @@ -2032,27 +3788,147 @@ mod tests { #[test] fn smoke_set_head() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); assert!(repo.set_head("refs/heads/does-not-exist").is_ok()); assert!(repo.head().is_err()); - assert!(repo.set_head("refs/heads/master").is_ok()); + assert!(repo.set_head("refs/heads/main").is_ok()); assert!(repo.head().is_ok()); assert!(repo.set_head("*").is_err()); } + #[test] + fn smoke_set_head_bytes() { + let (_td, repo) = crate::test::repo_init(); + + assert!(repo.set_head_bytes(b"refs/heads/does-not-exist").is_ok()); + assert!(repo.head().is_err()); + + assert!(repo.set_head_bytes(b"refs/heads/main").is_ok()); + assert!(repo.head().is_ok()); + + assert!(repo.set_head_bytes(b"*").is_err()); + } + #[test] fn smoke_set_head_detached() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let void_oid = Oid::from_bytes(b"00000000000000000000").unwrap(); assert!(repo.set_head_detached(void_oid).is_err()); - let master_oid = repo.revparse_single("master").unwrap().id(); - assert!(repo.set_head_detached(master_oid).is_ok()); - assert_eq!(repo.head().unwrap().target().unwrap(), master_oid); + let main_oid = repo.revparse_single("main").unwrap().id(); + assert!(repo.set_head_detached(main_oid).is_ok()); + assert_eq!(repo.head().unwrap().target().unwrap(), main_oid); + } + + #[test] + fn smoke_find_object_by_prefix() { + let (_td, repo) = crate::test::repo_init(); + let head = repo.head().unwrap().target().unwrap(); + let head = repo.find_commit(head).unwrap(); + let head_id = head.id(); + let head_prefix = &head_id.to_string()[..7]; + let obj = repo.find_object_by_prefix(head_prefix, None).unwrap(); + assert_eq!(obj.id(), head_id); + } + + /// create the following: + /// /---o4 + /// /---o3 + /// o1---o2 + #[test] + fn smoke_merge_base() { + let (_td, repo) = graph_repo_init(); + let sig = repo.signature().unwrap(); + + // let oid1 = head + let oid1 = repo.head().unwrap().target().unwrap(); + let commit1 = repo.find_commit(oid1).unwrap(); + println!("created oid1 {:?}", oid1); + + repo.branch("branch_a", &commit1, true).unwrap(); + repo.branch("branch_b", &commit1, true).unwrap(); + repo.branch("branch_c", &commit1, true).unwrap(); + + // create commit oid2 on branch_a + let mut index = repo.index().unwrap(); + let p = Path::new(repo.workdir().unwrap()).join("file_a"); + println!("using path {:?}", p); + fs::File::create(&p).unwrap(); + index.add_path(Path::new("file_a")).unwrap(); + let id_a = index.write_tree().unwrap(); + let tree_a = repo.find_tree(id_a).unwrap(); + let oid2 = repo + .commit( + Some("refs/heads/branch_a"), + &sig, + &sig, + "commit 2", + &tree_a, + &[&commit1], + ) + .unwrap(); + repo.find_commit(oid2).unwrap(); + println!("created oid2 {:?}", oid2); + + t!(repo.reset(commit1.as_object(), ResetType::Hard, None)); + + // create commit oid3 on branch_b + let mut index = repo.index().unwrap(); + let p = Path::new(repo.workdir().unwrap()).join("file_b"); + fs::File::create(&p).unwrap(); + index.add_path(Path::new("file_b")).unwrap(); + let id_b = index.write_tree().unwrap(); + let tree_b = repo.find_tree(id_b).unwrap(); + let oid3 = repo + .commit( + Some("refs/heads/branch_b"), + &sig, + &sig, + "commit 3", + &tree_b, + &[&commit1], + ) + .unwrap(); + repo.find_commit(oid3).unwrap(); + println!("created oid3 {:?}", oid3); + + t!(repo.reset(commit1.as_object(), ResetType::Hard, None)); + + // create commit oid4 on branch_c + let mut index = repo.index().unwrap(); + let p = Path::new(repo.workdir().unwrap()).join("file_c"); + fs::File::create(&p).unwrap(); + index.add_path(Path::new("file_c")).unwrap(); + let id_c = index.write_tree().unwrap(); + let tree_c = repo.find_tree(id_c).unwrap(); + let oid4 = repo + .commit( + Some("refs/heads/branch_c"), + &sig, + &sig, + "commit 3", + &tree_c, + &[&commit1], + ) + .unwrap(); + repo.find_commit(oid4).unwrap(); + println!("created oid4 {:?}", oid4); + + // the merge base of (oid2,oid3) should be oid1 + let merge_base = repo.merge_base(oid2, oid3).unwrap(); + assert_eq!(merge_base, oid1); + + // the merge base of (oid2,oid3,oid4) should be oid1 + let merge_base = repo.merge_base_many(&[oid2, oid3, oid4]).unwrap(); + assert_eq!(merge_base, oid1); + + // the octopus merge base of (oid2,oid3,oid4) should be oid1 + let merge_base = repo.merge_base_octopus(&[oid2, oid3, oid4]).unwrap(); + assert_eq!(merge_base, oid1); } /// create an octopus: @@ -2081,11 +3957,21 @@ mod tests { index.add_path(Path::new("file_a")).unwrap(); let id_a = index.write_tree().unwrap(); let tree_a = repo.find_tree(id_a).unwrap(); - let oid2 = repo.commit(Some("refs/heads/branch_a"), &sig, &sig, - "commit 2", &tree_a, &[&commit1]).unwrap(); + let oid2 = repo + .commit( + Some("refs/heads/branch_a"), + &sig, + &sig, + "commit 2", + &tree_a, + &[&commit1], + ) + .unwrap(); let commit2 = repo.find_commit(oid2).unwrap(); println!("created oid2 {:?}", oid2); + t!(repo.reset(commit1.as_object(), ResetType::Hard, None)); + // create commit oid3 on branchB let mut index = repo.index().unwrap(); let p = Path::new(repo.workdir().unwrap()).join("file_b"); @@ -2093,8 +3979,16 @@ mod tests { index.add_path(Path::new("file_b")).unwrap(); let id_b = index.write_tree().unwrap(); let tree_b = repo.find_tree(id_b).unwrap(); - let oid3 = repo.commit(Some("refs/heads/branch_b"), &sig, &sig, - "commit 3", &tree_b, &[&commit1]).unwrap(); + let oid3 = repo + .commit( + Some("refs/heads/branch_b"), + &sig, + &sig, + "commit 3", + &tree_b, + &[&commit1], + ) + .unwrap(); let commit3 = repo.find_commit(oid3).unwrap(); println!("created oid3 {:?}", oid3); @@ -2102,9 +3996,16 @@ mod tests { //let mut index4 = repo.merge_commits(&commit2, &commit3, None).unwrap(); repo.set_head("refs/heads/branch_a").unwrap(); repo.checkout_head(None).unwrap(); - let oid4 = repo.commit(Some("refs/heads/branch_a"), &sig, &sig, - "commit 4", &tree_a, - &[&commit2, &commit3]).unwrap(); + let oid4 = repo + .commit( + Some("refs/heads/branch_a"), + &sig, + &sig, + "commit 4", + &tree_a, + &[&commit2, &commit3], + ) + .unwrap(); //index4.write_tree_to(&repo).unwrap(); println!("created oid4 {:?}", oid4); @@ -2112,9 +4013,16 @@ mod tests { //let mut index5 = repo.merge_commits(&commit3, &commit2, None).unwrap(); repo.set_head("refs/heads/branch_b").unwrap(); repo.checkout_head(None).unwrap(); - let oid5 = repo.commit(Some("refs/heads/branch_b"), &sig, &sig, - "commit 5", &tree_a, - &[&commit3, &commit2]).unwrap(); + let oid5 = repo + .commit( + Some("refs/heads/branch_b"), + &sig, + &sig, + "commit 5", + &tree_a, + &[&commit3, &commit2], + ) + .unwrap(); //index5.write_tree_to(&repo).unwrap(); println!("created oid5 {:?}", oid5); @@ -2134,7 +4042,121 @@ mod tests { } assert!(found_oid2); assert!(found_oid3); - assert_eq!(merge_bases.len(), 2); + assert_eq!(merge_bases.len(), 2); + + // merge bases of (oid4,oid5) should be (oid2,oid3) + let merge_bases = repo.merge_bases_many(&[oid4, oid5]).unwrap(); + let mut found_oid2 = false; + let mut found_oid3 = false; + for mg in merge_bases.iter() { + println!("found merge base {:?}", mg); + if mg == &oid2 { + found_oid2 = true; + } else if mg == &oid3 { + found_oid3 = true; + } else { + assert!(false); + } + } + assert!(found_oid2); + assert!(found_oid3); + assert_eq!(merge_bases.len(), 2); + } + + #[test] + fn smoke_merge_file_from_index() { + let (_td, repo) = crate::test::repo_init(); + + let head_commit = { + let head = t!(repo.head()).target().unwrap(); + t!(repo.find_commit(head)) + }; + + let file_path = Path::new("file"); + let author = t!(Signature::now("committer", "committer@email")); + + let base_commit = { + t!(fs::write(repo.workdir().unwrap().join(&file_path), "base")); + let mut index = t!(repo.index()); + t!(index.add_path(&file_path)); + let tree_id = t!(index.write_tree()); + let tree = t!(repo.find_tree(tree_id)); + + let commit_id = t!(repo.commit( + Some("HEAD"), + &author, + &author, + r"Add file with contents 'base'", + &tree, + &[&head_commit], + )); + t!(repo.find_commit(commit_id)) + }; + + let foo_commit = { + t!(fs::write(repo.workdir().unwrap().join(&file_path), "foo")); + let mut index = t!(repo.index()); + t!(index.add_path(&file_path)); + let tree_id = t!(index.write_tree()); + let tree = t!(repo.find_tree(tree_id)); + + let commit_id = t!(repo.commit( + Some("refs/heads/foo"), + &author, + &author, + r"Update file with contents 'foo'", + &tree, + &[&base_commit], + )); + t!(repo.find_commit(commit_id)) + }; + + let bar_commit = { + t!(fs::write(repo.workdir().unwrap().join(&file_path), "bar")); + let mut index = t!(repo.index()); + t!(index.add_path(&file_path)); + let tree_id = t!(index.write_tree()); + let tree = t!(repo.find_tree(tree_id)); + + let commit_id = t!(repo.commit( + Some("refs/heads/bar"), + &author, + &author, + r"Update file with contents 'bar'", + &tree, + &[&base_commit], + )); + t!(repo.find_commit(commit_id)) + }; + + let index = t!(repo.merge_commits(&foo_commit, &bar_commit, None)); + + let base = index.get_path(file_path, 1).unwrap(); + let ours = index.get_path(file_path, 2).unwrap(); + let theirs = index.get_path(file_path, 3).unwrap(); + + let mut opts = MergeFileOptions::new(); + opts.ancestor_label("ancestor"); + opts.our_label("ours"); + opts.their_label("theirs"); + opts.style_diff3(true); + let merge_file_result = repo + .merge_file_from_index(&base, &ours, &theirs, Some(&mut opts)) + .unwrap(); + + assert!(!merge_file_result.is_automergeable()); + assert_eq!(merge_file_result.path(), Some("file")); + assert_eq!( + String::from_utf8_lossy(merge_file_result.content()).to_string(), + r"<<<<<<< ours +foo +||||||| ancestor +base +======= +bar +>>>>>>> theirs +", + ); } #[test] @@ -2142,8 +4164,8 @@ mod tests { let (_td, repo) = graph_repo_init(); { - let short_refname = "master"; - let expected_refname = "refs/heads/master"; + let short_refname = "main"; + let expected_refname = "refs/heads/main"; let (obj, reference) = repo.revparse_ext(short_refname).unwrap(); let expected_obj = repo.revparse_single(expected_refname).unwrap(); assert_eq!(obj.id(), expected_obj.id()); @@ -2158,4 +4180,358 @@ mod tests { assert!(reference.is_none()); } } + + #[test] + fn smoke_is_path_ignored() { + let (_td, repo) = graph_repo_init(); + + assert!(!repo.is_path_ignored(Path::new("foo")).unwrap()); + + let _ = repo.add_ignore_rule("/foo"); + assert!(repo.is_path_ignored(Path::new("foo")).unwrap()); + if cfg!(windows) { + assert!(repo.is_path_ignored(Path::new("foo\\thing")).unwrap()); + } + + let _ = repo.clear_ignore_rules(); + assert!(!repo.is_path_ignored(Path::new("foo")).unwrap()); + if cfg!(windows) { + assert!(!repo.is_path_ignored(Path::new("foo\\thing")).unwrap()); + } + } + + #[test] + fn smoke_cherrypick() { + let (_td, repo) = crate::test::repo_init(); + let sig = repo.signature().unwrap(); + + let oid1 = repo.head().unwrap().target().unwrap(); + let commit1 = repo.find_commit(oid1).unwrap(); + + repo.branch("branch_a", &commit1, true).unwrap(); + + // Add 2 commits on top of the initial one in branch_a + let mut index = repo.index().unwrap(); + let p1 = Path::new(repo.workdir().unwrap()).join("file_c"); + fs::File::create(&p1).unwrap(); + index.add_path(Path::new("file_c")).unwrap(); + let id = index.write_tree().unwrap(); + let tree_c = repo.find_tree(id).unwrap(); + let oid2 = repo + .commit( + Some("refs/heads/branch_a"), + &sig, + &sig, + "commit 2", + &tree_c, + &[&commit1], + ) + .unwrap(); + let commit2 = repo.find_commit(oid2).unwrap(); + println!("created oid2 {:?}", oid2); + assert!(p1.exists()); + + let mut index = repo.index().unwrap(); + let p2 = Path::new(repo.workdir().unwrap()).join("file_d"); + fs::File::create(&p2).unwrap(); + index.add_path(Path::new("file_d")).unwrap(); + let id = index.write_tree().unwrap(); + let tree_d = repo.find_tree(id).unwrap(); + let oid3 = repo + .commit( + Some("refs/heads/branch_a"), + &sig, + &sig, + "commit 3", + &tree_d, + &[&commit2], + ) + .unwrap(); + let commit3 = repo.find_commit(oid3).unwrap(); + println!("created oid3 {:?}", oid3); + assert!(p1.exists()); + assert!(p2.exists()); + + // cherry-pick commit3 on top of commit1 in branch b + repo.reset(commit1.as_object(), ResetType::Hard, None) + .unwrap(); + let mut cherrypick_opts = CherrypickOptions::new(); + repo.cherrypick(&commit3, Some(&mut cherrypick_opts)) + .unwrap(); + let id = repo.index().unwrap().write_tree().unwrap(); + let tree_d = repo.find_tree(id).unwrap(); + let oid4 = repo + .commit(Some("HEAD"), &sig, &sig, "commit 4", &tree_d, &[&commit1]) + .unwrap(); + let commit4 = repo.find_commit(oid4).unwrap(); + // should have file from commit3, but not the file from commit2 + assert_eq!(commit4.parent(0).unwrap().id(), commit1.id()); + assert!(!p1.exists()); + assert!(p2.exists()); + } + + #[test] + fn smoke_revert() { + let (_td, repo) = crate::test::repo_init(); + let foo_file = Path::new(repo.workdir().unwrap()).join("foo"); + assert!(!foo_file.exists()); + + let (oid1, _id) = crate::test::commit(&repo); + let commit1 = repo.find_commit(oid1).unwrap(); + t!(repo.reset(commit1.as_object(), ResetType::Hard, None)); + assert!(foo_file.exists()); + + repo.revert(&commit1, None).unwrap(); + let id = repo.index().unwrap().write_tree().unwrap(); + let tree2 = repo.find_tree(id).unwrap(); + let sig = repo.signature().unwrap(); + repo.commit(Some("HEAD"), &sig, &sig, "commit 1", &tree2, &[&commit1]) + .unwrap(); + // reverting once removes `foo` file + assert!(!foo_file.exists()); + + let oid2 = repo.head().unwrap().target().unwrap(); + let commit2 = repo.find_commit(oid2).unwrap(); + repo.revert(&commit2, None).unwrap(); + let id = repo.index().unwrap().write_tree().unwrap(); + let tree3 = repo.find_tree(id).unwrap(); + repo.commit(Some("HEAD"), &sig, &sig, "commit 2", &tree3, &[&commit2]) + .unwrap(); + // reverting twice restores `foo` file + assert!(foo_file.exists()); + } + + #[test] + fn smoke_config_write_and_read() { + let (td, repo) = crate::test::repo_init(); + + let mut config = repo.config().unwrap(); + + config.set_bool("commit.gpgsign", false).unwrap(); + + let c = fs::read_to_string(td.path().join(".git").join("config")).unwrap(); + + assert!(c.contains("[commit]")); + assert!(c.contains("gpgsign = false")); + + let config = repo.config().unwrap(); + + assert!(!config.get_bool("commit.gpgsign").unwrap()); + } + + #[test] + fn smoke_merge_analysis_for_ref() -> Result<(), crate::Error> { + let (_td, repo) = graph_repo_init(); + + // Set up this repo state: + // * second (their-branch) + // * initial (HEAD -> main) + // + // We expect that their-branch can be fast-forward merged into main. + + // git checkout --detach HEAD + let head_commit = repo.head()?.peel_to_commit()?; + repo.set_head_detached(head_commit.id())?; + + // git branch their-branch HEAD + let their_branch = repo.branch("their-branch", &head_commit, false)?; + + // git branch -f main HEAD~ + let mut parents_iter = head_commit.parents(); + let parent = parents_iter.next().unwrap(); + assert!(parents_iter.next().is_none()); + + let main = repo.branch("main", &parent, true)?; + + // git checkout main + repo.set_head(main.get().name().expect("should be utf-8"))?; + + let (merge_analysis, _merge_preference) = repo.merge_analysis_for_ref( + main.get(), + &[&repo.reference_to_annotated_commit(their_branch.get())?], + )?; + + assert!(merge_analysis.contains(crate::MergeAnalysis::ANALYSIS_FASTFORWARD)); + + Ok(()) + } + + #[test] + fn smoke_submodule_set() -> Result<(), crate::Error> { + let (td1, _repo) = crate::test::repo_init(); + let (td2, mut repo2) = crate::test::repo_init(); + let url = crate::test::path2url(https://github.com/td1.path()); + let name = "bar"; + { + let mut s = repo2.submodule(&url, Path::new(name), true)?; + fs::remove_dir_all(td2.path().join("bar")).unwrap(); + Repository::clone(&url, td2.path().join("bar"))?; + s.add_to_index(false)?; + s.add_finalize()?; + } + + // update strategy + repo2.submodule_set_update(name, SubmoduleUpdate::None)?; + assert!(matches!( + repo2.find_submodule(name)?.update_strategy(), + SubmoduleUpdate::None + )); + repo2.submodule_set_update(name, SubmoduleUpdate::Rebase)?; + assert!(matches!( + repo2.find_submodule(name)?.update_strategy(), + SubmoduleUpdate::Rebase + )); + + // ignore rule + repo2.submodule_set_ignore(name, SubmoduleIgnore::Untracked)?; + assert!(matches!( + repo2.find_submodule(name)?.ignore_rule(), + SubmoduleIgnore::Untracked + )); + repo2.submodule_set_ignore(name, SubmoduleIgnore::Dirty)?; + assert!(matches!( + repo2.find_submodule(name)?.ignore_rule(), + SubmoduleIgnore::Dirty + )); + + // url + repo2.submodule_set_url(https://github.com/name,%20%22fake-url")?; + assert_eq!(repo2.find_submodule(name)?.url(), Some("fake-url")); + + // branch + repo2.submodule_set_branch(name, "fake-branch")?; + assert_eq!(repo2.find_submodule(name)?.branch(), Some("fake-branch")); + + Ok(()) + } + + #[test] + fn smoke_mailmap_from_repository() { + let (_td, repo) = crate::test::repo_init(); + + let commit = { + let head = t!(repo.head()).target().unwrap(); + t!(repo.find_commit(head)) + }; + + // This is our baseline for HEAD. + let author = commit.author(); + let committer = commit.committer(); + assert_eq!(author.name(), Some("name")); + assert_eq!(author.email(), Some("email")); + assert_eq!(committer.name(), Some("name")); + assert_eq!(committer.email(), Some("email")); + + // There is no .mailmap file in the test repo so all signature identities are equal. + let mailmap = t!(repo.mailmap()); + let mailmapped_author = t!(commit.author_with_mailmap(&mailmap)); + let mailmapped_committer = t!(commit.committer_with_mailmap(&mailmap)); + assert_eq!(mailmapped_author.name(), author.name()); + assert_eq!(mailmapped_author.email(), author.email()); + assert_eq!(mailmapped_committer.name(), committer.name()); + assert_eq!(mailmapped_committer.email(), committer.email()); + + let commit = { + // - Add a .mailmap file to the repository. + // - Commit with a signature identity different from the author's. + // - Include entries for both author and committer to prove we call + // the right raw functions. + let mailmap_file = Path::new(".mailmap"); + let p = Path::new(repo.workdir().unwrap()).join(&mailmap_file); + t!(fs::write( + p, + r#" +Author Name name +Committer Name "#, + )); + let mut index = t!(repo.index()); + t!(index.add_path(&mailmap_file)); + let id_mailmap = t!(index.write_tree()); + let tree_mailmap = t!(repo.find_tree(id_mailmap)); + + let head = t!(repo.commit( + Some("HEAD"), + &author, + t!(&Signature::now("committer", "committer@email")), + "Add mailmap", + &tree_mailmap, + &[&commit], + )); + t!(repo.find_commit(head)) + }; + + // Sanity check that we're working with the right commit and that its + // author and committer identities differ. + let author = commit.author(); + let committer = commit.committer(); + assert_ne!(author.name(), committer.name()); + assert_ne!(author.email(), committer.email()); + assert_eq!(author.name(), Some("name")); + assert_eq!(author.email(), Some("email")); + assert_eq!(committer.name(), Some("committer")); + assert_eq!(committer.email(), Some("committer@email")); + + // Fetch the newly added .mailmap from the repository. + let mailmap = t!(repo.mailmap()); + let mailmapped_author = t!(commit.author_with_mailmap(&mailmap)); + let mailmapped_committer = t!(commit.committer_with_mailmap(&mailmap)); + + let mm_resolve_author = t!(mailmap.resolve_signature(&author)); + let mm_resolve_committer = t!(mailmap.resolve_signature(&committer)); + + // Mailmap Signature lifetime is independent of Commit lifetime. + drop(author); + drop(committer); + drop(commit); + + // author_with_mailmap() + committer_with_mailmap() work + assert_eq!(mailmapped_author.name(), Some("Author Name")); + assert_eq!(mailmapped_author.email(), Some("author.proper@email")); + assert_eq!(mailmapped_committer.name(), Some("Committer Name")); + assert_eq!(mailmapped_committer.email(), Some("committer.proper@email")); + + // resolve_signature() works + assert_eq!(mm_resolve_author.email(), mailmapped_author.email()); + assert_eq!(mm_resolve_committer.email(), mailmapped_committer.email()); + } + + #[test] + fn smoke_find_tag_by_prefix() { + let (_td, repo) = crate::test::repo_init(); + let head = repo.head().unwrap(); + let tag_oid = repo + .tag( + "tag", + &repo + .find_object(head.peel_to_commit().unwrap().id(), None) + .unwrap(), + &repo.signature().unwrap(), + "message", + false, + ) + .unwrap(); + let tag = repo.find_tag(tag_oid).unwrap(); + let found_tag = repo + .find_tag_by_prefix(&tag.id().to_string()[0..7]) + .unwrap(); + assert_eq!(tag.id(), found_tag.id()); + } + + #[test] + fn smoke_commondir() { + let (td, repo) = crate::test::repo_init(); + assert_eq!( + crate::test::realpath(repo.path()).unwrap(), + crate::test::realpath(repo.commondir()).unwrap() + ); + + let worktree = repo + .worktree("test", &td.path().join("worktree"), None) + .unwrap(); + let worktree_repo = Repository::open_from_worktree(&worktree).unwrap(); + assert_eq!( + crate::test::realpath(repo.path()).unwrap(), + crate::test::realpath(worktree_repo.commondir()).unwrap() + ); + } } diff --git a/src/revert.rs b/src/revert.rs new file mode 100644 index 0000000000..55d702600e --- /dev/null +++ b/src/revert.rs @@ -0,0 +1,69 @@ +use std::mem; + +use crate::build::CheckoutBuilder; +use crate::merge::MergeOptions; +use crate::raw; +use std::ptr; + +/// Options to specify when reverting +pub struct RevertOptions<'cb> { + mainline: u32, + checkout_builder: Option>, + merge_opts: Option, +} + +impl<'cb> RevertOptions<'cb> { + /// Creates a default set of revert options + pub fn new() -> RevertOptions<'cb> { + RevertOptions { + mainline: 0, + checkout_builder: None, + merge_opts: None, + } + } + + /// Set the mainline value + /// + /// For merge commits, the "mainline" is treated as the parent. + pub fn mainline(&mut self, mainline: u32) -> &mut Self { + self.mainline = mainline; + self + } + + /// Set the checkout builder + pub fn checkout_builder(&mut self, cb: CheckoutBuilder<'cb>) -> &mut Self { + self.checkout_builder = Some(cb); + self + } + + /// Set the merge options + pub fn merge_opts(&mut self, merge_opts: MergeOptions) -> &mut Self { + self.merge_opts = Some(merge_opts); + self + } + + /// Obtain the raw struct + pub fn raw(&mut self) -> raw::git_revert_options { + unsafe { + let mut checkout_opts: raw::git_checkout_options = mem::zeroed(); + raw::git_checkout_init_options(&mut checkout_opts, raw::GIT_CHECKOUT_OPTIONS_VERSION); + if let Some(ref mut cb) = self.checkout_builder { + cb.configure(&mut checkout_opts); + } + + let mut merge_opts: raw::git_merge_options = mem::zeroed(); + raw::git_merge_init_options(&mut merge_opts, raw::GIT_MERGE_OPTIONS_VERSION); + if let Some(ref opts) = self.merge_opts { + ptr::copy(opts.raw(), &mut merge_opts, 1); + } + + let mut revert_opts: raw::git_revert_options = mem::zeroed(); + raw::git_revert_options_init(&mut revert_opts, raw::GIT_REVERT_OPTIONS_VERSION); + revert_opts.mainline = self.mainline; + revert_opts.checkout_opts = checkout_opts; + revert_opts.merge_opts = merge_opts; + + revert_opts + } + } +} diff --git a/src/revspec.rs b/src/revspec.rs index eb18492efe..d2e08670af 100644 --- a/src/revspec.rs +++ b/src/revspec.rs @@ -1,4 +1,4 @@ -use {Object, RevparseMode}; +use crate::{Object, RevparseMode}; /// A revspec represents a range of revisions within a repository. pub struct Revspec<'repo> { @@ -9,18 +9,26 @@ pub struct Revspec<'repo> { impl<'repo> Revspec<'repo> { /// Assembles a new revspec from the from/to components. - pub fn from_objects(from: Option>, - to: Option>, - mode: RevparseMode) -> Revspec<'repo> { - Revspec { from: from, to: to, mode: mode } + pub fn from_objects( + from: Option>, + to: Option>, + mode: RevparseMode, + ) -> Revspec<'repo> { + Revspec { from, to, mode } } /// Access the `from` range of this revspec. - pub fn from(&self) -> Option<&Object<'repo>> { self.from.as_ref() } + pub fn from(&self) -> Option<&Object<'repo>> { + self.from.as_ref() + } /// Access the `to` range of this revspec. - pub fn to(&self) -> Option<&Object<'repo>> { self.to.as_ref() } + pub fn to(&self) -> Option<&Object<'repo>> { + self.to.as_ref() + } /// Returns the intent of the revspec. - pub fn mode(&self) -> RevparseMode { self.mode } + pub fn mode(&self) -> RevparseMode { + self.mode + } } diff --git a/src/revwalk.rs b/src/revwalk.rs index 1e661db526..7837f00d63 100644 --- a/src/revwalk.rs +++ b/src/revwalk.rs @@ -1,9 +1,9 @@ -use std::marker; +use libc::{c_int, c_uint, c_void}; use std::ffi::CString; -use libc::c_uint; +use std::marker; -use {raw, Error, Sort, Oid, Repository}; -use util::Binding; +use crate::util::Binding; +use crate::{panic, raw, Error, Oid, Repository, Sort}; /// A revwalk allows traversal of the commit graph defined by including one or /// more leaves and excluding one or more roots. @@ -12,32 +12,76 @@ pub struct Revwalk<'repo> { _marker: marker::PhantomData<&'repo Repository>, } +/// A `Revwalk` with an associated "hide callback", see `with_hide_callback` +pub struct RevwalkWithHideCb<'repo, 'cb, C> +where + C: FnMut(Oid) -> bool, +{ + revwalk: Revwalk<'repo>, + _marker: marker::PhantomData<&'cb C>, +} + +extern "C" fn revwalk_hide_cb(commit_id: *const raw::git_oid, payload: *mut c_void) -> c_int +where + C: FnMut(Oid) -> bool, +{ + panic::wrap(|| unsafe { + let hide_cb = payload as *mut C; + if (*hide_cb)(Oid::from_raw(commit_id)) { + 1 + } else { + 0 + } + }) + .unwrap_or(-1) +} + +impl<'repo, 'cb, C: FnMut(Oid) -> bool> RevwalkWithHideCb<'repo, 'cb, C> { + /// Consumes the `RevwalkWithHideCb` and returns the contained `Revwalk`. + /// + /// Note that this will reset the `Revwalk`. + pub fn into_inner(mut self) -> Result, Error> { + self.revwalk.reset()?; + Ok(self.revwalk) + } +} + impl<'repo> Revwalk<'repo> { /// Reset a revwalk to allow re-configuring it. /// /// The revwalk is automatically reset when iteration of its commits /// completes. - pub fn reset(&mut self) { - unsafe { raw::git_revwalk_reset(self.raw()) } + pub fn reset(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_revwalk_reset(self.raw())); + } + Ok(()) } /// Set the order in which commits are visited. - pub fn set_sorting(&mut self, sort_mode: Sort) { + pub fn set_sorting(&mut self, sort_mode: Sort) -> Result<(), Error> { unsafe { - raw::git_revwalk_sorting(self.raw(), sort_mode.bits() as c_uint) + try_call!(raw::git_revwalk_sorting( + self.raw(), + sort_mode.bits() as c_uint + )); } + Ok(()) } /// Simplify the history by first-parent /// /// No parents other than the first for each commit will be enqueued. - pub fn simplify_first_parent(&mut self) { - unsafe { raw::git_revwalk_simplify_first_parent(self.raw) } + pub fn simplify_first_parent(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_revwalk_simplify_first_parent(self.raw)); + } + Ok(()) } /// Mark a commit to start traversal from. /// - /// The given OID must belong to a committish on the walked repository. + /// The given OID must belong to a commitish on the walked repository. /// /// The given commit will be used as one of the roots when starting the /// revision walk. At least one commit must be pushed onto the walker before @@ -67,10 +111,10 @@ impl<'repo> Revwalk<'repo> { /// A leading 'refs/' is implied if not present as well as a trailing `/ \ /// *` if the glob lacks '?', ' \ *' or '['. /// - /// Any references matching this glob which do not point to a committish + /// Any references matching this glob which do not point to a commitish /// will be ignored. pub fn push_glob(&mut self, glob: &str) -> Result<(), Error> { - let glob = try!(CString::new(glob)); + let glob = CString::new(glob)?; unsafe { try_call!(raw::git_revwalk_push_glob(self.raw, glob)); } @@ -83,7 +127,7 @@ impl<'repo> Revwalk<'repo> { /// `` is in the form accepted by `revparse_single`. The left-hand /// commit will be hidden and the right-hand commit pushed. pub fn push_range(&mut self, range: &str) -> Result<(), Error> { - let range = try!(CString::new(range)); + let range = CString::new(range)?; unsafe { try_call!(raw::git_revwalk_push_range(self.raw, range)); } @@ -92,9 +136,9 @@ impl<'repo> Revwalk<'repo> { /// Push the OID pointed to by a reference /// - /// The reference must point to a committish. + /// The reference must point to a commitish. pub fn push_ref(&mut self, reference: &str) -> Result<(), Error> { - let reference = try!(CString::new(reference)); + let reference = CString::new(reference)?; unsafe { try_call!(raw::git_revwalk_push_ref(self.raw, reference)); } @@ -109,6 +153,29 @@ impl<'repo> Revwalk<'repo> { Ok(()) } + /// Hide all commits for which the callback returns true from + /// the walk. + pub fn with_hide_callback<'cb, C>( + self, + callback: &'cb mut C, + ) -> Result, Error> + where + C: FnMut(Oid) -> bool, + { + let r = RevwalkWithHideCb { + revwalk: self, + _marker: marker::PhantomData, + }; + unsafe { + raw::git_revwalk_add_hide_cb( + r.revwalk.raw(), + Some(revwalk_hide_cb::), + callback as *mut _ as *mut c_void, + ); + }; + Ok(r) + } + /// Hide the repository's HEAD /// /// For more information, see `hide`. @@ -127,10 +194,10 @@ impl<'repo> Revwalk<'repo> { /// A leading 'refs/' is implied if not present as well as a trailing `/ \ /// *` if the glob lacks '?', ' \ *' or '['. /// - /// Any references matching this glob which do not point to a committish + /// Any references matching this glob which do not point to a commitish /// will be ignored. pub fn hide_glob(&mut self, glob: &str) -> Result<(), Error> { - let glob = try!(CString::new(glob)); + let glob = CString::new(glob)?; unsafe { try_call!(raw::git_revwalk_hide_glob(self.raw, glob)); } @@ -139,9 +206,9 @@ impl<'repo> Revwalk<'repo> { /// Hide the OID pointed to by a reference. /// - /// The reference must point to a committish. + /// The reference must point to a commitish. pub fn hide_ref(&mut self, reference: &str) -> Result<(), Error> { - let reference = try!(CString::new(reference)); + let reference = CString::new(reference)?; unsafe { try_call!(raw::git_revwalk_hide_ref(self.raw, reference)); } @@ -152,9 +219,14 @@ impl<'repo> Revwalk<'repo> { impl<'repo> Binding for Revwalk<'repo> { type Raw = *mut raw::git_revwalk; unsafe fn from_raw(raw: *mut raw::git_revwalk) -> Revwalk<'repo> { - Revwalk { raw: raw, _marker: marker::PhantomData } + Revwalk { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_revwalk { + self.raw } - fn raw(&self) -> *mut raw::git_revwalk { self.raw } } impl<'repo> Drop for Revwalk<'repo> { @@ -166,7 +238,9 @@ impl<'repo> Drop for Revwalk<'repo> { impl<'repo> Iterator for Revwalk<'repo> { type Item = Result; fn next(&mut self) -> Option> { - let mut out: raw::git_oid = raw::git_oid{ id: [0; raw::GIT_OID_RAWSZ] }; + let mut out: raw::git_oid = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; unsafe { try_call_iter!(raw::git_revwalk_next(&mut out, self.raw())); Some(Ok(Binding::from_raw(&out as *const _))) @@ -174,30 +248,69 @@ impl<'repo> Iterator for Revwalk<'repo> { } } +impl<'repo, 'cb, C: FnMut(Oid) -> bool> Iterator for RevwalkWithHideCb<'repo, 'cb, C> { + type Item = Result; + fn next(&mut self) -> Option> { + let out = self.revwalk.next(); + crate::panic::check(); + out + } +} + #[cfg(test)] mod tests { #[test] fn smoke() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let head = repo.head().unwrap(); let target = head.target().unwrap(); let mut walk = repo.revwalk().unwrap(); walk.push(target).unwrap(); - let oids: Vec<::Oid> = walk.by_ref().collect::, _>>() - .unwrap(); + let oids: Vec = walk.by_ref().collect::, _>>().unwrap(); assert_eq!(oids.len(), 1); assert_eq!(oids[0], target); - walk.reset(); + walk.reset().unwrap(); walk.push_head().unwrap(); assert_eq!(walk.by_ref().count(), 1); - walk.reset(); + walk.reset().unwrap(); walk.push_head().unwrap(); walk.hide_head().unwrap(); assert_eq!(walk.by_ref().count(), 0); } + + #[test] + fn smoke_hide_cb() { + let (_td, repo) = crate::test::repo_init(); + let head = repo.head().unwrap(); + let target = head.target().unwrap(); + + let mut walk = repo.revwalk().unwrap(); + walk.push(target).unwrap(); + + let oids: Vec = walk.by_ref().collect::, _>>().unwrap(); + + assert_eq!(oids.len(), 1); + assert_eq!(oids[0], target); + + walk.reset().unwrap(); + walk.push_head().unwrap(); + assert_eq!(walk.by_ref().count(), 1); + + walk.reset().unwrap(); + walk.push_head().unwrap(); + + let mut hide_cb = |oid| oid == target; + let mut walk = walk.with_hide_callback(&mut hide_cb).unwrap(); + + assert_eq!(walk.by_ref().count(), 0); + + let mut walk = walk.into_inner().unwrap(); + walk.push_head().unwrap(); + assert_eq!(walk.by_ref().count(), 1); + } } diff --git a/src/signature.rs b/src/signature.rs index 5951960b40..7c9ffb3933 100644 --- a/src/signature.rs +++ b/src/signature.rs @@ -1,12 +1,12 @@ use std::ffi::CString; +use std::fmt; use std::marker; use std::mem; +use std::ptr; use std::str; -use std::fmt; -use libc; -use {raw, Error, Time}; -use util::Binding; +use crate::util::Binding; +use crate::{raw, Error, Time}; /// A Signature is used to indicate authorship of various actions throughout the /// library. @@ -28,10 +28,10 @@ impl<'a> Signature<'a> { /// /// See `new` for more information pub fn now(name: &str, email: &str) -> Result, Error> { - ::init(); - let mut ret = 0 as *mut raw::git_signature; - let name = try!(CString::new(name)); - let email = try!(CString::new(email)); + crate::init(); + let mut ret = ptr::null_mut(); + let name = CString::new(name)?; + let email = CString::new(email)?; unsafe { try_call!(raw::git_signature_now(&mut ret, name, email)); Ok(Binding::from_raw(ret)) @@ -44,16 +44,19 @@ impl<'a> Signature<'a> { /// the time zone offset in minutes. /// /// Returns error if either `name` or `email` contain angle brackets. - pub fn new(name: &str, email: &str, time: &Time) - -> Result, Error> { - ::init(); - let mut ret = 0 as *mut raw::git_signature; - let name = try!(CString::new(name)); - let email = try!(CString::new(email)); + pub fn new(name: &str, email: &str, time: &Time) -> Result, Error> { + crate::init(); + let mut ret = ptr::null_mut(); + let name = CString::new(name)?; + let email = CString::new(email)?; unsafe { - try_call!(raw::git_signature_new(&mut ret, name, email, - time.seconds() as raw::git_time_t, - time.offset_minutes() as libc::c_int)); + try_call!(raw::git_signature_new( + &mut ret, + name, + email, + time.seconds() as raw::git_time_t, + time.offset_minutes() as libc::c_int + )); Ok(Binding::from_raw(ret)) } } @@ -67,7 +70,7 @@ impl<'a> Signature<'a> { /// Gets the name on the signature as a byte slice. pub fn name_bytes(&self) -> &[u8] { - unsafe { ::opt_bytes(self, (*self.raw).name).unwrap() } + unsafe { crate::opt_bytes(self, (*self.raw).name).unwrap() } } /// Gets the email on the signature. @@ -79,7 +82,7 @@ impl<'a> Signature<'a> { /// Gets the email on the signature as a byte slice. pub fn email_bytes(&self) -> &[u8] { - unsafe { ::opt_bytes(self, (*self.raw).email).unwrap() } + unsafe { crate::opt_bytes(self, (*self.raw).email).unwrap() } } /// Get the `when` of this signature. @@ -101,12 +104,14 @@ impl<'a> Binding for Signature<'a> { type Raw = *mut raw::git_signature; unsafe fn from_raw(raw: *mut raw::git_signature) -> Signature<'a> { Signature { - raw: raw, + raw, _marker: marker::PhantomData, owned: true, } } - fn raw(&self) -> *mut raw::git_signature { self.raw } + fn raw(&self) -> *mut raw::git_signature { + self.raw + } } /// Creates a new signature from the give raw pointer, tied to the lifetime @@ -114,9 +119,7 @@ impl<'a> Binding for Signature<'a> { /// /// This function is unsafe as there is no guarantee that `raw` is valid for /// `'a` nor if it's a valid pointer. -pub unsafe fn from_raw_const<'b, T>(_lt: &'b T, - raw: *const raw::git_signature) - -> Signature<'b> { +pub unsafe fn from_raw_const<'b, T>(_lt: &'b T, raw: *const raw::git_signature) -> Signature<'b> { Signature { raw: raw as *mut raw::git_signature, _marker: marker::PhantomData, @@ -128,7 +131,7 @@ impl Clone for Signature<'static> { fn clone(&self) -> Signature<'static> { // TODO: can this be defined for 'a and just do a plain old copy if the // lifetime isn't static? - let mut raw = 0 as *mut raw::git_signature; + let mut raw = ptr::null_mut(); let rc = unsafe { raw::git_signature_dup(&mut raw, &*self.raw) }; assert_eq!(rc, 0); unsafe { Binding::from_raw(raw) } @@ -144,18 +147,29 @@ impl<'a> Drop for Signature<'a> { } impl<'a> fmt::Display for Signature<'a> { - - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} <{}>", - String::from_utf8_lossy(self.name_bytes()), - String::from_utf8_lossy(self.email_bytes())) + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} <{}>", + String::from_utf8_lossy(self.name_bytes()), + String::from_utf8_lossy(self.email_bytes()) + ) } +} +impl PartialEq for Signature<'_> { + fn eq(&self, other: &Self) -> bool { + self.when() == other.when() + && self.email_bytes() == other.email_bytes() + && self.name_bytes() == other.name_bytes() + } } +impl Eq for Signature<'_> {} + #[cfg(test)] mod tests { - use {Signature, Time}; + use crate::{Signature, Time}; #[test] fn smoke() { diff --git a/src/stash.rs b/src/stash.rs new file mode 100644 index 0000000000..ea898e46ba --- /dev/null +++ b/src/stash.rs @@ -0,0 +1,348 @@ +use crate::build::CheckoutBuilder; +use crate::util::{self, Binding}; +use crate::{panic, raw, IntoCString, Oid, Signature, StashApplyProgress, StashFlags}; +use libc::{c_char, c_int, c_void, size_t}; +use std::ffi::{c_uint, CStr, CString}; +use std::mem; + +/// Stash application options structure +pub struct StashSaveOptions<'a> { + message: Option, + flags: Option, + stasher: Signature<'a>, + pathspec: Vec, + pathspec_ptrs: Vec<*const c_char>, + raw_opts: raw::git_stash_save_options, +} + +impl<'a> StashSaveOptions<'a> { + /// Creates a default + pub fn new(stasher: Signature<'a>) -> Self { + let mut opts = Self { + message: None, + flags: None, + stasher, + pathspec: Vec::new(), + pathspec_ptrs: Vec::new(), + raw_opts: unsafe { mem::zeroed() }, + }; + assert_eq!( + unsafe { + raw::git_stash_save_options_init( + &mut opts.raw_opts, + raw::GIT_STASH_SAVE_OPTIONS_VERSION, + ) + }, + 0 + ); + opts + } + + /// Customize optional `flags` field + pub fn flags(&mut self, flags: Option) -> &mut Self { + self.flags = flags; + self + } + + /// Add to the array of paths patterns to build the stash. + pub fn pathspec(&mut self, pathspec: T) -> &mut Self { + let s = util::cstring_to_repo_path(pathspec).unwrap(); + self.pathspec_ptrs.push(s.as_ptr()); + self.pathspec.push(s); + self + } + + /// Acquire a pointer to the underlying raw options. + /// + /// This function is unsafe as the pointer is only valid so long as this + /// structure is not moved, modified, or used elsewhere. + pub unsafe fn raw(&mut self) -> *const raw::git_stash_save_options { + self.raw_opts.flags = self.flags.unwrap_or_else(StashFlags::empty).bits() as c_uint; + self.raw_opts.message = crate::call::convert(&self.message); + self.raw_opts.paths.count = self.pathspec_ptrs.len() as size_t; + self.raw_opts.paths.strings = self.pathspec_ptrs.as_ptr() as *mut _; + self.raw_opts.stasher = self.stasher.raw(); + + &self.raw_opts as *const _ + } +} + +/// Stash application progress notification function. +/// +/// Return `true` to continue processing, or `false` to +/// abort the stash application. +// FIXME: This probably should have been pub(crate) since it is not used anywhere. +pub type StashApplyProgressCb<'a> = dyn FnMut(StashApplyProgress) -> bool + 'a; + +/// This is a callback function you can provide to iterate over all the +/// stashed states that will be invoked per entry. +// FIXME: This probably should have been pub(crate) since it is not used anywhere. +pub type StashCb<'a> = dyn FnMut(usize, &str, &Oid) -> bool + 'a; + +/// Stash application options structure +pub struct StashApplyOptions<'cb> { + progress: Option>>, + checkout_options: Option>, + raw_opts: raw::git_stash_apply_options, +} + +impl<'cb> Default for StashApplyOptions<'cb> { + fn default() -> Self { + Self::new() + } +} + +impl<'cb> StashApplyOptions<'cb> { + /// Creates a default set of merge options. + pub fn new() -> StashApplyOptions<'cb> { + let mut opts = StashApplyOptions { + progress: None, + checkout_options: None, + raw_opts: unsafe { mem::zeroed() }, + }; + assert_eq!( + unsafe { raw::git_stash_apply_init_options(&mut opts.raw_opts, 1) }, + 0 + ); + opts + } + + /// Set stash application flag to GIT_STASH_APPLY_REINSTATE_INDEX + pub fn reinstantiate_index(&mut self) -> &mut StashApplyOptions<'cb> { + self.raw_opts.flags = raw::GIT_STASH_APPLY_REINSTATE_INDEX as u32; + self + } + + /// Options to use when writing files to the working directory + pub fn checkout_options(&mut self, opts: CheckoutBuilder<'cb>) -> &mut StashApplyOptions<'cb> { + self.checkout_options = Some(opts); + self + } + + /// Optional callback to notify the consumer of application progress. + /// + /// Return `true` to continue processing, or `false` to + /// abort the stash application. + pub fn progress_cb(&mut self, callback: C) -> &mut StashApplyOptions<'cb> + where + C: FnMut(StashApplyProgress) -> bool + 'cb, + { + self.progress = Some(Box::new(callback) as Box>); + self.raw_opts.progress_cb = Some(stash_apply_progress_cb); + self.raw_opts.progress_payload = self as *mut _ as *mut _; + self + } + + /// Pointer to a raw git_stash_apply_options + pub fn raw(&mut self) -> &raw::git_stash_apply_options { + unsafe { + if let Some(opts) = self.checkout_options.as_mut() { + opts.configure(&mut self.raw_opts.checkout_options); + } + } + &self.raw_opts + } +} + +pub(crate) struct StashCbData<'a> { + pub callback: &'a mut StashCb<'a>, +} + +pub(crate) extern "C" fn stash_cb( + index: size_t, + message: *const c_char, + stash_id: *const raw::git_oid, + payload: *mut c_void, +) -> c_int { + panic::wrap(|| unsafe { + let data = &mut *(payload as *mut StashCbData<'_>); + let res = { + let callback = &mut data.callback; + callback( + index, + CStr::from_ptr(message).to_str().unwrap(), + &Binding::from_raw(stash_id), + ) + }; + + if res { + 0 + } else { + 1 + } + }) + .unwrap_or(1) +} + +fn convert_progress(progress: raw::git_stash_apply_progress_t) -> StashApplyProgress { + match progress { + raw::GIT_STASH_APPLY_PROGRESS_NONE => StashApplyProgress::None, + raw::GIT_STASH_APPLY_PROGRESS_LOADING_STASH => StashApplyProgress::LoadingStash, + raw::GIT_STASH_APPLY_PROGRESS_ANALYZE_INDEX => StashApplyProgress::AnalyzeIndex, + raw::GIT_STASH_APPLY_PROGRESS_ANALYZE_MODIFIED => StashApplyProgress::AnalyzeModified, + raw::GIT_STASH_APPLY_PROGRESS_ANALYZE_UNTRACKED => StashApplyProgress::AnalyzeUntracked, + raw::GIT_STASH_APPLY_PROGRESS_CHECKOUT_UNTRACKED => StashApplyProgress::CheckoutUntracked, + raw::GIT_STASH_APPLY_PROGRESS_CHECKOUT_MODIFIED => StashApplyProgress::CheckoutModified, + raw::GIT_STASH_APPLY_PROGRESS_DONE => StashApplyProgress::Done, + + _ => StashApplyProgress::None, + } +} + +extern "C" fn stash_apply_progress_cb( + progress: raw::git_stash_apply_progress_t, + payload: *mut c_void, +) -> c_int { + panic::wrap(|| unsafe { + let options = &mut *(payload as *mut StashApplyOptions<'_>); + let res = { + let callback = options.progress.as_mut().unwrap(); + callback(convert_progress(progress)) + }; + + if res { + 0 + } else { + -1 + } + }) + .unwrap_or(-1) +} + +#[cfg(test)] +mod tests { + use crate::stash::{StashApplyOptions, StashSaveOptions}; + use crate::test::repo_init; + use crate::{IndexAddOption, Repository, StashFlags, Status}; + use std::fs; + use std::path::{Path, PathBuf}; + + fn make_stash(next: C) + where + C: FnOnce(&mut Repository), + { + let (_td, mut repo) = repo_init(); + let signature = repo.signature().unwrap(); + + let p = Path::new(repo.workdir().unwrap()).join("file_b.txt"); + println!("using path {:?}", p); + + fs::write(&p, "data".as_bytes()).unwrap(); + + let rel_p = Path::new("file_b.txt"); + assert!(repo.status_file(&rel_p).unwrap() == Status::WT_NEW); + + repo.stash_save(&signature, "msg1", Some(StashFlags::INCLUDE_UNTRACKED)) + .unwrap(); + + assert!(repo.status_file(&rel_p).is_err()); + + let mut count = 0; + repo.stash_foreach(|index, name, _oid| { + count += 1; + assert!(index == 0); + assert!(name == "On main: msg1"); + true + }) + .unwrap(); + + assert!(count == 1); + next(&mut repo); + } + + fn count_stash(repo: &mut Repository) -> usize { + let mut count = 0; + repo.stash_foreach(|_, _, _| { + count += 1; + true + }) + .unwrap(); + count + } + + #[test] + fn smoke_stash_save_drop() { + make_stash(|repo| { + repo.stash_drop(0).unwrap(); + assert!(count_stash(repo) == 0) + }) + } + + #[test] + fn smoke_stash_save_pop() { + make_stash(|repo| { + repo.stash_pop(0, None).unwrap(); + assert!(count_stash(repo) == 0) + }) + } + + #[test] + fn smoke_stash_save_apply() { + make_stash(|repo| { + let mut options = StashApplyOptions::new(); + options.progress_cb(|progress| { + println!("{:?}", progress); + true + }); + + repo.stash_apply(0, Some(&mut options)).unwrap(); + assert!(count_stash(repo) == 1) + }) + } + + #[test] + fn test_stash_save2_msg_none() { + let (_td, mut repo) = repo_init(); + let signature = repo.signature().unwrap(); + + let p = Path::new(repo.workdir().unwrap()).join("file_b.txt"); + + fs::write(&p, "data".as_bytes()).unwrap(); + + repo.stash_save2(&signature, None, Some(StashFlags::INCLUDE_UNTRACKED)) + .unwrap(); + + let mut stash_name = String::new(); + repo.stash_foreach(|index, name, _oid| { + assert_eq!(index, 0); + stash_name = name.to_string(); + true + }) + .unwrap(); + + assert!(stash_name.starts_with("WIP on main:")); + } + + fn create_file(r: &Repository, name: &str, data: &str) -> PathBuf { + let p = Path::new(r.workdir().unwrap()).join(name); + fs::write(&p, data).unwrap(); + p + } + + #[test] + fn test_stash_save_ext() { + let (_td, mut repo) = repo_init(); + let signature = repo.signature().unwrap(); + + create_file(&repo, "file_a", "foo"); + create_file(&repo, "file_b", "foo"); + + let mut index = repo.index().unwrap(); + index + .add_all(["*"].iter(), IndexAddOption::DEFAULT, None) + .unwrap(); + index.write().unwrap(); + + assert_eq!(repo.statuses(None).unwrap().len(), 2); + + let mut opt = StashSaveOptions::new(signature); + opt.pathspec("file_a"); + repo.stash_save_ext(Some(&mut opt)).unwrap(); + + assert_eq!(repo.statuses(None).unwrap().len(), 0); + + repo.stash_pop(0, None).unwrap(); + + assert_eq!(repo.statuses(None).unwrap().len(), 1); + } +} diff --git a/src/status.rs b/src/status.rs index 40e47b72d5..a5a8cffd39 100644 --- a/src/status.rs +++ b/src/status.rs @@ -1,12 +1,13 @@ +use libc::{c_char, c_uint, size_t}; use std::ffi::CString; -use std::ops::Range; +use std::iter::FusedIterator; use std::marker; use std::mem; +use std::ops::Range; use std::str; -use libc::{c_char, size_t, c_uint}; -use {raw, Status, DiffDelta, IntoCString, Repository}; -use util::Binding; +use crate::util::{self, Binding}; +use crate::{raw, DiffDelta, IntoCString, Repository, Status}; /// Options that can be provided to `repo.statuses()` to control how the status /// information is gathered. @@ -35,8 +36,8 @@ pub enum StatusShow { /// A container for a list of status information about a repository. /// -/// Each instances appears as a if it were a collection, having a length and -/// allowing indexing as well as provding an iterator. +/// Each instance appears as if it were a collection, having a length and +/// allowing indexing, as well as providing an iterator. pub struct Statuses<'repo> { raw: *mut raw::git_status_list, @@ -58,16 +59,21 @@ pub struct StatusEntry<'statuses> { _marker: marker::PhantomData<&'statuses DiffDelta<'statuses>>, } +impl Default for StatusOptions { + fn default() -> Self { + Self::new() + } +} + impl StatusOptions { /// Creates a new blank set of status options. pub fn new() -> StatusOptions { unsafe { let mut raw = mem::zeroed(); - let r = raw::git_status_init_options(&mut raw, - raw::GIT_STATUS_OPTIONS_VERSION); + let r = raw::git_status_init_options(&mut raw, raw::GIT_STATUS_OPTIONS_VERSION); assert_eq!(r, 0); StatusOptions { - raw: raw, + raw, pathspec: Vec::new(), ptrs: Vec::new(), } @@ -92,16 +98,14 @@ impl StatusOptions { /// If the `disable_pathspec_match` option is given, then this is a literal /// path to match. If this is not called, then there will be no patterns to /// match and the entire directory will be used. - pub fn pathspec(&mut self, pathspec: T) - -> &mut StatusOptions { - let s = pathspec.into_c_string().unwrap(); + pub fn pathspec(&mut self, pathspec: T) -> &mut StatusOptions { + let s = util::cstring_to_repo_path(pathspec).unwrap(); self.ptrs.push(s.as_ptr()); self.pathspec.push(s); self } - fn flag(&mut self, flag: raw::git_status_opt_t, val: bool) - -> &mut StatusOptions { + fn flag(&mut self, flag: raw::git_status_opt_t, val: bool) -> &mut StatusOptions { if val { self.raw.flags |= flag as c_uint; } else { @@ -143,55 +147,47 @@ impl StatusOptions { /// /// Normally if an entire directory is new then just the top-level directory /// is included (with a trailing slash on the entry name). - pub fn recurse_untracked_dirs(&mut self, include: bool) - -> &mut StatusOptions { + pub fn recurse_untracked_dirs(&mut self, include: bool) -> &mut StatusOptions { self.flag(raw::GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS, include) } /// Indicates that the given paths should be treated as literals paths, note /// patterns. - pub fn disable_pathspec_match(&mut self, include: bool) - -> &mut StatusOptions { + pub fn disable_pathspec_match(&mut self, include: bool) -> &mut StatusOptions { self.flag(raw::GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH, include) } /// Indicates that the contents of ignored directories should be included in /// the status. - pub fn recurse_ignored_dirs(&mut self, include: bool) - -> &mut StatusOptions { + pub fn recurse_ignored_dirs(&mut self, include: bool) -> &mut StatusOptions { self.flag(raw::GIT_STATUS_OPT_RECURSE_IGNORED_DIRS, include) } /// Indicates that rename detection should be processed between the head. - pub fn renames_head_to_index(&mut self, include: bool) - -> &mut StatusOptions { + pub fn renames_head_to_index(&mut self, include: bool) -> &mut StatusOptions { self.flag(raw::GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX, include) } /// Indicates that rename detection should be run between the index and the /// working directory. - pub fn renames_index_to_workdir(&mut self, include: bool) - -> &mut StatusOptions { + pub fn renames_index_to_workdir(&mut self, include: bool) -> &mut StatusOptions { self.flag(raw::GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR, include) } /// Override the native case sensitivity for the file system and force the /// output to be in case sensitive order. - pub fn sort_case_sensitively(&mut self, include: bool) - -> &mut StatusOptions { + pub fn sort_case_sensitively(&mut self, include: bool) -> &mut StatusOptions { self.flag(raw::GIT_STATUS_OPT_SORT_CASE_SENSITIVELY, include) } /// Override the native case sensitivity for the file system and force the /// output to be in case-insensitive order. - pub fn sort_case_insensitively(&mut self, include: bool) - -> &mut StatusOptions { + pub fn sort_case_insensitively(&mut self, include: bool) -> &mut StatusOptions { self.flag(raw::GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY, include) } /// Indicates that rename detection should include rewritten files. - pub fn renames_from_rewrites(&mut self, include: bool) - -> &mut StatusOptions { + pub fn renames_from_rewrites(&mut self, include: bool) -> &mut StatusOptions { self.flag(raw::GIT_STATUS_OPT_RENAMES_FROM_REWRITES, include) } @@ -217,11 +213,18 @@ impl StatusOptions { // erm... #[allow(missing_docs)] - pub fn include_unreadable_as_untracked(&mut self, include: bool) - -> &mut StatusOptions { + pub fn include_unreadable_as_untracked(&mut self, include: bool) -> &mut StatusOptions { self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNREADABLE_AS_UNTRACKED, include) } + /// Set threshold above which similar files will be considered renames. + /// + /// This is equivalent to the `-M` option. Defaults to 50. + pub fn rename_threshold(&mut self, threshold: u16) -> &mut StatusOptions { + self.raw.rename_threshold = threshold; + self + } + /// Get a pointer to the inner list of status options. /// /// This function is unsafe as the returned structure has interior pointers @@ -237,7 +240,7 @@ impl<'repo> Statuses<'repo> { /// Gets a status entry from this list at the specified index. /// /// Returns `None` if the index is out of bounds. - pub fn get(&self, index: usize) -> Option { + pub fn get(&self, index: usize) -> Option> { unsafe { let p = raw::git_status_byindex(self.raw, index as size_t); Binding::from_raw_opt(p) @@ -246,14 +249,19 @@ impl<'repo> Statuses<'repo> { /// Gets the count of status entries in this list. /// - /// If there are no changes in status (at least according the options given - /// when the status list was created), this can return 0. + /// If there are no changes in status (according to the options given + /// when the status list was created), this should return 0. pub fn len(&self) -> usize { unsafe { raw::git_status_list_entrycount(self.raw) as usize } } + /// Return `true` if there is no status entry in this list. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Returns an iterator over the statuses in this list. - pub fn iter(&self) -> StatusIter { + pub fn iter(&self) -> StatusIter<'_> { StatusIter { statuses: self, range: 0..self.len(), @@ -264,14 +272,21 @@ impl<'repo> Statuses<'repo> { impl<'repo> Binding for Statuses<'repo> { type Raw = *mut raw::git_status_list; unsafe fn from_raw(raw: *mut raw::git_status_list) -> Statuses<'repo> { - Statuses { raw: raw, _marker: marker::PhantomData } + Statuses { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_status_list { + self.raw } - fn raw(&self) -> *mut raw::git_status_list { self.raw } } impl<'repo> Drop for Statuses<'repo> { fn drop(&mut self) { - unsafe { raw::git_status_list_free(self.raw); } + unsafe { + raw::git_status_list_free(self.raw); + } } } @@ -280,31 +295,45 @@ impl<'a> Iterator for StatusIter<'a> { fn next(&mut self) -> Option> { self.range.next().and_then(|i| self.statuses.get(i)) } - fn size_hint(&self) -> (usize, Option) { self.range.size_hint() } + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() + } } impl<'a> DoubleEndedIterator for StatusIter<'a> { fn next_back(&mut self) -> Option> { self.range.next_back().and_then(|i| self.statuses.get(i)) } } +impl<'a> FusedIterator for StatusIter<'a> {} impl<'a> ExactSizeIterator for StatusIter<'a> {} +impl<'a> IntoIterator for &'a Statuses<'a> { + type Item = StatusEntry<'a>; + type IntoIter = StatusIter<'a>; + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + impl<'statuses> StatusEntry<'statuses> { /// Access the bytes for this entry's corresponding pathname pub fn path_bytes(&self) -> &[u8] { unsafe { if (*self.raw).head_to_index.is_null() { - ::opt_bytes(self, (*(*self.raw).index_to_workdir).old_file.path) + crate::opt_bytes(self, (*(*self.raw).index_to_workdir).old_file.path) } else { - ::opt_bytes(self, (*(*self.raw).head_to_index).old_file.path) - }.unwrap() + crate::opt_bytes(self, (*(*self.raw).head_to_index).old_file.path) + } + .unwrap() } } /// Access this entry's path name as a string. /// /// Returns `None` if the path is not valid utf-8. - pub fn path(&self) -> Option<&str> { str::from_utf8(self.path_bytes()).ok() } + pub fn path(&self) -> Option<&str> { + str::from_utf8(self.path_bytes()).ok() + } /// Access the status flags for this file pub fn status(&self) -> Status { @@ -314,48 +343,48 @@ impl<'statuses> StatusEntry<'statuses> { /// Access detailed information about the differences between the file in /// HEAD and the file in the index. pub fn head_to_index(&self) -> Option> { - unsafe { - Binding::from_raw_opt((*self.raw).head_to_index) - } + unsafe { Binding::from_raw_opt((*self.raw).head_to_index) } } /// Access detailed information about the differences between the file in /// the index and the file in the working directory. pub fn index_to_workdir(&self) -> Option> { - unsafe { - Binding::from_raw_opt((*self.raw).index_to_workdir) - } + unsafe { Binding::from_raw_opt((*self.raw).index_to_workdir) } } } impl<'statuses> Binding for StatusEntry<'statuses> { type Raw = *const raw::git_status_entry; - unsafe fn from_raw(raw: *const raw::git_status_entry) - -> StatusEntry<'statuses> { - StatusEntry { raw: raw, _marker: marker::PhantomData } + unsafe fn from_raw(raw: *const raw::git_status_entry) -> StatusEntry<'statuses> { + StatusEntry { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *const raw::git_status_entry { + self.raw } - fn raw(&self) -> *const raw::git_status_entry { self.raw } } #[cfg(test)] mod tests { + use super::StatusOptions; use std::fs::File; - use std::path::Path; use std::io::prelude::*; - use super::StatusOptions; + use std::path::Path; #[test] fn smoke() { - let (td, repo) = ::test::repo_init(); + let (td, repo) = crate::test::repo_init(); assert_eq!(repo.statuses(None).unwrap().len(), 0); File::create(&td.path().join("foo")).unwrap(); let statuses = repo.statuses(None).unwrap(); assert_eq!(statuses.iter().count(), 1); let status = statuses.iter().next().unwrap(); assert_eq!(status.path(), Some("foo")); - assert!(status.status().contains(::STATUS_WT_NEW)); - assert!(!status.status().contains(::STATUS_INDEX_NEW)); + assert!(status.status().contains(crate::Status::WT_NEW)); + assert!(!status.status().contains(crate::Status::INDEX_NEW)); assert!(status.head_to_index().is_none()); let diff = status.index_to_workdir().unwrap(); assert_eq!(diff.old_file().path_bytes().unwrap(), b"foo"); @@ -364,12 +393,11 @@ mod tests { #[test] fn filter() { - let (td, repo) = ::test::repo_init(); + let (td, repo) = crate::test::repo_init(); t!(File::create(&td.path().join("foo"))); t!(File::create(&td.path().join("bar"))); let mut opts = StatusOptions::new(); - opts.include_untracked(true) - .pathspec("foo"); + opts.include_untracked(true).pathspec("foo"); let statuses = t!(repo.statuses(Some(&mut opts))); assert_eq!(statuses.iter().count(), 1); @@ -379,7 +407,7 @@ mod tests { #[test] fn gitignore() { - let (td, repo) = ::test::repo_init(); + let (td, repo) = crate::test::repo_init(); t!(t!(File::create(td.path().join(".gitignore"))).write_all(b"foo\n")); assert!(!t!(repo.status_should_ignore(Path::new("bar")))); assert!(t!(repo.status_should_ignore(Path::new("foo")))); @@ -387,10 +415,21 @@ mod tests { #[test] fn status_file() { - let (td, repo) = ::test::repo_init(); + let (td, repo) = crate::test::repo_init(); assert!(repo.status_file(Path::new("foo")).is_err()); + if cfg!(windows) { + assert!(repo.status_file(Path::new("bar\\foo.txt")).is_err()); + } t!(File::create(td.path().join("foo"))); + if cfg!(windows) { + t!(::std::fs::create_dir_all(td.path().join("bar"))); + t!(File::create(td.path().join("bar").join("foo.txt"))); + } let status = t!(repo.status_file(Path::new("foo"))); - assert!(status.contains(::STATUS_WT_NEW)); + assert!(status.contains(crate::Status::WT_NEW)); + if cfg!(windows) { + let status = t!(repo.status_file(Path::new("bar\\foo.txt"))); + assert!(status.contains(crate::Status::WT_NEW)); + } } } diff --git a/src/string_array.rs b/src/string_array.rs index 60a209a0c7..c77ccdab96 100644 --- a/src/string_array.rs +++ b/src/string_array.rs @@ -1,14 +1,15 @@ -//! Bindings to libgit2's raw git_strarray type +//! Bindings to libgit2's raw `git_strarray` type -use std::str; +use std::iter::FusedIterator; use std::ops::Range; +use std::str; -use raw; -use util::Binding; +use crate::raw; +use crate::util::Binding; /// A string array structure used by libgit2 /// -/// Some apis return arrays of strings which originate from libgit2. This +/// Some APIs return arrays of strings which originate from libgit2. This /// wrapper type behaves a little like `Vec<&str>` but does so without copying /// the underlying strings until necessary. pub struct StringArray { @@ -37,8 +38,8 @@ impl StringArray { pub fn get_bytes(&self, i: usize) -> Option<&[u8]> { if i < self.raw.count as usize { unsafe { - let ptr = *self.raw.strings.offset(i as isize) as *const _; - Some(::opt_bytes(self, ptr).unwrap()) + let ptr = *self.raw.strings.add(i) as *const _; + Some(crate::opt_bytes(self, ptr).unwrap()) } } else { None @@ -49,26 +50,49 @@ impl StringArray { /// /// The iterator yields `Option<&str>` as it is unknown whether the contents /// are utf-8 or not. - pub fn iter(&self) -> Iter { - Iter { range: 0..self.len(), arr: self } + pub fn iter(&self) -> Iter<'_> { + Iter { + range: 0..self.len(), + arr: self, + } } /// Returns an iterator over the strings contained within this array, /// yielding byte slices. - pub fn iter_bytes(&self) -> IterBytes { - IterBytes { range: 0..self.len(), arr: self } + pub fn iter_bytes(&self) -> IterBytes<'_> { + IterBytes { + range: 0..self.len(), + arr: self, + } } /// Returns the number of strings in this array. - pub fn len(&self) -> usize { self.raw.count as usize } + pub fn len(&self) -> usize { + self.raw.count as usize + } + + /// Return `true` if this array is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } } impl Binding for StringArray { type Raw = raw::git_strarray; unsafe fn from_raw(raw: raw::git_strarray) -> StringArray { - StringArray { raw: raw } + StringArray { raw } + } + fn raw(&self) -> raw::git_strarray { + self.raw + } +} + +impl<'a> IntoIterator for &'a StringArray { + type Item = Option<&'a str>; + type IntoIter = Iter<'a>; + fn into_iter(self) -> Self::IntoIter { + self.iter() } - fn raw(&self) -> raw::git_strarray { self.raw } } impl<'a> Iterator for Iter<'a> { @@ -76,13 +100,16 @@ impl<'a> Iterator for Iter<'a> { fn next(&mut self) -> Option> { self.range.next().map(|i| self.arr.get(i)) } - fn size_hint(&self) -> (usize, Option) { self.range.size_hint() } + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() + } } impl<'a> DoubleEndedIterator for Iter<'a> { fn next_back(&mut self) -> Option> { self.range.next_back().map(|i| self.arr.get(i)) } } +impl<'a> FusedIterator for Iter<'a> {} impl<'a> ExactSizeIterator for Iter<'a> {} impl<'a> Iterator for IterBytes<'a> { @@ -90,13 +117,16 @@ impl<'a> Iterator for IterBytes<'a> { fn next(&mut self) -> Option<&'a [u8]> { self.range.next().and_then(|i| self.arr.get_bytes(i)) } - fn size_hint(&self) -> (usize, Option) { self.range.size_hint() } + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() + } } impl<'a> DoubleEndedIterator for IterBytes<'a> { fn next_back(&mut self) -> Option<&'a [u8]> { self.range.next_back().and_then(|i| self.arr.get_bytes(i)) } } +impl<'a> FusedIterator for IterBytes<'a> {} impl<'a> ExactSizeIterator for IterBytes<'a> {} impl Drop for StringArray { diff --git a/src/submodule.rs b/src/submodule.rs index 32d141e5f5..06a6359400 100644 --- a/src/submodule.rs +++ b/src/submodule.rs @@ -1,9 +1,13 @@ use std::marker; -use std::str; +use std::mem; +use std::os::raw::c_int; use std::path::Path; +use std::ptr; +use std::str; -use {raw, Oid, Repository, Error}; -use util::{self, Binding}; +use crate::util::{self, Binding}; +use crate::{build::CheckoutBuilder, SubmoduleIgnore, SubmoduleUpdate}; +use crate::{raw, Error, FetchOptions, Oid, Repository}; /// A structure to represent a git [submodule][1] /// @@ -26,54 +30,78 @@ impl<'repo> Submodule<'repo> { /// /// Returns `None` if the branch is not yet available. pub fn branch_bytes(&self) -> Option<&[u8]> { + unsafe { crate::opt_bytes(self, raw::git_submodule_branch(self.raw)) } + } + + /// Perform the clone step for a newly created submodule. + /// + /// This performs the necessary `git_clone` to setup a newly-created submodule. + pub fn clone( + &mut self, + opts: Option<&mut SubmoduleUpdateOptions<'_>>, + ) -> Result { unsafe { - ::opt_bytes(self, raw::git_submodule_branch(self.raw)) + let raw_opts = opts.map(|o| o.raw()); + let mut raw_repo = ptr::null_mut(); + try_call!(raw::git_submodule_clone( + &mut raw_repo, + self.raw, + raw_opts.as_ref() + )); + Ok(Binding::from_raw(raw_repo)) } } - /// Get the submodule's url. + /// Get the submodule's URL. /// - /// Returns `None` if the url is not valid utf-8 - pub fn url(https://github.com/&self) -> Option<&str> { str::from_utf8(self.url_bytes()).ok() } + /// Returns `None` if the URL is not valid utf-8 or if the URL isn't present + pub fn url(https://github.com/&self) -> Option<&str> { + self.opt_url_bytes().and_then(|b| str::from_utf8(b).ok()) + } - /// Get the url for the submodule. + /// Get the URL for the submodule. + #[doc(hidden)] + #[deprecated(note = "renamed to `opt_url_bytes`")] pub fn url_bytes(&self) -> &[u8] { - unsafe { - ::opt_bytes(self, raw::git_submodule_url(https://github.com/self.raw)).unwrap() - } + self.opt_url_bytes().unwrap() + } + + /// Get the URL for the submodule. + /// + /// Returns `None` if the URL isn't present + // TODO: delete this method and fix the signature of `url_bytes` on next + // major version bump + pub fn opt_url_bytes(&self) -> Option<&[u8]> { + unsafe { crate::opt_bytes(self, raw::git_submodule_url(https://github.com/self.raw)) } } /// Get the submodule's name. /// /// Returns `None` if the name is not valid utf-8 - pub fn name(&self) -> Option<&str> { str::from_utf8(self.name_bytes()).ok() } + pub fn name(&self) -> Option<&str> { + str::from_utf8(self.name_bytes()).ok() + } /// Get the name for the submodule. pub fn name_bytes(&self) -> &[u8] { - unsafe { - ::opt_bytes(self, raw::git_submodule_name(self.raw)).unwrap() - } + unsafe { crate::opt_bytes(self, raw::git_submodule_name(self.raw)).unwrap() } } /// Get the path for the submodule. pub fn path(&self) -> &Path { util::bytes2path(unsafe { - ::opt_bytes(self, raw::git_submodule_path(self.raw)).unwrap() + crate::opt_bytes(self, raw::git_submodule_path(self.raw)).unwrap() }) } /// Get the OID for the submodule in the current HEAD tree. pub fn head_id(&self) -> Option { - unsafe { - Binding::from_raw_opt(raw::git_submodule_head_id(self.raw)) - } + unsafe { Binding::from_raw_opt(raw::git_submodule_head_id(self.raw)) } } /// Get the OID for the submodule in the index. pub fn index_id(&self) -> Option { - unsafe { - Binding::from_raw_opt(raw::git_submodule_index_id(self.raw)) - } + unsafe { Binding::from_raw_opt(raw::git_submodule_index_id(self.raw)) } } /// Get the OID for the submodule in the current working directory. @@ -82,9 +110,17 @@ impl<'repo> Submodule<'repo> { /// checked out submodule. If there are pending changes in the index or /// anything else, this won't notice that. pub fn workdir_id(&self) -> Option { - unsafe { - Binding::from_raw_opt(raw::git_submodule_wd_id(self.raw)) - } + unsafe { Binding::from_raw_opt(raw::git_submodule_wd_id(self.raw)) } + } + + /// Get the ignore rule that will be used for the submodule. + pub fn ignore_rule(&self) -> SubmoduleIgnore { + SubmoduleIgnore::from_raw(unsafe { raw::git_submodule_ignore(self.raw) }) + } + + /// Get the update rule that will be used for the submodule. + pub fn update_strategy(&self) -> SubmoduleUpdate { + SubmoduleUpdate::from_raw(unsafe { raw::git_submodule_update_strategy(self.raw) }) } /// Copy submodule info into ".git/config" file. @@ -103,12 +139,31 @@ impl<'repo> Submodule<'repo> { Ok(()) } + /// Set up the subrepository for a submodule in preparation for clone. + /// + /// This function can be called to init and set up a submodule repository + /// from a submodule in preparation to clone it from its remote. + + /// use_gitlink: Should the workdir contain a gitlink to the repo in + /// .git/modules vs. repo directly in workdir. + pub fn repo_init(&mut self, use_gitlink: bool) -> Result { + unsafe { + let mut raw_repo = ptr::null_mut(); + try_call!(raw::git_submodule_repo_init( + &mut raw_repo, + self.raw, + use_gitlink + )); + Ok(Binding::from_raw(raw_repo)) + } + } + /// Open the repository for a submodule. /// /// This will only work if the submodule is checked out into the working /// directory. pub fn open(&self) -> Result { - let mut raw = 0 as *mut raw::git_repository; + let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_submodule_open(&mut raw, self.raw)); Ok(Binding::from_raw(raw)) @@ -136,7 +191,9 @@ impl<'repo> Submodule<'repo> { /// if you have altered the URL for the submodule (or it has been altered /// by a fetch of upstream changes) and you need to update your local repo. pub fn sync(&mut self) -> Result<(), Error> { - unsafe { try_call!(raw::git_submodule_sync(self.raw)); } + unsafe { + try_call!(raw::git_submodule_sync(self.raw)); + } Ok(()) } @@ -159,7 +216,34 @@ impl<'repo> Submodule<'repo> { /// newly cloned submodule to the index to be ready to be committed (but /// doesn't actually do the commit). pub fn add_finalize(&mut self) -> Result<(), Error> { - unsafe { try_call!(raw::git_submodule_add_finalize(self.raw)); } + unsafe { + try_call!(raw::git_submodule_add_finalize(self.raw)); + } + Ok(()) + } + + /// Update submodule. + /// + /// This will clone a missing submodule and check out the subrepository to + /// the commit specified in the index of the containing repository. If + /// the submodule repository doesn't contain the target commit, then the + /// submodule is fetched using the fetch options supplied in `opts`. + /// + /// `init` indicates if the submodule should be initialized first if it has + /// not been initialized yet. + pub fn update( + &mut self, + init: bool, + opts: Option<&mut SubmoduleUpdateOptions<'_>>, + ) -> Result<(), Error> { + unsafe { + let mut raw_opts = opts.map(|o| o.raw()); + try_call!(raw::git_submodule_update( + self.raw, + init as c_int, + raw_opts.as_mut().map_or(ptr::null_mut(), |o| o) + )); + } Ok(()) } } @@ -167,9 +251,14 @@ impl<'repo> Submodule<'repo> { impl<'repo> Binding for Submodule<'repo> { type Raw = *mut raw::git_submodule; unsafe fn from_raw(raw: *mut raw::git_submodule) -> Submodule<'repo> { - Submodule { raw: raw, _marker: marker::PhantomData } + Submodule { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_submodule { + self.raw } - fn raw(&self) -> *mut raw::git_submodule { self.raw } } impl<'repo> Drop for Submodule<'repo> { @@ -178,26 +267,87 @@ impl<'repo> Drop for Submodule<'repo> { } } +/// Options to update a submodule. +pub struct SubmoduleUpdateOptions<'cb> { + checkout_builder: CheckoutBuilder<'cb>, + fetch_opts: FetchOptions<'cb>, + allow_fetch: bool, +} + +impl<'cb> SubmoduleUpdateOptions<'cb> { + /// Return default options. + pub fn new() -> Self { + SubmoduleUpdateOptions { + checkout_builder: CheckoutBuilder::new(), + fetch_opts: FetchOptions::new(), + allow_fetch: true, + } + } + + unsafe fn raw(&mut self) -> raw::git_submodule_update_options { + let mut checkout_opts: raw::git_checkout_options = mem::zeroed(); + let init_res = + raw::git_checkout_init_options(&mut checkout_opts, raw::GIT_CHECKOUT_OPTIONS_VERSION); + assert_eq!(0, init_res); + self.checkout_builder.configure(&mut checkout_opts); + let opts = raw::git_submodule_update_options { + version: raw::GIT_SUBMODULE_UPDATE_OPTIONS_VERSION, + checkout_opts, + fetch_opts: self.fetch_opts.raw(), + allow_fetch: self.allow_fetch as c_int, + }; + opts + } + + /// Set checkout options. + pub fn checkout(&mut self, opts: CheckoutBuilder<'cb>) -> &mut Self { + self.checkout_builder = opts; + self + } + + /// Set fetch options and allow fetching. + pub fn fetch(&mut self, opts: FetchOptions<'cb>) -> &mut Self { + self.fetch_opts = opts; + self.allow_fetch = true; + self + } + + /// Allow or disallow fetching. + pub fn allow_fetch(&mut self, b: bool) -> &mut Self { + self.allow_fetch = b; + self + } +} + +impl<'cb> Default for SubmoduleUpdateOptions<'cb> { + fn default() -> Self { + Self::new() + } +} + #[cfg(test)] mod tests { - use std::path::Path; use std::fs; - use tempdir::TempDir; + use std::path::Path; + use tempfile::TempDir; use url::Url; - use Repository; + use crate::Repository; + use crate::SubmoduleUpdateOptions; #[test] fn smoke() { - let td = TempDir::new("test").unwrap(); + let td = TempDir::new().unwrap(); let repo = Repository::init(td.path()).unwrap(); - let mut s1 = repo.submodule("/path/to/nowhere", - Path::new("foo"), true).unwrap(); + let mut s1 = repo + .submodule("/path/to/nowhere", Path::new("foo"), true) + .unwrap(); s1.init(false).unwrap(); s1.sync().unwrap(); - let s2 = repo.submodule("/path/to/nowhere", - Path::new("bar"), true).unwrap(); + let s2 = repo + .submodule("/path/to/nowhere", Path::new("bar"), true) + .unwrap(); drop((s1, s2)); let mut submodules = repo.submodules().unwrap(); @@ -218,16 +368,104 @@ mod tests { #[test] fn add_a_submodule() { - let (_td, repo1) = ::test::repo_init(); - let (td, repo2) = ::test::repo_init(); + let (_td, repo1) = crate::test::repo_init(); + let (td, repo2) = crate::test::repo_init(); + + let url = Url::from_file_path(&repo1.workdir().unwrap()).unwrap(); + let mut s = repo2 + .submodule(&url.to_string(), Path::new("bar"), true) + .unwrap(); + t!(fs::remove_dir_all(td.path().join("bar"))); + t!(Repository::clone(&url.to_string(), td.path().join("bar"))); + t!(s.add_to_index(false)); + t!(s.add_finalize()); + } + + #[test] + fn update_submodule() { + // ----------------------------------- + // Same as `add_a_submodule()` + let (_td, repo1) = crate::test::repo_init(); + let (td, repo2) = crate::test::repo_init(); let url = Url::from_file_path(&repo1.workdir().unwrap()).unwrap(); - let mut s = repo2.submodule(&url.to_string(), Path::new("bar"), - true).unwrap(); + let mut s = repo2 + .submodule(&url.to_string(), Path::new("bar"), true) + .unwrap(); t!(fs::remove_dir_all(td.path().join("bar"))); - t!(Repository::clone(&url.to_string(), - td.path().join("bar"))); + t!(Repository::clone(&url.to_string(), td.path().join("bar"))); t!(s.add_to_index(false)); t!(s.add_finalize()); + // ----------------------------------- + + // Attempt to update submodule + let submodules = t!(repo1.submodules()); + for mut submodule in submodules { + let mut submodule_options = SubmoduleUpdateOptions::new(); + let init = true; + let opts = Some(&mut submodule_options); + + t!(submodule.update(init, opts)); + } + } + + #[test] + fn clone_submodule() { + // ----------------------------------- + // Same as `add_a_submodule()` + let (_td, repo1) = crate::test::repo_init(); + let (_td, repo2) = crate::test::repo_init(); + let (_td, parent) = crate::test::repo_init(); + + let url1 = Url::from_file_path(&repo1.workdir().unwrap()).unwrap(); + let url2 = Url::from_file_path(&repo2.workdir().unwrap()).unwrap(); + let mut s1 = parent + .submodule(&url1.to_string(), Path::new("bar"), true) + .unwrap(); + let mut s2 = parent + .submodule(&url2.to_string(), Path::new("bar2"), true) + .unwrap(); + // ----------------------------------- + + t!(s1.clone(Some(&mut SubmoduleUpdateOptions::default()))); + t!(s2.clone(None)); + } + + #[test] + fn repo_init_submodule() { + // ----------------------------------- + // Same as `clone_submodule()` + let (_td, child) = crate::test::repo_init(); + let (_td, parent) = crate::test::repo_init(); + + let url_child = Url::from_file_path(&child.workdir().unwrap()).unwrap(); + let url_parent = Url::from_file_path(&parent.workdir().unwrap()).unwrap(); + let mut sub = parent + .submodule(&url_child.to_string(), Path::new("bar"), true) + .unwrap(); + + // ----------------------------------- + // Let's commit the submodule for later clone + t!(sub.clone(None)); + t!(sub.add_to_index(true)); + t!(sub.add_finalize()); + + crate::test::commit(&parent); + + // Clone the parent to init its submodules + let td = TempDir::new().unwrap(); + let new_parent = Repository::clone(&url_parent.to_string(), &td).unwrap(); + + let mut submodules = new_parent.submodules().unwrap(); + let child = submodules.first_mut().unwrap(); + + // First init child + t!(child.init(false)); + assert_eq!(child.url().unwrap(), url_child.as_str()); + + // open() is not possible before initializing the repo + assert!(child.open().is_err()); + t!(child.repo_init(true)); + assert!(child.open().is_ok()); } } diff --git a/src/tag.rs b/src/tag.rs index 85739748de..6986c7c160 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -1,9 +1,11 @@ +use std::ffi::CString; use std::marker; use std::mem; +use std::ptr; use std::str; -use {raw, signature, Error, Oid, Object, Signature, ObjectType}; -use util::Binding; +use crate::util::Binding; +use crate::{call, raw, signature, Error, Object, ObjectType, Oid, Signature}; /// A structure to represent a git [tag][1] /// @@ -14,6 +16,19 @@ pub struct Tag<'repo> { } impl<'repo> Tag<'repo> { + /// Determine whether a tag name is valid, meaning that (when prefixed with refs/tags/) that + /// it is a valid reference name, and that any additional tag name restrictions are imposed + /// (eg, it cannot start with a -). + pub fn is_valid_name(tag_name: &str) -> bool { + crate::init(); + let tag_name = CString::new(tag_name).unwrap(); + let mut valid: libc::c_int = 0; + unsafe { + call::c_try(raw::git_tag_name_is_valid(&mut valid, tag_name.as_ptr())).unwrap(); + } + valid == 1 + } + /// Get the id (SHA1) of a repository tag pub fn id(&self) -> Oid { unsafe { Binding::from_raw(raw::git_tag_id(&*self.raw)) } @@ -30,7 +45,7 @@ impl<'repo> Tag<'repo> { /// /// Returns None if there is no message pub fn message_bytes(&self) -> Option<&[u8]> { - unsafe { ::opt_bytes(self, raw::git_tag_message(&*self.raw)) } + unsafe { crate::opt_bytes(self, raw::git_tag_message(&*self.raw)) } } /// Get the name of a tag @@ -42,12 +57,12 @@ impl<'repo> Tag<'repo> { /// Get the name of a tag pub fn name_bytes(&self) -> &[u8] { - unsafe { ::opt_bytes(self, raw::git_tag_name(&*self.raw)).unwrap() } + unsafe { crate::opt_bytes(self, raw::git_tag_name(&*self.raw)).unwrap() } } /// Recursively peel a tag until a non tag git_object is found pub fn peel(&self) -> Result, Error> { - let mut ret = 0 as *mut raw::git_object; + let mut ret = ptr::null_mut(); unsafe { try_call!(raw::git_tag_peel(&mut ret, &*self.raw)); Ok(Binding::from_raw(ret)) @@ -57,7 +72,7 @@ impl<'repo> Tag<'repo> { /// Get the tagger (author) of a tag /// /// If the author is unspecified, then `None` is returned. - pub fn tagger(&self) -> Option { + pub fn tagger(&self) -> Option> { unsafe { let ptr = raw::git_tag_tagger(&*self.raw); if ptr.is_null() { @@ -73,7 +88,7 @@ impl<'repo> Tag<'repo> { /// This method performs a repository lookup for the given object and /// returns it pub fn target(&self) -> Result, Error> { - let mut ret = 0 as *mut raw::git_object; + let mut ret = ptr::null_mut(); unsafe { try_call!(raw::git_tag_target(&mut ret, &*self.raw)); Ok(Binding::from_raw(ret)) @@ -85,33 +100,51 @@ impl<'repo> Tag<'repo> { unsafe { Binding::from_raw(raw::git_tag_target_id(&*self.raw)) } } - /// Get the OID of the tagged object of a tag + /// Get the ObjectType of the tagged object of a tag pub fn target_type(&self) -> Option { unsafe { ObjectType::from_raw(raw::git_tag_target_type(&*self.raw)) } } /// Casts this Tag to be usable as an `Object` pub fn as_object(&self) -> &Object<'repo> { - unsafe { - &*(self as *const _ as *const Object<'repo>) - } + unsafe { &*(self as *const _ as *const Object<'repo>) } } /// Consumes Tag to be returned as an `Object` pub fn into_object(self) -> Object<'repo> { - assert_eq!(mem::size_of_val(&self), mem::size_of::()); - unsafe { - mem::transmute(self) + assert_eq!(mem::size_of_val(&self), mem::size_of::>()); + unsafe { mem::transmute(self) } + } +} + +impl<'repo> std::fmt::Debug for Tag<'repo> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut ds = f.debug_struct("Tag"); + if let Some(name) = self.name() { + ds.field("name", &name); } + ds.field("id", &self.id()); + ds.finish() } } impl<'repo> Binding for Tag<'repo> { type Raw = *mut raw::git_tag; unsafe fn from_raw(raw: *mut raw::git_tag) -> Tag<'repo> { - Tag { raw: raw, _marker: marker::PhantomData } + Tag { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_tag { + self.raw + } +} + +impl<'repo> Clone for Tag<'repo> { + fn clone(&self) -> Self { + self.as_object().clone().into_tag().ok().unwrap() } - fn raw(&self) -> *mut raw::git_tag { self.raw } } impl<'repo> Drop for Tag<'repo> { @@ -122,9 +155,33 @@ impl<'repo> Drop for Tag<'repo> { #[cfg(test)] mod tests { + use crate::Tag; + + // Reference -- https://git-scm.com/docs/git-check-ref-format + #[test] + fn name_is_valid() { + assert_eq!(Tag::is_valid_name("blah_blah"), true); + assert_eq!(Tag::is_valid_name("v1.2.3"), true); + assert_eq!(Tag::is_valid_name("my/tag"), true); + assert_eq!(Tag::is_valid_name("@"), true); + + assert_eq!(Tag::is_valid_name("-foo"), false); + assert_eq!(Tag::is_valid_name("foo:bar"), false); + assert_eq!(Tag::is_valid_name("foo^bar"), false); + assert_eq!(Tag::is_valid_name("foo."), false); + assert_eq!(Tag::is_valid_name("@{"), false); + assert_eq!(Tag::is_valid_name("as\\cd"), false); + } + + #[test] + #[should_panic] + fn is_valid_name_for_invalid_tag() { + Tag::is_valid_name("ab\012"); + } + #[test] fn smoke() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let head = repo.head().unwrap(); let id = head.target().unwrap(); assert!(repo.find_tag(id).is_err()); @@ -143,21 +200,25 @@ mod tests { assert_eq!(tag.message(), Some("msg")); assert_eq!(tag.peel().unwrap().id(), obj.id()); assert_eq!(tag.target_id(), obj.id()); - assert_eq!(tag.target_type(), Some(::ObjectType::Commit)); + assert_eq!(tag.target_type(), Some(crate::ObjectType::Commit)); assert_eq!(tag.tagger().unwrap().name(), sig.name()); tag.target().unwrap(); tag.into_object(); repo.find_object(tag_id, None).unwrap().as_tag().unwrap(); - repo.find_object(tag_id, None).unwrap().into_tag().ok().unwrap(); + repo.find_object(tag_id, None) + .unwrap() + .into_tag() + .ok() + .unwrap(); repo.tag_delete("foo").unwrap(); } #[test] fn lite() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let head = t!(repo.head()); let id = head.target().unwrap(); let obj = t!(repo.find_object(id, None)); diff --git a/src/tagforeach.rs b/src/tagforeach.rs new file mode 100644 index 0000000000..425eea5a48 --- /dev/null +++ b/src/tagforeach.rs @@ -0,0 +1,69 @@ +//! git_tag_foreach support +//! see original: + +use crate::{panic, raw, util::Binding, Oid}; +use libc::{c_char, c_int}; +use raw::git_oid; +use std::ffi::{c_void, CStr}; + +/// boxed callback type +pub(crate) type TagForeachCB<'a> = Box bool + 'a>; + +/// helper type to be able to pass callback to payload +pub(crate) struct TagForeachData<'a> { + /// callback + pub(crate) cb: TagForeachCB<'a>, +} + +/// c callback forwarding to rust callback inside `TagForeachData` +/// see original: +pub(crate) extern "C" fn tag_foreach_cb( + name: *const c_char, + oid: *mut git_oid, + payload: *mut c_void, +) -> c_int { + panic::wrap(|| unsafe { + let id: Oid = Binding::from_raw(oid as *const _); + + let name = CStr::from_ptr(name); + let name = name.to_bytes(); + + let payload = &mut *(payload as *mut TagForeachData<'_>); + let cb = &mut payload.cb; + + let res = cb(id, name); + + if res { + 0 + } else { + -1 + } + }) + .unwrap_or(-1) +} + +#[cfg(test)] +mod tests { + + #[test] + fn smoke() { + let (_td, repo) = crate::test::repo_init(); + let head = repo.head().unwrap(); + let id = head.target().unwrap(); + assert!(repo.find_tag(id).is_err()); + + let obj = repo.find_object(id, None).unwrap(); + let sig = repo.signature().unwrap(); + let tag_id = repo.tag("foo", &obj, &sig, "msg", false).unwrap(); + + let mut tags = Vec::new(); + repo.tag_foreach(|id, name| { + tags.push((id, String::from_utf8(name.into()).unwrap())); + true + }) + .unwrap(); + + assert_eq!(tags[0].0, tag_id); + assert_eq!(tags[0].1, "refs/tags/foo"); + } +} diff --git a/src/test.rs b/src/test.rs index 55144611dc..57a590f519 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,20 +1,27 @@ -use std::path::{Path, PathBuf}; +use std::fs::File; use std::io; -use tempdir::TempDir; +use std::path::{Path, PathBuf}; +#[cfg(unix)] +use std::ptr; +use tempfile::TempDir; use url::Url; -use Repository; +use crate::{Branch, Oid, Repository, RepositoryInitOptions}; macro_rules! t { - ($e:expr) => (match $e { - Ok(e) => e, - Err(e) => panic!("{} failed with {}", stringify!($e), e), - }) + ($e:expr) => { + match $e { + Ok(e) => e, + Err(e) => panic!("{} failed with {}", stringify!($e), e), + } + }; } pub fn repo_init() -> (TempDir, Repository) { - let td = TempDir::new("test").unwrap(); - let repo = Repository::init(td.path()).unwrap(); + let td = TempDir::new().unwrap(); + let mut opts = RepositoryInitOptions::new(); + opts.initial_head("main"); + let repo = Repository::init_opts(td.path(), &opts).unwrap(); { let mut config = repo.config().unwrap(); config.set_str("user.name", "name").unwrap(); @@ -24,33 +31,56 @@ pub fn repo_init() -> (TempDir, Repository) { let tree = repo.find_tree(id).unwrap(); let sig = repo.signature().unwrap(); - repo.commit(Some("HEAD"), &sig, &sig, "initial", - &tree, &[]).unwrap(); + repo.commit(Some("HEAD"), &sig, &sig, "initial\n\nbody", &tree, &[]) + .unwrap(); } (td, repo) } +pub fn commit(repo: &Repository) -> (Oid, Oid) { + let mut index = t!(repo.index()); + let root = repo.path().parent().unwrap(); + t!(File::create(&root.join("foo"))); + t!(index.add_path(Path::new("foo"))); + + let tree_id = t!(index.write_tree()); + let tree = t!(repo.find_tree(tree_id)); + let sig = t!(repo.signature()); + let head_id = t!(repo.refname_to_id("HEAD")); + let parent = t!(repo.find_commit(head_id)); + let commit = t!(repo.commit(Some("HEAD"), &sig, &sig, "commit", &tree, &[&parent])); + (commit, tree_id) +} + pub fn path2url(/service/path: &Path) -> String { Url::from_file_path(path).unwrap().to_string() } +pub fn worktrees_env_init(repo: &Repository) -> (TempDir, Branch<'_>) { + let oid = repo.head().unwrap().target().unwrap(); + let commit = repo.find_commit(oid).unwrap(); + let branch = repo.branch("wt-branch", &commit, true).unwrap(); + let wtdir = TempDir::new().unwrap(); + (wtdir, branch) +} + #[cfg(windows)] pub fn realpath(original: &Path) -> io::Result { - Ok(original.to_path_buf()) + Ok(original.canonicalize()?.to_path_buf()) } #[cfg(unix)] pub fn realpath(original: &Path) -> io::Result { - use std::ffi::{CStr, OsString, CString}; + use libc::c_char; + use std::ffi::{CStr, CString, OsString}; use std::os::unix::prelude::*; - use libc::{self, c_char}; - extern { + extern "C" { fn realpath(name: *const c_char, resolved: *mut c_char) -> *mut c_char; } unsafe { - let cstr = try!(CString::new(original.as_os_str().as_bytes())); - let ptr = realpath(cstr.as_ptr(), 0 as *mut _); + let cstr = CString::new(original.as_os_str().as_bytes())?; + let ptr = realpath(cstr.as_ptr(), ptr::null_mut()); if ptr.is_null() { - return Err(io::Error::last_os_error()) + return Err(io::Error::last_os_error()); } let bytes = CStr::from_ptr(ptr).to_bytes().to_vec(); libc::free(ptr as *mut _); diff --git a/src/time.rs b/src/time.rs index e344aa3b14..46b5bd3f94 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1,18 +1,18 @@ use std::cmp::Ordering; -use libc::c_int; +use libc::{c_char, c_int}; -use raw; -use util::Binding; +use crate::raw; +use crate::util::Binding; /// Time in a signature -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct Time { raw: raw::git_time, } /// Time structure used in a git index entry. -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct IndexTime { raw: raw::git_index_time, } @@ -24,15 +24,26 @@ impl Time { Binding::from_raw(raw::git_time { time: time as raw::git_time_t, offset: offset as c_int, + sign: if offset < 0 { '-' } else { '+' } as c_char, }) } } /// Return the time, in seconds, from epoch - pub fn seconds(&self) -> i64 { self.raw.time as i64 } + pub fn seconds(&self) -> i64 { + self.raw.time as i64 + } /// Return the timezone offset, in minutes - pub fn offset_minutes(&self) -> i32 { self.raw.offset as i32 } + pub fn offset_minutes(&self) -> i32 { + self.raw.offset as i32 + } + + /// Return whether the offset was positive or negative. Primarily useful + /// in case the offset is specified as a negative zero. + pub fn sign(&self) -> char { + self.raw.sign as u8 as char + } } impl PartialOrd for Time { @@ -50,9 +61,11 @@ impl Ord for Time { impl Binding for Time { type Raw = raw::git_time; unsafe fn from_raw(raw: raw::git_time) -> Time { - Time { raw: raw } + Time { raw } + } + fn raw(&self) -> raw::git_time { + self.raw } - fn raw(&self) -> raw::git_time { self.raw } } impl IndexTime { @@ -60,24 +73,30 @@ impl IndexTime { pub fn new(seconds: i32, nanoseconds: u32) -> IndexTime { unsafe { Binding::from_raw(raw::git_index_time { - seconds: seconds, - nanoseconds: nanoseconds, + seconds, + nanoseconds, }) } } /// Returns the number of seconds in the second component of this time. - pub fn seconds(&self) -> i32 { self.raw.seconds } + pub fn seconds(&self) -> i32 { + self.raw.seconds + } /// Returns the nanosecond component of this time. - pub fn nanoseconds(&self) -> u32 { self.raw.nanoseconds } + pub fn nanoseconds(&self) -> u32 { + self.raw.nanoseconds + } } impl Binding for IndexTime { type Raw = raw::git_index_time; unsafe fn from_raw(raw: raw::git_index_time) -> IndexTime { - IndexTime { raw: raw } + IndexTime { raw } + } + fn raw(&self) -> raw::git_index_time { + self.raw } - fn raw(&self) -> raw::git_index_time { self.raw } } impl PartialOrd for IndexTime { @@ -93,3 +112,16 @@ impl Ord for IndexTime { me.cmp(&other) } } + +#[cfg(test)] +mod tests { + use crate::Time; + + #[test] + fn smoke() { + assert_eq!(Time::new(1608839587, -300).seconds(), 1608839587); + assert_eq!(Time::new(1608839587, -300).offset_minutes(), -300); + assert_eq!(Time::new(1608839587, -300).sign(), '-'); + assert_eq!(Time::new(1608839587, 300).sign(), '+'); + } +} diff --git a/src/tracing.rs b/src/tracing.rs new file mode 100644 index 0000000000..038ccd0438 --- /dev/null +++ b/src/tracing.rs @@ -0,0 +1,155 @@ +use std::{ + ffi::CStr, + sync::atomic::{AtomicPtr, Ordering}, +}; + +use libc::{c_char, c_int}; + +use crate::{raw, util::Binding, Error}; + +/// Available tracing levels. When tracing is set to a particular level, +/// callers will be provided tracing at the given level and all lower levels. +#[derive(Copy, Clone, Debug)] +pub enum TraceLevel { + /// No tracing will be performed. + None, + + /// Severe errors that may impact the program's execution + Fatal, + + /// Errors that do not impact the program's execution + Error, + + /// Warnings that suggest abnormal data + Warn, + + /// Informational messages about program execution + Info, + + /// Detailed data that allows for debugging + Debug, + + /// Exceptionally detailed debugging data + Trace, +} + +impl Binding for TraceLevel { + type Raw = raw::git_trace_level_t; + unsafe fn from_raw(raw: raw::git_trace_level_t) -> Self { + match raw { + raw::GIT_TRACE_NONE => Self::None, + raw::GIT_TRACE_FATAL => Self::Fatal, + raw::GIT_TRACE_ERROR => Self::Error, + raw::GIT_TRACE_WARN => Self::Warn, + raw::GIT_TRACE_INFO => Self::Info, + raw::GIT_TRACE_DEBUG => Self::Debug, + raw::GIT_TRACE_TRACE => Self::Trace, + _ => panic!("Unknown git trace level"), + } + } + fn raw(&self) -> raw::git_trace_level_t { + match *self { + Self::None => raw::GIT_TRACE_NONE, + Self::Fatal => raw::GIT_TRACE_FATAL, + Self::Error => raw::GIT_TRACE_ERROR, + Self::Warn => raw::GIT_TRACE_WARN, + Self::Info => raw::GIT_TRACE_INFO, + Self::Debug => raw::GIT_TRACE_DEBUG, + Self::Trace => raw::GIT_TRACE_TRACE, + } + } +} + +/// Callback type used to pass tracing events to the subscriber. +/// see `trace_set` to register a subscriber. +pub type TracingCb = fn(TraceLevel, &[u8]); + +/// Use an atomic pointer to store the global tracing subscriber function. +static CALLBACK: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); + +/// Set the global subscriber called when libgit2 produces a tracing message. +pub fn trace_set(level: TraceLevel, cb: TracingCb) -> Result<(), Error> { + // Store the callback in the global atomic. + CALLBACK.store(cb as *mut (), Ordering::SeqCst); + + // git_trace_set returns 0 if there was no error. + let return_code: c_int = unsafe { raw::git_trace_set(level.raw(), Some(tracing_cb_c)) }; + + if return_code != 0 { + Err(Error::last_error(return_code)) + } else { + Ok(()) + } +} + +/// The tracing callback we pass to libgit2 (C ABI compatible). +extern "C" fn tracing_cb_c(level: raw::git_trace_level_t, msg: *const c_char) { + // Load the callback function pointer from the global atomic. + let cb: *mut () = CALLBACK.load(Ordering::SeqCst); + + // Transmute the callback pointer into the function pointer we know it to be. + // + // SAFETY: We only ever set the callback pointer with something cast from a TracingCb + // so transmuting back to a TracingCb is safe. This is notably not an integer-to-pointer + // transmute as described in the mem::transmute documentation and is in-line with the + // example in that documentation for casing between *const () to fn pointers. + let cb: TracingCb = unsafe { std::mem::transmute(cb) }; + + // If libgit2 passes us a message that is null, drop it and do not pass it to the callback. + // This is to avoid ever exposing rust code to a null ref, which would be Undefined Behavior. + if msg.is_null() { + return; + } + + // Convert the message from a *const c_char to a &[u8] and pass it to the callback. + // + // SAFETY: We've just checked that the pointer is not null. The other safety requirements are left to + // libgit2 to enforce -- namely that it gives us a valid, nul-terminated, C string, that that string exists + // entirely in one allocation, that the string will not be mutated once passed to us, and that the nul-terminator is + // within isize::MAX bytes from the given pointers data address. + let msg: &CStr = unsafe { CStr::from_ptr(msg) }; + + // Convert from a CStr to &[u8] to pass to the rust code callback. + let msg: &[u8] = CStr::to_bytes(msg); + + // Do not bother with wrapping any of the following calls in `panic::wrap`: + // + // The previous implementation used `panic::wrap` here but never called `panic::check` to determine if the + // trace callback had panicked, much less what caused it. + // + // This had the potential to lead to lost errors/unwinds, confusing to debugging situations, and potential issues + // catching panics in other parts of the `git2-rs` codebase. + // + // Instead, we simply call the next two lines, both of which may panic, directly. We can rely on the + // `extern "C"` semantics to appropriately catch the panics generated here and abort the process: + // + // Per : + // > Rust functions that are expected to be called from foreign code that does not support + // > unwinding (such as C compiled with -fno-exceptions) should be defined using extern "C", which ensures + // > that if the Rust code panics, it is automatically caught and the process is aborted. If this is the desired + // > behavior, it is not necessary to use catch_unwind explicitly. This function should instead be used when + // > more graceful error-handling is needed. + + // Convert the raw trace level into a type we can pass to the rust callback fn. + // + // SAFETY: Currently the implementation of this function (above) may panic, but is only marked as unsafe to match + // the trait definition, thus we can consider this call safe. + let level: TraceLevel = unsafe { Binding::from_raw(level) }; + + // Call the user-supplied callback (which may panic). + (cb)(level, msg); +} + +#[cfg(test)] +mod tests { + use super::TraceLevel; + + // Test that using the above function to set a tracing callback doesn't panic. + #[test] + fn smoke() { + super::trace_set(TraceLevel::Trace, |level, msg| { + dbg!(level, msg); + }) + .expect("libgit2 can set global trace callback"); + } +} diff --git a/src/transaction.rs b/src/transaction.rs new file mode 100644 index 0000000000..4f661f1d48 --- /dev/null +++ b/src/transaction.rs @@ -0,0 +1,285 @@ +use std::ffi::CString; +use std::marker; + +use crate::{raw, util::Binding, Error, Oid, Reflog, Repository, Signature}; + +/// A structure representing a transactional update of a repository's references. +/// +/// Transactions work by locking loose refs for as long as the [`Transaction`] +/// is held, and committing all changes to disk when [`Transaction::commit`] is +/// called. Note that committing is not atomic: if an operation fails, the +/// transaction aborts, but previous successful operations are not rolled back. +pub struct Transaction<'repo> { + raw: *mut raw::git_transaction, + _marker: marker::PhantomData<&'repo Repository>, +} + +impl Drop for Transaction<'_> { + fn drop(&mut self) { + unsafe { raw::git_transaction_free(self.raw) } + } +} + +impl<'repo> Binding for Transaction<'repo> { + type Raw = *mut raw::git_transaction; + + unsafe fn from_raw(ptr: *mut raw::git_transaction) -> Transaction<'repo> { + Transaction { + raw: ptr, + _marker: marker::PhantomData, + } + } + + fn raw(&self) -> *mut raw::git_transaction { + self.raw + } +} + +impl<'repo> Transaction<'repo> { + /// Lock the specified reference by name. + pub fn lock_ref(&mut self, refname: &str) -> Result<(), Error> { + let refname = CString::new(refname).unwrap(); + unsafe { + try_call!(raw::git_transaction_lock_ref(self.raw, refname)); + } + + Ok(()) + } + + /// Set the target of the specified reference. + /// + /// The reference must have been locked via `lock_ref`. + /// + /// If `reflog_signature` is `None`, the [`Signature`] is read from the + /// repository config. + pub fn set_target( + &mut self, + refname: &str, + target: Oid, + reflog_signature: Option<&Signature<'_>>, + reflog_message: &str, + ) -> Result<(), Error> { + let refname = CString::new(refname).unwrap(); + let reflog_message = CString::new(reflog_message).unwrap(); + unsafe { + try_call!(raw::git_transaction_set_target( + self.raw, + refname, + target.raw(), + reflog_signature.map(|s| s.raw()), + reflog_message + )); + } + + Ok(()) + } + + /// Set the target of the specified symbolic reference. + /// + /// The reference must have been locked via `lock_ref`. + /// + /// If `reflog_signature` is `None`, the [`Signature`] is read from the + /// repository config. + pub fn set_symbolic_target( + &mut self, + refname: &str, + target: &str, + reflog_signature: Option<&Signature<'_>>, + reflog_message: &str, + ) -> Result<(), Error> { + let refname = CString::new(refname).unwrap(); + let target = CString::new(target).unwrap(); + let reflog_message = CString::new(reflog_message).unwrap(); + unsafe { + try_call!(raw::git_transaction_set_symbolic_target( + self.raw, + refname, + target, + reflog_signature.map(|s| s.raw()), + reflog_message + )); + } + + Ok(()) + } + + /// Add a [`Reflog`] to the transaction. + /// + /// This commit the in-memory [`Reflog`] to disk when the transaction commits. + /// Note that atomicity is **not* guaranteed: if the transaction fails to + /// modify `refname`, the reflog may still have been committed to disk. + /// + /// If this is combined with setting the target, that update won't be + /// written to the log (i.e. the `reflog_signature` and `reflog_message` + /// parameters will be ignored). + pub fn set_reflog(&mut self, refname: &str, reflog: Reflog) -> Result<(), Error> { + let refname = CString::new(refname).unwrap(); + unsafe { + try_call!(raw::git_transaction_set_reflog( + self.raw, + refname, + reflog.raw() + )); + } + + Ok(()) + } + + /// Remove a reference. + /// + /// The reference must have been locked via `lock_ref`. + pub fn remove(&mut self, refname: &str) -> Result<(), Error> { + let refname = CString::new(refname).unwrap(); + unsafe { + try_call!(raw::git_transaction_remove(self.raw, refname)); + } + + Ok(()) + } + + /// Commit the changes from the transaction. + /// + /// The updates will be made one by one, and the first failure will stop the + /// processing. + pub fn commit(self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_transaction_commit(self.raw)); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::{Error, ErrorClass, ErrorCode, Oid, Repository}; + + #[test] + fn smoke() { + let (_td, repo) = crate::test::repo_init(); + + let mut tx = t!(repo.transaction()); + + t!(tx.lock_ref("refs/heads/main")); + t!(tx.lock_ref("refs/heads/next")); + + t!(tx.set_target("refs/heads/main", Oid::zero(), None, "set main to zero")); + t!(tx.set_symbolic_target( + "refs/heads/next", + "refs/heads/main", + None, + "set next to main", + )); + + t!(tx.commit()); + + assert_eq!(repo.refname_to_id("refs/heads/main").unwrap(), Oid::zero()); + assert_eq!( + repo.find_reference("refs/heads/next") + .unwrap() + .symbolic_target() + .unwrap(), + "refs/heads/main" + ); + } + + #[test] + fn locks_same_repo_handle() { + let (_td, repo) = crate::test::repo_init(); + + let mut tx1 = t!(repo.transaction()); + t!(tx1.lock_ref("refs/heads/seen")); + + let mut tx2 = t!(repo.transaction()); + assert!(matches!(tx2.lock_ref("refs/heads/seen"), Err(e) if e.code() == ErrorCode::Locked)) + } + + #[test] + fn locks_across_repo_handles() { + let (td, repo1) = crate::test::repo_init(); + let repo2 = t!(Repository::open(&td)); + + let mut tx1 = t!(repo1.transaction()); + t!(tx1.lock_ref("refs/heads/seen")); + + let mut tx2 = t!(repo2.transaction()); + assert!(matches!(tx2.lock_ref("refs/heads/seen"), Err(e) if e.code() == ErrorCode::Locked)) + } + + #[test] + fn drop_unlocks() { + let (_td, repo) = crate::test::repo_init(); + + let mut tx = t!(repo.transaction()); + t!(tx.lock_ref("refs/heads/seen")); + drop(tx); + + let mut tx2 = t!(repo.transaction()); + t!(tx2.lock_ref("refs/heads/seen")) + } + + #[test] + fn commit_unlocks() { + let (_td, repo) = crate::test::repo_init(); + + let mut tx = t!(repo.transaction()); + t!(tx.lock_ref("refs/heads/seen")); + t!(tx.commit()); + + let mut tx2 = t!(repo.transaction()); + t!(tx2.lock_ref("refs/heads/seen")); + } + + #[test] + fn prevents_non_transactional_updates() { + let (_td, repo) = crate::test::repo_init(); + let head = t!(repo.refname_to_id("HEAD")); + + let mut tx = t!(repo.transaction()); + t!(tx.lock_ref("refs/heads/seen")); + + assert!(matches!( + repo.reference("refs/heads/seen", head, true, "competing with lock"), + Err(e) if e.code() == ErrorCode::Locked + )); + } + + #[test] + fn remove() { + let (_td, repo) = crate::test::repo_init(); + let head = t!(repo.refname_to_id("HEAD")); + let next = "refs/heads/next"; + + t!(repo.reference( + next, + head, + true, + "refs/heads/next@{0}: branch: Created from HEAD" + )); + + { + let mut tx = t!(repo.transaction()); + t!(tx.lock_ref(next)); + t!(tx.remove(next)); + t!(tx.commit()); + } + assert!(matches!(repo.refname_to_id(next), Err(e) if e.code() == ErrorCode::NotFound)) + } + + #[test] + fn must_lock_ref() { + let (_td, repo) = crate::test::repo_init(); + + // 🤷 + fn is_not_locked_err(e: &Error) -> bool { + e.code() == ErrorCode::NotFound + && e.class() == ErrorClass::Reference + && e.message() == "the specified reference is not locked" + } + + let mut tx = t!(repo.transaction()); + assert!(matches!( + tx.set_target("refs/heads/main", Oid::zero(), None, "set main to zero"), + Err(e) if is_not_locked_err(&e) + )) + } +} diff --git a/src/transport.rs b/src/transport.rs index 92fe3ad267..b1ca3f8b80 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -1,15 +1,16 @@ //! Interfaces for adding custom transports to libgit2 +use libc::{c_char, c_int, c_uint, c_void, size_t}; use std::ffi::{CStr, CString}; -use std::io::prelude::*; use std::io; +use std::io::prelude::*; use std::mem; +use std::ptr; use std::slice; use std::str; -use libc::{c_int, c_void, c_uint, c_char, size_t}; -use {raw, panic, Error, Remote}; -use util::Binding; +use crate::util::Binding; +use crate::{panic, raw, Error, Remote}; /// A transport is a structure which knows how to transfer data to and from a /// remote. @@ -22,9 +23,9 @@ pub struct Transport { owned: bool, } -/// Interfaced used by smart transports. +/// Interface used by smart transports. /// -/// The full-fledged definiton of transports has to deal with lots of +/// The full-fledged definition of transports has to deal with lots of /// nitty-gritty details of the git protocol, but "smart transports" largely /// only need to deal with read() and write() of data over a channel. /// @@ -39,12 +40,12 @@ pub trait SmartSubtransport: Send + 'static { /// returns a stream which can be read and written from in order to /// negotiate the git protocol. fn action(&self, url: &str, action: Service) - -> Result, Error>; + -> Result, Error>; /// Terminates a connection with the remote. /// /// Each subtransport is guaranteed a call to close() between calls to - /// action(), except for the following tow natural progressions of actions + /// action(), except for the following two natural progressions of actions /// against a constant URL. /// /// 1. UploadPackLs -> UploadPack @@ -53,7 +54,7 @@ pub trait SmartSubtransport: Send + 'static { } /// Actions that a smart transport can ask a subtransport to perform -#[derive(Copy, Clone)] +#[derive(Copy, Clone, PartialEq, Debug)] #[allow(missing_docs)] pub enum Service { UploadPackLs, @@ -72,8 +73,7 @@ pub trait SmartSubtransportStream: Read + Write + Send + 'static {} impl SmartSubtransportStream for T {} -type TransportFactory = Fn(&Remote) -> Result + Send + Sync + - 'static; +type TransportFactory = dyn Fn(&Remote<'_>) -> Result + Send + Sync + 'static; /// Boxed data payload used for registering new transports. /// @@ -87,7 +87,9 @@ struct TransportData { #[repr(C)] struct RawSmartSubtransport { raw: raw::git_smart_subtransport, - obj: Box, + stream: Option<*mut raw::git_smart_subtransport_stream>, + rpc: bool, + obj: Box, } /// Instance of a `git_smart_subtransport_stream`, must use `#[repr(C)]` to @@ -95,7 +97,7 @@ struct RawSmartSubtransport { #[repr(C)] struct RawSmartSubtransportStream { raw: raw::git_smart_subtransport_stream, - obj: Box, + obj: Box, } /// Add a custom transport definition, to be used in addition to the built-in @@ -104,16 +106,17 @@ struct RawSmartSubtransportStream { /// This function is unsafe as it needs to be externally synchronized with calls /// to creation of other transports. pub unsafe fn register(prefix: &str, factory: F) -> Result<(), Error> - where F: Fn(&Remote) -> Result + Send + Sync + 'static +where + F: Fn(&Remote<'_>) -> Result + Send + Sync + 'static, { + crate::init(); let mut data = Box::new(TransportData { factory: Box::new(factory), }); - let prefix = try!(CString::new(prefix)); + let prefix = CString::new(prefix)?; let datap = (&mut *data) as *mut TransportData as *mut c_void; - try_call!(raw::git_transport_register(prefix, - transport_factory, - datap)); + let factory: raw::git_transport_cb = Some(transport_factory); + try_call!(raw::git_transport_register(prefix, factory, datap)); mem::forget(data); Ok(()) } @@ -129,28 +132,29 @@ impl Transport { /// /// The `rpc` argument is `true` if the protocol is stateless, false /// otherwise. For example `http://` is stateless but `git://` is not. - pub fn smart(remote: &Remote, - rpc: bool, - subtransport: S) -> Result - where S: SmartSubtransport + pub fn smart(remote: &Remote<'_>, rpc: bool, subtransport: S) -> Result + where + S: SmartSubtransport, { - let mut ret = 0 as *mut _; + let mut ret = ptr::null_mut(); let mut raw = Box::new(RawSmartSubtransport { raw: raw::git_smart_subtransport { - action: subtransport_action, - close: subtransport_close, - free: subtransport_free, + action: Some(subtransport_action), + close: Some(subtransport_close), + free: Some(subtransport_free), }, + stream: None, + rpc, obj: Box::new(subtransport), }); let mut defn = raw::git_smart_subtransport_definition { - callback: smart_factory, + callback: Some(smart_factory), rpc: rpc as c_uint, param: &mut *raw as *mut _ as *mut _, }; - // Currently there's no way to pass a paload via the + // Currently there's no way to pass a payload via the // git_smart_subtransport_definition structure, but it's only used as a // configuration for the initial creation of the smart transport (verified // by reading the current code, hopefully it doesn't change!). @@ -159,15 +163,23 @@ impl Transport { // `RawSmartSubtransport`). This also means that this block must be // entirely synchronized with a lock (boo!) unsafe { - try_call!(raw::git_transport_smart(&mut ret, remote.raw(), - &mut defn as *mut _ as *mut _)); + try_call!(raw::git_transport_smart( + &mut ret, + remote.raw(), + &mut defn as *mut _ as *mut _ + )); mem::forget(raw); // ownership transport to `ret` } - return Ok(Transport { raw: ret, owned: true }); + return Ok(Transport { + raw: ret, + owned: true, + }); - extern fn smart_factory(out: *mut *mut raw::git_smart_subtransport, - _owner: *mut raw::git_transport, - ptr: *mut c_void) -> c_int { + extern "C" fn smart_factory( + out: *mut *mut raw::git_smart_subtransport, + _owner: *mut raw::git_transport, + ptr: *mut c_void, + ) -> c_int { unsafe { *out = ptr as *mut raw::git_smart_subtransport; 0 @@ -179,18 +191,20 @@ impl Transport { impl Drop for Transport { fn drop(&mut self) { if self.owned { - unsafe { - ((*self.raw).free)(self.raw) - } + unsafe { (*self.raw).free.unwrap()(self.raw) } } } } // callback used by register() to create new transports -extern fn transport_factory(out: *mut *mut raw::git_transport, - owner: *mut raw::git_remote, - param: *mut c_void) -> c_int { - struct Bomb<'a> { remote: Option> } +extern "C" fn transport_factory( + out: *mut *mut raw::git_transport, + owner: *mut raw::git_remote, + param: *mut c_void, +) -> c_int { + struct Bomb<'a> { + remote: Option>, + } impl<'a> Drop for Bomb<'a> { fn drop(&mut self) { // TODO: maybe a method instead? @@ -199,7 +213,9 @@ extern fn transport_factory(out: *mut *mut raw::git_transport, } panic::wrap(|| unsafe { - let remote = Bomb { remote: Some(Binding::from_raw(owner)) }; + let remote = Bomb { + remote: Some(Binding::from_raw(owner)), + }; let data = &mut *(param as *mut TransportData); match (data.factory)(remote.remote.as_ref().unwrap()) { Ok(mut transport) => { @@ -209,15 +225,18 @@ extern fn transport_factory(out: *mut *mut raw::git_transport, } Err(e) => e.raw_code() as c_int, } - }).unwrap_or(-1) + }) + .unwrap_or(-1) } // callback used by smart transports to delegate an action to a // `SmartSubtransport` trait object. -extern fn subtransport_action(stream: *mut *mut raw::git_smart_subtransport_stream, - raw_transport: *mut raw::git_smart_subtransport, - url: *const c_char, - action: raw::git_smart_service_t) -> c_int { +extern "C" fn subtransport_action( + stream: *mut *mut raw::git_smart_subtransport_stream, + raw_transport: *mut raw::git_smart_subtransport, + url: *const c_char, + action: raw::git_smart_service_t, +) -> c_int { panic::wrap(|| unsafe { let url = CStr::from_ptr(url).to_bytes(); let url = match str::from_utf8(url).ok() { @@ -231,28 +250,41 @@ extern fn subtransport_action(stream: *mut *mut raw::git_smart_subtransport_stre raw::GIT_SERVICE_RECEIVEPACK => Service::ReceivePack, n => panic!("unknown action: {}", n), }; + let transport = &mut *(raw_transport as *mut RawSmartSubtransport); - let obj = match transport.obj.action(url, action) { - Ok(s) => s, - Err(e) => return e.raw_code() as c_int, - }; - *stream = mem::transmute(Box::new(RawSmartSubtransportStream { - raw: raw::git_smart_subtransport_stream { - subtransport: raw_transport, - read: stream_read, - write: stream_write, - free: stream_free, - }, - obj: obj, - })); + // Note: we only need to generate if rpc is on. Else, for receive-pack and upload-pack + // libgit2 reuses the stream generated for receive-pack-ls or upload-pack-ls. + let generate_stream = + transport.rpc || action == Service::UploadPackLs || action == Service::ReceivePackLs; + if generate_stream { + let obj = match transport.obj.action(url, action) { + Ok(s) => s, + Err(e) => return e.raw_set_git_error(), + }; + *stream = mem::transmute(Box::new(RawSmartSubtransportStream { + raw: raw::git_smart_subtransport_stream { + subtransport: raw_transport, + read: Some(stream_read), + write: Some(stream_write), + free: Some(stream_free), + }, + obj, + })); + transport.stream = Some(*stream); + } else { + if transport.stream.is_none() { + return -1; + } + *stream = transport.stream.unwrap(); + } 0 - }).unwrap_or(-1) + }) + .unwrap_or(-1) } // callback used by smart transports to close a `SmartSubtransport` trait // object. -extern fn subtransport_close(transport: *mut raw::git_smart_subtransport) - -> c_int { +extern "C" fn subtransport_close(transport: *mut raw::git_smart_subtransport) -> c_int { let ret = panic::wrap(|| unsafe { let transport = &mut *(transport as *mut RawSmartSubtransport); transport.obj.close() @@ -266,7 +298,7 @@ extern fn subtransport_close(transport: *mut raw::git_smart_subtransport) // callback used by smart transports to free a `SmartSubtransport` trait // object. -extern fn subtransport_free(transport: *mut raw::git_smart_subtransport) { +extern "C" fn subtransport_free(transport: *mut raw::git_smart_subtransport) { let _ = panic::wrap(|| unsafe { mem::transmute::<_, Box>(transport); }); @@ -274,31 +306,40 @@ extern fn subtransport_free(transport: *mut raw::git_smart_subtransport) { // callback used by smart transports to read from a `SmartSubtransportStream` // object. -extern fn stream_read(stream: *mut raw::git_smart_subtransport_stream, - buffer: *mut c_char, - buf_size: size_t, - bytes_read: *mut size_t) -> c_int { +extern "C" fn stream_read( + stream: *mut raw::git_smart_subtransport_stream, + buffer: *mut c_char, + buf_size: size_t, + bytes_read: *mut size_t, +) -> c_int { let ret = panic::wrap(|| unsafe { let transport = &mut *(stream as *mut RawSmartSubtransportStream); - let buf = slice::from_raw_parts_mut(buffer as *mut u8, - buf_size as usize); + let buf = slice::from_raw_parts_mut(buffer as *mut u8, buf_size as usize); match transport.obj.read(buf) { - Ok(n) => { *bytes_read = n as size_t; Ok(n) } + Ok(n) => { + *bytes_read = n as size_t; + Ok(n) + } e => e, } }); match ret { Some(Ok(_)) => 0, - Some(Err(e)) => unsafe { set_err(e); -2 }, + Some(Err(e)) => unsafe { + set_err_io(&e); + -2 + }, None => -1, } } // callback used by smart transports to write to a `SmartSubtransportStream` // object. -extern fn stream_write(stream: *mut raw::git_smart_subtransport_stream, - buffer: *const c_char, - len: size_t) -> c_int { +extern "C" fn stream_write( + stream: *mut raw::git_smart_subtransport_stream, + buffer: *const c_char, + len: size_t, +) -> c_int { let ret = panic::wrap(|| unsafe { let transport = &mut *(stream as *mut RawSmartSubtransportStream); let buf = slice::from_raw_parts(buffer as *const u8, len as usize); @@ -306,20 +347,75 @@ extern fn stream_write(stream: *mut raw::git_smart_subtransport_stream, }); match ret { Some(Ok(())) => 0, - Some(Err(e)) => unsafe { set_err(e); -2 }, + Some(Err(e)) => unsafe { + set_err_io(&e); + -2 + }, None => -1, } } -unsafe fn set_err(e: io::Error) { +unsafe fn set_err_io(e: &io::Error) { let s = CString::new(e.to_string()).unwrap(); - raw::giterr_set_str(raw::GITERR_NET as c_int, s.as_ptr()) + raw::git_error_set_str(raw::GIT_ERROR_NET as c_int, s.as_ptr()); } // callback used by smart transports to free a `SmartSubtransportStream` // object. -extern fn stream_free(stream: *mut raw::git_smart_subtransport_stream) { +extern "C" fn stream_free(stream: *mut raw::git_smart_subtransport_stream) { let _ = panic::wrap(|| unsafe { mem::transmute::<_, Box>(stream); }); } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ErrorClass, ErrorCode}; + use std::sync::Once; + + struct DummyTransport; + + // in lieu of lazy_static + fn dummy_error() -> Error { + Error::new(ErrorCode::Ambiguous, ErrorClass::Net, "bleh") + } + + impl SmartSubtransport for DummyTransport { + fn action( + &self, + _url: &str, + _service: Service, + ) -> Result, Error> { + Err(dummy_error()) + } + + fn close(&self) -> Result<(), Error> { + Ok(()) + } + } + + #[test] + fn transport_error_propagates() { + static INIT: Once = Once::new(); + + unsafe { + INIT.call_once(|| { + register("dummy", move |remote| { + Transport::smart(&remote, true, DummyTransport) + }) + .unwrap(); + }) + } + + let (_td, repo) = crate::test::repo_init(); + t!(repo.remote("origin", "dummy://ball")); + + let mut origin = t!(repo.find_remote("origin")); + + match origin.fetch(&["main"], None, None) { + Ok(()) => unreachable!(), + Err(e) => assert_eq!(e, dummy_error()), + } + } +} diff --git a/src/tree.rs b/src/tree.rs index 27231adc03..e683257436 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -1,14 +1,16 @@ -use std::mem; +use libc::{c_char, c_int, c_void}; use std::cmp::Ordering; -use std::ffi::CString; -use std::ops::Range; +use std::ffi::{CStr, CString}; +use std::iter::FusedIterator; use std::marker; +use std::mem; +use std::ops::Range; use std::path::Path; +use std::ptr; use std::str; -use libc; -use {raw, Oid, Repository, Error, Object, ObjectType}; -use util::{Binding, IntoCString}; +use crate::util::{c_cmp_to_ordering, path_to_repo_path, Binding}; +use crate::{panic, raw, Error, Object, ObjectType, Oid, Repository}; /// A structure to represent a git [tree][1] /// @@ -32,6 +34,44 @@ pub struct TreeIter<'tree> { tree: &'tree Tree<'tree>, } +/// A binary indicator of whether a tree walk should be performed in pre-order +/// or post-order. +#[derive(Clone, Copy)] +pub enum TreeWalkMode { + /// Runs the traversal in pre-order. + PreOrder = 0, + /// Runs the traversal in post-order. + PostOrder = 1, +} + +/// Possible return codes for tree walking callback functions. +#[repr(i32)] +pub enum TreeWalkResult { + /// Continue with the traversal as normal. + Ok = 0, + /// Skip the current node (in pre-order mode). + Skip = 1, + /// Completely stop the traversal. + Abort = raw::GIT_EUSER, +} + +impl Into for TreeWalkResult { + fn into(self) -> i32 { + self as i32 + } +} + +impl Into for TreeWalkMode { + #[cfg(target_env = "msvc")] + fn into(self) -> raw::git_treewalk_mode { + self as i32 + } + #[cfg(not(target_env = "msvc"))] + fn into(self) -> raw::git_treewalk_mode { + self as u32 + } +} + impl<'repo> Tree<'repo> { /// Get the id (SHA1) of a repository object pub fn id(&self) -> Oid { @@ -43,13 +83,62 @@ impl<'repo> Tree<'repo> { unsafe { raw::git_tree_entrycount(&*self.raw) as usize } } + /// Return `true` if there is not entry + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Returns an iterator over the entries in this tree. - pub fn iter(&self) -> TreeIter { - TreeIter { range: 0..self.len(), tree: self } + pub fn iter(&self) -> TreeIter<'_> { + TreeIter { + range: 0..self.len(), + tree: self, + } + } + + /// Traverse the entries in a tree and its subtrees in post or pre-order. + /// The callback function will be run on each node of the tree that's + /// walked. The return code of this function will determine how the walk + /// continues. + /// + /// libgit2 requires that the callback be an integer, where 0 indicates a + /// successful visit, 1 skips the node, and -1 aborts the traversal completely. + /// You may opt to use the enum [`TreeWalkResult`] instead. + /// + /// ```ignore + /// let mut ct = 0; + /// tree.walk(TreeWalkMode::PreOrder, |_, entry| { + /// assert_eq!(entry.name(), Some("foo")); + /// ct += 1; + /// TreeWalkResult::Ok + /// }).unwrap(); + /// assert_eq!(ct, 1); + /// ``` + /// + /// See [libgit2 documentation][1] for more information. + /// + /// [1]: https://libgit2.org/libgit2/#HEAD/group/tree/git_tree_walk + pub fn walk(&self, mode: TreeWalkMode, mut callback: C) -> Result<(), Error> + where + C: FnMut(&str, &TreeEntry<'_>) -> T, + T: Into, + { + unsafe { + let mut data = TreeWalkCbData { + callback: &mut callback, + }; + try_call!(raw::git_tree_walk( + self.raw(), + mode as raw::git_treewalk_mode, + treewalk_cb::, + &mut data as *mut _ as *mut c_void + )); + Ok(()) + } } /// Lookup a tree entry by SHA value. - pub fn get_id(&self, id: Oid) -> Option { + pub fn get_id(&self, id: Oid) -> Option> { unsafe { let ptr = raw::git_tree_entry_byid(&*self.raw(), &*id.raw()); if ptr.is_null() { @@ -61,10 +150,9 @@ impl<'repo> Tree<'repo> { } /// Lookup a tree entry by its position in the tree - pub fn get(&self, n: usize) -> Option { + pub fn get(&self, n: usize) -> Option> { unsafe { - let ptr = raw::git_tree_entry_byindex(&*self.raw(), - n as libc::size_t); + let ptr = raw::git_tree_entry_byindex(&*self.raw(), n as libc::size_t); if ptr.is_null() { None } else { @@ -74,7 +162,14 @@ impl<'repo> Tree<'repo> { } /// Lookup a tree entry by its filename - pub fn get_name(&self, filename: &str) -> Option { + pub fn get_name(&self, filename: &str) -> Option> { + self.get_name_bytes(filename.as_bytes()) + } + + /// Lookup a tree entry by its filename, specified as bytes. + /// + /// This allows for non-UTF-8 filenames. + pub fn get_name_bytes(&self, filename: &[u8]) -> Option> { let filename = CString::new(filename).unwrap(); unsafe { let ptr = call!(raw::git_tree_entry_byname(&*self.raw(), filename)); @@ -89,8 +184,8 @@ impl<'repo> Tree<'repo> { /// Retrieve a tree entry contained in a tree or in any of its subtrees, /// given its relative path. pub fn get_path(&self, path: &Path) -> Result, Error> { - let path = try!(path.into_c_string()); - let mut ret = 0 as *mut raw::git_tree_entry; + let path = path_to_repo_path(path)?; + let mut ret = ptr::null_mut(); unsafe { try_call!(raw::git_tree_entry_bypath(&mut ret, &*self.raw(), path)); Ok(Binding::from_raw(ret)) @@ -99,17 +194,39 @@ impl<'repo> Tree<'repo> { /// Casts this Tree to be usable as an `Object` pub fn as_object(&self) -> &Object<'repo> { - unsafe { - &*(self as *const _ as *const Object<'repo>) - } + unsafe { &*(self as *const _ as *const Object<'repo>) } } - /// Consumes Commit to be returned as an `Object` + /// Consumes this Tree to be returned as an `Object` pub fn into_object(self) -> Object<'repo> { - assert_eq!(mem::size_of_val(&self), mem::size_of::()); - unsafe { - mem::transmute(self) - } + assert_eq!(mem::size_of_val(&self), mem::size_of::>()); + unsafe { mem::transmute(self) } + } +} + +type TreeWalkCb<'a, T> = dyn FnMut(&str, &TreeEntry<'_>) -> T + 'a; + +struct TreeWalkCbData<'a, T> { + callback: &'a mut TreeWalkCb<'a, T>, +} + +extern "C" fn treewalk_cb>( + root: *const c_char, + entry: *const raw::git_tree_entry, + payload: *mut c_void, +) -> c_int { + match panic::wrap(|| unsafe { + let root = match CStr::from_ptr(root).to_str() { + Ok(value) => value, + _ => return -1, + }; + let entry = entry_from_raw_const(entry); + let payload = &mut *(payload as *mut TreeWalkCbData<'_, T>); + let callback = &mut payload.callback; + callback(root, &entry).into() + }) { + Some(value) => value, + None => -1, } } @@ -117,9 +234,26 @@ impl<'repo> Binding for Tree<'repo> { type Raw = *mut raw::git_tree; unsafe fn from_raw(raw: *mut raw::git_tree) -> Tree<'repo> { - Tree { raw: raw, _marker: marker::PhantomData } + Tree { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_tree { + self.raw + } +} + +impl<'repo> std::fmt::Debug for Tree<'repo> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_struct("Tree").field("id", &self.id()).finish() + } +} + +impl<'repo> Clone for Tree<'repo> { + fn clone(&self) -> Self { + self.as_object().clone().into_tree().ok().unwrap() } - fn raw(&self) -> *mut raw::git_tree { self.raw } } impl<'repo> Drop for Tree<'repo> { @@ -140,8 +274,7 @@ impl<'repo, 'iter> IntoIterator for &'iter Tree<'repo> { /// /// The lifetime of the entry is tied to the tree provided and the function /// is unsafe because the validity of the pointer cannot be guaranteed. -pub unsafe fn entry_from_raw_const<'tree>(raw: *const raw::git_tree_entry) - -> TreeEntry<'tree> { +pub unsafe fn entry_from_raw_const<'tree>(raw: *const raw::git_tree_entry) -> TreeEntry<'tree> { TreeEntry { raw: raw as *mut raw::git_tree_entry, owned: false, @@ -164,18 +297,18 @@ impl<'tree> TreeEntry<'tree> { /// Get the filename of a tree entry pub fn name_bytes(&self) -> &[u8] { - unsafe { - ::opt_bytes(self, raw::git_tree_entry_name(&*self.raw())).unwrap() - } + unsafe { crate::opt_bytes(self, raw::git_tree_entry_name(&*self.raw())).unwrap() } } /// Convert a tree entry to the object it points to. - pub fn to_object<'a>(&self, repo: &'a Repository) - -> Result, Error> { - let mut ret = 0 as *mut raw::git_object; + pub fn to_object<'a>(&self, repo: &'a Repository) -> Result, Error> { + let mut ret = ptr::null_mut(); unsafe { - try_call!(raw::git_tree_entry_to_object(&mut ret, repo.raw(), - &*self.raw())); + try_call!(raw::git_tree_entry_to_object( + &mut ret, + repo.raw(), + &*self.raw() + )); Ok(Binding::from_raw(ret)) } } @@ -211,17 +344,19 @@ impl<'a> Binding for TreeEntry<'a> { type Raw = *mut raw::git_tree_entry; unsafe fn from_raw(raw: *mut raw::git_tree_entry) -> TreeEntry<'a> { TreeEntry { - raw: raw, + raw, owned: true, _marker: marker::PhantomData, } } - fn raw(&self) -> *mut raw::git_tree_entry { self.raw } + fn raw(&self) -> *mut raw::git_tree_entry { + self.raw + } } impl<'a> Clone for TreeEntry<'a> { fn clone(&self) -> TreeEntry<'a> { - let mut ret = 0 as *mut raw::git_tree_entry; + let mut ret = ptr::null_mut(); unsafe { assert_eq!(raw::git_tree_entry_dup(&mut ret, &*self.raw()), 0); Binding::from_raw(ret) @@ -236,11 +371,7 @@ impl<'a> PartialOrd for TreeEntry<'a> { } impl<'a> Ord for TreeEntry<'a> { fn cmp(&self, other: &TreeEntry<'a>) -> Ordering { - match unsafe { raw::git_tree_entry_cmp(&*self.raw(), &*other.raw()) } { - 0 => Ordering::Equal, - n if n < 0 => Ordering::Less, - _ => Ordering::Greater, - } + c_cmp_to_ordering(unsafe { raw::git_tree_entry_cmp(&*self.raw(), &*other.raw()) }) } } @@ -264,22 +395,29 @@ impl<'tree> Iterator for TreeIter<'tree> { fn next(&mut self) -> Option> { self.range.next().and_then(|i| self.tree.get(i)) } - fn size_hint(&self) -> (usize, Option) { self.range.size_hint() } + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() + } + fn nth(&mut self, n: usize) -> Option> { + self.range.nth(n).and_then(|i| self.tree.get(i)) + } } impl<'tree> DoubleEndedIterator for TreeIter<'tree> { fn next_back(&mut self) -> Option> { self.range.next_back().and_then(|i| self.tree.get(i)) } } +impl<'tree> FusedIterator for TreeIter<'tree> {} impl<'tree> ExactSizeIterator for TreeIter<'tree> {} #[cfg(test)] mod tests { - use {Repository,Tree,TreeEntry,ObjectType,Object}; - use tempdir::TempDir; + use super::{TreeWalkMode, TreeWalkResult}; + use crate::{Object, ObjectType, Repository, Tree, TreeEntry}; use std::fs::File; use std::io::prelude::*; use std::path::Path; + use tempfile::TempDir; pub struct TestTreeIter<'a> { entries: Vec>, @@ -289,7 +427,7 @@ mod tests { impl<'a> Iterator for TestTreeIter<'a> { type Item = TreeEntry<'a>; - fn next(&mut self) -> Option > { + fn next(&mut self) -> Option> { if self.entries.is_empty() { None } else { @@ -313,8 +451,7 @@ mod tests { } } - fn tree_iter<'repo>(tree: &Tree<'repo>, repo: &'repo Repository) - -> TestTreeIter<'repo> { + fn tree_iter<'repo>(tree: &Tree<'repo>, repo: &'repo Repository) -> TestTreeIter<'repo> { let mut initial = vec![]; for entry in tree.iter() { @@ -329,7 +466,7 @@ mod tests { #[test] fn smoke_tree_iter() { - let (td, repo) = ::test::repo_init(); + let (td, repo) = crate::test::repo_init(); setup_repo(&td, &repo); @@ -339,29 +476,61 @@ mod tests { let tree = repo.find_tree(commit.tree_id()).unwrap(); assert_eq!(tree.id(), commit.tree_id()); - assert_eq!(tree.len(), 1); + assert_eq!(tree.len(), 8); for entry in tree_iter(&tree, &repo) { println!("iter entry {:?}", entry.name()); } } + #[test] + fn smoke_tree_nth() { + let (td, repo) = crate::test::repo_init(); + + setup_repo(&td, &repo); + + let head = repo.head().unwrap(); + let target = head.target().unwrap(); + let commit = repo.find_commit(target).unwrap(); + + let tree = repo.find_tree(commit.tree_id()).unwrap(); + assert_eq!(tree.id(), commit.tree_id()); + assert_eq!(tree.len(), 8); + let mut it = tree.iter(); + let e = it.nth(4).unwrap(); + assert_eq!(e.name(), Some("f4")); + } + fn setup_repo(td: &TempDir, repo: &Repository) { let mut index = repo.index().unwrap(); - File::create(&td.path().join("foo")).unwrap().write_all(b"foo").unwrap(); - index.add_path(Path::new("foo")).unwrap(); + for n in 0..8 { + let name = format!("f{n}"); + File::create(&td.path().join(&name)) + .unwrap() + .write_all(name.as_bytes()) + .unwrap(); + index.add_path(Path::new(&name)).unwrap(); + } let id = index.write_tree().unwrap(); let sig = repo.signature().unwrap(); let tree = repo.find_tree(id).unwrap(); - let parent = repo.find_commit(repo.head().unwrap().target() - .unwrap()).unwrap(); - repo.commit(Some("HEAD"), &sig, &sig, "another commit", - &tree, &[&parent]).unwrap(); + let parent = repo + .find_commit(repo.head().unwrap().target().unwrap()) + .unwrap(); + repo.commit( + Some("HEAD"), + &sig, + &sig, + "another commit", + &tree, + &[&parent], + ) + .unwrap(); } #[test] fn smoke() { - let (td, repo) = ::test::repo_init(); + let (td, repo) = crate::test::repo_init(); setup_repo(&td, &repo); @@ -371,18 +540,78 @@ mod tests { let tree = repo.find_tree(commit.tree_id()).unwrap(); assert_eq!(tree.id(), commit.tree_id()); - assert_eq!(tree.len(), 1); + assert_eq!(tree.len(), 8); { - let e1 = tree.get(0).unwrap(); + let e0 = tree.get(0).unwrap(); + assert!(e0 == tree.get_id(e0.id()).unwrap()); + assert!(e0 == tree.get_name("f0").unwrap()); + assert!(e0 == tree.get_name_bytes(b"f0").unwrap()); + assert!(e0 == tree.get_path(Path::new("f0")).unwrap()); + assert_eq!(e0.name(), Some("f0")); + e0.to_object(&repo).unwrap(); + + let e1 = tree.get(1).unwrap(); assert!(e1 == tree.get_id(e1.id()).unwrap()); - assert!(e1 == tree.get_name("foo").unwrap()); - assert!(e1 == tree.get_path(Path::new("foo")).unwrap()); - assert_eq!(e1.name(), Some("foo")); + assert!(e1 == tree.get_name("f1").unwrap()); + assert!(e1 == tree.get_name_bytes(b"f1").unwrap()); + assert!(e1 == tree.get_path(Path::new("f1")).unwrap()); + assert_eq!(e1.name(), Some("f1")); e1.to_object(&repo).unwrap(); } tree.into_object(); - repo.find_object(commit.tree_id(), None).unwrap().as_tree().unwrap(); - repo.find_object(commit.tree_id(), None).unwrap().into_tree().ok().unwrap(); + repo.find_object(commit.tree_id(), None) + .unwrap() + .as_tree() + .unwrap(); + repo.find_object(commit.tree_id(), None) + .unwrap() + .into_tree() + .ok() + .unwrap(); + } + + #[test] + fn tree_walk() { + let (td, repo) = crate::test::repo_init(); + + setup_repo(&td, &repo); + + let head = repo.head().unwrap(); + let target = head.target().unwrap(); + let commit = repo.find_commit(target).unwrap(); + let tree = repo.find_tree(commit.tree_id()).unwrap(); + + let mut ct = 0; + tree.walk(TreeWalkMode::PreOrder, |_, entry| { + assert_eq!(entry.name(), Some(format!("f{ct}").as_str())); + ct += 1; + 0 + }) + .unwrap(); + assert_eq!(ct, 8); + + let mut ct = 0; + tree.walk(TreeWalkMode::PreOrder, |_, entry| { + assert_eq!(entry.name(), Some(format!("f{ct}").as_str())); + ct += 1; + TreeWalkResult::Ok + }) + .unwrap(); + assert_eq!(ct, 8); + } + + #[test] + fn tree_walk_error() { + let (td, repo) = crate::test::repo_init(); + + setup_repo(&td, &repo); + + let head = repo.head().unwrap(); + let target = head.target().unwrap(); + let commit = repo.find_commit(target).unwrap(); + let tree = repo.find_tree(commit.tree_id()).unwrap(); + let e = tree.walk(TreeWalkMode::PreOrder, |_, _| -1).unwrap_err(); + assert_eq!(e.class(), crate::ErrorClass::Callback); } } diff --git a/src/treebuilder.rs b/src/treebuilder.rs index f8446cdab8..1548a048cf 100644 --- a/src/treebuilder.rs +++ b/src/treebuilder.rs @@ -1,11 +1,20 @@ use std::marker; +use std::ptr; use libc::{c_int, c_void}; -use {panic, raw, tree, Error, Oid, Repository, TreeEntry}; -use util::{Binding, IntoCString}; - -/// Constructor for in-memory trees +use crate::util::{Binding, IntoCString}; +use crate::{panic, raw, tree, Error, Oid, Repository, TreeEntry}; + +/// Constructor for in-memory trees (low-level) +/// +/// You probably want to use [`build::TreeUpdateBuilder`] instead. +/// +/// This is the more raw of the two tree update facilities. It +/// handles only one level of a nested tree structure at a time. Each +/// path passed to `insert` etc. must be a single component. +/// +/// [`build::TreeUpdateBuilder`]: crate::build::TreeUpdateBuilder pub struct TreeBuilder<'repo> { raw: *mut raw::git_treebuilder, _marker: marker::PhantomData<&'repo Repository>, @@ -13,8 +22,11 @@ pub struct TreeBuilder<'repo> { impl<'repo> TreeBuilder<'repo> { /// Clear all the entries in the builder - pub fn clear(&mut self) { - unsafe { raw::git_treebuilder_clear(self.raw) } + pub fn clear(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_treebuilder_clear(self.raw)); + } + Ok(()) } /// Get the number of entries @@ -22,11 +34,17 @@ impl<'repo> TreeBuilder<'repo> { unsafe { raw::git_treebuilder_entrycount(self.raw) as usize } } + /// Return `true` if there is no entry + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Get en entry from the builder from its filename - pub fn get

(&self, filename: P) -> Result, Error> - where P: IntoCString + pub fn get

(&self, filename: P) -> Result>, Error> + where + P: IntoCString, { - let filename = try!(filename.into_c_string()); + let filename = filename.into_c_string()?; unsafe { let ret = raw::git_treebuilder_get(self.raw, filename.as_ptr()); if ret.is_null() { @@ -44,22 +62,31 @@ impl<'repo> TreeBuilder<'repo> { /// /// The mode given must be one of 0o040000, 0o100644, 0o100755, 0o120000 or /// 0o160000 currently. - pub fn insert(&mut self, filename: P, oid: Oid, - filemode: i32) -> Result { - let filename = try!(filename.into_c_string()); + pub fn insert( + &mut self, + filename: P, + oid: Oid, + filemode: i32, + ) -> Result, Error> { + let filename = filename.into_c_string()?; let filemode = filemode as raw::git_filemode_t; - let mut ret = 0 as *const raw::git_tree_entry; + let mut ret = ptr::null(); unsafe { - try_call!(raw::git_treebuilder_insert(&mut ret, self.raw, filename, - oid.raw(), filemode)); + try_call!(raw::git_treebuilder_insert( + &mut ret, + self.raw, + filename, + oid.raw(), + filemode + )); Ok(tree::entry_from_raw_const(ret)) } } /// Remove an entry from the builder by its filename pub fn remove(&mut self, filename: P) -> Result<(), Error> { - let filename = try!(filename.into_c_string()); + let filename = filename.into_c_string()?; unsafe { try_call!(raw::git_treebuilder_remove(self.raw, filename)); } @@ -70,21 +97,26 @@ impl<'repo> TreeBuilder<'repo> { /// /// Values for which the filter returns `true` will be kept. Note /// that this behavior is different from the libgit2 C interface. - pub fn filter(&mut self, mut filter: F) - where F: FnMut(&TreeEntry) -> bool + pub fn filter(&mut self, mut filter: F) -> Result<(), Error> + where + F: FnMut(&TreeEntry<'_>) -> bool, { - let mut cb: &mut FilterCb = &mut filter; + let mut cb: &mut FilterCb<'_> = &mut filter; let ptr = &mut cb as *mut _; + let cb: raw::git_treebuilder_filter_cb = Some(filter_cb); unsafe { - raw::git_treebuilder_filter(self.raw, filter_cb, ptr as *mut _); + try_call!(raw::git_treebuilder_filter(self.raw, cb, ptr as *mut _)); panic::check(); } + Ok(()) } /// Write the contents of the TreeBuilder as a Tree object and /// return its Oid pub fn write(&self) -> Result { - let mut raw = raw::git_oid { id: [0; raw::GIT_OID_RAWSZ] }; + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; unsafe { try_call!(raw::git_treebuilder_write(&mut raw, self.raw())); Ok(Binding::from_raw(&raw as *const _)) @@ -92,30 +124,38 @@ impl<'repo> TreeBuilder<'repo> { } } -type FilterCb<'a> = FnMut(&TreeEntry) -> bool + 'a; +type FilterCb<'a> = dyn FnMut(&TreeEntry<'_>) -> bool + 'a; -extern fn filter_cb(entry: *const raw::git_tree_entry, - payload: *mut c_void) -> c_int { +extern "C" fn filter_cb(entry: *const raw::git_tree_entry, payload: *mut c_void) -> c_int { let ret = panic::wrap(|| unsafe { // There's no way to return early from git_treebuilder_filter. if panic::panicked() { true } else { let entry = tree::entry_from_raw_const(entry); - let payload = payload as *mut &mut FilterCb; + let payload = payload as *mut &mut FilterCb<'_>; (*payload)(&entry) } }); - if ret == Some(false) {1} else {0} + if ret == Some(false) { + 1 + } else { + 0 + } } impl<'repo> Binding for TreeBuilder<'repo> { type Raw = *mut raw::git_treebuilder; unsafe fn from_raw(raw: *mut raw::git_treebuilder) -> TreeBuilder<'repo> { - TreeBuilder { raw: raw, _marker: marker::PhantomData } + TreeBuilder { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_treebuilder { + self.raw } - fn raw(&self) -> *mut raw::git_treebuilder { self.raw } } impl<'repo> Drop for TreeBuilder<'repo> { @@ -126,11 +166,11 @@ impl<'repo> Drop for TreeBuilder<'repo> { #[cfg(test)] mod tests { - use ObjectType; + use crate::ObjectType; #[test] fn smoke() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let mut builder = repo.treebuilder(None).unwrap(); assert_eq!(builder.len(), 0); @@ -144,13 +184,13 @@ mod tests { builder.remove("a").unwrap(); assert_eq!(builder.len(), 1); assert_eq!(builder.get("b").unwrap().unwrap().id(), blob); - builder.clear(); + builder.clear().unwrap(); assert_eq!(builder.len(), 0); } #[test] fn write() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let mut builder = repo.treebuilder(None).unwrap(); let data = repo.blob(b"data").unwrap(); @@ -169,13 +209,12 @@ mod tests { #[test] fn filter() { - let (_td, repo) = ::test::repo_init(); + let (_td, repo) = crate::test::repo_init(); let mut builder = repo.treebuilder(None).unwrap(); let blob = repo.blob(b"data").unwrap(); let tree = { - let head = repo.head().unwrap() - .peel(ObjectType::Commit).unwrap(); + let head = repo.head().unwrap().peel(ObjectType::Commit).unwrap(); let head = head.as_commit().unwrap(); head.tree_id() }; @@ -183,11 +222,13 @@ mod tests { builder.insert("dir", tree, 0o040000).unwrap(); builder.insert("dir2", tree, 0o040000).unwrap(); - builder.filter(|_| true); + builder.filter(|_| true).unwrap(); assert_eq!(builder.len(), 3); - builder.filter(|e| e.kind().unwrap() != ObjectType::Blob); + builder + .filter(|e| e.kind().unwrap() != ObjectType::Blob) + .unwrap(); assert_eq!(builder.len(), 2); - builder.filter(|_| false); + builder.filter(|_| false).unwrap(); assert_eq!(builder.len(), 0); } } diff --git a/src/util.rs b/src/util.rs index 91dd9b1de2..1315f0bd7a 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,9 +1,9 @@ +use libc::{c_char, c_int, size_t}; +use std::cmp::Ordering; use std::ffi::{CString, OsStr, OsString}; -use std::iter::IntoIterator; -use std::path::{Path, PathBuf}; -use libc::{c_char, size_t}; +use std::path::{Component, Path, PathBuf}; -use {raw, Error}; +use crate::{raw, Error}; #[doc(hidden)] pub trait IsNull { @@ -20,15 +20,31 @@ impl IsNull for *mut T { } } -#[doc(hidden)] +/// Provides access to the raw libgit2 pointer to be able to interact with libgit2-sys. +/// +/// If you are going to depend on this trait on your code, do consider contributing to the git2 +/// project to add the missing capabilities to git2. pub trait Binding: Sized { + /// The raw type that allows you to interact with libgit2-sys. type Raw; + /// Build a git2 struct from its [Binding::Raw] value. unsafe fn from_raw(raw: Self::Raw) -> Self; + + /// Access the [Binding::Raw] value for a struct. + /// + /// The returned value is only safe to use while its associated git2 struct is in scope. + /// Once the associated git2 struct is destroyed, the raw value can point to an invalid memory address. fn raw(&self) -> Self::Raw; + /// A null-handling version of [Binding::from_raw]. + /// + /// If the input parameter is null, then the funtion returns None. Otherwise, it + /// calls [Binding::from_raw]. unsafe fn from_raw_opt(raw: T) -> Option - where T: Copy + IsNull, Self: Binding + where + T: Copy + IsNull, + Self: Binding, { if raw.is_ptr_null() { None @@ -38,11 +54,40 @@ pub trait Binding: Sized { } } -pub fn iter2cstrs(iter: I) -> Result<(Vec, Vec<*const c_char>, - raw::git_strarray), Error> - where T: IntoCString, I: IntoIterator +/// Converts an iterator of repo paths into a git2-compatible array of cstrings. +/// +/// Only use this for repo-relative paths or pathspecs. +/// +/// See `iter2cstrs` for more details. +pub fn iter2cstrs_paths( + iter: I, +) -> Result<(Vec, Vec<*const c_char>, raw::git_strarray), Error> +where + T: IntoCString, + I: IntoIterator, +{ + let cstrs = iter + .into_iter() + .map(|i| fixup_windows_path(i.into_c_string()?)) + .collect::, _>>()?; + iter2cstrs(cstrs) +} + +/// Converts an iterator of things into a git array of c-strings. +/// +/// Returns a tuple `(cstrings, pointers, git_strarray)`. The first two values +/// should not be dropped before `git_strarray`. +pub fn iter2cstrs( + iter: I, +) -> Result<(Vec, Vec<*const c_char>, raw::git_strarray), Error> +where + T: IntoCString, + I: IntoIterator, { - let cstrs: Vec<_> = try!(iter.into_iter().map(|i| i.into_c_string()).collect()); + let cstrs = iter + .into_iter() + .map(|i| i.into_c_string()) + .collect::, _>>()?; let ptrs = cstrs.iter().map(|i| i.as_ptr()).collect::>(); let raw = raw::git_strarray { strings: ptrs.as_ptr() as *mut _, @@ -79,18 +124,20 @@ impl<'a, T: IntoCString + Clone> IntoCString for &'a T { impl<'a> IntoCString for &'a str { fn into_c_string(self) -> Result { - Ok(try!(CString::new(self))) + Ok(CString::new(self)?) } } impl IntoCString for String { fn into_c_string(self) -> Result { - Ok(try!(CString::new(self.into_bytes()))) + Ok(CString::new(self.into_bytes())?) } } impl IntoCString for CString { - fn into_c_string(self) -> Result { Ok(self) } + fn into_c_string(self) -> Result { + Ok(self) + } } impl<'a> IntoCString for &'a Path { @@ -118,26 +165,191 @@ impl IntoCString for OsString { fn into_c_string(self) -> Result { use std::os::unix::prelude::*; let s: &OsStr = self.as_ref(); - Ok(try!(CString::new(s.as_bytes()))) + Ok(CString::new(s.as_bytes())?) } #[cfg(windows)] fn into_c_string(self) -> Result { match self.to_str() { Some(s) => s.into_c_string(), - None => Err(Error::from_str("only valid unicode paths are accepted \ - on windows")), + None => Err(Error::from_str( + "only valid unicode paths are accepted on windows", + )), } } } impl<'a> IntoCString for &'a [u8] { fn into_c_string(self) -> Result { - Ok(try!(CString::new(self))) + Ok(CString::new(self)?) } } impl IntoCString for Vec { fn into_c_string(self) -> Result { - Ok(try!(CString::new(self))) + Ok(CString::new(self)?) + } +} + +pub fn into_opt_c_string(opt_s: Option) -> Result, Error> +where + S: IntoCString, +{ + match opt_s { + None => Ok(None), + Some(s) => Ok(Some(s.into_c_string()?)), + } +} + +pub fn c_cmp_to_ordering(cmp: c_int) -> Ordering { + match cmp { + 0 => Ordering::Equal, + n if n < 0 => Ordering::Less, + _ => Ordering::Greater, + } +} + +/// Converts a path to a CString that is usable by the libgit2 API. +/// +/// Checks if it is a relative path. +/// +/// On Windows, this also requires the path to be valid Unicode, and translates +/// back slashes to forward slashes. +pub fn path_to_repo_path(path: &Path) -> Result { + macro_rules! err { + ($msg:literal, $path:expr) => { + return Err(Error::from_str(&format!($msg, $path.display()))) + }; + } + match path.components().next() { + None => return Err(Error::from_str("repo path should not be empty")), + Some(Component::Prefix(_)) => err!( + "repo path `{}` should be relative, not a windows prefix", + path + ), + Some(Component::RootDir) => err!("repo path `{}` should be relative", path), + Some(Component::CurDir) => err!("repo path `{}` should not start with `.`", path), + Some(Component::ParentDir) => err!("repo path `{}` should not start with `..`", path), + Some(Component::Normal(_)) => {} + } + #[cfg(windows)] + { + match path.to_str() { + None => { + return Err(Error::from_str( + "only valid unicode paths are accepted on windows", + )) + } + Some(s) => return fixup_windows_path(s), + } + } + #[cfg(not(windows))] + { + path.into_c_string() + } +} + +pub fn cstring_to_repo_path(path: T) -> Result { + fixup_windows_path(path.into_c_string()?) +} + +#[cfg(windows)] +fn fixup_windows_path>>(path: P) -> Result { + let mut bytes: Vec = path.into(); + for i in 0..bytes.len() { + if bytes[i] == b'\\' { + bytes[i] = b'/'; + } + } + Ok(CString::new(bytes)?) +} + +#[cfg(not(windows))] +fn fixup_windows_path(path: CString) -> Result { + Ok(path) +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! assert_err { + ($path:expr, $msg:expr) => { + match path_to_repo_path(Path::new($path)) { + Ok(_) => panic!("expected `{}` to err", $path), + Err(e) => assert_eq!(e.message(), $msg), + } + }; + } + + macro_rules! assert_repo_path_ok { + ($path:expr) => { + assert_repo_path_ok!($path, $path) + }; + ($path:expr, $expect:expr) => { + assert_eq!( + path_to_repo_path(Path::new($path)), + Ok(CString::new($expect).unwrap()) + ); + }; + } + + #[test] + #[cfg(windows)] + fn path_to_repo_path_translate() { + assert_repo_path_ok!("foo"); + assert_repo_path_ok!("foo/bar"); + assert_repo_path_ok!(r"foo\bar", "foo/bar"); + assert_repo_path_ok!(r"foo\bar\", "foo/bar/"); + } + + #[test] + fn path_to_repo_path_no_weird() { + assert_err!("", "repo path should not be empty"); + assert_err!("./foo", "repo path `./foo` should not start with `.`"); + assert_err!("../foo", "repo path `../foo` should not start with `..`"); + } + + #[test] + #[cfg(not(windows))] + fn path_to_repo_path_no_absolute() { + assert_err!("/", "repo path `/` should be relative"); + assert_repo_path_ok!("foo/bar"); + } + + #[test] + #[cfg(windows)] + fn path_to_repo_path_no_absolute() { + assert_err!( + r"c:", + r"repo path `c:` should be relative, not a windows prefix" + ); + assert_err!( + r"c:\", + r"repo path `c:\` should be relative, not a windows prefix" + ); + assert_err!( + r"c:temp", + r"repo path `c:temp` should be relative, not a windows prefix" + ); + assert_err!( + r"\\?\UNC\a\b\c", + r"repo path `\\?\UNC\a\b\c` should be relative, not a windows prefix" + ); + assert_err!( + r"\\?\c:\foo", + r"repo path `\\?\c:\foo` should be relative, not a windows prefix" + ); + assert_err!( + r"\\.\COM42", + r"repo path `\\.\COM42` should be relative, not a windows prefix" + ); + assert_err!( + r"\\a\b", + r"repo path `\\a\b` should be relative, not a windows prefix" + ); + assert_err!(r"\", r"repo path `\` should be relative"); + assert_err!(r"/", r"repo path `/` should be relative"); + assert_err!(r"\foo", r"repo path `\foo` should be relative"); + assert_err!(r"/foo", r"repo path `/foo` should be relative"); } } diff --git a/src/version.rs b/src/version.rs new file mode 100644 index 0000000000..b5dd4fb123 --- /dev/null +++ b/src/version.rs @@ -0,0 +1,95 @@ +use crate::raw; +use libc::c_int; +use std::fmt; + +/// Version information about libgit2 and the capabilities it supports. +pub struct Version { + major: c_int, + minor: c_int, + rev: c_int, + features: c_int, +} + +macro_rules! flag_test { + ($features:expr, $flag:expr) => { + ($features as u32 & $flag as u32) != 0 + }; +} + +impl Version { + /// Returns a [`Version`] which provides information about libgit2. + pub fn get() -> Version { + let mut v = Version { + major: 0, + minor: 0, + rev: 0, + features: 0, + }; + unsafe { + raw::git_libgit2_version(&mut v.major, &mut v.minor, &mut v.rev); + v.features = raw::git_libgit2_features(); + } + v + } + + /// Returns the version of libgit2. + /// + /// The return value is a tuple of `(major, minor, rev)` + pub fn libgit2_version(&self) -> (u32, u32, u32) { + (self.major as u32, self.minor as u32, self.rev as u32) + } + + /// Returns the version of the libgit2-sys crate. + pub fn crate_version(&self) -> &'static str { + env!("CARGO_PKG_VERSION") + } + + /// Returns true if this was built with the vendored version of libgit2. + pub fn vendored(&self) -> bool { + raw::vendored() + } + + /// Returns true if libgit2 was built thread-aware and can be safely used + /// from multiple threads. + pub fn threads(&self) -> bool { + flag_test!(self.features, raw::GIT_FEATURE_THREADS) + } + + /// Returns true if libgit2 was built with and linked against a TLS implementation. + /// + /// Custom TLS streams may still be added by the user to support HTTPS + /// regardless of this. + pub fn https(&self) -> bool { + flag_test!(self.features, raw::GIT_FEATURE_HTTPS) + } + + /// Returns true if libgit2 was built with and linked against libssh2. + /// + /// A custom transport may still be added by the user to support libssh2 + /// regardless of this. + pub fn ssh(&self) -> bool { + flag_test!(self.features, raw::GIT_FEATURE_SSH) + } + + /// Returns true if libgit2 was built with support for sub-second + /// resolution in file modification times. + pub fn nsec(&self) -> bool { + flag_test!(self.features, raw::GIT_FEATURE_NSEC) + } +} + +impl fmt::Debug for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + let mut f = f.debug_struct("Version"); + f.field("major", &self.major) + .field("minor", &self.minor) + .field("rev", &self.rev) + .field("crate_version", &self.crate_version()) + .field("vendored", &self.vendored()) + .field("threads", &self.threads()) + .field("https", &self.https()) + .field("ssh", &self.ssh()) + .field("nsec", &self.nsec()); + f.finish() + } +} diff --git a/src/worktree.rs b/src/worktree.rs new file mode 100644 index 0000000000..fc32902db1 --- /dev/null +++ b/src/worktree.rs @@ -0,0 +1,337 @@ +use crate::buf::Buf; +use crate::reference::Reference; +use crate::repo::Repository; +use crate::util::{self, Binding}; +use crate::{raw, Error}; +use std::os::raw::c_int; +use std::path::Path; +use std::ptr; +use std::str; +use std::{marker, mem}; + +/// An owned git worktree +/// +/// This structure corresponds to a `git_worktree` in libgit2. +// +pub struct Worktree { + raw: *mut raw::git_worktree, +} + +/// Options which can be used to configure how a worktree is initialized +pub struct WorktreeAddOptions<'a> { + raw: raw::git_worktree_add_options, + _marker: marker::PhantomData>, +} + +/// Options to configure how worktree pruning is performed +pub struct WorktreePruneOptions { + raw: raw::git_worktree_prune_options, +} + +/// Lock Status of a worktree +#[derive(PartialEq, Debug)] +pub enum WorktreeLockStatus { + /// Worktree is Unlocked + Unlocked, + /// Worktree is locked with the optional message + Locked(Option), +} + +impl Worktree { + /// Open a worktree of a the repository + /// + /// If a repository is not the main tree but a worktree, this + /// function will look up the worktree inside the parent + /// repository and create a new `git_worktree` structure. + pub fn open_from_repository(repo: &Repository) -> Result { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_worktree_open_from_repository(&mut raw, repo.raw())); + Ok(Binding::from_raw(raw)) + } + } + + /// Retrieves the name of the worktree + /// + /// This is the name that can be passed to repo::Repository::find_worktree + /// to reopen the worktree. This is also the name that would appear in the + /// list returned by repo::Repository::worktrees + pub fn name(&self) -> Option<&str> { + unsafe { + crate::opt_bytes(self, raw::git_worktree_name(self.raw)) + .and_then(|s| str::from_utf8(s).ok()) + } + } + + /// Retrieves the path to the worktree + /// + /// This is the path to the top-level of the source and not the path to the + /// .git file within the worktree. This path can be passed to + /// repo::Repository::open. + pub fn path(&self) -> &Path { + unsafe { + util::bytes2path(crate::opt_bytes(self, raw::git_worktree_path(self.raw)).unwrap()) + } + } + + /// Validates the worktree + /// + /// This checks that it still exists on the + /// filesystem and that the metadata is correct + pub fn validate(&self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_worktree_validate(self.raw)); + } + Ok(()) + } + + /// Locks the worktree + pub fn lock(&self, reason: Option<&str>) -> Result<(), Error> { + let reason = crate::opt_cstr(reason)?; + unsafe { + try_call!(raw::git_worktree_lock(self.raw, reason)); + } + Ok(()) + } + + /// Unlocks the worktree + pub fn unlock(&self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_worktree_unlock(self.raw)); + } + Ok(()) + } + + /// Checks if worktree is locked + pub fn is_locked(&self) -> Result { + let buf = Buf::new(); + unsafe { + match try_call!(raw::git_worktree_is_locked(buf.raw(), self.raw)) { + 0 => Ok(WorktreeLockStatus::Unlocked), + _ => { + let v = buf.to_vec(); + Ok(WorktreeLockStatus::Locked(match v.len() { + 0 => None, + _ => Some(String::from_utf8(v).unwrap()), + })) + } + } + } + } + + /// Prunes the worktree + pub fn prune(&self, opts: Option<&mut WorktreePruneOptions>) -> Result<(), Error> { + // When successful the worktree should be removed however the backing structure + // of the git_worktree should still be valid. + unsafe { + try_call!(raw::git_worktree_prune(self.raw, opts.map(|o| o.raw()))); + } + Ok(()) + } + + /// Checks if the worktree is prunable + pub fn is_prunable(&self, opts: Option<&mut WorktreePruneOptions>) -> Result { + unsafe { + let rv = try_call!(raw::git_worktree_is_prunable( + self.raw, + opts.map(|o| o.raw()) + )); + Ok(rv != 0) + } + } +} + +impl<'a> WorktreeAddOptions<'a> { + /// Creates a default set of add options. + /// + /// By default this will not lock the worktree + pub fn new() -> WorktreeAddOptions<'a> { + unsafe { + let mut raw = mem::zeroed(); + assert_eq!( + raw::git_worktree_add_options_init(&mut raw, raw::GIT_WORKTREE_ADD_OPTIONS_VERSION), + 0 + ); + WorktreeAddOptions { + raw, + _marker: marker::PhantomData, + } + } + } + + /// If enabled, this will cause the newly added worktree to be locked + pub fn lock(&mut self, enabled: bool) -> &mut WorktreeAddOptions<'a> { + self.raw.lock = enabled as c_int; + self + } + + /// If enabled, this will checkout the existing branch matching the worktree name. + pub fn checkout_existing(&mut self, enabled: bool) -> &mut WorktreeAddOptions<'a> { + self.raw.checkout_existing = enabled as c_int; + self + } + + /// reference to use for the new worktree HEAD + pub fn reference( + &mut self, + reference: Option<&'a Reference<'_>>, + ) -> &mut WorktreeAddOptions<'a> { + self.raw.reference = if let Some(reference) = reference { + reference.raw() + } else { + ptr::null_mut() + }; + self + } + + /// Get a set of raw add options to be used with `git_worktree_add` + pub fn raw(&self) -> *const raw::git_worktree_add_options { + &self.raw + } +} + +impl WorktreePruneOptions { + /// Creates a default set of pruning options + /// + /// By defaults this will prune only worktrees that are no longer valid + /// unlocked and not checked out + pub fn new() -> WorktreePruneOptions { + unsafe { + let mut raw = mem::zeroed(); + assert_eq!( + raw::git_worktree_prune_options_init( + &mut raw, + raw::GIT_WORKTREE_PRUNE_OPTIONS_VERSION + ), + 0 + ); + WorktreePruneOptions { raw } + } + } + + /// Controls whether valid (still existing on the filesystem) worktrees + /// will be pruned + /// + /// Defaults to false + pub fn valid(&mut self, valid: bool) -> &mut WorktreePruneOptions { + self.flag(raw::GIT_WORKTREE_PRUNE_VALID, valid) + } + + /// Controls whether locked worktrees will be pruned + /// + /// Defaults to false + pub fn locked(&mut self, locked: bool) -> &mut WorktreePruneOptions { + self.flag(raw::GIT_WORKTREE_PRUNE_LOCKED, locked) + } + + /// Controls whether the actual working tree on the filesystem is recursively removed + /// + /// Defaults to false + pub fn working_tree(&mut self, working_tree: bool) -> &mut WorktreePruneOptions { + self.flag(raw::GIT_WORKTREE_PRUNE_WORKING_TREE, working_tree) + } + + fn flag(&mut self, flag: raw::git_worktree_prune_t, on: bool) -> &mut WorktreePruneOptions { + if on { + self.raw.flags |= flag as u32; + } else { + self.raw.flags &= !(flag as u32); + } + self + } + + /// Get a set of raw prune options to be used with `git_worktree_prune` + pub fn raw(&mut self) -> *mut raw::git_worktree_prune_options { + &mut self.raw + } +} + +impl Binding for Worktree { + type Raw = *mut raw::git_worktree; + unsafe fn from_raw(ptr: *mut raw::git_worktree) -> Worktree { + Worktree { raw: ptr } + } + fn raw(&self) -> *mut raw::git_worktree { + self.raw + } +} + +impl Drop for Worktree { + fn drop(&mut self) { + unsafe { raw::git_worktree_free(self.raw) } + } +} + +#[cfg(test)] +mod tests { + use crate::WorktreeAddOptions; + use crate::WorktreeLockStatus; + + use tempfile::TempDir; + + #[test] + fn smoke_add_no_ref() { + let (_td, repo) = crate::test::repo_init(); + + let wtdir = TempDir::new().unwrap(); + let wt_path = wtdir.path().join("tree-no-ref-dir"); + let opts = WorktreeAddOptions::new(); + + let wt = repo.worktree("tree-no-ref", &wt_path, Some(&opts)).unwrap(); + assert_eq!(wt.name(), Some("tree-no-ref")); + assert_eq!( + wt.path().canonicalize().unwrap(), + wt_path.canonicalize().unwrap() + ); + let status = wt.is_locked().unwrap(); + assert_eq!(status, WorktreeLockStatus::Unlocked); + } + + #[test] + fn smoke_add_locked() { + let (_td, repo) = crate::test::repo_init(); + + let wtdir = TempDir::new().unwrap(); + let wt_path = wtdir.path().join("locked-tree"); + let mut opts = WorktreeAddOptions::new(); + opts.lock(true); + + let wt = repo.worktree("locked-tree", &wt_path, Some(&opts)).unwrap(); + // shouldn't be able to lock a worktree that was created locked + assert!(wt.lock(Some("my reason")).is_err()); + assert_eq!(wt.name(), Some("locked-tree")); + assert_eq!( + wt.path().canonicalize().unwrap(), + wt_path.canonicalize().unwrap() + ); + assert_eq!(wt.is_locked().unwrap(), WorktreeLockStatus::Locked(None)); + assert!(wt.unlock().is_ok()); + assert!(wt.lock(Some("my reason")).is_ok()); + assert_eq!( + wt.is_locked().unwrap(), + WorktreeLockStatus::Locked(Some("my reason".to_string())) + ); + } + + #[test] + fn smoke_add_from_branch() { + let (_td, repo) = crate::test::repo_init(); + + let (wt_top, branch) = crate::test::worktrees_env_init(&repo); + let wt_path = wt_top.path().join("test"); + let mut opts = WorktreeAddOptions::new(); + let reference = branch.into_reference(); + opts.reference(Some(&reference)); + + let wt = repo + .worktree("test-worktree", &wt_path, Some(&opts)) + .unwrap(); + assert_eq!(wt.name(), Some("test-worktree")); + assert_eq!( + wt.path().canonicalize().unwrap(), + wt_path.canonicalize().unwrap() + ); + let status = wt.is_locked().unwrap(); + assert_eq!(status, WorktreeLockStatus::Unlocked); + } +} diff --git a/systest/Cargo.toml b/systest/Cargo.toml index 22b85073c1..708cda4303 100644 --- a/systest/Cargo.toml +++ b/systest/Cargo.toml @@ -3,10 +3,11 @@ name = "systest" version = "0.1.0" authors = ["Alex Crichton "] build = "build.rs" +edition = "2021" [dependencies] -libgit2-sys = { path = "../libgit2-sys" } +libgit2-sys = { path = "../libgit2-sys", features = ['https', 'ssh'] } libc = "0.2" [build-dependencies] -ctest = { git = "https://github.com/alexcrichton/ctest" } +ctest2 = "0.4" diff --git a/systest/build.rs b/systest/build.rs index e68803897b..9503af7e10 100644 --- a/systest/build.rs +++ b/systest/build.rs @@ -1,34 +1,46 @@ -extern crate ctest; - use std::env; use std::path::PathBuf; fn main() { - let root = PathBuf::from(env::var_os("DEP_GIT2_ROOT").unwrap()); - - let mut cfg = ctest::TestGenerator::new(); + let mut cfg = ctest2::TestGenerator::new(); + if let Some(root) = env::var_os("DEP_GIT2_ROOT") { + cfg.include(PathBuf::from(root).join("include")); + } cfg.header("git2.h") - .header("git2/sys/transport.h") - .header("git2/sys/repository.h") - .header("git2/cred_helpers.h") - .include(root.join("include")) - .type_name(|s, _| s.to_string()); - cfg.field_name(|_, f| { - match f { - "kind" => "type".to_string(), - _ => f.to_string(), - } + .header("git2/sys/errors.h") + .header("git2/sys/transport.h") + .header("git2/sys/refs.h") + .header("git2/sys/refdb_backend.h") + .header("git2/sys/odb_backend.h") + .header("git2/sys/mempack.h") + .header("git2/sys/repository.h") + .header("git2/sys/cred.h") + .header("git2/sys/email.h") + .header("git2/cred_helpers.h") + .type_name(|s, _, _| s.to_string()); + cfg.field_name(|_, f| match f { + "kind" => "type".to_string(), + _ => f.to_string(), + }); + cfg.skip_field(|struct_, f| { + // this field is marked as const which ctest complains about + (struct_ == "git_rebase_operation" && f == "id") || + // the real name of this field is ref but that is a reserved keyword + (struct_ == "git_worktree_add_options" && f == "reference") }); - cfg.skip_signededness(|s| { - match s { - s if s.ends_with("_cb") => true, - s if s.ends_with("_callback") => true, - "git_push_transfer_progress" | - "git_push_negotiation" | - "git_packbuilder_progress" => true, - _ => false, - } + cfg.skip_signededness(|s| match s { + s if s.ends_with("_cb") => true, + s if s.ends_with("_callback") => true, + "git_push_transfer_progress" | "git_push_negotiation" | "git_packbuilder_progress" => true, + _ => false, }); + + // GIT_FILEMODE_BLOB_GROUP_WRITABLE is not a public const in libgit2 + cfg.define("GIT_FILEMODE_BLOB_GROUP_WRITABLE", Some("0100664")); + + // not entirely sure why this is failing... + cfg.skip_roundtrip(|t| t == "git_clone_options" || t == "git_submodule_update_options"); + cfg.skip_type(|t| t == "__enum_ty"); cfg.generate("../libgit2-sys/lib.rs", "all.rs"); } diff --git a/systest/src/main.rs b/systest/src/main.rs index af7dd925e3..6193730dd8 100644 --- a/systest/src/main.rs +++ b/systest/src/main.rs @@ -1,8 +1,5 @@ #![allow(bad_style, improper_ctypes)] -extern crate libgit2_sys; -extern crate libc; - use libc::*; use libgit2_sys::*; diff --git a/tests/add_extensions.rs b/tests/add_extensions.rs new file mode 100644 index 0000000000..d49c33cf79 --- /dev/null +++ b/tests/add_extensions.rs @@ -0,0 +1,30 @@ +//! Test for `set_extensions`, which writes a global state maintained by libgit2 + +use git2::opts::{get_extensions, set_extensions}; +use git2::Error; + +#[test] +fn test_add_extensions() -> Result<(), Error> { + unsafe { + set_extensions(&["custom"])?; + } + + let extensions = unsafe { get_extensions() }?; + let extensions: Vec<_> = extensions.iter().collect(); + + assert_eq!( + extensions, + [ + Some("custom"), + Some("noop"), + // The objectformat extension was added in 1.6 + Some("objectformat"), + // The preciousobjects extension was added in 1.9 + Some("preciousobjects"), + // The worktreeconfig extension was added in 1.8 + Some("worktreeconfig") + ] + ); + + Ok(()) +} diff --git a/tests/get_extensions.rs b/tests/get_extensions.rs new file mode 100644 index 0000000000..2ac362d0ba --- /dev/null +++ b/tests/get_extensions.rs @@ -0,0 +1,25 @@ +//! Test for `get_extensions`, which reads a global state maintained by libgit2 + +use git2::opts::get_extensions; +use git2::Error; + +#[test] +fn test_get_extensions() -> Result<(), Error> { + let extensions = unsafe { get_extensions() }?; + let extensions: Vec<_> = extensions.iter().collect(); + + assert_eq!( + extensions, + [ + Some("noop"), + // The objectformat extension was added in 1.6 + Some("objectformat"), + // The preciousobjects extension was added in 1.9 + Some("preciousobjects"), + // The worktreeconfig extension was added in 1.8 + Some("worktreeconfig") + ] + ); + + Ok(()) +} diff --git a/tests/global_state.rs b/tests/global_state.rs new file mode 100644 index 0000000000..192acdbd3a --- /dev/null +++ b/tests/global_state.rs @@ -0,0 +1,47 @@ +//! Test for some global state set up by libgit2's `git_libgit2_init` function +//! that need to be synchronized within a single process. + +use git2::opts; +use git2::{ConfigLevel, IntoCString}; + +// Test for mutating configuration file search path which is set during +// initialization in libgit2's `git_sysdir_global_init` function. +#[test] +fn search_path() -> Result<(), Box> { + use std::env::join_paths; + + let path = "fake_path"; + let original = unsafe { opts::get_search_path(ConfigLevel::Global) }; + assert_ne!(original, Ok(path.into_c_string()?)); + + // Set + unsafe { + opts::set_search_path(ConfigLevel::Global, &path)?; + } + assert_eq!( + unsafe { opts::get_search_path(ConfigLevel::Global) }, + Ok(path.into_c_string()?) + ); + + // Append + let paths = join_paths(["$PATH", path].iter())?; + let expected_paths = join_paths([path, path].iter())?.into_c_string()?; + unsafe { + opts::set_search_path(ConfigLevel::Global, paths)?; + } + assert_eq!( + unsafe { opts::get_search_path(ConfigLevel::Global) }, + Ok(expected_paths) + ); + + // Reset + unsafe { + opts::reset_search_path(ConfigLevel::Global)?; + } + assert_eq!( + unsafe { opts::get_search_path(ConfigLevel::Global) }, + original + ); + + Ok(()) +} diff --git a/tests/remove_extensions.rs b/tests/remove_extensions.rs new file mode 100644 index 0000000000..3e54b427b7 --- /dev/null +++ b/tests/remove_extensions.rs @@ -0,0 +1,26 @@ +//! Test for `set_extensions`, which writes a global state maintained by libgit2 + +use git2::opts::{get_extensions, set_extensions}; +use git2::Error; + +#[test] +fn test_remove_extensions() -> Result<(), Error> { + unsafe { + set_extensions(&[ + "custom", + "!ignore", + "!noop", + "!objectformat", + "!preciousobjects", + "!worktreeconfig", + "other", + ])?; + } + + let extensions = unsafe { get_extensions() }?; + let extensions: Vec<_> = extensions.iter().collect(); + + assert_eq!(extensions, [Some("custom"), Some("other")]); + + Ok(()) +} diff --git a/triagebot.toml b/triagebot.toml new file mode 100644 index 0000000000..253fdd0bcd --- /dev/null +++ b/triagebot.toml @@ -0,0 +1,26 @@ +[relabel] +allow-unauthenticated = [ + "*", +] + +[assign] + +[shortcut] + +[transfer] + +[merge-conflicts] +remove = [] +add = ["S-waiting-on-author"] +unless = ["S-blocked", "S-waiting-on-review"] + +[autolabel."S-waiting-on-review"] +new_pr = true + +[review-submitted] +reviewed_label = "S-waiting-on-author" +review_labels = ["S-waiting-on-review"] + +[review-requested] +remove_labels = ["S-waiting-on-author"] +add_labels = ["S-waiting-on-review"]