diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 567a830852..98ed1523a4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,5 +1,7 @@ name: CI -on: [push, pull_request] +on: + pull_request: + merge_group: jobs: test: @@ -26,13 +28,15 @@ jobs: 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 --no-default-features - - run: cargo test - - run: cargo run --manifest-path systest/Cargo.toml - - run: cargo test --manifest-path git2-curl/Cargo.toml + - run: cargo test --locked + - run: cargo test --features https,ssh + - run: cargo run -p systest + - run: cargo test -p git2-curl rustfmt: name: Rustfmt @@ -42,3 +46,20 @@ jobs: - 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/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 1182647a9a..30a496deaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "git2" -version = "0.13.18" +version = "0.21.0" authors = ["Josh Triplett ", "Alex Crichton "] -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" readme = "README.md" keywords = ["git"] repository = "https://github.com/rust-lang/git2-rs" @@ -13,34 +13,41 @@ both threadsafe and memory safe and allows both reading and writing git repositories. """ categories = ["api-bindings"] -edition = "2018" +edition = "2021" [dependencies] -url = "2.0" -bitflags = "1.1.0" +url = { version = "2.5.4", optional = true } +bitflags = "2.1.0" libc = "0.2" log = "0.4.8" -libgit2-sys = { path = "libgit2-sys", version = "0.12.19" } +libgit2-sys = { path = "libgit2-sys", version = "0.18.1" } [target."cfg(all(unix, not(target_os = \"macos\")))".dependencies] -openssl-sys = { version = "0.9.0", optional = true } +openssl-sys = { version = "0.9.45", optional = true } openssl-probe = { version = "0.1", optional = true } [dev-dependencies] -structopt = "0.3" -time = "0.1.39" +clap = { version = "4.4.13", features = ["derive"] } +time = { version = "0.3.37", features = ["formatting"] } tempfile = "3.1.0" -thread-id = "3.3.0" # remove when we work with minimal-versions without it -paste = "1" +url = "2.5.4" [features] unstable = [] -default = ["ssh", "https", "ssh_key_from_memory"] -ssh = ["libgit2-sys/ssh"] -https = ["libgit2-sys/https", "openssl-sys", "openssl-probe"] -vendored-openssl = ["openssl-sys/vendored"] -ssh_key_from_memory = ["libgit2-sys/ssh_key_from_memory"] +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 15e5b6d3f6..516a087a6e 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,45 @@ [Documentation](https://docs.rs/git2) -libgit2 bindings for Rust +libgit2 bindings for Rust. -```toml -[dependencies] -git2 = "0.13" +``` +cargo add git2 +``` + +## Features + +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 ``` ## Rust version requirements git2-rs works with stable Rust, and typically works with the most recent prior -stable release as well. Check the MSRV job of [the CI script](.github/workflows/main.yml) to see the oldest -version of Rust known to pass tests. +stable release as well. ## Version of libgit2 -Currently this library requires libgit2 1.1.0. 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. +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. + +In particular, note that the environment variable overrides the Cargo feature. ## Building git2-rs @@ -38,23 +59,22 @@ 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`. - To skip tests on a simple commit or doc-fixes, use `git commit --no-verify`. -## Building on OSX 10.10+ +## Building on macOS 10.10+ -If the `ssh` feature is enabled (and it is by default) then this library depends +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#macos). +[`openssl` crate's instructions](https://github.com/sfackler/rust-openssl/blob/master/openssl/src/lib.rs#L31). # License This project is licensed under either of * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or - http://www.apache.org/licenses/LICENSE-2.0) + https://www.apache.org/licenses/LICENSE-2.0) * MIT license ([LICENSE-MIT](LICENSE-MIT) or - http://opensource.org/licenses/MIT) + https://opensource.org/licenses/MIT) at your option. 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 25e972c7aa..57c9bb10a9 100644 --- a/examples/add.rs +++ b/examples/add.rs @@ -15,15 +15,15 @@ #![deny(warnings)] #![allow(trivial_casts)] +use clap::Parser; use git2::Repository; use std::path::Path; -use structopt::StructOpt; -#[derive(StructOpt)] +#[derive(Parser)] struct Args { #[structopt(name = "spec")] arg_spec: Vec, - #[structopt(name = "dry_run", short = "n", long)] + #[structopt(name = "dry_run", short = 'n', long)] /// dry run flag_dry_run: bool, #[structopt(name = "verbose", short, long)] @@ -73,7 +73,7 @@ fn run(args: &Args) -> Result<(), git2::Error> { } fn main() { - let args = Args::from_args(); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/examples/blame.rs b/examples/blame.rs index 7cb1b69470..202989b3f0 100644 --- a/examples/blame.rs +++ b/examples/blame.rs @@ -14,25 +14,25 @@ #![deny(warnings)] +use clap::Parser; use git2::{BlameOptions, Repository}; use std::io::{BufRead, BufReader}; use std::path::Path; -use structopt::StructOpt; -#[derive(StructOpt)] +#[derive(Parser)] #[allow(non_snake_case)] struct Args { #[structopt(name = "path")] arg_path: String, #[structopt(name = "spec")] arg_spec: Option, - #[structopt(short = "M")] + #[structopt(short = 'M')] /// find line moves within and across files flag_M: bool, - #[structopt(short = "C")] + #[structopt(short = 'C')] /// find line copies within and across files flag_C: bool, - #[structopt(short = "F")] + #[structopt(short = 'F')] /// follow only the first parent commits flag_F: bool, } @@ -96,7 +96,7 @@ fn run(args: &Args) -> Result<(), git2::Error> { } fn main() { - let args = Args::from_args(); + 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 0ce21b34aa..6196b9bb9f 100644 --- a/examples/cat-file.rs +++ b/examples/cat-file.rs @@ -16,23 +16,23 @@ use std::io::{self, Write}; +use clap::Parser; use git2::{Blob, Commit, ObjectType, Repository, Signature, Tag, Tree}; -use structopt::StructOpt; -#[derive(StructOpt)] +#[derive(Parser)] struct Args { #[structopt(name = "object")] arg_object: String, - #[structopt(short = "t")] + #[structopt(short = 't')] /// show the object type flag_t: bool, - #[structopt(short = "s")] + #[structopt(short = 's')] /// show the object size flag_s: bool, - #[structopt(short = "e")] + #[structopt(short = 'e')] /// suppress all output flag_e: bool, - #[structopt(short = "p")] + #[structopt(short = 'p')] /// pretty print the contents of the object flag_p: bool, #[structopt(name = "quiet", short, long)] @@ -141,7 +141,7 @@ fn show_sig(header: &str, sig: Option) { } fn main() { - let args = Args::from_args(); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/examples/clone.rs b/examples/clone.rs index 5af73222f0..f6d00b14b6 100644 --- a/examples/clone.rs +++ b/examples/clone.rs @@ -14,14 +14,14 @@ #![deny(warnings)] +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}; -use structopt::StructOpt; -#[derive(StructOpt)] +#[derive(Parser)] struct Args { #[structopt(name = "url")] arg_url: String, @@ -118,7 +118,7 @@ fn run(args: &Args) -> Result<(), git2::Error> { } fn main() { - let args = Args::from_args(); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/examples/diff.rs b/examples/diff.rs index c3026edb0e..7440149ba0 100644 --- a/examples/diff.rs +++ b/examples/diff.rs @@ -14,12 +14,12 @@ #![deny(warnings)] +use clap::Parser; use git2::{Blob, Diff, DiffOptions, Error, Object, ObjectType, Oid, Repository}; use git2::{DiffDelta, DiffFindOptions, DiffFormat, DiffHunk, DiffLine}; use std::str; -use structopt::StructOpt; -#[derive(StructOpt)] +#[derive(Parser)] #[allow(non_snake_case)] struct Args { #[structopt(name = "from_oid")] @@ -56,19 +56,19 @@ struct Args { #[structopt(name = "no-color", long)] /// never use color output flag_no_color: bool, - #[structopt(short = "R")] + #[structopt(short = 'R')] /// swap two inputs flag_R: bool, - #[structopt(name = "text", short = "a", long)] + #[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)] + #[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)] + #[structopt(name = "ignore-all-space", short = 'w', long)] /// ignore whitespace when comparing lines flag_ignore_all_space: bool, #[structopt(name = "ignored", long)] @@ -95,19 +95,19 @@ struct Args { #[structopt(name = "summary", long)] /// output condensed summary of header info flag_summary: bool, - #[structopt(name = "find-renames", short = "M", long)] - /// set threshold for findind renames (default 50) + #[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)] + #[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)] + #[structopt(name = "break_rewrites", short = 'B', long)] /// break complete rewrite changes into pairs flag_break_rewrites: bool, - #[structopt(name = "unified", short = "U", long)] + #[structopt(name = "unified", short = 'U', long)] /// lints of context to show flag_unified: Option, #[structopt(name = "inter-hunk-context", long)] @@ -120,7 +120,7 @@ struct Args { /// show given source prefix instead of 'a/' flag_src_prefix: Option, #[structopt(name = "dst-prefix", long)] - /// show given destinction prefix instead of 'b/' + /// show given destination prefix instead of 'b/' flag_dst_prefix: Option, #[structopt(name = "path", long = "git-dir")] /// path to git repository to use @@ -360,7 +360,7 @@ impl Args { } fn main() { - let args = Args::from_args(); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/examples/fetch.rs b/examples/fetch.rs index 64374a6d48..af4aa98eb9 100644 --- a/examples/fetch.rs +++ b/examples/fetch.rs @@ -14,12 +14,12 @@ #![deny(warnings)] -use git2::{AutotagOption, FetchOptions, RemoteCallbacks, Repository}; +use clap::Parser; +use git2::{AutotagOption, FetchOptions, RemoteCallbacks, RemoteUpdateFlags, Repository}; use std::io::{self, Write}; use std::str; -use structopt::StructOpt; -#[derive(StructOpt)] +#[derive(Parser)] struct Args { #[structopt(name = "remote")] arg_remote: Option, @@ -113,13 +113,18 @@ fn run(args: &Args) -> Result<(), git2::Error> { // 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. - remote.update_tips(None, true, AutotagOption::Unspecified, None)?; + remote.update_tips( + None, + RemoteUpdateFlags::UPDATE_FETCHHEAD, + AutotagOption::Unspecified, + None, + )?; Ok(()) } fn main() { - let args = Args::from_args(); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/examples/init.rs b/examples/init.rs index 3e447cbdee..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,11 +14,11 @@ #![deny(warnings)] +use clap::Parser; use git2::{Error, Repository, RepositoryInitMode, RepositoryInitOptions}; use std::path::{Path, PathBuf}; -use structopt::StructOpt; -#[derive(StructOpt)] +#[derive(Parser)] struct Args { #[structopt(name = "directory")] arg_directory: String, @@ -137,7 +137,7 @@ fn parse_shared(shared: &str) -> Result { } fn main() { - let args = Args::from_args(); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/examples/log.rs b/examples/log.rs index ad3bb354d0..5f6fe0f210 100644 --- a/examples/log.rs +++ b/examples/log.rs @@ -14,12 +14,12 @@ #![deny(warnings)] +use clap::Parser; use git2::{Commit, DiffOptions, ObjectType, Repository, Signature, Time}; use git2::{DiffFormat, Error, Pathspec}; use std::str; -use structopt::StructOpt; -#[derive(StructOpt)] +#[derive(Parser)] struct Args { #[structopt(name = "topo-order", long)] /// sort commits in topological order @@ -45,7 +45,7 @@ struct Args { #[structopt(name = "skip", long)] /// number of commits to skip flag_skip: Option, - #[structopt(name = "max-count", short = "n", long)] + #[structopt(name = "max-count", short = 'n', long)] /// maximum number of commits to show flag_max_count: Option, #[structopt(name = "merges", long)] @@ -253,22 +253,15 @@ fn print_commit(commit: &Commit) { } 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); + 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!( - "{}{} {}{:02}{:02}", - prefix, - time.strftime("%a %b %e %T %Y").unwrap(), - sign, - hours, - minutes - ); + println!("{}{}", prefix, time_str); } fn match_with_parent( @@ -302,7 +295,7 @@ impl Args { } fn main() { - let args = Args::from_args(); + 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 1808459416..f88baaf906 100644 --- a/examples/ls-remote.rs +++ b/examples/ls-remote.rs @@ -14,10 +14,10 @@ #![deny(warnings)] +use clap::Parser; use git2::{Direction, Repository}; -use structopt::StructOpt; -#[derive(StructOpt)] +#[derive(Parser)] struct Args { #[structopt(name = "remote")] arg_remote: String, @@ -43,7 +43,7 @@ fn run(args: &Args) -> Result<(), git2::Error> { } fn main() { - let args = Args::from_args(); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/examples/pull.rs b/examples/pull.rs index 33f5cb1ef1..27f461e546 100644 --- a/examples/pull.rs +++ b/examples/pull.rs @@ -12,12 +12,12 @@ * . */ +use clap::Parser; use git2::Repository; use std::io::{self, Write}; use std::str; -use structopt::StructOpt; -#[derive(StructOpt)] +#[derive(Parser)] struct Args { arg_remote: Option, arg_branch: Option, @@ -120,7 +120,7 @@ fn normal_merge( let mut idx = repo.merge_trees(&ancestor, &local_tree, &remote_tree, None)?; if idx.has_conflicts() { - println!("Merge conficts detected..."); + println!("Merge conflicts detected..."); repo.checkout_index(Some(&mut idx), None)?; return Ok(()); } @@ -152,7 +152,7 @@ fn do_merge<'a>( // 1. do a merge analysis let analysis = repo.merge_analysis(&[&fetch_commit])?; - // 2. Do the appopriate merge + // 2. Do the appropriate merge if analysis.0.is_fast_forward() { println!("Doing a fast forward"); // do a fast forward @@ -200,7 +200,7 @@ fn run(args: &Args) -> Result<(), git2::Error> { } fn main() { - let args = Args::from_args(); + 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 9b49877283..2eaed78e9a 100644 --- a/examples/rev-list.rs +++ b/examples/rev-list.rs @@ -15,10 +15,10 @@ #![deny(warnings)] +use clap::Parser; use git2::{Error, Oid, Repository, Revwalk}; -use structopt::StructOpt; -#[derive(StructOpt)] +#[derive(Parser)] struct Args { #[structopt(name = "topo-order", long)] /// sort commits in topological order @@ -97,7 +97,7 @@ fn push(revwalk: &mut Revwalk, id: Oid, hide: bool) -> Result<(), Error> { } fn main() { - let args = Args::from_args(); + 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 a465f15a4d..8d3934ea8c 100644 --- a/examples/rev-parse.rs +++ b/examples/rev-parse.rs @@ -14,10 +14,10 @@ #![deny(warnings)] +use clap::Parser; use git2::Repository; -use structopt::StructOpt; -#[derive(StructOpt)] +#[derive(Parser)] struct Args { #[structopt(name = "spec")] arg_spec: String, @@ -52,7 +52,7 @@ fn run(args: &Args) -> Result<(), git2::Error> { } fn main() { - let args = Args::from_args(); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/examples/status.rs b/examples/status.rs index 4f7bc791c0..0ed61711c8 100644 --- a/examples/status.rs +++ b/examples/status.rs @@ -14,12 +14,12 @@ #![deny(warnings)] +use clap::Parser; use git2::{Error, ErrorCode, Repository, StatusOptions, SubmoduleIgnore}; use std::str; use std::time::Duration; -use structopt::StructOpt; -#[derive(StructOpt)] +#[derive(Parser)] struct Args { arg_spec: Vec, #[structopt(name = "long", long)] @@ -433,7 +433,7 @@ impl Args { } fn main() { - let args = Args::from_args(); + let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), diff --git a/examples/tag.rs b/examples/tag.rs index c44c2887d2..5d2af02063 100644 --- a/examples/tag.rs +++ b/examples/tag.rs @@ -14,11 +14,11 @@ #![deny(warnings)] +use clap::Parser; use git2::{Commit, Error, Repository, Tag}; use std::str; -use structopt::StructOpt; -#[derive(StructOpt)] +#[derive(Parser)] struct Args { arg_tagname: Option, arg_object: Option, @@ -119,7 +119,7 @@ fn print_list_lines(message: Option<&str>, args: &Args) { } fn main() { - let args = Args::from_args(); + 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 6b68488e3d..dd51ac0778 100644 --- a/git2-curl/Cargo.toml +++ b/git2-curl/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "git2-curl" -version = "0.14.1" +version = "0.22.0" authors = ["Josh Triplett ", "Alex Crichton "] -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" repository = "https://github.com/rust-lang/git2-rs" documentation = "https://docs.rs/git2-curl" description = """ @@ -10,18 +10,15 @@ Backend for an HTTP transport in libgit2 powered by libcurl. Intended to be used with the git2 crate. """ -edition = "2018" +edition = "2021" [dependencies] curl = "0.4.33" -url = "2.0" +url = "2.5.4" log = "0.4" -git2 = { path = "..", version = "0.13", default-features = false } +git2 = { path = "..", version = "0.21", default-features = false } [dev-dependencies] -civet = "0.11" -conduit = "0.8" -conduit-git-http-backend = "0.8" tempfile = "3.0" [features] diff --git a/git2-curl/src/lib.rs b/git2-curl/src/lib.rs index 8f59bceb59..6fa4532772 100644 --- a/git2-curl/src/lib.rs +++ b/git2-curl/src/lib.rs @@ -15,7 +15,7 @@ //! > **NOTE**: At this time this crate likely does not support a `git push` //! > operation, only clones. -#![doc(html_root_url = "https://docs.rs/git2-curl/0.14")] +#![doc(html_root_url = "https://docs.rs/git2-curl/0.21")] #![deny(missing_docs)] #![warn(rust_2018_idioms)] #![cfg_attr(test, deny(warnings))] @@ -35,7 +35,7 @@ use url::Url; struct CurlTransport { handle: Arc>, - /// The URL of the remote server, e.g. "https://github.com/user/repo" + /// 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. diff --git a/git2-curl/tests/all.rs b/git2-curl/tests/all.rs index c7f09dd40a..6b6fe9aca4 100644 --- a/git2-curl/tests/all.rs +++ b/git2-curl/tests/all.rs @@ -1,22 +1,141 @@ -use civet::{Config, Server}; -use conduit_git_http_backend as git_backend; +//! 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 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().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(); 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 8cc6e65b0c..b2df3ca828 100644 --- a/libgit2-sys/Cargo.toml +++ b/libgit2-sys/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "libgit2-sys" -version = "0.12.19+1.1.0" +version = "0.18.2+1.9.1" authors = ["Josh Triplett ", "Alex Crichton "] links = "git2" build = "build.rs" repository = "https://github.com/rust-lang/git2-rs" -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" description = "Native bindings to the libgit2 library" exclude = [ "libgit2/ci/*", @@ -14,7 +14,7 @@ exclude = [ "libgit2/fuzzers/*", "libgit2/tests/*", ] -edition = "2018" +edition = "2021" [lib] name = "libgit2_sys" @@ -22,23 +22,19 @@ path = "lib.rs" [dependencies] libc = "0.2" -libssh2-sys = { version = "0.2.19", optional = true } +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.7" +pkg-config = "0.3.15" cc = { version = "1.0.43", features = ['parallel'] } [target.'cfg(unix)'.dependencies] -openssl-sys = { version = "0.9", optional = true } +openssl-sys = { version = "0.9.45", optional = true } [features] ssh = ["libssh2-sys"] https = ["openssl-sys"] -ssh_key_from_memory = [] -# Cargo does not support requiring features on an optional dependency without -# requiring the dependency. Rather than introduce additional complexity, we -# just require libssh2 when using zlib-ng-compat. This will require building -# libssh2, but it will not add code to the final binary without the "ssh" -# feature enabled. -zlib-ng-compat = ["libz-sys/zlib-ng", "libssh2-sys/zlib-ng-compat"] +vendored = [] +vendored-openssl = ["openssl-sys/vendored"] +zlib-ng-compat = ["libz-sys/zlib-ng", "libssh2-sys?/zlib-ng-compat"] diff --git a/libgit2-sys/build.rs b/libgit2-sys/build.rs index 76aa687cf6..7b5a374e9b 100644 --- a/libgit2-sys/build.rs +++ b/libgit2-sys/build.rs @@ -4,23 +4,66 @@ use std::io; use std::path::{Path, PathBuf}; use std::process::Command; +/// 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(); + let vendored = env::var("CARGO_FEATURE_VENDORED").is_ok(); let zlib_ng_compat = env::var("CARGO_FEATURE_ZLIB_NG_COMPAT").is_ok(); - // To use zlib-ng in zlib-compat mode, we have to build libgit2 ourselves. - if !zlib_ng_compat { - let mut cfg = pkg_config::Config::new(); - if let Ok(lib) = cfg.atleast_version("1.1.0").probe("libgit2") { - for include in &lib.include_paths { - println!("cargo:root={}", include.display()); - } - return; + // 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 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() { + // 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(); @@ -37,23 +80,28 @@ fn main() { cp_r("libgit2/include", &include); cfg.include(&include) - .include("libgit2/src") + .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"); - add_c_files(&mut cfg, "libgit2/src/xdiff"); + 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/transports"); - add_c_files(&mut cfg, "libgit2/src/streams"); + add_c_files(&mut cfg, "libgit2/src/libgit2/transports"); + add_c_files(&mut cfg, "libgit2/src/libgit2/streams"); - // Always use bundled http-parser for now - cfg.include("libgit2/deps/http-parser") - .file("libgit2/deps/http-parser/http_parser.c"); + // 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. // @@ -78,11 +126,11 @@ fn main() { // when when COMPILE_PCRE8 is not defined, which is the default. add_c_files(&mut cfg, "libgit2/deps/pcre"); - cfg.file("libgit2/src/allocators/failalloc.c"); - cfg.file("libgit2/src/allocators/stdalloc.c"); + 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/win32"); + 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")); @@ -94,7 +142,7 @@ fn main() { cfg.define("__USE_MINGW_ANSI_STDIO", "1"); } } else { - add_c_files(&mut cfg, "libgit2/src/unix"); + add_c_files(&mut cfg, "libgit2/src/util/unix"); cfg.flag("-fvisibility=hidden"); } if target.contains("solaris") || target.contains("illumos") { @@ -107,11 +155,21 @@ fn main() { 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 windows { + features.push_str("#define GIT_IO_WSAPOLL 1\n"); + } else { + // 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 target.contains("apple") { features.push_str("#define GIT_USE_STAT_MTIMESPEC 1\n"); } else { @@ -129,7 +187,8 @@ fn main() { cfg.include(path); } features.push_str("#define GIT_SSH 1\n"); - features.push_str("#define GIT_SSH_MEMORY_CREDENTIALS 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"); @@ -151,9 +210,26 @@ fn main() { 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/hash/sha1/collisiondetect.c"); - cfg.file("libgit2/src/hash/sha1/sha1dc/sha1.c"); - cfg.file("libgit2/src/hash/sha1/sha1dc/ubc_check.c"); + 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); @@ -164,7 +240,7 @@ fn main() { } features.push_str("#endif\n"); - fs::write(include.join("git2/sys/features.h"), features).unwrap(); + fs::write(include.join("git2_features.h"), features).unwrap(); cfg.compile("git2"); @@ -175,7 +251,8 @@ fn main() { println!("cargo:rustc-link-lib=rpcrt4"); println!("cargo:rustc-link-lib=ole32"); println!("cargo:rustc-link-lib=crypt32"); - return; + println!("cargo:rustc-link-lib=secur32"); + println!("cargo:rustc-link-lib=advapi32"); } if target.contains("apple") { @@ -184,9 +261,9 @@ fn main() { println!("cargo:rustc-link-lib=framework=CoreFoundation"); } - rerun_if(Path::new("libgit2/include")); - rerun_if(Path::new("libgit2/src")); - rerun_if(Path::new("libgit2/deps")); + println!("cargo:rerun-if-changed=libgit2/include"); + println!("cargo:rerun-if-changed=libgit2/src"); + println!("cargo:rerun-if-changed=libgit2/deps"); } fn cp_r(from: impl AsRef, to: impl AsRef) { @@ -205,8 +282,12 @@ fn cp_r(from: impl AsRef, to: impl AsRef) { } 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.as_ref().read_dir().unwrap(); + let dir = path.read_dir().unwrap(); let mut paths = dir.collect::>>().unwrap(); paths.sort_by_key(|e| e.path()); @@ -219,13 +300,3 @@ fn add_c_files(build: &mut cc::Build, path: impl AsRef) { } } } - -fn rerun_if(path: &Path) { - if path.is_dir() { - for entry in fs::read_dir(path).expect("read_dir") { - rerun_if(&entry.expect("entry").path()); - } - } else { - println!("cargo:rerun-if-changed={}", path.display()); - } -} diff --git a/libgit2-sys/lib.rs b/libgit2-sys/lib.rs index 050989fed9..7e1ac1e998 100644 --- a/libgit2-sys/lib.rs +++ b/libgit2-sys/lib.rs @@ -1,10 +1,10 @@ -#![doc(html_root_url = "https://docs.rs/libgit2-sys/0.12")] +#![doc(html_root_url = "https://docs.rs/libgit2-sys/0.18")] #![allow(non_camel_case_types, unused_extern_crates)] // This is required to link libz when libssh2-sys is not included. extern crate libz_sys as libz; -use libc::{c_char, c_int, c_uchar, c_uint, c_void, size_t}; +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; @@ -25,6 +25,7 @@ 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)* }) => { @@ -91,6 +92,7 @@ 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 { @@ -143,7 +145,7 @@ 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, @@ -195,6 +197,11 @@ git_enum! { GIT_EMISMATCH = -33, GIT_EINDEXDIRTY = -34, GIT_EAPPLYFAIL = -35, + GIT_EOWNER = -36, + GIT_TIMEOUT = -37, + GIT_EUNCHANGED = -38, + GIT_ENOTSUPPORTED = -39, + GIT_EREADONLY = -40, } } @@ -333,7 +340,7 @@ pub struct git_checkout_perfdata { } #[repr(C)] -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Default)] pub struct git_indexer_progress { pub total_objects: c_uint, pub indexed_objects: c_uint, @@ -353,6 +360,23 @@ pub type git_indexer_progress_cb = )] 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, @@ -368,8 +392,18 @@ pub struct git_remote_callbacks { 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)] @@ -377,12 +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, @@ -484,6 +529,10 @@ git_enum! { 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, } } @@ -606,6 +655,7 @@ pub struct git_status_options { pub flags: c_uint, pub pathspec: git_strarray, pub baseline: *mut git_tree, + pub rename_threshold: u16, } #[repr(C)] @@ -627,8 +677,7 @@ pub struct git_status_entry { git_enum! { pub enum git_checkout_strategy_t { - GIT_CHECKOUT_NONE = 0, - GIT_CHECKOUT_SAFE = 1 << 0, + GIT_CHECKOUT_SAFE = 0, GIT_CHECKOUT_FORCE = 1 << 1, GIT_CHECKOUT_RECREATE_MISSING = 1 << 2, GIT_CHECKOUT_ALLOW_CONFLICTS = 1 << 4, @@ -645,6 +694,7 @@ git_enum! { 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, @@ -686,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, @@ -700,7 +751,7 @@ git_enum! { } pub type git_treewalk_cb = - Option c_int>; + extern "C" fn(*const c_char, *const git_tree_entry, *mut c_void) -> c_int; pub type git_treebuilder_filter_cb = Option c_int>; @@ -725,7 +776,7 @@ pub struct git_tree_update { #[derive(Copy, Clone)] pub struct git_buf { pub ptr: *mut c_char, - pub asize: size_t, + pub reserved: size_t, pub size: size_t, } @@ -766,10 +817,13 @@ 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, } @@ -814,7 +868,7 @@ 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, @@ -824,10 +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: Option, - pub payload: *mut c_void, } git_enum! { @@ -837,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, } } @@ -948,7 +1003,9 @@ pub struct git_push_options { 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 = @@ -1115,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, @@ -1149,36 +1215,40 @@ pub type git_diff_notify_cb = Option< pub type git_diff_progress_cb = Option c_int>; -pub type git_diff_option_t = i32; -pub const GIT_DIFF_NORMAL: git_diff_option_t = 0; -pub const GIT_DIFF_REVERSE: git_diff_option_t = 1 << 0; -pub const GIT_DIFF_INCLUDE_IGNORED: git_diff_option_t = 1 << 1; -pub const GIT_DIFF_RECURSE_IGNORED_DIRS: git_diff_option_t = 1 << 2; -pub const GIT_DIFF_INCLUDE_UNTRACKED: git_diff_option_t = 1 << 3; -pub const GIT_DIFF_RECURSE_UNTRACKED_DIRS: git_diff_option_t = 1 << 4; -pub const GIT_DIFF_INCLUDE_UNMODIFIED: git_diff_option_t = 1 << 5; -pub const GIT_DIFF_INCLUDE_TYPECHANGE: git_diff_option_t = 1 << 6; -pub const GIT_DIFF_INCLUDE_TYPECHANGE_TREES: git_diff_option_t = 1 << 7; -pub const GIT_DIFF_IGNORE_FILEMODE: git_diff_option_t = 1 << 8; -pub const GIT_DIFF_IGNORE_SUBMODULES: git_diff_option_t = 1 << 9; -pub const GIT_DIFF_IGNORE_CASE: git_diff_option_t = 1 << 10; -pub const GIT_DIFF_DISABLE_PATHSPEC_MATCH: git_diff_option_t = 1 << 12; -pub const GIT_DIFF_SKIP_BINARY_CHECK: git_diff_option_t = 1 << 13; -pub const GIT_DIFF_ENABLE_FAST_UNTRACKED_DIRS: git_diff_option_t = 1 << 14; -pub const GIT_DIFF_UPDATE_INDEX: git_diff_option_t = 1 << 15; -pub const GIT_DIFF_INCLUDE_UNREADABLE: git_diff_option_t = 1 << 16; -pub const GIT_DIFF_INCLUDE_UNREADABLE_AS_UNTRACKED: git_diff_option_t = 1 << 17; -pub const GIT_DIFF_INDENT_HEURISTIC: git_diff_option_t = 1 << 18; -pub const GIT_DIFF_FORCE_TEXT: git_diff_option_t = 1 << 20; -pub const GIT_DIFF_FORCE_BINARY: git_diff_option_t = 1 << 21; -pub const GIT_DIFF_IGNORE_WHITESPACE: git_diff_option_t = 1 << 22; -pub const GIT_DIFF_IGNORE_WHITESPACE_CHANGE: git_diff_option_t = 1 << 23; -pub const GIT_DIFF_IGNORE_WHITESPACE_EOL: git_diff_option_t = 1 << 24; -pub const GIT_DIFF_SHOW_UNTRACKED_CONTENT: git_diff_option_t = 1 << 25; -pub const GIT_DIFF_SHOW_UNMODIFIED: git_diff_option_t = 1 << 26; -pub const GIT_DIFF_PATIENCE: git_diff_option_t = 1 << 28; -pub const GIT_DIFF_MINIMAL: git_diff_option_t = 1 << 29; -pub const GIT_DIFF_SHOW_BINARY: git_diff_option_t = 1 << 30; +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 { @@ -1291,6 +1361,26 @@ pub struct git_merge_options { 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! { pub enum git_merge_flag_t { GIT_MERGE_FIND_RENAMES = 1 << 0, @@ -1320,6 +1410,8 @@ git_enum! { 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, } } @@ -1352,55 +1444,67 @@ pub type git_transport_cb = Option< #[repr(C)] pub struct git_transport { pub version: c_uint, - pub set_callbacks: Option< + pub connect: Option< extern "C" fn( - *mut git_transport, - git_transport_message_cb, - git_transport_message_cb, - git_transport_certificate_check_cb, - *mut c_void, + transport: *mut git_transport, + url: *const c_char, + direction: c_int, + connect_opts: *const git_remote_connect_options, ) -> c_int, >, - pub set_custom_headers: Option c_int>, - pub connect: Option< + pub set_connect_opts: Option< extern "C" fn( - *mut git_transport, - *const c_char, - git_cred_acquire_cb, - *mut c_void, - *const git_proxy_options, - c_int, - c_int, + transport: *mut git_transport, + connect_opts: *const git_remote_connect_options, ) -> c_int, >, + pub capabilities: + Option c_int>, pub ls: Option< - extern "C" fn(*mut *mut *const git_remote_head, *mut size_t, *mut git_transport) -> c_int, - >, - pub push: Option< - extern "C" fn(*mut git_transport, *mut git_push, *const git_remote_callbacks) -> c_int, + 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( - *mut git_transport, - *mut git_repository, - *const *const git_remote_head, - size_t, + 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( - *mut git_transport, - *mut git_repository, - *mut git_indexer_progress, - git_indexer_progress_cb, - *mut c_void, + transport: *mut git_transport, + repo: *mut git_repository, + stats: *mut git_indexer_progress, ) -> c_int, >, - pub is_connected: Option c_int>, - pub read_flags: Option c_int>, - pub cancel: Option, - pub close: Option c_int>, - pub free: Option, + 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)] @@ -1486,11 +1590,19 @@ pub struct git_odb_backend { ) -> 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, @@ -1686,6 +1798,7 @@ git_enum! { GIT_STASH_KEEP_INDEX = 1 << 0, GIT_STASH_INCLUDE_UNTRACKED = 1 << 1, GIT_STASH_INCLUDE_IGNORED = 1 << 2, + GIT_STASH_KEEP_ALL = 1 << 3, } } @@ -1709,6 +1822,17 @@ git_enum! { } } +#[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, @@ -1745,6 +1869,20 @@ pub type git_commit_signing_cb = Option< ) -> 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)] @@ -1755,6 +1893,7 @@ pub struct git_rebase_options { 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, } @@ -1849,6 +1988,20 @@ git_enum! { 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, } } @@ -1865,7 +2018,9 @@ git_enum! { 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; @@ -1889,6 +2044,85 @@ pub struct git_worktree_prune_options { 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; @@ -1947,6 +2181,7 @@ extern "C" { 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: *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: *const git_repository) -> *const c_char; pub fn git_repository_set_workdir( @@ -1981,6 +2216,16 @@ extern "C" { 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( @@ -2017,6 +2262,13 @@ extern "C" { 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, @@ -2109,6 +2361,7 @@ extern "C" { ) -> 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_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, @@ -2130,7 +2383,7 @@ extern "C" { pub fn git_remote_update_tips( remote: *mut git_remote, callbacks: *const git_remote_callbacks, - update_fetchead: c_int, + update_flags: c_uint, download_tags: git_remote_autotag_option_t, reflog_message: *const c_char, ) -> c_int; @@ -2260,6 +2513,7 @@ extern "C" { 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_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, @@ -2299,6 +2553,12 @@ extern "C" { 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, @@ -2370,6 +2630,15 @@ extern "C" { 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, @@ -2421,6 +2690,11 @@ extern "C" { 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_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, @@ -2617,6 +2891,12 @@ extern "C" { 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; @@ -2634,6 +2914,7 @@ extern "C" { 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; @@ -2732,6 +3013,7 @@ extern "C" { 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, @@ -2758,6 +3040,11 @@ extern "C" { 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_version(index: *mut git_index) -> c_uint; @@ -2807,6 +3094,11 @@ extern "C" { 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_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( @@ -3069,6 +3361,7 @@ extern "C" { 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_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) @@ -3124,6 +3417,8 @@ extern "C" { 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 @@ -3188,6 +3483,12 @@ extern "C" { ) -> c_int; // blame + 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, @@ -3245,6 +3546,13 @@ extern "C" { 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, @@ -3259,6 +3567,17 @@ extern "C" { 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( @@ -3603,6 +3922,13 @@ extern "C" { 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; @@ -3627,7 +3953,9 @@ extern "C" { 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_name(pb: *mut git_packbuilder) -> *const c_char; pub fn git_packbuilder_foreach( pb: *mut git_packbuilder, cb: git_packbuilder_foreach_cb, @@ -3642,6 +3970,28 @@ extern "C" { ) -> 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; @@ -3719,6 +4069,7 @@ extern "C" { ) -> 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; @@ -3764,8 +4115,12 @@ extern "C" { 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, @@ -3883,6 +4238,9 @@ extern "C" { 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 @@ -3977,6 +4335,27 @@ extern "C" { 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() { @@ -4025,3 +4404,8 @@ fn ssh_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 f4b473fa84..0060d9cf56 160000 --- a/libgit2-sys/libgit2 +++ b/libgit2-sys/libgit2 @@ -1 +1 @@ -Subproject commit f4b473fa842a84147d8036a27bac5c026c44dad4 +Subproject commit 0060d9cf5666f015b1067129bd874c6cc4c9c7ac diff --git a/src/attr.rs b/src/attr.rs index 1e689d98ca..33b1d2d4af 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -76,9 +76,9 @@ impl PartialEq for AttrValue<'_> { | (Self::False, AttrValue::False) | (Self::Unspecified, AttrValue::Unspecified) => true, (AttrValue::String(string), AttrValue::Bytes(bytes)) - | (Self::Bytes(bytes), AttrValue::String(string)) => string.as_bytes() == *bytes, - (Self::String(left), AttrValue::String(right)) => left == right, - (Self::Bytes(left), AttrValue::Bytes(right)) => left == right, + | (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, } } diff --git a/src/blame.rs b/src/blame.rs index 7db275229c..4bf41fed1e 100644 --- a/src/blame.rs +++ b/src/blame.rs @@ -1,9 +1,11 @@ use crate::util::{self, Binding}; -use crate::{raw, signature, Oid, Repository, Signature}; -use std::marker; +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,6 +31,24 @@ 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 } @@ -121,7 +141,7 @@ impl<'blame> BlameHunk<'blame> { /// 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) = crate::opt_bytes(self, (*self.raw).orig_path) { @@ -247,7 +267,7 @@ impl<'repo> Binding for Blame<'repo> { unsafe fn from_raw(raw: *mut raw::git_blame) -> Blame<'repo> { Blame { - raw: raw, + raw, _marker: marker::PhantomData, } } @@ -268,7 +288,7 @@ impl<'blame> Binding for BlameHunk<'blame> { unsafe fn from_raw(raw: *mut raw::git_blame_hunk) -> BlameHunk<'blame> { BlameHunk { - raw: raw, + raw, _marker: marker::PhantomData, } } @@ -307,6 +327,8 @@ impl<'blame> DoubleEndedIterator for BlameIter<'blame> { } } +impl<'blame> FusedIterator for BlameIter<'blame> {} + impl<'blame> ExactSizeIterator for BlameIter<'blame> {} #[cfg(test)] @@ -345,6 +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 ae7505a69e..5c4a6ce6b8 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -56,7 +56,7 @@ impl<'repo> Binding for Blob<'repo> { unsafe fn from_raw(raw: *mut raw::git_blob) -> Blob<'repo> { Blob { - raw: raw, + raw, _marker: marker::PhantomData, } } @@ -110,7 +110,7 @@ impl<'repo> Binding for BlobWriter<'repo> { unsafe fn from_raw(raw: *mut raw::git_writestream) -> BlobWriter<'repo> { BlobWriter { - raw: raw, + raw, need_cleanup: true, _marker: marker::PhantomData, } diff --git a/src/branch.rs b/src/branch.rs index 276bc534ac..e1eba99c2b 100644 --- a/src/branch.rs +++ b/src/branch.rs @@ -28,6 +28,17 @@ impl<'repo> Branch<'repo> { 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 @@ -120,7 +131,7 @@ impl<'repo> Branches<'repo> { /// pointer. pub unsafe fn from_raw(raw: *mut raw::git_branch_iterator) -> Branches<'repo> { Branches { - raw: raw, + raw, _marker: marker::PhantomData, } } @@ -151,7 +162,7 @@ impl<'repo> Drop for Branches<'repo> { #[cfg(test)] mod tests { - use crate::BranchType; + use crate::{Branch, BranchType}; #[test] fn smoke() { @@ -175,4 +186,12 @@ mod tests { 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 0ab560ce80..fd2bcbf96f 100644 --- a/src/buf.rs +++ b/src/buf.rs @@ -28,7 +28,7 @@ impl Buf { Binding::from_raw(&mut raw::git_buf { ptr: ptr::null_mut(), size: 0, - asize: 0, + reserved: 0, } as *mut _) } } diff --git a/src/build.rs b/src/build.rs index fd17848346..4ac62439b7 100644 --- a/src/build.rs +++ b/src/build.rs @@ -28,7 +28,7 @@ use crate::{CheckoutNotificationType, DiffFile, FileMode, Remote}; /// Cred::ssh_key( /// username_from_url.unwrap(), /// None, -/// std::path::Path::new(&format!("{}/.ssh/id_rsa", env::var("HOME").unwrap())), +/// Path::new(&format!("{}/.ssh/id_rsa", env::var("HOME").unwrap())), /// None, /// ) /// }); @@ -60,11 +60,19 @@ pub struct RepoBuilder<'cb> { /// Type of callback passed to `RepoBuilder::remote_create`. /// -/// The second and third arguments are the remote's name and the remote's url. +/// 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, for use with `git_tree_create_updated`. +/// 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, @@ -89,7 +97,7 @@ 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> = dyn FnMut(Option<&Path>, usize, usize) + 'a; @@ -121,10 +129,10 @@ 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. + /// use a normal fetch for `file://` URLs. Auto = raw::GIT_CLONE_LOCAL_AUTO as isize, - /// Bypass the git-aware transport even for `file://` urls. + /// Bypass the git-aware transport even for `file://` URLs. Local = raw::GIT_CLONE_LOCAL as isize, /// Never bypass the git-aware transport @@ -230,7 +238,7 @@ impl<'cb> RepoBuilder<'cb> { /// 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() }; @@ -354,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> { @@ -483,6 +491,12 @@ impl<'cb> 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> { self.disable_filters = disable; @@ -507,8 +521,13 @@ 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. + /// + /// [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()); @@ -680,6 +699,8 @@ extern "C" fn notify_cb( .unwrap_or(2) } +unsafe impl Send for TreeUpdateBuilder {} + impl Default for TreeUpdateBuilder { fn default() -> Self { Self::new() diff --git a/src/call.rs b/src/call.rs index d9fd234681..9aa3ae667f 100644 --- a/src/call.rs +++ b/src/call.rs @@ -1,5 +1,4 @@ #![macro_use] -use libc; use crate::Error; @@ -45,17 +44,13 @@ pub fn c_try(ret: libc::c_int) -> Result { } pub fn last_error(code: libc::c_int) -> Error { - // nowadays this unwrap is safe as `Error::last_error` always returns - // `Some`. - Error::last_error(code).unwrap() + Error::last_error(code) } mod impls { use std::ffi::CString; use std::ptr; - use libc; - use crate::call::Convert; use crate::{raw, BranchType, ConfigLevel, Direction, ObjectType, ResetType}; use crate::{ @@ -169,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, } diff --git a/src/cert.rs b/src/cert.rs index 9153880b69..b232cc3ce8 100644 --- a/src/cert.rs +++ b/src/cert.rs @@ -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. /// @@ -87,6 +135,39 @@ impl<'a> CertHostkey<'a> { } } } + + /// 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> { @@ -100,7 +181,7 @@ 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, + raw, _marker: marker::PhantomData, } } diff --git a/src/commit.rs b/src/commit.rs index 4df60f7d9c..7fef508096 100644 --- a/src/commit.rs +++ b/src/commit.rs @@ -1,4 +1,4 @@ -use libc; +use std::iter::FusedIterator; use std::marker; use std::mem; use std::ops::Range; @@ -145,6 +145,29 @@ impl<'repo> Commit<'repo> { 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. /// /// The first element of the tuple is the time, in seconds, since the epoch. @@ -313,7 +336,7 @@ 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, } } @@ -353,6 +376,8 @@ impl<'repo, 'commit> DoubleEndedIterator for Parents<'commit, 'repo> { } } +impl<'repo, 'commit> FusedIterator for Parents<'commit, 'repo> {} + impl<'repo, 'commit> ExactSizeIterator for Parents<'commit, 'repo> {} /// Aborts iteration when a commit cannot be found @@ -377,6 +402,8 @@ impl<'commit> DoubleEndedIterator for ParentIds<'commit> { } } +impl<'commit> FusedIterator for ParentIds<'commit> {} + impl<'commit> ExactSizeIterator for ParentIds<'commit> {} impl<'repo> Clone for Commit<'repo> { @@ -399,12 +426,14 @@ mod tests { let head = repo.head().unwrap(); let target = head.target().unwrap(); let commit = repo.find_commit(target).unwrap(); - assert_eq!(commit.message(), Some("initial")); + 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); diff --git a/src/config.rs b/src/config.rs index 66cd5bb4de..9ba0a6da64 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,3 @@ -use libc; use std::ffi::CString; use std::marker; use std::path::{Path, PathBuf}; @@ -23,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>, } @@ -77,7 +107,7 @@ 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 { crate::init(); @@ -90,7 +120,7 @@ impl Config { /// 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 { crate::init(); let buf = Buf::new(); @@ -100,9 +130,9 @@ impl Config { 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 { crate::init(); @@ -207,7 +237,10 @@ impl Config { /// This is the same as `get_bytes` except that it may return `Err` if /// the bytes are not valid utf-8. /// - /// 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_str(&self, name: &str) -> Result<&str, Error> { str::from_utf8(self.get_bytes(name)?) .map_err(|_| Error::from_str("configuration value is not valid utf8")) @@ -215,7 +248,10 @@ impl Config { /// 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 = ptr::null(); let name = CString::new(name)?; @@ -280,15 +316,18 @@ impl Config { /// 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()); /// } @@ -317,6 +356,9 @@ impl Config { /// 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)?; @@ -332,7 +374,7 @@ impl Config { /// 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 { @@ -465,7 +507,7 @@ impl Config { 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 @@ -534,7 +576,7 @@ impl<'cfg> Binding for ConfigEntry<'cfg> { unsafe fn from_raw(raw: *mut raw::git_config_entry) -> ConfigEntry<'cfg> { ConfigEntry { - raw: raw, + raw, _marker: marker::PhantomData, owned: true, } @@ -549,7 +591,8 @@ impl<'cfg> Binding for ConfigEntries<'cfg> { unsafe fn from_raw(raw: *mut raw::git_config_iterator) -> ConfigEntries<'cfg> { ConfigEntries { - raw: raw, + raw, + current: None, _marker: marker::PhantomData, } } @@ -558,23 +601,32 @@ impl<'cfg> Binding for ConfigEntries<'cfg> { } } -// 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>> { +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(()) } } @@ -628,7 +680,8 @@ 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(); @@ -649,39 +702,42 @@ mod tests { cfg.set_multivar("foo.baz", "^$", "oki").unwrap(); // `entries` filters by name - let mut entries: Vec = cfg - .entries(Some("foo.bar")) + let mut entries: Vec = Vec::new(); + cfg.entries(Some("foo.bar")) .unwrap() - .into_iter() - .map(|entry| entry.unwrap().value().unwrap().into()) - .collect(); + .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 = cfg - .multivar("foo.bar", None) + let mut multivals = Vec::new(); + cfg.multivar("foo.bar", None) .unwrap() - .into_iter() - .map(|entry| entry.unwrap().value().unwrap().into()) - .collect(); + .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 = cfg - .multivar("foo.bar", Some("qu.*x")) + let mut quxish = Vec::new(); + cfg.multivar("foo.bar", Some("qu.*x")) .unwrap() - .into_iter() - .map(|entry| entry.unwrap().value().unwrap().into()) - .collect(); + .for_each(|entry| quxish.push(entry.value().unwrap().to_string())) + .unwrap(); quxish.sort(); assert_eq!(quxish, ["quux", "qux"]); cfg.remove_multivar("foo.bar", ".*").unwrap(); - assert_eq!(cfg.entries(Some("foo.bar")).unwrap().count(), 0); - assert_eq!(cfg.multivar("foo.bar", None).unwrap().count(), 0); + 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] diff --git a/src/cred.rs b/src/cred.rs index 41ff1c193b..adf621b5d8 100644 --- a/src/cred.rs +++ b/src/cred.rs @@ -1,14 +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 std::ptr; -use url; use crate::util::Binding; -use crate::{raw, Config, Error, IntoCString}; +#[cfg(feature = "cred")] +use crate::Config; +use crate::{raw, Error, IntoCString}; /// A structure to represent git credentials in libgit2. pub struct Cred { @@ -16,6 +16,7 @@ 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. @@ -113,12 +114,13 @@ 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 + #[cfg(feature = "cred")] pub fn credential_helper( config: &Config, url: &str, @@ -140,7 +142,7 @@ impl Cred { /// Create a credential to specify a username. /// /// This is used with ssh authentication to query for the username if none is - /// specified in the url. + /// specified in the URL. pub fn username(username: &str) -> Result { crate::init(); let username = CString::new(username)?; @@ -171,7 +173,7 @@ 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 @@ -190,12 +192,13 @@ impl Drop for Cred { } } +#[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, @@ -293,6 +296,12 @@ impl CredentialHelper { // see https://www.kernel.org/pub/software/scm/git/docs/technical // /api-credentials.html#_credential_helpers fn add_command(&mut self, cmd: Option<&str>) { + 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, @@ -300,7 +309,7 @@ impl CredentialHelper { if cmd.starts_with('!') { self.commands.push(cmd[1..].to_string()); - } else if cmd.contains("/") || cmd.contains("\\") { + } else if is_absolute_path(cmd) { self.commands.push(cmd.to_string()); } else { self.commands.push(format!("git credential-{}", cmd)); @@ -353,6 +362,9 @@ impl CredentialHelper { 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, @@ -373,6 +385,12 @@ impl CredentialHelper { // 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()) @@ -385,6 +403,12 @@ impl CredentialHelper { 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); } @@ -427,7 +451,7 @@ impl CredentialHelper { let output = my_try!(p.wait_with_output()); if !output.status.success() { debug!( - "credential helper failed: {}\nstdout ---\n{}\nstdout ---\n{}", + "credential helper failed: {}\nstdout ---\n{}\nstderr ---\n{}", output.status, String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) @@ -471,6 +495,7 @@ impl CredentialHelper { } #[cfg(test)] +#[cfg(feature = "cred")] mod test { use std::env; use std::fs::File; @@ -483,7 +508,7 @@ mod test { 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 }) ); @@ -567,13 +592,13 @@ echo username=c return; } // shell scripts don't work on Windows let td = TempDir::new().unwrap(); - let path = td.path().join("git-credential-script"); + let path = td.path().join("git-credential-some-script"); File::create(&path) .unwrap() .write( br"\ #!/bin/sh -echo username=c +echo username=$1 ", ) .unwrap(); @@ -585,14 +610,14 @@ echo username=c env::set_var("PATH", &env::join_paths(paths).unwrap()); let cfg = test_cfg! { - "credential.https://example.com.helper" => "script", + "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"); + assert_eq!(u, "value/with\\slashes"); assert_eq!(p, "b"); } diff --git a/src/describe.rs b/src/describe.rs index efa66e826d..cbaa1893b6 100644 --- a/src/describe.rs +++ b/src/describe.rs @@ -44,7 +44,7 @@ impl<'repo> Binding for Describe<'repo> { unsafe fn from_raw(raw: *mut raw::git_describe_result) -> Describe<'repo> { Describe { - raw: raw, + raw, _marker: marker::PhantomData, } } diff --git a/src/diff.rs b/src/diff.rs index 0debc045bc..3070550390 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -1,5 +1,6 @@ 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; @@ -254,6 +255,8 @@ impl<'repo> Diff<'repo> { /// 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, @@ -264,7 +267,7 @@ impl<'repo> Diff<'repo> { assert!(patch_no > 0); assert!(patch_no <= total_patches); let mut default = DiffFormatEmailOptions::default(); - let mut raw_opts = opts.map_or(&mut default.raw, |opts| &mut opts.raw); + 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)); @@ -277,13 +280,14 @@ impl<'repo> Diff<'repo> { 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 an patchid from a diff. + /// 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], @@ -307,8 +311,9 @@ impl Diff<'static> { /// 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 abreviated. + /// 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 @@ -446,7 +451,7 @@ 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, + raw, _marker: marker::PhantomData, } } @@ -544,7 +549,7 @@ 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, } } @@ -617,6 +622,7 @@ impl<'a> DiffFile<'a> { 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, @@ -629,7 +635,7 @@ 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, } } @@ -675,7 +681,7 @@ impl DiffOptions { opts } - fn flag(&mut self, opt: i32, 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; @@ -813,6 +819,11 @@ impl 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 @@ -949,6 +960,8 @@ impl<'diff> DoubleEndedIterator for Deltas<'diff> { 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. @@ -1080,7 +1093,7 @@ 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, } } @@ -1142,7 +1155,7 @@ 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, } } @@ -1164,7 +1177,7 @@ impl<'a> std::fmt::Debug for DiffHunk<'a> { } 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 } } @@ -1198,7 +1211,7 @@ 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 @@ -1247,7 +1260,7 @@ 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, } } @@ -1279,7 +1292,7 @@ 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, } } @@ -1446,7 +1459,7 @@ impl DiffFindOptions { self } - /// Similarity of modified to be glegible rename source (default 50) + /// 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 @@ -1474,6 +1487,11 @@ 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 { @@ -1769,6 +1787,7 @@ mod tests { 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!( 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 4147d77391..ecc7f4f776 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,7 @@ use libc::c_int; use std::env::JoinPathsError; use std::error; -use std::ffi::{CStr, NulError}; +use std::ffi::{CStr, CString, NulError}; use std::fmt; use std::str; @@ -12,13 +12,13 @@ use crate::{raw, ErrorClass, ErrorCode}; pub struct Error { code: c_int, klass: c_int, - message: String, + message: Box, } impl Error { /// Creates a new error. /// - /// This is mainly intended for implementors of custom transports or + /// 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 { @@ -32,12 +32,7 @@ impl Error { /// /// The `code` argument typically comes from the return value of a function /// call. This code will later be returned from the `code` function. - /// - /// Historically this function returned `Some` or `None` based on the return - /// value of `git_error_last` but nowadays it always returns `Some` so it's - /// safe to unwrap the return value. This API will change in the next major - /// version. - pub fn last_error(code: c_int) -> Option { + pub fn last_error(code: c_int) -> Error { crate::init(); unsafe { // Note that whenever libgit2 returns an error any negative value @@ -64,17 +59,17 @@ impl Error { Error::from_raw(code, ptr) }; raw::git_error_clear(); - Some(err) + 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 = String::from_utf8_lossy(msg).into_owned(); + let message = CStr::from_ptr((*ptr).message as *const _).to_bytes(); + let message = String::from_utf8_lossy(message).into_owned().into(); Error { - code: code, + code, klass: (*ptr).klass, - message: msg, + message, } } @@ -86,7 +81,7 @@ impl Error { Error { code: raw::GIT_ERROR as c_int, klass: raw::GIT_ERROR_NONE as c_int, - message: s.to_string(), + message: s.into(), } } @@ -127,13 +122,15 @@ impl Error { 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 implementors of custom transports + /// 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 { @@ -163,6 +160,8 @@ impl Error { 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, }; } @@ -214,7 +213,7 @@ impl Error { /// Modify the error class associated with this error. /// - /// This is mainly intended to be used by implementors of custom transports + /// 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 { @@ -287,12 +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, ) } @@ -347,6 +353,17 @@ impl Error { pub fn message(&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 {} diff --git a/src/index.rs b/src/index.rs index 5612f43a30..c0d9294520 100644 --- a/src/index.rs +++ b/src/index.rs @@ -45,7 +45,7 @@ pub struct IndexConflict { /// 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> = dyn FnMut(&Path, &[u8]) -> i32 + 'a; @@ -54,6 +54,7 @@ pub type IndexMatchedPath<'a> = dyn FnMut(&Path, &[u8]) -> i32 + 'a; /// 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, @@ -166,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 { @@ -223,7 +224,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 { @@ -411,6 +412,39 @@ impl Index { 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, + }, + }) + } + } + /// Get the full path to the index file on disk. /// /// Returns `None` if this is an in-memory index. @@ -484,6 +518,15 @@ impl Index { 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(()) + } + /// Remove all matching index entries. /// /// If you provide a callback function, it will be invoked on each matching @@ -595,12 +638,74 @@ impl Index { 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 @@ -718,15 +823,15 @@ impl Binding for IndexEntry { 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), @@ -745,7 +850,7 @@ mod tests { use std::path::Path; use tempfile::TempDir; - use crate::{Index, IndexEntry, IndexTime, Oid, Repository, ResetType}; + use crate::{ErrorCode, Index, IndexEntry, IndexTime, Oid, Repository, ResetType}; #[test] fn smoke() { @@ -848,9 +953,6 @@ mod tests { #[test] fn add_then_read() { - let mut index = Index::new().unwrap(); - assert!(index.add(&entry()).is_err()); - let mut index = Index::new().unwrap(); let mut e = entry(); e.path = b"foobar".to_vec(); @@ -859,6 +961,27 @@ mod tests { assert_eq!(e.path.len(), 6); } + #[test] + fn add_then_find() { + let mut index = Index::new().unwrap(); + let mut e = entry(); + 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(); diff --git a/src/indexer.rs b/src/indexer.rs index 1003abeb76..3a3ff62a5a 100644 --- a/src/indexer.rs +++ b/src/indexer.rs @@ -1,7 +1,12 @@ -use std::marker; +use std::ffi::CStr; +use std::path::Path; +use std::{io, marker, mem, ptr}; -use crate::raw; +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> { @@ -94,3 +99,157 @@ impl<'a> Binding for Progress<'a> { )] #[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 52b49fd79f..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 @@ -65,7 +65,7 @@ //! source `Repository`, to ensure that they do not outlive the repository //! itself. -#![doc(html_root_url = "https://docs.rs/git2/0.13")] +#![doc(html_root_url = "https://docs.rs/git2/0.21")] #![allow(trivial_numeric_casts, trivial_casts)] #![deny(missing_docs)] #![warn(rust_2018_idioms)] @@ -88,20 +88,27 @@ 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, CredentialHelper}; +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}; +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::{IndexerProgress, Progress}; +pub use crate::indexer::{Indexer, IndexerProgress, Progress}; pub use crate::mailmap::Mailmap; pub use crate::mempack::Mempack; -pub use crate::merge::{AnnotatedCommit, MergeOptions}; -pub use crate::message::{message_prettify, DEFAULT_COMMENT_CHAR}; +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}; @@ -111,29 +118,32 @@ 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, + FetchOptions, PushOptions, Refspecs, Remote, RemoteConnection, RemoteHead, RemoteRedirect, }; -pub use crate::remote_callbacks::{Credentials, RemoteCallbacks}; +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}; +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::IntoCString; +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 @@ -208,6 +218,10 @@ pub enum ErrorCode { 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 @@ -240,7 +254,7 @@ pub enum ErrorClass { Object, /// Network error Net, - /// Error manpulating a tag + /// Error manipulating a tag Tag, /// Invalid value in tree Tree, @@ -248,7 +262,7 @@ pub enum ErrorClass { Indexer, /// Error from SSL Ssl, - /// Error involing submodules + /// Error involving submodules Submodule, /// Threading error Thread, @@ -305,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, @@ -315,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, @@ -340,7 +354,7 @@ pub enum ObjectType { Tag, } -/// An enumeration of all possile kinds of references. +/// 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. @@ -375,6 +389,8 @@ 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 @@ -405,6 +421,7 @@ pub enum FileFavor { bitflags! { /// Orderings that may be specified for Revwalk iteration. + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] pub struct Sort: u32 { /// Sort the repository contents in no particular ordering. /// @@ -439,6 +456,7 @@ impl Sort { bitflags! { /// Types of credentials that can be requested by a credential callback. + #[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; @@ -475,6 +493,7 @@ impl Default for CredentialType { bitflags! { /// Flags for the `flags` field of an IndexEntry. + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] pub struct IndexEntryFlag: u16 { /// Set when the `extended_flags` field is valid. const EXTENDED = raw::GIT_INDEX_ENTRY_EXTENDED as u16; @@ -490,6 +509,7 @@ impl IndexEntryFlag { bitflags! { /// Flags for the `extended_flags` field of an IndexEntry. + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] pub struct IndexEntryExtendedFlag: u16 { /// An "intent to add" entry from "git add -N" const INTENT_TO_ADD = raw::GIT_INDEX_ENTRY_INTENT_TO_ADD as u16; @@ -509,10 +529,11 @@ impl IndexEntryExtendedFlag { bitflags! { /// Flags for APIs that add files matching pathspec + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] pub struct IndexAddOption: u32 { - #[allow(missing_docs)] + /// Adds files that are not ignored to the index const DEFAULT = raw::GIT_INDEX_ADD_DEFAULT as u32; - #[allow(missing_docs)] + /// Allows adding otherwise ignored files to the index const FORCE = raw::GIT_INDEX_ADD_FORCE as u32; #[allow(missing_docs)] const DISABLE_PATHSPEC_MATCH = @@ -540,6 +561,7 @@ impl Default for IndexAddOption { bitflags! { /// Flags for `Repository::open_ext` + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] pub struct RepositoryOpenFlags: u32 { /// Only open the specified path; don't walk upward searching. const NO_SEARCH = raw::GIT_REPOSITORY_OPEN_NO_SEARCH as u32; @@ -564,6 +586,7 @@ impl RepositoryOpenFlags { bitflags! { /// Flags for the return value of `Repository::revparse` + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] pub struct RevparseMode: u32 { /// The spec targeted a single object const SINGLE = raw::GIT_REVPARSE_SINGLE as u32; @@ -582,6 +605,7 @@ impl RevparseMode { 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; @@ -612,6 +636,7 @@ impl MergeAnalysis { 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. @@ -631,6 +656,29 @@ impl MergePreference { 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; @@ -658,6 +706,7 @@ mod config; mod cred; mod describe; mod diff; +mod email; mod error; mod index; mod indexer; @@ -673,6 +722,7 @@ mod packbuilder; mod patch; mod pathspec; mod proxy_options; +mod push_update; mod rebase; mod reference; mod reflog; @@ -690,9 +740,11 @@ mod submodule; mod tag; mod tagforeach; mod time; +mod tracing; mod transaction; mod tree; mod treebuilder; +mod version; mod worktree; fn init() { @@ -823,6 +875,7 @@ fn openssl_env_init() { // 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(); } @@ -928,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), @@ -972,6 +1026,7 @@ 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. + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] pub struct Status: u32 { #[allow(missing_docs)] const CURRENT = raw::GIT_STATUS_CURRENT as u32; @@ -997,6 +1052,8 @@ bitflags! { const WT_TYPECHANGE = raw::GIT_STATUS_WT_TYPECHANGE as u32; #[allow(missing_docs)] const WT_RENAMED = raw::GIT_STATUS_WT_RENAMED as u32; + #[allow(missing_docs)] + const WT_UNREADABLE = raw::GIT_STATUS_WT_UNREADABLE as u32; #[allow(missing_docs)] const IGNORED = raw::GIT_STATUS_IGNORED as u32; @@ -1022,6 +1079,7 @@ impl Status { bitflags! { /// Mode options for RepositoryInitOptions + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] pub struct RepositoryInitMode: u32 { /// Use permissions configured by umask - the default const SHARED_UMASK = raw::GIT_REPOSITORY_INIT_SHARED_UMASK as u32; @@ -1075,6 +1133,8 @@ pub enum FileMode { Tree, /// Blob Blob, + /// Group writable blob. Obsolete mode kept for compatibility reasons + BlobGroupWritable, /// Blob executable BlobExecutable, /// Link @@ -1089,6 +1149,7 @@ impl From for i32 { 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, @@ -1102,6 +1163,7 @@ impl From for u32 { 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, @@ -1149,7 +1211,8 @@ bitflags! { /// /// Lastly, the following will only be returned for ignore "NONE". /// - /// * WD_UNTRACKED - wd contains untracked files + /// * WD_UNTRACKED - workdir contains untracked files + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] pub struct SubmoduleStatus: u32 { #[allow(missing_docs)] const IN_HEAD = raw::GIT_SUBMODULE_STATUS_IN_HEAD as u32; @@ -1205,7 +1268,7 @@ impl SubmoduleStatus { /// 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(Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum SubmoduleIgnore { /// Use the submodule's configuration Unspecified, @@ -1225,7 +1288,7 @@ pub enum SubmoduleIgnore { /// 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(Debug)] +#[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. @@ -1246,11 +1309,12 @@ pub enum SubmoduleUpdate { bitflags! { /// ... + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] pub struct PathspecFlags: u32 { /// Use the default pathspec matching configuration. const DEFAULT = raw::GIT_PATHSPEC_DEFAULT as u32; /// Force matching to ignore case, otherwise matching will use native - /// case sensitivity fo the platform filesystem. + /// 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. @@ -1291,6 +1355,7 @@ impl Default for PathspecFlags { bitflags! { /// Types of notifications emitted from checkouts. + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] pub struct CheckoutNotificationType: u32 { /// Notification about a conflict. const CONFLICT = raw::GIT_CHECKOUT_NOTIFY_CONFLICT as u32; @@ -1314,7 +1379,7 @@ impl CheckoutNotificationType { } /// Possible output formats for diff data -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum DiffFormat { /// full git diff Patch, @@ -1332,6 +1397,7 @@ pub enum DiffFormat { bitflags! { /// Formatting options for diff stats + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] pub struct DiffStatsFormat: raw::git_diff_stats_format_t { /// Don't generate any stats const NONE = raw::GIT_DIFF_STATS_NONE; @@ -1356,6 +1422,7 @@ impl DiffStatsFormat { } /// Automatic tag following options. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum AutotagOption { /// Use the setting from the remote's configuration Unspecified, @@ -1368,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, @@ -1378,7 +1446,7 @@ pub enum FetchPrune { } #[allow(missing_docs)] -#[derive(Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum StashApplyProgress { /// None None, @@ -1400,6 +1468,7 @@ pub enum StashApplyProgress { 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; @@ -1422,6 +1491,7 @@ impl Default for StashApplyFlags { 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; @@ -1434,6 +1504,8 @@ bitflags! { /// 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; } } @@ -1452,6 +1524,7 @@ impl Default for StashFlags { 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; @@ -1472,6 +1545,7 @@ impl Default for AttrCheckFlags { 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; @@ -1493,10 +1567,11 @@ impl DiffFlags { 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; - /// Constrol whether one-level refname are accepted (i.e., refnames that + /// 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`). @@ -1539,8 +1614,76 @@ mod tests { #[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/mempack.rs b/src/mempack.rs index 2de9a27813..a780707913 100644 --- a/src/mempack.rs +++ b/src/mempack.rs @@ -16,7 +16,7 @@ impl<'odb> Binding for Mempack<'odb> { unsafe fn from_raw(raw: *mut raw::git_odb_backend) -> Mempack<'odb> { Mempack { - raw: raw, + raw, _marker: marker::PhantomData, } } diff --git a/src/merge.rs b/src/merge.rs index 4c2749bf9c..bdb32970a9 100644 --- a/src/merge.rs +++ b/src/merge.rs @@ -1,10 +1,13 @@ -use libc::c_uint; +use libc::{c_uint, c_ushort}; +use std::ffi::CString; use std::marker; use std::mem; +use std::ptr; use std::str; 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. @@ -22,6 +25,19 @@ 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 { @@ -178,7 +194,7 @@ 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> { AnnotatedCommit { - raw: raw, + raw, _marker: marker::PhantomData, } } @@ -192,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 7c17eeffe0..a7041da3ae 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,4 +1,8 @@ +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}; @@ -31,12 +35,220 @@ fn _message_prettify(message: CString, comment_char: Option) -> Result = 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 crate::{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. @@ -58,4 +270,80 @@ mod tests { "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 99f7a686e8..50e5800fe7 100644 --- a/src/note.rs +++ b/src/note.rs @@ -53,7 +53,7 @@ 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, + raw, _marker: marker::PhantomData, } } @@ -80,7 +80,7 @@ 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, + raw, _marker: marker::PhantomData, } } diff --git a/src/object.rs b/src/object.rs index 278c55d8c0..fcae0066cb 100644 --- a/src/object.rs +++ b/src/object.rs @@ -232,7 +232,7 @@ impl<'repo> Binding for Object<'repo> { unsafe fn from_raw(raw: *mut raw::git_object) -> Object<'repo> { Object { - raw: raw, + raw, _marker: marker::PhantomData, } } diff --git a/src/odb.rs b/src/odb.rs index 7c53e41d26..2019908c48 100644 --- a/src/odb.rs +++ b/src/odb.rs @@ -1,16 +1,17 @@ use std::io; use std::marker; -use std::mem::MaybeUninit; use std::ptr; use std::slice; use std::ffi::CString; -use libc::{c_char, c_int, c_void, size_t}; +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, Oid, Progress}; +use crate::{ + raw, Error, IndexerProgress, Mempack, Object, ObjectType, OdbLookupFlags, Oid, Progress, +}; /// A structure to represent a git object database pub struct Odb<'repo> { @@ -18,12 +19,16 @@ pub struct Odb<'repo> { _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: raw, + raw, _marker: marker::PhantomData, } } @@ -156,7 +161,6 @@ impl<'repo> Odb<'repo> { /// Create stream for writing a pack file to the ODB pub fn packwriter(&self) -> Result, Error> { let mut out = ptr::null_mut(); - let progress = MaybeUninit::uninit(); 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); @@ -172,7 +176,7 @@ impl<'repo> Odb<'repo> { Ok(OdbPackwriter { raw: out, - progress, + progress: Default::default(), progress_payload_ptr, }) } @@ -182,6 +186,11 @@ impl<'repo> Odb<'repo> { 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 { @@ -229,7 +238,7 @@ impl<'repo> Odb<'repo> { /// 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: + /// mempack reference beyond the Odb's lifetime: /// /// ```compile_fail /// use git2::Odb; @@ -273,7 +282,7 @@ impl<'a> Binding for OdbObject<'a> { unsafe fn from_raw(raw: *mut raw::git_odb_object) -> OdbObject<'a> { OdbObject { - raw: raw, + raw, _marker: marker::PhantomData, } } @@ -322,12 +331,16 @@ pub struct OdbReader<'repo> { _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: raw, + raw, _marker: marker::PhantomData, } } @@ -351,7 +364,7 @@ impl<'repo> io::Read for OdbReader<'repo> { if res < 0 { Err(io::Error::new(io::ErrorKind::Other, "Read error")) } else { - Ok(len) + Ok(res as _) } } } @@ -363,13 +376,17 @@ pub struct OdbWriter<'repo> { _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() - /// Attepting write after finishing will be ignored. + /// 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], @@ -386,7 +403,7 @@ impl<'repo> Binding for OdbWriter<'repo> { unsafe fn from_raw(raw: *mut raw::git_odb_stream) -> OdbWriter<'repo> { OdbWriter { - raw: raw, + raw, _marker: marker::PhantomData, } } @@ -419,14 +436,14 @@ impl<'repo> io::Write for OdbWriter<'repo> { } } -struct OdbPackwriterCb<'repo> { - cb: Option>>, +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: MaybeUninit, + progress: raw::git_indexer_progress, progress_payload_ptr: *mut OdbPackwriterCb<'repo>, } @@ -436,12 +453,12 @@ impl<'repo> OdbPackwriter<'repo> { unsafe { let writepack = &*self.raw; let res = match writepack.commit { - Some(commit) => commit(self.raw, self.progress.as_mut_ptr()), + Some(commit) => commit(self.raw, &mut self.progress), None => -1, }; if res < 0 { - Err(Error::last_error(res).unwrap()) + Err(Error::last_error(res)) } else { Ok(res) } @@ -470,7 +487,7 @@ impl<'repo> io::Write for OdbPackwriter<'repo> { let writepack = &*self.raw; let res = match writepack.append { - Some(append) => append(self.raw, ptr, len, self.progress.as_mut_ptr()), + Some(append) => append(self.raw, ptr, len, &mut self.progress), None => -1, }; @@ -495,7 +512,7 @@ impl<'repo> Drop for OdbPackwriter<'repo> { None => (), }; - Box::from_raw(self.progress_payload_ptr); + drop(Box::from_raw(self.progress_payload_ptr)); } } } @@ -523,7 +540,7 @@ extern "C" fn foreach_cb(id: *const raw::git_oid, payload: *mut c_void) -> c_int .unwrap_or(1) } -extern "C" fn write_pack_progress_cb( +pub(crate) extern "C" fn write_pack_progress_cb( stats: *const raw::git_indexer_progress, payload: *mut c_void, ) -> c_int { @@ -709,4 +726,45 @@ mod tests { 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 207ecd6840..35516cb181 100644 --- a/src/oid.rs +++ b/src/oid.rs @@ -1,4 +1,3 @@ -use libc; use std::cmp::Ordering; use std::fmt; use std::hash::{Hash, Hasher}; @@ -35,7 +34,7 @@ impl Oid { s.len() as libc::size_t )); } - Ok(Oid { raw: raw }) + Ok(Oid { raw }) } /// Parse a raw object id into an Oid structure. @@ -52,7 +51,7 @@ impl Oid { unsafe { try_call!(raw::git_oid_fromraw(&mut raw, bytes.as_ptr())); } - Ok(Oid { raw: raw }) + Ok(Oid { raw }) } } diff --git a/src/oid_array.rs b/src/oid_array.rs index 914681ecc6..0d87ce9954 100644 --- a/src/oid_array.rs +++ b/src/oid_array.rs @@ -10,7 +10,7 @@ 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 { @@ -32,7 +32,7 @@ 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 diff --git a/src/opts.rs b/src/opts.rs index 789034d41d..232d81e996 100644 --- a/src/opts.rs +++ b/src/opts.rs @@ -1,9 +1,11 @@ //! 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}; +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. @@ -69,12 +71,88 @@ pub unsafe fn get_search_path(level: ConfigLevel) -> Result { 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, @@ -91,6 +169,7 @@ pub fn strict_object_creation(enabled: bool) { /// 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, @@ -102,6 +181,296 @@ pub fn strict_hash_verification(enabled: bool) { 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::*; @@ -110,4 +479,52 @@ mod 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 e20bf2a97c..de47bbce32 100644 --- a/src/packbuilder.rs +++ b/src/packbuilder.rs @@ -1,9 +1,13 @@ use libc::{c_int, c_uint, c_void, size_t}; use std::marker; +use std::path::Path; use std::ptr; use std::slice; +use std::str; +use crate::odb::{write_pack_progress_cb, OdbPackwriterCb}; use crate::util::Binding; +use crate::IntoCString; use crate::{panic, raw, Buf, Error, Oid, Repository, Revwalk}; #[derive(PartialEq, Eq, Clone, Debug, Copy)] @@ -83,6 +87,26 @@ 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 @@ -160,6 +184,8 @@ impl<'repo> PackBuilder<'repo> { /// 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))) } @@ -167,6 +193,25 @@ impl<'repo> PackBuilder<'repo> { 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> { @@ -248,7 +293,10 @@ extern "C" fn progress_c( #[cfg(test)] mod tests { - use crate::Buf; + 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 { [].iter() @@ -284,10 +332,26 @@ mod tests { 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) = crate::test::repo_init(); @@ -341,6 +405,41 @@ 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; @@ -376,6 +475,23 @@ 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(); diff --git a/src/patch.rs b/src/patch.rs index 2ff112b2c3..d44e87e925 100644 --- a/src/patch.rs +++ b/src/patch.rs @@ -21,7 +21,7 @@ impl<'buffers> Binding for Patch<'buffers> { type Raw = *mut raw::git_patch; unsafe fn from_raw(raw: Self::Raw) -> Self { Patch { - raw: raw, + raw, buffers: PhantomData, } } @@ -125,7 +125,7 @@ impl<'buffers> Patch<'buffers> { } /// Get the DiffDelta associated with the Patch. - pub fn delta(&self) -> DiffDelta<'buffers> { + pub fn delta<'a>(&'a self) -> DiffDelta<'a> { unsafe { Binding::from_raw(raw::git_patch_get_delta(self.raw) as *mut _) } } @@ -151,7 +151,7 @@ impl<'buffers> Patch<'buffers> { } /// Get a DiffHunk and its total line count from the Patch. - pub fn hunk(&self, hunk_idx: usize) -> Result<(DiffHunk<'buffers>, usize), Error> { + pub fn hunk<'a>(&'a self, hunk_idx: usize) -> Result<(DiffHunk<'a>, usize), Error> { let mut ret = ptr::null(); let mut lines = 0; unsafe { @@ -168,11 +168,11 @@ impl<'buffers> Patch<'buffers> { } /// Get a DiffLine from a hunk of the Patch. - pub fn line_in_hunk( - &self, + pub fn line_in_hunk<'a>( + &'a self, hunk_idx: usize, line_of_hunk: usize, - ) -> Result, Error> { + ) -> Result, Error> { let mut ret = ptr::null(); unsafe { try_call!(raw::git_patch_get_line_in_hunk( diff --git a/src/pathspec.rs b/src/pathspec.rs index e5fa0493d6..16850dc210 100644 --- a/src/pathspec.rs +++ b/src/pathspec.rs @@ -1,5 +1,5 @@ use libc::size_t; -use std::iter::IntoIterator; +use std::iter::FusedIterator; use std::marker; use std::ops::Range; use std::path::Path; @@ -168,7 +168,7 @@ 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 @@ -268,7 +268,7 @@ impl<'ps> Binding for PathspecMatchList<'ps> { unsafe fn from_raw(raw: *mut raw::git_pathspec_match_list) -> PathspecMatchList<'ps> { PathspecMatchList { - raw: raw, + raw, _marker: marker::PhantomData, } } @@ -297,6 +297,7 @@ impl<'list> DoubleEndedIterator for PathspecEntries<'list> { 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> { @@ -313,6 +314,7 @@ impl<'list> DoubleEndedIterator for PathspecDiffEntries<'list> { 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> { @@ -331,6 +333,7 @@ impl<'list> DoubleEndedIterator for PathspecFailedEntries<'list> { .and_then(|i| self.list.failed_entry(i)) } } +impl<'list> FusedIterator for PathspecFailedEntries<'list> {} impl<'list> ExactSizeIterator for PathspecFailedEntries<'list> {} #[cfg(test)] 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 index e44470b820..2bf8fe3e8a 100644 --- a/src/rebase.rs +++ b/src/rebase.rs @@ -230,7 +230,7 @@ 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: raw, + raw, _marker: marker::PhantomData, } } @@ -324,7 +324,7 @@ 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: raw, + raw, _marker: marker::PhantomData, } } diff --git a/src/reference.rs b/src/reference.rs index cc08b0dfa6..0af845d7c5 100644 --- a/src/reference.rs +++ b/src/reference.rs @@ -8,15 +8,21 @@ use std::str; use crate::object::CastOrPanic; use crate::util::{c_cmp_to_ordering, Binding}; use crate::{ - raw, Blob, Commit, Error, Object, ObjectType, Oid, ReferenceFormat, ReferenceType, Repository, - Tag, Tree, + 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; -struct Refdb<'repo>(&'repo Repository); +/// 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]. /// @@ -62,7 +68,15 @@ impl<'repo> Reference<'repo> { pub fn is_valid_name(refname: &str) -> bool { 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. @@ -361,6 +375,35 @@ impl<'repo> Reference<'repo> { 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> { @@ -387,7 +430,7 @@ 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, + raw, _marker: marker::PhantomData, } } @@ -419,7 +462,7 @@ 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, + raw, _marker: marker::PhantomData, } } @@ -463,13 +506,23 @@ mod tests { 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] + #[should_panic] + fn is_valid_name_for_invalid_ref() { + Reference::is_valid_name("ab\012"); } #[test] - fn smoke2() { + fn smoke() { let (_td, repo) = crate::test::repo_init(); let mut head = repo.head().unwrap(); assert!(head.is_branch()); @@ -512,6 +565,14 @@ mod tests { .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(); { diff --git a/src/reflog.rs b/src/reflog.rs index 4bd6f90f06..bbd2140ab2 100644 --- a/src/reflog.rs +++ b/src/reflog.rs @@ -1,4 +1,5 @@ use libc::size_t; +use std::iter::FusedIterator; use std::marker; use std::ops::Range; use std::str; @@ -103,7 +104,7 @@ 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 @@ -151,7 +152,7 @@ impl<'reflog> Binding for ReflogEntry<'reflog> { unsafe fn from_raw(raw: *const raw::git_reflog_entry) -> ReflogEntry<'reflog> { ReflogEntry { - raw: raw, + raw, _marker: marker::PhantomData, } } @@ -174,6 +175,7 @@ impl<'reflog> DoubleEndedIterator for ReflogIter<'reflog> { 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)] diff --git a/src/refspec.rs b/src/refspec.rs index dec71c3230..3f62e991c7 100644 --- a/src/refspec.rs +++ b/src/refspec.rs @@ -112,7 +112,7 @@ impl<'remote> Binding for Refspec<'remote> { unsafe fn from_raw(raw: *const raw::git_refspec) -> Refspec<'remote> { Refspec { - raw: raw, + raw, _marker: marker::PhantomData, } } diff --git a/src/remote.rs b/src/remote.rs index 042a3caa45..0c13a53fcf 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -1,16 +1,18 @@ -use libc; -use std::ffi::CString; +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 std::{ffi::CString, os::raw::c_char}; use crate::string_array::StringArray; use crate::util::Binding; -use crate::{raw, Buf, Direction, Error, FetchPrune, Oid, ProxyOptions, Refspec}; -use crate::{AutotagOption, Progress, RemoteCallbacks, Repository}; +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. /// @@ -39,10 +41,14 @@ 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. @@ -50,6 +56,11 @@ 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 @@ -59,10 +70,25 @@ pub struct RemoteConnection<'repo, 'connection, '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); - return ret; + ret } impl<'repo> Remote<'repo> { @@ -70,16 +96,24 @@ impl<'repo> Remote<'repo> { pub fn is_valid_name(remote_name: &str) -> bool { 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 + /// 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: &str) -> Result, Error> { + pub fn create_detached>>(url: S) -> Result, Error> { crate::init(); let mut ret = ptr::null_mut(); let url = CString::new(url)?; @@ -104,26 +138,28 @@ impl<'repo> Remote<'repo> { 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(&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 { crate::opt_bytes(self, raw::git_remote_url(&*self.raw)).unwrap() } + unsafe { crate::opt_bytes(self, raw::git_remote_url(&*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(&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 { crate::opt_bytes(self, raw::git_remote_pushurl(&*self.raw)) } } @@ -224,7 +260,7 @@ impl<'repo> Remote<'repo> { /// Cancel the operation /// /// At certain points in its operation, the network code checks whether the - /// operation has been cancelled and if so stops the operation. + /// 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)); @@ -258,7 +294,7 @@ impl<'repo> Remote<'repo> { /// /// # Examples /// - /// Example of functionality similar to `git fetch origin/main`: + /// Example of functionality similar to `git fetch origin main`: /// /// ```no_run /// fn fetch_origin_main(repo: git2::Repository) -> Result<(), git2::Error> { @@ -287,7 +323,7 @@ impl<'repo> Remote<'repo> { pub fn update_tips( &mut self, callbacks: Option<&mut RemoteCallbacks<'_>>, - update_fetchhead: bool, + update_flags: RemoteUpdateFlags, download_tags: AutotagOption, msg: Option<&str>, ) -> Result<(), Error> { @@ -297,7 +333,7 @@ impl<'repo> Remote<'repo> { try_call!(raw::git_remote_update_tips( self.raw, cbs.as_ref(), - update_fetchhead, + update_flags.bits() as c_uint, download_tags, msg )); @@ -401,7 +437,7 @@ impl<'repo> Binding for Remote<'repo> { unsafe fn from_raw(raw: *mut raw::git_remote) -> Remote<'repo> { Remote { - raw: raw, + raw, _marker: marker::PhantomData, } } @@ -432,6 +468,7 @@ impl<'repo> DoubleEndedIterator for Refspecs<'repo> { .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 :( @@ -472,8 +509,12 @@ impl<'cb> FetchOptions<'cb> { 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 } } @@ -499,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 } @@ -511,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> { @@ -533,12 +615,16 @@ impl<'cb> Binding for FetchOptions<'cb> { .map(|m| m.raw()) .unwrap_or_else(|| ProxyOptions::new().raw()), prune: crate::call::convert(&self.prune), - update_fetchhead: crate::call::convert(&self.update_fetchhead), + // `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), - // TODO: expose this as a builder option - custom_headers: raw::git_strarray { - count: 0, - strings: ptr::null_mut(), + 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 _, }, } } @@ -557,16 +643,21 @@ impl<'cb> PushOptions<'cb> { 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 fetch operation. + /// 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 @@ -582,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> { @@ -604,10 +729,14 @@ impl<'cb> Binding for PushOptions<'cb> { .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: ptr::null_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 _, }, } } @@ -647,9 +776,25 @@ impl<'repo, 'connection, 'cb> Drop for RemoteConnection<'repo, 'connection, 'cb> } } +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}; + use crate::{AutotagOption, PushOptions, RemoteUpdateFlags}; use crate::{Direction, FetchOptions, Remote, RemoteCallbacks, Repository}; use std::cell::Cell; use tempfile::TempDir; @@ -738,10 +883,20 @@ mod tests { origin.fetch(&[] as &[&str], None, None).unwrap(); origin.fetch(&[] as &[&str], None, Some("foo")).unwrap(); origin - .update_tips(None, true, AutotagOption::Unspecified, None) + .update_tips( + None, + RemoteUpdateFlags::UPDATE_FETCHHEAD, + AutotagOption::Unspecified, + None, + ) .unwrap(); origin - .update_tips(None, true, AutotagOption::All, Some("foo")) + .update_tips( + None, + RemoteUpdateFlags::UPDATE_FETCHHEAD, + AutotagOption::All, + Some("foo"), + ) .unwrap(); t!(repo.remote_add_fetch("origin", "foo")); @@ -767,11 +922,17 @@ 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) = crate::test::repo_init(); @@ -872,7 +1033,7 @@ mod tests { 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] @@ -916,4 +1077,94 @@ mod tests { 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(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 bcc73e85e9..2df2e7b015 100644 --- a/src/remote_callbacks.rs +++ b/src/remote_callbacks.rs @@ -1,5 +1,5 @@ use libc::{c_char, c_int, c_uint, c_void, size_t}; -use std::ffi::{CStr, CString}; +use std::ffi::CStr; use std::mem; use std::ptr; use std::slice; @@ -9,6 +9,7 @@ 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 @@ -25,14 +26,15 @@ pub struct RemoteCallbacks<'a> { update_tips: Option>>, certificate_check: Option>>, 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. +/// * `allowed_types` - a bitmask stating which cred types are OK to return. pub type Credentials<'a> = dyn FnMut(&str, Option<&str>, CredentialType) -> Result + 'a; @@ -46,12 +48,23 @@ 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> = dyn 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. /// @@ -63,19 +76,30 @@ pub type PushUpdateReference<'a> = dyn FnMut(&str, Option<&str>) -> Result<(), E /// Callback for push transfer progress /// /// Parameters: -/// * current -/// * total -/// * bytes +/// * 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 +/// * 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() @@ -94,6 +118,7 @@ impl<'a> RemoteCallbacks<'a> { certificate_check: None, push_update_reference: None, push_progress: None, + push_negotiation: None, } } @@ -162,7 +187,7 @@ impl<'a> RemoteCallbacks<'a> { /// connection to proceed. pub fn certificate_check(&mut self, cb: F) -> &mut RemoteCallbacks<'a> where - F: FnMut(&Cert<'_>, &str) -> bool + 'a, + F: FnMut(&Cert<'_>, &str) -> Result + 'a, { self.certificate_check = Some(Box::new(cb) as Box>); self @@ -182,6 +207,11 @@ impl<'a> RemoteCallbacks<'a> { } /// 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, @@ -191,8 +221,14 @@ impl<'a> RemoteCallbacks<'a> { } /// 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, @@ -200,6 +236,20 @@ impl<'a> RemoteCallbacks<'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> { @@ -245,6 +295,9 @@ impl<'a> Binding for RemoteCallbacks<'a> { ) -> 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 _; callbacks } @@ -277,11 +330,7 @@ extern "C" fn credentials_cb( let cred_type = CredentialType::from_bits_truncate(allowed_types as u32); - callback(url, username_from_url, cred_type).map_err(|e| { - let s = CString::new(e.to_string()).unwrap(); - raw::git_error_set_str(e.raw_code() as c_int, s.as_ptr()); - 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)) => { @@ -371,16 +420,20 @@ extern "C" fn certificate_check_cb( 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(); 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 + } } } @@ -403,7 +456,7 @@ extern "C" fn push_update_reference_cb( }; match callback(refname, status) { Ok(()) => 0, - Err(e) => e.raw_code(), + Err(e) => e.raw_set_git_error(), } }) .unwrap_or(-1) @@ -450,3 +503,24 @@ extern "C" fn pack_progress_cb( }) .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 539aafa8f2..07d3a7c55f 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -1,9 +1,8 @@ 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; @@ -11,30 +10,93 @@ 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::mailmap::Mailmap; use crate::oid_array::OidArray; -use crate::stash::{stash_cb, StashApplyOptions, StashCbData}; +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, MergeOptions, MergePreference, SubmoduleIgnore, - SubmoduleStatus, SubmoduleUpdate, + 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, Oid, Tree}; +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. /// @@ -94,7 +156,7 @@ impl Repository { /// Find and open an existing repository, respecting git environment /// variables. This acts like `open_ext` with the - /// `REPOSITORY_OPEN_FROM_ENV` flag, but additionally respects `$GIT_DIR`. + /// [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 { @@ -114,30 +176,32 @@ impl Repository { /// 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 REPOSITORY_OPEN_NO_DOTGIT, don't try appending + /// If flags contains [NO_DOTGIT](RepositoryOpenFlags::NO_DOTGIT), don't try appending /// `/.git` to `path`. /// - /// If flags contains REPOSITORY_OPEN_FROM_ENV, `open_ext` will ignore + /// 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. + /// 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, @@ -198,6 +262,33 @@ impl Repository { 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 @@ -367,6 +458,18 @@ impl Repository { } } + /// 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()) + } + } + /// Returns the current state of this repository pub fn state(&self) -> RepositoryState { let state = unsafe { raw::git_repository_state(self.raw) }; @@ -538,7 +641,7 @@ impl Repository { /// 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, Error> { @@ -619,7 +722,7 @@ impl Repository { 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 @@ -633,7 +736,7 @@ impl Repository { 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 @@ -729,6 +832,22 @@ impl Repository { /// Otherwise, the HEAD will be detached and will directly point to the /// commit. pub fn set_head(&self, refname: &str) -> Result<(), Error> { + 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)); @@ -743,14 +862,14 @@ impl Repository { match value { 0 => Ok(false), 1 => Ok(true), - _ => Err(Error::last_error(value).unwrap()), + _ => 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 @@ -770,7 +889,7 @@ impl Repository { /// 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 /// unaltered and an error is returned. @@ -924,6 +1043,11 @@ 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 = ptr::null_mut(); unsafe { @@ -1123,7 +1247,7 @@ impl Repository { /// /// This behaves like `Repository::branch()` but takes /// an annotated commit, which lets you specify which - /// extended sha syntax string was specified by a user, + /// extended SHA syntax string was specified by a user, /// allowing for more exact reflog messages. /// /// See the documentation for `Repository::branch()` @@ -1307,6 +1431,20 @@ impl Repository { } } + /// 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 { @@ -1334,6 +1472,25 @@ impl Repository { } } + /// 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_prefix( + &mut raw, + self.raw(), + Oid::from_str(prefix_hash)?.raw(), + prefix_hash.len(), + kind + )); + Ok(Binding::from_raw(raw)) + } + } + /// Create a new direct reference. /// /// This function will return an error if a reference already exists with @@ -1419,6 +1576,19 @@ impl Repository { /// 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. @@ -1757,6 +1927,38 @@ impl Repository { } } + /// 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. @@ -1793,6 +1995,20 @@ impl Repository { } } + /// 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 @@ -1827,8 +2043,10 @@ impl Repository { } } - /// iterate over all tags calling `cb` on each. - /// the callback is provided the tag id and name + /// 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, @@ -1849,6 +2067,13 @@ impl Repository { /// Updates files in the index and the working tree to match the content of /// the commit pointed at by HEAD. + /// + /// 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(); @@ -1921,7 +2146,7 @@ impl Repository { /// /// For compatibility with git, the repository is put into a merging state. /// Once the commit is done (or if the user wishes to abort), you should - /// clear this state by calling cleanup_state(). + /// clear this state by calling [`cleanup_state()`][Repository::cleanup_state]. pub fn merge( &self, annotated_commits: &[&AnnotatedCommit<'_>], @@ -2253,6 +2478,38 @@ impl Repository { } /// 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], @@ -2269,6 +2526,23 @@ impl Repository { } } + /// 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_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 _)) + } + } + /// Find all merge bases between two commits pub fn merge_bases(&self, one: Oid, two: Oid) -> Result { let mut arr = raw::git_oidarray { @@ -2303,6 +2577,33 @@ impl Repository { } } + /// 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 /// /// There is no need for branches containing the commits to have any @@ -2325,6 +2626,9 @@ impl Repository { } /// Determine if a commit is the descendant of another commit + /// + /// 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( @@ -2685,6 +2989,25 @@ impl Repository { } } + /// 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, @@ -2782,7 +3105,7 @@ impl Repository { let raw_opts = options.map(|o| o.raw()); let ptr_raw_opts = match raw_opts.as_ref() { Some(v) => v, - None => 0 as *const _, + None => std::ptr::null(), }; unsafe { try_call!(raw::git_cherrypick(self.raw(), commit.raw(), ptr_raw_opts)); @@ -2836,6 +3159,8 @@ impl Repository { } /// 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 { @@ -2849,6 +3174,19 @@ impl Repository { } } + /// 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, @@ -2868,6 +3206,26 @@ impl Repository { } } + /// 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, @@ -2972,6 +3330,52 @@ impl Repository { 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 { @@ -3081,7 +3485,7 @@ impl RepositoryInitOptions { /// 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 { @@ -3153,7 +3557,7 @@ impl RepositoryInitOptions { #[cfg(test)] mod tests { use crate::build::CheckoutBuilder; - use crate::CherrypickOptions; + use crate::{CherrypickOptions, MergeFileOptions}; use crate::{ ObjectType, Oid, Repository, ResetType, Signature, SubmoduleIgnore, SubmoduleUpdate, }; @@ -3253,6 +3657,34 @@ mod tests { ); } + #[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().unwrap(); @@ -3367,6 +3799,19 @@ mod tests { 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) = crate::test::repo_init(); @@ -3379,6 +3824,17 @@ mod tests { 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 @@ -3469,6 +3925,10 @@ mod tests { // 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: @@ -3603,6 +4063,102 @@ mod tests { 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] fn smoke_revparse_ext() { let (_td, repo) = graph_repo_init(); @@ -3938,4 +4494,44 @@ Committer Name "#, 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/revspec.rs b/src/revspec.rs index 120f83a359..d2e08670af 100644 --- a/src/revspec.rs +++ b/src/revspec.rs @@ -14,11 +14,7 @@ impl<'repo> Revspec<'repo> { to: Option>, mode: RevparseMode, ) -> Revspec<'repo> { - Revspec { - from: from, - to: to, - mode: mode, - } + Revspec { from, to, mode } } /// Access the `from` range of this revspec. diff --git a/src/revwalk.rs b/src/revwalk.rs index 7730551092..7837f00d63 100644 --- a/src/revwalk.rs +++ b/src/revwalk.rs @@ -12,7 +12,7 @@ pub struct Revwalk<'repo> { _marker: marker::PhantomData<&'repo Repository>, } -/// A `Revwalk` with an assiciated "hide callback", see `with_hide_callback` +/// A `Revwalk` with an associated "hide callback", see `with_hide_callback` pub struct RevwalkWithHideCb<'repo, 'cb, C> where C: FnMut(Oid) -> bool, @@ -28,9 +28,10 @@ where panic::wrap(|| unsafe { let hide_cb = payload as *mut C; if (*hide_cb)(Oid::from_raw(commit_id)) { - return 1; + 1 + } else { + 0 } - return 0; }) .unwrap_or(-1) } @@ -80,7 +81,7 @@ impl<'repo> Revwalk<'repo> { /// 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 @@ -110,7 +111,7 @@ 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 = CString::new(glob)?; @@ -135,7 +136,7 @@ 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 = CString::new(reference)?; unsafe { @@ -156,7 +157,7 @@ impl<'repo> Revwalk<'repo> { /// the walk. pub fn with_hide_callback<'cb, C>( self, - callback: &'cb C, + callback: &'cb mut C, ) -> Result, Error> where C: FnMut(Oid) -> bool, @@ -169,7 +170,7 @@ impl<'repo> Revwalk<'repo> { raw::git_revwalk_add_hide_cb( r.revwalk.raw(), Some(revwalk_hide_cb::), - callback as *const _ as *mut c_void, + callback as *mut _ as *mut c_void, ); }; Ok(r) @@ -193,7 +194,7 @@ 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 = CString::new(glob)?; @@ -205,7 +206,7 @@ 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 = CString::new(reference)?; unsafe { @@ -219,7 +220,7 @@ 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, + raw, _marker: marker::PhantomData, } } @@ -303,8 +304,8 @@ mod tests { walk.reset().unwrap(); walk.push_head().unwrap(); - let hide_cb = |oid| oid == target; - let mut walk = walk.with_hide_callback(&hide_cb).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); diff --git a/src/signature.rs b/src/signature.rs index 5677b7b577..7c9ffb3933 100644 --- a/src/signature.rs +++ b/src/signature.rs @@ -1,4 +1,3 @@ -use libc; use std::ffi::CString; use std::fmt; use std::marker; @@ -105,7 +104,7 @@ 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, } @@ -158,6 +157,16 @@ impl<'a> fmt::Display for Signature<'a> { } } +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 crate::{Signature, Time}; diff --git a/src/stash.rs b/src/stash.rs index 97e02b5de6..ea898e46ba 100644 --- a/src/stash.rs +++ b/src/stash.rs @@ -1,21 +1,84 @@ use crate::build::CheckoutBuilder; -use crate::util::Binding; -use crate::{panic, raw, Oid, StashApplyProgress}; +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::CStr; +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; -#[allow(unused)] /// Stash application options structure pub struct StashApplyOptions<'cb> { progress: Option>>, @@ -81,22 +144,20 @@ impl<'cb> StashApplyOptions<'cb> { } } -#[allow(unused)] -pub struct StashCbData<'a> { +pub(crate) struct StashCbData<'a> { pub callback: &'a mut StashCb<'a>, } -#[allow(unused)] -pub extern "C" fn stash_cb( +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 mut data = &mut *(payload as *mut StashCbData<'_>); + let data = &mut *(payload as *mut StashCbData<'_>); let res = { - let mut callback = &mut data.callback; + let callback = &mut data.callback; callback( index, CStr::from_ptr(message).to_str().unwrap(), @@ -128,15 +189,14 @@ fn convert_progress(progress: raw::git_stash_apply_progress_t) -> StashApplyProg } } -#[allow(unused)] extern "C" fn stash_apply_progress_cb( progress: raw::git_stash_apply_progress_t, payload: *mut c_void, ) -> c_int { panic::wrap(|| unsafe { - let mut options = &mut *(payload as *mut StashApplyOptions<'_>); + let options = &mut *(payload as *mut StashApplyOptions<'_>); let res = { - let mut callback = options.progress.as_mut().unwrap(); + let callback = options.progress.as_mut().unwrap(); callback(convert_progress(progress)) }; @@ -151,12 +211,11 @@ extern "C" fn stash_apply_progress_cb( #[cfg(test)] mod tests { - use crate::stash::StashApplyOptions; + use crate::stash::{StashApplyOptions, StashSaveOptions}; use crate::test::repo_init; - use crate::{Repository, StashFlags, Status}; + use crate::{IndexAddOption, Repository, StashFlags, Status}; use std::fs; - use std::io::Write; - use std::path::Path; + use std::path::{Path, PathBuf}; fn make_stash(next: C) where @@ -167,10 +226,8 @@ mod tests { let p = Path::new(repo.workdir().unwrap()).join("file_b.txt"); println!("using path {:?}", p); - fs::File::create(&p) - .unwrap() - .write("data".as_bytes()) - .unwrap(); + + 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); @@ -240,10 +297,7 @@ mod tests { let p = Path::new(repo.workdir().unwrap()).join("file_b.txt"); - fs::File::create(&p) - .unwrap() - .write("data".as_bytes()) - .unwrap(); + fs::write(&p, "data".as_bytes()).unwrap(); repo.stash_save2(&signature, None, Some(StashFlags::INCLUDE_UNTRACKED)) .unwrap(); @@ -258,4 +312,37 @@ mod tests { 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 92858e48a5..a5a8cffd39 100644 --- a/src/status.rs +++ b/src/status.rs @@ -1,5 +1,6 @@ use libc::{c_char, c_uint, size_t}; use std::ffi::CString; +use std::iter::FusedIterator; use std::marker; use std::mem; use std::ops::Range; @@ -72,7 +73,7 @@ impl StatusOptions { 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(), } @@ -216,6 +217,14 @@ impl 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 @@ -264,7 +273,7 @@ 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, + raw, _marker: marker::PhantomData, } } @@ -295,8 +304,17 @@ impl<'a> DoubleEndedIterator for StatusIter<'a> { 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] { @@ -340,7 +358,7 @@ impl<'statuses> Binding for StatusEntry<'statuses> { unsafe fn from_raw(raw: *const raw::git_status_entry) -> StatusEntry<'statuses> { StatusEntry { - raw: raw, + raw, _marker: marker::PhantomData, } } diff --git a/src/string_array.rs b/src/string_array.rs index b924e4033a..c77ccdab96 100644 --- a/src/string_array.rs +++ b/src/string_array.rs @@ -1,5 +1,6 @@ //! Bindings to libgit2's raw `git_strarray` type +use std::iter::FusedIterator; use std::ops::Range; use std::str; @@ -8,7 +9,7 @@ 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 { @@ -79,7 +80,7 @@ impl StringArray { 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 @@ -108,6 +109,7 @@ impl<'a> DoubleEndedIterator for Iter<'a> { 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> { @@ -124,6 +126,7 @@ impl<'a> DoubleEndedIterator for IterBytes<'a> { 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 c8f72efbde..06a6359400 100644 --- a/src/submodule.rs +++ b/src/submodule.rs @@ -52,21 +52,21 @@ impl<'repo> Submodule<'repo> { } } - /// Get the submodule's url. + /// Get the submodule's URL. /// - /// Returns `None` if the url is not valid utf-8 or if the URL isn't present + /// Returns `None` if the URL is not valid utf-8 or if the URL isn't present pub fn url(&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] { self.opt_url_bytes().unwrap() } - /// Get the url for the submodule. + /// 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 @@ -139,6 +139,25 @@ 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 @@ -233,7 +252,7 @@ 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, + raw, _marker: marker::PhantomData, } } @@ -399,16 +418,54 @@ mod tests { let (_td, parent) = crate::test::repo_init(); let url1 = Url::from_file_path(&repo1.workdir().unwrap()).unwrap(); - let url3 = Url::from_file_path(&repo2.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(&url3.to_string(), Path::new("bar2"), true) + .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 697e4f2757..6986c7c160 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -1,10 +1,11 @@ +use std::ffi::CString; use std::marker; use std::mem; use std::ptr; use std::str; use crate::util::Binding; -use crate::{raw, signature, Error, Object, ObjectType, Oid, Signature}; +use crate::{call, raw, signature, Error, Object, ObjectType, Oid, Signature}; /// A structure to represent a git [tag][1] /// @@ -15,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)) } @@ -86,7 +100,7 @@ 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)) } } @@ -118,7 +132,7 @@ 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, + raw, _marker: marker::PhantomData, } } @@ -141,6 +155,30 @@ 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) = crate::test::repo_init(); diff --git a/src/test.rs b/src/test.rs index 89a13beafe..57a590f519 100644 --- a/src/test.rs +++ b/src/test.rs @@ -31,7 +31,7 @@ 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, &[]) + repo.commit(Some("HEAD"), &sig, &sig, "initial\n\nbody", &tree, &[]) .unwrap(); } (td, repo) @@ -66,7 +66,7 @@ pub fn worktrees_env_init(repo: &Repository) -> (TempDir, 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 { diff --git a/src/time.rs b/src/time.rs index a01de7abb0..46b5bd3f94 100644 --- a/src/time.rs +++ b/src/time.rs @@ -6,13 +6,13 @@ 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, } @@ -61,7 +61,7 @@ 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 @@ -73,8 +73,8 @@ impl IndexTime { pub fn new(seconds: i32, nanoseconds: u32) -> IndexTime { unsafe { Binding::from_raw(raw::git_index_time { - seconds: seconds, - nanoseconds: nanoseconds, + seconds, + nanoseconds, }) } } @@ -92,7 +92,7 @@ impl IndexTime { 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 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 index 80cb4dfe0f..4f661f1d48 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -7,7 +7,7 @@ use crate::{raw, util::Binding, Error, Oid, Reflog, Repository, Signature}; /// /// 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 comitting is not atomic: if an operation fails, the +/// 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, @@ -106,11 +106,11 @@ impl<'repo> Transaction<'repo> { /// Add a [`Reflog`] to the transaction. /// /// This commit the in-memory [`Reflog`] to disk when the transaction commits. - /// Note that atomicty is **not* guaranteed: if the transaction fails to - /// modify `refname`, the reflog may still have been comitted to disk. + /// 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 (ie. the `reflog_signature` and `reflog_message` + /// 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(); diff --git a/src/transport.rs b/src/transport.rs index db3fc3ef04..b1ca3f8b80 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -25,7 +25,7 @@ pub struct Transport { /// 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. /// @@ -54,7 +54,7 @@ pub trait SmartSubtransport: Send + 'static { } /// Actions that a smart transport can ask a subtransport to perform -#[derive(Copy, Clone, PartialEq)] +#[derive(Copy, Clone, PartialEq, Debug)] #[allow(missing_docs)] pub enum Service { UploadPackLs, @@ -251,7 +251,7 @@ extern "C" fn subtransport_action( n => panic!("unknown action: {}", n), }; - let mut transport = &mut *(raw_transport as *mut RawSmartSubtransport); + let transport = &mut *(raw_transport as *mut RawSmartSubtransport); // 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 = @@ -259,10 +259,7 @@ extern "C" fn subtransport_action( if generate_stream { let obj = match transport.obj.action(url, action) { Ok(s) => s, - Err(e) => { - set_err(&e); - return e.raw_code() as c_int; - } + Err(e) => return e.raw_set_git_error(), }; *stream = mem::transmute(Box::new(RawSmartSubtransportStream { raw: raw::git_smart_subtransport_stream { @@ -271,7 +268,7 @@ extern "C" fn subtransport_action( write: Some(stream_write), free: Some(stream_free), }, - obj: obj, + obj, })); transport.stream = Some(*stream); } else { @@ -363,11 +360,6 @@ unsafe fn set_err_io(e: &io::Error) { raw::git_error_set_str(raw::GIT_ERROR_NET as c_int, s.as_ptr()); } -unsafe fn set_err(e: &Error) { - let s = CString::new(e.message()).unwrap(); - raw::git_error_set_str(e.raw_class() as c_int, s.as_ptr()); -} - // callback used by smart transports to free a `SmartSubtransportStream` // object. extern "C" fn stream_free(stream: *mut raw::git_smart_subtransport_stream) { diff --git a/src/tree.rs b/src/tree.rs index 2f644833dc..e683257436 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -1,6 +1,7 @@ -use libc::{self, c_char, c_int, c_void}; +use libc::{c_char, c_int, c_void}; use std::cmp::Ordering; use std::ffi::{CStr, CString}; +use std::iter::FusedIterator; use std::marker; use std::mem; use std::ops::Range; @@ -35,10 +36,11 @@ pub struct TreeIter<'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. + /// Runs the traversal in pre-order. PreOrder = 0, - /// Runs the traversal in post order. + /// Runs the traversal in post-order. PostOrder = 1, } @@ -94,14 +96,14 @@ impl<'repo> Tree<'repo> { } } - /// Traverse the entries in a tree and its subtrees in post or pre order. + /// 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. /// - /// libgit requires that the callback be an integer, where 0 indicates a + /// 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`](TreeWalkResult) instead. + /// You may opt to use the enum [`TreeWalkResult`] instead. /// /// ```ignore /// let mut ct = 0; @@ -113,7 +115,7 @@ impl<'repo> Tree<'repo> { /// assert_eq!(ct, 1); /// ``` /// - /// See [libgit documentation][1] for more information. + /// 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> @@ -121,20 +123,16 @@ impl<'repo> Tree<'repo> { C: FnMut(&str, &TreeEntry<'_>) -> T, T: Into, { - #[allow(unused)] - struct TreeWalkCbData<'a, T> { - pub callback: &'a mut TreeWalkCb<'a, T>, - } unsafe { let mut data = TreeWalkCbData { callback: &mut callback, }; - raw::git_tree_walk( + try_call!(raw::git_tree_walk( self.raw(), - mode.into(), - Some(treewalk_cb::), - &mut data as *mut _ as *mut c_void, - ); + mode as raw::git_treewalk_mode, + treewalk_cb::, + &mut data as *mut _ as *mut c_void + )); Ok(()) } } @@ -165,6 +163,13 @@ impl<'repo> Tree<'repo> { /// Lookup a tree entry by its filename 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)); @@ -192,7 +197,7 @@ impl<'repo> Tree<'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) } @@ -201,6 +206,10 @@ impl<'repo> Tree<'repo> { 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, @@ -212,8 +221,9 @@ extern "C" fn treewalk_cb>( _ => return -1, }; let entry = entry_from_raw_const(entry); - let payload = payload as *mut &mut TreeWalkCb<'_, T>; - (*payload)(root, &entry).into() + let payload = &mut *(payload as *mut TreeWalkCbData<'_, T>); + let callback = &mut payload.callback; + callback(root, &entry).into() }) { Some(value) => value, None => -1, @@ -225,7 +235,7 @@ impl<'repo> Binding for Tree<'repo> { unsafe fn from_raw(raw: *mut raw::git_tree) -> Tree<'repo> { Tree { - raw: raw, + raw, _marker: marker::PhantomData, } } @@ -334,7 +344,7 @@ 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, } @@ -388,12 +398,16 @@ impl<'tree> Iterator for TreeIter<'tree> { 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)] @@ -462,20 +476,41 @@ 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(); @@ -505,13 +540,22 @@ 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(); @@ -540,20 +584,34 @@ mod tests { let mut ct = 0; tree.walk(TreeWalkMode::PreOrder, |_, entry| { - assert_eq!(entry.name(), Some("foo")); + assert_eq!(entry.name(), Some(format!("f{ct}").as_str())); ct += 1; 0 }) .unwrap(); - assert_eq!(ct, 1); + assert_eq!(ct, 8); let mut ct = 0; tree.walk(TreeWalkMode::PreOrder, |_, entry| { - assert_eq!(entry.name(), Some("foo")); + assert_eq!(entry.name(), Some(format!("f{ct}").as_str())); ct += 1; TreeWalkResult::Ok }) .unwrap(); - assert_eq!(ct, 1); + 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 5d40ea9b39..1548a048cf 100644 --- a/src/treebuilder.rs +++ b/src/treebuilder.rs @@ -6,7 +6,15 @@ use libc::{c_int, c_void}; use crate::util::{Binding, IntoCString}; use crate::{panic, raw, tree, Error, Oid, Repository, TreeEntry}; -/// Constructor for in-memory trees +/// 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>, @@ -141,7 +149,7 @@ impl<'repo> Binding for TreeBuilder<'repo> { unsafe fn from_raw(raw: *mut raw::git_treebuilder) -> TreeBuilder<'repo> { TreeBuilder { - raw: raw, + raw, _marker: marker::PhantomData, } } diff --git a/src/util.rs b/src/util.rs index 1c6001ddbf..1315f0bd7a 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,7 +1,6 @@ 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::{Component, Path, PathBuf}; use crate::{raw, Error}; @@ -21,13 +20,27 @@ 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, @@ -199,7 +212,7 @@ pub fn c_cmp_to_ordering(cmp: c_int) -> Ordering { /// /// Checks if it is a relative path. /// -/// On Windows, this also requires the path to be valid unicode, and translates +/// 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 { 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 index 3cb75c6edf..fc32902db1 100644 --- a/src/worktree.rs +++ b/src/worktree.rs @@ -165,6 +165,12 @@ impl<'a> WorktreeAddOptions<'a> { 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, @@ -218,7 +224,7 @@ impl WorktreePruneOptions { self.flag(raw::GIT_WORKTREE_PRUNE_LOCKED, locked) } - /// Controls whether the actual working tree on the fs is recursively removed + /// 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 { diff --git a/systest/Cargo.toml b/systest/Cargo.toml index 9047e7f307..708cda4303 100644 --- a/systest/Cargo.toml +++ b/systest/Cargo.toml @@ -3,11 +3,11 @@ name = "systest" version = "0.1.0" authors = ["Alex Crichton "] build = "build.rs" -edition = "2018" +edition = "2021" [dependencies] libgit2-sys = { path = "../libgit2-sys", features = ['https', 'ssh'] } libc = "0.2" [build-dependencies] -ctest = "0.2.17" +ctest2 = "0.4" diff --git a/systest/build.rs b/systest/build.rs index eba93f3090..9503af7e10 100644 --- a/systest/build.rs +++ b/systest/build.rs @@ -2,11 +2,12 @@ use std::env; use std::path::PathBuf; fn main() { - 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/errors.h") .header("git2/sys/transport.h") .header("git2/sys/refs.h") .header("git2/sys/refdb_backend.h") @@ -14,6 +15,7 @@ fn main() { .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 { @@ -26,19 +28,16 @@ fn main() { // 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 - } - // TODO: fix this on the next major update of libgit2-sys - "git_diff_option_t" => 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"); 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/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"]