diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:41 +0000 |
commit | 10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87 (patch) | |
tree | bdffd5d80c26cf4a7a518281a204be1ace85b4c1 /extra/git2 | |
parent | Releasing progress-linux version 1.70.0+dfsg1-9~progress7.99u1. (diff) | |
download | rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.tar.xz rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.zip |
Merging upstream version 1.70.0+dfsg2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'extra/git2')
94 files changed, 29503 insertions, 0 deletions
diff --git a/extra/git2/.cargo-checksum.json b/extra/git2/.cargo-checksum.json new file mode 100644 index 000000000..d5f0c2b7c --- /dev/null +++ b/extra/git2/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{},"package":"23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7"}
\ No newline at end of file diff --git a/extra/git2/CHANGELOG.md b/extra/git2/CHANGELOG.md new file mode 100644 index 000000000..efdcfcace --- /dev/null +++ b/extra/git2/CHANGELOG.md @@ -0,0 +1,195 @@ +# Changelog + +## 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/extra/git2/CONTRIBUTING.md b/extra/git2/CONTRIBUTING.md new file mode 100644 index 000000000..1ab0961f1 --- /dev/null +++ b/extra/git2/CONTRIBUTING.md @@ -0,0 +1,62 @@ +# 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 <https://github.com/libgit2/libgit2/blob/main/docs/changelog.md> + can be helpful for seeing what has changed. + The project has recently started labeling API and ABI breaking changes with labels: + <https://github.com/libgit2/libgit2/pulls?q=is%3Apr+label%3A%22api+breaking%22%2C%22abi+breaking%22+is%3Aclosed> +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 <https://github.com/rust-lang/git2-rs/actions/workflows/publish.yml> (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/extra/git2/Cargo.lock b/extra/git2/Cargo.lock new file mode 100644 index 000000000..06e9aa423 --- /dev/null +++ b/extra/git2/Cargo.lock @@ -0,0 +1,578 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "errno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "git2" +version = "0.18.1" +dependencies = [ + "bitflags 2.4.0", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "structopt", + "tempfile", + "time", + "url", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" + +[[package]] +name = "libgit2-sys" +version = "0.16.1+1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2a2bb3680b094add03bb3732ec520ece34da31a8cd2d633d1389d0f0fb60d0c" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libssh2-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" +dependencies = [ + "cc", + "cmake", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "300.1.5+3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "559068e4c12950d7dcaa1857a61725c0d38d4fc03ff8e070ab31a75d6e316491" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "rustix" +version = "0.38.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[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.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/extra/git2/Cargo.toml b/extra/git2/Cargo.toml new file mode 100644 index 000000000..da9aed14c --- /dev/null +++ b/extra/git2/Cargo.toml @@ -0,0 +1,77 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "git2" +version = "0.18.1" +authors = [ + "Josh Triplett <josh@joshtriplett.org>", + "Alex Crichton <alex@alexcrichton.com>", +] +description = """ +Bindings to libgit2 for interoperating with git repositories. This library is +both threadsafe and memory safe and allows both reading and writing git +repositories. +""" +documentation = "https://docs.rs/git2" +readme = "README.md" +keywords = ["git"] +categories = ["api-bindings"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/rust-lang/git2-rs" + +[dependencies.bitflags] +version = "2.1.0" + +[dependencies.libc] +version = "0.2" + +[dependencies.libgit2-sys] +version = "0.16.0" + +[dependencies.log] +version = "0.4.8" + +[dependencies.url] +version = "2.0" + +[dev-dependencies.structopt] +version = "0.3" + +[dev-dependencies.tempfile] +version = "3.1.0" + +[dev-dependencies.time] +version = "0.1.39" + +[features] +default = [ + "ssh", + "https", + "ssh_key_from_memory", +] +https = [ + "libgit2-sys/https", + "openssl-sys", + "openssl-probe", +] +ssh = ["libgit2-sys/ssh"] +ssh_key_from_memory = ["libgit2-sys/ssh_key_from_memory"] +unstable = [] + +[target."cfg(all(unix, not(target_os = \"macos\")))".dependencies.openssl-probe] +version = "0.1" +optional = true + +[target."cfg(all(unix, not(target_os = \"macos\")))".dependencies.openssl-sys] +version = "0.9.45" +optional = true diff --git a/extra/git2/LICENSE-APACHE b/extra/git2/LICENSE-APACHE new file mode 100644 index 000000000..16fe87b06 --- /dev/null +++ b/extra/git2/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/extra/git2/LICENSE-MIT b/extra/git2/LICENSE-MIT new file mode 100644 index 000000000..39e0ed660 --- /dev/null +++ b/extra/git2/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2014 Alex Crichton + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/extra/git2/README.md b/extra/git2/README.md new file mode 100644 index 000000000..ba75127ac --- /dev/null +++ b/extra/git2/README.md @@ -0,0 +1,71 @@ +# git2-rs + +[Documentation](https://docs.rs/git2) + +libgit2 bindings for Rust. + +```toml +[dependencies] +git2 = "0.18.1" +``` + +## Rust version requirements + +git2-rs works with stable Rust, and typically works with the most recent prior +stable release as well. + +## Version of libgit2 + +Currently this library requires libgit2 1.7.1 (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. + +You can enable the Cargo feature `vendored-libgit2` to always compile and +statically link to a copy of libgit2. Sometimes the libgit2 on the system is +required to be found and used even when `vendored-libgit2` is activated. In +this case, you shall set the environment variable `LIBGIT2_NO_VENDOR=1`. + +## Building git2-rs + +```sh +$ git clone https://github.com/rust-lang/git2-rs +$ cd git2-rs +$ cargo build +``` + +### Automating Testing + +Running tests and handling all of the associated edge cases on every commit +proves tedious very quickly. To automate tests and handle proper stashing and +unstashing of unstaged changes and thus avoid nasty surprises, use the +pre-commit hook found [here][pre-commit-hook] and place it into the +`.git/hooks/` with the name `pre-commit`. You may need to add execution +permissions with `chmod +x`. + +To skip tests on a simple commit or doc-fixes, use `git commit --no-verify`. + +## Building on macOS 10.10+ + +If the `ssh` feature is enabled (and it is by default) then this library depends +on libssh2 which depends on OpenSSL. To get OpenSSL working follow the +[`openssl` crate's instructions](https://github.com/sfackler/rust-openssl/blob/master/openssl/src/lib.rs#L31). + +# License + +This project is licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or + https://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or + https://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in git2-rs by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + +[pre-commit-hook]: https://gist.github.com/glfmn/0c5e9e2b41b48007ed3497d11e3dbbfa diff --git a/extra/git2/ci/publish.sh b/extra/git2/ci/publish.sh new file mode 100755 index 000000000..69c2556bc --- /dev/null +++ b/extra/git2/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 https://crates.io/api/v1/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/extra/git2/debian/patches/disable-vendor.patch b/extra/git2/debian/patches/disable-vendor.patch new file mode 100644 index 000000000..a4899d8bb --- /dev/null +++ b/extra/git2/debian/patches/disable-vendor.patch @@ -0,0 +1,14 @@ +--- a/Cargo.toml ++++ b/Cargo.toml +@@ -70,11 +70,6 @@ + ssh = ["libgit2-sys/ssh"] + ssh_key_from_memory = ["libgit2-sys/ssh_key_from_memory"] + unstable = [] +-vendored-libgit2 = ["libgit2-sys/vendored"] +-vendored-openssl = [ +- "openssl-sys/vendored", +- "libgit2-sys/vendored-openssl", +-] + zlib-ng-compat = ["libgit2-sys/zlib-ng-compat"] + + [target."cfg(all(unix, not(target_os = \"macos\")))".dependencies.openssl-probe] diff --git a/extra/git2/debian/patches/remove-zlib-ng-compat.patch b/extra/git2/debian/patches/remove-zlib-ng-compat.patch new file mode 100644 index 000000000..1ac449c02 --- /dev/null +++ b/extra/git2/debian/patches/remove-zlib-ng-compat.patch @@ -0,0 +1,10 @@ +--- a/Cargo.toml ++++ b/Cargo.toml +@@ -70,7 +70,6 @@ + ssh = ["libgit2-sys/ssh"] + ssh_key_from_memory = ["libgit2-sys/ssh_key_from_memory"] + unstable = [] +-zlib-ng-compat = ["libgit2-sys/zlib-ng-compat"] + + [target."cfg(all(unix, not(target_os = \"macos\")))".dependencies.openssl-probe] + version = "0.1" diff --git a/extra/git2/debian/patches/series b/extra/git2/debian/patches/series new file mode 100644 index 000000000..5e0e458b2 --- /dev/null +++ b/extra/git2/debian/patches/series @@ -0,0 +1,3 @@ +disable-vendor.patch +remove-zlib-ng-compat.patch +skip-credential_helper5-if-no-git.patch diff --git a/extra/git2/debian/patches/skip-credential_helper5-if-no-git.patch b/extra/git2/debian/patches/skip-credential_helper5-if-no-git.patch new file mode 100644 index 000000000..e5686020c --- /dev/null +++ b/extra/git2/debian/patches/skip-credential_helper5-if-no-git.patch @@ -0,0 +1,15 @@ +Skip the "credential_helper5" test if git is not installled. +Index: git2/src/cred.rs +=================================================================== +--- git2.orig/src/cred.rs ++++ git2/src/cred.rs +@@ -563,6 +563,9 @@ echo username=c + + #[test] + fn credential_helper5() { ++ if !Path::new("/usr/bin/git").exists() { ++ return; ++ } //this test does not work if git is not installed + if cfg!(windows) { + return; + } // shell scripts don't work on Windows diff --git a/extra/git2/examples/add.rs b/extra/git2/examples/add.rs new file mode 100644 index 000000000..25e972c7a --- /dev/null +++ b/extra/git2/examples/add.rs @@ -0,0 +1,81 @@ +/* + * libgit2 "add" example - shows how to modify the index + * + * Written by the libgit2 contributors + * + * To the extent possible under law, the author(s) have dedicated all copyright + * and related and neighboring rights to this software to the public domain + * worldwide. This software is distributed without any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication along + * with this software. If not, see + * <http://creativecommons.org/publicdomain/zero/1.0/>. + */ + +#![deny(warnings)] +#![allow(trivial_casts)] + +use git2::Repository; +use std::path::Path; +use structopt::StructOpt; + +#[derive(StructOpt)] +struct Args { + #[structopt(name = "spec")] + arg_spec: Vec<String>, + #[structopt(name = "dry_run", short = "n", long)] + /// dry run + flag_dry_run: bool, + #[structopt(name = "verbose", short, long)] + /// be verbose + flag_verbose: bool, + #[structopt(name = "update", short, long)] + /// update tracked files + flag_update: bool, +} + +fn run(args: &Args) -> Result<(), git2::Error> { + let repo = Repository::open(&Path::new("."))?; + let mut index = repo.index()?; + + let cb = &mut |path: &Path, _matched_spec: &[u8]| -> i32 { + let status = repo.status_file(path).unwrap(); + + let ret = if status.contains(git2::Status::WT_MODIFIED) + || status.contains(git2::Status::WT_NEW) + { + println!("add '{}'", path.display()); + 0 + } else { + 1 + }; + + if args.flag_dry_run { + 1 + } else { + ret + } + }; + let cb = if args.flag_verbose || args.flag_update { + Some(cb as &mut git2::IndexMatchedPath) + } else { + None + }; + + if args.flag_update { + index.update_all(args.arg_spec.iter(), cb)?; + } else { + index.add_all(args.arg_spec.iter(), git2::IndexAddOption::DEFAULT, cb)?; + } + + index.write()?; + Ok(()) +} + +fn main() { + let args = Args::from_args(); + match run(&args) { + Ok(()) => {} + Err(e) => println!("error: {}", e), + } +} diff --git a/extra/git2/examples/blame.rs b/extra/git2/examples/blame.rs new file mode 100644 index 000000000..7cb1b6947 --- /dev/null +++ b/extra/git2/examples/blame.rs @@ -0,0 +1,104 @@ +/* + * libgit2 "blame" example - shows how to use the blame API + * + * Written by the libgit2 contributors + * + * To the extent possible under law, the author(s) have dedicated all copyright + * and related and neighboring rights to this software to the public domain + * worldwide. This software is distributed without any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication along + * with this software. If not, see + * <http://creativecommons.org/publicdomain/zero/1.0/>. + */ + +#![deny(warnings)] + +use git2::{BlameOptions, Repository}; +use std::io::{BufRead, BufReader}; +use std::path::Path; +use structopt::StructOpt; + +#[derive(StructOpt)] +#[allow(non_snake_case)] +struct Args { + #[structopt(name = "path")] + arg_path: String, + #[structopt(name = "spec")] + arg_spec: Option<String>, + #[structopt(short = "M")] + /// find line moves within and across files + flag_M: bool, + #[structopt(short = "C")] + /// find line copies within and across files + flag_C: bool, + #[structopt(short = "F")] + /// follow only the first parent commits + flag_F: bool, +} + +fn run(args: &Args) -> Result<(), git2::Error> { + let repo = Repository::open(".")?; + let path = Path::new(&args.arg_path[..]); + + // Prepare our blame options + let mut opts = BlameOptions::new(); + opts.track_copies_same_commit_moves(args.flag_M) + .track_copies_same_commit_copies(args.flag_C) + .first_parent(args.flag_F); + + let mut commit_id = "HEAD".to_string(); + + // Parse spec + if let Some(spec) = args.arg_spec.as_ref() { + let revspec = repo.revparse(spec)?; + + let (oldest, newest) = if revspec.mode().contains(git2::RevparseMode::SINGLE) { + (None, revspec.from()) + } else if revspec.mode().contains(git2::RevparseMode::RANGE) { + (revspec.from(), revspec.to()) + } else { + (None, None) + }; + + if let Some(commit) = oldest { + opts.oldest_commit(commit.id()); + } + + if let Some(commit) = newest { + opts.newest_commit(commit.id()); + if !commit.id().is_zero() { + commit_id = format!("{}", commit.id()) + } + } + } + + let spec = format!("{}:{}", commit_id, path.display()); + let blame = repo.blame_file(path, Some(&mut opts))?; + let object = repo.revparse_single(&spec[..])?; + let blob = repo.find_blob(object.id())?; + let reader = BufReader::new(blob.content()); + + for (i, line) in reader.lines().enumerate() { + if let (Ok(line), Some(hunk)) = (line, blame.get_line(i + 1)) { + let sig = hunk.final_signature(); + println!( + "{} {} <{}> {}", + hunk.final_commit_id(), + String::from_utf8_lossy(sig.name_bytes()), + String::from_utf8_lossy(sig.email_bytes()), + line + ); + } + } + + Ok(()) +} + +fn main() { + let args = Args::from_args(); + match run(&args) { + Ok(()) => {} + Err(e) => println!("error: {}", e), + } +} diff --git a/extra/git2/examples/cat-file.rs b/extra/git2/examples/cat-file.rs new file mode 100644 index 000000000..0ce21b34a --- /dev/null +++ b/extra/git2/examples/cat-file.rs @@ -0,0 +1,149 @@ +/* + * libgit2 "cat-file" example - shows how to print data from the ODB + * + * Written by the libgit2 contributors + * + * To the extent possible under law, the author(s) have dedicated all copyright + * and related and neighboring rights to this software to the public domain + * worldwide. This software is distributed without any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication along + * with this software. If not, see + * <http://creativecommons.org/publicdomain/zero/1.0/>. + */ + +#![deny(warnings)] + +use std::io::{self, Write}; + +use git2::{Blob, Commit, ObjectType, Repository, Signature, Tag, Tree}; +use structopt::StructOpt; + +#[derive(StructOpt)] +struct Args { + #[structopt(name = "object")] + arg_object: String, + #[structopt(short = "t")] + /// show the object type + flag_t: bool, + #[structopt(short = "s")] + /// show the object size + flag_s: bool, + #[structopt(short = "e")] + /// suppress all output + flag_e: bool, + #[structopt(short = "p")] + /// pretty print the contents of the object + flag_p: bool, + #[structopt(name = "quiet", short, long)] + /// suppress output + flag_q: bool, + #[structopt(name = "verbose", short, long)] + flag_v: bool, + #[structopt(name = "dir", long = "git-dir")] + /// use the specified directory as the base directory + flag_git_dir: Option<String>, +} + +fn run(args: &Args) -> Result<(), git2::Error> { + let path = args.flag_git_dir.as_ref().map(|s| &s[..]).unwrap_or("."); + let repo = Repository::open(path)?; + + let obj = repo.revparse_single(&args.arg_object)?; + if args.flag_v && !args.flag_q { + println!("{} {}\n--", obj.kind().unwrap().str(), obj.id()); + } + + if args.flag_t { + println!("{}", obj.kind().unwrap().str()); + } else if args.flag_s || args.flag_e { + /* ... */ + } else if args.flag_p { + match obj.kind() { + Some(ObjectType::Blob) => { + show_blob(obj.as_blob().unwrap()); + } + Some(ObjectType::Commit) => { + show_commit(obj.as_commit().unwrap()); + } + Some(ObjectType::Tag) => { + show_tag(obj.as_tag().unwrap()); + } + Some(ObjectType::Tree) => { + show_tree(obj.as_tree().unwrap()); + } + Some(ObjectType::Any) | None => println!("unknown {}", obj.id()), + } + } + Ok(()) +} + +fn show_blob(blob: &Blob) { + io::stdout().write_all(blob.content()).unwrap(); +} + +fn show_commit(commit: &Commit) { + println!("tree {}", commit.tree_id()); + for parent in commit.parent_ids() { + println!("parent {}", parent); + } + show_sig("author", Some(commit.author())); + show_sig("committer", Some(commit.committer())); + if let Some(msg) = commit.message() { + println!("\n{}", msg); + } +} + +fn show_tag(tag: &Tag) { + println!("object {}", tag.target_id()); + println!("type {}", tag.target_type().unwrap().str()); + println!("tag {}", tag.name().unwrap()); + show_sig("tagger", tag.tagger()); + + if let Some(msg) = tag.message() { + println!("\n{}", msg); + } +} + +fn show_tree(tree: &Tree) { + for entry in tree.iter() { + println!( + "{:06o} {} {}\t{}", + entry.filemode(), + entry.kind().unwrap().str(), + entry.id(), + entry.name().unwrap() + ); + } +} + +fn show_sig(header: &str, sig: Option<Signature>) { + let sig = match sig { + Some(s) => s, + None => return, + }; + let offset = sig.when().offset_minutes(); + let (sign, offset) = if offset < 0 { + ('-', -offset) + } else { + ('+', offset) + }; + let (hours, minutes) = (offset / 60, offset % 60); + println!( + "{} {} {} {}{:02}{:02}", + header, + sig, + sig.when().seconds(), + sign, + hours, + minutes + ); +} + +fn main() { + let args = Args::from_args(); + match run(&args) { + Ok(()) => {} + Err(e) => println!("error: {}", e), + } +} diff --git a/extra/git2/examples/clone.rs b/extra/git2/examples/clone.rs new file mode 100644 index 000000000..5af73222f --- /dev/null +++ b/extra/git2/examples/clone.rs @@ -0,0 +1,126 @@ +/* + * libgit2 "clone" example + * + * Written by the libgit2 contributors + * + * To the extent possible under law, the author(s) have dedicated all copyright + * and related and neighboring rights to this software to the public domain + * worldwide. This software is distributed without any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication along + * with this software. If not, see + * <http://creativecommons.org/publicdomain/zero/1.0/>. + */ + +#![deny(warnings)] + +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)] +struct Args { + #[structopt(name = "url")] + arg_url: String, + #[structopt(name = "path")] + arg_path: String, +} + +struct State { + progress: Option<Progress<'static>>, + total: usize, + current: usize, + path: Option<PathBuf>, + newline: bool, +} + +fn print(state: &mut State) { + let stats = state.progress.as_ref().unwrap(); + let network_pct = (100 * stats.received_objects()) / stats.total_objects(); + let index_pct = (100 * stats.indexed_objects()) / stats.total_objects(); + let co_pct = if state.total > 0 { + (100 * state.current) / state.total + } else { + 0 + }; + let kbytes = stats.received_bytes() / 1024; + if stats.received_objects() == stats.total_objects() { + if !state.newline { + println!(); + state.newline = true; + } + print!( + "Resolving deltas {}/{}\r", + stats.indexed_deltas(), + stats.total_deltas() + ); + } else { + print!( + "net {:3}% ({:4} kb, {:5}/{:5}) / idx {:3}% ({:5}/{:5}) \ + / chk {:3}% ({:4}/{:4}) {}\r", + network_pct, + kbytes, + stats.received_objects(), + stats.total_objects(), + index_pct, + stats.indexed_objects(), + stats.total_objects(), + co_pct, + state.current, + state.total, + state + .path + .as_ref() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_default() + ) + } + io::stdout().flush().unwrap(); +} + +fn run(args: &Args) -> Result<(), git2::Error> { + let state = RefCell::new(State { + progress: None, + total: 0, + current: 0, + path: None, + newline: false, + }); + let mut cb = RemoteCallbacks::new(); + cb.transfer_progress(|stats| { + let mut state = state.borrow_mut(); + state.progress = Some(stats.to_owned()); + print(&mut *state); + true + }); + + let mut co = CheckoutBuilder::new(); + co.progress(|path, cur, total| { + let mut state = state.borrow_mut(); + state.path = path.map(|p| p.to_path_buf()); + state.current = cur; + state.total = total; + print(&mut *state); + }); + + let mut fo = FetchOptions::new(); + fo.remote_callbacks(cb); + RepoBuilder::new() + .fetch_options(fo) + .with_checkout(co) + .clone(&args.arg_url, Path::new(&args.arg_path))?; + println!(); + + Ok(()) +} + +fn main() { + let args = Args::from_args(); + match run(&args) { + Ok(()) => {} + Err(e) => println!("error: {}", e), + } +} diff --git a/extra/git2/examples/diff.rs b/extra/git2/examples/diff.rs new file mode 100644 index 000000000..62f165db3 --- /dev/null +++ b/extra/git2/examples/diff.rs @@ -0,0 +1,368 @@ +/* + * libgit2 "diff" example - shows how to use the diff API + * + * Written by the libgit2 contributors + * + * To the extent possible under law, the author(s) have dedicated all copyright + * and related and neighboring rights to this software to the public domain + * worldwide. This software is distributed without any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication along + * with this software. If not, see + * <http://creativecommons.org/publicdomain/zero/1.0/>. + */ + +#![deny(warnings)] + +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)] +#[allow(non_snake_case)] +struct Args { + #[structopt(name = "from_oid")] + arg_from_oid: Option<String>, + #[structopt(name = "to_oid")] + arg_to_oid: Option<String>, + #[structopt(name = "blobs", long)] + /// treat from_oid and to_oid as blob ids + flag_blobs: bool, + #[structopt(name = "patch", short, long)] + /// show output in patch format + flag_patch: bool, + #[structopt(name = "cached", long)] + /// use staged changes as diff + flag_cached: bool, + #[structopt(name = "nocached", long)] + /// do not use staged changes + flag_nocached: bool, + #[structopt(name = "name-only", long)] + /// show only names of changed files + flag_name_only: bool, + #[structopt(name = "name-status", long)] + /// show only names and status changes + flag_name_status: bool, + #[structopt(name = "raw", long)] + /// generate the raw format + flag_raw: bool, + #[structopt(name = "format", long)] + /// specify format for stat summary + flag_format: Option<String>, + #[structopt(name = "color", long)] + /// use color output + flag_color: bool, + #[structopt(name = "no-color", long)] + /// never use color output + flag_no_color: bool, + #[structopt(short = "R")] + /// swap two inputs + flag_R: bool, + #[structopt(name = "text", short = "a", long)] + /// treat all files as text + flag_text: bool, + #[structopt(name = "ignore-space-at-eol", long)] + /// ignore changes in whitespace at EOL + flag_ignore_space_at_eol: bool, + #[structopt(name = "ignore-space-change", short = "b", long)] + /// ignore changes in amount of whitespace + flag_ignore_space_change: bool, + #[structopt(name = "ignore-all-space", short = "w", long)] + /// ignore whitespace when comparing lines + flag_ignore_all_space: bool, + #[structopt(name = "ignored", long)] + /// show untracked files + flag_ignored: bool, + #[structopt(name = "untracked", long)] + /// generate diff using the patience algorithm + flag_untracked: bool, + #[structopt(name = "patience", long)] + /// show ignored files as well + flag_patience: bool, + #[structopt(name = "minimal", long)] + /// spend extra time to find smallest diff + flag_minimal: bool, + #[structopt(name = "stat", long)] + /// generate a diffstat + flag_stat: bool, + #[structopt(name = "numstat", long)] + /// similar to --stat, but more machine friendly + flag_numstat: bool, + #[structopt(name = "shortstat", long)] + /// only output last line of --stat + flag_shortstat: bool, + #[structopt(name = "summary", long)] + /// output condensed summary of header info + flag_summary: bool, + #[structopt(name = "find-renames", short = "M", long)] + /// set threshold for finding renames (default 50) + flag_find_renames: Option<u16>, + #[structopt(name = "find-copies", short = "C", long)] + /// set threshold for finding copies (default 50) + flag_find_copies: Option<u16>, + #[structopt(name = "find-copies-harder", long)] + /// inspect unmodified files for sources of copies + flag_find_copies_harder: bool, + #[structopt(name = "break_rewrites", short = "B", long)] + /// break complete rewrite changes into pairs + flag_break_rewrites: bool, + #[structopt(name = "unified", short = "U", long)] + /// lints of context to show + flag_unified: Option<u32>, + #[structopt(name = "inter-hunk-context", long)] + /// maximum lines of change between hunks + flag_inter_hunk_context: Option<u32>, + #[structopt(name = "abbrev", long)] + /// length to abbreviate commits to + flag_abbrev: Option<u16>, + #[structopt(name = "src-prefix", long)] + /// show given source prefix instead of 'a/' + flag_src_prefix: Option<String>, + #[structopt(name = "dst-prefix", long)] + /// show given destination prefix instead of 'b/' + flag_dst_prefix: Option<String>, + #[structopt(name = "path", long = "git-dir")] + /// path to git repository to use + flag_git_dir: Option<String>, +} + +const RESET: &str = "\u{1b}[m"; +const BOLD: &str = "\u{1b}[1m"; +const RED: &str = "\u{1b}[31m"; +const GREEN: &str = "\u{1b}[32m"; +const CYAN: &str = "\u{1b}[36m"; + +#[derive(PartialEq, Eq, Copy, Clone)] +enum Cache { + Normal, + Only, + None, +} + +fn line_color(line: &DiffLine) -> Option<&'static str> { + match line.origin() { + '+' => Some(GREEN), + '-' => Some(RED), + '>' => Some(GREEN), + '<' => Some(RED), + 'F' => Some(BOLD), + 'H' => Some(CYAN), + _ => None, + } +} + +fn print_diff_line( + _delta: DiffDelta, + _hunk: Option<DiffHunk>, + line: DiffLine, + args: &Args, +) -> bool { + if args.color() { + print!("{}", RESET); + if let Some(color) = line_color(&line) { + print!("{}", color); + } + } + match line.origin() { + '+' | '-' | ' ' => print!("{}", line.origin()), + _ => {} + } + print!("{}", str::from_utf8(line.content()).unwrap()); + true +} + +fn run(args: &Args) -> Result<(), Error> { + let path = args.flag_git_dir.as_ref().map(|s| &s[..]).unwrap_or("."); + let repo = Repository::open(path)?; + + // Prepare our diff options based on the arguments given + let mut opts = DiffOptions::new(); + opts.reverse(args.flag_R) + .force_text(args.flag_text) + .ignore_whitespace_eol(args.flag_ignore_space_at_eol) + .ignore_whitespace_change(args.flag_ignore_space_change) + .ignore_whitespace(args.flag_ignore_all_space) + .include_ignored(args.flag_ignored) + .include_untracked(args.flag_untracked) + .patience(args.flag_patience) + .minimal(args.flag_minimal); + if let Some(amt) = args.flag_unified { + opts.context_lines(amt); + } + if let Some(amt) = args.flag_inter_hunk_context { + opts.interhunk_lines(amt); + } + if let Some(amt) = args.flag_abbrev { + opts.id_abbrev(amt); + } + if let Some(ref s) = args.flag_src_prefix { + opts.old_prefix(&s); + } + if let Some(ref s) = args.flag_dst_prefix { + opts.new_prefix(&s); + } + if let Some("diff-index") = args.flag_format.as_ref().map(|s| &s[..]) { + opts.id_abbrev(40); + } + + if args.flag_blobs { + let b1 = resolve_blob(&repo, args.arg_from_oid.as_ref())?; + let b2 = resolve_blob(&repo, args.arg_to_oid.as_ref())?; + repo.diff_blobs( + b1.as_ref(), + None, + b2.as_ref(), + None, + Some(&mut opts), + None, + None, + None, + Some(&mut |d, h, l| print_diff_line(d, h, l, args)), + )?; + if args.color() { + print!("{}", RESET); + } + return Ok(()); + } + + // Prepare the diff to inspect + let t1 = tree_to_treeish(&repo, args.arg_from_oid.as_ref())?; + let t2 = tree_to_treeish(&repo, args.arg_to_oid.as_ref())?; + let head = tree_to_treeish(&repo, Some(&"HEAD".to_string()))?.unwrap(); + let mut diff = match (t1, t2, args.cache()) { + (Some(t1), Some(t2), _) => { + repo.diff_tree_to_tree(t1.as_tree(), t2.as_tree(), Some(&mut opts))? + } + (t1, None, Cache::None) => { + let t1 = t1.unwrap_or(head); + repo.diff_tree_to_workdir(t1.as_tree(), Some(&mut opts))? + } + (t1, None, Cache::Only) => { + let t1 = t1.unwrap_or(head); + repo.diff_tree_to_index(t1.as_tree(), None, Some(&mut opts))? + } + (Some(t1), None, _) => { + repo.diff_tree_to_workdir_with_index(t1.as_tree(), Some(&mut opts))? + } + (None, None, _) => repo.diff_index_to_workdir(None, Some(&mut opts))?, + (None, Some(_), _) => unreachable!(), + }; + + // Apply rename and copy detection if requested + if args.flag_break_rewrites + || args.flag_find_copies_harder + || args.flag_find_renames.is_some() + || args.flag_find_copies.is_some() + { + let mut opts = DiffFindOptions::new(); + if let Some(t) = args.flag_find_renames { + opts.rename_threshold(t); + opts.renames(true); + } + if let Some(t) = args.flag_find_copies { + opts.copy_threshold(t); + opts.copies(true); + } + opts.copies_from_unmodified(args.flag_find_copies_harder) + .rewrites(args.flag_break_rewrites); + diff.find_similar(Some(&mut opts))?; + } + + // Generate simple output + let stats = args.flag_stat | args.flag_numstat | args.flag_shortstat | args.flag_summary; + if stats { + print_stats(&diff, args)?; + } + if args.flag_patch || !stats { + diff.print(args.diff_format(), |d, h, l| print_diff_line(d, h, l, args))?; + if args.color() { + print!("{}", RESET); + } + } + + Ok(()) +} + +fn print_stats(diff: &Diff, args: &Args) -> Result<(), Error> { + let stats = diff.stats()?; + let mut format = git2::DiffStatsFormat::NONE; + if args.flag_stat { + format |= git2::DiffStatsFormat::FULL; + } + if args.flag_shortstat { + format |= git2::DiffStatsFormat::SHORT; + } + if args.flag_numstat { + format |= git2::DiffStatsFormat::NUMBER; + } + if args.flag_summary { + format |= git2::DiffStatsFormat::INCLUDE_SUMMARY; + } + let buf = stats.to_buf(format, 80)?; + print!("{}", str::from_utf8(&*buf).unwrap()); + Ok(()) +} + +fn tree_to_treeish<'a>( + repo: &'a Repository, + arg: Option<&String>, +) -> Result<Option<Object<'a>>, Error> { + let arg = match arg { + Some(s) => s, + None => return Ok(None), + }; + let obj = repo.revparse_single(arg)?; + let tree = obj.peel(ObjectType::Tree)?; + Ok(Some(tree)) +} + +fn resolve_blob<'a>(repo: &'a Repository, arg: Option<&String>) -> Result<Option<Blob<'a>>, Error> { + let arg = match arg { + Some(s) => Oid::from_str(s)?, + None => return Ok(None), + }; + repo.find_blob(arg).map(|b| Some(b)) +} + +impl Args { + fn cache(&self) -> Cache { + if self.flag_cached { + Cache::Only + } else if self.flag_nocached { + Cache::None + } else { + Cache::Normal + } + } + fn color(&self) -> bool { + self.flag_color && !self.flag_no_color + } + fn diff_format(&self) -> DiffFormat { + if self.flag_patch { + DiffFormat::Patch + } else if self.flag_name_only { + DiffFormat::NameOnly + } else if self.flag_name_status { + DiffFormat::NameStatus + } else if self.flag_raw { + DiffFormat::Raw + } else { + match self.flag_format.as_ref().map(|s| &s[..]) { + Some("name") => DiffFormat::NameOnly, + Some("name-status") => DiffFormat::NameStatus, + Some("raw") => DiffFormat::Raw, + Some("diff-index") => DiffFormat::Raw, + _ => DiffFormat::Patch, + } + } + } +} + +fn main() { + let args = Args::from_args(); + match run(&args) { + Ok(()) => {} + Err(e) => println!("error: {}", e), + } +} diff --git a/extra/git2/examples/fetch.rs b/extra/git2/examples/fetch.rs new file mode 100644 index 000000000..64374a6d4 --- /dev/null +++ b/extra/git2/examples/fetch.rs @@ -0,0 +1,127 @@ +/* + * libgit2 "fetch" example - shows how to fetch remote data + * + * Written by the libgit2 contributors + * + * To the extent possible under law, the author(s) have dedicated all copyright + * and related and neighboring rights to this software to the public domain + * worldwide. This software is distributed without any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication along + * with this software. If not, see + * <http://creativecommons.org/publicdomain/zero/1.0/>. + */ + +#![deny(warnings)] + +use git2::{AutotagOption, FetchOptions, RemoteCallbacks, Repository}; +use std::io::{self, Write}; +use std::str; +use structopt::StructOpt; + +#[derive(StructOpt)] +struct Args { + #[structopt(name = "remote")] + arg_remote: Option<String>, +} + +fn run(args: &Args) -> Result<(), git2::Error> { + let repo = Repository::open(".")?; + let remote = args.arg_remote.as_ref().map(|s| &s[..]).unwrap_or("origin"); + + // Figure out whether it's a named remote or a URL + println!("Fetching {} for repo", remote); + let mut cb = RemoteCallbacks::new(); + let mut remote = repo + .find_remote(remote) + .or_else(|_| repo.remote_anonymous(remote))?; + cb.sideband_progress(|data| { + print!("remote: {}", str::from_utf8(data).unwrap()); + io::stdout().flush().unwrap(); + true + }); + + // This callback gets called for each remote-tracking branch that gets + // updated. The message we output depends on whether it's a new one or an + // update. + cb.update_tips(|refname, a, b| { + if a.is_zero() { + println!("[new] {:20} {}", b, refname); + } else { + println!("[updated] {:10}..{:10} {}", a, b, refname); + } + true + }); + + // Here we show processed and total objects in the pack and the amount of + // received data. Most frontends will probably want to show a percentage and + // the download rate. + cb.transfer_progress(|stats| { + if stats.received_objects() == stats.total_objects() { + print!( + "Resolving deltas {}/{}\r", + stats.indexed_deltas(), + stats.total_deltas() + ); + } else if stats.total_objects() > 0 { + print!( + "Received {}/{} objects ({}) in {} bytes\r", + stats.received_objects(), + stats.total_objects(), + stats.indexed_objects(), + stats.received_bytes() + ); + } + io::stdout().flush().unwrap(); + true + }); + + // Download the packfile and index it. This function updates the amount of + // received data and the indexer stats which lets you inform the user about + // progress. + let mut fo = FetchOptions::new(); + fo.remote_callbacks(cb); + remote.download(&[] as &[&str], Some(&mut fo))?; + + { + // If there are local objects (we got a thin pack), then tell the user + // how many objects we saved from having to cross the network. + let stats = remote.stats(); + if stats.local_objects() > 0 { + println!( + "\rReceived {}/{} objects in {} bytes (used {} local \ + objects)", + stats.indexed_objects(), + stats.total_objects(), + stats.received_bytes(), + stats.local_objects() + ); + } else { + println!( + "\rReceived {}/{} objects in {} bytes", + stats.indexed_objects(), + stats.total_objects(), + stats.received_bytes() + ); + } + } + + // Disconnect the underlying connection to prevent from idling. + remote.disconnect()?; + + // Update the references in the remote's namespace to point to the right + // commits. This may be needed even if there was no packfile to download, + // which can happen e.g. when the branches have been changed but all the + // needed objects are available locally. + remote.update_tips(None, true, AutotagOption::Unspecified, None)?; + + Ok(()) +} + +fn main() { + let args = Args::from_args(); + match run(&args) { + Ok(()) => {} + Err(e) => println!("error: {}", e), + } +} diff --git a/extra/git2/examples/init.rs b/extra/git2/examples/init.rs new file mode 100644 index 000000000..3e447cbde --- /dev/null +++ b/extra/git2/examples/init.rs @@ -0,0 +1,145 @@ +/* + * libgit2 "init" example - shows how to initialize a new repo + * + * Written by the libgit2 contributors + * + * To the extent possible under law, the author(s) have dedicated all copyright + * and related and neighboring rights to this software to the public domain + * worldwide. This software is distributed without any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication along + * with this software. If not, see + * <http://creativecommons.org/publicdomain/zero/1.0/>. + */ + +#![deny(warnings)] + +use git2::{Error, Repository, RepositoryInitMode, RepositoryInitOptions}; +use std::path::{Path, PathBuf}; +use structopt::StructOpt; + +#[derive(StructOpt)] +struct Args { + #[structopt(name = "directory")] + arg_directory: String, + #[structopt(name = "quiet", short, long)] + /// don't print information to stdout + flag_quiet: bool, + #[structopt(name = "bare", long)] + /// initialize a new bare repository + flag_bare: bool, + #[structopt(name = "dir", long = "template")] + /// use <dir> as an initialization template + flag_template: Option<String>, + #[structopt(name = "separate-git-dir", long)] + /// use <dir> as the .git directory + flag_separate_git_dir: Option<String>, + #[structopt(name = "initial-commit", long)] + /// create an initial empty commit + flag_initial_commit: bool, + #[structopt(name = "perms", long = "shared")] + /// permissions to create the repository with + flag_shared: Option<String>, +} + +fn run(args: &Args) -> Result<(), Error> { + let mut path = PathBuf::from(&args.arg_directory); + let repo = if !args.flag_bare + && args.flag_template.is_none() + && args.flag_shared.is_none() + && args.flag_separate_git_dir.is_none() + { + Repository::init(&path)? + } else { + let mut opts = RepositoryInitOptions::new(); + opts.bare(args.flag_bare); + if let Some(ref s) = args.flag_template { + opts.template_path(Path::new(s)); + } + + // If you specified a separate git directory, then initialize + // the repository at that path and use the second path as the + // working directory of the repository (with a git-link file) + if let Some(ref s) = args.flag_separate_git_dir { + opts.workdir_path(&path); + path = PathBuf::from(s); + } + + if let Some(ref s) = args.flag_shared { + opts.mode(parse_shared(s)?); + } + Repository::init_opts(&path, &opts)? + }; + + // Print a message to stdout like "git init" does + if !args.flag_quiet { + if args.flag_bare || args.flag_separate_git_dir.is_some() { + path = repo.path().to_path_buf(); + } else { + path = repo.workdir().unwrap().to_path_buf(); + } + println!("Initialized empty Git repository in {}", path.display()); + } + + if args.flag_initial_commit { + create_initial_commit(&repo)?; + println!("Created empty initial commit"); + } + + Ok(()) +} + +/// Unlike regular "git init", this example shows how to create an initial empty +/// commit in the repository. This is the helper function that does that. +fn create_initial_commit(repo: &Repository) -> Result<(), Error> { + // First use the config to initialize a commit signature for the user. + let sig = repo.signature()?; + + // Now let's create an empty tree for this commit + let tree_id = { + let mut index = repo.index()?; + + // Outside of this example, you could call index.add_path() + // here to put actual files into the index. For our purposes, we'll + // leave it empty for now. + + index.write_tree()? + }; + + let tree = repo.find_tree(tree_id)?; + + // Ready to create the initial commit. + // + // Normally creating a commit would involve looking up the current HEAD + // commit and making that be the parent of the initial commit, but here this + // is the first commit so there will be no parent. + repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])?; + + Ok(()) +} + +fn parse_shared(shared: &str) -> Result<RepositoryInitMode, Error> { + match shared { + "false" | "umask" => Ok(git2::RepositoryInitMode::SHARED_UMASK), + "true" | "group" => Ok(git2::RepositoryInitMode::SHARED_GROUP), + "all" | "world" => Ok(git2::RepositoryInitMode::SHARED_ALL), + _ => { + if shared.starts_with('0') { + match u32::from_str_radix(&shared[1..], 8).ok() { + Some(n) => Ok(RepositoryInitMode::from_bits_truncate(n)), + None => Err(Error::from_str("invalid octal value for --shared")), + } + } else { + Err(Error::from_str("unknown value for --shared")) + } + } + } +} + +fn main() { + let args = Args::from_args(); + match run(&args) { + Ok(()) => {} + Err(e) => println!("error: {}", e), + } +} diff --git a/extra/git2/examples/log.rs b/extra/git2/examples/log.rs new file mode 100644 index 000000000..ad3bb354d --- /dev/null +++ b/extra/git2/examples/log.rs @@ -0,0 +1,310 @@ +/* + * libgit2 "log" example - shows how to walk history and get commit info + * + * Written by the libgit2 contributors + * + * To the extent possible under law, the author(s) have dedicated all copyright + * and related and neighboring rights to this software to the public domain + * worldwide. This software is distributed without any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication along + * with this software. If not, see + * <http://creativecommons.org/publicdomain/zero/1.0/>. + */ + +#![deny(warnings)] + +use git2::{Commit, DiffOptions, ObjectType, Repository, Signature, Time}; +use git2::{DiffFormat, Error, Pathspec}; +use std::str; +use structopt::StructOpt; + +#[derive(StructOpt)] +struct Args { + #[structopt(name = "topo-order", long)] + /// sort commits in topological order + flag_topo_order: bool, + #[structopt(name = "date-order", long)] + /// sort commits in date order + flag_date_order: bool, + #[structopt(name = "reverse", long)] + /// sort commits in reverse + flag_reverse: bool, + #[structopt(name = "author", long)] + /// author to sort by + flag_author: Option<String>, + #[structopt(name = "committer", long)] + /// committer to sort by + flag_committer: Option<String>, + #[structopt(name = "pat", long = "grep")] + /// pattern to filter commit messages by + flag_grep: Option<String>, + #[structopt(name = "dir", long = "git-dir")] + /// alternative git directory to use + flag_git_dir: Option<String>, + #[structopt(name = "skip", long)] + /// number of commits to skip + flag_skip: Option<usize>, + #[structopt(name = "max-count", short = "n", long)] + /// maximum number of commits to show + flag_max_count: Option<usize>, + #[structopt(name = "merges", long)] + /// only show merge commits + flag_merges: bool, + #[structopt(name = "no-merges", long)] + /// don't show merge commits + flag_no_merges: bool, + #[structopt(name = "no-min-parents", long)] + /// don't require a minimum number of parents + flag_no_min_parents: bool, + #[structopt(name = "no-max-parents", long)] + /// don't require a maximum number of parents + flag_no_max_parents: bool, + #[structopt(name = "max-parents")] + /// specify a maximum number of parents for a commit + flag_max_parents: Option<usize>, + #[structopt(name = "min-parents")] + /// specify a minimum number of parents for a commit + flag_min_parents: Option<usize>, + #[structopt(name = "patch", long, short)] + /// show commit diff + flag_patch: bool, + #[structopt(name = "commit")] + arg_commit: Vec<String>, + #[structopt(name = "spec", last = true)] + arg_spec: Vec<String>, +} + +fn run(args: &Args) -> Result<(), Error> { + let path = args.flag_git_dir.as_ref().map(|s| &s[..]).unwrap_or("."); + let repo = Repository::open(path)?; + let mut revwalk = repo.revwalk()?; + + // Prepare the revwalk based on CLI parameters + let base = if args.flag_reverse { + git2::Sort::REVERSE + } else { + git2::Sort::NONE + }; + revwalk.set_sorting( + base | if args.flag_topo_order { + git2::Sort::TOPOLOGICAL + } else if args.flag_date_order { + git2::Sort::TIME + } else { + git2::Sort::NONE + }, + )?; + for commit in &args.arg_commit { + if commit.starts_with('^') { + let obj = repo.revparse_single(&commit[1..])?; + revwalk.hide(obj.id())?; + continue; + } + let revspec = repo.revparse(commit)?; + if revspec.mode().contains(git2::RevparseMode::SINGLE) { + revwalk.push(revspec.from().unwrap().id())?; + } else { + let from = revspec.from().unwrap().id(); + let to = revspec.to().unwrap().id(); + revwalk.push(to)?; + if revspec.mode().contains(git2::RevparseMode::MERGE_BASE) { + let base = repo.merge_base(from, to)?; + let o = repo.find_object(base, Some(ObjectType::Commit))?; + revwalk.push(o.id())?; + } + revwalk.hide(from)?; + } + } + if args.arg_commit.is_empty() { + revwalk.push_head()?; + } + + // Prepare our diff options and pathspec matcher + let (mut diffopts, mut diffopts2) = (DiffOptions::new(), DiffOptions::new()); + for spec in &args.arg_spec { + diffopts.pathspec(spec); + diffopts2.pathspec(spec); + } + let ps = Pathspec::new(args.arg_spec.iter())?; + + // Filter our revwalk based on the CLI parameters + macro_rules! filter_try { + ($e:expr) => { + match $e { + Ok(t) => t, + Err(e) => return Some(Err(e)), + } + }; + } + let revwalk = revwalk + .filter_map(|id| { + let id = filter_try!(id); + let commit = filter_try!(repo.find_commit(id)); + let parents = commit.parents().len(); + if parents < args.min_parents() { + return None; + } + if let Some(n) = args.max_parents() { + if parents >= n { + return None; + } + } + if !args.arg_spec.is_empty() { + match commit.parents().len() { + 0 => { + let tree = filter_try!(commit.tree()); + let flags = git2::PathspecFlags::NO_MATCH_ERROR; + if ps.match_tree(&tree, flags).is_err() { + return None; + } + } + _ => { + let m = commit.parents().all(|parent| { + match_with_parent(&repo, &commit, &parent, &mut diffopts) + .unwrap_or(false) + }); + if !m { + return None; + } + } + } + } + if !sig_matches(&commit.author(), &args.flag_author) { + return None; + } + if !sig_matches(&commit.committer(), &args.flag_committer) { + return None; + } + if !log_message_matches(commit.message(), &args.flag_grep) { + return None; + } + Some(Ok(commit)) + }) + .skip(args.flag_skip.unwrap_or(0)) + .take(args.flag_max_count.unwrap_or(!0)); + + // print! + for commit in revwalk { + let commit = commit?; + print_commit(&commit); + if !args.flag_patch || commit.parents().len() > 1 { + continue; + } + let a = if commit.parents().len() == 1 { + let parent = commit.parent(0)?; + Some(parent.tree()?) + } else { + None + }; + let b = commit.tree()?; + let diff = repo.diff_tree_to_tree(a.as_ref(), Some(&b), Some(&mut diffopts2))?; + diff.print(DiffFormat::Patch, |_delta, _hunk, line| { + match line.origin() { + ' ' | '+' | '-' => print!("{}", line.origin()), + _ => {} + } + print!("{}", str::from_utf8(line.content()).unwrap()); + true + })?; + } + + Ok(()) +} + +fn sig_matches(sig: &Signature, arg: &Option<String>) -> bool { + match *arg { + Some(ref s) => { + sig.name().map(|n| n.contains(s)).unwrap_or(false) + || sig.email().map(|n| n.contains(s)).unwrap_or(false) + } + None => true, + } +} + +fn log_message_matches(msg: Option<&str>, grep: &Option<String>) -> bool { + match (grep, msg) { + (&None, _) => true, + (&Some(_), None) => false, + (&Some(ref s), Some(msg)) => msg.contains(s), + } +} + +fn print_commit(commit: &Commit) { + println!("commit {}", commit.id()); + + if commit.parents().len() > 1 { + print!("Merge:"); + for id in commit.parent_ids() { + print!(" {:.8}", id); + } + println!(); + } + + let author = commit.author(); + println!("Author: {}", author); + print_time(&author.when(), "Date: "); + println!(); + + for line in String::from_utf8_lossy(commit.message_bytes()).lines() { + println!(" {}", line); + } + println!(); +} + +fn print_time(time: &Time, prefix: &str) { + let (offset, sign) = match time.offset_minutes() { + n if n < 0 => (-n, '-'), + n => (n, '+'), + }; + let (hours, minutes) = (offset / 60, offset % 60); + let ts = time::Timespec::new(time.seconds() + (time.offset_minutes() as i64) * 60, 0); + let time = time::at(ts); + + println!( + "{}{} {}{:02}{:02}", + prefix, + time.strftime("%a %b %e %T %Y").unwrap(), + sign, + hours, + minutes + ); +} + +fn match_with_parent( + repo: &Repository, + commit: &Commit, + parent: &Commit, + opts: &mut DiffOptions, +) -> Result<bool, Error> { + let a = parent.tree()?; + let b = commit.tree()?; + let diff = repo.diff_tree_to_tree(Some(&a), Some(&b), Some(opts))?; + Ok(diff.deltas().len() > 0) +} + +impl Args { + fn min_parents(&self) -> usize { + if self.flag_no_min_parents { + return 0; + } + self.flag_min_parents + .unwrap_or(if self.flag_merges { 2 } else { 0 }) + } + + fn max_parents(&self) -> Option<usize> { + if self.flag_no_max_parents { + return None; + } + self.flag_max_parents + .or(if self.flag_no_merges { Some(1) } else { None }) + } +} + +fn main() { + let args = Args::from_args(); + match run(&args) { + Ok(()) => {} + Err(e) => println!("error: {}", e), + } +} diff --git a/extra/git2/examples/ls-remote.rs b/extra/git2/examples/ls-remote.rs new file mode 100644 index 000000000..180845941 --- /dev/null +++ b/extra/git2/examples/ls-remote.rs @@ -0,0 +1,51 @@ +/* + * libgit2 "ls-remote" example + * + * Written by the libgit2 contributors + * + * To the extent possible under law, the author(s) have dedicated all copyright + * and related and neighboring rights to this software to the public domain + * worldwide. This software is distributed without any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication along + * with this software. If not, see + * <http://creativecommons.org/publicdomain/zero/1.0/>. + */ + +#![deny(warnings)] + +use git2::{Direction, Repository}; +use structopt::StructOpt; + +#[derive(StructOpt)] +struct Args { + #[structopt(name = "remote")] + arg_remote: String, +} + +fn run(args: &Args) -> Result<(), git2::Error> { + let repo = Repository::open(".")?; + let remote = &args.arg_remote; + let mut remote = repo + .find_remote(remote) + .or_else(|_| repo.remote_anonymous(remote))?; + + // Connect to the remote and call the printing function for each of the + // remote references. + let connection = remote.connect_auth(Direction::Fetch, None, None)?; + + // Get the list of references on the remote and print out their name next to + // what they point to. + for head in connection.list()?.iter() { + println!("{}\t{}", head.oid(), head.name()); + } + Ok(()) +} + +fn main() { + let args = Args::from_args(); + match run(&args) { + Ok(()) => {} + Err(e) => println!("error: {}", e), + } +} diff --git a/extra/git2/examples/pull.rs b/extra/git2/examples/pull.rs new file mode 100644 index 000000000..61251b481 --- /dev/null +++ b/extra/git2/examples/pull.rs @@ -0,0 +1,208 @@ +/* + * libgit2 "pull" example - shows how to pull remote data into a local branch. + * + * Written by the libgit2 contributors + * + * To the extent possible under law, the author(s) have dedicated all copyright + * and related and neighboring rights to this software to the public domain + * worldwide. This software is distributed without any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication along + * with this software. If not, see + * <http://creativecommons.org/publicdomain/zero/1.0/>. + */ + +use git2::Repository; +use std::io::{self, Write}; +use std::str; +use structopt::StructOpt; + +#[derive(StructOpt)] +struct Args { + arg_remote: Option<String>, + arg_branch: Option<String>, +} + +fn do_fetch<'a>( + repo: &'a git2::Repository, + refs: &[&str], + remote: &'a mut git2::Remote, +) -> Result<git2::AnnotatedCommit<'a>, git2::Error> { + let mut cb = git2::RemoteCallbacks::new(); + + // Print out our transfer progress. + cb.transfer_progress(|stats| { + if stats.received_objects() == stats.total_objects() { + print!( + "Resolving deltas {}/{}\r", + stats.indexed_deltas(), + stats.total_deltas() + ); + } else if stats.total_objects() > 0 { + print!( + "Received {}/{} objects ({}) in {} bytes\r", + stats.received_objects(), + stats.total_objects(), + stats.indexed_objects(), + stats.received_bytes() + ); + } + io::stdout().flush().unwrap(); + true + }); + + let mut fo = git2::FetchOptions::new(); + fo.remote_callbacks(cb); + // Always fetch all tags. + // Perform a download and also update tips + fo.download_tags(git2::AutotagOption::All); + println!("Fetching {} for repo", remote.name().unwrap()); + remote.fetch(refs, Some(&mut fo), None)?; + + // If there are local objects (we got a thin pack), then tell the user + // how many objects we saved from having to cross the network. + let stats = remote.stats(); + if stats.local_objects() > 0 { + println!( + "\rReceived {}/{} objects in {} bytes (used {} local \ + objects)", + stats.indexed_objects(), + stats.total_objects(), + stats.received_bytes(), + stats.local_objects() + ); + } else { + println!( + "\rReceived {}/{} objects in {} bytes", + stats.indexed_objects(), + stats.total_objects(), + stats.received_bytes() + ); + } + + let fetch_head = repo.find_reference("FETCH_HEAD")?; + Ok(repo.reference_to_annotated_commit(&fetch_head)?) +} + +fn fast_forward( + repo: &Repository, + lb: &mut git2::Reference, + rc: &git2::AnnotatedCommit, +) -> Result<(), git2::Error> { + let name = match lb.name() { + Some(s) => s.to_string(), + None => String::from_utf8_lossy(lb.name_bytes()).to_string(), + }; + let msg = format!("Fast-Forward: Setting {} to id: {}", name, rc.id()); + println!("{}", msg); + lb.set_target(rc.id(), &msg)?; + repo.set_head(&name)?; + repo.checkout_head(Some( + git2::build::CheckoutBuilder::default() + // For some reason the force is required to make the working directory actually get updated + // I suspect we should be adding some logic to handle dirty working directory states + // but this is just an example so maybe not. + .force(), + ))?; + Ok(()) +} + +fn normal_merge( + repo: &Repository, + local: &git2::AnnotatedCommit, + remote: &git2::AnnotatedCommit, +) -> Result<(), git2::Error> { + let local_tree = repo.find_commit(local.id())?.tree()?; + let remote_tree = repo.find_commit(remote.id())?.tree()?; + let ancestor = repo + .find_commit(repo.merge_base(local.id(), remote.id())?)? + .tree()?; + let mut idx = repo.merge_trees(&ancestor, &local_tree, &remote_tree, None)?; + + if idx.has_conflicts() { + println!("Merge conflicts detected..."); + repo.checkout_index(Some(&mut idx), None)?; + return Ok(()); + } + let result_tree = repo.find_tree(idx.write_tree_to(repo)?)?; + // now create the merge commit + let msg = format!("Merge: {} into {}", remote.id(), local.id()); + let sig = repo.signature()?; + let local_commit = repo.find_commit(local.id())?; + let remote_commit = repo.find_commit(remote.id())?; + // Do our merge commit and set current branch head to that commit. + let _merge_commit = repo.commit( + Some("HEAD"), + &sig, + &sig, + &msg, + &result_tree, + &[&local_commit, &remote_commit], + )?; + // Set working tree to match head. + repo.checkout_head(None)?; + Ok(()) +} + +fn do_merge<'a>( + repo: &'a Repository, + remote_branch: &str, + fetch_commit: git2::AnnotatedCommit<'a>, +) -> Result<(), git2::Error> { + // 1. do a merge analysis + let analysis = repo.merge_analysis(&[&fetch_commit])?; + + // 2. Do the appropriate merge + if analysis.0.is_fast_forward() { + println!("Doing a fast forward"); + // do a fast forward + let refname = format!("refs/heads/{}", remote_branch); + match repo.find_reference(&refname) { + Ok(mut r) => { + fast_forward(repo, &mut r, &fetch_commit)?; + } + Err(_) => { + // The branch doesn't exist so just set the reference to the + // commit directly. Usually this is because you are pulling + // into an empty repository. + repo.reference( + &refname, + fetch_commit.id(), + true, + &format!("Setting {} to {}", remote_branch, fetch_commit.id()), + )?; + repo.set_head(&refname)?; + repo.checkout_head(Some( + git2::build::CheckoutBuilder::default() + .allow_conflicts(true) + .conflict_style_merge(true) + .force(), + ))?; + } + }; + } else if analysis.0.is_normal() { + // do a normal merge + let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?; + normal_merge(&repo, &head_commit, &fetch_commit)?; + } else { + println!("Nothing to do..."); + } + Ok(()) +} + +fn run(args: &Args) -> Result<(), git2::Error> { + let remote_name = args.arg_remote.as_ref().map(|s| &s[..]).unwrap_or("origin"); + let remote_branch = args.arg_branch.as_ref().map(|s| &s[..]).unwrap_or("master"); + let repo = Repository::open(".")?; + let mut remote = repo.find_remote(remote_name)?; + let fetch_commit = do_fetch(&repo, &[remote_branch], &mut remote)?; + do_merge(&repo, &remote_branch, fetch_commit) +} + +fn main() { + let args = Args::from_args(); + match run(&args) { + Ok(()) => {} + Err(e) => println!("error: {}", e), + } +} diff --git a/extra/git2/examples/rev-list.rs b/extra/git2/examples/rev-list.rs new file mode 100644 index 000000000..9b4987728 --- /dev/null +++ b/extra/git2/examples/rev-list.rs @@ -0,0 +1,105 @@ +/* + * libgit2 "rev-list" example - shows how to transform a rev-spec into a list + * of commit ids + * + * Written by the libgit2 contributors + * + * To the extent possible under law, the author(s) have dedicated all copyright + * and related and neighboring rights to this software to the public domain + * worldwide. This software is distributed without any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication along + * with this software. If not, see + * <http://creativecommons.org/publicdomain/zero/1.0/>. + */ + +#![deny(warnings)] + +use git2::{Error, Oid, Repository, Revwalk}; +use structopt::StructOpt; + +#[derive(StructOpt)] +struct Args { + #[structopt(name = "topo-order", long)] + /// sort commits in topological order + flag_topo_order: bool, + #[structopt(name = "date-order", long)] + /// sort commits in date order + flag_date_order: bool, + #[structopt(name = "reverse", long)] + /// sort commits in reverse + flag_reverse: bool, + #[structopt(name = "not")] + /// don't show <spec> + flag_not: Vec<String>, + #[structopt(name = "spec", last = true)] + arg_spec: Vec<String>, +} + +fn run(args: &Args) -> Result<(), git2::Error> { + let repo = Repository::open(".")?; + let mut revwalk = repo.revwalk()?; + + let base = if args.flag_reverse { + git2::Sort::REVERSE + } else { + git2::Sort::NONE + }; + revwalk.set_sorting( + base | if args.flag_topo_order { + git2::Sort::TOPOLOGICAL + } else if args.flag_date_order { + git2::Sort::TIME + } else { + git2::Sort::NONE + }, + )?; + + let specs = args + .flag_not + .iter() + .map(|s| (s, true)) + .chain(args.arg_spec.iter().map(|s| (s, false))) + .map(|(spec, hide)| { + if spec.starts_with('^') { + (&spec[1..], !hide) + } else { + (&spec[..], hide) + } + }); + for (spec, hide) in specs { + let id = if spec.contains("..") { + let revspec = repo.revparse(spec)?; + if revspec.mode().contains(git2::RevparseMode::MERGE_BASE) { + return Err(Error::from_str("merge bases not implemented")); + } + push(&mut revwalk, revspec.from().unwrap().id(), !hide)?; + revspec.to().unwrap().id() + } else { + repo.revparse_single(spec)?.id() + }; + push(&mut revwalk, id, hide)?; + } + + for id in revwalk { + let id = id?; + println!("{}", id); + } + Ok(()) +} + +fn push(revwalk: &mut Revwalk, id: Oid, hide: bool) -> Result<(), Error> { + if hide { + revwalk.hide(id) + } else { + revwalk.push(id) + } +} + +fn main() { + let args = Args::from_args(); + match run(&args) { + Ok(()) => {} + Err(e) => println!("error: {}", e), + } +} diff --git a/extra/git2/examples/rev-parse.rs b/extra/git2/examples/rev-parse.rs new file mode 100644 index 000000000..a465f15a4 --- /dev/null +++ b/extra/git2/examples/rev-parse.rs @@ -0,0 +1,60 @@ +/* + * libgit2 "rev-parse" example - shows how to parse revspecs + * + * Written by the libgit2 contributors + * + * To the extent possible under law, the author(s) have dedicated all copyright + * and related and neighboring rights to this software to the public domain + * worldwide. This software is distributed without any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication along + * with this software. If not, see + * <http://creativecommons.org/publicdomain/zero/1.0/>. + */ + +#![deny(warnings)] + +use git2::Repository; +use structopt::StructOpt; + +#[derive(StructOpt)] +struct Args { + #[structopt(name = "spec")] + arg_spec: String, + #[structopt(name = "dir", long = "git-dir")] + /// directory of the git repository to check + flag_git_dir: Option<String>, +} + +fn run(args: &Args) -> Result<(), git2::Error> { + let path = args.flag_git_dir.as_ref().map(|s| &s[..]).unwrap_or("."); + let repo = Repository::open(path)?; + + let revspec = repo.revparse(&args.arg_spec)?; + + if revspec.mode().contains(git2::RevparseMode::SINGLE) { + println!("{}", revspec.from().unwrap().id()); + } else if revspec.mode().contains(git2::RevparseMode::RANGE) { + let to = revspec.to().unwrap(); + let from = revspec.from().unwrap(); + println!("{}", to.id()); + + if revspec.mode().contains(git2::RevparseMode::MERGE_BASE) { + let base = repo.merge_base(from.id(), to.id())?; + println!("{}", base); + } + + println!("^{}", from.id()); + } else { + return Err(git2::Error::from_str("invalid results from revparse")); + } + Ok(()) +} + +fn main() { + let args = Args::from_args(); + match run(&args) { + Ok(()) => {} + Err(e) => println!("error: {}", e), + } +} diff --git a/extra/git2/examples/status.rs b/extra/git2/examples/status.rs new file mode 100644 index 000000000..4f7bc791c --- /dev/null +++ b/extra/git2/examples/status.rs @@ -0,0 +1,441 @@ +/* + * libgit2 "status" example - shows how to use the status APIs + * + * Written by the libgit2 contributors + * + * To the extent possible under law, the author(s) have dedicated all copyright + * and related and neighboring rights to this software to the public domain + * worldwide. This software is distributed without any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication along + * with this software. If not, see + * <http://creativecommons.org/publicdomain/zero/1.0/>. + */ + +#![deny(warnings)] + +use git2::{Error, ErrorCode, Repository, StatusOptions, SubmoduleIgnore}; +use std::str; +use std::time::Duration; +use structopt::StructOpt; + +#[derive(StructOpt)] +struct Args { + arg_spec: Vec<String>, + #[structopt(name = "long", long)] + /// show longer statuses (default) + _flag_long: bool, + /// show short statuses + #[structopt(name = "short", long)] + flag_short: bool, + #[structopt(name = "porcelain", long)] + /// ?? + flag_porcelain: bool, + #[structopt(name = "branch", short, long)] + /// show branch information + flag_branch: bool, + #[structopt(name = "z", short)] + /// ?? + flag_z: bool, + #[structopt(name = "ignored", long)] + /// show ignored files as well + flag_ignored: bool, + #[structopt(name = "opt-modules", long = "untracked-files")] + /// setting for showing untracked files [no|normal|all] + flag_untracked_files: Option<String>, + #[structopt(name = "opt-files", long = "ignore-submodules")] + /// setting for ignoring submodules [all] + flag_ignore_submodules: Option<String>, + #[structopt(name = "dir", long = "git-dir")] + /// git directory to analyze + flag_git_dir: Option<String>, + #[structopt(name = "repeat", long)] + /// repeatedly show status, sleeping inbetween + flag_repeat: bool, + #[structopt(name = "list-submodules", long)] + /// show submodules + flag_list_submodules: bool, +} + +#[derive(Eq, PartialEq)] +enum Format { + Long, + Short, + Porcelain, +} + +fn run(args: &Args) -> Result<(), Error> { + let path = args.flag_git_dir.clone().unwrap_or_else(|| ".".to_string()); + let repo = Repository::open(&path)?; + if repo.is_bare() { + return Err(Error::from_str("cannot report status on bare repository")); + } + + let mut opts = StatusOptions::new(); + opts.include_ignored(args.flag_ignored); + match args.flag_untracked_files.as_ref().map(|s| &s[..]) { + Some("no") => { + opts.include_untracked(false); + } + Some("normal") => { + opts.include_untracked(true); + } + Some("all") => { + opts.include_untracked(true).recurse_untracked_dirs(true); + } + Some(_) => return Err(Error::from_str("invalid untracked-files value")), + None => {} + } + match args.flag_ignore_submodules.as_ref().map(|s| &s[..]) { + Some("all") => { + opts.exclude_submodules(true); + } + Some(_) => return Err(Error::from_str("invalid ignore-submodules value")), + None => {} + } + opts.include_untracked(!args.flag_ignored); + for spec in &args.arg_spec { + opts.pathspec(spec); + } + + loop { + if args.flag_repeat { + println!("\u{1b}[H\u{1b}[2J"); + } + + let statuses = repo.statuses(Some(&mut opts))?; + + if args.flag_branch { + show_branch(&repo, &args.format())?; + } + if args.flag_list_submodules { + print_submodules(&repo)?; + } + + if args.format() == Format::Long { + print_long(&statuses); + } else { + print_short(&repo, &statuses); + } + + if args.flag_repeat { + std::thread::sleep(Duration::new(10, 0)); + } else { + return Ok(()); + } + } +} + +fn show_branch(repo: &Repository, format: &Format) -> Result<(), Error> { + let head = match repo.head() { + Ok(head) => Some(head), + Err(ref e) if e.code() == ErrorCode::UnbornBranch || e.code() == ErrorCode::NotFound => { + None + } + Err(e) => return Err(e), + }; + let head = head.as_ref().and_then(|h| h.shorthand()); + + if format == &Format::Long { + println!( + "# On branch {}", + head.unwrap_or("Not currently on any branch") + ); + } else { + println!("## {}", head.unwrap_or("HEAD (no branch)")); + } + Ok(()) +} + +fn print_submodules(repo: &Repository) -> Result<(), Error> { + let modules = repo.submodules()?; + println!("# Submodules"); + for sm in &modules { + println!( + "# - submodule '{}' at {}", + sm.name().unwrap(), + sm.path().display() + ); + } + Ok(()) +} + +// This function print out an output similar to git's status command in long +// form, including the command-line hints. +fn print_long(statuses: &git2::Statuses) { + let mut header = false; + let mut rm_in_workdir = false; + let mut changes_in_index = false; + let mut changed_in_workdir = false; + + // Print index changes + for entry in statuses + .iter() + .filter(|e| e.status() != git2::Status::CURRENT) + { + if entry.status().contains(git2::Status::WT_DELETED) { + rm_in_workdir = true; + } + let istatus = match entry.status() { + s if s.contains(git2::Status::INDEX_NEW) => "new file: ", + s if s.contains(git2::Status::INDEX_MODIFIED) => "modified: ", + s if s.contains(git2::Status::INDEX_DELETED) => "deleted: ", + s if s.contains(git2::Status::INDEX_RENAMED) => "renamed: ", + s if s.contains(git2::Status::INDEX_TYPECHANGE) => "typechange:", + _ => continue, + }; + if !header { + println!( + "\ +# Changes to be committed: +# (use \"git reset HEAD <file>...\" to unstage) +#" + ); + header = true; + } + + let old_path = entry.head_to_index().unwrap().old_file().path(); + let new_path = entry.head_to_index().unwrap().new_file().path(); + match (old_path, new_path) { + (Some(old), Some(new)) if old != new => { + println!("#\t{} {} -> {}", istatus, old.display(), new.display()); + } + (old, new) => { + println!("#\t{} {}", istatus, old.or(new).unwrap().display()); + } + } + } + + if header { + changes_in_index = true; + println!("#"); + } + header = false; + + // Print workdir changes to tracked files + for entry in statuses.iter() { + // With `Status::OPT_INCLUDE_UNMODIFIED` (not used in this example) + // `index_to_workdir` may not be `None` even if there are no differences, + // in which case it will be a `Delta::Unmodified`. + if entry.status() == git2::Status::CURRENT || entry.index_to_workdir().is_none() { + continue; + } + + let istatus = match entry.status() { + s if s.contains(git2::Status::WT_MODIFIED) => "modified: ", + s if s.contains(git2::Status::WT_DELETED) => "deleted: ", + s if s.contains(git2::Status::WT_RENAMED) => "renamed: ", + s if s.contains(git2::Status::WT_TYPECHANGE) => "typechange:", + _ => continue, + }; + + if !header { + println!( + "\ +# Changes not staged for commit: +# (use \"git add{} <file>...\" to update what will be committed) +# (use \"git checkout -- <file>...\" to discard changes in working directory) +#\ + ", + if rm_in_workdir { "/rm" } else { "" } + ); + header = true; + } + + let old_path = entry.index_to_workdir().unwrap().old_file().path(); + let new_path = entry.index_to_workdir().unwrap().new_file().path(); + match (old_path, new_path) { + (Some(old), Some(new)) if old != new => { + println!("#\t{} {} -> {}", istatus, old.display(), new.display()); + } + (old, new) => { + println!("#\t{} {}", istatus, old.or(new).unwrap().display()); + } + } + } + + if header { + changed_in_workdir = true; + println!("#"); + } + header = false; + + // Print untracked files + for entry in statuses + .iter() + .filter(|e| e.status() == git2::Status::WT_NEW) + { + if !header { + println!( + "\ +# Untracked files +# (use \"git add <file>...\" to include in what will be committed) +#" + ); + header = true; + } + let file = entry.index_to_workdir().unwrap().old_file().path().unwrap(); + println!("#\t{}", file.display()); + } + header = false; + + // Print ignored files + for entry in statuses + .iter() + .filter(|e| e.status() == git2::Status::IGNORED) + { + if !header { + println!( + "\ +# Ignored files +# (use \"git add -f <file>...\" to include in what will be committed) +#" + ); + header = true; + } + let file = entry.index_to_workdir().unwrap().old_file().path().unwrap(); + println!("#\t{}", file.display()); + } + + if !changes_in_index && changed_in_workdir { + println!( + "no changes added to commit (use \"git add\" and/or \ + \"git commit -a\")" + ); + } +} + +// This version of the output prefixes each path with two status columns and +// shows submodule status information. +fn print_short(repo: &Repository, statuses: &git2::Statuses) { + for entry in statuses + .iter() + .filter(|e| e.status() != git2::Status::CURRENT) + { + let mut istatus = match entry.status() { + s if s.contains(git2::Status::INDEX_NEW) => 'A', + s if s.contains(git2::Status::INDEX_MODIFIED) => 'M', + s if s.contains(git2::Status::INDEX_DELETED) => 'D', + s if s.contains(git2::Status::INDEX_RENAMED) => 'R', + s if s.contains(git2::Status::INDEX_TYPECHANGE) => 'T', + _ => ' ', + }; + let mut wstatus = match entry.status() { + s if s.contains(git2::Status::WT_NEW) => { + if istatus == ' ' { + istatus = '?'; + } + '?' + } + s if s.contains(git2::Status::WT_MODIFIED) => 'M', + s if s.contains(git2::Status::WT_DELETED) => 'D', + s if s.contains(git2::Status::WT_RENAMED) => 'R', + s if s.contains(git2::Status::WT_TYPECHANGE) => 'T', + _ => ' ', + }; + + if entry.status().contains(git2::Status::IGNORED) { + istatus = '!'; + wstatus = '!'; + } + if istatus == '?' && wstatus == '?' { + continue; + } + let mut extra = ""; + + // A commit in a tree is how submodules are stored, so let's go take a + // look at its status. + // + // TODO: check for GIT_FILEMODE_COMMIT + let status = entry.index_to_workdir().and_then(|diff| { + let ignore = SubmoduleIgnore::Unspecified; + diff.new_file() + .path_bytes() + .and_then(|s| str::from_utf8(s).ok()) + .and_then(|name| repo.submodule_status(name, ignore).ok()) + }); + if let Some(status) = status { + if status.contains(git2::SubmoduleStatus::WD_MODIFIED) { + extra = " (new commits)"; + } else if status.contains(git2::SubmoduleStatus::WD_INDEX_MODIFIED) + || status.contains(git2::SubmoduleStatus::WD_WD_MODIFIED) + { + extra = " (modified content)"; + } else if status.contains(git2::SubmoduleStatus::WD_UNTRACKED) { + extra = " (untracked content)"; + } + } + + let (mut a, mut b, mut c) = (None, None, None); + if let Some(diff) = entry.head_to_index() { + a = diff.old_file().path(); + b = diff.new_file().path(); + } + if let Some(diff) = entry.index_to_workdir() { + a = a.or_else(|| diff.old_file().path()); + b = b.or_else(|| diff.old_file().path()); + c = diff.new_file().path(); + } + + match (istatus, wstatus) { + ('R', 'R') => println!( + "RR {} {} {}{}", + a.unwrap().display(), + b.unwrap().display(), + c.unwrap().display(), + extra + ), + ('R', w) => println!( + "R{} {} {}{}", + w, + a.unwrap().display(), + b.unwrap().display(), + extra + ), + (i, 'R') => println!( + "{}R {} {}{}", + i, + a.unwrap().display(), + c.unwrap().display(), + extra + ), + (i, w) => println!("{}{} {}{}", i, w, a.unwrap().display(), extra), + } + } + + for entry in statuses + .iter() + .filter(|e| e.status() == git2::Status::WT_NEW) + { + println!( + "?? {}", + entry + .index_to_workdir() + .unwrap() + .old_file() + .path() + .unwrap() + .display() + ); + } +} + +impl Args { + fn format(&self) -> Format { + if self.flag_short { + Format::Short + } else if self.flag_porcelain || self.flag_z { + Format::Porcelain + } else { + Format::Long + } + } +} + +fn main() { + let args = Args::from_args(); + match run(&args) { + Ok(()) => {} + Err(e) => println!("error: {}", e), + } +} diff --git a/extra/git2/examples/tag.rs b/extra/git2/examples/tag.rs new file mode 100644 index 000000000..c44c2887d --- /dev/null +++ b/extra/git2/examples/tag.rs @@ -0,0 +1,127 @@ +/* + * libgit2 "tag" example - shows how to list, create and delete tags + * + * Written by the libgit2 contributors + * + * To the extent possible under law, the author(s) have dedicated all copyright + * and related and neighboring rights to this software to the public domain + * worldwide. This software is distributed without any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication along + * with this software. If not, see + * <http://creativecommons.org/publicdomain/zero/1.0/>. + */ + +#![deny(warnings)] + +use git2::{Commit, Error, Repository, Tag}; +use std::str; +use structopt::StructOpt; + +#[derive(StructOpt)] +struct Args { + arg_tagname: Option<String>, + arg_object: Option<String>, + arg_pattern: Option<String>, + #[structopt(name = "n", short)] + /// specify number of lines from the annotation to print + flag_n: Option<u32>, + #[structopt(name = "force", short, long)] + /// replace an existing tag with the given name + flag_force: bool, + #[structopt(name = "list", short, long)] + /// list tags with names matching the pattern given + flag_list: bool, + #[structopt(name = "tag", short, long = "delete")] + /// delete the tag specified + flag_delete: Option<String>, + #[structopt(name = "msg", short, long = "message")] + /// message for a new tag + flag_message: Option<String>, +} + +fn run(args: &Args) -> Result<(), Error> { + let repo = Repository::open(".")?; + + if let Some(ref name) = args.arg_tagname { + let target = args.arg_object.as_ref().map(|s| &s[..]).unwrap_or("HEAD"); + let obj = repo.revparse_single(target)?; + + if let Some(ref message) = args.flag_message { + let sig = repo.signature()?; + repo.tag(name, &obj, &sig, message, args.flag_force)?; + } else { + repo.tag_lightweight(name, &obj, args.flag_force)?; + } + } else if let Some(ref name) = args.flag_delete { + let obj = repo.revparse_single(name)?; + let id = obj.short_id()?; + repo.tag_delete(name)?; + println!( + "Deleted tag '{}' (was {})", + name, + str::from_utf8(&*id).unwrap() + ); + } else if args.flag_list { + let pattern = args.arg_pattern.as_ref().map(|s| &s[..]).unwrap_or("*"); + for name in repo.tag_names(Some(pattern))?.iter() { + let name = name.unwrap(); + let obj = repo.revparse_single(name)?; + + if let Some(tag) = obj.as_tag() { + print_tag(tag, args); + } else if let Some(commit) = obj.as_commit() { + print_commit(commit, name, args); + } else { + print_name(name); + } + } + } + Ok(()) +} + +fn print_tag(tag: &Tag, args: &Args) { + print!("{:<16}", tag.name().unwrap()); + if args.flag_n.is_some() { + print_list_lines(tag.message(), args); + } else { + println!(); + } +} + +fn print_commit(commit: &Commit, name: &str, args: &Args) { + print!("{:<16}", name); + if args.flag_n.is_some() { + print_list_lines(commit.message(), args); + } else { + println!(); + } +} + +fn print_name(name: &str) { + println!("{}", name); +} + +fn print_list_lines(message: Option<&str>, args: &Args) { + let message = match message { + Some(s) => s, + None => return, + }; + let mut lines = message.lines().filter(|l| !l.trim().is_empty()); + if let Some(first) = lines.next() { + print!("{}", first); + } + println!(); + + for line in lines.take(args.flag_n.unwrap_or(0) as usize) { + print!(" {}", line); + } +} + +fn main() { + let args = Args::from_args(); + match run(&args) { + Ok(()) => {} + Err(e) => println!("error: {}", e), + } +} diff --git a/extra/git2/src/apply.rs b/extra/git2/src/apply.rs new file mode 100644 index 000000000..34dc811a0 --- /dev/null +++ b/extra/git2/src/apply.rs @@ -0,0 +1,208 @@ +//! git_apply support +//! see original: <https://github.com/libgit2/libgit2/blob/master/include/git2/apply.h> + +use crate::{panic, raw, util::Binding, DiffDelta, DiffHunk}; +use libc::c_int; +use std::{ffi::c_void, mem}; + +/// Possible application locations for git_apply +/// see <https://libgit2.org/libgit2/#HEAD/type/git_apply_options> +#[derive(Copy, Clone, Debug)] +pub enum ApplyLocation { + /// Apply the patch to the workdir + WorkDir, + /// Apply the patch to the index + Index, + /// Apply the patch to both the working directory and the index + Both, +} + +impl Binding for ApplyLocation { + type Raw = raw::git_apply_location_t; + unsafe fn from_raw(raw: raw::git_apply_location_t) -> Self { + match raw { + raw::GIT_APPLY_LOCATION_WORKDIR => Self::WorkDir, + raw::GIT_APPLY_LOCATION_INDEX => Self::Index, + raw::GIT_APPLY_LOCATION_BOTH => Self::Both, + _ => panic!("Unknown git diff binary kind"), + } + } + fn raw(&self) -> raw::git_apply_location_t { + match *self { + Self::WorkDir => raw::GIT_APPLY_LOCATION_WORKDIR, + Self::Index => raw::GIT_APPLY_LOCATION_INDEX, + Self::Both => raw::GIT_APPLY_LOCATION_BOTH, + } + } +} + +/// Options to specify when applying a diff +pub struct ApplyOptions<'cb> { + raw: raw::git_apply_options, + hunk_cb: Option<Box<HunkCB<'cb>>>, + delta_cb: Option<Box<DeltaCB<'cb>>>, +} + +type HunkCB<'a> = dyn FnMut(Option<DiffHunk<'_>>) -> bool + 'a; +type DeltaCB<'a> = dyn FnMut(Option<DiffDelta<'_>>) -> bool + 'a; + +extern "C" fn delta_cb_c(delta: *const raw::git_diff_delta, data: *mut c_void) -> c_int { + panic::wrap(|| unsafe { + let delta = Binding::from_raw_opt(delta as *mut _); + + let payload = &mut *(data as *mut ApplyOptions<'_>); + let callback = match payload.delta_cb { + Some(ref mut c) => c, + None => return -1, + }; + + let apply = callback(delta); + if apply { + 0 + } else { + 1 + } + }) + .unwrap_or(-1) +} + +extern "C" fn hunk_cb_c(hunk: *const raw::git_diff_hunk, data: *mut c_void) -> c_int { + panic::wrap(|| unsafe { + let hunk = Binding::from_raw_opt(hunk); + + let payload = &mut *(data as *mut ApplyOptions<'_>); + let callback = match payload.hunk_cb { + Some(ref mut c) => c, + None => return -1, + }; + + let apply = callback(hunk); + if apply { + 0 + } else { + 1 + } + }) + .unwrap_or(-1) +} + +impl<'cb> ApplyOptions<'cb> { + /// Creates a new set of empty options (zeroed). + pub fn new() -> Self { + let mut opts = Self { + raw: unsafe { mem::zeroed() }, + hunk_cb: None, + delta_cb: None, + }; + assert_eq!( + unsafe { raw::git_apply_options_init(&mut opts.raw, raw::GIT_APPLY_OPTIONS_VERSION) }, + 0 + ); + opts + } + + fn flag(&mut self, opt: raw::git_apply_flags_t, val: bool) -> &mut Self { + let opt = opt as u32; + if val { + self.raw.flags |= opt; + } else { + self.raw.flags &= !opt; + } + self + } + + /// Don't actually make changes, just test that the patch applies. + pub fn check(&mut self, check: bool) -> &mut Self { + self.flag(raw::GIT_APPLY_CHECK, check) + } + + /// When applying a patch, callback that will be made per hunk. + pub fn hunk_callback<F>(&mut self, cb: F) -> &mut Self + where + F: FnMut(Option<DiffHunk<'_>>) -> bool + 'cb, + { + self.hunk_cb = Some(Box::new(cb) as Box<HunkCB<'cb>>); + + self.raw.hunk_cb = Some(hunk_cb_c); + self.raw.payload = self as *mut _ as *mut _; + + self + } + + /// When applying a patch, callback that will be made per delta (file). + pub fn delta_callback<F>(&mut self, cb: F) -> &mut Self + where + F: FnMut(Option<DiffDelta<'_>>) -> bool + 'cb, + { + self.delta_cb = Some(Box::new(cb) as Box<DeltaCB<'cb>>); + + self.raw.delta_cb = Some(delta_cb_c); + self.raw.payload = self as *mut _ as *mut _; + + self + } + + /// Pointer to a raw git_stash_apply_options + pub unsafe fn raw(&mut self) -> *const raw::git_apply_options { + &self.raw as *const _ + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{fs::File, io::Write, path::Path}; + + #[test] + fn smoke_test() { + let (_td, repo) = crate::test::repo_init(); + let diff = t!(repo.diff_tree_to_workdir(None, None)); + let mut count_hunks = 0; + let mut count_delta = 0; + { + let mut opts = ApplyOptions::new(); + opts.hunk_callback(|_hunk| { + count_hunks += 1; + true + }); + opts.delta_callback(|_delta| { + count_delta += 1; + true + }); + t!(repo.apply(&diff, ApplyLocation::Both, Some(&mut opts))); + } + assert_eq!(count_hunks, 0); + assert_eq!(count_delta, 0); + } + + #[test] + fn apply_hunks_and_delta() { + let file_path = Path::new("foo.txt"); + let (td, repo) = crate::test::repo_init(); + // create new file + t!(t!(File::create(&td.path().join(file_path))).write_all(b"bar")); + // stage the new file + t!(t!(repo.index()).add_path(file_path)); + // now change workdir version + t!(t!(File::create(&td.path().join(file_path))).write_all(b"foo\nbar")); + + let diff = t!(repo.diff_index_to_workdir(None, None)); + assert_eq!(diff.deltas().len(), 1); + let mut count_hunks = 0; + let mut count_delta = 0; + { + let mut opts = ApplyOptions::new(); + opts.hunk_callback(|_hunk| { + count_hunks += 1; + true + }); + opts.delta_callback(|_delta| { + count_delta += 1; + true + }); + t!(repo.apply(&diff, ApplyLocation::Index, Some(&mut opts))); + } + assert_eq!(count_delta, 1); + assert_eq!(count_hunks, 1); + } +} diff --git a/extra/git2/src/attr.rs b/extra/git2/src/attr.rs new file mode 100644 index 000000000..33b1d2d4a --- /dev/null +++ b/extra/git2/src/attr.rs @@ -0,0 +1,175 @@ +use crate::raw; +use std::ptr; +use std::str; + +/// All possible states of an attribute. +/// +/// This enum is used to interpret the value returned by +/// [`Repository::get_attr`](crate::Repository::get_attr) and +/// [`Repository::get_attr_bytes`](crate::Repository::get_attr_bytes). +#[derive(Debug, Clone, Copy, Eq)] +pub enum AttrValue<'string> { + /// The attribute is set to true. + True, + /// The attribute is unset (set to false). + False, + /// The attribute is set to a [valid UTF-8 string](prim@str). + String(&'string str), + /// The attribute is set to a string that might not be [valid UTF-8](prim@str). + Bytes(&'string [u8]), + /// The attribute is not specified. + Unspecified, +} + +macro_rules! from_value { + ($value:expr => $string:expr) => { + match unsafe { raw::git_attr_value($value.map_or(ptr::null(), |v| v.as_ptr().cast())) } { + raw::GIT_ATTR_VALUE_TRUE => Self::True, + raw::GIT_ATTR_VALUE_FALSE => Self::False, + raw::GIT_ATTR_VALUE_STRING => $string, + raw::GIT_ATTR_VALUE_UNSPECIFIED => Self::Unspecified, + _ => unreachable!(), + } + }; +} + +impl<'string> AttrValue<'string> { + /// Returns the state of an attribute by inspecting its [value](crate::Repository::get_attr) + /// by a [string](prim@str). + /// + /// This function always returns [`AttrValue::String`] and never returns [`AttrValue::Bytes`] + /// when the attribute is set to a string. + pub fn from_string(value: Option<&'string str>) -> Self { + from_value!(value => Self::String(value.unwrap())) + } + + /// Returns the state of an attribute by inspecting its [value](crate::Repository::get_attr_bytes) + /// by a [byte](u8) [slice]. + /// + /// This function will perform UTF-8 validation when the attribute is set to a string, returns + /// [`AttrValue::String`] if it's valid UTF-8 and [`AttrValue::Bytes`] otherwise. + pub fn from_bytes(value: Option<&'string [u8]>) -> Self { + let mut value = Self::always_bytes(value); + if let Self::Bytes(bytes) = value { + if let Ok(string) = str::from_utf8(bytes) { + value = Self::String(string); + } + } + value + } + + /// Returns the state of an attribute just like [`AttrValue::from_bytes`], but skips UTF-8 + /// validation and always returns [`AttrValue::Bytes`] when it's set to a string. + pub fn always_bytes(value: Option<&'string [u8]>) -> Self { + from_value!(value => Self::Bytes(value.unwrap())) + } +} + +/// Compare two [`AttrValue`]s. +/// +/// Note that this implementation does not differentiate between [`AttrValue::String`] and +/// [`AttrValue::Bytes`]. +impl PartialEq for AttrValue<'_> { + fn eq(&self, other: &AttrValue<'_>) -> bool { + match (self, other) { + (Self::True, AttrValue::True) + | (Self::False, AttrValue::False) + | (Self::Unspecified, AttrValue::Unspecified) => true, + (AttrValue::String(string), AttrValue::Bytes(bytes)) + | (AttrValue::Bytes(bytes), AttrValue::String(string)) => string.as_bytes() == *bytes, + (AttrValue::String(left), AttrValue::String(right)) => left == right, + (AttrValue::Bytes(left), AttrValue::Bytes(right)) => left == right, + _ => false, + } + } +} + +#[cfg(test)] +mod tests { + use super::AttrValue; + + macro_rules! test_attr_value { + ($function:ident, $variant:ident) => { + const ATTR_TRUE: &str = "[internal]__TRUE__"; + const ATTR_FALSE: &str = "[internal]__FALSE__"; + const ATTR_UNSET: &str = "[internal]__UNSET__"; + let as_bytes = AsRef::<[u8]>::as_ref; + // Use `matches!` here since the `PartialEq` implementation does not differentiate + // between `String` and `Bytes`. + assert!(matches!( + AttrValue::$function(Some(ATTR_TRUE.as_ref())), + AttrValue::$variant(s) if as_bytes(s) == ATTR_TRUE.as_bytes() + )); + assert!(matches!( + AttrValue::$function(Some(ATTR_FALSE.as_ref())), + AttrValue::$variant(s) if as_bytes(s) == ATTR_FALSE.as_bytes() + )); + assert!(matches!( + AttrValue::$function(Some(ATTR_UNSET.as_ref())), + AttrValue::$variant(s) if as_bytes(s) == ATTR_UNSET.as_bytes() + )); + assert!(matches!( + AttrValue::$function(Some("foo".as_ref())), + AttrValue::$variant(s) if as_bytes(s) == b"foo" + )); + assert!(matches!( + AttrValue::$function(Some("bar".as_ref())), + AttrValue::$variant(s) if as_bytes(s) == b"bar" + )); + assert_eq!(AttrValue::$function(None), AttrValue::Unspecified); + }; + } + + #[test] + fn attr_value_from_string() { + test_attr_value!(from_string, String); + } + + #[test] + fn attr_value_from_bytes() { + test_attr_value!(from_bytes, String); + assert!(matches!( + AttrValue::from_bytes(Some(&[0xff])), + AttrValue::Bytes(&[0xff]) + )); + assert!(matches!( + AttrValue::from_bytes(Some(b"\xffoobar")), + AttrValue::Bytes(b"\xffoobar") + )); + } + + #[test] + fn attr_value_always_bytes() { + test_attr_value!(always_bytes, Bytes); + assert!(matches!( + AttrValue::always_bytes(Some(&[0xff; 2])), + AttrValue::Bytes(&[0xff, 0xff]) + )); + assert!(matches!( + AttrValue::always_bytes(Some(b"\xffoo")), + AttrValue::Bytes(b"\xffoo") + )); + } + + #[test] + fn attr_value_partial_eq() { + assert_eq!(AttrValue::True, AttrValue::True); + assert_eq!(AttrValue::False, AttrValue::False); + assert_eq!(AttrValue::String("foo"), AttrValue::String("foo")); + assert_eq!(AttrValue::Bytes(b"foo"), AttrValue::Bytes(b"foo")); + assert_eq!(AttrValue::String("bar"), AttrValue::Bytes(b"bar")); + assert_eq!(AttrValue::Bytes(b"bar"), AttrValue::String("bar")); + assert_eq!(AttrValue::Unspecified, AttrValue::Unspecified); + assert_ne!(AttrValue::True, AttrValue::False); + assert_ne!(AttrValue::False, AttrValue::Unspecified); + assert_ne!(AttrValue::Unspecified, AttrValue::True); + assert_ne!(AttrValue::True, AttrValue::String("true")); + assert_ne!(AttrValue::Unspecified, AttrValue::Bytes(b"unspecified")); + assert_ne!(AttrValue::Bytes(b"false"), AttrValue::False); + assert_ne!(AttrValue::String("unspecified"), AttrValue::Unspecified); + assert_ne!(AttrValue::String("foo"), AttrValue::String("bar")); + assert_ne!(AttrValue::Bytes(b"foo"), AttrValue::Bytes(b"bar")); + assert_ne!(AttrValue::String("foo"), AttrValue::Bytes(b"bar")); + assert_ne!(AttrValue::Bytes(b"foo"), AttrValue::String("bar")); + } +} diff --git a/extra/git2/src/blame.rs b/extra/git2/src/blame.rs new file mode 100644 index 000000000..4bf41fed1 --- /dev/null +++ b/extra/git2/src/blame.rs @@ -0,0 +1,379 @@ +use crate::util::{self, Binding}; +use crate::{raw, signature, Error, Oid, Repository, Signature}; +use libc::c_char; +use std::iter::FusedIterator; +use std::mem; +use std::ops::Range; +use std::path::Path; +use std::{marker, ptr}; + +/// Opaque structure to hold blame results. +pub struct Blame<'repo> { + raw: *mut raw::git_blame, + _marker: marker::PhantomData<&'repo Repository>, +} + +/// Structure that represents a blame hunk. +pub struct BlameHunk<'blame> { + raw: *mut raw::git_blame_hunk, + _marker: marker::PhantomData<&'blame raw::git_blame>, +} + +/// Blame options +pub struct BlameOptions { + raw: raw::git_blame_options, +} + +/// An iterator over the hunks in a blame. +pub struct BlameIter<'blame> { + range: Range<usize>, + blame: &'blame Blame<'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<Blame<'_>, Error> { + let mut raw = ptr::null_mut(); + + unsafe { + try_call!(raw::git_blame_buffer( + &mut raw, + self.raw, + buffer.as_ptr() as *const c_char, + buffer.len() + )); + Ok(Binding::from_raw(raw)) + } + } + + /// Gets the number of hunks that exist in the blame structure. + pub fn len(&self) -> usize { + unsafe { raw::git_blame_get_hunk_count(self.raw) as usize } + } + + /// Return `true` is there is no hunk in the blame structure. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Gets the blame hunk at the given index. + pub fn get_index(&self, index: usize) -> Option<BlameHunk<'_>> { + unsafe { + let ptr = raw::git_blame_get_hunk_byindex(self.raw(), index as u32); + if ptr.is_null() { + None + } else { + Some(BlameHunk::from_raw_const(ptr)) + } + } + } + + /// Gets the hunk that relates to the given line number in the newest + /// commit. + pub fn get_line(&self, lineno: usize) -> Option<BlameHunk<'_>> { + unsafe { + let ptr = raw::git_blame_get_hunk_byline(self.raw(), lineno); + if ptr.is_null() { + None + } else { + Some(BlameHunk::from_raw_const(ptr)) + } + } + } + + /// Returns an iterator over the hunks in this blame. + pub fn iter(&self) -> BlameIter<'_> { + BlameIter { + range: 0..self.len(), + blame: self, + } + } +} + +impl<'blame> BlameHunk<'blame> { + unsafe fn from_raw_const(raw: *const raw::git_blame_hunk) -> BlameHunk<'blame> { + BlameHunk { + raw: raw as *mut raw::git_blame_hunk, + _marker: marker::PhantomData, + } + } + + /// Returns OID of the commit where this line was last changed + pub fn final_commit_id(&self) -> Oid { + unsafe { Oid::from_raw(&(*self.raw).final_commit_id) } + } + + /// Returns signature of the commit. + pub fn final_signature(&self) -> Signature<'_> { + unsafe { signature::from_raw_const(self, (*self.raw).final_signature) } + } + + /// Returns line number where this hunk begins. + /// + /// Note that the start line is counting from 1. + pub fn final_start_line(&self) -> usize { + unsafe { (*self.raw).final_start_line_number } + } + + /// Returns the OID of the commit where this hunk was found. + /// + /// This will usually be the same as `final_commit_id`, + /// except when `BlameOptions::track_copies_any_commit_copies` has been + /// turned on + pub fn orig_commit_id(&self) -> Oid { + unsafe { Oid::from_raw(&(*self.raw).orig_commit_id) } + } + + /// Returns signature of the commit. + pub fn orig_signature(&self) -> Signature<'_> { + unsafe { signature::from_raw_const(self, (*self.raw).orig_signature) } + } + + /// Returns line number where this hunk begins. + /// + /// Note that the start line is counting from 1. + pub fn orig_start_line(&self) -> usize { + unsafe { (*self.raw).orig_start_line_number } + } + + /// Returns path to the file where this hunk originated. + /// + /// Note: `None` could be returned for non-unicode paths on Windows. + pub fn path(&self) -> Option<&Path> { + unsafe { + if let Some(bytes) = crate::opt_bytes(self, (*self.raw).orig_path) { + Some(util::bytes2path(bytes)) + } else { + None + } + } + } + + /// Tests whether this hunk has been tracked to a boundary commit + /// (the root, or the commit specified in git_blame_options.oldest_commit). + pub fn is_boundary(&self) -> bool { + unsafe { (*self.raw).boundary == 1 } + } + + /// Returns number of lines in this hunk. + pub fn lines_in_hunk(&self) -> usize { + unsafe { (*self.raw).lines_in_hunk as usize } + } +} + +impl Default for BlameOptions { + fn default() -> Self { + Self::new() + } +} + +impl BlameOptions { + /// Initialize options + pub fn new() -> BlameOptions { + unsafe { + let mut raw: raw::git_blame_options = mem::zeroed(); + assert_eq!( + raw::git_blame_init_options(&mut raw, raw::GIT_BLAME_OPTIONS_VERSION), + 0 + ); + + Binding::from_raw(&raw as *const _ as *mut _) + } + } + + fn flag(&mut self, opt: u32, val: bool) -> &mut BlameOptions { + if val { + self.raw.flags |= opt; + } else { + self.raw.flags &= !opt; + } + self + } + + /// Track lines that have moved within a file. + pub fn track_copies_same_file(&mut self, opt: bool) -> &mut BlameOptions { + self.flag(raw::GIT_BLAME_TRACK_COPIES_SAME_FILE, opt) + } + + /// Track lines that have moved across files in the same commit. + pub fn track_copies_same_commit_moves(&mut self, opt: bool) -> &mut BlameOptions { + self.flag(raw::GIT_BLAME_TRACK_COPIES_SAME_COMMIT_MOVES, opt) + } + + /// Track lines that have been copied from another file that exists + /// in the same commit. + pub fn track_copies_same_commit_copies(&mut self, opt: bool) -> &mut BlameOptions { + self.flag(raw::GIT_BLAME_TRACK_COPIES_SAME_COMMIT_COPIES, opt) + } + + /// Track lines that have been copied from another file that exists + /// in any commit. + pub fn track_copies_any_commit_copies(&mut self, opt: bool) -> &mut BlameOptions { + self.flag(raw::GIT_BLAME_TRACK_COPIES_ANY_COMMIT_COPIES, opt) + } + + /// Restrict the search of commits to those reachable following only + /// the first parents. + pub fn first_parent(&mut self, opt: bool) -> &mut BlameOptions { + self.flag(raw::GIT_BLAME_FIRST_PARENT, opt) + } + + /// Use mailmap file to map author and committer names and email addresses + /// to canonical real names and email addresses. The mailmap will be read + /// from the working directory, or HEAD in a bare repository. + pub fn use_mailmap(&mut self, opt: bool) -> &mut BlameOptions { + self.flag(raw::GIT_BLAME_USE_MAILMAP, opt) + } + + /// Ignore whitespace differences. + pub fn ignore_whitespace(&mut self, opt: bool) -> &mut BlameOptions { + self.flag(raw::GIT_BLAME_IGNORE_WHITESPACE, opt) + } + + /// Setter for the id of the newest commit to consider. + pub fn newest_commit(&mut self, id: Oid) -> &mut BlameOptions { + unsafe { + self.raw.newest_commit = *id.raw(); + } + self + } + + /// Setter for the id of the oldest commit to consider. + pub fn oldest_commit(&mut self, id: Oid) -> &mut BlameOptions { + unsafe { + self.raw.oldest_commit = *id.raw(); + } + self + } + + /// The first line in the file to blame. + pub fn min_line(&mut self, lineno: usize) -> &mut BlameOptions { + self.raw.min_line = lineno; + self + } + + /// The last line in the file to blame. + pub fn max_line(&mut self, lineno: usize) -> &mut BlameOptions { + self.raw.max_line = lineno; + self + } +} + +impl<'repo> Binding for Blame<'repo> { + type Raw = *mut raw::git_blame; + + unsafe fn from_raw(raw: *mut raw::git_blame) -> Blame<'repo> { + Blame { + raw, + _marker: marker::PhantomData, + } + } + + fn raw(&self) -> *mut raw::git_blame { + self.raw + } +} + +impl<'repo> Drop for Blame<'repo> { + fn drop(&mut self) { + unsafe { raw::git_blame_free(self.raw) } + } +} + +impl<'blame> Binding for BlameHunk<'blame> { + type Raw = *mut raw::git_blame_hunk; + + unsafe fn from_raw(raw: *mut raw::git_blame_hunk) -> BlameHunk<'blame> { + BlameHunk { + raw, + _marker: marker::PhantomData, + } + } + + fn raw(&self) -> *mut raw::git_blame_hunk { + self.raw + } +} + +impl Binding for BlameOptions { + type Raw = *mut raw::git_blame_options; + + unsafe fn from_raw(opts: *mut raw::git_blame_options) -> BlameOptions { + BlameOptions { raw: *opts } + } + + fn raw(&self) -> *mut raw::git_blame_options { + &self.raw as *const _ as *mut _ + } +} + +impl<'blame> Iterator for BlameIter<'blame> { + type Item = BlameHunk<'blame>; + fn next(&mut self) -> Option<BlameHunk<'blame>> { + self.range.next().and_then(|i| self.blame.get_index(i)) + } + + fn size_hint(&self) -> (usize, Option<usize>) { + self.range.size_hint() + } +} + +impl<'blame> DoubleEndedIterator for BlameIter<'blame> { + fn next_back(&mut self) -> Option<BlameHunk<'blame>> { + self.range.next_back().and_then(|i| self.blame.get_index(i)) + } +} + +impl<'blame> FusedIterator for BlameIter<'blame> {} + +impl<'blame> ExactSizeIterator for BlameIter<'blame> {} + +#[cfg(test)] +mod tests { + use std::fs::{self, File}; + use std::path::Path; + + #[test] + fn smoke() { + let (_td, repo) = crate::test::repo_init(); + let mut index = repo.index().unwrap(); + + let root = repo.workdir().unwrap(); + fs::create_dir(&root.join("foo")).unwrap(); + File::create(&root.join("foo/bar")).unwrap(); + index.add_path(Path::new("foo/bar")).unwrap(); + + let id = index.write_tree().unwrap(); + let tree = repo.find_tree(id).unwrap(); + let sig = repo.signature().unwrap(); + let id = repo.refname_to_id("HEAD").unwrap(); + let parent = repo.find_commit(id).unwrap(); + let commit = repo + .commit(Some("HEAD"), &sig, &sig, "commit", &tree, &[&parent]) + .unwrap(); + + let blame = repo.blame_file(Path::new("foo/bar"), None).unwrap(); + + assert_eq!(blame.len(), 1); + assert_eq!(blame.iter().count(), 1); + + let hunk = blame.get_index(0).unwrap(); + assert_eq!(hunk.final_commit_id(), commit); + assert_eq!(hunk.final_signature().name(), sig.name()); + assert_eq!(hunk.final_signature().email(), sig.email()); + 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()); + + 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/extra/git2/src/blob.rs b/extra/git2/src/blob.rs new file mode 100644 index 000000000..5c4a6ce6b --- /dev/null +++ b/extra/git2/src/blob.rs @@ -0,0 +1,208 @@ +use std::io; +use std::marker; +use std::mem; +use std::slice; + +use crate::util::Binding; +use crate::{raw, Error, Object, Oid}; + +/// A structure to represent a git [blob][1] +/// +/// [1]: http://git-scm.com/book/en/Git-Internals-Git-Objects +pub struct Blob<'repo> { + raw: *mut raw::git_blob, + _marker: marker::PhantomData<Object<'repo>>, +} + +impl<'repo> Blob<'repo> { + /// Get the id (SHA1) of a repository blob + pub fn id(&self) -> Oid { + unsafe { Binding::from_raw(raw::git_blob_id(&*self.raw)) } + } + + /// Determine if the blob content is most certainly binary or not. + pub fn is_binary(&self) -> bool { + unsafe { raw::git_blob_is_binary(&*self.raw) == 1 } + } + + /// Get the content of this blob. + pub fn content(&self) -> &[u8] { + unsafe { + let data = raw::git_blob_rawcontent(&*self.raw) as *const u8; + let len = raw::git_blob_rawsize(&*self.raw) as usize; + slice::from_raw_parts(data, len) + } + } + + /// Get the size in bytes of the contents of this blob. + pub fn size(&self) -> usize { + unsafe { raw::git_blob_rawsize(&*self.raw) as usize } + } + + /// Casts this Blob to be usable as an `Object` + pub fn as_object(&self) -> &Object<'repo> { + unsafe { &*(self as *const _ as *const Object<'repo>) } + } + + /// Consumes Blob to be returned as an `Object` + pub fn into_object(self) -> Object<'repo> { + assert_eq!(mem::size_of_val(&self), mem::size_of::<Object<'_>>()); + unsafe { mem::transmute(self) } + } +} + +impl<'repo> Binding for Blob<'repo> { + type Raw = *mut raw::git_blob; + + unsafe fn from_raw(raw: *mut raw::git_blob) -> Blob<'repo> { + Blob { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_blob { + self.raw + } +} + +impl<'repo> std::fmt::Debug for Blob<'repo> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_struct("Blob").field("id", &self.id()).finish() + } +} + +impl<'repo> Clone for Blob<'repo> { + fn clone(&self) -> Self { + self.as_object().clone().into_blob().ok().unwrap() + } +} + +impl<'repo> Drop for Blob<'repo> { + fn drop(&mut self) { + unsafe { raw::git_blob_free(self.raw) } + } +} + +/// A structure to represent a git writestream for blobs +pub struct BlobWriter<'repo> { + raw: *mut raw::git_writestream, + need_cleanup: bool, + _marker: marker::PhantomData<Object<'repo>>, +} + +impl<'repo> BlobWriter<'repo> { + /// Finalize blob writing stream and write the blob to the object db + pub fn commit(mut self) -> Result<Oid, Error> { + // After commit we already doesn't need cleanup on drop + self.need_cleanup = false; + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_blob_create_fromstream_commit(&mut raw, self.raw)); + Ok(Binding::from_raw(&raw as *const _)) + } + } +} + +impl<'repo> Binding for BlobWriter<'repo> { + type Raw = *mut raw::git_writestream; + + unsafe fn from_raw(raw: *mut raw::git_writestream) -> BlobWriter<'repo> { + BlobWriter { + raw, + need_cleanup: true, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_writestream { + self.raw + } +} + +impl<'repo> Drop for BlobWriter<'repo> { + fn drop(&mut self) { + // We need cleanup in case the stream has not been committed + if self.need_cleanup { + unsafe { + if let Some(f) = (*self.raw).free { + f(self.raw) + } + } + } + } +} + +impl<'repo> io::Write for BlobWriter<'repo> { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + unsafe { + if let Some(f) = (*self.raw).write { + let res = f(self.raw, buf.as_ptr() as *const _, buf.len()); + if res < 0 { + Err(io::Error::new(io::ErrorKind::Other, "Write error")) + } else { + Ok(buf.len()) + } + } else { + Err(io::Error::new(io::ErrorKind::Other, "no write callback")) + } + } + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::Repository; + use std::fs::File; + use std::io::prelude::*; + use std::path::Path; + use tempfile::TempDir; + + #[test] + fn buffer() { + let td = TempDir::new().unwrap(); + let repo = Repository::init(td.path()).unwrap(); + let id = repo.blob(&[5, 4, 6]).unwrap(); + let blob = repo.find_blob(id).unwrap(); + + assert_eq!(blob.id(), id); + assert_eq!(blob.size(), 3); + assert_eq!(blob.content(), [5, 4, 6]); + assert!(blob.is_binary()); + + repo.find_object(id, None).unwrap().as_blob().unwrap(); + repo.find_object(id, None) + .unwrap() + .into_blob() + .ok() + .unwrap(); + } + + #[test] + fn path() { + let td = TempDir::new().unwrap(); + let path = td.path().join("foo"); + File::create(&path).unwrap().write_all(&[7, 8, 9]).unwrap(); + let repo = Repository::init(td.path()).unwrap(); + let id = repo.blob_path(&path).unwrap(); + let blob = repo.find_blob(id).unwrap(); + assert_eq!(blob.content(), [7, 8, 9]); + blob.into_object(); + } + + #[test] + fn stream() { + let td = TempDir::new().unwrap(); + let repo = Repository::init(td.path()).unwrap(); + let mut ws = repo.blob_writer(Some(Path::new("foo"))).unwrap(); + let wl = ws.write(&[10, 11, 12]).unwrap(); + assert_eq!(wl, 3); + let id = ws.commit().unwrap(); + let blob = repo.find_blob(id).unwrap(); + assert_eq!(blob.content(), [10, 11, 12]); + blob.into_object(); + } +} diff --git a/extra/git2/src/branch.rs b/extra/git2/src/branch.rs new file mode 100644 index 000000000..e1eba99c2 --- /dev/null +++ b/extra/git2/src/branch.rs @@ -0,0 +1,197 @@ +use std::ffi::CString; +use std::marker; +use std::ptr; +use std::str; + +use crate::util::Binding; +use crate::{raw, BranchType, Error, Reference, References}; + +/// A structure to represent a git [branch][1] +/// +/// A branch is currently just a wrapper to an underlying `Reference`. The +/// reference can be accessed through the `get` and `into_reference` methods. +/// +/// [1]: http://git-scm.com/book/en/Git-Branching-What-a-Branch-Is +pub struct Branch<'repo> { + inner: Reference<'repo>, +} + +/// An iterator over the branches inside of a repository. +pub struct Branches<'repo> { + raw: *mut raw::git_branch_iterator, + _marker: marker::PhantomData<References<'repo>>, +} + +impl<'repo> Branch<'repo> { + /// Creates Branch type from a Reference + pub fn wrap(reference: Reference<'_>) -> Branch<'_> { + Branch { inner: reference } + } + + /// Ensure the branch name is well-formed. + pub fn name_is_valid(name: &str) -> Result<bool, Error> { + 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 + } + + /// Gain mutable access to the reference that is this branch + pub fn get_mut(&mut self) -> &mut Reference<'repo> { + &mut self.inner + } + + /// Take ownership of the underlying reference. + pub fn into_reference(self) -> Reference<'repo> { + self.inner + } + + /// Delete an existing branch reference. + pub fn delete(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_branch_delete(self.get().raw())); + } + Ok(()) + } + + /// Determine if the current local branch is pointed at by HEAD. + pub fn is_head(&self) -> bool { + unsafe { raw::git_branch_is_head(&*self.get().raw()) == 1 } + } + + /// Move/rename an existing local branch reference. + pub fn rename(&mut self, new_branch_name: &str, force: bool) -> Result<Branch<'repo>, Error> { + let mut ret = ptr::null_mut(); + let new_branch_name = CString::new(new_branch_name)?; + unsafe { + try_call!(raw::git_branch_move( + &mut ret, + self.get().raw(), + new_branch_name, + force + )); + Ok(Branch::wrap(Binding::from_raw(ret))) + } + } + + /// Return the name of the given local or remote branch. + /// + /// May return `Ok(None)` if the name is not valid utf-8. + pub fn name(&self) -> Result<Option<&str>, Error> { + self.name_bytes().map(|s| str::from_utf8(s).ok()) + } + + /// Return the name of the given local or remote branch. + pub fn name_bytes(&self) -> Result<&[u8], Error> { + let mut ret = ptr::null(); + unsafe { + try_call!(raw::git_branch_name(&mut ret, &*self.get().raw())); + Ok(crate::opt_bytes(self, ret).unwrap()) + } + } + + /// Return the reference supporting the remote tracking branch, given a + /// local branch reference. + pub fn upstream(&self) -> Result<Branch<'repo>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_branch_upstream(&mut ret, &*self.get().raw())); + Ok(Branch::wrap(Binding::from_raw(ret))) + } + } + + /// Set the upstream configuration for a given local branch. + /// + /// If `None` is specified, then the upstream branch is unset. The name + /// provided is the name of the branch to set as upstream. + pub fn set_upstream(&mut self, upstream_name: Option<&str>) -> Result<(), Error> { + let upstream_name = crate::opt_cstr(upstream_name)?; + unsafe { + try_call!(raw::git_branch_set_upstream( + self.get().raw(), + upstream_name + )); + Ok(()) + } + } +} + +impl<'repo> Branches<'repo> { + /// Creates a new iterator from the raw pointer given. + /// + /// This function is unsafe as it is not guaranteed that `raw` is a valid + /// pointer. + pub unsafe fn from_raw(raw: *mut raw::git_branch_iterator) -> Branches<'repo> { + Branches { + raw, + _marker: marker::PhantomData, + } + } +} + +impl<'repo> Iterator for Branches<'repo> { + type Item = Result<(Branch<'repo>, BranchType), Error>; + fn next(&mut self) -> Option<Result<(Branch<'repo>, BranchType), Error>> { + let mut ret = ptr::null_mut(); + let mut typ = raw::GIT_BRANCH_LOCAL; + unsafe { + try_call_iter!(raw::git_branch_next(&mut ret, &mut typ, self.raw)); + let typ = match typ { + raw::GIT_BRANCH_LOCAL => BranchType::Local, + raw::GIT_BRANCH_REMOTE => BranchType::Remote, + n => panic!("unexected branch type: {}", n), + }; + Some(Ok((Branch::wrap(Binding::from_raw(ret)), typ))) + } + } +} + +impl<'repo> Drop for Branches<'repo> { + fn drop(&mut self) { + unsafe { raw::git_branch_iterator_free(self.raw) } + } +} + +#[cfg(test)] +mod tests { + use crate::{Branch, BranchType}; + + #[test] + fn smoke() { + let (_td, repo) = crate::test::repo_init(); + let head = repo.head().unwrap(); + let target = head.target().unwrap(); + let commit = repo.find_commit(target).unwrap(); + + let mut b1 = repo.branch("foo", &commit, false).unwrap(); + assert!(!b1.is_head()); + repo.branch("foo2", &commit, false).unwrap(); + + assert_eq!(repo.branches(None).unwrap().count(), 3); + repo.find_branch("foo", BranchType::Local).unwrap(); + let mut b1 = b1.rename("bar", false).unwrap(); + assert_eq!(b1.name().unwrap(), Some("bar")); + assert!(b1.upstream().is_err()); + b1.set_upstream(Some("main")).unwrap(); + b1.upstream().unwrap(); + b1.set_upstream(None).unwrap(); + + b1.delete().unwrap(); + } + + #[test] + fn name_is_valid() { + assert!(Branch::name_is_valid("foo").unwrap()); + assert!(!Branch::name_is_valid("").unwrap()); + assert!(!Branch::name_is_valid("with spaces").unwrap()); + assert!(!Branch::name_is_valid("~tilde").unwrap()); + } +} diff --git a/extra/git2/src/buf.rs b/extra/git2/src/buf.rs new file mode 100644 index 000000000..fd2bcbf96 --- /dev/null +++ b/extra/git2/src/buf.rs @@ -0,0 +1,71 @@ +use std::ops::{Deref, DerefMut}; +use std::ptr; +use std::slice; +use std::str; + +use crate::raw; +use crate::util::Binding; + +/// A structure to wrap an intermediate buffer used by libgit2. +/// +/// A buffer can be thought of a `Vec<u8>`, but the `Vec` type is not used to +/// avoid copying data back and forth. +pub struct Buf { + raw: raw::git_buf, +} + +impl Default for Buf { + fn default() -> Self { + Self::new() + } +} + +impl Buf { + /// Creates a new empty buffer. + pub fn new() -> Buf { + crate::init(); + unsafe { + Binding::from_raw(&mut raw::git_buf { + ptr: ptr::null_mut(), + size: 0, + reserved: 0, + } as *mut _) + } + } + + /// Attempt to view this buffer as a string slice. + /// + /// Returns `None` if the buffer is not valid utf-8. + pub fn as_str(&self) -> Option<&str> { + str::from_utf8(&**self).ok() + } +} + +impl Deref for Buf { + type Target = [u8]; + fn deref(&self) -> &[u8] { + unsafe { slice::from_raw_parts(self.raw.ptr as *const u8, self.raw.size as usize) } + } +} + +impl DerefMut for Buf { + fn deref_mut(&mut self) -> &mut [u8] { + unsafe { slice::from_raw_parts_mut(self.raw.ptr as *mut u8, self.raw.size as usize) } + } +} + +impl Binding for Buf { + type Raw = *mut raw::git_buf; + unsafe fn from_raw(raw: *mut raw::git_buf) -> Buf { + Buf { raw: *raw } + } + fn raw(&self) -> *mut raw::git_buf { + &self.raw as *const _ as *mut _ + } +} + +impl Drop for Buf { + fn drop(&mut self) { + unsafe { raw::git_buf_dispose(&mut self.raw) } + } +} diff --git a/extra/git2/src/build.rs b/extra/git2/src/build.rs new file mode 100644 index 000000000..d3c95f655 --- /dev/null +++ b/extra/git2/src/build.rs @@ -0,0 +1,861 @@ +//! Builder-pattern objects for configuration various git operations. + +use libc::{c_char, c_int, c_uint, c_void, size_t}; +use std::ffi::{CStr, CString}; +use std::mem; +use std::path::Path; +use std::ptr; + +use crate::util::{self, Binding}; +use crate::{panic, raw, Error, FetchOptions, IntoCString, Oid, Repository, Tree}; +use crate::{CheckoutNotificationType, DiffFile, FileMode, Remote}; + +/// A builder struct which is used to build configuration for cloning a new git +/// repository. +/// +/// # Example +/// +/// Cloning using SSH: +/// +/// ```no_run +/// use git2::{Cred, Error, RemoteCallbacks}; +/// use std::env; +/// use std::path::Path; +/// +/// // Prepare callbacks. +/// let mut callbacks = RemoteCallbacks::new(); +/// callbacks.credentials(|_url, username_from_url, _allowed_types| { +/// Cred::ssh_key( +/// username_from_url.unwrap(), +/// None, +/// Path::new(&format!("{}/.ssh/id_rsa", env::var("HOME").unwrap())), +/// None, +/// ) +/// }); +/// +/// // Prepare fetch options. +/// let mut fo = git2::FetchOptions::new(); +/// fo.remote_callbacks(callbacks); +/// +/// // Prepare builder. +/// let mut builder = git2::build::RepoBuilder::new(); +/// builder.fetch_options(fo); +/// +/// // Clone the project. +/// builder.clone( +/// "git@github.com:rust-lang/git2-rs.git", +/// Path::new("/tmp/git2-rs"), +/// ); +/// ``` +pub struct RepoBuilder<'cb> { + bare: bool, + branch: Option<CString>, + local: bool, + hardlinks: bool, + checkout: Option<CheckoutBuilder<'cb>>, + fetch_opts: Option<FetchOptions<'cb>>, + clone_local: Option<CloneLocal>, + remote_create: Option<Box<RemoteCreate<'cb>>>, +} + +/// Type of callback passed to `RepoBuilder::remote_create`. +/// +/// The second and third arguments are the remote's name and the remote's URL. +pub type RemoteCreate<'cb> = + dyn for<'a> FnMut(&'a Repository, &str, &str) -> Result<Remote<'a>, Error> + 'cb; + +/// A builder struct for git tree updates. +/// +/// Paths passed to `remove` and `upsert` can be multi-component paths, i.e. they +/// may contain slashes. +/// +/// This is a higher-level tree update facility. There is also [`TreeBuilder`] +/// which is lower-level (and operates only on one level of the tree at a time). +/// +/// [`TreeBuilder`]: crate::TreeBuilder +pub struct TreeUpdateBuilder { + updates: Vec<raw::git_tree_update>, + paths: Vec<CString>, +} + +/// A builder struct for configuring checkouts of a repository. +pub struct CheckoutBuilder<'cb> { + their_label: Option<CString>, + our_label: Option<CString>, + ancestor_label: Option<CString>, + target_dir: Option<CString>, + paths: Vec<CString>, + path_ptrs: Vec<*const c_char>, + file_perm: Option<i32>, + dir_perm: Option<i32>, + disable_filters: bool, + checkout_opts: u32, + progress: Option<Box<Progress<'cb>>>, + notify: Option<Box<Notify<'cb>>>, + notify_flags: CheckoutNotificationType, +} + +/// Checkout progress notification callback. +/// +/// 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; + +/// Checkout notifications callback. +/// +/// The first argument is the notification type, the next is the path for the +/// the notification, followed by the baseline diff, target diff, and workdir diff. +/// +/// The callback must return a bool specifying whether the checkout should +/// continue. +pub type Notify<'a> = dyn FnMut( + CheckoutNotificationType, + Option<&Path>, + Option<DiffFile<'_>>, + Option<DiffFile<'_>>, + Option<DiffFile<'_>>, + ) -> bool + + 'a; + +impl<'cb> Default for RepoBuilder<'cb> { + fn default() -> Self { + Self::new() + } +} + +/// Options that can be passed to `RepoBuilder::clone_local`. +#[derive(Clone, Copy)] +pub enum CloneLocal { + /// Auto-detect (default) + /// + /// Here libgit2 will bypass the git-aware transport for local paths, but + /// use a normal fetch for `file://` URLs. + Auto = raw::GIT_CLONE_LOCAL_AUTO as isize, + + /// Bypass the git-aware transport even for `file://` URLs. + Local = raw::GIT_CLONE_LOCAL as isize, + + /// Never bypass the git-aware transport + None = raw::GIT_CLONE_NO_LOCAL as isize, + + /// Bypass the git-aware transport, but don't try to use hardlinks. + NoLinks = raw::GIT_CLONE_LOCAL_NO_LINKS as isize, + + #[doc(hidden)] + __Nonexhaustive = 0xff, +} + +impl<'cb> RepoBuilder<'cb> { + /// Creates a new repository builder with all of the default configuration. + /// + /// When ready, the `clone()` method can be used to clone a new repository + /// using this configuration. + pub fn new() -> RepoBuilder<'cb> { + crate::init(); + RepoBuilder { + bare: false, + branch: None, + local: true, + clone_local: None, + hardlinks: true, + checkout: None, + fetch_opts: None, + remote_create: None, + } + } + + /// Indicate whether the repository will be cloned as a bare repository or + /// not. + pub fn bare(&mut self, bare: bool) -> &mut RepoBuilder<'cb> { + self.bare = bare; + self + } + + /// Specify the name of the branch to check out after the clone. + /// + /// If not specified, the remote's default branch will be used. + pub fn branch(&mut self, branch: &str) -> &mut RepoBuilder<'cb> { + self.branch = Some(CString::new(branch).unwrap()); + self + } + + /// Configures options for bypassing the git-aware transport on clone. + /// + /// Bypassing it means that instead of a fetch libgit2 will copy the object + /// database directory instead of figuring out what it needs, which is + /// faster. If possible, it will hardlink the files to save space. + pub fn clone_local(&mut self, clone_local: CloneLocal) -> &mut RepoBuilder<'cb> { + self.clone_local = Some(clone_local); + self + } + + /// Set the flag for bypassing the git aware transport mechanism for local + /// paths. + /// + /// If `true`, the git-aware transport will be bypassed for local paths. If + /// `false`, the git-aware transport will not be bypassed. + #[deprecated(note = "use `clone_local` instead")] + #[doc(hidden)] + pub fn local(&mut self, local: bool) -> &mut RepoBuilder<'cb> { + self.local = local; + self + } + + /// Set the flag for whether hardlinks are used when using a local git-aware + /// transport mechanism. + #[deprecated(note = "use `clone_local` instead")] + #[doc(hidden)] + pub fn hardlinks(&mut self, links: bool) -> &mut RepoBuilder<'cb> { + self.hardlinks = links; + self + } + + /// Configure the checkout which will be performed by consuming a checkout + /// builder. + pub fn with_checkout(&mut self, checkout: CheckoutBuilder<'cb>) -> &mut RepoBuilder<'cb> { + self.checkout = Some(checkout); + self + } + + /// Options which control the fetch, including callbacks. + /// + /// The callbacks are used for reporting fetch progress, and for acquiring + /// credentials in the event they are needed. + pub fn fetch_options(&mut self, fetch_opts: FetchOptions<'cb>) -> &mut RepoBuilder<'cb> { + self.fetch_opts = Some(fetch_opts); + self + } + + /// Configures a callback used to create the git remote, prior to its being + /// used to perform the clone operation. + pub fn remote_create<F>(&mut self, f: F) -> &mut RepoBuilder<'cb> + where + F: for<'a> FnMut(&'a Repository, &str, &str) -> Result<Remote<'a>, Error> + 'cb, + { + self.remote_create = Some(Box::new(f)); + self + } + + /// Clone a remote repository. + /// + /// This will use the options configured so far to clone the specified URL + /// into the specified local path. + pub fn clone(&mut self, url: &str, into: &Path) -> Result<Repository, Error> { + let mut opts: raw::git_clone_options = unsafe { mem::zeroed() }; + unsafe { + try_call!(raw::git_clone_init_options( + &mut opts, + raw::GIT_CLONE_OPTIONS_VERSION + )); + } + opts.bare = self.bare as c_int; + opts.checkout_branch = self + .branch + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(ptr::null()); + + if let Some(ref local) = self.clone_local { + opts.local = *local as raw::git_clone_local_t; + } else { + opts.local = match (self.local, self.hardlinks) { + (true, false) => raw::GIT_CLONE_LOCAL_NO_LINKS, + (false, _) => raw::GIT_CLONE_NO_LOCAL, + (true, _) => raw::GIT_CLONE_LOCAL_AUTO, + }; + } + + if let Some(ref mut cbs) = self.fetch_opts { + opts.fetch_opts = cbs.raw(); + } + + if let Some(ref mut c) = self.checkout { + unsafe { + c.configure(&mut opts.checkout_opts); + } + } + + if let Some(ref mut callback) = self.remote_create { + opts.remote_cb = Some(remote_create_cb); + opts.remote_cb_payload = callback as *mut _ as *mut _; + } + + let url = CString::new(url)?; + // Normal file path OK (does not need Windows conversion). + let into = into.into_c_string()?; + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_clone(&mut raw, url, into, &opts)); + Ok(Binding::from_raw(raw)) + } + } +} + +extern "C" fn remote_create_cb( + out: *mut *mut raw::git_remote, + repo: *mut raw::git_repository, + name: *const c_char, + url: *const c_char, + payload: *mut c_void, +) -> c_int { + unsafe { + let repo = Repository::from_raw(repo); + let code = panic::wrap(|| { + let name = CStr::from_ptr(name).to_str().unwrap(); + let url = CStr::from_ptr(url).to_str().unwrap(); + let f = payload as *mut Box<RemoteCreate<'_>>; + match (*f)(&repo, name, url) { + Ok(remote) => { + *out = crate::remote::remote_into_raw(remote); + 0 + } + Err(e) => e.raw_code(), + } + }); + mem::forget(repo); + code.unwrap_or(-1) + } +} + +impl<'cb> Default for CheckoutBuilder<'cb> { + fn default() -> Self { + Self::new() + } +} + +impl<'cb> CheckoutBuilder<'cb> { + /// Creates a new builder for checkouts with all of its default + /// configuration. + pub fn new() -> CheckoutBuilder<'cb> { + crate::init(); + CheckoutBuilder { + disable_filters: false, + dir_perm: None, + file_perm: None, + path_ptrs: Vec::new(), + paths: Vec::new(), + target_dir: None, + ancestor_label: None, + our_label: None, + their_label: None, + checkout_opts: raw::GIT_CHECKOUT_SAFE as u32, + progress: None, + notify: None, + notify_flags: CheckoutNotificationType::empty(), + } + } + + /// Indicate that this checkout should perform a dry run by checking for + /// conflicts but not make any actual changes. + pub fn dry_run(&mut self) -> &mut CheckoutBuilder<'cb> { + self.checkout_opts &= !((1 << 4) - 1); + self.checkout_opts |= raw::GIT_CHECKOUT_NONE as u32; + self + } + + /// Take any action necessary to get the working directory to match the + /// target including potentially discarding modified files. + pub fn force(&mut self) -> &mut CheckoutBuilder<'cb> { + self.checkout_opts &= !((1 << 4) - 1); + self.checkout_opts |= raw::GIT_CHECKOUT_FORCE as u32; + self + } + + /// Indicate that the checkout should be performed safely, allowing new + /// files to be created but not overwriting existing files or changes. + /// + /// This is the default. + pub fn safe(&mut self) -> &mut CheckoutBuilder<'cb> { + self.checkout_opts &= !((1 << 4) - 1); + self.checkout_opts |= raw::GIT_CHECKOUT_SAFE as u32; + self + } + + fn flag(&mut self, bit: raw::git_checkout_strategy_t, on: bool) -> &mut CheckoutBuilder<'cb> { + if on { + self.checkout_opts |= bit as u32; + } else { + self.checkout_opts &= !(bit as u32); + } + self + } + + /// In safe mode, create files that don't exist. + /// + /// Defaults to false. + pub fn recreate_missing(&mut self, allow: bool) -> &mut CheckoutBuilder<'cb> { + self.flag(raw::GIT_CHECKOUT_RECREATE_MISSING, allow) + } + + /// In safe mode, apply safe file updates even when there are conflicts + /// instead of canceling the checkout. + /// + /// Defaults to false. + pub fn allow_conflicts(&mut self, allow: bool) -> &mut CheckoutBuilder<'cb> { + self.flag(raw::GIT_CHECKOUT_ALLOW_CONFLICTS, allow) + } + + /// Remove untracked files from the working dir. + /// + /// Defaults to false. + pub fn remove_untracked(&mut self, remove: bool) -> &mut CheckoutBuilder<'cb> { + self.flag(raw::GIT_CHECKOUT_REMOVE_UNTRACKED, remove) + } + + /// Remove ignored files from the working dir. + /// + /// Defaults to false. + pub fn remove_ignored(&mut self, remove: bool) -> &mut CheckoutBuilder<'cb> { + self.flag(raw::GIT_CHECKOUT_REMOVE_IGNORED, remove) + } + + /// Only update the contents of files that already exist. + /// + /// If set, files will not be created or deleted. + /// + /// Defaults to false. + pub fn update_only(&mut self, update: bool) -> &mut CheckoutBuilder<'cb> { + self.flag(raw::GIT_CHECKOUT_UPDATE_ONLY, update) + } + + /// Prevents checkout from writing the updated files' information to the + /// index. + /// + /// Defaults to true. + pub fn update_index(&mut self, update: bool) -> &mut CheckoutBuilder<'cb> { + self.flag(raw::GIT_CHECKOUT_DONT_UPDATE_INDEX, !update) + } + + /// Indicate whether the index and git attributes should be refreshed from + /// disk before any operations. + /// + /// Defaults to true, + pub fn refresh(&mut self, refresh: bool) -> &mut CheckoutBuilder<'cb> { + self.flag(raw::GIT_CHECKOUT_NO_REFRESH, !refresh) + } + + /// Skip files with unmerged index entries. + /// + /// Defaults to false. + pub fn skip_unmerged(&mut self, skip: bool) -> &mut CheckoutBuilder<'cb> { + self.flag(raw::GIT_CHECKOUT_SKIP_UNMERGED, skip) + } + + /// Indicate whether the checkout should proceed on conflicts by using the + /// stage 2 version of the file ("ours"). + /// + /// Defaults to false. + pub fn use_ours(&mut self, ours: bool) -> &mut CheckoutBuilder<'cb> { + self.flag(raw::GIT_CHECKOUT_USE_OURS, ours) + } + + /// Indicate whether the checkout should proceed on conflicts by using the + /// stage 3 version of the file ("theirs"). + /// + /// Defaults to false. + pub fn use_theirs(&mut self, theirs: bool) -> &mut CheckoutBuilder<'cb> { + self.flag(raw::GIT_CHECKOUT_USE_THEIRS, theirs) + } + + /// Indicate whether ignored files should be overwritten during the checkout. + /// + /// Defaults to true. + pub fn overwrite_ignored(&mut self, overwrite: bool) -> &mut CheckoutBuilder<'cb> { + self.flag(raw::GIT_CHECKOUT_DONT_OVERWRITE_IGNORED, !overwrite) + } + + /// Indicate whether a normal merge file should be written for conflicts. + /// + /// Defaults to false. + pub fn conflict_style_merge(&mut self, on: bool) -> &mut CheckoutBuilder<'cb> { + self.flag(raw::GIT_CHECKOUT_CONFLICT_STYLE_MERGE, on) + } + + /// Specify for which notification types to invoke the notification + /// callback. + /// + /// Defaults to none. + pub fn notify_on( + &mut self, + notification_types: CheckoutNotificationType, + ) -> &mut CheckoutBuilder<'cb> { + self.notify_flags = notification_types; + self + } + + /// Indicates whether to include common ancestor data in diff3 format files + /// for conflicts. + /// + /// Defaults to false. + pub fn conflict_style_diff3(&mut self, on: bool) -> &mut CheckoutBuilder<'cb> { + self.flag(raw::GIT_CHECKOUT_CONFLICT_STYLE_DIFF3, on) + } + + /// Indicate whether to apply filters like CRLF conversion. + pub fn disable_filters(&mut self, disable: bool) -> &mut CheckoutBuilder<'cb> { + self.disable_filters = disable; + self + } + + /// Set the mode with which new directories are created. + /// + /// Default is 0755 + pub fn dir_perm(&mut self, perm: i32) -> &mut CheckoutBuilder<'cb> { + self.dir_perm = Some(perm); + self + } + + /// Set the mode with which new files are created. + /// + /// The default is 0644 or 0755 as dictated by the blob. + pub fn file_perm(&mut self, perm: i32) -> &mut CheckoutBuilder<'cb> { + self.file_perm = Some(perm); + self + } + + /// Add a path to be checked out. + /// + /// If no paths are specified, then all files are checked out. Otherwise + /// only these specified paths are checked out. + pub fn path<T: IntoCString>(&mut self, path: T) -> &mut CheckoutBuilder<'cb> { + let path = util::cstring_to_repo_path(path).unwrap(); + self.path_ptrs.push(path.as_ptr()); + self.paths.push(path); + self + } + + /// Set the directory to check out to + pub fn target_dir(&mut self, dst: &Path) -> &mut CheckoutBuilder<'cb> { + // Normal file path OK (does not need Windows conversion). + self.target_dir = Some(dst.into_c_string().unwrap()); + self + } + + /// The name of the common ancestor side of conflicts + pub fn ancestor_label(&mut self, label: &str) -> &mut CheckoutBuilder<'cb> { + self.ancestor_label = Some(CString::new(label).unwrap()); + self + } + + /// The name of the common our side of conflicts + pub fn our_label(&mut self, label: &str) -> &mut CheckoutBuilder<'cb> { + self.our_label = Some(CString::new(label).unwrap()); + self + } + + /// The name of the common their side of conflicts + pub fn their_label(&mut self, label: &str) -> &mut CheckoutBuilder<'cb> { + self.their_label = Some(CString::new(label).unwrap()); + self + } + + /// Set a callback to receive notifications of checkout progress. + pub fn progress<F>(&mut self, cb: F) -> &mut CheckoutBuilder<'cb> + where + F: FnMut(Option<&Path>, usize, usize) + 'cb, + { + self.progress = Some(Box::new(cb) as Box<Progress<'cb>>); + self + } + + /// Set a callback to receive checkout notifications. + /// + /// Callbacks are invoked prior to modifying any files on disk. + /// Returning `false` from the callback will cancel the checkout. + pub fn notify<F>(&mut self, cb: F) -> &mut CheckoutBuilder<'cb> + where + F: FnMut( + CheckoutNotificationType, + Option<&Path>, + Option<DiffFile<'_>>, + Option<DiffFile<'_>>, + Option<DiffFile<'_>>, + ) -> bool + + 'cb, + { + self.notify = Some(Box::new(cb) as Box<Notify<'cb>>); + self + } + + /// Configure a raw checkout options based on this configuration. + /// + /// This method is unsafe as there is no guarantee that this structure will + /// outlive the provided checkout options. + pub unsafe fn configure(&mut self, opts: &mut raw::git_checkout_options) { + opts.version = raw::GIT_CHECKOUT_OPTIONS_VERSION; + opts.disable_filters = self.disable_filters as c_int; + opts.dir_mode = self.dir_perm.unwrap_or(0) as c_uint; + opts.file_mode = self.file_perm.unwrap_or(0) as c_uint; + + if !self.path_ptrs.is_empty() { + opts.paths.strings = self.path_ptrs.as_ptr() as *mut _; + opts.paths.count = self.path_ptrs.len() as size_t; + } + + if let Some(ref c) = self.target_dir { + opts.target_directory = c.as_ptr(); + } + if let Some(ref c) = self.ancestor_label { + opts.ancestor_label = c.as_ptr(); + } + if let Some(ref c) = self.our_label { + opts.our_label = c.as_ptr(); + } + if let Some(ref c) = self.their_label { + opts.their_label = c.as_ptr(); + } + if self.progress.is_some() { + opts.progress_cb = Some(progress_cb); + opts.progress_payload = self as *mut _ as *mut _; + } + if self.notify.is_some() { + opts.notify_cb = Some(notify_cb); + opts.notify_payload = self as *mut _ as *mut _; + opts.notify_flags = self.notify_flags.bits() as c_uint; + } + opts.checkout_strategy = self.checkout_opts as c_uint; + } +} + +extern "C" fn progress_cb( + path: *const c_char, + completed: size_t, + total: size_t, + data: *mut c_void, +) { + panic::wrap(|| unsafe { + let payload = &mut *(data as *mut CheckoutBuilder<'_>); + let callback = match payload.progress { + Some(ref mut c) => c, + None => return, + }; + let path = if path.is_null() { + None + } else { + Some(util::bytes2path(CStr::from_ptr(path).to_bytes())) + }; + callback(path, completed as usize, total as usize) + }); +} + +extern "C" fn notify_cb( + why: raw::git_checkout_notify_t, + path: *const c_char, + baseline: *const raw::git_diff_file, + target: *const raw::git_diff_file, + workdir: *const raw::git_diff_file, + data: *mut c_void, +) -> c_int { + // pack callback etc + panic::wrap(|| unsafe { + let payload = &mut *(data as *mut CheckoutBuilder<'_>); + let callback = match payload.notify { + Some(ref mut c) => c, + None => return 0, + }; + let path = if path.is_null() { + None + } else { + Some(util::bytes2path(CStr::from_ptr(path).to_bytes())) + }; + + let baseline = if baseline.is_null() { + None + } else { + Some(DiffFile::from_raw(baseline)) + }; + + let target = if target.is_null() { + None + } else { + Some(DiffFile::from_raw(target)) + }; + + let workdir = if workdir.is_null() { + None + } else { + Some(DiffFile::from_raw(workdir)) + }; + + let why = CheckoutNotificationType::from_bits_truncate(why as u32); + let keep_going = callback(why, path, baseline, target, workdir); + if keep_going { + 0 + } else { + 1 + } + }) + .unwrap_or(2) +} + +unsafe impl Send for TreeUpdateBuilder {} + +impl Default for TreeUpdateBuilder { + fn default() -> Self { + Self::new() + } +} + +impl TreeUpdateBuilder { + /// Create a new empty series of updates. + pub fn new() -> Self { + Self { + updates: Vec::new(), + paths: Vec::new(), + } + } + + /// Add an update removing the specified `path` from a tree. + pub fn remove<T: IntoCString>(&mut self, path: T) -> &mut Self { + let path = util::cstring_to_repo_path(path).unwrap(); + let path_ptr = path.as_ptr(); + self.paths.push(path); + self.updates.push(raw::git_tree_update { + action: raw::GIT_TREE_UPDATE_REMOVE, + id: raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }, + filemode: raw::GIT_FILEMODE_UNREADABLE, + path: path_ptr, + }); + self + } + + /// Add an update setting the specified `path` to a specific Oid, whether it currently exists + /// or not. + /// + /// Note that libgit2 does not support an upsert of a previously removed path, or an upsert + /// that changes the type of an object (such as from tree to blob or vice versa). + pub fn upsert<T: IntoCString>(&mut self, path: T, id: Oid, filemode: FileMode) -> &mut Self { + let path = util::cstring_to_repo_path(path).unwrap(); + let path_ptr = path.as_ptr(); + self.paths.push(path); + self.updates.push(raw::git_tree_update { + action: raw::GIT_TREE_UPDATE_UPSERT, + id: unsafe { *id.raw() }, + filemode: u32::from(filemode) as raw::git_filemode_t, + path: path_ptr, + }); + self + } + + /// Create a new tree from the specified baseline and this series of updates. + /// + /// The baseline tree must exist in the specified repository. + pub fn create_updated(&mut self, repo: &Repository, baseline: &Tree<'_>) -> Result<Oid, Error> { + let mut ret = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_tree_create_updated( + &mut ret, + repo.raw(), + baseline.raw(), + self.updates.len(), + self.updates.as_ptr() + )); + Ok(Binding::from_raw(&ret as *const _)) + } + } +} + +#[cfg(test)] +mod tests { + use super::{CheckoutBuilder, RepoBuilder, TreeUpdateBuilder}; + use crate::{CheckoutNotificationType, FileMode, Repository}; + use std::fs; + use std::path::Path; + use tempfile::TempDir; + + #[test] + fn smoke() { + let r = RepoBuilder::new().clone("/path/to/nowhere", Path::new("foo")); + assert!(r.is_err()); + } + + #[test] + fn smoke2() { + let td = TempDir::new().unwrap(); + Repository::init_bare(&td.path().join("bare")).unwrap(); + let url = if cfg!(unix) { + format!("file://{}/bare", td.path().display()) + } else { + format!( + "file:///{}/bare", + td.path().display().to_string().replace("\\", "/") + ) + }; + + let dst = td.path().join("foo"); + RepoBuilder::new().clone(&url, &dst).unwrap(); + fs::remove_dir_all(&dst).unwrap(); + assert!(RepoBuilder::new().branch("foo").clone(&url, &dst).is_err()); + } + + #[test] + fn smoke_tree_create_updated() { + let (_tempdir, repo) = crate::test::repo_init(); + let (_, tree_id) = crate::test::commit(&repo); + let tree = t!(repo.find_tree(tree_id)); + assert!(tree.get_name("bar").is_none()); + let foo_id = tree.get_name("foo").unwrap().id(); + let tree2_id = t!(TreeUpdateBuilder::new() + .remove("foo") + .upsert("bar/baz", foo_id, FileMode::Blob) + .create_updated(&repo, &tree)); + let tree2 = t!(repo.find_tree(tree2_id)); + assert!(tree2.get_name("foo").is_none()); + let baz_id = tree2.get_path(Path::new("bar/baz")).unwrap().id(); + assert_eq!(foo_id, baz_id); + } + + /// Issue regression test #365 + #[test] + fn notify_callback() { + let td = TempDir::new().unwrap(); + let cd = TempDir::new().unwrap(); + + { + let mut opts = crate::RepositoryInitOptions::new(); + opts.initial_head("main"); + let repo = Repository::init_opts(&td.path(), &opts).unwrap(); + + let mut config = repo.config().unwrap(); + config.set_str("user.name", "name").unwrap(); + config.set_str("user.email", "email").unwrap(); + + let mut index = repo.index().unwrap(); + let p = Path::new(td.path()).join("file"); + println!("using path {:?}", p); + fs::File::create(&p).unwrap(); + index.add_path(&Path::new("file")).unwrap(); + let id = index.write_tree().unwrap(); + + let tree = repo.find_tree(id).unwrap(); + let sig = repo.signature().unwrap(); + repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[]) + .unwrap(); + } + + let repo = Repository::open_bare(&td.path().join(".git")).unwrap(); + let tree = repo + .revparse_single(&"main") + .unwrap() + .peel_to_tree() + .unwrap(); + let mut index = repo.index().unwrap(); + index.read_tree(&tree).unwrap(); + + let mut checkout_opts = CheckoutBuilder::new(); + checkout_opts.target_dir(&cd.path()); + checkout_opts.notify_on(CheckoutNotificationType::all()); + checkout_opts.notify(|_notif, _path, baseline, target, workdir| { + assert!(baseline.is_none()); + assert_eq!(target.unwrap().path(), Some(Path::new("file"))); + assert!(workdir.is_none()); + true + }); + repo.checkout_index(Some(&mut index), Some(&mut checkout_opts)) + .unwrap(); + } +} diff --git a/extra/git2/src/call.rs b/extra/git2/src/call.rs new file mode 100644 index 000000000..d9fd23468 --- /dev/null +++ b/extra/git2/src/call.rs @@ -0,0 +1,246 @@ +#![macro_use] +use libc; + +use crate::Error; + +macro_rules! call { + (raw::$p:ident ($($e:expr),*)) => ( + raw::$p($(crate::call::convert(&$e)),*) + ) +} + +macro_rules! try_call { + (raw::$p:ident ($($e:expr),*)) => ({ + match crate::call::c_try(raw::$p($(crate::call::convert(&$e)),*)) { + Ok(o) => o, + Err(e) => { crate::panic::check(); return Err(e) } + } + }) +} + +macro_rules! try_call_iter { + ($($f:tt)*) => { + match call!($($f)*) { + 0 => {} + raw::GIT_ITEROVER => return None, + e => return Some(Err(crate::call::last_error(e))) + } + } +} + +#[doc(hidden)] +pub trait Convert<T> { + fn convert(&self) -> T; +} + +pub fn convert<T, U: Convert<T>>(u: &U) -> T { + u.convert() +} + +pub fn c_try(ret: libc::c_int) -> Result<libc::c_int, Error> { + match ret { + n if n < 0 => Err(last_error(n)), + n => Ok(n), + } +} + +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() +} + +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::{ + AutotagOption, DiffFormat, FetchPrune, FileFavor, SubmoduleIgnore, SubmoduleUpdate, + }; + + impl<T: Copy> Convert<T> for T { + fn convert(&self) -> T { + *self + } + } + + impl Convert<libc::c_int> for bool { + fn convert(&self) -> libc::c_int { + *self as libc::c_int + } + } + impl<'a, T> Convert<*const T> for &'a T { + fn convert(&self) -> *const T { + *self as *const T + } + } + impl<'a, T> Convert<*mut T> for &'a mut T { + fn convert(&self) -> *mut T { + &**self as *const T as *mut T + } + } + impl<T> Convert<*const T> for *mut T { + fn convert(&self) -> *const T { + *self as *const T + } + } + + impl Convert<*const libc::c_char> for CString { + fn convert(&self) -> *const libc::c_char { + self.as_ptr() + } + } + + impl<T, U: Convert<*const T>> Convert<*const T> for Option<U> { + fn convert(&self) -> *const T { + self.as_ref().map(|s| s.convert()).unwrap_or(ptr::null()) + } + } + + impl<T, U: Convert<*mut T>> Convert<*mut T> for Option<U> { + fn convert(&self) -> *mut T { + self.as_ref() + .map(|s| s.convert()) + .unwrap_or(ptr::null_mut()) + } + } + + impl Convert<raw::git_reset_t> for ResetType { + fn convert(&self) -> raw::git_reset_t { + match *self { + ResetType::Soft => raw::GIT_RESET_SOFT, + ResetType::Hard => raw::GIT_RESET_HARD, + ResetType::Mixed => raw::GIT_RESET_MIXED, + } + } + } + + impl Convert<raw::git_direction> for Direction { + fn convert(&self) -> raw::git_direction { + match *self { + Direction::Push => raw::GIT_DIRECTION_PUSH, + Direction::Fetch => raw::GIT_DIRECTION_FETCH, + } + } + } + + impl Convert<raw::git_object_t> for ObjectType { + fn convert(&self) -> raw::git_object_t { + match *self { + ObjectType::Any => raw::GIT_OBJECT_ANY, + ObjectType::Commit => raw::GIT_OBJECT_COMMIT, + ObjectType::Tree => raw::GIT_OBJECT_TREE, + ObjectType::Blob => raw::GIT_OBJECT_BLOB, + ObjectType::Tag => raw::GIT_OBJECT_TAG, + } + } + } + + impl Convert<raw::git_object_t> for Option<ObjectType> { + fn convert(&self) -> raw::git_object_t { + self.unwrap_or(ObjectType::Any).convert() + } + } + + impl Convert<raw::git_branch_t> for BranchType { + fn convert(&self) -> raw::git_branch_t { + match *self { + BranchType::Remote => raw::GIT_BRANCH_REMOTE, + BranchType::Local => raw::GIT_BRANCH_LOCAL, + } + } + } + + impl Convert<raw::git_branch_t> for Option<BranchType> { + fn convert(&self) -> raw::git_branch_t { + self.map(|s| s.convert()).unwrap_or(raw::GIT_BRANCH_ALL) + } + } + + impl Convert<raw::git_config_level_t> for ConfigLevel { + fn convert(&self) -> raw::git_config_level_t { + match *self { + ConfigLevel::ProgramData => raw::GIT_CONFIG_LEVEL_PROGRAMDATA, + ConfigLevel::System => raw::GIT_CONFIG_LEVEL_SYSTEM, + ConfigLevel::XDG => raw::GIT_CONFIG_LEVEL_XDG, + ConfigLevel::Global => raw::GIT_CONFIG_LEVEL_GLOBAL, + ConfigLevel::Local => raw::GIT_CONFIG_LEVEL_LOCAL, + ConfigLevel::App => raw::GIT_CONFIG_LEVEL_APP, + ConfigLevel::Highest => raw::GIT_CONFIG_HIGHEST_LEVEL, + } + } + } + + impl Convert<raw::git_diff_format_t> for DiffFormat { + fn convert(&self) -> raw::git_diff_format_t { + match *self { + DiffFormat::Patch => raw::GIT_DIFF_FORMAT_PATCH, + DiffFormat::PatchHeader => raw::GIT_DIFF_FORMAT_PATCH_HEADER, + DiffFormat::Raw => raw::GIT_DIFF_FORMAT_RAW, + DiffFormat::NameOnly => raw::GIT_DIFF_FORMAT_NAME_ONLY, + DiffFormat::NameStatus => raw::GIT_DIFF_FORMAT_NAME_STATUS, + DiffFormat::PatchId => raw::GIT_DIFF_FORMAT_PATCH_ID, + } + } + } + + impl Convert<raw::git_merge_file_favor_t> for FileFavor { + fn convert(&self) -> raw::git_merge_file_favor_t { + match *self { + FileFavor::Normal => raw::GIT_MERGE_FILE_FAVOR_NORMAL, + FileFavor::Ours => raw::GIT_MERGE_FILE_FAVOR_OURS, + FileFavor::Theirs => raw::GIT_MERGE_FILE_FAVOR_THEIRS, + FileFavor::Union => raw::GIT_MERGE_FILE_FAVOR_UNION, + } + } + } + + impl Convert<raw::git_submodule_ignore_t> for SubmoduleIgnore { + fn convert(&self) -> raw::git_submodule_ignore_t { + match *self { + SubmoduleIgnore::Unspecified => raw::GIT_SUBMODULE_IGNORE_UNSPECIFIED, + SubmoduleIgnore::None => raw::GIT_SUBMODULE_IGNORE_NONE, + SubmoduleIgnore::Untracked => raw::GIT_SUBMODULE_IGNORE_UNTRACKED, + SubmoduleIgnore::Dirty => raw::GIT_SUBMODULE_IGNORE_DIRTY, + SubmoduleIgnore::All => raw::GIT_SUBMODULE_IGNORE_ALL, + } + } + } + + impl Convert<raw::git_submodule_update_t> for SubmoduleUpdate { + fn convert(&self) -> raw::git_submodule_update_t { + match *self { + SubmoduleUpdate::Checkout => raw::GIT_SUBMODULE_UPDATE_CHECKOUT, + SubmoduleUpdate::Rebase => raw::GIT_SUBMODULE_UPDATE_REBASE, + SubmoduleUpdate::Merge => raw::GIT_SUBMODULE_UPDATE_MERGE, + SubmoduleUpdate::None => raw::GIT_SUBMODULE_UPDATE_NONE, + SubmoduleUpdate::Default => raw::GIT_SUBMODULE_UPDATE_DEFAULT, + } + } + } + + impl Convert<raw::git_remote_autotag_option_t> for AutotagOption { + fn convert(&self) -> raw::git_remote_autotag_option_t { + match *self { + AutotagOption::Unspecified => raw::GIT_REMOTE_DOWNLOAD_TAGS_UNSPECIFIED, + AutotagOption::None => raw::GIT_REMOTE_DOWNLOAD_TAGS_NONE, + AutotagOption::Auto => raw::GIT_REMOTE_DOWNLOAD_TAGS_AUTO, + AutotagOption::All => raw::GIT_REMOTE_DOWNLOAD_TAGS_ALL, + } + } + } + + impl Convert<raw::git_fetch_prune_t> for FetchPrune { + fn convert(&self) -> raw::git_fetch_prune_t { + match *self { + FetchPrune::Unspecified => raw::GIT_FETCH_PRUNE_UNSPECIFIED, + FetchPrune::On => raw::GIT_FETCH_PRUNE, + FetchPrune::Off => raw::GIT_FETCH_NO_PRUNE, + } + } + } +} diff --git a/extra/git2/src/cert.rs b/extra/git2/src/cert.rs new file mode 100644 index 000000000..b232cc3ce --- /dev/null +++ b/extra/git2/src/cert.rs @@ -0,0 +1,191 @@ +//! Certificate types which are passed to `CertificateCheck` in +//! `RemoteCallbacks`. + +use std::marker; +use std::mem; +use std::slice; + +use crate::raw; +use crate::util::Binding; + +/// A certificate for a remote connection, viewable as one of `CertHostkey` or +/// `CertX509` currently. +pub struct Cert<'a> { + raw: *mut raw::git_cert, + _marker: marker::PhantomData<&'a raw::git_cert>, +} + +/// Hostkey information taken from libssh2 +pub struct CertHostkey<'a> { + raw: *mut raw::git_cert_hostkey, + _marker: marker::PhantomData<&'a raw::git_cert>, +} + +/// X.509 certificate information +pub struct CertX509<'a> { + raw: *mut raw::git_cert_x509, + _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. + /// + /// Returns `None` if this is not actually an SSH hostkey. + pub fn as_hostkey(&self) -> Option<&CertHostkey<'a>> { + self.cast(raw::GIT_CERT_HOSTKEY_LIBSSH2) + } + + /// Attempt to view this certificate as an X.509 certificate. + /// + /// Returns `None` if this is not actually an X.509 certificate. + pub fn as_x509(&self) -> Option<&CertX509<'a>> { + self.cast(raw::GIT_CERT_X509) + } + + fn cast<T>(&self, kind: raw::git_cert_t) -> Option<&T> { + assert_eq!(mem::size_of::<Cert<'a>>(), mem::size_of::<T>()); + unsafe { + if kind == (*self.raw).cert_type { + Some(&*(self as *const Cert<'a> as *const T)) + } else { + None + } + } + } +} + +impl<'a> CertHostkey<'a> { + /// Returns the md5 hash of the hostkey, if available. + pub fn hash_md5(&self) -> Option<&[u8; 16]> { + unsafe { + if (*self.raw).kind as u32 & raw::GIT_CERT_SSH_MD5 as u32 == 0 { + None + } else { + Some(&(*self.raw).hash_md5) + } + } + } + + /// Returns the SHA-1 hash of the hostkey, if available. + pub fn hash_sha1(&self) -> Option<&[u8; 20]> { + unsafe { + if (*self.raw).kind as u32 & raw::GIT_CERT_SSH_SHA1 as u32 == 0 { + None + } else { + Some(&(*self.raw).hash_sha1) + } + } + } + + /// Returns the SHA-256 hash of the hostkey, if available. + pub fn hash_sha256(&self) -> Option<&[u8; 32]> { + unsafe { + if (*self.raw).kind as u32 & raw::GIT_CERT_SSH_SHA256 as u32 == 0 { + None + } else { + Some(&(*self.raw).hash_sha256) + } + } + } + + /// Returns the raw host key. + pub fn hostkey(&self) -> Option<&[u8]> { + unsafe { + if (*self.raw).kind & raw::GIT_CERT_SSH_RAW == 0 { + return None; + } + Some(slice::from_raw_parts( + (*self.raw).hostkey as *const u8, + (*self.raw).hostkey_len as usize, + )) + } + } + + /// Returns the type of the host key. + pub fn hostkey_type(&self) -> Option<SshHostKeyType> { + unsafe { + if (*self.raw).kind & raw::GIT_CERT_SSH_RAW == 0 { + return None; + } + let t = match (*self.raw).raw_type { + raw::GIT_CERT_SSH_RAW_TYPE_UNKNOWN => SshHostKeyType::Unknown, + raw::GIT_CERT_SSH_RAW_TYPE_RSA => SshHostKeyType::Rsa, + raw::GIT_CERT_SSH_RAW_TYPE_DSS => SshHostKeyType::Dss, + raw::GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_256 => SshHostKeyType::Ecdsa256, + raw::GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_384 => SshHostKeyType::Ecdsa384, + raw::GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_521 => SshHostKeyType::Ecdsa521, + raw::GIT_CERT_SSH_RAW_TYPE_KEY_ED25519 => SshHostKeyType::Ed255219, + t => panic!("unexpected host key type {:?}", t), + }; + Some(t) + } + } +} + +impl<'a> CertX509<'a> { + /// Return the X.509 certificate data as a byte slice + pub fn data(&self) -> &[u8] { + unsafe { slice::from_raw_parts((*self.raw).data as *const u8, (*self.raw).len as usize) } + } +} + +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, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_cert { + self.raw + } +} diff --git a/extra/git2/src/cherrypick.rs b/extra/git2/src/cherrypick.rs new file mode 100644 index 000000000..659b73089 --- /dev/null +++ b/extra/git2/src/cherrypick.rs @@ -0,0 +1,72 @@ +use std::mem; + +use crate::build::CheckoutBuilder; +use crate::merge::MergeOptions; +use crate::raw; +use std::ptr; + +/// Options to specify when cherry picking +pub struct CherrypickOptions<'cb> { + mainline: u32, + checkout_builder: Option<CheckoutBuilder<'cb>>, + merge_opts: Option<MergeOptions>, +} + +impl<'cb> CherrypickOptions<'cb> { + /// Creates a default set of cherrypick options + pub fn new() -> CherrypickOptions<'cb> { + CherrypickOptions { + mainline: 0, + checkout_builder: None, + merge_opts: None, + } + } + + /// Set the mainline value + /// + /// For merge commits, the "mainline" is treated as the parent. + pub fn mainline(&mut self, mainline: u32) -> &mut Self { + self.mainline = mainline; + self + } + + /// Set the checkout builder + pub fn checkout_builder(&mut self, cb: CheckoutBuilder<'cb>) -> &mut Self { + self.checkout_builder = Some(cb); + self + } + + /// Set the merge options + pub fn merge_opts(&mut self, merge_opts: MergeOptions) -> &mut Self { + self.merge_opts = Some(merge_opts); + self + } + + /// Obtain the raw struct + pub fn raw(&mut self) -> raw::git_cherrypick_options { + unsafe { + let mut checkout_opts: raw::git_checkout_options = mem::zeroed(); + raw::git_checkout_init_options(&mut checkout_opts, raw::GIT_CHECKOUT_OPTIONS_VERSION); + if let Some(ref mut cb) = self.checkout_builder { + cb.configure(&mut checkout_opts); + } + + let mut merge_opts: raw::git_merge_options = mem::zeroed(); + raw::git_merge_init_options(&mut merge_opts, raw::GIT_MERGE_OPTIONS_VERSION); + if let Some(ref opts) = self.merge_opts { + ptr::copy(opts.raw(), &mut merge_opts, 1); + } + + let mut cherrypick_opts: raw::git_cherrypick_options = mem::zeroed(); + raw::git_cherrypick_init_options( + &mut cherrypick_opts, + raw::GIT_CHERRYPICK_OPTIONS_VERSION, + ); + cherrypick_opts.mainline = self.mainline; + cherrypick_opts.checkout_opts = checkout_opts; + cherrypick_opts.merge_opts = merge_opts; + + cherrypick_opts + } + } +} diff --git a/extra/git2/src/commit.rs b/extra/git2/src/commit.rs new file mode 100644 index 000000000..4887e927e --- /dev/null +++ b/extra/git2/src/commit.rs @@ -0,0 +1,473 @@ +use libc; +use std::iter::FusedIterator; +use std::marker; +use std::mem; +use std::ops::Range; +use std::ptr; +use std::str; + +use crate::util::Binding; +use crate::{raw, signature, Buf, Error, IntoCString, Mailmap, Object, Oid, Signature, Time, Tree}; + +/// A structure to represent a git [commit][1] +/// +/// [1]: http://git-scm.com/book/en/Git-Internals-Git-Objects +pub struct Commit<'repo> { + raw: *mut raw::git_commit, + _marker: marker::PhantomData<Object<'repo>>, +} + +/// An iterator over the parent commits of a commit. +/// +/// Aborts iteration when a commit cannot be found +pub struct Parents<'commit, 'repo> { + range: Range<usize>, + commit: &'commit Commit<'repo>, +} + +/// An iterator over the parent commits' ids of a commit. +/// +/// Aborts iteration when a commit cannot be found +pub struct ParentIds<'commit> { + range: Range<usize>, + commit: &'commit Commit<'commit>, +} + +impl<'repo> Commit<'repo> { + /// Get the id (SHA1) of a repository commit + pub fn id(&self) -> Oid { + unsafe { Binding::from_raw(raw::git_commit_id(&*self.raw)) } + } + + /// Get the id of the tree pointed to by this commit. + /// + /// No attempts are made to fetch an object from the ODB. + pub fn tree_id(&self) -> Oid { + unsafe { Binding::from_raw(raw::git_commit_tree_id(&*self.raw)) } + } + + /// Get the tree pointed to by a commit. + pub fn tree(&self) -> Result<Tree<'repo>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_commit_tree(&mut ret, &*self.raw)); + Ok(Binding::from_raw(ret)) + } + } + + /// Get access to the underlying raw pointer. + pub fn raw(&self) -> *mut raw::git_commit { + self.raw + } + + /// Get the full message of a commit. + /// + /// The returned message will be slightly prettified by removing any + /// potential leading newlines. + /// + /// `None` will be returned if the message is not valid utf-8 + pub fn message(&self) -> Option<&str> { + str::from_utf8(self.message_bytes()).ok() + } + + /// Get the full message of a commit as a byte slice. + /// + /// The returned message will be slightly prettified by removing any + /// potential leading newlines. + pub fn message_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, raw::git_commit_message(&*self.raw)).unwrap() } + } + + /// Get the encoding for the message of a commit, as a string representing a + /// standard encoding name. + /// + /// `None` will be returned if the encoding is not known + pub fn message_encoding(&self) -> Option<&str> { + let bytes = unsafe { crate::opt_bytes(self, raw::git_commit_message_encoding(&*self.raw)) }; + bytes.and_then(|b| str::from_utf8(b).ok()) + } + + /// Get the full raw message of a commit. + /// + /// `None` will be returned if the message is not valid utf-8 + pub fn message_raw(&self) -> Option<&str> { + str::from_utf8(self.message_raw_bytes()).ok() + } + + /// Get the full raw message of a commit. + pub fn message_raw_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, raw::git_commit_message_raw(&*self.raw)).unwrap() } + } + + /// Get the full raw text of the commit header. + /// + /// `None` will be returned if the message is not valid utf-8 + pub fn raw_header(&self) -> Option<&str> { + str::from_utf8(self.raw_header_bytes()).ok() + } + + /// Get an arbitrary header field. + pub fn header_field_bytes<T: IntoCString>(&self, field: T) -> Result<Buf, Error> { + let buf = Buf::new(); + let raw_field = field.into_c_string()?; + unsafe { + try_call!(raw::git_commit_header_field( + buf.raw(), + &*self.raw, + raw_field + )); + } + Ok(buf) + } + + /// Get the full raw text of the commit header. + pub fn raw_header_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, raw::git_commit_raw_header(&*self.raw)).unwrap() } + } + + /// Get the short "summary" of the git commit message. + /// + /// The returned message is the summary of the commit, comprising the first + /// paragraph of the message with whitespace trimmed and squashed. + /// + /// `None` may be returned if an error occurs or if the summary is not valid + /// utf-8. + pub fn summary(&self) -> Option<&str> { + self.summary_bytes().and_then(|s| str::from_utf8(s).ok()) + } + + /// Get the short "summary" of the git commit message. + /// + /// The returned message is the summary of the commit, comprising the first + /// paragraph of the message with whitespace trimmed and squashed. + /// + /// `None` may be returned if an error occurs + pub fn summary_bytes(&self) -> Option<&[u8]> { + unsafe { crate::opt_bytes(self, raw::git_commit_summary(self.raw)) } + } + + /// Get the long "body" of the git commit message. + /// + /// The returned message is the body of the commit, comprising everything + /// but the first paragraph of the message. Leading and trailing whitespaces + /// are trimmed. + /// + /// `None` may be returned if an error occurs or if the summary is not valid + /// utf-8. + pub fn body(&self) -> Option<&str> { + self.body_bytes().and_then(|s| str::from_utf8(s).ok()) + } + + /// Get the long "body" of the git commit message. + /// + /// The returned message is the body of the commit, comprising everything + /// but the first paragraph of the message. Leading and trailing whitespaces + /// are trimmed. + /// + /// `None` may be returned if an error occurs. + pub fn body_bytes(&self) -> Option<&[u8]> { + unsafe { crate::opt_bytes(self, raw::git_commit_body(self.raw)) } + } + + /// Get the commit time (i.e. committer time) of a commit. + /// + /// The first element of the tuple is the time, in seconds, since the epoch. + /// The second element is the offset, in minutes, of the time zone of the + /// committer's preferred time zone. + pub fn time(&self) -> Time { + unsafe { + Time::new( + raw::git_commit_time(&*self.raw) as i64, + raw::git_commit_time_offset(&*self.raw) as i32, + ) + } + } + + /// Creates a new iterator over the parents of this commit. + pub fn parents<'a>(&'a self) -> Parents<'a, 'repo> { + Parents { + range: 0..self.parent_count(), + commit: self, + } + } + + /// Creates a new iterator over the parents of this commit. + pub fn parent_ids(&self) -> ParentIds<'_> { + ParentIds { + range: 0..self.parent_count(), + commit: self, + } + } + + /// Get the author of this commit. + pub fn author(&self) -> Signature<'_> { + unsafe { + let ptr = raw::git_commit_author(&*self.raw); + signature::from_raw_const(self, ptr) + } + } + + /// Get the author of this commit, using the mailmap to map names and email + /// addresses to canonical real names and email addresses. + pub fn author_with_mailmap(&self, mailmap: &Mailmap) -> Result<Signature<'static>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_commit_author_with_mailmap( + &mut ret, + &*self.raw, + &*mailmap.raw() + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Get the committer of this commit. + pub fn committer(&self) -> Signature<'_> { + unsafe { + let ptr = raw::git_commit_committer(&*self.raw); + signature::from_raw_const(self, ptr) + } + } + + /// Get the committer of this commit, using the mailmap to map names and email + /// addresses to canonical real names and email addresses. + pub fn committer_with_mailmap(&self, mailmap: &Mailmap) -> Result<Signature<'static>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_commit_committer_with_mailmap( + &mut ret, + &*self.raw, + &*mailmap.raw() + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Amend this existing commit with all non-`None` values + /// + /// This creates a new commit that is exactly the same as the old commit, + /// except that any non-`None` values will be updated. The new commit has + /// the same parents as the old commit. + /// + /// For information about `update_ref`, see [`Repository::commit`]. + /// + /// [`Repository::commit`]: struct.Repository.html#method.commit + pub fn amend( + &self, + update_ref: Option<&str>, + author: Option<&Signature<'_>>, + committer: Option<&Signature<'_>>, + message_encoding: Option<&str>, + message: Option<&str>, + tree: Option<&Tree<'repo>>, + ) -> Result<Oid, Error> { + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + let update_ref = crate::opt_cstr(update_ref)?; + let encoding = crate::opt_cstr(message_encoding)?; + let message = crate::opt_cstr(message)?; + unsafe { + try_call!(raw::git_commit_amend( + &mut raw, + self.raw(), + update_ref, + author.map(|s| s.raw()), + committer.map(|s| s.raw()), + encoding, + message, + tree.map(|t| t.raw()) + )); + Ok(Binding::from_raw(&raw as *const _)) + } + } + + /// Get the number of parents of this commit. + /// + /// Use the `parents` iterator to return an iterator over all parents. + pub fn parent_count(&self) -> usize { + unsafe { raw::git_commit_parentcount(&*self.raw) as usize } + } + + /// Get the specified parent of the commit. + /// + /// Use the `parents` iterator to return an iterator over all parents. + pub fn parent(&self, i: usize) -> Result<Commit<'repo>, Error> { + unsafe { + let mut raw = ptr::null_mut(); + try_call!(raw::git_commit_parent( + &mut raw, + &*self.raw, + i as libc::c_uint + )); + Ok(Binding::from_raw(raw)) + } + } + + /// Get the specified parent id of the commit. + /// + /// This is different from `parent`, which will attempt to load the + /// parent commit from the ODB. + /// + /// Use the `parent_ids` iterator to return an iterator over all parents. + pub fn parent_id(&self, i: usize) -> Result<Oid, Error> { + unsafe { + let id = raw::git_commit_parent_id(self.raw, i as libc::c_uint); + if id.is_null() { + Err(Error::from_str("parent index out of bounds")) + } else { + Ok(Binding::from_raw(id)) + } + } + } + + /// Casts this Commit to be usable as an `Object` + pub fn as_object(&self) -> &Object<'repo> { + unsafe { &*(self as *const _ as *const Object<'repo>) } + } + + /// Consumes Commit to be returned as an `Object` + pub fn into_object(self) -> Object<'repo> { + assert_eq!(mem::size_of_val(&self), mem::size_of::<Object<'_>>()); + unsafe { mem::transmute(self) } + } +} + +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, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_commit { + self.raw + } +} + +impl<'repo> std::fmt::Debug for Commit<'repo> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut ds = f.debug_struct("Commit"); + ds.field("id", &self.id()); + if let Some(summary) = self.summary() { + ds.field("summary", &summary); + } + ds.finish() + } +} + +/// Aborts iteration when a commit cannot be found +impl<'repo, 'commit> Iterator for Parents<'commit, 'repo> { + type Item = Commit<'repo>; + fn next(&mut self) -> Option<Commit<'repo>> { + self.range.next().and_then(|i| self.commit.parent(i).ok()) + } + fn size_hint(&self) -> (usize, Option<usize>) { + self.range.size_hint() + } +} + +/// Aborts iteration when a commit cannot be found +impl<'repo, 'commit> DoubleEndedIterator for Parents<'commit, 'repo> { + fn next_back(&mut self) -> Option<Commit<'repo>> { + self.range + .next_back() + .and_then(|i| self.commit.parent(i).ok()) + } +} + +impl<'repo, 'commit> FusedIterator for Parents<'commit, 'repo> {} + +impl<'repo, 'commit> ExactSizeIterator for Parents<'commit, 'repo> {} + +/// Aborts iteration when a commit cannot be found +impl<'commit> Iterator for ParentIds<'commit> { + type Item = Oid; + fn next(&mut self) -> Option<Oid> { + self.range + .next() + .and_then(|i| self.commit.parent_id(i).ok()) + } + fn size_hint(&self) -> (usize, Option<usize>) { + self.range.size_hint() + } +} + +/// Aborts iteration when a commit cannot be found +impl<'commit> DoubleEndedIterator for ParentIds<'commit> { + fn next_back(&mut self) -> Option<Oid> { + self.range + .next_back() + .and_then(|i| self.commit.parent_id(i).ok()) + } +} + +impl<'commit> FusedIterator for ParentIds<'commit> {} + +impl<'commit> ExactSizeIterator for ParentIds<'commit> {} + +impl<'repo> Clone for Commit<'repo> { + fn clone(&self) -> Self { + self.as_object().clone().into_commit().ok().unwrap() + } +} + +impl<'repo> Drop for Commit<'repo> { + fn drop(&mut self) { + unsafe { raw::git_commit_free(self.raw) } + } +} + +#[cfg(test)] +mod tests { + #[test] + fn smoke() { + let (_td, repo) = crate::test::repo_init(); + let head = repo.head().unwrap(); + let target = head.target().unwrap(); + let commit = repo.find_commit(target).unwrap(); + assert_eq!(commit.message(), Some("initial\n\nbody")); + assert_eq!(commit.body(), Some("body")); + assert_eq!(commit.id(), target); + commit.message_raw().unwrap(); + commit.raw_header().unwrap(); + commit.message_encoding(); + commit.summary().unwrap(); + commit.body().unwrap(); + commit.tree_id(); + commit.tree().unwrap(); + assert_eq!(commit.parents().count(), 0); + + let tree_header_bytes = commit.header_field_bytes("tree").unwrap(); + assert_eq!( + crate::Oid::from_str(tree_header_bytes.as_str().unwrap()).unwrap(), + commit.tree_id() + ); + assert_eq!(commit.author().name(), Some("name")); + assert_eq!(commit.author().email(), Some("email")); + assert_eq!(commit.committer().name(), Some("name")); + assert_eq!(commit.committer().email(), Some("email")); + + let sig = repo.signature().unwrap(); + let tree = repo.find_tree(commit.tree_id()).unwrap(); + let id = repo + .commit(Some("HEAD"), &sig, &sig, "bar", &tree, &[&commit]) + .unwrap(); + let head = repo.find_commit(id).unwrap(); + + let new_head = head + .amend(Some("HEAD"), None, None, None, Some("new message"), None) + .unwrap(); + let new_head = repo.find_commit(new_head).unwrap(); + assert_eq!(new_head.message(), Some("new message")); + new_head.into_object(); + + repo.find_object(target, None).unwrap().as_commit().unwrap(); + repo.find_object(target, None) + .unwrap() + .into_commit() + .ok() + .unwrap(); + } +} diff --git a/extra/git2/src/config.rs b/extra/git2/src/config.rs new file mode 100644 index 000000000..ae5c4ff63 --- /dev/null +++ b/extra/git2/src/config.rs @@ -0,0 +1,777 @@ +use libc; +use std::ffi::CString; +use std::marker; +use std::path::{Path, PathBuf}; +use std::ptr; +use std::str; + +use crate::util::{self, Binding}; +use crate::{raw, Buf, ConfigLevel, Error, IntoCString}; + +/// A structure representing a git configuration key/value store +pub struct Config { + raw: *mut raw::git_config, +} + +/// A struct representing a certain entry owned by a `Config` instance. +/// +/// An entry has a name, a value, and a level it applies to. +pub struct ConfigEntry<'cfg> { + raw: *mut raw::git_config_entry, + _marker: marker::PhantomData<&'cfg Config>, + owned: bool, +} + +/// 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<ConfigEntry<'cfg>>, + _marker: marker::PhantomData<&'cfg Config>, +} + +impl Config { + /// Allocate a new configuration object + /// + /// This object is empty, so you have to add a file to it before you can do + /// anything with it. + pub fn new() -> Result<Config, Error> { + crate::init(); + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_config_new(&mut raw)); + Ok(Binding::from_raw(raw)) + } + } + + /// Create a new config instance containing a single on-disk file + pub fn open(path: &Path) -> Result<Config, Error> { + crate::init(); + let mut raw = ptr::null_mut(); + // Normal file path OK (does not need Windows conversion). + let path = path.into_c_string()?; + unsafe { + try_call!(raw::git_config_open_ondisk(&mut raw, path)); + Ok(Binding::from_raw(raw)) + } + } + + /// Open the global, XDG and system configuration files + /// + /// Utility wrapper that finds the global, XDG and system configuration + /// files and opens them into a single prioritized config object that can + /// be used when accessing default config data outside a repository. + pub fn open_default() -> Result<Config, Error> { + crate::init(); + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_config_open_default(&mut raw)); + Ok(Binding::from_raw(raw)) + } + } + + /// Locate the path to the global configuration file + /// + /// The user or global configuration file is usually located in + /// `$HOME/.gitconfig`. + /// + /// This method will try to guess the full path to that file, if the file + /// 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 + /// (`.config/git/config`). + pub fn find_global() -> Result<PathBuf, Error> { + crate::init(); + let buf = Buf::new(); + unsafe { + try_call!(raw::git_config_find_global(buf.raw())); + } + Ok(util::bytes2path(&buf).to_path_buf()) + } + + /// Locate the path to the system configuration file + /// + /// If /etc/gitconfig doesn't exist, it will look for `%PROGRAMFILES%` + pub fn find_system() -> Result<PathBuf, Error> { + crate::init(); + let buf = Buf::new(); + unsafe { + try_call!(raw::git_config_find_system(buf.raw())); + } + Ok(util::bytes2path(&buf).to_path_buf()) + } + + /// Locate the path to the global XDG compatible configuration file + /// + /// The XDG compatible configuration file is usually located in + /// `$HOME/.config/git/config`. + pub fn find_xdg() -> Result<PathBuf, Error> { + crate::init(); + let buf = Buf::new(); + unsafe { + try_call!(raw::git_config_find_xdg(buf.raw())); + } + Ok(util::bytes2path(&buf).to_path_buf()) + } + + /// Add an on-disk config file instance to an existing config + /// + /// The on-disk file pointed at by path will be opened and parsed; it's + /// expected to be a native Git config file following the default Git config + /// syntax (see man git-config). + /// + /// Further queries on this config object will access each of the config + /// file instances in order (instances with a higher priority level will be + /// accessed first). + pub fn add_file(&mut self, path: &Path, level: ConfigLevel, force: bool) -> Result<(), Error> { + // Normal file path OK (does not need Windows conversion). + let path = path.into_c_string()?; + unsafe { + try_call!(raw::git_config_add_file_ondisk( + self.raw, + path, + level, + ptr::null(), + force + )); + Ok(()) + } + } + + /// Delete a config variable from the config file with the highest level + /// (usually the local one). + pub fn remove(&mut self, name: &str) -> Result<(), Error> { + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_config_delete_entry(self.raw, name)); + Ok(()) + } + } + + /// Remove multivar config variables in the config file with the highest level (usually the + /// local one). + /// + /// The regular expression is applied case-sensitively on the value. + pub fn remove_multivar(&mut self, name: &str, regexp: &str) -> Result<(), Error> { + let name = CString::new(name)?; + let regexp = CString::new(regexp)?; + unsafe { + try_call!(raw::git_config_delete_multivar(self.raw, name, regexp)); + } + Ok(()) + } + + /// Get the value of a boolean config variable. + /// + /// All config files will be looked into, in the order of their defined + /// level. A higher level means a higher priority. The first occurrence of + /// the variable will be returned here. + pub fn get_bool(&self, name: &str) -> Result<bool, Error> { + let mut out = 0 as libc::c_int; + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_config_get_bool(&mut out, &*self.raw, name)); + } + Ok(out != 0) + } + + /// Get the value of an integer config variable. + /// + /// All config files will be looked into, in the order of their defined + /// level. A higher level means a higher priority. The first occurrence of + /// the variable will be returned here. + pub fn get_i32(&self, name: &str) -> Result<i32, Error> { + let mut out = 0i32; + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_config_get_int32(&mut out, &*self.raw, name)); + } + Ok(out) + } + + /// Get the value of an integer config variable. + /// + /// All config files will be looked into, in the order of their defined + /// level. A higher level means a higher priority. The first occurrence of + /// the variable will be returned here. + pub fn get_i64(&self, name: &str) -> Result<i64, Error> { + let mut out = 0i64; + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_config_get_int64(&mut out, &*self.raw, name)); + } + Ok(out) + } + + /// Get the value of a string config variable. + /// + /// 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. + 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")) + } + + /// 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. + pub fn get_bytes(&self, name: &str) -> Result<&[u8], Error> { + let mut ret = ptr::null(); + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_config_get_string(&mut ret, &*self.raw, name)); + Ok(crate::opt_bytes(self, ret).unwrap()) + } + } + + /// Get the value of a string config variable as an owned string. + /// + /// All config files will be looked into, in the order of their + /// defined level. A higher level means a higher priority. The + /// first occurrence of the variable will be returned here. + /// + /// An error will be returned if the config value is not valid utf-8. + pub fn get_string(&self, name: &str) -> Result<String, Error> { + let ret = Buf::new(); + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_config_get_string_buf(ret.raw(), self.raw, name)); + } + str::from_utf8(&ret) + .map(|s| s.to_string()) + .map_err(|_| Error::from_str("configuration value is not valid utf8")) + } + + /// Get the value of a path config variable as an owned `PathBuf`. + /// + /// A leading '~' will be expanded to the global search path (which + /// defaults to the user's home directory but can be overridden via + /// [`raw::git_libgit2_opts`]. + /// + /// All config files will be looked into, in the order of their + /// defined level. A higher level means a higher priority. The + /// first occurrence of the variable will be returned here. + pub fn get_path(&self, name: &str) -> Result<PathBuf, Error> { + let ret = Buf::new(); + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_config_get_path(ret.raw(), self.raw, name)); + } + Ok(crate::util::bytes2path(&ret).to_path_buf()) + } + + /// Get the ConfigEntry for a config variable. + pub fn get_entry(&self, name: &str) -> Result<ConfigEntry<'_>, Error> { + let mut ret = ptr::null_mut(); + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_config_get_entry(&mut ret, self.raw, name)); + Ok(Binding::from_raw(ret)) + } + } + + /// Iterate over all the config variables + /// + /// If `glob` is `Some`, then the iterator will only iterate over all + /// variables whose name matches the pattern. + /// + /// The regular expression is applied case-sensitively on the normalized form of + /// the variable name: the section and variable parts are lower-cased. The + /// subsection is left unchanged. + /// + /// Due to lifetime restrictions, the returned value does not implement + /// the standard [`Iterator`] trait. See [`ConfigEntries`] for more. + /// + /// # Example + /// + /// ``` + /// use git2::Config; + /// + /// let cfg = Config::new().unwrap(); + /// + /// let mut entries = cfg.entries(None).unwrap(); + /// while let Some(entry) = entries.next() { + /// let entry = entry.unwrap(); + /// println!("{} => {}", entry.name().unwrap(), entry.value().unwrap()); + /// } + /// ``` + pub fn entries(&self, glob: Option<&str>) -> Result<ConfigEntries<'_>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + match glob { + Some(s) => { + let s = CString::new(s)?; + try_call!(raw::git_config_iterator_glob_new(&mut ret, &*self.raw, s)); + } + None => { + try_call!(raw::git_config_iterator_new(&mut ret, &*self.raw)); + } + } + Ok(Binding::from_raw(ret)) + } + } + + /// Iterate over the values of a multivar + /// + /// If `regexp` is `Some`, then the iterator will only iterate over all + /// values which match the pattern. + /// + /// The regular expression is applied case-sensitively on the normalized form of + /// the variable name: the section and variable parts are lower-cased. The + /// subsection is left unchanged. + /// + /// Due to lifetime restrictions, the returned value does not implement + /// the standard [`Iterator`] trait. See [`ConfigEntries`] for more. + pub fn multivar(&self, name: &str, regexp: Option<&str>) -> Result<ConfigEntries<'_>, Error> { + let mut ret = ptr::null_mut(); + let name = CString::new(name)?; + let regexp = regexp.map(CString::new).transpose()?; + unsafe { + try_call!(raw::git_config_multivar_iterator_new( + &mut ret, &*self.raw, name, regexp + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Open the global/XDG configuration file according to git's rules + /// + /// Git allows you to store your global configuration at `$HOME/.config` or + /// `$XDG_CONFIG_HOME/git/config`. For backwards 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<Config, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_config_open_global(&mut raw, self.raw)); + Ok(Binding::from_raw(raw)) + } + } + + /// Build a single-level focused config object from a multi-level one. + /// + /// The returned config object can be used to perform get/set/delete + /// operations on a single specific level. + pub fn open_level(&self, level: ConfigLevel) -> Result<Config, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_config_open_level(&mut raw, &*self.raw, level)); + Ok(Binding::from_raw(raw)) + } + } + + /// Set the value of a boolean config variable in the config file with the + /// highest level (usually the local one). + pub fn set_bool(&mut self, name: &str, value: bool) -> Result<(), Error> { + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_config_set_bool(self.raw, name, value)); + } + Ok(()) + } + + /// Set the value of an integer config variable in the config file with the + /// highest level (usually the local one). + pub fn set_i32(&mut self, name: &str, value: i32) -> Result<(), Error> { + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_config_set_int32(self.raw, name, value)); + } + Ok(()) + } + + /// Set the value of an integer config variable in the config file with the + /// highest level (usually the local one). + pub fn set_i64(&mut self, name: &str, value: i64) -> Result<(), Error> { + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_config_set_int64(self.raw, name, value)); + } + Ok(()) + } + + /// Set the value of an multivar config variable in the config file with the + /// highest level (usually the local one). + /// + /// The regular expression is applied case-sensitively on the value. + pub fn set_multivar(&mut self, name: &str, regexp: &str, value: &str) -> Result<(), Error> { + let name = CString::new(name)?; + let regexp = CString::new(regexp)?; + let value = CString::new(value)?; + unsafe { + try_call!(raw::git_config_set_multivar(self.raw, name, regexp, value)); + } + Ok(()) + } + + /// Set the value of a string config variable in the config file with the + /// highest level (usually the local one). + pub fn set_str(&mut self, name: &str, value: &str) -> Result<(), Error> { + let name = CString::new(name)?; + let value = CString::new(value)?; + unsafe { + try_call!(raw::git_config_set_string(self.raw, name, value)); + } + Ok(()) + } + + /// Create a snapshot of the configuration + /// + /// Create a snapshot of the current state of a configuration, which allows + /// you to look into a consistent view of the configuration for looking up + /// complex values (e.g. a remote, submodule). + pub fn snapshot(&mut self) -> Result<Config, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_config_snapshot(&mut ret, self.raw)); + Ok(Binding::from_raw(ret)) + } + } + + /// Parse a string as a bool. + /// + /// Interprets "true", "yes", "on", 1, or any non-zero number as true. + /// Interprets "false", "no", "off", 0, or an empty string as false. + pub fn parse_bool<S: IntoCString>(s: S) -> Result<bool, Error> { + let s = s.into_c_string()?; + let mut out = 0; + crate::init(); + unsafe { + try_call!(raw::git_config_parse_bool(&mut out, s)); + } + Ok(out != 0) + } + + /// Parse a string as an i32; handles suffixes like k, M, or G, and + /// multiplies by the appropriate power of 1024. + pub fn parse_i32<S: IntoCString>(s: S) -> Result<i32, Error> { + let s = s.into_c_string()?; + let mut out = 0; + crate::init(); + unsafe { + try_call!(raw::git_config_parse_int32(&mut out, s)); + } + Ok(out) + } + + /// Parse a string as an i64; handles suffixes like k, M, or G, and + /// multiplies by the appropriate power of 1024. + pub fn parse_i64<S: IntoCString>(s: S) -> Result<i64, Error> { + let s = s.into_c_string()?; + let mut out = 0; + crate::init(); + unsafe { + try_call!(raw::git_config_parse_int64(&mut out, s)); + } + Ok(out) + } +} + +impl Binding for Config { + type Raw = *mut raw::git_config; + unsafe fn from_raw(raw: *mut raw::git_config) -> Config { + Config { raw } + } + fn raw(&self) -> *mut raw::git_config { + self.raw + } +} + +impl Drop for Config { + fn drop(&mut self) { + unsafe { raw::git_config_free(self.raw) } + } +} + +impl<'cfg> ConfigEntry<'cfg> { + /// Gets the name of this entry. + /// + /// May return `None` if the name is not valid utf-8 + pub fn name(&self) -> Option<&str> { + str::from_utf8(self.name_bytes()).ok() + } + + /// Gets the name of this entry as a byte slice. + pub fn name_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, (*self.raw).name).unwrap() } + } + + /// Gets the value of this entry. + /// + /// May return `None` if the value is not valid utf-8 + /// + /// # Panics + /// + /// Panics when no value is defined. + pub fn value(&self) -> Option<&str> { + str::from_utf8(self.value_bytes()).ok() + } + + /// Gets the value of this entry as a byte slice. + /// + /// # Panics + /// + /// Panics when no value is defined. + pub fn value_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, (*self.raw).value).unwrap() } + } + + /// Returns `true` when a value is defined otherwise `false`. + /// + /// No value defined is a short-hand to represent a Boolean `true`. + pub fn has_value(&self) -> bool { + unsafe { !(*self.raw).value.is_null() } + } + + /// Gets the configuration level of this entry. + pub fn level(&self) -> ConfigLevel { + unsafe { ConfigLevel::from_raw((*self.raw).level) } + } + + /// Depth of includes where this variable was found + pub fn include_depth(&self) -> u32 { + unsafe { (*self.raw).include_depth as u32 } + } +} + +impl<'cfg> Binding for ConfigEntry<'cfg> { + type Raw = *mut raw::git_config_entry; + + unsafe fn from_raw(raw: *mut raw::git_config_entry) -> ConfigEntry<'cfg> { + ConfigEntry { + raw, + _marker: marker::PhantomData, + owned: true, + } + } + fn raw(&self) -> *mut raw::git_config_entry { + self.raw + } +} + +impl<'cfg> Binding for ConfigEntries<'cfg> { + type Raw = *mut raw::git_config_iterator; + + unsafe fn from_raw(raw: *mut raw::git_config_iterator) -> ConfigEntries<'cfg> { + ConfigEntries { + raw, + current: None, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_config_iterator { + self.raw + } +} + +impl<'cfg> ConfigEntries<'cfg> { + /// Advances the iterator and returns the next value. + /// + /// Returns `None` when iteration is finished. + pub fn next(&mut self) -> Option<Result<&ConfigEntry<'cfg>, Error>> { + let mut raw = ptr::null_mut(); + drop(self.current.take()); + unsafe { + try_call_iter!(raw::git_config_next(&mut raw, self.raw)); + let entry = ConfigEntry { + owned: false, + 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<F: FnMut(&ConfigEntry<'cfg>)>(mut self, mut f: F) -> Result<(), Error> { + while let Some(entry) = self.next() { + let entry = entry?; + f(entry); + } + Ok(()) + } +} + +impl<'cfg> Drop for ConfigEntries<'cfg> { + fn drop(&mut self) { + unsafe { raw::git_config_iterator_free(self.raw) } + } +} + +impl<'cfg> Drop for ConfigEntry<'cfg> { + fn drop(&mut self) { + if self.owned { + unsafe { raw::git_config_entry_free(self.raw) } + } + } +} + +#[cfg(test)] +mod tests { + use std::fs::File; + use tempfile::TempDir; + + use crate::Config; + + #[test] + fn smoke() { + let _cfg = Config::new().unwrap(); + let _ = Config::find_global(); + let _ = Config::find_system(); + let _ = Config::find_xdg(); + } + + #[test] + fn persisted() { + let td = TempDir::new().unwrap(); + let path = td.path().join("foo"); + File::create(&path).unwrap(); + + let mut cfg = Config::open(&path).unwrap(); + assert!(cfg.get_bool("foo.bar").is_err()); + cfg.set_bool("foo.k1", true).unwrap(); + cfg.set_i32("foo.k2", 1).unwrap(); + cfg.set_i64("foo.k3", 2).unwrap(); + cfg.set_str("foo.k4", "bar").unwrap(); + cfg.snapshot().unwrap(); + drop(cfg); + + let cfg = Config::open(&path).unwrap().snapshot().unwrap(); + assert_eq!(cfg.get_bool("foo.k1").unwrap(), true); + assert_eq!(cfg.get_i32("foo.k2").unwrap(), 1); + assert_eq!(cfg.get_i64("foo.k3").unwrap(), 2); + assert_eq!(cfg.get_str("foo.k4").unwrap(), "bar"); + + let mut entries = cfg.entries(None).unwrap(); + while let Some(entry) = entries.next() { + let entry = entry.unwrap(); + entry.name(); + entry.value(); + entry.level(); + } + } + + #[test] + fn multivar() { + let td = TempDir::new().unwrap(); + let path = td.path().join("foo"); + File::create(&path).unwrap(); + + let mut cfg = Config::open(&path).unwrap(); + cfg.set_multivar("foo.bar", "^$", "baz").unwrap(); + cfg.set_multivar("foo.bar", "^$", "qux").unwrap(); + cfg.set_multivar("foo.bar", "^$", "quux").unwrap(); + cfg.set_multivar("foo.baz", "^$", "oki").unwrap(); + + // `entries` filters by name + let mut entries: Vec<String> = Vec::new(); + cfg.entries(Some("foo.bar")) + .unwrap() + .for_each(|entry| entries.push(entry.value().unwrap().to_string())) + .unwrap(); + entries.sort(); + assert_eq!(entries, ["baz", "quux", "qux"]); + + // which is the same as `multivar` without a regex + let mut multivals = Vec::new(); + cfg.multivar("foo.bar", None) + .unwrap() + .for_each(|entry| multivals.push(entry.value().unwrap().to_string())) + .unwrap(); + multivals.sort(); + assert_eq!(multivals, entries); + + // yet _with_ a regex, `multivar` filters by value + let mut quxish = Vec::new(); + cfg.multivar("foo.bar", Some("qu.*x")) + .unwrap() + .for_each(|entry| quxish.push(entry.value().unwrap().to_string())) + .unwrap(); + quxish.sort(); + assert_eq!(quxish, ["quux", "qux"]); + + cfg.remove_multivar("foo.bar", ".*").unwrap(); + + let count = |entries: super::ConfigEntries<'_>| -> usize { + let mut c = 0; + entries.for_each(|_| c += 1).unwrap(); + c + }; + + assert_eq!(count(cfg.entries(Some("foo.bar")).unwrap()), 0); + assert_eq!(count(cfg.multivar("foo.bar", None).unwrap()), 0); + } + + #[test] + fn parse() { + assert_eq!(Config::parse_bool("").unwrap(), false); + assert_eq!(Config::parse_bool("false").unwrap(), false); + assert_eq!(Config::parse_bool("no").unwrap(), false); + assert_eq!(Config::parse_bool("off").unwrap(), false); + assert_eq!(Config::parse_bool("0").unwrap(), false); + + assert_eq!(Config::parse_bool("true").unwrap(), true); + assert_eq!(Config::parse_bool("yes").unwrap(), true); + assert_eq!(Config::parse_bool("on").unwrap(), true); + assert_eq!(Config::parse_bool("1").unwrap(), true); + assert_eq!(Config::parse_bool("42").unwrap(), true); + + assert!(Config::parse_bool(" ").is_err()); + assert!(Config::parse_bool("some-string").is_err()); + assert!(Config::parse_bool("-").is_err()); + + assert_eq!(Config::parse_i32("0").unwrap(), 0); + assert_eq!(Config::parse_i32("1").unwrap(), 1); + assert_eq!(Config::parse_i32("100").unwrap(), 100); + assert_eq!(Config::parse_i32("-1").unwrap(), -1); + assert_eq!(Config::parse_i32("-100").unwrap(), -100); + assert_eq!(Config::parse_i32("1k").unwrap(), 1024); + assert_eq!(Config::parse_i32("4k").unwrap(), 4096); + assert_eq!(Config::parse_i32("1M").unwrap(), 1048576); + assert_eq!(Config::parse_i32("1G").unwrap(), 1024 * 1024 * 1024); + + assert_eq!(Config::parse_i64("0").unwrap(), 0); + assert_eq!(Config::parse_i64("1").unwrap(), 1); + assert_eq!(Config::parse_i64("100").unwrap(), 100); + assert_eq!(Config::parse_i64("-1").unwrap(), -1); + assert_eq!(Config::parse_i64("-100").unwrap(), -100); + assert_eq!(Config::parse_i64("1k").unwrap(), 1024); + assert_eq!(Config::parse_i64("4k").unwrap(), 4096); + assert_eq!(Config::parse_i64("1M").unwrap(), 1048576); + assert_eq!(Config::parse_i64("1G").unwrap(), 1024 * 1024 * 1024); + assert_eq!(Config::parse_i64("100G").unwrap(), 100 * 1024 * 1024 * 1024); + } +} diff --git a/extra/git2/src/cred.rs b/extra/git2/src/cred.rs new file mode 100644 index 000000000..49afb4239 --- /dev/null +++ b/extra/git2/src/cred.rs @@ -0,0 +1,717 @@ +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}; + +/// A structure to represent git credentials in libgit2. +pub struct Cred { + raw: *mut raw::git_cred, +} + +/// Management of the gitcredentials(7) interface. +pub struct CredentialHelper { + /// A public field representing the currently discovered username from + /// configuration. + pub username: Option<String>, + protocol: Option<String>, + host: Option<String>, + port: Option<u16>, + path: Option<String>, + url: String, + commands: Vec<String>, +} + +impl Cred { + /// Create a "default" credential usable for Negotiate mechanisms like NTLM + /// or Kerberos authentication. + pub fn default() -> Result<Cred, Error> { + crate::init(); + let mut out = ptr::null_mut(); + unsafe { + try_call!(raw::git_cred_default_new(&mut out)); + Ok(Binding::from_raw(out)) + } + } + + /// Create a new ssh key credential object used for querying an ssh-agent. + /// + /// The username specified is the username to authenticate. + pub fn ssh_key_from_agent(username: &str) -> Result<Cred, Error> { + crate::init(); + let mut out = ptr::null_mut(); + let username = CString::new(username)?; + unsafe { + try_call!(raw::git_cred_ssh_key_from_agent(&mut out, username)); + Ok(Binding::from_raw(out)) + } + } + + /// Create a new passphrase-protected ssh key credential object. + pub fn ssh_key( + username: &str, + publickey: Option<&Path>, + privatekey: &Path, + passphrase: Option<&str>, + ) -> Result<Cred, Error> { + crate::init(); + let username = CString::new(username)?; + let publickey = crate::opt_cstr(publickey)?; + let privatekey = privatekey.into_c_string()?; + let passphrase = crate::opt_cstr(passphrase)?; + let mut out = ptr::null_mut(); + unsafe { + try_call!(raw::git_cred_ssh_key_new( + &mut out, username, publickey, privatekey, passphrase + )); + Ok(Binding::from_raw(out)) + } + } + + /// Create a new ssh key credential object reading the keys from memory. + pub fn ssh_key_from_memory( + username: &str, + publickey: Option<&str>, + privatekey: &str, + passphrase: Option<&str>, + ) -> Result<Cred, Error> { + crate::init(); + let username = CString::new(username)?; + let publickey = crate::opt_cstr(publickey)?; + let privatekey = CString::new(privatekey)?; + let passphrase = crate::opt_cstr(passphrase)?; + let mut out = ptr::null_mut(); + unsafe { + try_call!(raw::git_cred_ssh_key_memory_new( + &mut out, username, publickey, privatekey, passphrase + )); + Ok(Binding::from_raw(out)) + } + } + + /// Create a new plain-text username and password credential object. + pub fn userpass_plaintext(username: &str, password: &str) -> Result<Cred, Error> { + crate::init(); + let username = CString::new(username)?; + let password = CString::new(password)?; + let mut out = ptr::null_mut(); + unsafe { + try_call!(raw::git_cred_userpass_plaintext_new( + &mut out, username, password + )); + Ok(Binding::from_raw(out)) + } + } + + /// Attempt to read `credential.helper` according to gitcredentials(7) [1] + /// + /// 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. + /// + /// The returned credential type will be a username/password credential if + /// successful. + /// + /// [1]: https://www.kernel.org/pub/software/scm/git/docs/gitcredentials.html + pub fn credential_helper( + config: &Config, + url: &str, + username: Option<&str>, + ) -> Result<Cred, Error> { + match CredentialHelper::new(url) + .config(config) + .username(username) + .execute() + { + Some((username, password)) => Cred::userpass_plaintext(&username, &password), + None => Err(Error::from_str( + "failed to acquire username/password \ + from local configuration", + )), + } + } + + /// Create a credential to specify a username. + /// + /// This is used with ssh authentication to query for the username if none is + /// specified in the URL. + pub fn username(username: &str) -> Result<Cred, Error> { + crate::init(); + let username = CString::new(username)?; + let mut out = ptr::null_mut(); + unsafe { + try_call!(raw::git_cred_username_new(&mut out, username)); + Ok(Binding::from_raw(out)) + } + } + + /// Check whether a credential object contains username information. + pub fn has_username(&self) -> bool { + unsafe { raw::git_cred_has_username(self.raw) == 1 } + } + + /// Return the type of credentials that this object represents. + pub fn credtype(&self) -> raw::git_credtype_t { + unsafe { (*self.raw).credtype } + } + + /// Unwrap access to the underlying raw pointer, canceling the destructor + pub unsafe fn unwrap(mut self) -> *mut raw::git_cred { + mem::replace(&mut self.raw, ptr::null_mut()) + } +} + +impl Binding for Cred { + type Raw = *mut raw::git_cred; + + unsafe fn from_raw(raw: *mut raw::git_cred) -> Cred { + Cred { raw } + } + fn raw(&self) -> *mut raw::git_cred { + self.raw + } +} + +impl Drop for Cred { + fn drop(&mut self) { + if !self.raw.is_null() { + unsafe { + if let Some(f) = (*self.raw).free { + f(self.raw) + } + } + } + } +} + +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. + pub fn new(url: &str) -> CredentialHelper { + let mut ret = CredentialHelper { + protocol: None, + host: None, + port: None, + path: None, + username: None, + url: url.to_string(), + commands: Vec::new(), + }; + + // Parse out the (protocol, host) if one is available + if let Ok(url) = url::Url::parse(url) { + if let Some(url::Host::Domain(s)) = url.host() { + ret.host = Some(s.to_string()); + } + ret.port = url.port(); + ret.protocol = Some(url.scheme().to_string()); + } + ret + } + + /// Set the username that this credential helper will query with. + /// + /// By default the username is `None`. + pub fn username(&mut self, username: Option<&str>) -> &mut CredentialHelper { + self.username = username.map(|s| s.to_string()); + self + } + + /// Query the specified configuration object to discover commands to + /// execute, usernames to query, etc. + pub fn config(&mut self, config: &Config) -> &mut CredentialHelper { + // Figure out the configured username/helper program. + // + // see http://git-scm.com/docs/gitcredentials.html#_configuration_options + if self.username.is_none() { + self.config_username(config); + } + self.config_helper(config); + self.config_use_http_path(config); + self + } + + // Configure the queried username from `config` + fn config_username(&mut self, config: &Config) { + let key = self.exact_key("username"); + self.username = config + .get_string(&key) + .ok() + .or_else(|| { + self.url_key("username") + .and_then(|s| config.get_string(&s).ok()) + }) + .or_else(|| config.get_string("credential.username").ok()) + } + + // Discover all `helper` directives from `config` + fn config_helper(&mut self, config: &Config) { + let exact = config.get_string(&self.exact_key("helper")); + self.add_command(exact.as_ref().ok().map(|s| &s[..])); + if let Some(key) = self.url_key("helper") { + let url = config.get_string(&key); + self.add_command(url.as_ref().ok().map(|s| &s[..])); + } + let global = config.get_string("credential.helper"); + self.add_command(global.as_ref().ok().map(|s| &s[..])); + } + + // Discover `useHttpPath` from `config` + fn config_use_http_path(&mut self, config: &Config) { + let mut use_http_path = false; + if let Some(value) = config.get_bool(&self.exact_key("useHttpPath")).ok() { + use_http_path = value; + } else if let Some(value) = self + .url_key("useHttpPath") + .and_then(|key| config.get_bool(&key).ok()) + { + use_http_path = value; + } else if let Some(value) = config.get_bool("credential.useHttpPath").ok() { + use_http_path = value; + } + + if use_http_path { + if let Ok(url) = url::Url::parse(&self.url) { + let path = url.path(); + // Url::parse always includes a leading slash for rooted URLs, while git does not. + self.path = Some(path.strip_prefix('/').unwrap_or(path).to_string()); + } + } + } + + // Add a `helper` configured command to the list of commands to execute. + // + // see https://www.kernel.org/pub/software/scm/git/docs/technical + // /api-credentials.html#_credential_helpers + fn add_command(&mut self, cmd: Option<&str>) { + let cmd = match cmd { + Some("") | None => return, + Some(s) => s, + }; + + if cmd.starts_with('!') { + self.commands.push(cmd[1..].to_string()); + } else if cmd.contains("/") || cmd.contains("\\") { + self.commands.push(cmd.to_string()); + } else { + self.commands.push(format!("git credential-{}", cmd)); + } + } + + fn exact_key(&self, name: &str) -> String { + format!("credential.{}.{}", self.url, name) + } + + fn url_key(&self, name: &str) -> Option<String> { + match (&self.host, &self.protocol) { + (&Some(ref host), &Some(ref protocol)) => { + Some(format!("credential.{}://{}.{}", protocol, host, name)) + } + _ => None, + } + } + + /// Execute this helper, attempting to discover a username/password pair. + /// + /// All I/O errors are ignored, (to match git behavior), and this function + /// only succeeds if both a username and a password were found + pub fn execute(&self) -> Option<(String, String)> { + let mut username = self.username.clone(); + let mut password = None; + for cmd in &self.commands { + let (u, p) = self.execute_cmd(cmd, &username); + if u.is_some() && username.is_none() { + username = u; + } + if p.is_some() && password.is_none() { + password = p; + } + if username.is_some() && password.is_some() { + break; + } + } + + match (username, password) { + (Some(u), Some(p)) => Some((u, p)), + _ => None, + } + } + + // Execute the given `cmd`, providing the appropriate variables on stdin and + // then afterwards parsing the output into the username/password on stdout. + fn execute_cmd( + &self, + cmd: &str, + username: &Option<String>, + ) -> (Option<String>, Option<String>) { + macro_rules! my_try( ($e:expr) => ( + match $e { + Ok(e) => e, + Err(e) => { + debug!("{} failed with {}", stringify!($e), e); + return (None, None) + } + } + ) ); + + // It looks like the `cmd` specification is typically bourne-shell-like + // syntax, so try that first. If that fails, though, we may be on a + // Windows machine for example where `sh` isn't actually available by + // default. Most credential helper configurations though are pretty + // simple (aka one or two space-separated strings) so also try to invoke + // the process directly. + // + // If that fails then it's up to the user to put `sh` in path and make + // sure it works. + let mut c = Command::new("sh"); + c.arg("-c") + .arg(&format!("{} get", cmd)) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + debug!("executing credential helper {:?}", c); + let mut p = match c.spawn() { + Ok(p) => p, + Err(e) => { + debug!("`sh` failed to spawn: {}", e); + let mut parts = cmd.split_whitespace(); + let mut c = Command::new(parts.next().unwrap()); + for arg in parts { + c.arg(arg); + } + c.arg("get") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + debug!("executing credential helper {:?}", c); + match c.spawn() { + Ok(p) => p, + Err(e) => { + debug!("fallback of {:?} failed with {}", cmd, e); + return (None, None); + } + } + } + }; + + // Ignore write errors as the command may not actually be listening for + // stdin + { + let stdin = p.stdin.as_mut().unwrap(); + if let Some(ref p) = self.protocol { + let _ = writeln!(stdin, "protocol={}", p); + } + if let Some(ref p) = self.host { + if let Some(ref p2) = self.port { + let _ = writeln!(stdin, "host={}:{}", p, p2); + } else { + let _ = writeln!(stdin, "host={}", p); + } + } + if let Some(ref p) = self.path { + let _ = writeln!(stdin, "path={}", p); + } + if let Some(ref p) = *username { + let _ = writeln!(stdin, "username={}", p); + } + } + let output = my_try!(p.wait_with_output()); + if !output.status.success() { + debug!( + "credential helper failed: {}\nstdout ---\n{}\nstderr ---\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + return (None, None); + } + trace!( + "credential helper stderr ---\n{}", + String::from_utf8_lossy(&output.stderr) + ); + self.parse_output(output.stdout) + } + + // Parse the output of a command into the username/password found + fn parse_output(&self, output: Vec<u8>) -> (Option<String>, Option<String>) { + // Parse the output of the command, looking for username/password + let mut username = None; + let mut password = None; + for line in output.split(|t| *t == b'\n') { + let mut parts = line.splitn(2, |t| *t == b'='); + let key = parts.next().unwrap(); + let value = match parts.next() { + Some(s) => s, + None => { + trace!("ignoring output line: {}", String::from_utf8_lossy(line)); + continue; + } + }; + let value = match String::from_utf8(value.to_vec()) { + Ok(s) => s, + Err(..) => continue, + }; + match key { + b"username" => username = Some(value), + b"password" => password = Some(value), + _ => {} + } + } + (username, password) + } +} + +#[cfg(test)] +mod test { + use std::env; + use std::fs::File; + use std::io::prelude::*; + use std::path::Path; + use tempfile::TempDir; + + use crate::{Config, ConfigLevel, Cred, CredentialHelper}; + + 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.set_str($k, $v).unwrap();)* + cfg + }) ); + + #[test] + fn smoke() { + Cred::default().unwrap(); + } + + #[test] + fn credential_helper1() { + let cfg = test_cfg! { + "credential.helper" => "!f() { echo username=a; echo password=b; }; f" + }; + let (u, p) = CredentialHelper::new("https://example.com/foo/bar") + .config(&cfg) + .execute() + .unwrap(); + assert_eq!(u, "a"); + assert_eq!(p, "b"); + } + + #[test] + fn credential_helper2() { + let cfg = test_cfg! {}; + assert!(CredentialHelper::new("https://example.com/foo/bar") + .config(&cfg) + .execute() + .is_none()); + } + + #[test] + fn credential_helper3() { + let cfg = test_cfg! { + "credential.https://example.com.helper" => + "!f() { echo username=c; }; f", + "credential.helper" => "!f() { echo username=a; echo password=b; }; f" + }; + let (u, p) = CredentialHelper::new("https://example.com/foo/bar") + .config(&cfg) + .execute() + .unwrap(); + assert_eq!(u, "c"); + assert_eq!(p, "b"); + } + + #[test] + fn credential_helper4() { + if cfg!(windows) { + return; + } // shell scripts don't work on Windows + + let td = TempDir::new().unwrap(); + let path = td.path().join("script"); + File::create(&path) + .unwrap() + .write( + br"\ +#!/bin/sh +echo username=c +", + ) + .unwrap(); + chmod(&path); + let cfg = test_cfg! { + "credential.https://example.com.helper" => + &path.display().to_string()[..], + "credential.helper" => "!f() { echo username=a; echo password=b; }; f" + }; + let (u, p) = CredentialHelper::new("https://example.com/foo/bar") + .config(&cfg) + .execute() + .unwrap(); + assert_eq!(u, "c"); + assert_eq!(p, "b"); + } + + #[test] + fn credential_helper5() { + if !Path::new("/usr/bin/git").exists() { + return; + } //this test does not work if git is not installed + if cfg!(windows) { + return; + } // shell scripts don't work on Windows + let td = TempDir::new().unwrap(); + let path = td.path().join("git-credential-script"); + File::create(&path) + .unwrap() + .write( + br"\ +#!/bin/sh +echo username=c +", + ) + .unwrap(); + chmod(&path); + + let paths = env::var("PATH").unwrap(); + let paths = + env::split_paths(&paths).chain(path.parent().map(|p| p.to_path_buf()).into_iter()); + env::set_var("PATH", &env::join_paths(paths).unwrap()); + + let cfg = test_cfg! { + "credential.https://example.com.helper" => "script", + "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!(p, "b"); + } + + #[test] + fn credential_helper6() { + let cfg = test_cfg! { + "credential.helper" => "" + }; + assert!(CredentialHelper::new("https://example.com/foo/bar") + .config(&cfg) + .execute() + .is_none()); + } + + #[test] + fn credential_helper7() { + if cfg!(windows) { + return; + } // shell scripts don't work on Windows + let td = TempDir::new().unwrap(); + let path = td.path().join("script"); + File::create(&path) + .unwrap() + .write( + br"\ +#!/bin/sh +echo username=$1 +echo password=$2 +", + ) + .unwrap(); + chmod(&path); + let cfg = test_cfg! { + "credential.helper" => &format!("{} a b", path.display()) + }; + let (u, p) = CredentialHelper::new("https://example.com/foo/bar") + .config(&cfg) + .execute() + .unwrap(); + assert_eq!(u, "a"); + assert_eq!(p, "b"); + } + + #[test] + fn credential_helper8() { + let cfg = test_cfg! { + "credential.useHttpPath" => "true" + }; + let mut helper = CredentialHelper::new("https://example.com/foo/bar"); + helper.config(&cfg); + assert_eq!(helper.path.as_deref(), Some("foo/bar")); + } + + #[test] + fn credential_helper9() { + let cfg = test_cfg! { + "credential.helper" => "!f() { while read line; do eval $line; done; if [ \"$host\" = example.com:3000 ]; then echo username=a; echo password=b; fi; }; f" + }; + let (u, p) = CredentialHelper::new("https://example.com:3000/foo/bar") + .config(&cfg) + .execute() + .unwrap(); + assert_eq!(u, "a"); + assert_eq!(p, "b"); + } + + #[test] + #[cfg(feature = "ssh")] + fn ssh_key_from_memory() { + let cred = Cred::ssh_key_from_memory( + "test", + Some("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDByAO8uj+kXicj6C2ODMspgmUoVyl5eaw8vR6a1yEnFuJFzevabNlN6Ut+CPT3TRnYk5BW73pyXBtnSL2X95BOnbjMDXc4YIkgs3YYHWnxbqsD4Pj/RoGqhf+gwhOBtL0poh8tT8WqXZYxdJQKLQC7oBqf3ykCEYulE4oeRUmNh4IzEE+skD/zDkaJ+S1HRD8D8YCiTO01qQnSmoDFdmIZTi8MS8Cw+O/Qhym1271ThMlhD6PubSYJXfE6rVbE7A9RzH73A6MmKBlzK8VTb4SlNSrr/DOk+L0uq+wPkv+pm+D9WtxoqQ9yl6FaK1cPawa3+7yRNle3m+72KCtyMkQv"), + r#" + -----BEGIN RSA PRIVATE KEY----- + Proc-Type: 4,ENCRYPTED + DEK-Info: AES-128-CBC,818C7722D3B01F2161C2ACF6A5BBAAE8 + + 3Cht4QB3PcoQ0I55j1B3m2ZzIC/mrh+K5nQeA1Vy2GBTMyM7yqGHqTOv7qLhJscd + H+cB0Pm6yCr3lYuNrcKWOCUto+91P7ikyARruHVwyIxKdNx15uNulOzQJHQWNbA4 + RQHlhjON4atVo2FyJ6n+ujK6QiBg2PR5Vbbw/AtV6zBCFW3PhzDn+qqmHjpBFqj2 + vZUUe+MkDQcaF5J45XMHahhSdo/uKCDhfbylExp/+ACWkvxdPpsvcARM6X434ucD + aPY+4i0/JyLkdbm0GFN9/q3i53qf4kCBhojFl4AYJdGI0AzAgbdTXZ7EJHbAGZHS + os5K0oTwDVXMI0sSE2I/qHxaZZsDP1dOKq6di6SFPUp8liYimm7rNintRX88Gl2L + g1ko9abp/NlgD0YY/3mad+NNAISDL/YfXq2fklH3En3/7ZrOVZFKfZXwQwas5g+p + VQPKi3+ae74iOjLyuPDSc1ePmhUNYeP+9rLSc0wiaiHqls+2blPPDxAGMEo63kbz + YPVjdmuVX4VWnyEsfTxxJdFDYGSNh6rlrrO1RFrex7kJvpg5gTX4M/FT8TfCd7Hn + M6adXsLMqwu5tz8FuDmAtVdq8zdSrgZeAbpJ9D3EDOmZ70xz4XBL19ImxDp+Qqs2 + kQX7kobRzeeP2URfRoGr7XZikQWyQ2UASfPcQULY8R58QoZWWsQ4w51GZHg7TDnw + 1DRo/0OgkK7Gqf215nFmMpB4uyi58cq3WFwWQa1IqslkObpVgBQZcNZb/hKUYPGk + g4zehfIgAfCdnQHwZvQ6Fdzhcs3SZeO+zVyuiZN3Gsi9HU0/1vpAKiuuOzcG02vF + b6Y6hwsAA9yphF3atI+ARD4ZwXdDfzuGb3yJglMT3Fr/xuLwAvdchRo1spANKA0E + tT5okLrK0H4wnHvf2SniVVWRhmJis0lQo9LjGGwRIdsPpVnJSDvaISIVF+fHT90r + HvxN8zXI93x9jcPtwp7puQ1C7ehKJK10sZ71OLIZeuUgwt+5DRunqg6evPco9Go7 + UOGwcVhLY200KT+1k7zWzCS0yVQp2HRm6cxsZXAp4ClBSwIx15eIoLIrjZdJRjCq + COp6pZx1fnvJ9ERIvl5hon+Ty+renMcFKz2HmchC7egpcqIxW9Dsv6zjhHle6pxb + 37GaEKHF2KA3RN+dSV/K8n+C9Yent5tx5Y9a/pMcgRGtgu+G+nyFmkPKn5Zt39yX + qDpyM0LtbRVZPs+MgiqoGIwYc/ujoCq7GL38gezsBQoHaTt79yYBqCp6UR0LMuZ5 + f/7CtWqffgySfJ/0wjGidDAumDv8CK45AURpL/Z+tbFG3M9ar/LZz/Y6EyBcLtGY + Wwb4zs8zXIA0qHrjNTnPqHDvezziArYfgPjxCIHMZzms9Yn8+N02p39uIytqg434 + BAlCqZ7GYdDFfTpWIwX+segTK9ux0KdBqcQv+9Fwwjkq9KySnRKqNl7ZJcefFZJq + c6PA1iinZWBjuaO1HKx3PFulrl0bcpR9Kud1ZIyfnh5rwYN8UQkkcR/wZPla04TY + 8l5dq/LI/3G5sZXwUHKOcuQWTj7Saq7Q6gkKoMfqt0wC5bpZ1m17GHPoMz6GtX9O + -----END RSA PRIVATE KEY----- + "#, + Some("test123")); + assert!(cred.is_ok()); + } + + #[cfg(unix)] + fn chmod(path: &Path) { + use std::fs; + use std::os::unix::prelude::*; + let mut perms = fs::metadata(path).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(path, perms).unwrap(); + } + #[cfg(windows)] + fn chmod(_path: &Path) {} +} diff --git a/extra/git2/src/describe.rs b/extra/git2/src/describe.rs new file mode 100644 index 000000000..cbaa1893b --- /dev/null +++ b/extra/git2/src/describe.rs @@ -0,0 +1,201 @@ +use std::ffi::CString; +use std::marker; +use std::mem; +use std::ptr; + +use libc::{c_int, c_uint}; + +use crate::util::Binding; +use crate::{raw, Buf, Error, Repository}; + +/// The result of a `describe` operation on either an `Describe` or a +/// `Repository`. +pub struct Describe<'repo> { + raw: *mut raw::git_describe_result, + _marker: marker::PhantomData<&'repo Repository>, +} + +/// Options which indicate how a `Describe` is created. +pub struct DescribeOptions { + raw: raw::git_describe_options, + pattern: CString, +} + +/// Options which can be used to customize how a description is formatted. +pub struct DescribeFormatOptions { + raw: raw::git_describe_format_options, + dirty_suffix: CString, +} + +impl<'repo> Describe<'repo> { + /// Prints this describe result, returning the result as a string. + pub fn format(&self, opts: Option<&DescribeFormatOptions>) -> Result<String, Error> { + let buf = Buf::new(); + let opts = opts.map(|o| &o.raw as *const _).unwrap_or(ptr::null()); + unsafe { + try_call!(raw::git_describe_format(buf.raw(), self.raw, opts)); + } + Ok(String::from_utf8(buf.to_vec()).unwrap()) + } +} + +impl<'repo> Binding for Describe<'repo> { + type Raw = *mut raw::git_describe_result; + + unsafe fn from_raw(raw: *mut raw::git_describe_result) -> Describe<'repo> { + Describe { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_describe_result { + self.raw + } +} + +impl<'repo> Drop for Describe<'repo> { + fn drop(&mut self) { + unsafe { raw::git_describe_result_free(self.raw) } + } +} + +impl Default for DescribeFormatOptions { + fn default() -> Self { + Self::new() + } +} + +impl DescribeFormatOptions { + /// Creates a new blank set of formatting options for a description. + pub fn new() -> DescribeFormatOptions { + let mut opts = DescribeFormatOptions { + raw: unsafe { mem::zeroed() }, + dirty_suffix: CString::new(Vec::new()).unwrap(), + }; + opts.raw.version = 1; + opts.raw.abbreviated_size = 7; + opts + } + + /// Sets the size of the abbreviated commit id to use. + /// + /// The value is the lower bound for the length of the abbreviated string, + /// and the default is 7. + pub fn abbreviated_size(&mut self, size: u32) -> &mut Self { + self.raw.abbreviated_size = size as c_uint; + self + } + + /// Sets whether or not the long format is used even when a shorter name + /// could be used. + pub fn always_use_long_format(&mut self, long: bool) -> &mut Self { + self.raw.always_use_long_format = long as c_int; + self + } + + /// If the workdir is dirty and this is set, this string will be appended to + /// the description string. + pub fn dirty_suffix(&mut self, suffix: &str) -> &mut Self { + self.dirty_suffix = CString::new(suffix).unwrap(); + self.raw.dirty_suffix = self.dirty_suffix.as_ptr(); + self + } +} + +impl Default for DescribeOptions { + fn default() -> Self { + Self::new() + } +} + +impl DescribeOptions { + /// Creates a new blank set of formatting options for a description. + pub fn new() -> DescribeOptions { + let mut opts = DescribeOptions { + raw: unsafe { mem::zeroed() }, + pattern: CString::new(Vec::new()).unwrap(), + }; + opts.raw.version = 1; + opts.raw.max_candidates_tags = 10; + opts + } + + #[allow(missing_docs)] + pub fn max_candidates_tags(&mut self, max: u32) -> &mut Self { + self.raw.max_candidates_tags = max as c_uint; + self + } + + /// Sets the reference lookup strategy + /// + /// This behaves like the `--tags` option to git-describe. + pub fn describe_tags(&mut self) -> &mut Self { + self.raw.describe_strategy = raw::GIT_DESCRIBE_TAGS as c_uint; + self + } + + /// Sets the reference lookup strategy + /// + /// This behaves like the `--all` option to git-describe. + pub fn describe_all(&mut self) -> &mut Self { + self.raw.describe_strategy = raw::GIT_DESCRIBE_ALL as c_uint; + self + } + + /// Indicates when calculating the distance from the matching tag or + /// reference whether to only walk down the first-parent ancestry. + pub fn only_follow_first_parent(&mut self, follow: bool) -> &mut Self { + self.raw.only_follow_first_parent = follow as c_int; + self + } + + /// If no matching tag or reference is found whether a describe option would + /// normally fail. This option indicates, however, that it will instead fall + /// back to showing the full id of the commit. + pub fn show_commit_oid_as_fallback(&mut self, show: bool) -> &mut Self { + self.raw.show_commit_oid_as_fallback = show as c_int; + self + } + + #[allow(missing_docs)] + pub fn pattern(&mut self, pattern: &str) -> &mut Self { + self.pattern = CString::new(pattern).unwrap(); + self.raw.pattern = self.pattern.as_ptr(); + self + } +} + +impl Binding for DescribeOptions { + type Raw = *mut raw::git_describe_options; + + unsafe fn from_raw(_raw: *mut raw::git_describe_options) -> DescribeOptions { + panic!("unimplemened") + } + fn raw(&self) -> *mut raw::git_describe_options { + &self.raw as *const _ as *mut _ + } +} + +#[cfg(test)] +mod tests { + use crate::DescribeOptions; + + #[test] + fn smoke() { + let (_td, repo) = crate::test::repo_init(); + let head = t!(repo.head()).target().unwrap(); + + let d = t!(repo.describe(DescribeOptions::new().show_commit_oid_as_fallback(true))); + let id = head.to_string(); + assert_eq!(t!(d.format(None)), &id[..7]); + + let obj = t!(repo.find_object(head, None)); + let sig = t!(repo.signature()); + t!(repo.tag("foo", &obj, &sig, "message", true)); + let d = t!(repo.describe(&DescribeOptions::new())); + assert_eq!(t!(d.format(None)), "foo"); + + let d = t!(obj.describe(&DescribeOptions::new())); + assert_eq!(t!(d.format(None)), "foo"); + } +} diff --git a/extra/git2/src/diff.rs b/extra/git2/src/diff.rs new file mode 100644 index 000000000..16595509d --- /dev/null +++ b/extra/git2/src/diff.rs @@ -0,0 +1,1863 @@ +use libc::{c_char, c_int, c_void, size_t}; +use std::ffi::CString; +use std::iter::FusedIterator; +use std::marker; +use std::mem; +use std::ops::Range; +use std::path::Path; +use std::ptr; +use std::slice; + +use crate::util::{self, Binding}; +use crate::{panic, raw, Buf, Delta, DiffFormat, Error, FileMode, Oid, Repository}; +use crate::{DiffFlags, DiffStatsFormat, IntoCString}; + +/// The diff object that contains all individual file deltas. +/// +/// This is an opaque structure which will be allocated by one of the diff +/// generator functions on the `Repository` structure (e.g. `diff_tree_to_tree` +/// or other `diff_*` functions). +pub struct Diff<'repo> { + raw: *mut raw::git_diff, + _marker: marker::PhantomData<&'repo Repository>, +} + +unsafe impl<'repo> Send for Diff<'repo> {} + +/// Description of changes to one entry. +pub struct DiffDelta<'a> { + raw: *mut raw::git_diff_delta, + _marker: marker::PhantomData<&'a raw::git_diff_delta>, +} + +/// Description of one side of a delta. +/// +/// Although this is called a "file" it could represent a file, a symbolic +/// link, a submodule commit id, or even a tree (although that only happens if +/// you are tracking type changes or ignored/untracked directories). +pub struct DiffFile<'a> { + raw: *const raw::git_diff_file, + _marker: marker::PhantomData<&'a raw::git_diff_file>, +} + +/// Structure describing options about how the diff should be executed. +pub struct DiffOptions { + pathspec: Vec<CString>, + pathspec_ptrs: Vec<*const c_char>, + old_prefix: Option<CString>, + new_prefix: Option<CString>, + raw: raw::git_diff_options, +} + +/// Control behavior of rename and copy detection +pub struct DiffFindOptions { + raw: raw::git_diff_find_options, +} + +/// Control behavior of formatting emails +pub struct DiffFormatEmailOptions { + raw: raw::git_diff_format_email_options, +} + +/// Control behavior of formatting emails +pub struct DiffPatchidOptions { + raw: raw::git_diff_patchid_options, +} + +/// An iterator over the diffs in a delta +pub struct Deltas<'diff> { + range: Range<usize>, + diff: &'diff Diff<'diff>, +} + +/// Structure describing a line (or data span) of a diff. +pub struct DiffLine<'a> { + raw: *const raw::git_diff_line, + _marker: marker::PhantomData<&'a raw::git_diff_line>, +} + +/// Structure describing a hunk of a diff. +pub struct DiffHunk<'a> { + raw: *const raw::git_diff_hunk, + _marker: marker::PhantomData<&'a raw::git_diff_hunk>, +} + +/// Structure describing a hunk of a diff. +pub struct DiffStats { + raw: *mut raw::git_diff_stats, +} + +/// Structure describing the binary contents of a diff. +pub struct DiffBinary<'a> { + raw: *const raw::git_diff_binary, + _marker: marker::PhantomData<&'a raw::git_diff_binary>, +} + +/// The contents of one of the files in a binary diff. +pub struct DiffBinaryFile<'a> { + raw: *const raw::git_diff_binary_file, + _marker: marker::PhantomData<&'a raw::git_diff_binary_file>, +} + +/// When producing a binary diff, the binary data returned will be +/// either the deflated full ("literal") contents of the file, or +/// the deflated binary delta between the two sides (whichever is +/// smaller). +#[derive(Copy, Clone, Debug)] +pub enum DiffBinaryKind { + /// There is no binary delta + None, + /// The binary data is the literal contents of the file + Literal, + /// The binary data is the delta from one side to the other + Delta, +} + +type PrintCb<'a> = dyn FnMut(DiffDelta<'_>, Option<DiffHunk<'_>>, DiffLine<'_>) -> bool + 'a; + +pub type FileCb<'a> = dyn FnMut(DiffDelta<'_>, f32) -> bool + 'a; +pub type BinaryCb<'a> = dyn FnMut(DiffDelta<'_>, DiffBinary<'_>) -> bool + 'a; +pub type HunkCb<'a> = dyn FnMut(DiffDelta<'_>, DiffHunk<'_>) -> bool + 'a; +pub type LineCb<'a> = dyn FnMut(DiffDelta<'_>, Option<DiffHunk<'_>>, DiffLine<'_>) -> bool + 'a; + +pub struct DiffCallbacks<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> { + pub file: Option<&'a mut FileCb<'b>>, + pub binary: Option<&'c mut BinaryCb<'d>>, + pub hunk: Option<&'e mut HunkCb<'f>>, + pub line: Option<&'g mut LineCb<'h>>, +} + +impl<'repo> Diff<'repo> { + /// Merge one diff into another. + /// + /// This merges items from the "from" list into the "self" list. The + /// resulting diff will have all items that appear in either list. + /// If an item appears in both lists, then it will be "merged" to appear + /// as if the old version was from the "onto" list and the new version + /// is from the "from" list (with the exception that if the item has a + /// pending DELETE in the middle, then it will show as deleted). + pub fn merge(&mut self, from: &Diff<'repo>) -> Result<(), Error> { + unsafe { + try_call!(raw::git_diff_merge(self.raw, &*from.raw)); + } + Ok(()) + } + + /// Returns an iterator over the deltas in this diff. + pub fn deltas(&self) -> Deltas<'_> { + let num_deltas = unsafe { raw::git_diff_num_deltas(&*self.raw) }; + Deltas { + range: 0..(num_deltas as usize), + diff: self, + } + } + + /// Return the diff delta for an entry in the diff list. + pub fn get_delta(&self, i: usize) -> Option<DiffDelta<'_>> { + unsafe { + let ptr = raw::git_diff_get_delta(&*self.raw, i as size_t); + Binding::from_raw_opt(ptr as *mut _) + } + } + + /// Check if deltas are sorted case sensitively or insensitively. + pub fn is_sorted_icase(&self) -> bool { + unsafe { raw::git_diff_is_sorted_icase(&*self.raw) == 1 } + } + + /// Iterate over a diff generating formatted text output. + /// + /// Returning `false` from the callback will terminate the iteration and + /// return an error from this function. + pub fn print<F>(&self, format: DiffFormat, mut cb: F) -> Result<(), Error> + where + F: FnMut(DiffDelta<'_>, Option<DiffHunk<'_>>, DiffLine<'_>) -> bool, + { + let mut cb: &mut PrintCb<'_> = &mut cb; + let ptr = &mut cb as *mut _; + let print: raw::git_diff_line_cb = Some(print_cb); + unsafe { + try_call!(raw::git_diff_print(self.raw, format, print, ptr as *mut _)); + Ok(()) + } + } + + /// Loop over all deltas in a diff issuing callbacks. + /// + /// Returning `false` from any callback will terminate the iteration and + /// return an error from this function. + pub fn foreach( + &self, + file_cb: &mut FileCb<'_>, + binary_cb: Option<&mut BinaryCb<'_>>, + hunk_cb: Option<&mut HunkCb<'_>>, + line_cb: Option<&mut LineCb<'_>>, + ) -> Result<(), Error> { + let mut cbs = DiffCallbacks { + file: Some(file_cb), + binary: binary_cb, + hunk: hunk_cb, + line: line_cb, + }; + let ptr = &mut cbs as *mut _; + unsafe { + let binary_cb_c: raw::git_diff_binary_cb = if cbs.binary.is_some() { + Some(binary_cb_c) + } else { + None + }; + let hunk_cb_c: raw::git_diff_hunk_cb = if cbs.hunk.is_some() { + Some(hunk_cb_c) + } else { + None + }; + let line_cb_c: raw::git_diff_line_cb = if cbs.line.is_some() { + Some(line_cb_c) + } else { + None + }; + let file_cb: raw::git_diff_file_cb = Some(file_cb_c); + try_call!(raw::git_diff_foreach( + self.raw, + file_cb, + binary_cb_c, + hunk_cb_c, + line_cb_c, + ptr as *mut _ + )); + Ok(()) + } + } + + /// Accumulate diff statistics for all patches. + pub fn stats(&self) -> Result<DiffStats, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_diff_get_stats(&mut ret, self.raw)); + Ok(Binding::from_raw(ret)) + } + } + + /// Transform a diff marking file renames, copies, etc. + /// + /// This modifies a diff in place, replacing old entries that look like + /// renames or copies with new entries reflecting those changes. This also + /// will, if requested, break modified files into add/remove pairs if the + /// amount of change is above a threshold. + pub fn find_similar(&mut self, opts: Option<&mut DiffFindOptions>) -> Result<(), Error> { + let opts = opts.map(|opts| &opts.raw); + unsafe { + try_call!(raw::git_diff_find_similar(self.raw, opts)); + } + Ok(()) + } + + /// Create an e-mail ready patch from a diff. + /// + /// Matches the format created by `git format-patch` + #[doc(hidden)] + #[deprecated(note = "refactored to `Email::from_diff` to match upstream")] + pub fn format_email( + &mut self, + patch_no: usize, + total_patches: usize, + commit: &crate::Commit<'repo>, + opts: Option<&mut DiffFormatEmailOptions>, + ) -> Result<Buf, Error> { + assert!(patch_no > 0); + assert!(patch_no <= total_patches); + let mut default = DiffFormatEmailOptions::default(); + let raw_opts = opts.map_or(&mut default.raw, |opts| &mut opts.raw); + let summary = commit.summary_bytes().unwrap(); + let mut message = commit.message_bytes(); + assert!(message.starts_with(summary)); + message = &message[summary.len()..]; + raw_opts.patch_no = patch_no; + raw_opts.total_patches = total_patches; + let id = commit.id(); + raw_opts.id = id.raw(); + raw_opts.summary = summary.as_ptr() as *const _; + raw_opts.body = message.as_ptr() as *const _; + raw_opts.author = commit.author().raw(); + let buf = Buf::new(); + #[allow(deprecated)] + unsafe { + try_call!(raw::git_diff_format_email(buf.raw(), self.raw, &*raw_opts)); + } + Ok(buf) + } + + /// Create an patch ID from a diff. + pub fn patchid(&self, opts: Option<&mut DiffPatchidOptions>) -> Result<Oid, Error> { + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_diff_patchid( + &mut raw, + self.raw, + opts.map(|o| &mut o.raw) + )); + Ok(Binding::from_raw(&raw as *const _)) + } + } + + // TODO: num_deltas_of_type, find_similar +} +impl Diff<'static> { + /// Read the contents of a git patch file into a `git_diff` object. + /// + /// The diff object produced is similar to the one that would be + /// produced if you actually produced it computationally by comparing + /// two trees, however there may be subtle differences. For example, + /// a patch file likely contains abbreviated object IDs, so the + /// object IDs parsed by this function will also be abbreviated. + pub fn from_buffer(buffer: &[u8]) -> Result<Diff<'static>, Error> { + crate::init(); + let mut diff: *mut raw::git_diff = std::ptr::null_mut(); + unsafe { + // NOTE: Doesn't depend on repo, so lifetime can be 'static + try_call!(raw::git_diff_from_buffer( + &mut diff, + buffer.as_ptr() as *const c_char, + buffer.len() + )); + Ok(Diff::from_raw(diff)) + } + } +} + +pub extern "C" fn print_cb( + delta: *const raw::git_diff_delta, + hunk: *const raw::git_diff_hunk, + line: *const raw::git_diff_line, + data: *mut c_void, +) -> c_int { + unsafe { + let delta = Binding::from_raw(delta as *mut _); + let hunk = Binding::from_raw_opt(hunk); + let line = Binding::from_raw(line); + + let r = panic::wrap(|| { + let data = data as *mut &mut PrintCb<'_>; + (*data)(delta, hunk, line) + }); + if r == Some(true) { + raw::GIT_OK + } else { + raw::GIT_EUSER + } + } +} + +pub extern "C" fn file_cb_c( + delta: *const raw::git_diff_delta, + progress: f32, + data: *mut c_void, +) -> c_int { + unsafe { + let delta = Binding::from_raw(delta as *mut _); + + let r = panic::wrap(|| { + let cbs = data as *mut DiffCallbacks<'_, '_, '_, '_, '_, '_, '_, '_>; + match (*cbs).file { + Some(ref mut cb) => cb(delta, progress), + None => false, + } + }); + if r == Some(true) { + raw::GIT_OK + } else { + raw::GIT_EUSER + } + } +} + +pub extern "C" fn binary_cb_c( + delta: *const raw::git_diff_delta, + binary: *const raw::git_diff_binary, + data: *mut c_void, +) -> c_int { + unsafe { + let delta = Binding::from_raw(delta as *mut _); + let binary = Binding::from_raw(binary); + + let r = panic::wrap(|| { + let cbs = data as *mut DiffCallbacks<'_, '_, '_, '_, '_, '_, '_, '_>; + match (*cbs).binary { + Some(ref mut cb) => cb(delta, binary), + None => false, + } + }); + if r == Some(true) { + raw::GIT_OK + } else { + raw::GIT_EUSER + } + } +} + +pub extern "C" fn hunk_cb_c( + delta: *const raw::git_diff_delta, + hunk: *const raw::git_diff_hunk, + data: *mut c_void, +) -> c_int { + unsafe { + let delta = Binding::from_raw(delta as *mut _); + let hunk = Binding::from_raw(hunk); + + let r = panic::wrap(|| { + let cbs = data as *mut DiffCallbacks<'_, '_, '_, '_, '_, '_, '_, '_>; + match (*cbs).hunk { + Some(ref mut cb) => cb(delta, hunk), + None => false, + } + }); + if r == Some(true) { + raw::GIT_OK + } else { + raw::GIT_EUSER + } + } +} + +pub extern "C" fn line_cb_c( + delta: *const raw::git_diff_delta, + hunk: *const raw::git_diff_hunk, + line: *const raw::git_diff_line, + data: *mut c_void, +) -> c_int { + unsafe { + let delta = Binding::from_raw(delta as *mut _); + let hunk = Binding::from_raw_opt(hunk); + let line = Binding::from_raw(line); + + let r = panic::wrap(|| { + let cbs = data as *mut DiffCallbacks<'_, '_, '_, '_, '_, '_, '_, '_>; + match (*cbs).line { + Some(ref mut cb) => cb(delta, hunk, line), + None => false, + } + }); + if r == Some(true) { + raw::GIT_OK + } else { + raw::GIT_EUSER + } + } +} + +impl<'repo> Binding for Diff<'repo> { + type Raw = *mut raw::git_diff; + unsafe fn from_raw(raw: *mut raw::git_diff) -> Diff<'repo> { + Diff { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_diff { + self.raw + } +} + +impl<'repo> Drop for Diff<'repo> { + fn drop(&mut self) { + unsafe { raw::git_diff_free(self.raw) } + } +} + +impl<'a> DiffDelta<'a> { + /// Returns the flags on the delta. + /// + /// For more information, see `DiffFlags`'s documentation. + pub fn flags(&self) -> DiffFlags { + let flags = unsafe { (*self.raw).flags }; + let mut result = DiffFlags::empty(); + + #[cfg(target_env = "msvc")] + fn as_u32(flag: i32) -> u32 { + flag as u32 + } + #[cfg(not(target_env = "msvc"))] + fn as_u32(flag: u32) -> u32 { + flag + } + + if (flags & as_u32(raw::GIT_DIFF_FLAG_BINARY)) != 0 { + result |= DiffFlags::BINARY; + } + if (flags & as_u32(raw::GIT_DIFF_FLAG_NOT_BINARY)) != 0 { + result |= DiffFlags::NOT_BINARY; + } + if (flags & as_u32(raw::GIT_DIFF_FLAG_VALID_ID)) != 0 { + result |= DiffFlags::VALID_ID; + } + if (flags & as_u32(raw::GIT_DIFF_FLAG_EXISTS)) != 0 { + result |= DiffFlags::EXISTS; + } + result + } + + // TODO: expose when diffs are more exposed + // pub fn similarity(&self) -> u16 { + // unsafe { (*self.raw).similarity } + // } + + /// Returns the number of files in this delta. + pub fn nfiles(&self) -> u16 { + unsafe { (*self.raw).nfiles } + } + + /// Returns the status of this entry + /// + /// For more information, see `Delta`'s documentation + pub fn status(&self) -> Delta { + match unsafe { (*self.raw).status } { + raw::GIT_DELTA_UNMODIFIED => Delta::Unmodified, + raw::GIT_DELTA_ADDED => Delta::Added, + raw::GIT_DELTA_DELETED => Delta::Deleted, + raw::GIT_DELTA_MODIFIED => Delta::Modified, + raw::GIT_DELTA_RENAMED => Delta::Renamed, + raw::GIT_DELTA_COPIED => Delta::Copied, + raw::GIT_DELTA_IGNORED => Delta::Ignored, + raw::GIT_DELTA_UNTRACKED => Delta::Untracked, + raw::GIT_DELTA_TYPECHANGE => Delta::Typechange, + raw::GIT_DELTA_UNREADABLE => Delta::Unreadable, + raw::GIT_DELTA_CONFLICTED => Delta::Conflicted, + n => panic!("unknown diff status: {}", n), + } + } + + /// Return the file which represents the "from" side of the diff. + /// + /// What side this means depends on the function that was used to generate + /// the diff and will be documented on the function itself. + pub fn old_file(&self) -> DiffFile<'a> { + unsafe { Binding::from_raw(&(*self.raw).old_file as *const _) } + } + + /// Return the file which represents the "to" side of the diff. + /// + /// What side this means depends on the function that was used to generate + /// the diff and will be documented on the function itself. + pub fn new_file(&self) -> DiffFile<'a> { + unsafe { Binding::from_raw(&(*self.raw).new_file as *const _) } + } +} + +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, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_diff_delta { + self.raw + } +} + +impl<'a> std::fmt::Debug for DiffDelta<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_struct("DiffDelta") + .field("nfiles", &self.nfiles()) + .field("status", &self.status()) + .field("old_file", &self.old_file()) + .field("new_file", &self.new_file()) + .finish() + } +} + +impl<'a> DiffFile<'a> { + /// Returns the Oid of this item. + /// + /// If this entry represents an absent side of a diff (e.g. the `old_file` + /// of a `Added` delta), then the oid returned will be zeroes. + pub fn id(&self) -> Oid { + unsafe { Binding::from_raw(&(*self.raw).id as *const _) } + } + + /// Returns the path, in bytes, of the entry relative to the working + /// directory of the repository. + pub fn path_bytes(&self) -> Option<&'a [u8]> { + static FOO: () = (); + unsafe { crate::opt_bytes(&FOO, (*self.raw).path) } + } + + /// Returns the path of the entry relative to the working directory of the + /// repository. + pub fn path(&self) -> Option<&'a Path> { + self.path_bytes().map(util::bytes2path) + } + + /// Returns the size of this entry, in bytes + pub fn size(&self) -> u64 { + unsafe { (*self.raw).size as u64 } + } + + /// Returns `true` if file(s) are treated as binary data. + pub fn is_binary(&self) -> bool { + unsafe { (*self.raw).flags & raw::GIT_DIFF_FLAG_BINARY as u32 != 0 } + } + + /// Returns `true` if file(s) are treated as text data. + pub fn is_not_binary(&self) -> bool { + unsafe { (*self.raw).flags & raw::GIT_DIFF_FLAG_NOT_BINARY as u32 != 0 } + } + + /// Returns `true` if `id` value is known correct. + pub fn is_valid_id(&self) -> bool { + unsafe { (*self.raw).flags & raw::GIT_DIFF_FLAG_VALID_ID as u32 != 0 } + } + + /// Returns `true` if file exists at this side of the delta. + pub fn exists(&self) -> bool { + unsafe { (*self.raw).flags & raw::GIT_DIFF_FLAG_EXISTS as u32 != 0 } + } + + /// Returns file mode. + pub fn mode(&self) -> FileMode { + match unsafe { (*self.raw).mode.into() } { + raw::GIT_FILEMODE_UNREADABLE => FileMode::Unreadable, + raw::GIT_FILEMODE_TREE => FileMode::Tree, + raw::GIT_FILEMODE_BLOB => FileMode::Blob, + raw::GIT_FILEMODE_BLOB_GROUP_WRITABLE => FileMode::BlobGroupWritable, + raw::GIT_FILEMODE_BLOB_EXECUTABLE => FileMode::BlobExecutable, + raw::GIT_FILEMODE_LINK => FileMode::Link, + raw::GIT_FILEMODE_COMMIT => FileMode::Commit, + mode => panic!("unknown mode: {}", mode), + } + } +} + +impl<'a> Binding for DiffFile<'a> { + type Raw = *const raw::git_diff_file; + unsafe fn from_raw(raw: *const raw::git_diff_file) -> DiffFile<'a> { + DiffFile { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *const raw::git_diff_file { + self.raw + } +} + +impl<'a> std::fmt::Debug for DiffFile<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut ds = f.debug_struct("DiffFile"); + ds.field("id", &self.id()); + if let Some(path_bytes) = &self.path_bytes() { + ds.field("path_bytes", path_bytes); + } + if let Some(path) = &self.path() { + ds.field("path", path); + } + ds.field("size", &self.size()).finish() + } +} + +impl Default for DiffOptions { + fn default() -> Self { + Self::new() + } +} + +impl DiffOptions { + /// Creates a new set of empty diff options. + /// + /// All flags and other options are defaulted to false or their otherwise + /// zero equivalents. + pub fn new() -> DiffOptions { + let mut opts = DiffOptions { + pathspec: Vec::new(), + pathspec_ptrs: Vec::new(), + raw: unsafe { mem::zeroed() }, + old_prefix: None, + new_prefix: None, + }; + assert_eq!(unsafe { raw::git_diff_init_options(&mut opts.raw, 1) }, 0); + opts + } + + fn flag(&mut self, opt: raw::git_diff_option_t, val: bool) -> &mut DiffOptions { + let opt = opt as u32; + if val { + self.raw.flags |= opt; + } else { + self.raw.flags &= !opt; + } + self + } + + /// Flag indicating whether the sides of the diff will be reversed. + pub fn reverse(&mut self, reverse: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_REVERSE, reverse) + } + + /// Flag indicating whether ignored files are included. + pub fn include_ignored(&mut self, include: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_INCLUDE_IGNORED, include) + } + + /// Flag indicating whether ignored directories are traversed deeply or not. + pub fn recurse_ignored_dirs(&mut self, recurse: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_RECURSE_IGNORED_DIRS, recurse) + } + + /// Flag indicating whether untracked files are in the diff + pub fn include_untracked(&mut self, include: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_INCLUDE_UNTRACKED, include) + } + + /// Flag indicating whether untracked directories are traversed deeply or + /// not. + pub fn recurse_untracked_dirs(&mut self, recurse: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_RECURSE_UNTRACKED_DIRS, recurse) + } + + /// Flag indicating whether unmodified files are in the diff. + pub fn include_unmodified(&mut self, include: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_INCLUDE_UNMODIFIED, include) + } + + /// If enabled, then Typechange delta records are generated. + pub fn include_typechange(&mut self, include: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_INCLUDE_TYPECHANGE, include) + } + + /// Event with `include_typechange`, the tree returned generally shows a + /// deleted blob. This flag correctly labels the tree transitions as a + /// typechange record with the `new_file`'s mode set to tree. + /// + /// Note that the tree SHA will not be available. + pub fn include_typechange_trees(&mut self, include: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_INCLUDE_TYPECHANGE_TREES, include) + } + + /// Flag indicating whether file mode changes are ignored. + pub fn ignore_filemode(&mut self, ignore: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_IGNORE_FILEMODE, ignore) + } + + /// Flag indicating whether all submodules should be treated as unmodified. + pub fn ignore_submodules(&mut self, ignore: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_IGNORE_SUBMODULES, ignore) + } + + /// Flag indicating whether case insensitive filenames should be used. + pub fn ignore_case(&mut self, ignore: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_IGNORE_CASE, ignore) + } + + /// If pathspecs are specified, this flag means that they should be applied + /// as an exact match instead of a fnmatch pattern. + pub fn disable_pathspec_match(&mut self, disable: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_DISABLE_PATHSPEC_MATCH, disable) + } + + /// Disable updating the `binary` flag in delta records. This is useful when + /// iterating over a diff if you don't need hunk and data callbacks and want + /// to avoid having to load a file completely. + pub fn skip_binary_check(&mut self, skip: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_SKIP_BINARY_CHECK, skip) + } + + /// When diff finds an untracked directory, to match the behavior of core + /// Git, it scans the contents for ignored and untracked files. If all + /// contents are ignored, then the directory is ignored; if any contents are + /// not ignored, then the directory is untracked. This is extra work that + /// may not matter in many cases. + /// + /// This flag turns off that scan and immediately labels an untracked + /// directory as untracked (changing the behavior to not match core git). + pub fn enable_fast_untracked_dirs(&mut self, enable: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_ENABLE_FAST_UNTRACKED_DIRS, enable) + } + + /// When diff finds a file in the working directory with stat information + /// different from the index, but the OID ends up being the same, write the + /// correct stat information into the index. Note: without this flag, diff + /// will always leave the index untouched. + pub fn update_index(&mut self, update: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_UPDATE_INDEX, update) + } + + /// Include unreadable files in the diff + pub fn include_unreadable(&mut self, include: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_INCLUDE_UNREADABLE, include) + } + + /// Include unreadable files in the diff as untracked files + pub fn include_unreadable_as_untracked(&mut self, include: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_INCLUDE_UNREADABLE_AS_UNTRACKED, include) + } + + /// Treat all files as text, disabling binary attributes and detection. + pub fn force_text(&mut self, force: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_FORCE_TEXT, force) + } + + /// Treat all files as binary, disabling text diffs + pub fn force_binary(&mut self, force: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_FORCE_BINARY, force) + } + + /// Ignore all whitespace + pub fn ignore_whitespace(&mut self, ignore: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_IGNORE_WHITESPACE, ignore) + } + + /// Ignore changes in the amount of whitespace + pub fn ignore_whitespace_change(&mut self, ignore: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_IGNORE_WHITESPACE_CHANGE, ignore) + } + + /// Ignore whitespace at the end of line + pub fn ignore_whitespace_eol(&mut self, ignore: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_IGNORE_WHITESPACE_EOL, ignore) + } + + /// Ignore blank lines + pub fn ignore_blank_lines(&mut self, ignore: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_IGNORE_BLANK_LINES, ignore) + } + + /// When generating patch text, include the content of untracked files. + /// + /// This automatically turns on `include_untracked` but it does not turn on + /// `recurse_untracked_dirs`. Add that flag if you want the content of every + /// single untracked file. + pub fn show_untracked_content(&mut self, show: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_SHOW_UNTRACKED_CONTENT, show) + } + + /// When generating output, include the names of unmodified files if they + /// are included in the `Diff`. Normally these are skipped in the formats + /// that list files (e.g. name-only, name-status, raw). Even with this these + /// will not be included in the patch format. + pub fn show_unmodified(&mut self, show: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_SHOW_UNMODIFIED, show) + } + + /// Use the "patience diff" algorithm + pub fn patience(&mut self, patience: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_PATIENCE, patience) + } + + /// Take extra time to find the minimal diff + pub fn minimal(&mut self, minimal: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_MINIMAL, minimal) + } + + /// Include the necessary deflate/delta information so that `git-apply` can + /// apply given diff information to binary files. + pub fn show_binary(&mut self, show: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_SHOW_BINARY, show) + } + + /// Use a heuristic that takes indentation and whitespace into account + /// which generally can produce better diffs when dealing with ambiguous + /// diff hunks. + pub fn indent_heuristic(&mut self, heuristic: bool) -> &mut DiffOptions { + self.flag(raw::GIT_DIFF_INDENT_HEURISTIC, heuristic) + } + + /// Set the number of unchanged lines that define the boundary of a hunk + /// (and to display before and after). + /// + /// The default value for this is 3. + pub fn context_lines(&mut self, lines: u32) -> &mut DiffOptions { + self.raw.context_lines = lines; + self + } + + /// Set the maximum number of unchanged lines between hunk boundaries before + /// the hunks will be merged into one. + /// + /// The default value for this is 0. + pub fn interhunk_lines(&mut self, lines: u32) -> &mut DiffOptions { + self.raw.interhunk_lines = lines; + self + } + + /// The default value for this is `core.abbrev` or 7 if unset. + pub fn id_abbrev(&mut self, abbrev: u16) -> &mut DiffOptions { + self.raw.id_abbrev = abbrev; + self + } + + /// Maximum size (in bytes) above which a blob will be marked as binary + /// automatically. + /// + /// A negative value will disable this entirely. + /// + /// The default value for this is 512MB. + pub fn max_size(&mut self, size: i64) -> &mut DiffOptions { + self.raw.max_size = size as raw::git_off_t; + self + } + + /// The virtual "directory" to prefix old file names with in hunk headers. + /// + /// The default value for this is "a". + pub fn old_prefix<T: IntoCString>(&mut self, t: T) -> &mut DiffOptions { + self.old_prefix = Some(t.into_c_string().unwrap()); + self + } + + /// The virtual "directory" to prefix new file names with in hunk headers. + /// + /// The default value for this is "b". + pub fn new_prefix<T: IntoCString>(&mut self, t: T) -> &mut DiffOptions { + self.new_prefix = Some(t.into_c_string().unwrap()); + self + } + + /// Add to the array of paths/fnmatch patterns to constrain the diff. + pub fn pathspec<T: IntoCString>(&mut self, pathspec: T) -> &mut DiffOptions { + let s = util::cstring_to_repo_path(pathspec).unwrap(); + self.pathspec_ptrs.push(s.as_ptr()); + self.pathspec.push(s); + self + } + + /// 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_diff_options { + self.raw.old_prefix = self + .old_prefix + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(ptr::null()); + self.raw.new_prefix = self + .new_prefix + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(ptr::null()); + self.raw.pathspec.count = self.pathspec_ptrs.len() as size_t; + self.raw.pathspec.strings = self.pathspec_ptrs.as_ptr() as *mut _; + &self.raw as *const _ + } + + // TODO: expose ignore_submodules, notify_cb/notify_payload +} + +impl<'diff> Iterator for Deltas<'diff> { + type Item = DiffDelta<'diff>; + fn next(&mut self) -> Option<DiffDelta<'diff>> { + self.range.next().and_then(|i| self.diff.get_delta(i)) + } + fn size_hint(&self) -> (usize, Option<usize>) { + self.range.size_hint() + } +} +impl<'diff> DoubleEndedIterator for Deltas<'diff> { + fn next_back(&mut self) -> Option<DiffDelta<'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. +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum DiffLineType { + /// These values will be sent to `git_diff_line_cb` along with the line + Context, + /// + Addition, + /// + Deletion, + /// Both files have no LF at end + ContextEOFNL, + /// Old has no LF at end, new does + AddEOFNL, + /// Old has LF at end, new does not + DeleteEOFNL, + /// The following values will only be sent to a `git_diff_line_cb` when + /// the content of a diff is being formatted through `git_diff_print`. + FileHeader, + /// + HunkHeader, + /// For "Binary files x and y differ" + Binary, +} + +impl Binding for DiffLineType { + type Raw = raw::git_diff_line_t; + unsafe fn from_raw(raw: raw::git_diff_line_t) -> Self { + match raw { + raw::GIT_DIFF_LINE_CONTEXT => DiffLineType::Context, + raw::GIT_DIFF_LINE_ADDITION => DiffLineType::Addition, + raw::GIT_DIFF_LINE_DELETION => DiffLineType::Deletion, + raw::GIT_DIFF_LINE_CONTEXT_EOFNL => DiffLineType::ContextEOFNL, + raw::GIT_DIFF_LINE_ADD_EOFNL => DiffLineType::AddEOFNL, + raw::GIT_DIFF_LINE_DEL_EOFNL => DiffLineType::DeleteEOFNL, + raw::GIT_DIFF_LINE_FILE_HDR => DiffLineType::FileHeader, + raw::GIT_DIFF_LINE_HUNK_HDR => DiffLineType::HunkHeader, + raw::GIT_DIFF_LINE_BINARY => DiffLineType::Binary, + _ => panic!("Unknown git diff line type"), + } + } + fn raw(&self) -> raw::git_diff_line_t { + match *self { + DiffLineType::Context => raw::GIT_DIFF_LINE_CONTEXT, + DiffLineType::Addition => raw::GIT_DIFF_LINE_ADDITION, + DiffLineType::Deletion => raw::GIT_DIFF_LINE_DELETION, + DiffLineType::ContextEOFNL => raw::GIT_DIFF_LINE_CONTEXT_EOFNL, + DiffLineType::AddEOFNL => raw::GIT_DIFF_LINE_ADD_EOFNL, + DiffLineType::DeleteEOFNL => raw::GIT_DIFF_LINE_DEL_EOFNL, + DiffLineType::FileHeader => raw::GIT_DIFF_LINE_FILE_HDR, + DiffLineType::HunkHeader => raw::GIT_DIFF_LINE_HUNK_HDR, + DiffLineType::Binary => raw::GIT_DIFF_LINE_BINARY, + } + } +} + +impl<'a> DiffLine<'a> { + /// Line number in old file or `None` for added line + pub fn old_lineno(&self) -> Option<u32> { + match unsafe { (*self.raw).old_lineno } { + n if n < 0 => None, + n => Some(n as u32), + } + } + + /// Line number in new file or `None` for deleted line + pub fn new_lineno(&self) -> Option<u32> { + match unsafe { (*self.raw).new_lineno } { + n if n < 0 => None, + n => Some(n as u32), + } + } + + /// Number of newline characters in content + pub fn num_lines(&self) -> u32 { + unsafe { (*self.raw).num_lines as u32 } + } + + /// Offset in the original file to the content + pub fn content_offset(&self) -> i64 { + unsafe { (*self.raw).content_offset as i64 } + } + + /// Content of this line as bytes. + pub fn content(&self) -> &'a [u8] { + unsafe { + slice::from_raw_parts( + (*self.raw).content as *const u8, + (*self.raw).content_len as usize, + ) + } + } + + /// origin of this `DiffLine`. + /// + pub fn origin_value(&self) -> DiffLineType { + unsafe { Binding::from_raw((*self.raw).origin as raw::git_diff_line_t) } + } + + /// Sigil showing the origin of this `DiffLine`. + /// + /// * ` ` - Line context + /// * `+` - Line addition + /// * `-` - Line deletion + /// * `=` - Context (End of file) + /// * `>` - Add (End of file) + /// * `<` - Remove (End of file) + /// * `F` - File header + /// * `H` - Hunk header + /// * `B` - Line binary + pub fn origin(&self) -> char { + match unsafe { (*self.raw).origin as raw::git_diff_line_t } { + raw::GIT_DIFF_LINE_CONTEXT => ' ', + raw::GIT_DIFF_LINE_ADDITION => '+', + raw::GIT_DIFF_LINE_DELETION => '-', + raw::GIT_DIFF_LINE_CONTEXT_EOFNL => '=', + raw::GIT_DIFF_LINE_ADD_EOFNL => '>', + raw::GIT_DIFF_LINE_DEL_EOFNL => '<', + raw::GIT_DIFF_LINE_FILE_HDR => 'F', + raw::GIT_DIFF_LINE_HUNK_HDR => 'H', + raw::GIT_DIFF_LINE_BINARY => 'B', + _ => ' ', + } + } +} + +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, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *const raw::git_diff_line { + self.raw + } +} + +impl<'a> std::fmt::Debug for DiffLine<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut ds = f.debug_struct("DiffLine"); + if let Some(old_lineno) = &self.old_lineno() { + ds.field("old_lineno", old_lineno); + } + if let Some(new_lineno) = &self.new_lineno() { + ds.field("new_lineno", new_lineno); + } + ds.field("num_lines", &self.num_lines()) + .field("content_offset", &self.content_offset()) + .field("content", &self.content()) + .field("origin", &self.origin()) + .finish() + } +} + +impl<'a> DiffHunk<'a> { + /// Starting line number in old_file + pub fn old_start(&self) -> u32 { + unsafe { (*self.raw).old_start as u32 } + } + + /// Number of lines in old_file + pub fn old_lines(&self) -> u32 { + unsafe { (*self.raw).old_lines as u32 } + } + + /// Starting line number in new_file + pub fn new_start(&self) -> u32 { + unsafe { (*self.raw).new_start as u32 } + } + + /// Number of lines in new_file + pub fn new_lines(&self) -> u32 { + unsafe { (*self.raw).new_lines as u32 } + } + + /// Header text + pub fn header(&self) -> &'a [u8] { + unsafe { + slice::from_raw_parts( + (*self.raw).header.as_ptr() as *const u8, + (*self.raw).header_len as usize, + ) + } + } +} + +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, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *const raw::git_diff_hunk { + self.raw + } +} + +impl<'a> std::fmt::Debug for DiffHunk<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_struct("DiffHunk") + .field("old_start", &self.old_start()) + .field("old_lines", &self.old_lines()) + .field("new_start", &self.new_start()) + .field("new_lines", &self.new_lines()) + .field("header", &self.header()) + .finish() + } +} + +impl DiffStats { + /// Get the total number of files changed in a diff. + pub fn files_changed(&self) -> usize { + unsafe { raw::git_diff_stats_files_changed(&*self.raw) as usize } + } + + /// Get the total number of insertions in a diff + pub fn insertions(&self) -> usize { + unsafe { raw::git_diff_stats_insertions(&*self.raw) as usize } + } + + /// Get the total number of deletions in a diff + pub fn deletions(&self) -> usize { + unsafe { raw::git_diff_stats_deletions(&*self.raw) as usize } + } + + /// Print diff statistics to a Buf + pub fn to_buf(&self, format: DiffStatsFormat, width: usize) -> Result<Buf, Error> { + let buf = Buf::new(); + unsafe { + try_call!(raw::git_diff_stats_to_buf( + buf.raw(), + self.raw, + format.bits(), + width as size_t + )); + } + Ok(buf) + } +} + +impl Binding for DiffStats { + type Raw = *mut raw::git_diff_stats; + + unsafe fn from_raw(raw: *mut raw::git_diff_stats) -> DiffStats { + DiffStats { raw } + } + fn raw(&self) -> *mut raw::git_diff_stats { + self.raw + } +} + +impl Drop for DiffStats { + fn drop(&mut self) { + unsafe { raw::git_diff_stats_free(self.raw) } + } +} + +impl std::fmt::Debug for DiffStats { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_struct("DiffStats") + .field("files_changed", &self.files_changed()) + .field("insertions", &self.insertions()) + .field("deletions", &self.deletions()) + .finish() + } +} + +impl<'a> DiffBinary<'a> { + /// Returns whether there is data in this binary structure or not. + /// + /// If this is `true`, then this was produced and included binary content. + /// If this is `false` then this was generated knowing only that a binary + /// file changed but without providing the data, probably from a patch that + /// said `Binary files a/file.txt and b/file.txt differ`. + pub fn contains_data(&self) -> bool { + unsafe { (*self.raw).contains_data == 1 } + } + + /// The contents of the old file. + pub fn old_file(&self) -> DiffBinaryFile<'a> { + unsafe { Binding::from_raw(&(*self.raw).old_file as *const _) } + } + + /// The contents of the new file. + pub fn new_file(&self) -> DiffBinaryFile<'a> { + unsafe { Binding::from_raw(&(*self.raw).new_file as *const _) } + } +} + +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, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *const raw::git_diff_binary { + self.raw + } +} + +impl<'a> DiffBinaryFile<'a> { + /// The type of binary data for this file + pub fn kind(&self) -> DiffBinaryKind { + unsafe { Binding::from_raw((*self.raw).kind) } + } + + /// The binary data, deflated + pub fn data(&self) -> &[u8] { + unsafe { + slice::from_raw_parts((*self.raw).data as *const u8, (*self.raw).datalen as usize) + } + } + + /// The length of the binary data after inflation + pub fn inflated_len(&self) -> usize { + unsafe { (*self.raw).inflatedlen as usize } + } +} + +impl<'a> Binding for DiffBinaryFile<'a> { + type Raw = *const raw::git_diff_binary_file; + unsafe fn from_raw(raw: *const raw::git_diff_binary_file) -> DiffBinaryFile<'a> { + DiffBinaryFile { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *const raw::git_diff_binary_file { + self.raw + } +} + +impl Binding for DiffBinaryKind { + type Raw = raw::git_diff_binary_t; + unsafe fn from_raw(raw: raw::git_diff_binary_t) -> DiffBinaryKind { + match raw { + raw::GIT_DIFF_BINARY_NONE => DiffBinaryKind::None, + raw::GIT_DIFF_BINARY_LITERAL => DiffBinaryKind::Literal, + raw::GIT_DIFF_BINARY_DELTA => DiffBinaryKind::Delta, + _ => panic!("Unknown git diff binary kind"), + } + } + fn raw(&self) -> raw::git_diff_binary_t { + match *self { + DiffBinaryKind::None => raw::GIT_DIFF_BINARY_NONE, + DiffBinaryKind::Literal => raw::GIT_DIFF_BINARY_LITERAL, + DiffBinaryKind::Delta => raw::GIT_DIFF_BINARY_DELTA, + } + } +} + +impl Default for DiffFindOptions { + fn default() -> Self { + Self::new() + } +} + +impl DiffFindOptions { + /// Creates a new set of empty diff find options. + /// + /// All flags and other options are defaulted to false or their otherwise + /// zero equivalents. + pub fn new() -> DiffFindOptions { + let mut opts = DiffFindOptions { + raw: unsafe { mem::zeroed() }, + }; + assert_eq!( + unsafe { raw::git_diff_find_init_options(&mut opts.raw, 1) }, + 0 + ); + opts + } + + fn flag(&mut self, opt: u32, val: bool) -> &mut DiffFindOptions { + if val { + self.raw.flags |= opt; + } else { + self.raw.flags &= !opt; + } + self + } + + /// Reset all flags back to their unset state, indicating that + /// `diff.renames` should be used instead. This is overridden once any flag + /// is set. + pub fn by_config(&mut self) -> &mut DiffFindOptions { + self.flag(0xffffffff, false) + } + + /// Look for renames? + pub fn renames(&mut self, find: bool) -> &mut DiffFindOptions { + self.flag(raw::GIT_DIFF_FIND_RENAMES, find) + } + + /// Consider old side of modified for renames? + pub fn renames_from_rewrites(&mut self, find: bool) -> &mut DiffFindOptions { + self.flag(raw::GIT_DIFF_FIND_RENAMES_FROM_REWRITES, find) + } + + /// Look for copies? + pub fn copies(&mut self, find: bool) -> &mut DiffFindOptions { + self.flag(raw::GIT_DIFF_FIND_COPIES, find) + } + + /// Consider unmodified as copy sources? + /// + /// For this to work correctly, use `include_unmodified` when the initial + /// diff is being generated. + pub fn copies_from_unmodified(&mut self, find: bool) -> &mut DiffFindOptions { + self.flag(raw::GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED, find) + } + + /// Mark significant rewrites for split. + pub fn rewrites(&mut self, find: bool) -> &mut DiffFindOptions { + self.flag(raw::GIT_DIFF_FIND_REWRITES, find) + } + + /// Actually split large rewrites into delete/add pairs + pub fn break_rewrites(&mut self, find: bool) -> &mut DiffFindOptions { + self.flag(raw::GIT_DIFF_BREAK_REWRITES, find) + } + + #[doc(hidden)] + pub fn break_rewries(&mut self, find: bool) -> &mut DiffFindOptions { + self.break_rewrites(find) + } + + /// Find renames/copies for untracked items in working directory. + /// + /// For this to work correctly use the `include_untracked` option when the + /// initial diff is being generated. + pub fn for_untracked(&mut self, find: bool) -> &mut DiffFindOptions { + self.flag(raw::GIT_DIFF_FIND_FOR_UNTRACKED, find) + } + + /// Turn on all finding features. + pub fn all(&mut self, find: bool) -> &mut DiffFindOptions { + self.flag(raw::GIT_DIFF_FIND_ALL, find) + } + + /// Measure similarity ignoring leading whitespace (default) + pub fn ignore_leading_whitespace(&mut self, ignore: bool) -> &mut DiffFindOptions { + self.flag(raw::GIT_DIFF_FIND_IGNORE_LEADING_WHITESPACE, ignore) + } + + /// Measure similarity ignoring all whitespace + pub fn ignore_whitespace(&mut self, ignore: bool) -> &mut DiffFindOptions { + self.flag(raw::GIT_DIFF_FIND_IGNORE_WHITESPACE, ignore) + } + + /// Measure similarity including all data + pub fn dont_ignore_whitespace(&mut self, dont: bool) -> &mut DiffFindOptions { + self.flag(raw::GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE, dont) + } + + /// Measure similarity only by comparing SHAs (fast and cheap) + pub fn exact_match_only(&mut self, exact: bool) -> &mut DiffFindOptions { + self.flag(raw::GIT_DIFF_FIND_EXACT_MATCH_ONLY, exact) + } + + /// Do not break rewrites unless they contribute to a rename. + /// + /// Normally, `break_rewrites` and `rewrites` will measure the + /// self-similarity of modified files and split the ones that have changed a + /// lot into a delete/add pair. Then the sides of that pair will be + /// considered candidates for rename and copy detection + /// + /// If you add this flag in and the split pair is not used for an actual + /// rename or copy, then the modified record will be restored to a regular + /// modified record instead of being split. + pub fn break_rewrites_for_renames_only(&mut self, b: bool) -> &mut DiffFindOptions { + self.flag(raw::GIT_DIFF_BREAK_REWRITES_FOR_RENAMES_ONLY, b) + } + + /// Remove any unmodified deltas after find_similar is done. + /// + /// Using `copies_from_unmodified` to emulate the `--find-copies-harder` + /// behavior requires building a diff with the `include_unmodified` flag. If + /// you do not want unmodified records in the final result, pas this flag to + /// have them removed. + pub fn remove_unmodified(&mut self, remove: bool) -> &mut DiffFindOptions { + self.flag(raw::GIT_DIFF_FIND_REMOVE_UNMODIFIED, remove) + } + + /// Similarity to consider a file renamed (default 50) + pub fn rename_threshold(&mut self, thresh: u16) -> &mut DiffFindOptions { + self.raw.rename_threshold = thresh; + self + } + + /// 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 + } + + /// Similarity to consider a file copy (default 50) + pub fn copy_threshold(&mut self, thresh: u16) -> &mut DiffFindOptions { + self.raw.copy_threshold = thresh; + self + } + + /// Similarity to split modify into delete/add pair (default 60) + pub fn break_rewrite_threshold(&mut self, thresh: u16) -> &mut DiffFindOptions { + self.raw.break_rewrite_threshold = thresh; + self + } + + /// Maximum similarity sources to examine for a file (somewhat like + /// git-diff's `-l` option or `diff.renameLimit` config) + /// + /// Defaults to 200 + pub fn rename_limit(&mut self, limit: usize) -> &mut DiffFindOptions { + self.raw.rename_limit = limit as size_t; + self + } + + // TODO: expose git_diff_similarity_metric + + /// Acquire a pointer to the underlying raw options. + pub unsafe fn raw(&mut self) -> *const raw::git_diff_find_options { + &self.raw + } +} + +impl Default for DiffFormatEmailOptions { + fn default() -> Self { + Self::new() + } +} + +impl DiffFormatEmailOptions { + /// Creates a new set of email options, + /// initialized to the default values + pub fn new() -> Self { + let mut opts = DiffFormatEmailOptions { + raw: unsafe { mem::zeroed() }, + }; + assert_eq!( + unsafe { raw::git_diff_format_email_options_init(&mut opts.raw, 1) }, + 0 + ); + opts + } + + fn flag(&mut self, opt: u32, val: bool) -> &mut Self { + if val { + self.raw.flags |= opt; + } else { + self.raw.flags &= !opt; + } + self + } + + /// Exclude `[PATCH]` from the subject header + pub fn exclude_subject_patch_header(&mut self, should_exclude: bool) -> &mut Self { + self.flag( + raw::GIT_DIFF_FORMAT_EMAIL_EXCLUDE_SUBJECT_PATCH_MARKER, + should_exclude, + ) + } +} + +impl DiffPatchidOptions { + /// Creates a new set of patchid options, + /// initialized to the default values + pub fn new() -> Self { + let mut opts = DiffPatchidOptions { + raw: unsafe { mem::zeroed() }, + }; + assert_eq!( + unsafe { + raw::git_diff_patchid_options_init( + &mut opts.raw, + raw::GIT_DIFF_PATCHID_OPTIONS_VERSION, + ) + }, + 0 + ); + opts + } +} + +#[cfg(test)] +mod tests { + use crate::{DiffLineType, DiffOptions, Oid, Signature, Time}; + use std::borrow::Borrow; + use std::fs::File; + use std::io::Write; + use std::path::Path; + + #[test] + fn smoke() { + let (_td, repo) = crate::test::repo_init(); + let diff = repo.diff_tree_to_workdir(None, None).unwrap(); + assert_eq!(diff.deltas().len(), 0); + let stats = diff.stats().unwrap(); + assert_eq!(stats.insertions(), 0); + assert_eq!(stats.deletions(), 0); + assert_eq!(stats.files_changed(), 0); + let patchid = diff.patchid(None).unwrap(); + assert_ne!(patchid, Oid::zero()); + } + + #[test] + fn foreach_smoke() { + let (_td, repo) = crate::test::repo_init(); + let diff = t!(repo.diff_tree_to_workdir(None, None)); + let mut count = 0; + t!(diff.foreach( + &mut |_file, _progress| { + count = count + 1; + true + }, + None, + None, + None + )); + assert_eq!(count, 0); + } + + #[test] + fn foreach_file_only() { + let path = Path::new("foo"); + let (td, repo) = crate::test::repo_init(); + t!(t!(File::create(&td.path().join(path))).write_all(b"bar")); + let mut opts = DiffOptions::new(); + opts.include_untracked(true); + let diff = t!(repo.diff_tree_to_workdir(None, Some(&mut opts))); + let mut count = 0; + let mut result = None; + t!(diff.foreach( + &mut |file, _progress| { + count = count + 1; + result = file.new_file().path().map(ToOwned::to_owned); + true + }, + None, + None, + None + )); + assert_eq!(result.as_ref().map(Borrow::borrow), Some(path)); + assert_eq!(count, 1); + } + + #[test] + fn foreach_file_and_hunk() { + let path = Path::new("foo"); + let (td, repo) = crate::test::repo_init(); + t!(t!(File::create(&td.path().join(path))).write_all(b"bar")); + let mut index = t!(repo.index()); + t!(index.add_path(path)); + let mut opts = DiffOptions::new(); + opts.include_untracked(true); + let diff = t!(repo.diff_tree_to_index(None, Some(&index), Some(&mut opts))); + let mut new_lines = 0; + t!(diff.foreach( + &mut |_file, _progress| { true }, + None, + Some(&mut |_file, hunk| { + new_lines = hunk.new_lines(); + true + }), + None + )); + assert_eq!(new_lines, 1); + } + + #[test] + fn foreach_all_callbacks() { + let fib = vec![0, 1, 1, 2, 3, 5, 8]; + // Verified with a node implementation of deflate, might be worth + // adding a deflate lib to do this inline here. + let deflated_fib = vec![120, 156, 99, 96, 100, 100, 98, 102, 229, 0, 0, 0, 53, 0, 21]; + let foo_path = Path::new("foo"); + let bin_path = Path::new("bin"); + let (td, repo) = crate::test::repo_init(); + t!(t!(File::create(&td.path().join(foo_path))).write_all(b"bar\n")); + t!(t!(File::create(&td.path().join(bin_path))).write_all(&fib)); + let mut index = t!(repo.index()); + t!(index.add_path(foo_path)); + t!(index.add_path(bin_path)); + let mut opts = DiffOptions::new(); + opts.include_untracked(true).show_binary(true); + let diff = t!(repo.diff_tree_to_index(None, Some(&index), Some(&mut opts))); + let mut bin_content = None; + let mut new_lines = 0; + let mut line_content = None; + t!(diff.foreach( + &mut |_file, _progress| { true }, + Some(&mut |_file, binary| { + bin_content = Some(binary.new_file().data().to_owned()); + true + }), + Some(&mut |_file, hunk| { + new_lines = hunk.new_lines(); + true + }), + Some(&mut |_file, _hunk, line| { + line_content = String::from_utf8(line.content().into()).ok(); + true + }) + )); + assert_eq!(bin_content, Some(deflated_fib)); + assert_eq!(new_lines, 1); + assert_eq!(line_content, Some("bar\n".to_string())); + } + + #[test] + fn format_email_simple() { + let (_td, repo) = crate::test::repo_init(); + const COMMIT_MESSAGE: &str = "Modify some content"; + const EXPECTED_EMAIL_START: &str = concat!( + "From f1234fb0588b6ed670779a34ba5c51ef962f285f Mon Sep 17 00:00:00 2001\n", + "From: Techcable <dummy@dummy.org>\n", + "Date: Tue, 11 Jan 1972 17:46:40 +0000\n", + "Subject: [PATCH] Modify some content\n", + "\n", + "---\n", + " file1.txt | 8 +++++---\n", + " 1 file changed, 5 insertions(+), 3 deletions(-)\n", + "\n", + "diff --git a/file1.txt b/file1.txt\n", + "index 94aaae8..af8f41d 100644\n", + "--- a/file1.txt\n", + "+++ b/file1.txt\n", + "@@ -1,15 +1,17 @@\n", + " file1.txt\n", + " file1.txt\n", + "+_file1.txt_\n", + " file1.txt\n", + " file1.txt\n", + " file1.txt\n", + " file1.txt\n", + "+\n", + "+\n", + " file1.txt\n", + " file1.txt\n", + " file1.txt\n", + " file1.txt\n", + " file1.txt\n", + "-file1.txt\n", + "-file1.txt\n", + "-file1.txt\n", + "+_file1.txt_\n", + "+_file1.txt_\n", + " file1.txt\n", + "--\n" + ); + const ORIGINAL_FILE: &str = concat!( + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n" + ); + const UPDATED_FILE: &str = concat!( + "file1.txt\n", + "file1.txt\n", + "_file1.txt_\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "\n", + "\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "file1.txt\n", + "_file1.txt_\n", + "_file1.txt_\n", + "file1.txt\n" + ); + const FILE_MODE: i32 = 0o100644; + let original_file = repo.blob(ORIGINAL_FILE.as_bytes()).unwrap(); + let updated_file = repo.blob(UPDATED_FILE.as_bytes()).unwrap(); + let mut original_tree = repo.treebuilder(None).unwrap(); + original_tree + .insert("file1.txt", original_file, FILE_MODE) + .unwrap(); + let original_tree = original_tree.write().unwrap(); + let mut updated_tree = repo.treebuilder(None).unwrap(); + updated_tree + .insert("file1.txt", updated_file, FILE_MODE) + .unwrap(); + let updated_tree = updated_tree.write().unwrap(); + let time = Time::new(64_000_000, 0); + let author = Signature::new("Techcable", "dummy@dummy.org", &time).unwrap(); + let updated_commit = repo + .commit( + None, + &author, + &author, + COMMIT_MESSAGE, + &repo.find_tree(updated_tree).unwrap(), + &[], // NOTE: Have no parents to ensure stable hash + ) + .unwrap(); + let updated_commit = repo.find_commit(updated_commit).unwrap(); + let mut diff = repo + .diff_tree_to_tree( + Some(&repo.find_tree(original_tree).unwrap()), + Some(&repo.find_tree(updated_tree).unwrap()), + None, + ) + .unwrap(); + #[allow(deprecated)] + let actual_email = diff.format_email(1, 1, &updated_commit, None).unwrap(); + let actual_email = actual_email.as_str().unwrap(); + assert!( + actual_email.starts_with(EXPECTED_EMAIL_START), + "Unexpected email:\n{}", + actual_email + ); + let mut remaining_lines = actual_email[EXPECTED_EMAIL_START.len()..].lines(); + let version_line = remaining_lines.next(); + assert!( + version_line.unwrap().starts_with("libgit2"), + "Invalid version line: {:?}", + version_line + ); + while let Some(line) = remaining_lines.next() { + assert_eq!(line.trim(), "") + } + } + + #[test] + fn foreach_diff_line_origin_value() { + let foo_path = Path::new("foo"); + let (td, repo) = crate::test::repo_init(); + t!(t!(File::create(&td.path().join(foo_path))).write_all(b"bar\n")); + let mut index = t!(repo.index()); + t!(index.add_path(foo_path)); + let mut opts = DiffOptions::new(); + opts.include_untracked(true); + let diff = t!(repo.diff_tree_to_index(None, Some(&index), Some(&mut opts))); + let mut origin_values: Vec<DiffLineType> = Vec::new(); + t!(diff.foreach( + &mut |_file, _progress| { true }, + None, + None, + Some(&mut |_file, _hunk, line| { + origin_values.push(line.origin_value()); + true + }) + )); + assert_eq!(origin_values.len(), 1); + assert_eq!(origin_values[0], DiffLineType::Addition); + } + + #[test] + fn foreach_exits_with_euser() { + let foo_path = Path::new("foo"); + let bar_path = Path::new("foo"); + + let (td, repo) = crate::test::repo_init(); + t!(t!(File::create(&td.path().join(foo_path))).write_all(b"bar\n")); + + let mut index = t!(repo.index()); + t!(index.add_path(foo_path)); + t!(index.add_path(bar_path)); + + let mut opts = DiffOptions::new(); + opts.include_untracked(true); + let diff = t!(repo.diff_tree_to_index(None, Some(&index), Some(&mut opts))); + + let mut calls = 0; + let result = diff.foreach( + &mut |_file, _progress| { + calls += 1; + false + }, + None, + None, + None, + ); + + assert_eq!(result.unwrap_err().code(), crate::ErrorCode::User); + } +} diff --git a/extra/git2/src/email.rs b/extra/git2/src/email.rs new file mode 100644 index 000000000..d3ebc0384 --- /dev/null +++ b/extra/git2/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<CString>, + 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<T: IntoCString>(&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<T: IntoCString>( + diff: &Diff<'_>, + patch_idx: usize, + patch_count: usize, + commit_id: &Oid, + summary: T, + body: T, + author: &Signature<'_>, + opts: &mut EmailCreateOptions, + ) -> Result<Self, Error> { + 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<Self, Error> { + 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/extra/git2/src/error.rs b/extra/git2/src/error.rs new file mode 100644 index 000000000..6f1c4d4c7 --- /dev/null +++ b/extra/git2/src/error.rs @@ -0,0 +1,399 @@ +use libc::c_int; +use std::env::JoinPathsError; +use std::error; +use std::ffi::{CStr, NulError}; +use std::fmt; +use std::str; + +use crate::{raw, ErrorClass, ErrorCode}; + +/// A structure to represent errors coming out of libgit2. +#[derive(Debug, PartialEq)] +pub struct Error { + code: c_int, + klass: c_int, + message: String, +} + +impl Error { + /// Creates a new error. + /// + /// This is mainly intended for implementers of custom transports or + /// database backends, where it is desirable to propagate an [`Error`] + /// through `libgit2`. + pub fn new<S: AsRef<str>>(code: ErrorCode, class: ErrorClass, message: S) -> Self { + let mut err = Error::from_str(message.as_ref()); + err.set_code(code); + err.set_class(class); + err + } + + /// Returns the last error that happened with the code specified by `code`. + /// + /// The `code` argument typically comes from the return value of a function + /// call. This code will later be returned from the `code` function. + /// + /// 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<Error> { + crate::init(); + unsafe { + // Note that whenever libgit2 returns an error any negative value + // indicates that an error happened. Auxiliary information is + // *usually* in `git_error_last` but unfortunately that's not always + // the case. Sometimes a negative error code is returned from + // libgit2 *without* calling `git_error_set` internally to configure + // the error. + // + // To handle this case and hopefully provide better error messages + // on our end we unconditionally call `git_error_clear` when we're done + // with an error. This is an attempt to clear it as aggressively as + // possible when we can to ensure that error information from one + // api invocation doesn't leak over to the next api invocation. + // + // Additionally if `git_error_last` returns null then we returned a + // canned error out. + let ptr = raw::git_error_last(); + let err = if ptr.is_null() { + let mut error = Error::from_str("an unknown git error occurred"); + error.code = code; + error + } else { + Error::from_raw(code, ptr) + }; + raw::git_error_clear(); + Some(err) + } + } + + unsafe fn from_raw(code: c_int, ptr: *const raw::git_error) -> Error { + let message = CStr::from_ptr((*ptr).message as *const _).to_bytes(); + let message = String::from_utf8_lossy(message).into_owned(); + Error { + code, + klass: (*ptr).klass, + message, + } + } + + /// Creates a new error from the given string as the error. + /// + /// The error returned will have the code `GIT_ERROR` and the class + /// `GIT_ERROR_NONE`. + pub fn from_str(s: &str) -> Error { + Error { + code: raw::GIT_ERROR as c_int, + klass: raw::GIT_ERROR_NONE as c_int, + message: s.to_string(), + } + } + + /// Return the error code associated with this error. + /// + /// An error code is intended to be programmatically actionable most of the + /// time. For example the code `GIT_EAGAIN` indicates that an error could be + /// fixed by trying again, while the code `GIT_ERROR` is more bland and + /// doesn't convey anything in particular. + pub fn code(&self) -> ErrorCode { + match self.raw_code() { + raw::GIT_OK => super::ErrorCode::GenericError, + raw::GIT_ERROR => super::ErrorCode::GenericError, + raw::GIT_ENOTFOUND => super::ErrorCode::NotFound, + raw::GIT_EEXISTS => super::ErrorCode::Exists, + raw::GIT_EAMBIGUOUS => super::ErrorCode::Ambiguous, + raw::GIT_EBUFS => super::ErrorCode::BufSize, + raw::GIT_EUSER => super::ErrorCode::User, + raw::GIT_EBAREREPO => super::ErrorCode::BareRepo, + raw::GIT_EUNBORNBRANCH => super::ErrorCode::UnbornBranch, + raw::GIT_EUNMERGED => super::ErrorCode::Unmerged, + raw::GIT_ENONFASTFORWARD => super::ErrorCode::NotFastForward, + raw::GIT_EINVALIDSPEC => super::ErrorCode::InvalidSpec, + raw::GIT_ECONFLICT => super::ErrorCode::Conflict, + raw::GIT_ELOCKED => super::ErrorCode::Locked, + raw::GIT_EMODIFIED => super::ErrorCode::Modified, + raw::GIT_PASSTHROUGH => super::ErrorCode::GenericError, + raw::GIT_ITEROVER => super::ErrorCode::GenericError, + raw::GIT_EAUTH => super::ErrorCode::Auth, + raw::GIT_ECERTIFICATE => super::ErrorCode::Certificate, + raw::GIT_EAPPLIED => super::ErrorCode::Applied, + raw::GIT_EPEEL => super::ErrorCode::Peel, + raw::GIT_EEOF => super::ErrorCode::Eof, + raw::GIT_EINVALID => super::ErrorCode::Invalid, + raw::GIT_EUNCOMMITTED => super::ErrorCode::Uncommitted, + raw::GIT_EDIRECTORY => super::ErrorCode::Directory, + raw::GIT_EMERGECONFLICT => super::ErrorCode::MergeConflict, + raw::GIT_EMISMATCH => super::ErrorCode::HashsumMismatch, + raw::GIT_EINDEXDIRTY => super::ErrorCode::IndexDirty, + raw::GIT_EAPPLYFAIL => super::ErrorCode::ApplyFail, + raw::GIT_EOWNER => super::ErrorCode::Owner, + _ => super::ErrorCode::GenericError, + } + } + + /// Modify the error code associated with this error. + /// + /// This is mainly intended to be used by implementers of custom transports + /// or database backends, and should be used with care. + pub fn set_code(&mut self, code: ErrorCode) { + self.code = match code { + ErrorCode::GenericError => raw::GIT_ERROR, + ErrorCode::NotFound => raw::GIT_ENOTFOUND, + ErrorCode::Exists => raw::GIT_EEXISTS, + ErrorCode::Ambiguous => raw::GIT_EAMBIGUOUS, + ErrorCode::BufSize => raw::GIT_EBUFS, + ErrorCode::User => raw::GIT_EUSER, + ErrorCode::BareRepo => raw::GIT_EBAREREPO, + ErrorCode::UnbornBranch => raw::GIT_EUNBORNBRANCH, + ErrorCode::Unmerged => raw::GIT_EUNMERGED, + ErrorCode::NotFastForward => raw::GIT_ENONFASTFORWARD, + ErrorCode::InvalidSpec => raw::GIT_EINVALIDSPEC, + ErrorCode::Conflict => raw::GIT_ECONFLICT, + ErrorCode::Locked => raw::GIT_ELOCKED, + ErrorCode::Modified => raw::GIT_EMODIFIED, + ErrorCode::Auth => raw::GIT_EAUTH, + ErrorCode::Certificate => raw::GIT_ECERTIFICATE, + ErrorCode::Applied => raw::GIT_EAPPLIED, + ErrorCode::Peel => raw::GIT_EPEEL, + ErrorCode::Eof => raw::GIT_EEOF, + ErrorCode::Invalid => raw::GIT_EINVALID, + ErrorCode::Uncommitted => raw::GIT_EUNCOMMITTED, + ErrorCode::Directory => raw::GIT_EDIRECTORY, + ErrorCode::MergeConflict => raw::GIT_EMERGECONFLICT, + ErrorCode::HashsumMismatch => raw::GIT_EMISMATCH, + ErrorCode::IndexDirty => raw::GIT_EINDEXDIRTY, + ErrorCode::ApplyFail => raw::GIT_EAPPLYFAIL, + ErrorCode::Owner => raw::GIT_EOWNER, + }; + } + + /// Return the error class associated with this error. + /// + /// Error classes are in general mostly just informative. For example the + /// class will show up in the error message but otherwise an error class is + /// typically not directly actionable. + pub fn class(&self) -> ErrorClass { + match self.raw_class() { + raw::GIT_ERROR_NONE => super::ErrorClass::None, + raw::GIT_ERROR_NOMEMORY => super::ErrorClass::NoMemory, + raw::GIT_ERROR_OS => super::ErrorClass::Os, + raw::GIT_ERROR_INVALID => super::ErrorClass::Invalid, + raw::GIT_ERROR_REFERENCE => super::ErrorClass::Reference, + raw::GIT_ERROR_ZLIB => super::ErrorClass::Zlib, + raw::GIT_ERROR_REPOSITORY => super::ErrorClass::Repository, + raw::GIT_ERROR_CONFIG => super::ErrorClass::Config, + raw::GIT_ERROR_REGEX => super::ErrorClass::Regex, + raw::GIT_ERROR_ODB => super::ErrorClass::Odb, + raw::GIT_ERROR_INDEX => super::ErrorClass::Index, + raw::GIT_ERROR_OBJECT => super::ErrorClass::Object, + raw::GIT_ERROR_NET => super::ErrorClass::Net, + raw::GIT_ERROR_TAG => super::ErrorClass::Tag, + raw::GIT_ERROR_TREE => super::ErrorClass::Tree, + raw::GIT_ERROR_INDEXER => super::ErrorClass::Indexer, + raw::GIT_ERROR_SSL => super::ErrorClass::Ssl, + raw::GIT_ERROR_SUBMODULE => super::ErrorClass::Submodule, + raw::GIT_ERROR_THREAD => super::ErrorClass::Thread, + raw::GIT_ERROR_STASH => super::ErrorClass::Stash, + raw::GIT_ERROR_CHECKOUT => super::ErrorClass::Checkout, + raw::GIT_ERROR_FETCHHEAD => super::ErrorClass::FetchHead, + raw::GIT_ERROR_MERGE => super::ErrorClass::Merge, + raw::GIT_ERROR_SSH => super::ErrorClass::Ssh, + raw::GIT_ERROR_FILTER => super::ErrorClass::Filter, + raw::GIT_ERROR_REVERT => super::ErrorClass::Revert, + raw::GIT_ERROR_CALLBACK => super::ErrorClass::Callback, + raw::GIT_ERROR_CHERRYPICK => super::ErrorClass::CherryPick, + raw::GIT_ERROR_DESCRIBE => super::ErrorClass::Describe, + raw::GIT_ERROR_REBASE => super::ErrorClass::Rebase, + raw::GIT_ERROR_FILESYSTEM => super::ErrorClass::Filesystem, + raw::GIT_ERROR_PATCH => super::ErrorClass::Patch, + raw::GIT_ERROR_WORKTREE => super::ErrorClass::Worktree, + raw::GIT_ERROR_SHA1 => super::ErrorClass::Sha1, + raw::GIT_ERROR_HTTP => super::ErrorClass::Http, + _ => super::ErrorClass::None, + } + } + + /// Modify the error class associated with this error. + /// + /// This is mainly intended to be used by implementers of custom transports + /// or database backends, and should be used with care. + pub fn set_class(&mut self, class: ErrorClass) { + self.klass = match class { + ErrorClass::None => raw::GIT_ERROR_NONE, + ErrorClass::NoMemory => raw::GIT_ERROR_NOMEMORY, + ErrorClass::Os => raw::GIT_ERROR_OS, + ErrorClass::Invalid => raw::GIT_ERROR_INVALID, + ErrorClass::Reference => raw::GIT_ERROR_REFERENCE, + ErrorClass::Zlib => raw::GIT_ERROR_ZLIB, + ErrorClass::Repository => raw::GIT_ERROR_REPOSITORY, + ErrorClass::Config => raw::GIT_ERROR_CONFIG, + ErrorClass::Regex => raw::GIT_ERROR_REGEX, + ErrorClass::Odb => raw::GIT_ERROR_ODB, + ErrorClass::Index => raw::GIT_ERROR_INDEX, + ErrorClass::Object => raw::GIT_ERROR_OBJECT, + ErrorClass::Net => raw::GIT_ERROR_NET, + ErrorClass::Tag => raw::GIT_ERROR_TAG, + ErrorClass::Tree => raw::GIT_ERROR_TREE, + ErrorClass::Indexer => raw::GIT_ERROR_INDEXER, + ErrorClass::Ssl => raw::GIT_ERROR_SSL, + ErrorClass::Submodule => raw::GIT_ERROR_SUBMODULE, + ErrorClass::Thread => raw::GIT_ERROR_THREAD, + ErrorClass::Stash => raw::GIT_ERROR_STASH, + ErrorClass::Checkout => raw::GIT_ERROR_CHECKOUT, + ErrorClass::FetchHead => raw::GIT_ERROR_FETCHHEAD, + ErrorClass::Merge => raw::GIT_ERROR_MERGE, + ErrorClass::Ssh => raw::GIT_ERROR_SSH, + ErrorClass::Filter => raw::GIT_ERROR_FILTER, + ErrorClass::Revert => raw::GIT_ERROR_REVERT, + ErrorClass::Callback => raw::GIT_ERROR_CALLBACK, + ErrorClass::CherryPick => raw::GIT_ERROR_CHERRYPICK, + ErrorClass::Describe => raw::GIT_ERROR_DESCRIBE, + ErrorClass::Rebase => raw::GIT_ERROR_REBASE, + ErrorClass::Filesystem => raw::GIT_ERROR_FILESYSTEM, + ErrorClass::Patch => raw::GIT_ERROR_PATCH, + ErrorClass::Worktree => raw::GIT_ERROR_WORKTREE, + ErrorClass::Sha1 => raw::GIT_ERROR_SHA1, + ErrorClass::Http => raw::GIT_ERROR_HTTP, + } as c_int; + } + + /// Return the raw error code associated with this error. + pub fn raw_code(&self) -> raw::git_error_code { + macro_rules! check( ($($e:ident,)*) => ( + $(if self.code == raw::$e as c_int { raw::$e }) else * + else { + raw::GIT_ERROR + } + ) ); + check!( + GIT_OK, + GIT_ERROR, + GIT_ENOTFOUND, + GIT_EEXISTS, + GIT_EAMBIGUOUS, + GIT_EBUFS, + GIT_EUSER, + GIT_EBAREREPO, + GIT_EUNBORNBRANCH, + GIT_EUNMERGED, + GIT_ENONFASTFORWARD, + GIT_EINVALIDSPEC, + GIT_ECONFLICT, + GIT_ELOCKED, + GIT_EMODIFIED, + GIT_EAUTH, + GIT_ECERTIFICATE, + GIT_EAPPLIED, + GIT_EPEEL, + GIT_EEOF, + GIT_EINVALID, + GIT_EUNCOMMITTED, + GIT_PASSTHROUGH, + GIT_ITEROVER, + GIT_RETRY, + GIT_EMISMATCH, + GIT_EINDEXDIRTY, + GIT_EAPPLYFAIL, + GIT_EOWNER, + ) + } + + /// Return the raw error class associated with this error. + pub fn raw_class(&self) -> raw::git_error_t { + macro_rules! check( ($($e:ident,)*) => ( + $(if self.klass == raw::$e as c_int { raw::$e }) else * + else { + raw::GIT_ERROR_NONE + } + ) ); + check!( + GIT_ERROR_NONE, + GIT_ERROR_NOMEMORY, + GIT_ERROR_OS, + GIT_ERROR_INVALID, + GIT_ERROR_REFERENCE, + GIT_ERROR_ZLIB, + GIT_ERROR_REPOSITORY, + GIT_ERROR_CONFIG, + GIT_ERROR_REGEX, + GIT_ERROR_ODB, + GIT_ERROR_INDEX, + GIT_ERROR_OBJECT, + GIT_ERROR_NET, + GIT_ERROR_TAG, + GIT_ERROR_TREE, + GIT_ERROR_INDEXER, + GIT_ERROR_SSL, + GIT_ERROR_SUBMODULE, + GIT_ERROR_THREAD, + GIT_ERROR_STASH, + GIT_ERROR_CHECKOUT, + GIT_ERROR_FETCHHEAD, + GIT_ERROR_MERGE, + GIT_ERROR_SSH, + GIT_ERROR_FILTER, + GIT_ERROR_REVERT, + GIT_ERROR_CALLBACK, + GIT_ERROR_CHERRYPICK, + GIT_ERROR_DESCRIBE, + GIT_ERROR_REBASE, + GIT_ERROR_FILESYSTEM, + GIT_ERROR_PATCH, + GIT_ERROR_WORKTREE, + GIT_ERROR_SHA1, + GIT_ERROR_HTTP, + ) + } + + /// Return the message associated with this error + pub fn message(&self) -> &str { + &self.message + } +} + +impl error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.message)?; + match self.class() { + ErrorClass::None => {} + other => write!(f, "; class={:?} ({})", other, self.klass)?, + } + match self.code() { + ErrorCode::GenericError => {} + other => write!(f, "; code={:?} ({})", other, self.code)?, + } + Ok(()) + } +} + +impl From<NulError> for Error { + fn from(_: NulError) -> Error { + Error::from_str( + "data contained a nul byte that could not be \ + represented as a string", + ) + } +} + +impl From<JoinPathsError> for Error { + fn from(e: JoinPathsError) -> Error { + Error::from_str(&e.to_string()) + } +} + +#[cfg(test)] +mod tests { + use crate::{ErrorClass, ErrorCode}; + + #[test] + fn smoke() { + let (_td, repo) = crate::test::repo_init(); + + let err = repo.find_submodule("does_not_exist").err().unwrap(); + assert_eq!(err.code(), ErrorCode::NotFound); + assert_eq!(err.class(), ErrorClass::Submodule); + } +} diff --git a/extra/git2/src/index.rs b/extra/git2/src/index.rs new file mode 100644 index 000000000..0291d3cb9 --- /dev/null +++ b/extra/git2/src/index.rs @@ -0,0 +1,929 @@ +use std::ffi::{CStr, CString}; +use std::marker; +use std::ops::Range; +use std::path::Path; +use std::ptr; +use std::slice; + +use libc::{c_char, c_int, c_uint, c_void, size_t}; + +use crate::util::{self, path_to_repo_path, Binding}; +use crate::IntoCString; +use crate::{panic, raw, Error, IndexAddOption, IndexTime, Oid, Repository, Tree}; + +/// A structure to represent a git [index][1] +/// +/// [1]: http://git-scm.com/book/en/Git-Internals-Git-Objects +pub struct Index { + raw: *mut raw::git_index, +} + +/// An iterator over the entries in an index +pub struct IndexEntries<'index> { + range: Range<usize>, + index: &'index Index, +} + +/// An iterator over the conflicting entries in an index +pub struct IndexConflicts<'index> { + conflict_iter: *mut raw::git_index_conflict_iterator, + _marker: marker::PhantomData<&'index Index>, +} + +/// A structure to represent the information returned when a conflict is detected in an index entry +pub struct IndexConflict { + /// The ancestor index entry of the two conflicting index entries + pub ancestor: Option<IndexEntry>, + /// The index entry originating from the user's copy of the repository. + /// Its contents conflict with 'their' index entry + pub our: Option<IndexEntry>, + /// The index entry originating from the external repository. + /// Its contents conflict with 'our' index entry + pub their: Option<IndexEntry>, +} + +/// 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 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; + +/// A structure to represent an entry or a file inside of an index. +/// +/// All fields of an entry are public for modification and inspection. This is +/// also how a new index entry is created. +#[allow(missing_docs)] +#[derive(Debug)] +pub struct IndexEntry { + pub ctime: IndexTime, + pub mtime: IndexTime, + pub dev: u32, + pub ino: u32, + pub mode: u32, + pub uid: u32, + pub gid: u32, + pub file_size: u32, + pub id: Oid, + pub flags: u16, + pub flags_extended: u16, + + /// The path of this index entry as a byte vector. Regardless of the + /// current platform, the directory separator is an ASCII forward slash + /// (`0x2F`). There are no terminating or internal NUL characters, and no + /// trailing slashes. Most of the time, paths will be valid utf-8 — but + /// not always. For more information on the path storage format, see + /// [these git docs][git-index-docs]. Note that libgit2 will take care of + /// handling the prefix compression mentioned there. + /// + /// [git-index-docs]: https://github.com/git/git/blob/a08a83db2bf27f015bec9a435f6d73e223c21c5e/Documentation/technical/index-format.txt#L107-L124 + /// + /// You can turn this value into a `std::ffi::CString` with + /// `CString::new(&entry.path[..]).unwrap()`. To turn a reference into a + /// `&std::path::Path`, see the `bytes2path()` function in the private, + /// internal `util` module in this crate’s source code. + pub path: Vec<u8>, +} + +impl Index { + /// Creates a new in-memory index. + /// + /// This index object cannot be read/written to the filesystem, but may be + /// used to perform in-memory index operations. + pub fn new() -> Result<Index, Error> { + crate::init(); + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_index_new(&mut raw)); + Ok(Binding::from_raw(raw)) + } + } + + /// Create a new bare Git index object as a memory representation of the Git + /// index file in 'index_path', without a repository to back it. + /// + /// Since there is no ODB or working directory behind this index, any Index + /// methods which rely on these (e.g. add_path) will fail. + /// + /// If you need an index attached to a repository, use the `index()` method + /// on `Repository`. + pub fn open(index_path: &Path) -> Result<Index, Error> { + crate::init(); + let mut raw = ptr::null_mut(); + // Normal file path OK (does not need Windows conversion). + let index_path = index_path.into_c_string()?; + unsafe { + try_call!(raw::git_index_open(&mut raw, index_path)); + Ok(Binding::from_raw(raw)) + } + } + + /// Get index on-disk version. + /// + /// Valid return values are 2, 3, or 4. If 3 is returned, an index + /// with version 2 may be written instead, if the extension data in + /// version 3 is not necessary. + pub fn version(&self) -> u32 { + unsafe { raw::git_index_version(self.raw) } + } + + /// Set index on-disk version. + /// + /// Valid values are 2, 3, or 4. If 2 is given, git_index_write may + /// write an index with version 3 instead, if necessary to accurately + /// represent the index. + pub fn set_version(&mut self, version: u32) -> Result<(), Error> { + unsafe { + try_call!(raw::git_index_set_version(self.raw, version)); + } + Ok(()) + } + + /// Add or update an index entry from an in-memory struct + /// + /// If a previous index entry exists that has the same path and stage as the + /// given 'source_entry', it will be replaced. Otherwise, the 'source_entry' + /// will be added. + pub fn add(&mut self, entry: &IndexEntry) -> Result<(), Error> { + let path = CString::new(&entry.path[..])?; + + // libgit2 encodes the length of the path in the lower bits of the + // `flags` entry, so mask those out and recalculate here to ensure we + // don't corrupt anything. + let mut flags = entry.flags & !raw::GIT_INDEX_ENTRY_NAMEMASK; + + if entry.path.len() < raw::GIT_INDEX_ENTRY_NAMEMASK as usize { + flags |= entry.path.len() as u16; + } else { + flags |= raw::GIT_INDEX_ENTRY_NAMEMASK; + } + + unsafe { + let raw = raw::git_index_entry { + dev: entry.dev, + ino: entry.ino, + mode: entry.mode, + uid: entry.uid, + gid: entry.gid, + file_size: entry.file_size, + id: *entry.id.raw(), + flags, + flags_extended: entry.flags_extended, + path: path.as_ptr(), + mtime: raw::git_index_time { + seconds: entry.mtime.seconds(), + nanoseconds: entry.mtime.nanoseconds(), + }, + ctime: raw::git_index_time { + seconds: entry.ctime.seconds(), + nanoseconds: entry.ctime.nanoseconds(), + }, + }; + try_call!(raw::git_index_add(self.raw, &raw)); + Ok(()) + } + } + + /// Add or update an index entry from a buffer in memory + /// + /// This method will create a blob in the repository that owns the index and + /// then add the index entry to the index. The path of the entry represents + /// the position of the blob relative to the repository's root folder. + /// + /// If a previous index entry exists that has the same path as the given + /// 'entry', it will be replaced. Otherwise, the 'entry' will be added. + /// The id and the file_size of the 'entry' are updated with the real value + /// of the blob. + /// + /// This forces the file to be added to the index, not looking at gitignore + /// rules. + /// + /// If this file currently is the result of a merge conflict, this file will + /// no longer be marked as conflicting. The data about the conflict will be + /// moved to the "resolve undo" (REUC) section. + pub fn add_frombuffer(&mut self, entry: &IndexEntry, data: &[u8]) -> Result<(), Error> { + let path = CString::new(&entry.path[..])?; + + // libgit2 encodes the length of the path in the lower bits of the + // `flags` entry, so mask those out and recalculate here to ensure we + // don't corrupt anything. + let mut flags = entry.flags & !raw::GIT_INDEX_ENTRY_NAMEMASK; + + if entry.path.len() < raw::GIT_INDEX_ENTRY_NAMEMASK as usize { + flags |= entry.path.len() as u16; + } else { + flags |= raw::GIT_INDEX_ENTRY_NAMEMASK; + } + + unsafe { + let raw = raw::git_index_entry { + dev: entry.dev, + ino: entry.ino, + mode: entry.mode, + uid: entry.uid, + gid: entry.gid, + file_size: entry.file_size, + id: *entry.id.raw(), + flags, + flags_extended: entry.flags_extended, + path: path.as_ptr(), + mtime: raw::git_index_time { + seconds: entry.mtime.seconds(), + nanoseconds: entry.mtime.nanoseconds(), + }, + ctime: raw::git_index_time { + seconds: entry.ctime.seconds(), + nanoseconds: entry.ctime.nanoseconds(), + }, + }; + + let ptr = data.as_ptr() as *const c_void; + let len = data.len() as size_t; + try_call!(raw::git_index_add_frombuffer(self.raw, &raw, ptr, len)); + Ok(()) + } + } + + /// Add or update an index entry from a file on disk + /// + /// The file path must be relative to the repository's working folder and + /// must be readable. + /// + /// This method will fail in bare index instances. + /// + /// This forces the file to be added to the index, not looking at gitignore + /// rules. + /// + /// If this file currently is the result of a merge conflict, this file will + /// no longer be marked as conflicting. The data about the conflict will be + /// moved to the "resolve undo" (REUC) section. + pub fn add_path(&mut self, path: &Path) -> Result<(), Error> { + let posix_path = path_to_repo_path(path)?; + unsafe { + try_call!(raw::git_index_add_bypath(self.raw, posix_path)); + Ok(()) + } + } + + /// Add or update index entries matching files in the working directory. + /// + /// This method will fail in bare index instances. + /// + /// The `pathspecs` are a list of file names or shell glob patterns that + /// will matched against files in the repository's working directory. Each + /// file that matches will be added to the index (either updating an + /// existing entry or adding a new entry). You can disable glob expansion + /// and force exact matching with the `AddDisablePathspecMatch` flag. + /// + /// Files that are ignored will be skipped (unlike `add_path`). If a file is + /// already tracked in the index, then it will be updated even if it is + /// ignored. Pass the `AddForce` flag to skip the checking of ignore rules. + /// + /// To emulate `git add -A` and generate an error if the pathspec contains + /// the exact path of an ignored file (when not using `AddForce`), add the + /// `AddCheckPathspec` flag. This checks that each entry in `pathspecs` + /// that is an exact match to a filename on disk is either not ignored or + /// already in the index. If this check fails, the function will return + /// an error. + /// + /// To emulate `git add -A` with the "dry-run" option, just use a callback + /// function that always returns a positive value. See below for details. + /// + /// If any files are currently the result of a merge conflict, those files + /// will no longer be marked as conflicting. The data about the conflicts + /// will be moved to the "resolve undo" (REUC) section. + /// + /// If you provide a callback function, it will be invoked on each matching + /// item in the working directory immediately before it is added to / + /// updated in the index. Returning zero will add the item to the index, + /// greater than zero will skip the item, and less than zero will abort the + /// scan an return an error to the caller. + /// + /// # Example + /// + /// Emulate `git add *`: + /// + /// ```no_run + /// use git2::{Index, IndexAddOption, Repository}; + /// + /// let repo = Repository::open("/path/to/a/repo").expect("failed to open"); + /// let mut index = repo.index().expect("cannot get the Index file"); + /// index.add_all(["*"].iter(), IndexAddOption::DEFAULT, None); + /// index.write(); + /// ``` + pub fn add_all<T, I>( + &mut self, + pathspecs: I, + flag: IndexAddOption, + mut cb: Option<&mut IndexMatchedPath<'_>>, + ) -> Result<(), Error> + where + T: IntoCString, + I: IntoIterator<Item = T>, + { + let (_a, _b, raw_strarray) = crate::util::iter2cstrs_paths(pathspecs)?; + let ptr = cb.as_mut(); + let callback = ptr + .as_ref() + .map(|_| index_matched_path_cb as extern "C" fn(_, _, _) -> _); + unsafe { + try_call!(raw::git_index_add_all( + self.raw, + &raw_strarray, + flag.bits() as c_uint, + callback, + ptr.map(|p| p as *mut _).unwrap_or(ptr::null_mut()) as *mut c_void + )); + } + Ok(()) + } + + /// Clear the contents (all the entries) of an index object. + /// + /// This clears the index object in memory; changes must be explicitly + /// written to disk for them to take effect persistently via `write_*`. + pub fn clear(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_index_clear(self.raw)); + } + Ok(()) + } + + /// Get the count of entries currently in the index + pub fn len(&self) -> usize { + unsafe { raw::git_index_entrycount(&*self.raw) as usize } + } + + /// Return `true` is there is no entry in the index + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Get one of the entries in the index by its position. + pub fn get(&self, n: usize) -> Option<IndexEntry> { + unsafe { + let ptr = raw::git_index_get_byindex(self.raw, n as size_t); + if ptr.is_null() { + None + } else { + Some(Binding::from_raw(*ptr)) + } + } + } + + /// Get an iterator over the entries in this index. + pub fn iter(&self) -> IndexEntries<'_> { + IndexEntries { + range: 0..self.len(), + index: self, + } + } + + /// Get an iterator over the index entries that have conflicts + pub fn conflicts(&self) -> Result<IndexConflicts<'_>, Error> { + crate::init(); + let mut conflict_iter = ptr::null_mut(); + unsafe { + try_call!(raw::git_index_conflict_iterator_new( + &mut conflict_iter, + self.raw + )); + Ok(Binding::from_raw(conflict_iter)) + } + } + + /// Get one of the entries in the index by its path. + pub fn get_path(&self, path: &Path, stage: i32) -> Option<IndexEntry> { + let path = path_to_repo_path(path).unwrap(); + unsafe { + let ptr = call!(raw::git_index_get_bypath(self.raw, path, stage as c_int)); + if ptr.is_null() { + None + } else { + Some(Binding::from_raw(*ptr)) + } + } + } + + /// Does this index have conflicts? + /// + /// Returns `true` if the index contains conflicts, `false` if it does not. + pub fn has_conflicts(&self) -> bool { + unsafe { raw::git_index_has_conflicts(self.raw) == 1 } + } + + /// Get the full path to the index file on disk. + /// + /// Returns `None` if this is an in-memory index. + pub fn path(&self) -> Option<&Path> { + unsafe { crate::opt_bytes(self, raw::git_index_path(&*self.raw)).map(util::bytes2path) } + } + + /// Update the contents of an existing index object in memory by reading + /// from the hard disk. + /// + /// If force is true, this performs a "hard" read that discards in-memory + /// changes and always reloads the on-disk index data. If there is no + /// on-disk version, the index will be cleared. + /// + /// If force is false, this does a "soft" read that reloads the index data + /// from disk only if it has changed since the last time it was loaded. + /// Purely in-memory index data will be untouched. Be aware: if there are + /// changes on disk, unwritten in-memory changes are discarded. + pub fn read(&mut self, force: bool) -> Result<(), Error> { + unsafe { + try_call!(raw::git_index_read(self.raw, force)); + } + Ok(()) + } + + /// Read a tree into the index file with stats + /// + /// The current index contents will be replaced by the specified tree. + pub fn read_tree(&mut self, tree: &Tree<'_>) -> Result<(), Error> { + unsafe { + try_call!(raw::git_index_read_tree(self.raw, &*tree.raw())); + } + Ok(()) + } + + /// Remove an entry from the index + pub fn remove(&mut self, path: &Path, stage: i32) -> Result<(), Error> { + let path = path_to_repo_path(path)?; + unsafe { + try_call!(raw::git_index_remove(self.raw, path, stage as c_int)); + } + Ok(()) + } + + /// Remove an index entry corresponding to a file on disk. + /// + /// The file path must be relative to the repository's working folder. It + /// may exist. + /// + /// If this file currently is the result of a merge conflict, this file will + /// no longer be marked as conflicting. The data about the conflict will be + /// moved to the "resolve undo" (REUC) section. + pub fn remove_path(&mut self, path: &Path) -> Result<(), Error> { + let path = path_to_repo_path(path)?; + unsafe { + try_call!(raw::git_index_remove_bypath(self.raw, path)); + } + Ok(()) + } + + /// Remove all entries from the index under a given directory. + pub fn remove_dir(&mut self, path: &Path, stage: i32) -> Result<(), Error> { + let path = path_to_repo_path(path)?; + unsafe { + try_call!(raw::git_index_remove_directory( + self.raw, + path, + stage as c_int + )); + } + Ok(()) + } + + /// Remove all matching index entries. + /// + /// If you provide a callback function, it will be invoked on each matching + /// item in the index immediately before it is removed. Return 0 to remove + /// the item, > 0 to skip the item, and < 0 to abort the scan. + pub fn remove_all<T, I>( + &mut self, + pathspecs: I, + mut cb: Option<&mut IndexMatchedPath<'_>>, + ) -> Result<(), Error> + where + T: IntoCString, + I: IntoIterator<Item = T>, + { + let (_a, _b, raw_strarray) = crate::util::iter2cstrs_paths(pathspecs)?; + let ptr = cb.as_mut(); + let callback = ptr + .as_ref() + .map(|_| index_matched_path_cb as extern "C" fn(_, _, _) -> _); + unsafe { + try_call!(raw::git_index_remove_all( + self.raw, + &raw_strarray, + callback, + ptr.map(|p| p as *mut _).unwrap_or(ptr::null_mut()) as *mut c_void + )); + } + Ok(()) + } + + /// Update all index entries to match the working directory + /// + /// This method will fail in bare index instances. + /// + /// This scans the existing index entries and synchronizes them with the + /// working directory, deleting them if the corresponding working directory + /// file no longer exists otherwise updating the information (including + /// adding the latest version of file to the ODB if needed). + /// + /// If you provide a callback function, it will be invoked on each matching + /// item in the index immediately before it is updated (either refreshed or + /// removed depending on working directory state). Return 0 to proceed with + /// updating the item, > 0 to skip the item, and < 0 to abort the scan. + pub fn update_all<T, I>( + &mut self, + pathspecs: I, + mut cb: Option<&mut IndexMatchedPath<'_>>, + ) -> Result<(), Error> + where + T: IntoCString, + I: IntoIterator<Item = T>, + { + let (_a, _b, raw_strarray) = crate::util::iter2cstrs_paths(pathspecs)?; + let ptr = cb.as_mut(); + let callback = ptr + .as_ref() + .map(|_| index_matched_path_cb as extern "C" fn(_, _, _) -> _); + unsafe { + try_call!(raw::git_index_update_all( + self.raw, + &raw_strarray, + callback, + ptr.map(|p| p as *mut _).unwrap_or(ptr::null_mut()) as *mut c_void + )); + } + Ok(()) + } + + /// Write an existing index object from memory back to disk using an atomic + /// file lock. + pub fn write(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_index_write(self.raw)); + } + Ok(()) + } + + /// Write the index as a tree. + /// + /// This method will scan the index and write a representation of its + /// current state back to disk; it recursively creates tree objects for each + /// of the subtrees stored in the index, but only returns the OID of the + /// root tree. This is the OID that can be used e.g. to create a commit. + /// + /// The index instance cannot be bare, and needs to be associated to an + /// existing repository. + /// + /// The index must not contain any file in conflict. + pub fn write_tree(&mut self) -> Result<Oid, Error> { + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_index_write_tree(&mut raw, self.raw)); + Ok(Binding::from_raw(&raw as *const _)) + } + } + + /// Write the index as a tree to the given repository + /// + /// This is the same as `write_tree` except that the destination repository + /// can be chosen. + pub fn write_tree_to(&mut self, repo: &Repository) -> Result<Oid, Error> { + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_index_write_tree_to(&mut raw, self.raw, repo.raw())); + 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<T: IntoCString>(&self, prefix: T) -> Result<usize, Error> { + 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 Binding for Index { + type Raw = *mut raw::git_index; + unsafe fn from_raw(raw: *mut raw::git_index) -> Index { + Index { raw } + } + fn raw(&self) -> *mut raw::git_index { + self.raw + } +} + +impl<'index> Binding for IndexConflicts<'index> { + type Raw = *mut raw::git_index_conflict_iterator; + + unsafe fn from_raw(raw: *mut raw::git_index_conflict_iterator) -> IndexConflicts<'index> { + IndexConflicts { + conflict_iter: raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_index_conflict_iterator { + self.conflict_iter + } +} + +extern "C" fn index_matched_path_cb( + path: *const c_char, + matched_pathspec: *const c_char, + payload: *mut c_void, +) -> c_int { + unsafe { + let path = CStr::from_ptr(path).to_bytes(); + let matched_pathspec = CStr::from_ptr(matched_pathspec).to_bytes(); + + panic::wrap(|| { + let payload = payload as *mut &mut IndexMatchedPath<'_>; + (*payload)(util::bytes2path(path), matched_pathspec) as c_int + }) + .unwrap_or(-1) + } +} + +impl Drop for Index { + fn drop(&mut self) { + unsafe { raw::git_index_free(self.raw) } + } +} + +impl<'index> Drop for IndexConflicts<'index> { + fn drop(&mut self) { + unsafe { raw::git_index_conflict_iterator_free(self.conflict_iter) } + } +} + +impl<'index> Iterator for IndexEntries<'index> { + type Item = IndexEntry; + fn next(&mut self) -> Option<IndexEntry> { + self.range.next().map(|i| self.index.get(i).unwrap()) + } +} + +impl<'index> Iterator for IndexConflicts<'index> { + type Item = Result<IndexConflict, Error>; + fn next(&mut self) -> Option<Result<IndexConflict, Error>> { + let mut ancestor = ptr::null(); + let mut our = ptr::null(); + let mut their = ptr::null(); + unsafe { + try_call_iter!(raw::git_index_conflict_next( + &mut ancestor, + &mut our, + &mut their, + self.conflict_iter + )); + Some(Ok(IndexConflict { + ancestor: match ancestor.is_null() { + false => Some(IndexEntry::from_raw(*ancestor)), + true => None, + }, + our: match our.is_null() { + false => Some(IndexEntry::from_raw(*our)), + true => None, + }, + their: match their.is_null() { + false => Some(IndexEntry::from_raw(*their)), + true => None, + }, + })) + } + } +} + +impl Binding for IndexEntry { + type Raw = raw::git_index_entry; + + unsafe fn from_raw(raw: raw::git_index_entry) -> IndexEntry { + let raw::git_index_entry { + ctime, + mtime, + dev, + ino, + mode, + uid, + gid, + file_size, + id, + flags, + flags_extended, + path, + } = raw; + + // libgit2 encodes the length of the path in the lower bits of `flags`, + // but if the length exceeds the number of bits then the path is + // nul-terminated. + let mut pathlen = (flags & raw::GIT_INDEX_ENTRY_NAMEMASK) as usize; + if pathlen == raw::GIT_INDEX_ENTRY_NAMEMASK as usize { + pathlen = CStr::from_ptr(path).to_bytes().len(); + } + + let path = slice::from_raw_parts(path as *const u8, pathlen); + + IndexEntry { + dev, + ino, + mode, + uid, + gid, + file_size, + id: Binding::from_raw(&id as *const _), + flags, + flags_extended, + path: path.to_vec(), + mtime: Binding::from_raw(mtime), + ctime: Binding::from_raw(ctime), + } + } + + fn raw(&self) -> raw::git_index_entry { + // not implemented, may require a CString in storage + panic!() + } +} + +#[cfg(test)] +mod tests { + use std::fs::{self, File}; + use std::path::Path; + use tempfile::TempDir; + + use crate::{ErrorCode, Index, IndexEntry, IndexTime, Oid, Repository, ResetType}; + + #[test] + fn smoke() { + let mut index = Index::new().unwrap(); + assert!(index.add_path(&Path::new(".")).is_err()); + index.clear().unwrap(); + assert_eq!(index.len(), 0); + assert!(index.get(0).is_none()); + assert!(index.path().is_none()); + assert!(index.read(true).is_err()); + } + + #[test] + fn smoke_from_repo() { + let (_td, repo) = crate::test::repo_init(); + let mut index = repo.index().unwrap(); + assert_eq!( + index.path().map(|s| s.to_path_buf()), + Some(repo.path().join("index")) + ); + Index::open(&repo.path().join("index")).unwrap(); + + index.clear().unwrap(); + index.read(true).unwrap(); + index.write().unwrap(); + index.write_tree().unwrap(); + index.write_tree_to(&repo).unwrap(); + } + + #[test] + fn add_all() { + let (_td, repo) = crate::test::repo_init(); + let mut index = repo.index().unwrap(); + + let root = repo.path().parent().unwrap(); + fs::create_dir(&root.join("foo")).unwrap(); + File::create(&root.join("foo/bar")).unwrap(); + let mut called = false; + index + .add_all( + ["foo"].iter(), + crate::IndexAddOption::DEFAULT, + Some(&mut |a: &Path, b: &[u8]| { + assert!(!called); + called = true; + assert_eq!(b, b"foo"); + assert_eq!(a, Path::new("foo/bar")); + 0 + }), + ) + .unwrap(); + assert!(called); + + called = false; + index + .remove_all( + ["."].iter(), + Some(&mut |a: &Path, b: &[u8]| { + assert!(!called); + called = true; + assert_eq!(b, b"."); + assert_eq!(a, Path::new("foo/bar")); + 0 + }), + ) + .unwrap(); + assert!(called); + } + + #[test] + fn smoke_add() { + let (_td, repo) = crate::test::repo_init(); + let mut index = repo.index().unwrap(); + + let root = repo.path().parent().unwrap(); + fs::create_dir(&root.join("foo")).unwrap(); + File::create(&root.join("foo/bar")).unwrap(); + index.add_path(Path::new("foo/bar")).unwrap(); + index.write().unwrap(); + assert_eq!(index.iter().count(), 1); + + // Make sure we can use this repo somewhere else now. + let id = index.write_tree().unwrap(); + let tree = repo.find_tree(id).unwrap(); + let sig = repo.signature().unwrap(); + let id = repo.refname_to_id("HEAD").unwrap(); + let parent = repo.find_commit(id).unwrap(); + let commit = repo + .commit(Some("HEAD"), &sig, &sig, "commit", &tree, &[&parent]) + .unwrap(); + let obj = repo.find_object(commit, None).unwrap(); + repo.reset(&obj, ResetType::Hard, None).unwrap(); + + let td2 = TempDir::new().unwrap(); + let url = crate::test::path2url(&root); + let repo = Repository::clone(&url, td2.path()).unwrap(); + let obj = repo.find_object(commit, None).unwrap(); + repo.reset(&obj, ResetType::Hard, None).unwrap(); + } + + #[test] + fn add_then_read() { + let mut index = Index::new().unwrap(); + let mut e = entry(); + e.path = b"foobar".to_vec(); + index.add(&e).unwrap(); + let e = index.get(0).unwrap(); + assert_eq!(e.path.len(), 6); + } + + #[test] + fn add_then_find() { + let mut index = Index::new().unwrap(); + let mut e = entry(); + e.path = b"foo/bar".to_vec(); + index.add(&e).unwrap(); + let mut e = entry(); + e.path = b"foo2/bar".to_vec(); + index.add(&e).unwrap(); + assert_eq!(index.get(0).unwrap().path, b"foo/bar"); + assert_eq!( + index.get_path(Path::new("foo/bar"), 0).unwrap().path, + b"foo/bar" + ); + assert_eq!(index.find_prefix(Path::new("foo2/")), Ok(1)); + assert_eq!( + index.find_prefix(Path::new("empty/")).unwrap_err().code(), + ErrorCode::NotFound + ); + } + + #[test] + fn add_frombuffer_then_read() { + let (_td, repo) = crate::test::repo_init(); + let mut index = repo.index().unwrap(); + + let mut e = entry(); + e.path = b"foobar".to_vec(); + let content = b"the contents"; + index.add_frombuffer(&e, content).unwrap(); + let e = index.get(0).unwrap(); + assert_eq!(e.path.len(), 6); + + let b = repo.find_blob(e.id).unwrap(); + assert_eq!(b.content(), content); + } + + fn entry() -> IndexEntry { + IndexEntry { + ctime: IndexTime::new(0, 0), + mtime: IndexTime::new(0, 0), + dev: 0, + ino: 0, + mode: 0o100644, + uid: 0, + gid: 0, + file_size: 0, + id: Oid::from_bytes(&[0; 20]).unwrap(), + flags: 0, + flags_extended: 0, + path: Vec::new(), + } + } +} diff --git a/extra/git2/src/indexer.rs b/extra/git2/src/indexer.rs new file mode 100644 index 000000000..0aaf353d5 --- /dev/null +++ b/extra/git2/src/indexer.rs @@ -0,0 +1,255 @@ +use std::ffi::CStr; +use std::path::Path; +use std::{io, marker, mem, ptr}; + +use libc::c_void; + +use crate::odb::{write_pack_progress_cb, OdbPackwriterCb}; +use crate::util::Binding; +use crate::{raw, Error, IntoCString, Odb}; + +/// Struct representing the progress by an in-flight transfer. +pub struct Progress<'a> { + pub(crate) raw: ProgressState, + pub(crate) _marker: marker::PhantomData<&'a raw::git_indexer_progress>, +} + +pub(crate) enum ProgressState { + Borrowed(*const raw::git_indexer_progress), + Owned(raw::git_indexer_progress), +} + +/// Callback to be invoked while indexing is in progress. +/// +/// This callback will be periodically called with updates to the progress of +/// the indexing so far. The return value indicates whether the indexing or +/// transfer should continue. A return value of `false` will cancel the +/// indexing or transfer. +/// +/// * `progress` - the progress being made so far. +pub type IndexerProgress<'a> = dyn FnMut(Progress<'_>) -> bool + 'a; + +impl<'a> Progress<'a> { + /// Number of objects in the packfile being downloaded + pub fn total_objects(&self) -> usize { + unsafe { (*self.raw()).total_objects as usize } + } + /// Received objects that have been hashed + pub fn indexed_objects(&self) -> usize { + unsafe { (*self.raw()).indexed_objects as usize } + } + /// Objects which have been downloaded + pub fn received_objects(&self) -> usize { + unsafe { (*self.raw()).received_objects as usize } + } + /// Locally-available objects that have been injected in order to fix a thin + /// pack. + pub fn local_objects(&self) -> usize { + unsafe { (*self.raw()).local_objects as usize } + } + /// Number of deltas in the packfile being downloaded + pub fn total_deltas(&self) -> usize { + unsafe { (*self.raw()).total_deltas as usize } + } + /// Received deltas that have been hashed. + pub fn indexed_deltas(&self) -> usize { + unsafe { (*self.raw()).indexed_deltas as usize } + } + /// Size of the packfile received up to now + pub fn received_bytes(&self) -> usize { + unsafe { (*self.raw()).received_bytes as usize } + } + + /// Convert this to an owned version of `Progress`. + pub fn to_owned(&self) -> Progress<'static> { + Progress { + raw: ProgressState::Owned(unsafe { *self.raw() }), + _marker: marker::PhantomData, + } + } +} + +impl<'a> Binding for Progress<'a> { + type Raw = *const raw::git_indexer_progress; + unsafe fn from_raw(raw: *const raw::git_indexer_progress) -> Progress<'a> { + Progress { + raw: ProgressState::Borrowed(raw), + _marker: marker::PhantomData, + } + } + + fn raw(&self) -> *const raw::git_indexer_progress { + match self.raw { + ProgressState::Borrowed(raw) => raw, + ProgressState::Owned(ref raw) => raw as *const _, + } + } +} + +/// Callback to be invoked while a transfer is in progress. +/// +/// This callback will be periodically called with updates to the progress of +/// the transfer so far. The return value indicates whether the transfer should +/// continue. A return value of `false` will cancel the transfer. +/// +/// * `progress` - the progress being made so far. +#[deprecated( + since = "0.11.0", + note = "renamed to `IndexerProgress` to match upstream" +)] +#[allow(dead_code)] +pub type TransportProgress<'a> = IndexerProgress<'a>; + +/// A stream to write and index a packfile +/// +/// This is equivalent to [`crate::OdbPackwriter`], but allows to store the pack +/// and index at an arbitrary path. It also does not require access to an object +/// database if, and only if, the pack file is self-contained (i.e. not "thin"). +pub struct Indexer<'odb> { + raw: *mut raw::git_indexer, + progress: raw::git_indexer_progress, + progress_payload_ptr: *mut OdbPackwriterCb<'odb>, +} + +impl<'a> Indexer<'a> { + /// Create a new indexer + /// + /// The [`Odb`] is used to resolve base objects when fixing thin packs. It + /// can be `None` if no thin pack is expected, in which case missing bases + /// will result in an error. + /// + /// `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<Self, Error> { + 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-<checksum>.pack` and + /// `pack-<checksum>.idx` respectively). + pub fn commit(mut self) -> Result<String, Error> { + 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<F>(&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<IndexerProgress<'a>>); + + self + } +} + +impl io::Write for Indexer<'_> { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + 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).unwrap(), + )) + } 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/extra/git2/src/lib.rs b/extra/git2/src/lib.rs new file mode 100644 index 000000000..3dd6fe92e --- /dev/null +++ b/extra/git2/src/lib.rs @@ -0,0 +1,1668 @@ +//! # libgit2 bindings for Rust +//! +//! This library contains bindings to the [libgit2][1] C library which is used +//! 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/ +//! +//! 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 +//! is automatic as well as adding strong types to all interfaces (including +//! `Result`) +//! +//! ## Creating a `Repository` +//! +//! The `Repository` is the source from which almost all other objects in git-rs +//! are spawned. A repository can be created through opening, initializing, or +//! cloning. +//! +//! ### Initializing a new repository +//! +//! The `init` method will create a new repository, assuming one does not +//! already exist. +//! +//! ```no_run +//! # #![allow(unstable)] +//! use git2::Repository; +//! +//! let repo = match Repository::init("/path/to/a/repo") { +//! Ok(repo) => repo, +//! Err(e) => panic!("failed to init: {}", e), +//! }; +//! ``` +//! +//! ### Opening an existing repository +//! +//! ```no_run +//! # #![allow(unstable)] +//! use git2::Repository; +//! +//! let repo = match Repository::open("/path/to/a/repo") { +//! Ok(repo) => repo, +//! Err(e) => panic!("failed to open: {}", e), +//! }; +//! ``` +//! +//! ### Cloning an existing repository +//! +//! ```no_run +//! # #![allow(unstable)] +//! use git2::Repository; +//! +//! let url = "https://github.com/alexcrichton/git2-rs"; +//! let repo = match Repository::clone(url, "/path/to/a/repo") { +//! Ok(repo) => repo, +//! Err(e) => panic!("failed to clone: {}", e), +//! }; +//! ``` +//! +//! To clone using SSH, refer to [RepoBuilder](./build/struct.RepoBuilder.html). +//! +//! ## Working with a `Repository` +//! +//! All derivative objects, references, etc are attached to the lifetime of the +//! source `Repository`, to ensure that they do not outlive the repository +//! itself. + +#![doc(html_root_url = "https://docs.rs/git2/0.18")] +#![allow(trivial_numeric_casts, trivial_casts)] +#![deny(missing_docs)] +#![warn(rust_2018_idioms)] +#![cfg_attr(test, deny(warnings))] + +use bitflags::bitflags; +use libgit2_sys as raw; + +use std::ffi::{CStr, CString}; +use std::fmt; +use std::str; +use std::sync::Once; + +pub use crate::apply::{ApplyLocation, ApplyOptions}; +pub use crate::attr::AttrValue; +pub use crate::blame::{Blame, BlameHunk, BlameIter, BlameOptions}; +pub use crate::blob::{Blob, BlobWriter}; +pub use crate::branch::{Branch, Branches}; +pub use crate::buf::Buf; +pub use crate::cherrypick::CherrypickOptions; +pub use crate::commit::{Commit, Parents}; +pub use crate::config::{Config, ConfigEntries, ConfigEntry}; +pub use crate::cred::{Cred, CredentialHelper}; +pub use crate::describe::{Describe, DescribeFormatOptions, DescribeOptions}; +pub use crate::diff::{Deltas, Diff, DiffDelta, DiffFile, DiffOptions}; +pub use crate::diff::{DiffBinary, DiffBinaryFile, DiffBinaryKind, DiffPatchidOptions}; +pub use crate::diff::{DiffFindOptions, DiffHunk, DiffLine, DiffLineType, DiffStats}; +pub use crate::email::{Email, EmailCreateOptions}; +pub use crate::error::Error; +pub use crate::index::{ + Index, IndexConflict, IndexConflicts, IndexEntries, IndexEntry, IndexMatchedPath, +}; +pub use crate::indexer::{Indexer, IndexerProgress, Progress}; +pub use crate::mailmap::Mailmap; +pub use crate::mempack::Mempack; +pub use crate::merge::{AnnotatedCommit, MergeOptions}; +pub use crate::message::{ + message_prettify, message_trailers_bytes, message_trailers_strs, MessageTrailersBytes, + MessageTrailersBytesIterator, MessageTrailersStrs, MessageTrailersStrsIterator, + DEFAULT_COMMENT_CHAR, +}; +pub use crate::note::{Note, Notes}; +pub use crate::object::Object; +pub use crate::odb::{Odb, OdbObject, OdbPackwriter, OdbReader, OdbWriter}; +pub use crate::oid::Oid; +pub use crate::packbuilder::{PackBuilder, PackBuilderStage}; +pub use crate::patch::Patch; +pub use crate::pathspec::{Pathspec, PathspecFailedEntries, PathspecMatchList}; +pub use crate::pathspec::{PathspecDiffEntries, PathspecEntries}; +pub use crate::proxy_options::ProxyOptions; +pub use crate::push_update::PushUpdate; +pub use crate::rebase::{Rebase, RebaseOperation, RebaseOperationType, RebaseOptions}; +pub use crate::reference::{Reference, ReferenceNames, References}; +pub use crate::reflog::{Reflog, ReflogEntry, ReflogIter}; +pub use crate::refspec::Refspec; +pub use crate::remote::{ + FetchOptions, PushOptions, Refspecs, Remote, RemoteConnection, RemoteHead, RemoteRedirect, +}; +pub use crate::remote_callbacks::{CertificateCheckStatus, Credentials, RemoteCallbacks}; +pub use crate::remote_callbacks::{TransportMessage, UpdateTips}; +pub use crate::repo::{Repository, RepositoryInitOptions}; +pub use crate::revert::RevertOptions; +pub use crate::revspec::Revspec; +pub use crate::revwalk::Revwalk; +pub use crate::signature::Signature; +pub use crate::stash::{StashApplyOptions, StashApplyProgressCb, StashCb, StashSaveOptions}; +pub use crate::status::{StatusEntry, StatusIter, StatusOptions, StatusShow, Statuses}; +pub use crate::submodule::{Submodule, SubmoduleUpdateOptions}; +pub use crate::tag::Tag; +pub use crate::time::{IndexTime, Time}; +pub use crate::tracing::{trace_set, TraceLevel}; +pub use crate::transaction::Transaction; +pub use crate::tree::{Tree, TreeEntry, TreeIter, TreeWalkMode, TreeWalkResult}; +pub use crate::treebuilder::TreeBuilder; +pub use crate::util::IntoCString; +pub use crate::version::Version; +pub use crate::worktree::{Worktree, WorktreeAddOptions, WorktreeLockStatus, WorktreePruneOptions}; + +// Create a convinience method on bitflag struct which checks the given flag +macro_rules! is_bit_set { + ($name:ident, $flag:expr) => { + #[allow(missing_docs)] + pub fn $name(&self) -> bool { + self.intersects($flag) + } + }; +} + +/// An enumeration of possible errors that can happen when working with a git +/// repository. +// Note: We omit a few native error codes, as they are unlikely to be propagated +// to the library user. Currently: +// +// * GIT_EPASSTHROUGH +// * GIT_ITEROVER +// * GIT_RETRY +#[derive(PartialEq, Eq, Clone, Debug, Copy)] +pub enum ErrorCode { + /// Generic error + GenericError, + /// Requested object could not be found + NotFound, + /// Object exists preventing operation + Exists, + /// More than one object matches + Ambiguous, + /// Output buffer too short to hold data + BufSize, + /// User-generated error + User, + /// Operation not allowed on bare repository + BareRepo, + /// HEAD refers to branch with no commits + UnbornBranch, + /// Merge in progress prevented operation + Unmerged, + /// Reference was not fast-forwardable + NotFastForward, + /// Name/ref spec was not in a valid format + InvalidSpec, + /// Checkout conflicts prevented operation + Conflict, + /// Lock file prevented operation + Locked, + /// Reference value does not match expected + Modified, + /// Authentication error + Auth, + /// Server certificate is invalid + Certificate, + /// Patch/merge has already been applied + Applied, + /// The requested peel operation is not possible + Peel, + /// Unexpected EOF + Eof, + /// Invalid operation or input + Invalid, + /// Uncommitted changes in index prevented operation + Uncommitted, + /// Operation was not valid for a directory + Directory, + /// A merge conflict exists and cannot continue + MergeConflict, + /// Hashsum mismatch in object + HashsumMismatch, + /// Unsaved changes in the index would be overwritten + IndexDirty, + /// Patch application failed + ApplyFail, + /// The object is not owned by the current user + Owner, +} + +/// An enumeration of possible categories of things that can have +/// errors when working with a git repository. +#[derive(PartialEq, Eq, Clone, Debug, Copy)] +pub enum ErrorClass { + /// Uncategorized + None, + /// Out of memory or insufficient allocated space + NoMemory, + /// Syscall or standard system library error + Os, + /// Invalid input + Invalid, + /// Error resolving or manipulating a reference + Reference, + /// ZLib failure + Zlib, + /// Bad repository state + Repository, + /// Bad configuration + Config, + /// Regex failure + Regex, + /// Bad object + Odb, + /// Invalid index data + Index, + /// Error creating or obtaining an object + Object, + /// Network error + Net, + /// Error manipulating a tag + Tag, + /// Invalid value in tree + Tree, + /// Hashing or packing error + Indexer, + /// Error from SSL + Ssl, + /// Error involving submodules + Submodule, + /// Threading error + Thread, + /// Error manipulating a stash + Stash, + /// Checkout failure + Checkout, + /// Invalid FETCH_HEAD + FetchHead, + /// Merge failure + Merge, + /// SSH failure + Ssh, + /// Error manipulating filters + Filter, + /// Error reverting commit + Revert, + /// Error from a user callback + Callback, + /// Error cherry-picking commit + CherryPick, + /// Can't describe object + Describe, + /// Error during rebase + Rebase, + /// Filesystem-related error + Filesystem, + /// Invalid patch data + Patch, + /// Error involving worktrees + Worktree, + /// Hash library error or SHA-1 collision + Sha1, + /// HTTP error + Http, +} + +/// A listing of the possible states that a repository can be in. +#[derive(PartialEq, Eq, Clone, Debug, Copy)] +#[allow(missing_docs)] +pub enum RepositoryState { + Clean, + Merge, + Revert, + RevertSequence, + CherryPick, + CherryPickSequence, + Bisect, + Rebase, + RebaseInteractive, + RebaseMerge, + ApplyMailbox, + ApplyMailboxOrRebase, +} + +/// An enumeration of the possible directions for a remote. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Direction { + /// Data will be fetched (read) from this remote. + Fetch, + /// Data will be pushed (written) to this remote. + Push, +} + +/// An enumeration of the operations that can be performed for the `reset` +/// method on a `Repository`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ResetType { + /// Move the head to the given commit. + Soft, + /// Soft plus reset the index to the commit. + Mixed, + /// Mixed plus changes in the working tree are discarded. + Hard, +} + +/// An enumeration all possible kinds objects may have. +#[derive(PartialEq, Eq, Copy, Clone, Debug)] +pub enum ObjectType { + /// Any kind of git object + Any, + /// An object which corresponds to a git commit + Commit, + /// An object which corresponds to a git tree + Tree, + /// An object which corresponds to a git blob + Blob, + /// An object which corresponds to a git tag + Tag, +} + +/// An enumeration of all possible kinds of references. +#[derive(PartialEq, Eq, Copy, Clone, Debug)] +pub enum ReferenceType { + /// A reference which points at an object id. + Direct, + + /// A reference which points at another reference. + Symbolic, +} + +/// An enumeration for the possible types of branches +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +pub enum BranchType { + /// A local branch not on a remote. + Local, + /// A branch for a remote. + Remote, +} + +/// An enumeration of the possible priority levels of a config file. +/// +/// The levels corresponding to the escalation logic (higher to lower) when +/// searching for config entries. +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +pub enum ConfigLevel { + /// System-wide on Windows, for compatibility with portable git + ProgramData = 1, + /// System-wide configuration file, e.g. /etc/gitconfig + System, + /// XDG-compatible configuration file, e.g. ~/.config/git/config + XDG, + /// User-specific configuration, e.g. ~/.gitconfig + Global, + /// Repository specific config, e.g. $PWD/.git/config + Local, + /// Application specific configuration file + App, + /// Highest level available + Highest = -1, +} + +/// Merge file favor options for `MergeOptions` instruct the file-level +/// merging functionality how to deal with conflicting regions of the files. +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +pub enum FileFavor { + /// When a region of a file is changed in both branches, a conflict will be + /// recorded in the index so that git_checkout can produce a merge file with + /// conflict markers in the working directory. This is the default. + Normal, + /// When a region of a file is changed in both branches, the file created + /// in the index will contain the "ours" side of any conflicting region. + /// The index will not record a conflict. + Ours, + /// When a region of a file is changed in both branches, the file created + /// in the index will contain the "theirs" side of any conflicting region. + /// The index will not record a conflict. + Theirs, + /// When a region of a file is changed in both branches, the file created + /// in the index will contain each unique line from each side, which has + /// the result of combining both files. The index will not record a conflict. + Union, +} + +bitflags! { + /// Orderings that may be specified for Revwalk iteration. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct Sort: u32 { + /// Sort the repository contents in no particular ordering. + /// + /// This sorting is arbitrary, implementation-specific, and subject to + /// change at any time. This is the default sorting for new walkers. + const NONE = raw::GIT_SORT_NONE as u32; + + /// Sort the repository contents in topological order (children before + /// parents). + /// + /// This sorting mode can be combined with time sorting. + const TOPOLOGICAL = raw::GIT_SORT_TOPOLOGICAL as u32; + + /// Sort the repository contents by commit time. + /// + /// This sorting mode can be combined with topological sorting. + const TIME = raw::GIT_SORT_TIME as u32; + + /// Iterate through the repository contents in reverse order. + /// + /// This sorting mode can be combined with any others. + const REVERSE = raw::GIT_SORT_REVERSE as u32; + } +} + +impl Sort { + is_bit_set!(is_none, Sort::NONE); + is_bit_set!(is_topological, Sort::TOPOLOGICAL); + is_bit_set!(is_time, Sort::TIME); + is_bit_set!(is_reverse, Sort::REVERSE); +} + +bitflags! { + /// Types of credentials that can be requested by a credential callback. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct CredentialType: u32 { + #[allow(missing_docs)] + const USER_PASS_PLAINTEXT = raw::GIT_CREDTYPE_USERPASS_PLAINTEXT as u32; + #[allow(missing_docs)] + const SSH_KEY = raw::GIT_CREDTYPE_SSH_KEY as u32; + #[allow(missing_docs)] + const SSH_MEMORY = raw::GIT_CREDTYPE_SSH_MEMORY as u32; + #[allow(missing_docs)] + const SSH_CUSTOM = raw::GIT_CREDTYPE_SSH_CUSTOM as u32; + #[allow(missing_docs)] + const DEFAULT = raw::GIT_CREDTYPE_DEFAULT as u32; + #[allow(missing_docs)] + const SSH_INTERACTIVE = raw::GIT_CREDTYPE_SSH_INTERACTIVE as u32; + #[allow(missing_docs)] + const USERNAME = raw::GIT_CREDTYPE_USERNAME as u32; + } +} + +impl CredentialType { + is_bit_set!(is_user_pass_plaintext, CredentialType::USER_PASS_PLAINTEXT); + is_bit_set!(is_ssh_key, CredentialType::SSH_KEY); + is_bit_set!(is_ssh_memory, CredentialType::SSH_MEMORY); + is_bit_set!(is_ssh_custom, CredentialType::SSH_CUSTOM); + is_bit_set!(is_default, CredentialType::DEFAULT); + is_bit_set!(is_ssh_interactive, CredentialType::SSH_INTERACTIVE); + is_bit_set!(is_username, CredentialType::USERNAME); +} + +impl Default for CredentialType { + fn default() -> Self { + CredentialType::DEFAULT + } +} + +bitflags! { + /// Flags for the `flags` field of an IndexEntry. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct IndexEntryFlag: u16 { + /// Set when the `extended_flags` field is valid. + const EXTENDED = raw::GIT_INDEX_ENTRY_EXTENDED as u16; + /// "Assume valid" flag + const VALID = raw::GIT_INDEX_ENTRY_VALID as u16; + } +} + +impl IndexEntryFlag { + is_bit_set!(is_extended, IndexEntryFlag::EXTENDED); + is_bit_set!(is_valid, IndexEntryFlag::VALID); +} + +bitflags! { + /// Flags for the `extended_flags` field of an IndexEntry. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + 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; + /// Skip the associated worktree file, for sparse checkouts + const SKIP_WORKTREE = raw::GIT_INDEX_ENTRY_SKIP_WORKTREE as u16; + + #[allow(missing_docs)] + const UPTODATE = raw::GIT_INDEX_ENTRY_UPTODATE as u16; + } +} + +impl IndexEntryExtendedFlag { + is_bit_set!(is_intent_to_add, IndexEntryExtendedFlag::INTENT_TO_ADD); + is_bit_set!(is_skip_worktree, IndexEntryExtendedFlag::SKIP_WORKTREE); + is_bit_set!(is_up_to_date, IndexEntryExtendedFlag::UPTODATE); +} + +bitflags! { + /// Flags for APIs that add files matching pathspec + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct IndexAddOption: u32 { + #[allow(missing_docs)] + const DEFAULT = raw::GIT_INDEX_ADD_DEFAULT as u32; + #[allow(missing_docs)] + const FORCE = raw::GIT_INDEX_ADD_FORCE as u32; + #[allow(missing_docs)] + const DISABLE_PATHSPEC_MATCH = + raw::GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH as u32; + #[allow(missing_docs)] + const CHECK_PATHSPEC = raw::GIT_INDEX_ADD_CHECK_PATHSPEC as u32; + } +} + +impl IndexAddOption { + is_bit_set!(is_default, IndexAddOption::DEFAULT); + is_bit_set!(is_force, IndexAddOption::FORCE); + is_bit_set!( + is_disable_pathspec_match, + IndexAddOption::DISABLE_PATHSPEC_MATCH + ); + is_bit_set!(is_check_pathspec, IndexAddOption::CHECK_PATHSPEC); +} + +impl Default for IndexAddOption { + fn default() -> Self { + IndexAddOption::DEFAULT + } +} + +bitflags! { + /// Flags for `Repository::open_ext` + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + 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; + /// Search across filesystem boundaries. + const CROSS_FS = raw::GIT_REPOSITORY_OPEN_CROSS_FS as u32; + /// Force opening as bare repository, and defer loading its config. + const BARE = raw::GIT_REPOSITORY_OPEN_BARE as u32; + /// Don't try appending `/.git` to the specified repository path. + const NO_DOTGIT = raw::GIT_REPOSITORY_OPEN_NO_DOTGIT as u32; + /// Respect environment variables like `$GIT_DIR`. + const FROM_ENV = raw::GIT_REPOSITORY_OPEN_FROM_ENV as u32; + } +} + +impl RepositoryOpenFlags { + is_bit_set!(is_no_search, RepositoryOpenFlags::NO_SEARCH); + is_bit_set!(is_cross_fs, RepositoryOpenFlags::CROSS_FS); + is_bit_set!(is_bare, RepositoryOpenFlags::BARE); + is_bit_set!(is_no_dotgit, RepositoryOpenFlags::NO_DOTGIT); + is_bit_set!(is_from_env, RepositoryOpenFlags::FROM_ENV); +} + +bitflags! { + /// Flags for the return value of `Repository::revparse` + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct RevparseMode: u32 { + /// The spec targeted a single object + const SINGLE = raw::GIT_REVPARSE_SINGLE as u32; + /// The spec targeted a range of commits + const RANGE = raw::GIT_REVPARSE_RANGE as u32; + /// The spec used the `...` operator, which invokes special semantics. + const MERGE_BASE = raw::GIT_REVPARSE_MERGE_BASE as u32; + } +} + +impl RevparseMode { + is_bit_set!(is_no_single, RevparseMode::SINGLE); + is_bit_set!(is_range, RevparseMode::RANGE); + is_bit_set!(is_merge_base, RevparseMode::MERGE_BASE); +} + +bitflags! { + /// The results of `merge_analysis` indicating the merge opportunities. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct MergeAnalysis: u32 { + /// No merge is possible. + const ANALYSIS_NONE = raw::GIT_MERGE_ANALYSIS_NONE as u32; + /// A "normal" merge; both HEAD and the given merge input have diverged + /// from their common ancestor. The divergent commits must be merged. + const ANALYSIS_NORMAL = raw::GIT_MERGE_ANALYSIS_NORMAL as u32; + /// All given merge inputs are reachable from HEAD, meaning the + /// repository is up-to-date and no merge needs to be performed. + const ANALYSIS_UP_TO_DATE = raw::GIT_MERGE_ANALYSIS_UP_TO_DATE as u32; + /// The given merge input is a fast-forward from HEAD and no merge + /// needs to be performed. Instead, the client can check out the + /// given merge input. + const ANALYSIS_FASTFORWARD = raw::GIT_MERGE_ANALYSIS_FASTFORWARD as u32; + /// The HEAD of the current repository is "unborn" and does not point to + /// a valid commit. No merge can be performed, but the caller may wish + /// to simply set HEAD to the target commit(s). + const ANALYSIS_UNBORN = raw::GIT_MERGE_ANALYSIS_UNBORN as u32; + } +} + +impl MergeAnalysis { + is_bit_set!(is_none, MergeAnalysis::ANALYSIS_NONE); + is_bit_set!(is_normal, MergeAnalysis::ANALYSIS_NORMAL); + is_bit_set!(is_up_to_date, MergeAnalysis::ANALYSIS_UP_TO_DATE); + is_bit_set!(is_fast_forward, MergeAnalysis::ANALYSIS_FASTFORWARD); + is_bit_set!(is_unborn, MergeAnalysis::ANALYSIS_UNBORN); +} + +bitflags! { + /// The user's stated preference for merges. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct MergePreference: u32 { + /// No configuration was found that suggests a preferred behavior for + /// merge. + const NONE = raw::GIT_MERGE_PREFERENCE_NONE as u32; + /// There is a `merge.ff=false` configuration setting, suggesting that + /// the user does not want to allow a fast-forward merge. + const NO_FAST_FORWARD = raw::GIT_MERGE_PREFERENCE_NO_FASTFORWARD as u32; + /// There is a `merge.ff=only` configuration setting, suggesting that + /// the user only wants fast-forward merges. + const FASTFORWARD_ONLY = raw::GIT_MERGE_PREFERENCE_FASTFORWARD_ONLY as u32; + } +} + +impl MergePreference { + is_bit_set!(is_none, MergePreference::NONE); + is_bit_set!(is_no_fast_forward, MergePreference::NO_FAST_FORWARD); + is_bit_set!(is_fastforward_only, MergePreference::FASTFORWARD_ONLY); +} + +bitflags! { + /// Flags controlling the behavior of ODB lookup operations + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + 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; + } +} + +#[cfg(test)] +#[macro_use] +mod test; +#[macro_use] +mod panic; +mod attr; +mod call; +mod util; + +pub mod build; +pub mod cert; +pub mod oid_array; +pub mod opts; +pub mod string_array; +pub mod transport; + +mod apply; +mod blame; +mod blob; +mod branch; +mod buf; +mod cherrypick; +mod commit; +mod config; +mod cred; +mod describe; +mod diff; +mod email; +mod error; +mod index; +mod indexer; +mod mailmap; +mod mempack; +mod merge; +mod message; +mod note; +mod object; +mod odb; +mod oid; +mod packbuilder; +mod patch; +mod pathspec; +mod proxy_options; +mod push_update; +mod rebase; +mod reference; +mod reflog; +mod refspec; +mod remote; +mod remote_callbacks; +mod repo; +mod revert; +mod revspec; +mod revwalk; +mod signature; +mod stash; +mod status; +mod submodule; +mod tag; +mod tagforeach; +mod time; +mod tracing; +mod transaction; +mod tree; +mod treebuilder; +mod version; +mod worktree; + +fn init() { + static INIT: Once = Once::new(); + + INIT.call_once(|| { + openssl_env_init(); + }); + + raw::init(); +} + +#[cfg(all( + unix, + not(target_os = "macos"), + not(target_os = "ios"), + feature = "https" +))] +fn openssl_env_init() { + // Currently, libgit2 leverages OpenSSL for SSL support when cloning + // repositories over HTTPS. This means that we're picking up an OpenSSL + // dependency on non-Windows platforms (where it has its own HTTPS + // subsystem). As a result, we need to link to OpenSSL. + // + // Now actually *linking* to OpenSSL isn't so hard. We just need to make + // sure to use pkg-config to discover any relevant system dependencies for + // differences between distributions like CentOS and Ubuntu. The actual + // trickiness comes about when we start *distributing* the resulting + // binaries. Currently Cargo is distributed in binary form as nightlies, + // which means we're distributing a binary with OpenSSL linked in. + // + // For historical reasons, the Linux nightly builder is running a CentOS + // distribution in order to have as much ABI compatibility with other + // distributions as possible. Sadly, however, this compatibility does not + // extend to OpenSSL. Currently OpenSSL has two major versions, 0.9 and 1.0, + // which are incompatible (many ABI differences). The CentOS builder we + // build on has version 1.0, as do most distributions today. Some still have + // 0.9, however. This means that if we are to distribute the binaries built + // by the CentOS machine, we would only be compatible with OpenSSL 1.0 and + // we would fail to run (a dynamic linker error at runtime) on systems with + // only 9.8 installed (hopefully). + // + // But wait, the plot thickens! Apparently CentOS has dubbed their OpenSSL + // library as `libssl.so.10`, notably the `10` is included at the end. On + // the other hand Ubuntu, for example, only distributes `libssl.so`. This + // means that the binaries created at CentOS are hard-wired to probe for a + // file called `libssl.so.10` at runtime (using the LD_LIBRARY_PATH), which + // will not be found on ubuntu. The conclusion of this is that binaries + // built on CentOS cannot be distributed to Ubuntu and run successfully. + // + // There are a number of sneaky things we could do, including, but not + // limited to: + // + // 1. Create a shim program which runs "just before" cargo runs. The + // responsibility of this shim program would be to locate `libssl.so`, + // whatever it's called, on the current system, make sure there's a + // symlink *somewhere* called `libssl.so.10`, and then set up + // LD_LIBRARY_PATH and run the actual cargo. + // + // This approach definitely seems unconventional, and is borderline + // overkill for this problem. It's also dubious if we can find a + // libssl.so reliably on the target system. + // + // 2. Somehow re-work the CentOS installation so that the linked-against + // library is called libssl.so instead of libssl.so.10 + // + // The problem with this approach is that systems with 0.9 installed will + // start to silently fail, due to also having libraries called libssl.so + // (probably symlinked under a more appropriate version). + // + // 3. Compile Cargo against both OpenSSL 1.0 *and* OpenSSL 0.9, and + // distribute both. Also make sure that the linked-against name of the + // library is `libssl.so`. At runtime we determine which version is + // installed, and we then the appropriate binary. + // + // This approach clearly has drawbacks in terms of infrastructure and + // feasibility. + // + // 4. Build a nightly of Cargo for each distribution we'd like to support. + // You would then pick the appropriate Cargo nightly to install locally. + // + // So, with all this in mind, the decision was made to *statically* link + // OpenSSL. This solves any problem of relying on a downstream OpenSSL + // version being available. This does, however, open a can of worms related + // to security issues. It's generally a good idea to dynamically link + // OpenSSL as you'll get security updates over time without having to do + // anything (the system administrator will update the local openssl + // package). By statically linking, we're forfeiting this feature. + // + // The conclusion was made it is likely appropriate for the Cargo nightlies + // to statically link OpenSSL, but highly encourage distributions and + // packagers of Cargo to dynamically link OpenSSL. Packagers are targeting + // one system and are distributing to only that system, so none of the + // problems mentioned above would arise. + // + // In order to support this, a new package was made: openssl-static-sys. + // This package currently performs a fairly simple task: + // + // 1. Run pkg-config to discover where openssl is installed. + // 2. If openssl is installed in a nonstandard location, *and* static copies + // of the libraries are available, copy them to $OUT_DIR. + // + // This library will bring in libssl.a and libcrypto.a into the local build, + // allowing them to be picked up by this crate. This allows us to configure + // our own buildbots to have pkg-config point to these local pre-built + // copies of a static OpenSSL (with very few dependencies) while allowing + // most other builds of Cargo to naturally dynamically link OpenSSL. + // + // So in summary, if you're with me so far, we've statically linked OpenSSL + // to the Cargo binary (or any binary, for that matter) and we're ready to + // distribute it to *all* linux distributions. Remember that our original + // intent for openssl was for HTTPS support, which implies that we need some + // for of CA certificate store to validate certificates. This is normally + // installed in a standard system location. + // + // Unfortunately, as one might imagine, OpenSSL is configured for where this + // standard location is at *build time*, but it often varies widely + // per-system. Consequently, it was discovered that OpenSSL will respect the + // SSL_CERT_FILE and SSL_CERT_DIR environment variables in order to assist + // in discovering the location of this file (hurray!). + // + // So, finally getting to the point, this function solely exists to support + // our static builds of OpenSSL by probing for the "standard system + // location" of certificates and setting relevant environment variable to + // point to them. + // + // Ah, and as a final note, this is only a problem on Linux, not on OS X. On + // OS X the OpenSSL binaries are stable enough that we can just rely on + // dynamic linkage (plus they have some weird modifications to OpenSSL which + // means we wouldn't want to link statically). + openssl_probe::init_ssl_cert_env_vars(); +} + +#[cfg(any( + windows, + target_os = "macos", + target_os = "ios", + not(feature = "https") +))] +fn openssl_env_init() {} + +unsafe fn opt_bytes<'a, T>(_anchor: &'a T, c: *const libc::c_char) -> Option<&'a [u8]> { + if c.is_null() { + None + } else { + Some(CStr::from_ptr(c).to_bytes()) + } +} + +fn opt_cstr<T: IntoCString>(o: Option<T>) -> Result<Option<CString>, Error> { + match o { + Some(s) => s.into_c_string().map(Some), + None => Ok(None), + } +} + +impl ObjectType { + /// Convert an object type to its string representation. + pub fn str(&self) -> &'static str { + unsafe { + let ptr = call!(raw::git_object_type2string(*self)) as *const _; + let data = CStr::from_ptr(ptr).to_bytes(); + str::from_utf8(data).unwrap() + } + } + + /// Determine if the given git_object_t is a valid loose object type. + pub fn is_loose(&self) -> bool { + unsafe { call!(raw::git_object_typeisloose(*self)) == 1 } + } + + /// Convert a raw git_object_t to an ObjectType + pub fn from_raw(raw: raw::git_object_t) -> Option<ObjectType> { + match raw { + raw::GIT_OBJECT_ANY => Some(ObjectType::Any), + raw::GIT_OBJECT_COMMIT => Some(ObjectType::Commit), + raw::GIT_OBJECT_TREE => Some(ObjectType::Tree), + raw::GIT_OBJECT_BLOB => Some(ObjectType::Blob), + raw::GIT_OBJECT_TAG => Some(ObjectType::Tag), + _ => None, + } + } + + /// Convert this kind into its raw representation + pub fn raw(&self) -> raw::git_object_t { + call::convert(self) + } + + /// Convert a string object type representation to its object type. + pub fn from_str(s: &str) -> Option<ObjectType> { + let raw = unsafe { call!(raw::git_object_string2type(CString::new(s).unwrap())) }; + ObjectType::from_raw(raw) + } +} + +impl fmt::Display for ObjectType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.str().fmt(f) + } +} + +impl ReferenceType { + /// Convert an object type to its string representation. + pub fn str(&self) -> &'static str { + match self { + ReferenceType::Direct => "direct", + ReferenceType::Symbolic => "symbolic", + } + } + + /// Convert a raw git_reference_t to a ReferenceType. + pub fn from_raw(raw: raw::git_reference_t) -> Option<ReferenceType> { + match raw { + raw::GIT_REFERENCE_DIRECT => Some(ReferenceType::Direct), + raw::GIT_REFERENCE_SYMBOLIC => Some(ReferenceType::Symbolic), + _ => None, + } + } +} + +impl fmt::Display for ReferenceType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.str().fmt(f) + } +} + +impl ConfigLevel { + /// Converts a raw configuration level to a ConfigLevel + pub fn from_raw(raw: raw::git_config_level_t) -> ConfigLevel { + match raw { + raw::GIT_CONFIG_LEVEL_PROGRAMDATA => ConfigLevel::ProgramData, + raw::GIT_CONFIG_LEVEL_SYSTEM => ConfigLevel::System, + 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_APP => ConfigLevel::App, + raw::GIT_CONFIG_HIGHEST_LEVEL => ConfigLevel::Highest, + n => panic!("unknown config level: {}", n), + } + } +} + +impl SubmoduleIgnore { + /// Converts a [`raw::git_submodule_ignore_t`] to a [`SubmoduleIgnore`] + pub fn from_raw(raw: raw::git_submodule_ignore_t) -> Self { + match raw { + raw::GIT_SUBMODULE_IGNORE_UNSPECIFIED => SubmoduleIgnore::Unspecified, + raw::GIT_SUBMODULE_IGNORE_NONE => SubmoduleIgnore::None, + raw::GIT_SUBMODULE_IGNORE_UNTRACKED => SubmoduleIgnore::Untracked, + raw::GIT_SUBMODULE_IGNORE_DIRTY => SubmoduleIgnore::Dirty, + raw::GIT_SUBMODULE_IGNORE_ALL => SubmoduleIgnore::All, + n => panic!("unknown submodule ignore rule: {}", n), + } + } +} + +impl SubmoduleUpdate { + /// Converts a [`raw::git_submodule_update_t`] to a [`SubmoduleUpdate`] + pub fn from_raw(raw: raw::git_submodule_update_t) -> Self { + match raw { + raw::GIT_SUBMODULE_UPDATE_CHECKOUT => SubmoduleUpdate::Checkout, + raw::GIT_SUBMODULE_UPDATE_REBASE => SubmoduleUpdate::Rebase, + raw::GIT_SUBMODULE_UPDATE_MERGE => SubmoduleUpdate::Merge, + raw::GIT_SUBMODULE_UPDATE_NONE => SubmoduleUpdate::None, + raw::GIT_SUBMODULE_UPDATE_DEFAULT => SubmoduleUpdate::Default, + n => panic!("unknown submodule update strategy: {}", n), + } + } +} + +bitflags! { + /// Status flags for a single file + /// + /// A combination of these values will be returned to indicate the status of + /// a file. Status compares the working directory, the index, and the + /// current HEAD of the repository. The `STATUS_INDEX_*` set of flags + /// 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)] + pub struct Status: u32 { + #[allow(missing_docs)] + const CURRENT = raw::GIT_STATUS_CURRENT as u32; + + #[allow(missing_docs)] + const INDEX_NEW = raw::GIT_STATUS_INDEX_NEW as u32; + #[allow(missing_docs)] + const INDEX_MODIFIED = raw::GIT_STATUS_INDEX_MODIFIED as u32; + #[allow(missing_docs)] + const INDEX_DELETED = raw::GIT_STATUS_INDEX_DELETED as u32; + #[allow(missing_docs)] + const INDEX_RENAMED = raw::GIT_STATUS_INDEX_RENAMED as u32; + #[allow(missing_docs)] + const INDEX_TYPECHANGE = raw::GIT_STATUS_INDEX_TYPECHANGE as u32; + + #[allow(missing_docs)] + const WT_NEW = raw::GIT_STATUS_WT_NEW as u32; + #[allow(missing_docs)] + const WT_MODIFIED = raw::GIT_STATUS_WT_MODIFIED as u32; + #[allow(missing_docs)] + const WT_DELETED = raw::GIT_STATUS_WT_DELETED as u32; + #[allow(missing_docs)] + 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 IGNORED = raw::GIT_STATUS_IGNORED as u32; + #[allow(missing_docs)] + const CONFLICTED = raw::GIT_STATUS_CONFLICTED as u32; + } +} + +impl Status { + is_bit_set!(is_index_new, Status::INDEX_NEW); + is_bit_set!(is_index_modified, Status::INDEX_MODIFIED); + is_bit_set!(is_index_deleted, Status::INDEX_DELETED); + is_bit_set!(is_index_renamed, Status::INDEX_RENAMED); + is_bit_set!(is_index_typechange, Status::INDEX_TYPECHANGE); + is_bit_set!(is_wt_new, Status::WT_NEW); + is_bit_set!(is_wt_modified, Status::WT_MODIFIED); + is_bit_set!(is_wt_deleted, Status::WT_DELETED); + is_bit_set!(is_wt_typechange, Status::WT_TYPECHANGE); + is_bit_set!(is_wt_renamed, Status::WT_RENAMED); + is_bit_set!(is_ignored, Status::IGNORED); + is_bit_set!(is_conflicted, Status::CONFLICTED); +} + +bitflags! { + /// Mode options for RepositoryInitOptions + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct RepositoryInitMode: u32 { + /// Use permissions configured by umask - the default + const SHARED_UMASK = raw::GIT_REPOSITORY_INIT_SHARED_UMASK as u32; + /// Use `--shared=group` behavior, chmod'ing the new repo to be + /// group writable and \"g+sx\" for sticky group assignment + const SHARED_GROUP = raw::GIT_REPOSITORY_INIT_SHARED_GROUP as u32; + /// Use `--shared=all` behavior, adding world readability. + const SHARED_ALL = raw::GIT_REPOSITORY_INIT_SHARED_ALL as u32; + } +} + +impl RepositoryInitMode { + is_bit_set!(is_shared_umask, RepositoryInitMode::SHARED_UMASK); + is_bit_set!(is_shared_group, RepositoryInitMode::SHARED_GROUP); + is_bit_set!(is_shared_all, RepositoryInitMode::SHARED_ALL); +} + +/// What type of change is described by a `DiffDelta`? +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Delta { + /// No changes + Unmodified, + /// Entry does not exist in old version + Added, + /// Entry does not exist in new version + Deleted, + /// Entry content changed between old and new + Modified, + /// Entry was renamed between old and new + Renamed, + /// Entry was copied from another old entry + Copied, + /// Entry is ignored item in workdir + Ignored, + /// Entry is untracked item in workdir + Untracked, + /// Type of entry changed between old and new + Typechange, + /// Entry is unreadable + Unreadable, + /// Entry in the index is conflicted + Conflicted, +} + +/// Valid modes for index and tree entries. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum FileMode { + /// Unreadable + Unreadable, + /// Tree + Tree, + /// Blob + Blob, + /// Group writable blob. Obsolete mode kept for compatibility reasons + BlobGroupWritable, + /// Blob executable + BlobExecutable, + /// Link + Link, + /// Commit + Commit, +} + +impl From<FileMode> for i32 { + fn from(mode: FileMode) -> i32 { + match mode { + FileMode::Unreadable => raw::GIT_FILEMODE_UNREADABLE as i32, + FileMode::Tree => raw::GIT_FILEMODE_TREE as i32, + FileMode::Blob => raw::GIT_FILEMODE_BLOB as i32, + FileMode::BlobGroupWritable => raw::GIT_FILEMODE_BLOB_GROUP_WRITABLE as i32, + FileMode::BlobExecutable => raw::GIT_FILEMODE_BLOB_EXECUTABLE as i32, + FileMode::Link => raw::GIT_FILEMODE_LINK as i32, + FileMode::Commit => raw::GIT_FILEMODE_COMMIT as i32, + } + } +} + +impl From<FileMode> for u32 { + fn from(mode: FileMode) -> u32 { + match mode { + FileMode::Unreadable => raw::GIT_FILEMODE_UNREADABLE as u32, + FileMode::Tree => raw::GIT_FILEMODE_TREE as u32, + FileMode::Blob => raw::GIT_FILEMODE_BLOB as u32, + FileMode::BlobGroupWritable => raw::GIT_FILEMODE_BLOB_GROUP_WRITABLE as u32, + FileMode::BlobExecutable => raw::GIT_FILEMODE_BLOB_EXECUTABLE as u32, + FileMode::Link => raw::GIT_FILEMODE_LINK as u32, + FileMode::Commit => raw::GIT_FILEMODE_COMMIT as u32, + } + } +} + +bitflags! { + /// Return codes for submodule status. + /// + /// A combination of these flags will be returned to describe the status of a + /// submodule. Depending on the "ignore" property of the submodule, some of + /// the flags may never be returned because they indicate changes that are + /// supposed to be ignored. + /// + /// Submodule info is contained in 4 places: the HEAD tree, the index, config + /// files (both .git/config and .gitmodules), and the working directory. Any + /// or all of those places might be missing information about the submodule + /// depending on what state the repo is in. We consider all four places to + /// build the combination of status flags. + /// + /// There are four values that are not really status, but give basic info + /// about what sources of submodule data are available. These will be + /// returned even if ignore is set to "ALL". + /// + /// * IN_HEAD - superproject head contains submodule + /// * IN_INDEX - superproject index contains submodule + /// * IN_CONFIG - superproject gitmodules has submodule + /// * IN_WD - superproject workdir has submodule + /// + /// The following values will be returned so long as ignore is not "ALL". + /// + /// * INDEX_ADDED - in index, not in head + /// * INDEX_DELETED - in head, not in index + /// * INDEX_MODIFIED - index and head don't match + /// * WD_UNINITIALIZED - workdir contains empty directory + /// * WD_ADDED - in workdir, not index + /// * WD_DELETED - in index, not workdir + /// * WD_MODIFIED - index and workdir head don't match + /// + /// The following can only be returned if ignore is "NONE" or "UNTRACKED". + /// + /// * WD_INDEX_MODIFIED - submodule workdir index is dirty + /// * WD_WD_MODIFIED - submodule workdir has modified files + /// + /// Lastly, the following will only be returned for ignore "NONE". + /// + /// * WD_UNTRACKED - workdir contains untracked files + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct SubmoduleStatus: u32 { + #[allow(missing_docs)] + const IN_HEAD = raw::GIT_SUBMODULE_STATUS_IN_HEAD as u32; + #[allow(missing_docs)] + const IN_INDEX = raw::GIT_SUBMODULE_STATUS_IN_INDEX as u32; + #[allow(missing_docs)] + const IN_CONFIG = raw::GIT_SUBMODULE_STATUS_IN_CONFIG as u32; + #[allow(missing_docs)] + const IN_WD = raw::GIT_SUBMODULE_STATUS_IN_WD as u32; + #[allow(missing_docs)] + const INDEX_ADDED = raw::GIT_SUBMODULE_STATUS_INDEX_ADDED as u32; + #[allow(missing_docs)] + const INDEX_DELETED = raw::GIT_SUBMODULE_STATUS_INDEX_DELETED as u32; + #[allow(missing_docs)] + const INDEX_MODIFIED = raw::GIT_SUBMODULE_STATUS_INDEX_MODIFIED as u32; + #[allow(missing_docs)] + const WD_UNINITIALIZED = + raw::GIT_SUBMODULE_STATUS_WD_UNINITIALIZED as u32; + #[allow(missing_docs)] + const WD_ADDED = raw::GIT_SUBMODULE_STATUS_WD_ADDED as u32; + #[allow(missing_docs)] + const WD_DELETED = raw::GIT_SUBMODULE_STATUS_WD_DELETED as u32; + #[allow(missing_docs)] + const WD_MODIFIED = raw::GIT_SUBMODULE_STATUS_WD_MODIFIED as u32; + #[allow(missing_docs)] + const WD_INDEX_MODIFIED = + raw::GIT_SUBMODULE_STATUS_WD_INDEX_MODIFIED as u32; + #[allow(missing_docs)] + const WD_WD_MODIFIED = raw::GIT_SUBMODULE_STATUS_WD_WD_MODIFIED as u32; + #[allow(missing_docs)] + const WD_UNTRACKED = raw::GIT_SUBMODULE_STATUS_WD_UNTRACKED as u32; + } +} + +impl SubmoduleStatus { + is_bit_set!(is_in_head, SubmoduleStatus::IN_HEAD); + is_bit_set!(is_in_index, SubmoduleStatus::IN_INDEX); + is_bit_set!(is_in_config, SubmoduleStatus::IN_CONFIG); + is_bit_set!(is_in_wd, SubmoduleStatus::IN_WD); + is_bit_set!(is_index_added, SubmoduleStatus::INDEX_ADDED); + is_bit_set!(is_index_deleted, SubmoduleStatus::INDEX_DELETED); + is_bit_set!(is_index_modified, SubmoduleStatus::INDEX_MODIFIED); + is_bit_set!(is_wd_uninitialized, SubmoduleStatus::WD_UNINITIALIZED); + is_bit_set!(is_wd_added, SubmoduleStatus::WD_ADDED); + is_bit_set!(is_wd_deleted, SubmoduleStatus::WD_DELETED); + is_bit_set!(is_wd_modified, SubmoduleStatus::WD_MODIFIED); + is_bit_set!(is_wd_wd_modified, SubmoduleStatus::WD_WD_MODIFIED); + is_bit_set!(is_wd_untracked, SubmoduleStatus::WD_UNTRACKED); +} + +/// Submodule ignore values +/// +/// These values represent settings for the `submodule.$name.ignore` +/// configuration value which says how deeply to look at the working +/// directory when getting the submodule status. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum SubmoduleIgnore { + /// Use the submodule's configuration + Unspecified, + /// Any change or untracked file is considered dirty + None, + /// Only dirty if tracked files have changed + Untracked, + /// Only dirty if HEAD has moved + Dirty, + /// Never dirty + All, +} + +/// Submodule update values +/// +/// These values represent settings for the `submodule.$name.update` +/// configuration value which says how to handle `git submodule update` +/// for this submodule. The value is usually set in the ".gitmodules" +/// file and copied to ".git/config" when the submodule is initialized. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum SubmoduleUpdate { + /// The default; when a submodule is updated, checkout the new detached + /// HEAD to the submodule directory. + Checkout, + /// Update by rebasing the current checked out branch onto the commit from + /// the superproject. + Rebase, + /// Update by merging the commit in the superproject into the current + /// checkout out branch of the submodule. + Merge, + /// Do not update this submodule even when the commit in the superproject + /// is updated. + None, + /// Not used except as static initializer when we don't want any particular + /// update rule to be specified. + Default, +} + +bitflags! { + /// ... + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + 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 of the platform filesystem. + const IGNORE_CASE = raw::GIT_PATHSPEC_IGNORE_CASE as u32; + /// Force case sensitive matches, otherwise match will use the native + /// case sensitivity of the platform filesystem. + const USE_CASE = raw::GIT_PATHSPEC_USE_CASE as u32; + /// Disable glob patterns and just use simple string comparison for + /// matching. + const NO_GLOB = raw::GIT_PATHSPEC_NO_GLOB as u32; + /// Means that match functions return the error code `NotFound` if no + /// matches are found. By default no matches is a success. + const NO_MATCH_ERROR = raw::GIT_PATHSPEC_NO_MATCH_ERROR as u32; + /// Means that the list returned should track which patterns matched + /// which files so that at the end of the match we can identify patterns + /// that did not match any files. + const FIND_FAILURES = raw::GIT_PATHSPEC_FIND_FAILURES as u32; + /// Means that the list returned does not need to keep the actual + /// matching filenames. Use this to just test if there were any matches + /// at all or in combination with `PATHSPEC_FAILURES` to validate a + /// pathspec. + const FAILURES_ONLY = raw::GIT_PATHSPEC_FAILURES_ONLY as u32; + } +} + +impl PathspecFlags { + is_bit_set!(is_default, PathspecFlags::DEFAULT); + is_bit_set!(is_ignore_case, PathspecFlags::IGNORE_CASE); + is_bit_set!(is_use_case, PathspecFlags::USE_CASE); + is_bit_set!(is_no_glob, PathspecFlags::NO_GLOB); + is_bit_set!(is_no_match_error, PathspecFlags::NO_MATCH_ERROR); + is_bit_set!(is_find_failures, PathspecFlags::FIND_FAILURES); + is_bit_set!(is_failures_only, PathspecFlags::FAILURES_ONLY); +} + +impl Default for PathspecFlags { + fn default() -> Self { + PathspecFlags::DEFAULT + } +} + +bitflags! { + /// Types of notifications emitted from checkouts. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct CheckoutNotificationType: u32 { + /// Notification about a conflict. + const CONFLICT = raw::GIT_CHECKOUT_NOTIFY_CONFLICT as u32; + /// Notification about a dirty file. + const DIRTY = raw::GIT_CHECKOUT_NOTIFY_DIRTY as u32; + /// Notification about an updated file. + const UPDATED = raw::GIT_CHECKOUT_NOTIFY_UPDATED as u32; + /// Notification about an untracked file. + const UNTRACKED = raw::GIT_CHECKOUT_NOTIFY_UNTRACKED as u32; + /// Notification about an ignored file. + const IGNORED = raw::GIT_CHECKOUT_NOTIFY_IGNORED as u32; + } +} + +impl CheckoutNotificationType { + is_bit_set!(is_conflict, CheckoutNotificationType::CONFLICT); + is_bit_set!(is_dirty, CheckoutNotificationType::DIRTY); + is_bit_set!(is_updated, CheckoutNotificationType::UPDATED); + is_bit_set!(is_untracked, CheckoutNotificationType::UNTRACKED); + is_bit_set!(is_ignored, CheckoutNotificationType::IGNORED); +} + +/// Possible output formats for diff data +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum DiffFormat { + /// full git diff + Patch, + /// just the headers of the patch + PatchHeader, + /// like git diff --raw + Raw, + /// like git diff --name-only + NameOnly, + /// like git diff --name-status + NameStatus, + /// git diff as used by git patch-id + PatchId, +} + +bitflags! { + /// Formatting options for diff stats + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct DiffStatsFormat: raw::git_diff_stats_format_t { + /// Don't generate any stats + const NONE = raw::GIT_DIFF_STATS_NONE; + /// Equivalent of `--stat` in git + const FULL = raw::GIT_DIFF_STATS_FULL; + /// Equivalent of `--shortstat` in git + const SHORT = raw::GIT_DIFF_STATS_SHORT; + /// Equivalent of `--numstat` in git + const NUMBER = raw::GIT_DIFF_STATS_NUMBER; + /// Extended header information such as creations, renames and mode + /// changes, equivalent of `--summary` in git + const INCLUDE_SUMMARY = raw::GIT_DIFF_STATS_INCLUDE_SUMMARY; + } +} + +impl DiffStatsFormat { + is_bit_set!(is_none, DiffStatsFormat::NONE); + is_bit_set!(is_full, DiffStatsFormat::FULL); + is_bit_set!(is_short, DiffStatsFormat::SHORT); + is_bit_set!(is_number, DiffStatsFormat::NUMBER); + is_bit_set!(is_include_summary, DiffStatsFormat::INCLUDE_SUMMARY); +} + +/// Automatic tag following options. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum AutotagOption { + /// Use the setting from the remote's configuration + Unspecified, + /// Ask the server for tags pointing to objects we're already downloading + Auto, + /// Don't ask for any tags beyond the refspecs + None, + /// Ask for all the tags + All, +} + +/// 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, + /// Force pruning on + On, + /// Force pruning off + Off, +} + +#[allow(missing_docs)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum StashApplyProgress { + /// None + None, + /// Loading the stashed data from the object database + LoadingStash, + /// The stored index is being analyzed + AnalyzeIndex, + /// The modified files are being analyzed + AnalyzeModified, + /// The untracked and ignored files are being analyzed + AnalyzeUntracked, + /// The untracked files are being written to disk + CheckoutUntracked, + /// The modified files are being written to disk + CheckoutModified, + /// The stash was applied successfully + Done, +} + +bitflags! { + #[allow(missing_docs)] + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct StashApplyFlags: u32 { + #[allow(missing_docs)] + const DEFAULT = raw::GIT_STASH_APPLY_DEFAULT as u32; + /// Try to reinstate not only the working tree's changes, + /// but also the index's changes. + const REINSTATE_INDEX = raw::GIT_STASH_APPLY_REINSTATE_INDEX as u32; + } +} + +impl StashApplyFlags { + is_bit_set!(is_default, StashApplyFlags::DEFAULT); + is_bit_set!(is_reinstate_index, StashApplyFlags::REINSTATE_INDEX); +} + +impl Default for StashApplyFlags { + fn default() -> Self { + StashApplyFlags::DEFAULT + } +} + +bitflags! { + #[allow(missing_docs)] + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct StashFlags: u32 { + #[allow(missing_docs)] + const DEFAULT = raw::GIT_STASH_DEFAULT as u32; + /// All changes already added to the index are left intact in + /// the working directory + const KEEP_INDEX = raw::GIT_STASH_KEEP_INDEX as u32; + /// All untracked files are also stashed and then cleaned up + /// from the working directory + const INCLUDE_UNTRACKED = raw::GIT_STASH_INCLUDE_UNTRACKED as u32; + /// All ignored files are also stashed and then cleaned up from + /// the working directory + const INCLUDE_IGNORED = raw::GIT_STASH_INCLUDE_IGNORED as u32; + /// All changes in the index and working directory are left intact + const KEEP_ALL = raw::GIT_STASH_KEEP_ALL as u32; + } +} + +impl StashFlags { + is_bit_set!(is_default, StashFlags::DEFAULT); + is_bit_set!(is_keep_index, StashFlags::KEEP_INDEX); + is_bit_set!(is_include_untracked, StashFlags::INCLUDE_UNTRACKED); + is_bit_set!(is_include_ignored, StashFlags::INCLUDE_IGNORED); +} + +impl Default for StashFlags { + fn default() -> Self { + StashFlags::DEFAULT + } +} + +bitflags! { + #[allow(missing_docs)] + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct AttrCheckFlags: u32 { + /// Check the working directory, then the index. + const FILE_THEN_INDEX = raw::GIT_ATTR_CHECK_FILE_THEN_INDEX as u32; + /// Check the index, then the working directory. + const INDEX_THEN_FILE = raw::GIT_ATTR_CHECK_INDEX_THEN_FILE as u32; + /// Check the index only. + const INDEX_ONLY = raw::GIT_ATTR_CHECK_INDEX_ONLY as u32; + /// Do not use the system gitattributes file. + const NO_SYSTEM = raw::GIT_ATTR_CHECK_NO_SYSTEM as u32; + } +} + +impl Default for AttrCheckFlags { + fn default() -> Self { + AttrCheckFlags::FILE_THEN_INDEX + } +} + +bitflags! { + #[allow(missing_docs)] + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct DiffFlags: u32 { + /// File(s) treated as binary data. + const BINARY = raw::GIT_DIFF_FLAG_BINARY as u32; + /// File(s) treated as text data. + const NOT_BINARY = raw::GIT_DIFF_FLAG_NOT_BINARY as u32; + /// `id` value is known correct. + const VALID_ID = raw::GIT_DIFF_FLAG_VALID_ID as u32; + /// File exists at this side of the delta. + const EXISTS = raw::GIT_DIFF_FLAG_EXISTS as u32; + } +} + +impl DiffFlags { + is_bit_set!(is_binary, DiffFlags::BINARY); + is_bit_set!(is_not_binary, DiffFlags::NOT_BINARY); + is_bit_set!(has_valid_id, DiffFlags::VALID_ID); + is_bit_set!(exists, DiffFlags::EXISTS); +} + +bitflags! { + /// Options for [`Reference::normalize_name`]. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct ReferenceFormat: u32 { + /// No particular normalization. + const NORMAL = raw::GIT_REFERENCE_FORMAT_NORMAL as u32; + /// Control whether one-level refname are accepted (i.e., refnames that + /// do not contain multiple `/`-separated components). Those are + /// expected to be written only using uppercase letters and underscore + /// (e.g. `HEAD`, `FETCH_HEAD`). + const ALLOW_ONELEVEL = raw::GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL as u32; + /// Interpret the provided name as a reference pattern for a refspec (as + /// used with remote repositories). If this option is enabled, the name + /// is allowed to contain a single `*` in place of a full pathname + /// components (e.g., `foo/*/bar` but not `foo/bar*`). + const REFSPEC_PATTERN = raw::GIT_REFERENCE_FORMAT_REFSPEC_PATTERN as u32; + /// Interpret the name as part of a refspec in shorthand form so the + /// `ALLOW_ONELEVEL` naming rules aren't enforced and `main` becomes a + /// valid name. + const REFSPEC_SHORTHAND = raw::GIT_REFERENCE_FORMAT_REFSPEC_SHORTHAND as u32; + } +} + +impl ReferenceFormat { + is_bit_set!(is_allow_onelevel, ReferenceFormat::ALLOW_ONELEVEL); + is_bit_set!(is_refspec_pattern, ReferenceFormat::REFSPEC_PATTERN); + is_bit_set!(is_refspec_shorthand, ReferenceFormat::REFSPEC_SHORTHAND); +} + +impl Default for ReferenceFormat { + fn default() -> Self { + ReferenceFormat::NORMAL + } +} + +#[cfg(test)] +mod tests { + use super::{FileMode, ObjectType}; + + #[test] + fn convert() { + assert_eq!(ObjectType::Blob.str(), "blob"); + assert_eq!(ObjectType::from_str("blob"), Some(ObjectType::Blob)); + assert!(ObjectType::Blob.is_loose()); + } + + #[test] + fn convert_filemode() { + assert_eq!(i32::from(FileMode::Blob), 0o100644); + assert_eq!(i32::from(FileMode::BlobGroupWritable), 0o100664); + assert_eq!(i32::from(FileMode::BlobExecutable), 0o100755); + assert_eq!(u32::from(FileMode::Blob), 0o100644); + assert_eq!(u32::from(FileMode::BlobGroupWritable), 0o100664); + assert_eq!(u32::from(FileMode::BlobExecutable), 0o100755); + } + + #[test] + fn bitflags_partial_eq() { + use super::{ + AttrCheckFlags, CheckoutNotificationType, CredentialType, DiffFlags, DiffStatsFormat, + IndexAddOption, IndexEntryExtendedFlag, IndexEntryFlag, MergeAnalysis, MergePreference, + OdbLookupFlags, PathspecFlags, ReferenceFormat, RepositoryInitMode, + RepositoryOpenFlags, RevparseMode, Sort, StashApplyFlags, StashFlags, Status, + SubmoduleStatus, + }; + + assert_eq!( + AttrCheckFlags::FILE_THEN_INDEX, + AttrCheckFlags::FILE_THEN_INDEX + ); + assert_eq!( + CheckoutNotificationType::CONFLICT, + CheckoutNotificationType::CONFLICT + ); + assert_eq!( + CredentialType::USER_PASS_PLAINTEXT, + CredentialType::USER_PASS_PLAINTEXT + ); + assert_eq!(DiffFlags::BINARY, DiffFlags::BINARY); + assert_eq!( + DiffStatsFormat::INCLUDE_SUMMARY, + DiffStatsFormat::INCLUDE_SUMMARY + ); + assert_eq!( + IndexAddOption::CHECK_PATHSPEC, + IndexAddOption::CHECK_PATHSPEC + ); + assert_eq!( + IndexEntryExtendedFlag::INTENT_TO_ADD, + IndexEntryExtendedFlag::INTENT_TO_ADD + ); + assert_eq!(IndexEntryFlag::EXTENDED, IndexEntryFlag::EXTENDED); + assert_eq!( + MergeAnalysis::ANALYSIS_FASTFORWARD, + MergeAnalysis::ANALYSIS_FASTFORWARD + ); + assert_eq!( + MergePreference::FASTFORWARD_ONLY, + MergePreference::FASTFORWARD_ONLY + ); + assert_eq!(OdbLookupFlags::NO_REFRESH, OdbLookupFlags::NO_REFRESH); + assert_eq!(PathspecFlags::FAILURES_ONLY, PathspecFlags::FAILURES_ONLY); + assert_eq!( + ReferenceFormat::ALLOW_ONELEVEL, + ReferenceFormat::ALLOW_ONELEVEL + ); + assert_eq!( + RepositoryInitMode::SHARED_ALL, + RepositoryInitMode::SHARED_ALL + ); + assert_eq!(RepositoryOpenFlags::CROSS_FS, RepositoryOpenFlags::CROSS_FS); + assert_eq!(RevparseMode::RANGE, RevparseMode::RANGE); + assert_eq!(Sort::REVERSE, Sort::REVERSE); + assert_eq!( + StashApplyFlags::REINSTATE_INDEX, + StashApplyFlags::REINSTATE_INDEX + ); + assert_eq!(StashFlags::INCLUDE_IGNORED, StashFlags::INCLUDE_IGNORED); + assert_eq!(Status::WT_MODIFIED, Status::WT_MODIFIED); + assert_eq!(SubmoduleStatus::WD_ADDED, SubmoduleStatus::WD_ADDED); + } +} diff --git a/extra/git2/src/mailmap.rs b/extra/git2/src/mailmap.rs new file mode 100644 index 000000000..096b3227c --- /dev/null +++ b/extra/git2/src/mailmap.rs @@ -0,0 +1,134 @@ +use std::ffi::CString; +use std::ptr; + +use crate::util::Binding; +use crate::{raw, Error, Signature}; + +/// A structure to represent a repository's .mailmap file. +/// +/// The representation cannot be written to disk. +pub struct Mailmap { + raw: *mut raw::git_mailmap, +} + +impl Binding for Mailmap { + type Raw = *mut raw::git_mailmap; + + unsafe fn from_raw(ptr: *mut raw::git_mailmap) -> Mailmap { + Mailmap { raw: ptr } + } + + fn raw(&self) -> *mut raw::git_mailmap { + self.raw + } +} + +impl Drop for Mailmap { + fn drop(&mut self) { + unsafe { + raw::git_mailmap_free(self.raw); + } + } +} + +impl Mailmap { + /// Creates an empty, in-memory mailmap object. + pub fn new() -> Result<Mailmap, Error> { + crate::init(); + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_mailmap_new(&mut ret)); + Ok(Binding::from_raw(ret)) + } + } + + /// Creates an in-memory mailmap object representing the given buffer. + pub fn from_buffer(buf: &str) -> Result<Mailmap, Error> { + crate::init(); + let mut ret = ptr::null_mut(); + let len = buf.len(); + let buf = CString::new(buf)?; + unsafe { + try_call!(raw::git_mailmap_from_buffer(&mut ret, buf, len)); + Ok(Binding::from_raw(ret)) + } + } + + /// Adds a new entry to this in-memory mailmap object. + pub fn add_entry( + &mut self, + real_name: Option<&str>, + real_email: Option<&str>, + replace_name: Option<&str>, + replace_email: &str, + ) -> Result<(), Error> { + let real_name = crate::opt_cstr(real_name)?; + let real_email = crate::opt_cstr(real_email)?; + let replace_name = crate::opt_cstr(replace_name)?; + let replace_email = CString::new(replace_email)?; + unsafe { + try_call!(raw::git_mailmap_add_entry( + self.raw, + real_name, + real_email, + replace_name, + replace_email + )); + Ok(()) + } + } + + /// Resolves a signature to its real name and email address. + pub fn resolve_signature(&self, sig: &Signature<'_>) -> Result<Signature<'static>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_mailmap_resolve_signature( + &mut ret, + &*self.raw, + sig.raw() + )); + Ok(Binding::from_raw(ret)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn smoke() { + let sig_name = "name"; + let sig_email = "email"; + let sig = t!(Signature::now(sig_name, sig_email)); + + let mut mm = t!(Mailmap::new()); + + let mailmapped_sig = t!(mm.resolve_signature(&sig)); + assert_eq!(mailmapped_sig.name(), Some(sig_name)); + assert_eq!(mailmapped_sig.email(), Some(sig_email)); + + t!(mm.add_entry(None, None, None, sig_email)); + t!(mm.add_entry( + Some("real name"), + Some("real@email"), + Some(sig_name), + sig_email, + )); + + let mailmapped_sig = t!(mm.resolve_signature(&sig)); + assert_eq!(mailmapped_sig.name(), Some("real name")); + assert_eq!(mailmapped_sig.email(), Some("real@email")); + } + + #[test] + fn from_buffer() { + let buf = "<prøper@emæil> <email>"; + let mm = t!(Mailmap::from_buffer(&buf)); + + let sig = t!(Signature::now("name", "email")); + let mailmapped_sig = t!(mm.resolve_signature(&sig)); + assert_eq!(mailmapped_sig.name(), Some("name")); + assert_eq!(mailmapped_sig.email(), Some("prøper@emæil")); + } +} diff --git a/extra/git2/src/mempack.rs b/extra/git2/src/mempack.rs new file mode 100644 index 000000000..a78070791 --- /dev/null +++ b/extra/git2/src/mempack.rs @@ -0,0 +1,49 @@ +use std::marker; + +use crate::util::Binding; +use crate::{raw, Buf, Error, Odb, Repository}; + +/// A structure to represent a mempack backend for the object database. The +/// Mempack is bound to the Odb that it was created from, and cannot outlive +/// that Odb. +pub struct Mempack<'odb> { + raw: *mut raw::git_odb_backend, + _marker: marker::PhantomData<&'odb Odb<'odb>>, +} + +impl<'odb> Binding for Mempack<'odb> { + type Raw = *mut raw::git_odb_backend; + + unsafe fn from_raw(raw: *mut raw::git_odb_backend) -> Mempack<'odb> { + Mempack { + raw, + _marker: marker::PhantomData, + } + } + + fn raw(&self) -> *mut raw::git_odb_backend { + self.raw + } +} + +// We don't need to implement `Drop` for Mempack because it is owned by the +// odb to which it is attached, and that will take care of freeing the mempack +// and associated memory. + +impl<'odb> Mempack<'odb> { + /// Dumps the contents of the mempack into the provided buffer. + pub fn dump(&self, repo: &Repository, buf: &mut Buf) -> Result<(), Error> { + unsafe { + try_call!(raw::git_mempack_dump(buf.raw(), repo.raw(), self.raw)); + } + Ok(()) + } + + /// Clears all data in the mempack. + pub fn reset(&self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_mempack_reset(self.raw)); + } + Ok(()) + } +} diff --git a/extra/git2/src/merge.rs b/extra/git2/src/merge.rs new file mode 100644 index 000000000..6bd30c10d --- /dev/null +++ b/extra/git2/src/merge.rs @@ -0,0 +1,194 @@ +use libc::c_uint; +use std::marker; +use std::mem; +use std::str; + +use crate::call::Convert; +use crate::util::Binding; +use crate::{raw, Commit, FileFavor, Oid}; + +/// A structure to represent an annotated commit, the input to merge and rebase. +/// +/// An annotated commit contains information about how it was looked up, which +/// may be useful for functions like merge or rebase to provide context to the +/// operation. +pub struct AnnotatedCommit<'repo> { + raw: *mut raw::git_annotated_commit, + _marker: marker::PhantomData<Commit<'repo>>, +} + +/// Options to specify when merging. +pub struct MergeOptions { + raw: raw::git_merge_options, +} + +impl<'repo> AnnotatedCommit<'repo> { + /// Gets the commit ID that the given git_annotated_commit refers to + pub fn id(&self) -> Oid { + unsafe { Binding::from_raw(raw::git_annotated_commit_id(self.raw)) } + } + + /// Get the refname that the given git_annotated_commit refers to + /// + /// Returns None if it is not valid utf8 + pub fn refname(&self) -> Option<&str> { + str::from_utf8(self.refname_bytes()).ok() + } + + /// Get the refname that the given git_annotated_commit refers to. + pub fn refname_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, raw::git_annotated_commit_ref(&*self.raw)).unwrap() } + } +} + +impl Default for MergeOptions { + fn default() -> Self { + Self::new() + } +} + +impl MergeOptions { + /// Creates a default set of merge options. + pub fn new() -> MergeOptions { + let mut opts = MergeOptions { + raw: unsafe { mem::zeroed() }, + }; + assert_eq!(unsafe { raw::git_merge_init_options(&mut opts.raw, 1) }, 0); + opts + } + + fn flag(&mut self, opt: u32, val: bool) -> &mut MergeOptions { + if val { + self.raw.flags |= opt; + } else { + self.raw.flags &= !opt; + } + self + } + + /// Detect file renames + pub fn find_renames(&mut self, find: bool) -> &mut MergeOptions { + self.flag(raw::GIT_MERGE_FIND_RENAMES as u32, find) + } + + /// If a conflict occurs, exit immediately instead of attempting to continue + /// resolving conflicts + pub fn fail_on_conflict(&mut self, fail: bool) -> &mut MergeOptions { + self.flag(raw::GIT_MERGE_FAIL_ON_CONFLICT as u32, fail) + } + + /// Do not write the REUC extension on the generated index + pub fn skip_reuc(&mut self, skip: bool) -> &mut MergeOptions { + self.flag(raw::GIT_MERGE_FAIL_ON_CONFLICT as u32, skip) + } + + /// If the commits being merged have multiple merge bases, do not build a + /// recursive merge base (by merging the multiple merge bases), instead + /// simply use the first base. + pub fn no_recursive(&mut self, disable: bool) -> &mut MergeOptions { + self.flag(raw::GIT_MERGE_NO_RECURSIVE as u32, disable) + } + + /// Similarity to consider a file renamed (default 50) + pub fn rename_threshold(&mut self, thresh: u32) -> &mut MergeOptions { + self.raw.rename_threshold = thresh; + self + } + + /// Maximum similarity sources to examine for renames (default 200). + /// If the number of rename candidates (add / delete pairs) is greater + /// than this value, inexact rename detection is aborted. This setting + /// overrides the `merge.renameLimit` configuration value. + pub fn target_limit(&mut self, limit: u32) -> &mut MergeOptions { + self.raw.target_limit = limit as c_uint; + self + } + + /// Maximum number of times to merge common ancestors to build a + /// virtual merge base when faced with criss-cross merges. When + /// this limit is reached, the next ancestor will simply be used + /// instead of attempting to merge it. The default is unlimited. + pub fn recursion_limit(&mut self, limit: u32) -> &mut MergeOptions { + self.raw.recursion_limit = limit as c_uint; + self + } + + /// Specify a side to favor for resolving conflicts + pub fn file_favor(&mut self, favor: FileFavor) -> &mut MergeOptions { + self.raw.file_favor = favor.convert(); + self + } + + fn file_flag(&mut self, opt: u32, val: bool) -> &mut MergeOptions { + if val { + self.raw.file_flags |= opt; + } else { + self.raw.file_flags &= !opt; + } + self + } + + /// Create standard conflicted merge files + pub fn standard_style(&mut self, standard: bool) -> &mut MergeOptions { + self.file_flag(raw::GIT_MERGE_FILE_STYLE_MERGE as u32, standard) + } + + /// Create diff3-style file + pub fn diff3_style(&mut self, diff3: bool) -> &mut MergeOptions { + self.file_flag(raw::GIT_MERGE_FILE_STYLE_DIFF3 as u32, diff3) + } + + /// Condense non-alphanumeric regions for simplified diff file + pub fn simplify_alnum(&mut self, simplify: bool) -> &mut MergeOptions { + self.file_flag(raw::GIT_MERGE_FILE_SIMPLIFY_ALNUM as u32, simplify) + } + + /// Ignore all whitespace + pub fn ignore_whitespace(&mut self, ignore: bool) -> &mut MergeOptions { + self.file_flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE as u32, ignore) + } + + /// Ignore changes in amount of whitespace + pub fn ignore_whitespace_change(&mut self, ignore: bool) -> &mut MergeOptions { + self.file_flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE_CHANGE as u32, ignore) + } + + /// Ignore whitespace at end of line + pub fn ignore_whitespace_eol(&mut self, ignore: bool) -> &mut MergeOptions { + self.file_flag(raw::GIT_MERGE_FILE_IGNORE_WHITESPACE_EOL as u32, ignore) + } + + /// Use the "patience diff" algorithm + pub fn patience(&mut self, patience: bool) -> &mut MergeOptions { + self.file_flag(raw::GIT_MERGE_FILE_DIFF_PATIENCE as u32, patience) + } + + /// Take extra time to find minimal diff + pub fn minimal(&mut self, minimal: bool) -> &mut MergeOptions { + self.file_flag(raw::GIT_MERGE_FILE_DIFF_MINIMAL as u32, minimal) + } + + /// Acquire a pointer to the underlying raw options. + pub unsafe fn raw(&self) -> *const raw::git_merge_options { + &self.raw as *const _ + } +} + +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, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_annotated_commit { + self.raw + } +} + +impl<'repo> Drop for AnnotatedCommit<'repo> { + fn drop(&mut self) { + unsafe { raw::git_annotated_commit_free(self.raw) } + } +} diff --git a/extra/git2/src/message.rs b/extra/git2/src/message.rs new file mode 100644 index 000000000..a7041da3a --- /dev/null +++ b/extra/git2/src/message.rs @@ -0,0 +1,349 @@ +use core::ops::Range; +use std::ffi::CStr; +use std::ffi::CString; +use std::iter::FusedIterator; +use std::ptr; + +use libc::{c_char, c_int}; + +use crate::util::Binding; +use crate::{raw, Buf, Error, IntoCString}; + +/// Clean up a message, removing extraneous whitespace, and ensure that the +/// message ends with a newline. If `comment_char` is `Some`, also remove comment +/// lines starting with that character. +pub fn message_prettify<T: IntoCString>( + message: T, + comment_char: Option<u8>, +) -> Result<String, Error> { + _message_prettify(message.into_c_string()?, comment_char) +} + +fn _message_prettify(message: CString, comment_char: Option<u8>) -> Result<String, Error> { + let ret = Buf::new(); + unsafe { + try_call!(raw::git_message_prettify( + ret.raw(), + message, + comment_char.is_some() as c_int, + comment_char.unwrap_or(0) as c_char + )); + } + Ok(ret.as_str().unwrap().to_string()) +} + +/// The default comment character for `message_prettify` ('#') +pub const DEFAULT_COMMENT_CHAR: Option<u8> = 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<MessageTrailersStrs, Error> { + _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<S: IntoCString>(message: S) -> Result<MessageTrailersBytes, Error> { + _message_trailers(message.into_c_string()?).map(|res| MessageTrailersBytes(res)) +} + +fn _message_trailers(message: CString) -> Result<MessageTrailers, Error> { + 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<usize>, +} + +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::Item> { + self.0 + .range + .next() + .map(|index| to_str_tuple(&self.0.trailers, index)) + } + + fn size_hint(&self) -> (usize, Option<usize>) { + 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::Item> { + 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::Item> { + self.0 + .range + .next() + .map(|index| to_bytes_tuple(&self.0.trailers, index)) + } + + fn size_hint(&self) -> (usize, Option<usize>) { + 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::Item> { + 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 { + + #[test] + fn prettify() { + use crate::{message_prettify, DEFAULT_COMMENT_CHAR}; + + // This does not attempt to duplicate the extensive tests for + // git_message_prettify in libgit2, just a few representative values to + // make sure the interface works as expected. + assert_eq!(message_prettify("1\n\n\n2", None).unwrap(), "1\n\n2\n"); + assert_eq!( + message_prettify("1\n\n\n2\n\n\n3", None).unwrap(), + "1\n\n2\n\n3\n" + ); + assert_eq!( + message_prettify("1\n# comment\n# more", None).unwrap(), + "1\n# comment\n# more\n" + ); + assert_eq!( + message_prettify("1\n# comment\n# more", DEFAULT_COMMENT_CHAR).unwrap(), + "1\n" + ); + assert_eq!( + message_prettify("1\n; comment\n; more", Some(';' as u8)).unwrap(), + "1\n" + ); + } + + #[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/extra/git2/src/note.rs b/extra/git2/src/note.rs new file mode 100644 index 000000000..50e5800fe --- /dev/null +++ b/extra/git2/src/note.rs @@ -0,0 +1,147 @@ +use std::marker; +use std::str; + +use crate::util::Binding; +use crate::{raw, signature, Error, Oid, Repository, Signature}; + +/// A structure representing a [note][note] in git. +/// +/// [note]: http://alblue.bandlem.com/2011/11/git-tip-of-week-git-notes.html +pub struct Note<'repo> { + raw: *mut raw::git_note, + + // Hmm, the current libgit2 version does not have this inside of it, but + // perhaps it's a good idea to keep it around? Can always remove it later I + // suppose... + _marker: marker::PhantomData<&'repo Repository>, +} + +/// An iterator over all of the notes within a repository. +pub struct Notes<'repo> { + raw: *mut raw::git_note_iterator, + _marker: marker::PhantomData<&'repo Repository>, +} + +impl<'repo> Note<'repo> { + /// Get the note author + pub fn author(&self) -> Signature<'_> { + unsafe { signature::from_raw_const(self, raw::git_note_author(&*self.raw)) } + } + + /// Get the note committer + pub fn committer(&self) -> Signature<'_> { + unsafe { signature::from_raw_const(self, raw::git_note_committer(&*self.raw)) } + } + + /// Get the note message, in bytes. + pub fn message_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, raw::git_note_message(&*self.raw)).unwrap() } + } + + /// Get the note message as a string, returning `None` if it is not UTF-8. + pub fn message(&self) -> Option<&str> { + str::from_utf8(self.message_bytes()).ok() + } + + /// Get the note object's id + pub fn id(&self) -> Oid { + unsafe { Binding::from_raw(raw::git_note_id(&*self.raw)) } + } +} + +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, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_note { + self.raw + } +} + +impl<'repo> std::fmt::Debug for Note<'repo> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_struct("Note").field("id", &self.id()).finish() + } +} + +impl<'repo> Drop for Note<'repo> { + fn drop(&mut self) { + unsafe { + raw::git_note_free(self.raw); + } + } +} + +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, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_note_iterator { + self.raw + } +} + +impl<'repo> Iterator for Notes<'repo> { + type Item = Result<(Oid, Oid), Error>; + fn next(&mut self) -> Option<Result<(Oid, Oid), Error>> { + let mut note_id = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + let mut annotated_id = note_id; + unsafe { + try_call_iter!(raw::git_note_next( + &mut note_id, + &mut annotated_id, + self.raw + )); + Some(Ok(( + Binding::from_raw(¬e_id as *const _), + Binding::from_raw(&annotated_id as *const _), + ))) + } + } +} + +impl<'repo> Drop for Notes<'repo> { + fn drop(&mut self) { + unsafe { + raw::git_note_iterator_free(self.raw); + } + } +} + +#[cfg(test)] +mod tests { + #[test] + fn smoke() { + let (_td, repo) = crate::test::repo_init(); + assert!(repo.notes(None).is_err()); + + let sig = repo.signature().unwrap(); + let head = repo.head().unwrap().target().unwrap(); + let note = repo.note(&sig, &sig, None, head, "foo", false).unwrap(); + assert_eq!(repo.notes(None).unwrap().count(), 1); + + let note_obj = repo.find_note(None, head).unwrap(); + assert_eq!(note_obj.id(), note); + assert_eq!(note_obj.message(), Some("foo")); + + let (a, b) = repo.notes(None).unwrap().next().unwrap().unwrap(); + assert_eq!(a, note); + assert_eq!(b, head); + + assert_eq!(repo.note_default_ref().unwrap(), "refs/notes/commits"); + + assert_eq!(sig.name(), note_obj.author().name()); + assert_eq!(sig.name(), note_obj.committer().name()); + assert!(sig.when() == note_obj.committer().when()); + } +} diff --git a/extra/git2/src/object.rs b/extra/git2/src/object.rs new file mode 100644 index 000000000..fcae0066c --- /dev/null +++ b/extra/git2/src/object.rs @@ -0,0 +1,248 @@ +use std::marker; +use std::mem; +use std::ptr; + +use crate::util::Binding; +use crate::{raw, Blob, Buf, Commit, Error, ObjectType, Oid, Repository, Tag, Tree}; +use crate::{Describe, DescribeOptions}; + +/// A structure to represent a git [object][1] +/// +/// [1]: http://git-scm.com/book/en/Git-Internals-Git-Objects +pub struct Object<'repo> { + raw: *mut raw::git_object, + _marker: marker::PhantomData<&'repo Repository>, +} + +impl<'repo> Object<'repo> { + /// Get the id (SHA1) of a repository object + pub fn id(&self) -> Oid { + unsafe { Binding::from_raw(raw::git_object_id(&*self.raw)) } + } + + /// Get the object type of an object. + /// + /// If the type is unknown, then `None` is returned. + pub fn kind(&self) -> Option<ObjectType> { + ObjectType::from_raw(unsafe { raw::git_object_type(&*self.raw) }) + } + + /// Recursively peel an object until an object of the specified type is met. + /// + /// If you pass `Any` as the target type, then the object will be + /// peeled until the type changes (e.g. a tag will be chased until the + /// referenced object is no longer a tag). + pub fn peel(&self, kind: ObjectType) -> Result<Object<'repo>, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_object_peel(&mut raw, &*self.raw(), kind)); + Ok(Binding::from_raw(raw)) + } + } + + /// Recursively peel an object until a blob is found + pub fn peel_to_blob(&self) -> Result<Blob<'repo>, Error> { + self.peel(ObjectType::Blob) + .map(|o| o.cast_or_panic(ObjectType::Blob)) + } + + /// Recursively peel an object until a commit is found + pub fn peel_to_commit(&self) -> Result<Commit<'repo>, Error> { + self.peel(ObjectType::Commit) + .map(|o| o.cast_or_panic(ObjectType::Commit)) + } + + /// Recursively peel an object until a tag is found + pub fn peel_to_tag(&self) -> Result<Tag<'repo>, Error> { + self.peel(ObjectType::Tag) + .map(|o| o.cast_or_panic(ObjectType::Tag)) + } + + /// Recursively peel an object until a tree is found + pub fn peel_to_tree(&self) -> Result<Tree<'repo>, Error> { + self.peel(ObjectType::Tree) + .map(|o| o.cast_or_panic(ObjectType::Tree)) + } + + /// Get a short abbreviated OID string for the object + /// + /// This starts at the "core.abbrev" length (default 7 characters) and + /// iteratively extends to a longer string if that length is ambiguous. The + /// result will be unambiguous (at least until new objects are added to the + /// repository). + pub fn short_id(&self) -> Result<Buf, Error> { + unsafe { + let buf = Buf::new(); + try_call!(raw::git_object_short_id(buf.raw(), &*self.raw())); + Ok(buf) + } + } + + /// Attempt to view this object as a commit. + /// + /// Returns `None` if the object is not actually a commit. + pub fn as_commit(&self) -> Option<&Commit<'repo>> { + self.cast(ObjectType::Commit) + } + + /// Attempt to consume this object and return a commit. + /// + /// Returns `Err(self)` if this object is not actually a commit. + pub fn into_commit(self) -> Result<Commit<'repo>, Object<'repo>> { + self.cast_into(ObjectType::Commit) + } + + /// Attempt to view this object as a tag. + /// + /// Returns `None` if the object is not actually a tag. + pub fn as_tag(&self) -> Option<&Tag<'repo>> { + self.cast(ObjectType::Tag) + } + + /// Attempt to consume this object and return a tag. + /// + /// Returns `Err(self)` if this object is not actually a tag. + pub fn into_tag(self) -> Result<Tag<'repo>, Object<'repo>> { + self.cast_into(ObjectType::Tag) + } + + /// Attempt to view this object as a tree. + /// + /// Returns `None` if the object is not actually a tree. + pub fn as_tree(&self) -> Option<&Tree<'repo>> { + self.cast(ObjectType::Tree) + } + + /// Attempt to consume this object and return a tree. + /// + /// Returns `Err(self)` if this object is not actually a tree. + pub fn into_tree(self) -> Result<Tree<'repo>, Object<'repo>> { + self.cast_into(ObjectType::Tree) + } + + /// Attempt to view this object as a blob. + /// + /// Returns `None` if the object is not actually a blob. + pub fn as_blob(&self) -> Option<&Blob<'repo>> { + self.cast(ObjectType::Blob) + } + + /// Attempt to consume this object and return a blob. + /// + /// Returns `Err(self)` if this object is not actually a blob. + pub fn into_blob(self) -> Result<Blob<'repo>, Object<'repo>> { + self.cast_into(ObjectType::Blob) + } + + /// Describes a commit + /// + /// Performs a describe operation on this commitish object. + pub fn describe(&self, opts: &DescribeOptions) -> Result<Describe<'_>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_describe_commit(&mut ret, self.raw, opts.raw())); + Ok(Binding::from_raw(ret)) + } + } + + fn cast<T>(&self, kind: ObjectType) -> Option<&T> { + assert_eq!(mem::size_of::<Object<'_>>(), mem::size_of::<T>()); + if self.kind() == Some(kind) { + unsafe { Some(&*(self as *const _ as *const T)) } + } else { + None + } + } + + fn cast_into<T>(self, kind: ObjectType) -> Result<T, Object<'repo>> { + assert_eq!(mem::size_of_val(&self), mem::size_of::<T>()); + if self.kind() == Some(kind) { + Ok(unsafe { + let other = ptr::read(&self as *const _ as *const T); + mem::forget(self); + other + }) + } else { + Err(self) + } + } +} + +/// This trait is useful to export cast_or_panic into crate but not outside +pub trait CastOrPanic { + fn cast_or_panic<T>(self, kind: ObjectType) -> T; +} + +impl<'repo> CastOrPanic for Object<'repo> { + fn cast_or_panic<T>(self, kind: ObjectType) -> T { + assert_eq!(mem::size_of_val(&self), mem::size_of::<T>()); + if self.kind() == Some(kind) { + unsafe { + let other = ptr::read(&self as *const _ as *const T); + mem::forget(self); + other + } + } else { + let buf; + let akind = match self.kind() { + Some(akind) => akind.str(), + None => { + buf = format!("unknown ({})", unsafe { raw::git_object_type(&*self.raw) }); + &buf + } + }; + panic!( + "Expected object {} to be {} but it is {}", + self.id(), + kind.str(), + akind + ) + } + } +} + +impl<'repo> Clone for Object<'repo> { + fn clone(&self) -> Object<'repo> { + let mut raw = ptr::null_mut(); + unsafe { + let rc = raw::git_object_dup(&mut raw, self.raw); + assert_eq!(rc, 0); + Binding::from_raw(raw) + } + } +} + +impl<'repo> std::fmt::Debug for Object<'repo> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut ds = f.debug_struct("Object"); + match self.kind() { + Some(kind) => ds.field("kind", &kind), + None => ds.field( + "kind", + &format!("Unknow ({})", unsafe { raw::git_object_type(&*self.raw) }), + ), + }; + ds.field("id", &self.id()); + ds.finish() + } +} + +impl<'repo> Binding for Object<'repo> { + type Raw = *mut raw::git_object; + + unsafe fn from_raw(raw: *mut raw::git_object) -> Object<'repo> { + Object { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_object { + self.raw + } +} + +impl<'repo> Drop for Object<'repo> { + fn drop(&mut self) { + unsafe { raw::git_object_free(self.raw) } + } +} diff --git a/extra/git2/src/odb.rs b/extra/git2/src/odb.rs new file mode 100644 index 000000000..7f6da5eb3 --- /dev/null +++ b/extra/git2/src/odb.rs @@ -0,0 +1,729 @@ +use std::io; +use std::marker; +use std::ptr; +use std::slice; + +use std::ffi::CString; + +use libc::{c_char, c_int, c_uint, c_void, size_t}; + +use crate::panic; +use crate::util::Binding; +use crate::{ + raw, Error, IndexerProgress, Mempack, Object, ObjectType, OdbLookupFlags, Oid, Progress, +}; + +/// A structure to represent a git object database +pub struct Odb<'repo> { + raw: *mut raw::git_odb, + _marker: marker::PhantomData<Object<'repo>>, +} + +// `git_odb` uses locking and atomics internally. +unsafe impl<'repo> Send for Odb<'repo> {} +unsafe impl<'repo> Sync for Odb<'repo> {} + +impl<'repo> Binding for Odb<'repo> { + type Raw = *mut raw::git_odb; + + unsafe fn from_raw(raw: *mut raw::git_odb) -> Odb<'repo> { + Odb { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_odb { + self.raw + } +} + +impl<'repo> Drop for Odb<'repo> { + fn drop(&mut self) { + unsafe { raw::git_odb_free(self.raw) } + } +} + +impl<'repo> Odb<'repo> { + /// Creates an object database without any backends. + pub fn new<'a>() -> Result<Odb<'a>, Error> { + crate::init(); + unsafe { + let mut out = ptr::null_mut(); + try_call!(raw::git_odb_new(&mut out)); + Ok(Odb::from_raw(out)) + } + } + + /// Create object database reading stream. + /// + /// Note that most backends do not support streaming reads because they store their objects as compressed/delta'ed blobs. + /// If the backend does not support streaming reads, use the `read` method instead. + pub fn reader(&self, oid: Oid) -> Result<(OdbReader<'_>, usize, ObjectType), Error> { + let mut out = ptr::null_mut(); + let mut size = 0usize; + let mut otype: raw::git_object_t = ObjectType::Any.raw(); + unsafe { + try_call!(raw::git_odb_open_rstream( + &mut out, + &mut size, + &mut otype, + self.raw, + oid.raw() + )); + Ok(( + OdbReader::from_raw(out), + size, + ObjectType::from_raw(otype).unwrap(), + )) + } + } + + /// Create object database writing stream. + /// + /// The type and final length of the object must be specified when opening the stream. + /// If the backend does not support streaming writes, use the `write` method instead. + pub fn writer(&self, size: usize, obj_type: ObjectType) -> Result<OdbWriter<'_>, Error> { + let mut out = ptr::null_mut(); + unsafe { + try_call!(raw::git_odb_open_wstream( + &mut out, + self.raw, + size as raw::git_object_size_t, + obj_type.raw() + )); + Ok(OdbWriter::from_raw(out)) + } + } + + /// Iterate over all objects in the object database.s + pub fn foreach<C>(&self, mut callback: C) -> Result<(), Error> + where + C: FnMut(&Oid) -> bool, + { + unsafe { + let mut data = ForeachCbData { + callback: &mut callback, + }; + let cb: raw::git_odb_foreach_cb = Some(foreach_cb); + try_call!(raw::git_odb_foreach( + self.raw(), + cb, + &mut data as *mut _ as *mut _ + )); + Ok(()) + } + } + + /// Read an object from the database. + pub fn read(&self, oid: Oid) -> Result<OdbObject<'_>, Error> { + let mut out = ptr::null_mut(); + unsafe { + try_call!(raw::git_odb_read(&mut out, self.raw, oid.raw())); + Ok(OdbObject::from_raw(out)) + } + } + + /// Reads the header of an object from the database + /// without reading the full content. + pub fn read_header(&self, oid: Oid) -> Result<(usize, ObjectType), Error> { + let mut size: usize = 0; + let mut kind_id: i32 = ObjectType::Any.raw(); + + unsafe { + try_call!(raw::git_odb_read_header( + &mut size as *mut size_t, + &mut kind_id as *mut raw::git_object_t, + self.raw, + oid.raw() + )); + + Ok((size, ObjectType::from_raw(kind_id).unwrap())) + } + } + + /// Write an object to the database. + pub fn write(&self, kind: ObjectType, data: &[u8]) -> Result<Oid, Error> { + unsafe { + let mut out = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + try_call!(raw::git_odb_write( + &mut out, + self.raw, + data.as_ptr() as *const c_void, + data.len(), + kind.raw() + )); + Ok(Oid::from_raw(&mut out)) + } + } + + /// Create stream for writing a pack file to the ODB + pub fn packwriter(&self) -> Result<OdbPackwriter<'_>, Error> { + let mut out = ptr::null_mut(); + let progress_cb: raw::git_indexer_progress_cb = Some(write_pack_progress_cb); + let progress_payload = Box::new(OdbPackwriterCb { cb: None }); + let progress_payload_ptr = Box::into_raw(progress_payload); + + unsafe { + try_call!(raw::git_odb_write_pack( + &mut out, + self.raw, + progress_cb, + progress_payload_ptr as *mut c_void + )); + } + + Ok(OdbPackwriter { + raw: out, + progress: Default::default(), + progress_payload_ptr, + }) + } + + /// Checks if the object database has an object. + pub fn exists(&self, oid: Oid) -> bool { + unsafe { raw::git_odb_exists(self.raw, oid.raw()) != 0 } + } + + /// Checks if the object database has an object, with extended flags. + pub fn exists_ext(&self, oid: Oid, flags: OdbLookupFlags) -> bool { + unsafe { raw::git_odb_exists_ext(self.raw, oid.raw(), flags.bits() as c_uint) != 0 } + } + + /// Potentially finds an object that starts with the given prefix. + pub fn exists_prefix(&self, short_oid: Oid, len: usize) -> Result<Oid, Error> { + unsafe { + let mut out = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + try_call!(raw::git_odb_exists_prefix( + &mut out, + self.raw, + short_oid.raw(), + len + )); + Ok(Oid::from_raw(&out)) + } + } + + /// Refresh the object database. + /// This should never be needed, and is + /// provided purely for convenience. + /// The object database will automatically + /// refresh when an object is not found when + /// requested. + pub fn refresh(&self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_odb_refresh(self.raw)); + Ok(()) + } + } + + /// Adds an alternate disk backend to the object database. + pub fn add_disk_alternate(&self, path: &str) -> Result<(), Error> { + unsafe { + let path = CString::new(path)?; + try_call!(raw::git_odb_add_disk_alternate(self.raw, path)); + Ok(()) + } + } + + /// Create a new mempack backend, and add it to this odb with the given + /// priority. Higher values give the backend higher precedence. The default + /// loose and pack backends have priorities 1 and 2 respectively (hard-coded + /// in libgit2). A reference to the new mempack backend is returned on + /// success. The lifetime of the backend must be contained within the + /// lifetime of this odb, since deletion of the odb will also result in + /// deletion of the mempack backend. + /// + /// Here is an example that fails to compile because it tries to hold the + /// mempack reference beyond the Odb's lifetime: + /// + /// ```compile_fail + /// use git2::Odb; + /// let mempack = { + /// let odb = Odb::new().unwrap(); + /// odb.add_new_mempack_backend(1000).unwrap() + /// }; + /// ``` + pub fn add_new_mempack_backend<'odb>( + &'odb self, + priority: i32, + ) -> Result<Mempack<'odb>, Error> { + unsafe { + let mut mempack = ptr::null_mut(); + // The mempack backend object in libgit2 is only ever freed by an + // odb that has the backend in its list. So to avoid potentially + // leaking the mempack backend, this API ensures that the backend + // is added to the odb before returning it. The lifetime of the + // mempack is also bound to the lifetime of the odb, so that users + // can't end up with a dangling reference to a mempack object that + // was actually freed when the odb was destroyed. + try_call!(raw::git_mempack_new(&mut mempack)); + try_call!(raw::git_odb_add_backend( + self.raw, + mempack, + priority as c_int + )); + Ok(Mempack::from_raw(mempack)) + } + } +} + +/// An object from the Object Database. +pub struct OdbObject<'a> { + raw: *mut raw::git_odb_object, + _marker: marker::PhantomData<Object<'a>>, +} + +impl<'a> Binding for OdbObject<'a> { + type Raw = *mut raw::git_odb_object; + + unsafe fn from_raw(raw: *mut raw::git_odb_object) -> OdbObject<'a> { + OdbObject { + raw, + _marker: marker::PhantomData, + } + } + + fn raw(&self) -> *mut raw::git_odb_object { + self.raw + } +} + +impl<'a> Drop for OdbObject<'a> { + fn drop(&mut self) { + unsafe { raw::git_odb_object_free(self.raw) } + } +} + +impl<'a> OdbObject<'a> { + /// Get the object type. + pub fn kind(&self) -> ObjectType { + unsafe { ObjectType::from_raw(raw::git_odb_object_type(self.raw)).unwrap() } + } + + /// Get the object size. + pub fn len(&self) -> usize { + unsafe { raw::git_odb_object_size(self.raw) } + } + + /// Get the object data. + pub fn data(&self) -> &[u8] { + unsafe { + let size = self.len(); + let ptr: *const u8 = raw::git_odb_object_data(self.raw) as *const u8; + let buffer = slice::from_raw_parts(ptr, size); + return buffer; + } + } + + /// Get the object id. + pub fn id(&self) -> Oid { + unsafe { Oid::from_raw(raw::git_odb_object_id(self.raw)) } + } +} + +/// A structure to represent a git ODB rstream +pub struct OdbReader<'repo> { + raw: *mut raw::git_odb_stream, + _marker: marker::PhantomData<Object<'repo>>, +} + +// `git_odb_stream` is not thread-safe internally, so it can't use `Sync`, but moving it to another +// thread and continuing to read will work. +unsafe impl<'repo> Send for OdbReader<'repo> {} + +impl<'repo> Binding for OdbReader<'repo> { + type Raw = *mut raw::git_odb_stream; + + unsafe fn from_raw(raw: *mut raw::git_odb_stream) -> OdbReader<'repo> { + OdbReader { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_odb_stream { + self.raw + } +} + +impl<'repo> Drop for OdbReader<'repo> { + fn drop(&mut self) { + unsafe { raw::git_odb_stream_free(self.raw) } + } +} + +impl<'repo> io::Read for OdbReader<'repo> { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + unsafe { + let ptr = buf.as_ptr() as *mut c_char; + let len = buf.len(); + let res = raw::git_odb_stream_read(self.raw, ptr, len); + if res < 0 { + Err(io::Error::new(io::ErrorKind::Other, "Read error")) + } else { + Ok(len) + } + } + } +} + +/// A structure to represent a git ODB wstream +pub struct OdbWriter<'repo> { + raw: *mut raw::git_odb_stream, + _marker: marker::PhantomData<Object<'repo>>, +} + +// `git_odb_stream` is not thread-safe internally, so it can't use `Sync`, but moving it to another +// thread and continuing to write will work. +unsafe impl<'repo> Send for OdbWriter<'repo> {} + +impl<'repo> OdbWriter<'repo> { + /// Finish writing to an ODB stream + /// + /// This method can be used to finalize writing object to the database and get an identifier. + /// The object will take its final name and will be available to the odb. + /// This method will fail if the total number of received bytes differs from the size declared with odb_writer() + /// Attempting write after finishing will be ignored. + pub fn finalize(&mut self) -> Result<Oid, Error> { + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_odb_stream_finalize_write(&mut raw, self.raw)); + Ok(Binding::from_raw(&raw as *const _)) + } + } +} + +impl<'repo> Binding for OdbWriter<'repo> { + type Raw = *mut raw::git_odb_stream; + + unsafe fn from_raw(raw: *mut raw::git_odb_stream) -> OdbWriter<'repo> { + OdbWriter { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_odb_stream { + self.raw + } +} + +impl<'repo> Drop for OdbWriter<'repo> { + fn drop(&mut self) { + unsafe { raw::git_odb_stream_free(self.raw) } + } +} + +impl<'repo> io::Write for OdbWriter<'repo> { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + unsafe { + let ptr = buf.as_ptr() as *const c_char; + let len = buf.len(); + let res = raw::git_odb_stream_write(self.raw, ptr, len); + if res < 0 { + Err(io::Error::new(io::ErrorKind::Other, "Write error")) + } else { + Ok(buf.len()) + } + } + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +pub(crate) struct OdbPackwriterCb<'repo> { + pub(crate) cb: Option<Box<IndexerProgress<'repo>>>, +} + +/// A stream to write a packfile to the ODB +pub struct OdbPackwriter<'repo> { + raw: *mut raw::git_odb_writepack, + progress: raw::git_indexer_progress, + progress_payload_ptr: *mut OdbPackwriterCb<'repo>, +} + +impl<'repo> OdbPackwriter<'repo> { + /// Finish writing the packfile + pub fn commit(&mut self) -> Result<i32, Error> { + unsafe { + let writepack = &*self.raw; + let res = match writepack.commit { + Some(commit) => commit(self.raw, &mut self.progress), + None => -1, + }; + + if res < 0 { + Err(Error::last_error(res).unwrap()) + } else { + Ok(res) + } + } + } + + /// The callback through which progress is monitored. Be aware that this is + /// called inline, so performance may be affected. + pub fn progress<F>(&mut self, cb: F) -> &mut OdbPackwriter<'repo> + where + F: FnMut(Progress<'_>) -> bool + 'repo, + { + let progress_payload = + unsafe { &mut *(self.progress_payload_ptr as *mut OdbPackwriterCb<'_>) }; + + progress_payload.cb = Some(Box::new(cb) as Box<IndexerProgress<'repo>>); + self + } +} + +impl<'repo> io::Write for OdbPackwriter<'repo> { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + unsafe { + let ptr = buf.as_ptr() as *mut c_void; + let len = buf.len(); + + let writepack = &*self.raw; + let res = match writepack.append { + Some(append) => append(self.raw, ptr, len, &mut self.progress), + None => -1, + }; + + if res < 0 { + Err(io::Error::new(io::ErrorKind::Other, "Write error")) + } else { + Ok(buf.len()) + } + } + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl<'repo> Drop for OdbPackwriter<'repo> { + fn drop(&mut self) { + unsafe { + let writepack = &*self.raw; + match writepack.free { + Some(free) => free(self.raw), + None => (), + }; + + drop(Box::from_raw(self.progress_payload_ptr)); + } + } +} + +pub type ForeachCb<'a> = dyn FnMut(&Oid) -> bool + 'a; + +struct ForeachCbData<'a> { + pub callback: &'a mut ForeachCb<'a>, +} + +extern "C" fn foreach_cb(id: *const raw::git_oid, payload: *mut c_void) -> c_int { + panic::wrap(|| unsafe { + let data = &mut *(payload as *mut ForeachCbData<'_>); + let res = { + let callback = &mut data.callback; + callback(&Binding::from_raw(id)) + }; + + if res { + 0 + } else { + 1 + } + }) + .unwrap_or(1) +} + +pub(crate) extern "C" fn write_pack_progress_cb( + stats: *const raw::git_indexer_progress, + payload: *mut c_void, +) -> c_int { + let ok = panic::wrap(|| unsafe { + let payload = &mut *(payload as *mut OdbPackwriterCb<'_>); + + let callback = match payload.cb { + Some(ref mut cb) => cb, + None => return true, + }; + + let progress: Progress<'_> = Binding::from_raw(stats); + callback(progress) + }); + if ok == Some(true) { + 0 + } else { + -1 + } +} + +#[cfg(test)] +mod tests { + use crate::{Buf, ObjectType, Oid, Repository}; + use std::io::prelude::*; + use tempfile::TempDir; + + #[test] + fn read() { + let td = TempDir::new().unwrap(); + let repo = Repository::init(td.path()).unwrap(); + let dat = [4, 3, 5, 6, 9]; + let id = repo.blob(&dat).unwrap(); + let db = repo.odb().unwrap(); + let obj = db.read(id).unwrap(); + let data = obj.data(); + let size = obj.len(); + assert_eq!(size, 5); + assert_eq!(dat, data); + assert_eq!(id, obj.id()); + } + + #[test] + fn read_header() { + let td = TempDir::new().unwrap(); + let repo = Repository::init(td.path()).unwrap(); + let dat = [4, 3, 5, 6, 9]; + let id = repo.blob(&dat).unwrap(); + let db = repo.odb().unwrap(); + let (size, kind) = db.read_header(id).unwrap(); + + assert_eq!(size, 5); + assert_eq!(kind, ObjectType::Blob); + } + + #[test] + fn write() { + let td = TempDir::new().unwrap(); + let repo = Repository::init(td.path()).unwrap(); + let dat = [4, 3, 5, 6, 9]; + let db = repo.odb().unwrap(); + let id = db.write(ObjectType::Blob, &dat).unwrap(); + let blob = repo.find_blob(id).unwrap(); + assert_eq!(blob.content(), dat); + } + + #[test] + fn writer() { + let td = TempDir::new().unwrap(); + let repo = Repository::init(td.path()).unwrap(); + let dat = [4, 3, 5, 6, 9]; + let db = repo.odb().unwrap(); + let mut ws = db.writer(dat.len(), ObjectType::Blob).unwrap(); + let wl = ws.write(&dat[0..3]).unwrap(); + assert_eq!(wl, 3); + let wl = ws.write(&dat[3..5]).unwrap(); + assert_eq!(wl, 2); + let id = ws.finalize().unwrap(); + let blob = repo.find_blob(id).unwrap(); + assert_eq!(blob.content(), dat); + } + + #[test] + fn exists() { + let td = TempDir::new().unwrap(); + let repo = Repository::init(td.path()).unwrap(); + let dat = [4, 3, 5, 6, 9]; + let db = repo.odb().unwrap(); + let id = db.write(ObjectType::Blob, &dat).unwrap(); + assert!(db.exists(id)); + } + + #[test] + fn exists_prefix() { + let td = TempDir::new().unwrap(); + let repo = Repository::init(td.path()).unwrap(); + let dat = [4, 3, 5, 6, 9]; + let db = repo.odb().unwrap(); + let id = db.write(ObjectType::Blob, &dat).unwrap(); + let id_prefix_str = &id.to_string()[0..10]; + let id_prefix = Oid::from_str(id_prefix_str).unwrap(); + let found_oid = db.exists_prefix(id_prefix, 10).unwrap(); + assert_eq!(found_oid, id); + } + + #[test] + fn packwriter() { + let (_td, repo_source) = crate::test::repo_init(); + let (_td, repo_target) = crate::test::repo_init(); + let mut builder = t!(repo_source.packbuilder()); + let mut buf = Buf::new(); + let (commit_source_id, _tree) = crate::test::commit(&repo_source); + t!(builder.insert_object(commit_source_id, None)); + t!(builder.write_buf(&mut buf)); + let db = repo_target.odb().unwrap(); + let mut packwriter = db.packwriter().unwrap(); + packwriter.write(&buf).unwrap(); + packwriter.commit().unwrap(); + let commit_target = repo_target.find_commit(commit_source_id).unwrap(); + assert_eq!(commit_target.id(), commit_source_id); + } + + #[test] + fn packwriter_progress() { + let mut progress_called = false; + { + let (_td, repo_source) = crate::test::repo_init(); + let (_td, repo_target) = crate::test::repo_init(); + let mut builder = t!(repo_source.packbuilder()); + let mut buf = Buf::new(); + let (commit_source_id, _tree) = crate::test::commit(&repo_source); + t!(builder.insert_object(commit_source_id, None)); + t!(builder.write_buf(&mut buf)); + let db = repo_target.odb().unwrap(); + let mut packwriter = db.packwriter().unwrap(); + packwriter.progress(|_| { + progress_called = true; + true + }); + packwriter.write(&buf).unwrap(); + packwriter.commit().unwrap(); + } + assert_eq!(progress_called, true); + } + + #[test] + fn write_with_mempack() { + use crate::{Buf, ResetType}; + use std::io::Write; + use std::path::Path; + + // Create a repo, add a mempack backend + let (_td, repo) = crate::test::repo_init(); + let odb = repo.odb().unwrap(); + let mempack = odb.add_new_mempack_backend(1000).unwrap(); + + // Sanity check that foo doesn't exist initially + let foo_file = Path::new(repo.workdir().unwrap()).join("foo"); + assert!(!foo_file.exists()); + + // Make a commit that adds foo. This writes new stuff into the mempack + // backend. + let (oid1, _id) = crate::test::commit(&repo); + let commit1 = repo.find_commit(oid1).unwrap(); + t!(repo.reset(commit1.as_object(), ResetType::Hard, None)); + assert!(foo_file.exists()); + + // Dump the mempack modifications into a buf, and reset it. This "erases" + // commit-related objects from the repository. Ensure the commit appears + // to have become invalid, by checking for failure in `reset --hard`. + let mut buf = Buf::new(); + mempack.dump(&repo, &mut buf).unwrap(); + mempack.reset().unwrap(); + assert!(repo + .reset(commit1.as_object(), ResetType::Hard, None) + .is_err()); + + // Write the buf into a packfile in the repo. This brings back the + // missing objects, and we verify everything is good again. + let mut packwriter = odb.packwriter().unwrap(); + packwriter.write(&buf).unwrap(); + packwriter.commit().unwrap(); + t!(repo.reset(commit1.as_object(), ResetType::Hard, None)); + assert!(foo_file.exists()); + } +} diff --git a/extra/git2/src/oid.rs b/extra/git2/src/oid.rs new file mode 100644 index 000000000..145458aec --- /dev/null +++ b/extra/git2/src/oid.rs @@ -0,0 +1,259 @@ +use libc; +use std::cmp::Ordering; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::path::Path; +use std::str; + +use crate::{raw, Error, IntoCString, ObjectType}; + +use crate::util::{c_cmp_to_ordering, Binding}; + +/// Unique identity of any object (commit, tree, blob, tag). +#[derive(Copy, Clone)] +#[repr(C)] +pub struct Oid { + raw: raw::git_oid, +} + +impl Oid { + /// Parse a hex-formatted object id into an Oid structure. + /// + /// # Errors + /// + /// Returns an error if the string is empty, is longer than 40 hex + /// characters, or contains any non-hex characters. + pub fn from_str(s: &str) -> Result<Oid, Error> { + crate::init(); + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_oid_fromstrn( + &mut raw, + s.as_bytes().as_ptr() as *const libc::c_char, + s.len() as libc::size_t + )); + } + Ok(Oid { raw }) + } + + /// Parse a raw object id into an Oid structure. + /// + /// If the array given is not 20 bytes in length, an error is returned. + pub fn from_bytes(bytes: &[u8]) -> Result<Oid, Error> { + crate::init(); + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + if bytes.len() != raw::GIT_OID_RAWSZ { + Err(Error::from_str("raw byte array must be 20 bytes")) + } else { + unsafe { + try_call!(raw::git_oid_fromraw(&mut raw, bytes.as_ptr())); + } + Ok(Oid { raw }) + } + } + + /// Creates an all zero Oid structure. + pub fn zero() -> Oid { + let out = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + Oid { raw: out } + } + + /// Hashes the provided data as an object of the provided type, and returns + /// an Oid corresponding to the result. This does not store the object + /// inside any object database or repository. + pub fn hash_object(kind: ObjectType, bytes: &[u8]) -> Result<Oid, Error> { + crate::init(); + + let mut out = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_odb_hash( + &mut out, + bytes.as_ptr() as *const libc::c_void, + bytes.len(), + kind.raw() + )); + } + + Ok(Oid { raw: out }) + } + + /// Hashes the content of the provided file as an object of the provided type, + /// and returns an Oid corresponding to the result. This does not store the object + /// inside any object database or repository. + pub fn hash_file<P: AsRef<Path>>(kind: ObjectType, path: P) -> Result<Oid, Error> { + crate::init(); + + // Normal file path OK (does not need Windows conversion). + let rpath = path.as_ref().into_c_string()?; + + let mut out = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_odb_hashfile(&mut out, rpath, kind.raw())); + } + + Ok(Oid { raw: out }) + } + + /// View this OID as a byte-slice 20 bytes in length. + pub fn as_bytes(&self) -> &[u8] { + &self.raw.id + } + + /// Test if this OID is all zeros. + pub fn is_zero(&self) -> bool { + unsafe { raw::git_oid_iszero(&self.raw) == 1 } + } +} + +impl Binding for Oid { + type Raw = *const raw::git_oid; + + unsafe fn from_raw(oid: *const raw::git_oid) -> Oid { + Oid { raw: *oid } + } + fn raw(&self) -> *const raw::git_oid { + &self.raw as *const _ + } +} + +impl fmt::Debug for Oid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl fmt::Display for Oid { + /// Hex-encode this Oid into a formatter. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut dst = [0u8; raw::GIT_OID_HEXSZ + 1]; + unsafe { + raw::git_oid_tostr( + dst.as_mut_ptr() as *mut libc::c_char, + dst.len() as libc::size_t, + &self.raw, + ); + } + let s = &dst[..dst.iter().position(|&a| a == 0).unwrap()]; + str::from_utf8(s).unwrap().fmt(f) + } +} + +impl str::FromStr for Oid { + type Err = Error; + + /// Parse a hex-formatted object id into an Oid structure. + /// + /// # Errors + /// + /// Returns an error if the string is empty, is longer than 40 hex + /// characters, or contains any non-hex characters. + fn from_str(s: &str) -> Result<Oid, Error> { + Oid::from_str(s) + } +} + +impl PartialEq for Oid { + fn eq(&self, other: &Oid) -> bool { + unsafe { raw::git_oid_equal(&self.raw, &other.raw) != 0 } + } +} +impl Eq for Oid {} + +impl PartialOrd for Oid { + fn partial_cmp(&self, other: &Oid) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for Oid { + fn cmp(&self, other: &Oid) -> Ordering { + c_cmp_to_ordering(unsafe { raw::git_oid_cmp(&self.raw, &other.raw) }) + } +} + +impl Hash for Oid { + fn hash<H: Hasher>(&self, into: &mut H) { + self.raw.id.hash(into) + } +} + +impl AsRef<[u8]> for Oid { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +#[cfg(test)] +mod tests { + use std::fs::File; + use std::io::prelude::*; + + use super::Error; + use super::Oid; + use crate::ObjectType; + use tempfile::TempDir; + + #[test] + fn conversions() { + assert!(Oid::from_str("foo").is_err()); + assert!(Oid::from_str("decbf2be529ab6557d5429922251e5ee36519817").is_ok()); + assert!(Oid::from_bytes(b"foo").is_err()); + assert!(Oid::from_bytes(b"00000000000000000000").is_ok()); + } + + #[test] + fn comparisons() -> Result<(), Error> { + assert_eq!(Oid::from_str("decbf2b")?, Oid::from_str("decbf2b")?); + assert!(Oid::from_str("decbf2b")? <= Oid::from_str("decbf2b")?); + assert!(Oid::from_str("decbf2b")? >= Oid::from_str("decbf2b")?); + { + let o = Oid::from_str("decbf2b")?; + assert_eq!(o, o); + assert!(o <= o); + assert!(o >= o); + } + assert_eq!( + Oid::from_str("decbf2b")?, + Oid::from_str("decbf2b000000000000000000000000000000000")? + ); + assert!( + Oid::from_bytes(b"00000000000000000000")? < Oid::from_bytes(b"00000000000000000001")? + ); + assert!(Oid::from_bytes(b"00000000000000000000")? < Oid::from_str("decbf2b")?); + assert_eq!( + Oid::from_bytes(b"00000000000000000000")?, + Oid::from_str("3030303030303030303030303030303030303030")? + ); + Ok(()) + } + + #[test] + fn zero_is_zero() { + assert!(Oid::zero().is_zero()); + } + + #[test] + fn hash_object() { + let bytes = "Hello".as_bytes(); + assert!(Oid::hash_object(ObjectType::Blob, bytes).is_ok()); + } + + #[test] + fn hash_file() { + let td = TempDir::new().unwrap(); + let path = td.path().join("hello.txt"); + let mut file = File::create(&path).unwrap(); + file.write_all("Hello".as_bytes()).unwrap(); + assert!(Oid::hash_file(ObjectType::Blob, &path).is_ok()); + } +} diff --git a/extra/git2/src/oid_array.rs b/extra/git2/src/oid_array.rs new file mode 100644 index 000000000..0d87ce995 --- /dev/null +++ b/extra/git2/src/oid_array.rs @@ -0,0 +1,52 @@ +//! Bindings to libgit2's raw `git_oidarray` type + +use std::ops::Deref; + +use crate::oid::Oid; +use crate::raw; +use crate::util::Binding; +use std::mem; +use std::slice; + +/// An oid array structure used by libgit2 +/// +/// Some APIs return arrays of OIDs which originate from libgit2. This +/// wrapper type behaves a little like `Vec<&Oid>` but does so without copying +/// the underlying Oids until necessary. +pub struct OidArray { + raw: raw::git_oidarray, +} + +impl Deref for OidArray { + type Target = [Oid]; + + fn deref(&self) -> &[Oid] { + unsafe { + debug_assert_eq!(mem::size_of::<Oid>(), mem::size_of_val(&*self.raw.ids)); + + slice::from_raw_parts(self.raw.ids as *const Oid, self.raw.count as usize) + } + } +} + +impl Binding for OidArray { + type Raw = raw::git_oidarray; + unsafe fn from_raw(raw: raw::git_oidarray) -> OidArray { + OidArray { raw } + } + fn raw(&self) -> raw::git_oidarray { + self.raw + } +} + +impl<'repo> std::fmt::Debug for OidArray { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_tuple("OidArray").field(&self.deref()).finish() + } +} + +impl Drop for OidArray { + fn drop(&mut self) { + unsafe { raw::git_oidarray_free(&mut self.raw) } + } +} diff --git a/extra/git2/src/opts.rs b/extra/git2/src/opts.rs new file mode 100644 index 000000000..e90bea0b1 --- /dev/null +++ b/extra/git2/src/opts.rs @@ -0,0 +1,206 @@ +//! 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}; + +/// Set the search path for a level of config data. The search path applied to +/// shared attributes and ignore files, too. +/// +/// `level` must be one of [`ConfigLevel::System`], [`ConfigLevel::Global`], +/// [`ConfigLevel::XDG`], [`ConfigLevel::ProgramData`]. +/// +/// `path` lists directories delimited by `GIT_PATH_LIST_SEPARATOR`. +/// Use magic path `$PATH` to include the old value of the path +/// (if you want to prepend or append, for instance). +/// +/// This function is unsafe as it mutates the global state but cannot guarantee +/// thread-safety. It needs to be externally synchronized with calls to access +/// the global state. +pub unsafe fn set_search_path<P>(level: ConfigLevel, path: P) -> Result<(), Error> +where + P: IntoCString, +{ + crate::init(); + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_SET_SEARCH_PATH as libc::c_int, + level as libc::c_int, + path.into_c_string()?.as_ptr() + )); + Ok(()) +} + +/// Reset the search path for a given level of config data to the default +/// (generally based on environment variables). +/// +/// `level` must be one of [`ConfigLevel::System`], [`ConfigLevel::Global`], +/// [`ConfigLevel::XDG`], [`ConfigLevel::ProgramData`]. +/// +/// This function is unsafe as it mutates the global state but cannot guarantee +/// thread-safety. It needs to be externally synchronized with calls to access +/// the global state. +pub unsafe fn reset_search_path(level: ConfigLevel) -> Result<(), Error> { + crate::init(); + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_SET_SEARCH_PATH as libc::c_int, + level as libc::c_int, + core::ptr::null::<u8>() + )); + Ok(()) +} + +/// Get the search path for a given level of config data. +/// +/// `level` must be one of [`ConfigLevel::System`], [`ConfigLevel::Global`], +/// [`ConfigLevel::XDG`], [`ConfigLevel::ProgramData`]. +/// +/// This function is unsafe as it mutates the global state but cannot guarantee +/// thread-safety. It needs to be externally synchronized with calls to access +/// the global state. +pub unsafe fn get_search_path(level: ConfigLevel) -> Result<CString, Error> { + crate::init(); + let buf = Buf::new(); + try_call!(raw::git_libgit2_opts( + raw::GIT_OPT_GET_SEARCH_PATH as libc::c_int, + level as libc::c_int, + buf.raw() as *const _ + )); + buf.into_c_string() +} + +/// Controls whether or not libgit2 will cache loaded objects. Enabled by +/// default, but disabling this can improve performance and memory usage if +/// loading a large number of objects that will not be referenced again. +/// Disabling this will cause repository objects to clear their caches when next +/// accessed. +pub fn enable_caching(enabled: bool) { + crate::init(); + let error = unsafe { + raw::git_libgit2_opts( + raw::GIT_OPT_ENABLE_CACHING as libc::c_int, + enabled as libc::c_int, + ) + }; + // This function cannot actually fail, but the function has an error return + // for other options that can. + debug_assert!(error >= 0); +} + +/// Controls whether or not libgit2 will verify when writing an object that all +/// objects it references are valid. Enabled by default, but disabling this can +/// significantly improve performance, at the cost of potentially allowing the +/// creation of objects that reference invalid objects (due to programming +/// error or repository corruption). +pub fn strict_object_creation(enabled: bool) { + crate::init(); + let error = unsafe { + raw::git_libgit2_opts( + raw::GIT_OPT_ENABLE_STRICT_OBJECT_CREATION as libc::c_int, + enabled as libc::c_int, + ) + }; + // This function cannot actually fail, but the function has an error return + // for other options that can. + debug_assert!(error >= 0); +} + +/// Controls whether or not libgit2 will verify that objects loaded have the +/// expected hash. Enabled by default, but disabling this can significantly +/// improve performance, at the cost of relying on repository integrity +/// without checking it. +pub fn strict_hash_verification(enabled: bool) { + crate::init(); + let error = unsafe { + raw::git_libgit2_opts( + raw::GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION as libc::c_int, + enabled as libc::c_int, + ) + }; + // This function cannot actually fail, but the function has an error return + // for other options that can. + debug_assert!(error >= 0); +} + +/// Returns the list of git extensions that are supported. This is the list of +/// built-in extensions supported by libgit2 and custom extensions that have +/// been added with [`set_extensions`]. Extensions that have been negated will +/// not be returned. +/// +/// # Safety +/// +/// libgit2 stores user extensions in a static variable. +/// This function is effectively reading a `static mut` and should be treated as such +pub unsafe fn get_extensions() -> Result<StringArray, Error> { + 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<E>(extensions: &[E]) -> Result<(), Error> +where + for<'x> &'x E: IntoCString, +{ + crate::init(); + + let extensions = extensions + .iter() + .map(|e| e.into_c_string()) + .collect::<Result<Vec<_>, _>>()?; + + let extension_ptrs = extensions.iter().map(|e| e.as_ptr()).collect::<Vec<_>>(); + + 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(()) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn smoke() { + strict_hash_verification(false); + } +} diff --git a/extra/git2/src/packbuilder.rs b/extra/git2/src/packbuilder.rs new file mode 100644 index 000000000..9b93e7654 --- /dev/null +++ b/extra/git2/src/packbuilder.rs @@ -0,0 +1,413 @@ +use libc::{c_int, c_uint, c_void, size_t}; +use std::marker; +use std::ptr; +use std::slice; +use std::str; + +use crate::util::Binding; +use crate::{panic, raw, Buf, Error, Oid, Repository, Revwalk}; + +#[derive(PartialEq, Eq, Clone, Debug, Copy)] +/// Stages that are reported by the `PackBuilder` progress callback. +pub enum PackBuilderStage { + /// Adding objects to the pack + AddingObjects, + /// Deltafication of the pack + Deltafication, +} + +pub type ProgressCb<'a> = dyn FnMut(PackBuilderStage, u32, u32) -> bool + 'a; +pub type ForEachCb<'a> = dyn FnMut(&[u8]) -> bool + 'a; + +/// A builder for creating a packfile +pub struct PackBuilder<'repo> { + raw: *mut raw::git_packbuilder, + _progress: Option<Box<Box<ProgressCb<'repo>>>>, + _marker: marker::PhantomData<&'repo Repository>, +} + +impl<'repo> PackBuilder<'repo> { + /// Insert a single object. For an optimal pack it's mandatory to insert + /// objects in recency order, commits followed by trees and blobs. + pub fn insert_object(&mut self, id: Oid, name: Option<&str>) -> Result<(), Error> { + let name = crate::opt_cstr(name)?; + unsafe { + try_call!(raw::git_packbuilder_insert(self.raw, id.raw(), name)); + } + Ok(()) + } + + /// Insert a root tree object. This will add the tree as well as all + /// referenced trees and blobs. + pub fn insert_tree(&mut self, id: Oid) -> Result<(), Error> { + unsafe { + try_call!(raw::git_packbuilder_insert_tree(self.raw, id.raw())); + } + Ok(()) + } + + /// Insert a commit object. This will add a commit as well as the completed + /// referenced tree. + pub fn insert_commit(&mut self, id: Oid) -> Result<(), Error> { + unsafe { + try_call!(raw::git_packbuilder_insert_commit(self.raw, id.raw())); + } + Ok(()) + } + + /// Insert objects as given by the walk. Those commits and all objects they + /// reference will be inserted into the packbuilder. + pub fn insert_walk(&mut self, walk: &mut Revwalk<'_>) -> Result<(), Error> { + unsafe { + try_call!(raw::git_packbuilder_insert_walk(self.raw, walk.raw())); + } + Ok(()) + } + + /// Recursively insert an object and its referenced objects. Insert the + /// object as well as any object it references. + pub fn insert_recursive(&mut self, id: Oid, name: Option<&str>) -> Result<(), Error> { + let name = crate::opt_cstr(name)?; + unsafe { + try_call!(raw::git_packbuilder_insert_recur(self.raw, id.raw(), name)); + } + Ok(()) + } + + /// Write the contents of the packfile to an in-memory buffer. The contents + /// of the buffer will become a valid packfile, even though there will be + /// no attached index. + pub fn write_buf(&mut self, buf: &mut Buf) -> Result<(), Error> { + unsafe { + try_call!(raw::git_packbuilder_write_buf(buf.raw(), self.raw)); + } + Ok(()) + } + + /// Create the new pack and pass each object to the callback. + pub fn foreach<F>(&mut self, mut cb: F) -> Result<(), Error> + where + F: FnMut(&[u8]) -> bool, + { + let mut cb = &mut cb as &mut ForEachCb<'_>; + let ptr = &mut cb as *mut _; + let foreach: raw::git_packbuilder_foreach_cb = Some(foreach_c); + unsafe { + try_call!(raw::git_packbuilder_foreach( + self.raw, + foreach, + ptr as *mut _ + )); + } + Ok(()) + } + + /// `progress` will be called with progress information during pack + /// building. Be aware that this is called inline with pack building + /// operations, so performance may be affected. + /// + /// There can only be one progress callback attached, this will replace any + /// existing one. See `unset_progress_callback` to remove the current + /// progress callback without attaching a new one. + pub fn set_progress_callback<F>(&mut self, progress: F) -> Result<(), Error> + where + F: FnMut(PackBuilderStage, u32, u32) -> bool + 'repo, + { + let mut progress = Box::new(Box::new(progress) as Box<ProgressCb<'_>>); + let ptr = &mut *progress as *mut _; + let progress_c: raw::git_packbuilder_progress = Some(progress_c); + unsafe { + try_call!(raw::git_packbuilder_set_callbacks( + self.raw, + progress_c, + ptr as *mut _ + )); + } + self._progress = Some(progress); + Ok(()) + } + + /// Remove the current progress callback. See `set_progress_callback` to + /// set the progress callback. + pub fn unset_progress_callback(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_packbuilder_set_callbacks( + self.raw, + None, + ptr::null_mut() + )); + self._progress = None; + } + Ok(()) + } + + /// Set the number of threads to be used. + /// + /// Returns the number of threads to be used. + pub fn set_threads(&mut self, threads: u32) -> u32 { + unsafe { raw::git_packbuilder_set_threads(self.raw, threads) } + } + + /// Get the total number of objects the packbuilder will write out. + pub fn object_count(&self) -> usize { + unsafe { raw::git_packbuilder_object_count(self.raw) } + } + + /// Get the number of objects the packbuilder has already written out. + pub fn written(&self) -> usize { + unsafe { raw::git_packbuilder_written(self.raw) } + } + + /// Get the packfile's hash. A packfile's name is derived from the sorted + /// hashing of all object names. This is only correct after the packfile + /// has been written. + #[deprecated = "use `name()` to retrieve the filename"] + #[allow(deprecated)] + pub fn hash(&self) -> Option<Oid> { + if self.object_count() == 0 { + unsafe { Some(Binding::from_raw(raw::git_packbuilder_hash(self.raw))) } + } else { + None + } + } + + /// Get the unique name for the resulting packfile. + /// + /// The packfile's name is derived from the packfile's content. This is only + /// correct after the packfile has been written. + /// + /// Returns `None` if the packfile has not been written or if the name is + /// not valid utf-8. + pub fn name(&self) -> Option<&str> { + self.name_bytes().and_then(|s| str::from_utf8(s).ok()) + } + + /// Get the unique name for the resulting packfile, in bytes. + /// + /// The packfile's name is derived from the packfile's content. This is only + /// correct after the packfile has been written. + pub fn name_bytes(&self) -> Option<&[u8]> { + unsafe { crate::opt_bytes(self, raw::git_packbuilder_name(self.raw)) } + } +} + +impl<'repo> Binding for PackBuilder<'repo> { + type Raw = *mut raw::git_packbuilder; + unsafe fn from_raw(ptr: *mut raw::git_packbuilder) -> PackBuilder<'repo> { + PackBuilder { + raw: ptr, + _progress: None, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_packbuilder { + self.raw + } +} + +impl<'repo> Drop for PackBuilder<'repo> { + fn drop(&mut self) { + unsafe { + raw::git_packbuilder_set_callbacks(self.raw, None, ptr::null_mut()); + raw::git_packbuilder_free(self.raw); + } + } +} + +impl Binding for PackBuilderStage { + type Raw = raw::git_packbuilder_stage_t; + unsafe fn from_raw(raw: raw::git_packbuilder_stage_t) -> PackBuilderStage { + match raw { + raw::GIT_PACKBUILDER_ADDING_OBJECTS => PackBuilderStage::AddingObjects, + raw::GIT_PACKBUILDER_DELTAFICATION => PackBuilderStage::Deltafication, + _ => panic!("Unknown git diff binary kind"), + } + } + fn raw(&self) -> raw::git_packbuilder_stage_t { + match *self { + PackBuilderStage::AddingObjects => raw::GIT_PACKBUILDER_ADDING_OBJECTS, + PackBuilderStage::Deltafication => raw::GIT_PACKBUILDER_DELTAFICATION, + } + } +} + +extern "C" fn foreach_c(buf: *const c_void, size: size_t, data: *mut c_void) -> c_int { + unsafe { + let buf = slice::from_raw_parts(buf as *const u8, size as usize); + + let r = panic::wrap(|| { + let data = data as *mut &mut ForEachCb<'_>; + (*data)(buf) + }); + if r == Some(true) { + 0 + } else { + -1 + } + } +} + +extern "C" fn progress_c( + stage: raw::git_packbuilder_stage_t, + current: c_uint, + total: c_uint, + data: *mut c_void, +) -> c_int { + unsafe { + let stage = Binding::from_raw(stage); + + let r = panic::wrap(|| { + let data = data as *mut Box<ProgressCb<'_>>; + (*data)(stage, current, total) + }); + if r == Some(true) { + 0 + } else { + -1 + } + } +} + +#[cfg(test)] +mod tests { + use crate::Buf; + + fn pack_header(len: u8) -> Vec<u8> { + [].iter() + .chain(b"PACK") // signature + .chain(&[0, 0, 0, 2]) // version number + .chain(&[0, 0, 0, len]) // number of objects + .cloned() + .collect::<Vec<u8>>() + } + + fn empty_pack_header() -> Vec<u8> { + pack_header(0) + .iter() + .chain(&[ + 0x02, 0x9d, 0x08, 0x82, 0x3b, // ^ + 0xd8, 0xa8, 0xea, 0xb5, 0x10, // | SHA-1 of the zero + 0xad, 0x6a, 0xc7, 0x5c, 0x82, // | object pack header + 0x3c, 0xfd, 0x3e, 0xd3, 0x1e, + ]) // v + .cloned() + .collect::<Vec<u8>>() + } + + #[test] + fn smoke() { + let (_td, repo) = crate::test::repo_init(); + let _builder = t!(repo.packbuilder()); + } + + #[test] + fn smoke_write_buf() { + let (_td, repo) = crate::test::repo_init(); + let mut builder = t!(repo.packbuilder()); + let mut buf = Buf::new(); + t!(builder.write_buf(&mut buf)); + #[allow(deprecated)] + { + assert!(builder.hash().unwrap().is_zero()); + } + assert!(builder.name().is_none()); + assert_eq!(&*buf, &*empty_pack_header()); + } + + #[test] + fn smoke_foreach() { + let (_td, repo) = crate::test::repo_init(); + let mut builder = t!(repo.packbuilder()); + let mut buf = Vec::<u8>::new(); + t!(builder.foreach(|bytes| { + buf.extend(bytes); + true + })); + assert_eq!(&*buf, &*empty_pack_header()); + } + + #[test] + fn insert_write_buf() { + let (_td, repo) = crate::test::repo_init(); + let mut builder = t!(repo.packbuilder()); + let mut buf = Buf::new(); + let (commit, _tree) = crate::test::commit(&repo); + t!(builder.insert_object(commit, None)); + assert_eq!(builder.object_count(), 1); + t!(builder.write_buf(&mut buf)); + // Just check that the correct number of objects are written + assert_eq!(&buf[0..12], &*pack_header(1)); + } + + #[test] + fn insert_tree_write_buf() { + let (_td, repo) = crate::test::repo_init(); + let mut builder = t!(repo.packbuilder()); + let mut buf = Buf::new(); + 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_buf(&mut buf)); + // Just check that the correct number of objects are written + assert_eq!(&buf[0..12], &*pack_header(2)); + } + + #[test] + fn insert_commit_write_buf() { + let (_td, repo) = crate::test::repo_init(); + let mut builder = t!(repo.packbuilder()); + let mut buf = Buf::new(); + 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_buf(&mut buf)); + // Just check that the correct number of objects are written + assert_eq!(&buf[0..12], &*pack_header(3)); + } + + #[test] + fn progress_callback() { + 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_buf(&mut Buf::new())); + } + assert_eq!(progress_called, true); + } + + #[test] + fn clear_progress_callback() { + 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.unset_progress_callback()); + t!(builder.insert_commit(commit)); + t!(builder.write_buf(&mut Buf::new())); + } + assert_eq!(progress_called, false); + } + + #[test] + fn set_threads() { + let (_td, repo) = crate::test::repo_init(); + let mut builder = t!(repo.packbuilder()); + let used = builder.set_threads(4); + // Will be 1 if not compiled with threading. + assert!(used == 1 || used == 4); + } +} diff --git a/extra/git2/src/panic.rs b/extra/git2/src/panic.rs new file mode 100644 index 000000000..3e1b208bc --- /dev/null +++ b/extra/git2/src/panic.rs @@ -0,0 +1,33 @@ +use std::any::Any; +use std::cell::RefCell; + +thread_local!(static LAST_ERROR: RefCell<Option<Box<dyn Any + Send>>> = { + RefCell::new(None) +}); + +pub fn wrap<T, F: FnOnce() -> T + std::panic::UnwindSafe>(f: F) -> Option<T> { + use std::panic; + if LAST_ERROR.with(|slot| slot.borrow().is_some()) { + return None; + } + match panic::catch_unwind(f) { + Ok(ret) => Some(ret), + Err(e) => { + LAST_ERROR.with(move |slot| { + *slot.borrow_mut() = Some(e); + }); + None + } + } +} + +pub fn check() { + let err = LAST_ERROR.with(|slot| slot.borrow_mut().take()); + if let Some(err) = err { + std::panic::resume_unwind(err); + } +} + +pub fn panicked() -> bool { + LAST_ERROR.with(|slot| slot.borrow().is_some()) +} diff --git a/extra/git2/src/patch.rs b/extra/git2/src/patch.rs new file mode 100644 index 000000000..67b84c0f0 --- /dev/null +++ b/extra/git2/src/patch.rs @@ -0,0 +1,235 @@ +use libc::{c_int, c_void}; +use std::marker::PhantomData; +use std::path::Path; +use std::ptr; + +use crate::diff::{print_cb, LineCb}; +use crate::util::{into_opt_c_string, Binding}; +use crate::{raw, Blob, Buf, Diff, DiffDelta, DiffHunk, DiffLine, DiffOptions, Error}; + +/// A structure representing the text changes in a single diff delta. +/// +/// This is an opaque structure. +pub struct Patch<'buffers> { + raw: *mut raw::git_patch, + buffers: PhantomData<&'buffers ()>, +} + +unsafe impl<'buffers> Send for Patch<'buffers> {} + +impl<'buffers> Binding for Patch<'buffers> { + type Raw = *mut raw::git_patch; + unsafe fn from_raw(raw: Self::Raw) -> Self { + Patch { + raw, + buffers: PhantomData, + } + } + fn raw(&self) -> Self::Raw { + self.raw + } +} + +impl<'buffers> Drop for Patch<'buffers> { + fn drop(&mut self) { + unsafe { raw::git_patch_free(self.raw) } + } +} + +impl<'buffers> Patch<'buffers> { + /// Return a Patch for one file in a Diff. + /// + /// Returns Ok(None) for an unchanged or binary file. + pub fn from_diff(diff: &Diff<'buffers>, idx: usize) -> Result<Option<Self>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_patch_from_diff(&mut ret, diff.raw(), idx)); + Ok(Binding::from_raw_opt(ret)) + } + } + + /// Generate a Patch by diffing two blobs. + pub fn from_blobs( + old_blob: &Blob<'buffers>, + old_path: Option<&Path>, + new_blob: &Blob<'buffers>, + new_path: Option<&Path>, + opts: Option<&mut DiffOptions>, + ) -> Result<Self, Error> { + let mut ret = ptr::null_mut(); + let old_path = into_opt_c_string(old_path)?; + let new_path = into_opt_c_string(new_path)?; + unsafe { + try_call!(raw::git_patch_from_blobs( + &mut ret, + old_blob.raw(), + old_path, + new_blob.raw(), + new_path, + opts.map(|s| s.raw()) + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Generate a Patch by diffing a blob and a buffer. + pub fn from_blob_and_buffer( + old_blob: &Blob<'buffers>, + old_path: Option<&Path>, + new_buffer: &'buffers [u8], + new_path: Option<&Path>, + opts: Option<&mut DiffOptions>, + ) -> Result<Self, Error> { + let mut ret = ptr::null_mut(); + let old_path = into_opt_c_string(old_path)?; + let new_path = into_opt_c_string(new_path)?; + unsafe { + try_call!(raw::git_patch_from_blob_and_buffer( + &mut ret, + old_blob.raw(), + old_path, + new_buffer.as_ptr() as *const c_void, + new_buffer.len(), + new_path, + opts.map(|s| s.raw()) + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Generate a Patch by diffing two buffers. + pub fn from_buffers( + old_buffer: &'buffers [u8], + old_path: Option<&Path>, + new_buffer: &'buffers [u8], + new_path: Option<&Path>, + opts: Option<&mut DiffOptions>, + ) -> Result<Self, Error> { + crate::init(); + let mut ret = ptr::null_mut(); + let old_path = into_opt_c_string(old_path)?; + let new_path = into_opt_c_string(new_path)?; + unsafe { + try_call!(raw::git_patch_from_buffers( + &mut ret, + old_buffer.as_ptr() as *const c_void, + old_buffer.len(), + old_path, + new_buffer.as_ptr() as *const c_void, + new_buffer.len(), + new_path, + opts.map(|s| s.raw()) + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Get the DiffDelta associated with the Patch. + pub fn delta(&self) -> DiffDelta<'buffers> { + unsafe { Binding::from_raw(raw::git_patch_get_delta(self.raw) as *mut _) } + } + + /// Get the number of hunks in the Patch. + pub fn num_hunks(&self) -> usize { + unsafe { raw::git_patch_num_hunks(self.raw) } + } + + /// Get the number of lines of context, additions, and deletions in the Patch. + pub fn line_stats(&self) -> Result<(usize, usize, usize), Error> { + let mut context = 0; + let mut additions = 0; + let mut deletions = 0; + unsafe { + try_call!(raw::git_patch_line_stats( + &mut context, + &mut additions, + &mut deletions, + self.raw + )); + } + Ok((context, additions, deletions)) + } + + /// Get a DiffHunk and its total line count from the Patch. + pub fn hunk(&self, hunk_idx: usize) -> Result<(DiffHunk<'buffers>, usize), Error> { + let mut ret = ptr::null(); + let mut lines = 0; + unsafe { + try_call!(raw::git_patch_get_hunk( + &mut ret, &mut lines, self.raw, hunk_idx + )); + Ok((Binding::from_raw(ret), lines)) + } + } + + /// Get the number of lines in a hunk. + pub fn num_lines_in_hunk(&self, hunk_idx: usize) -> Result<usize, Error> { + unsafe { Ok(try_call!(raw::git_patch_num_lines_in_hunk(self.raw, hunk_idx)) as usize) } + } + + /// Get a DiffLine from a hunk of the Patch. + pub fn line_in_hunk( + &self, + hunk_idx: usize, + line_of_hunk: usize, + ) -> Result<DiffLine<'buffers>, Error> { + let mut ret = ptr::null(); + unsafe { + try_call!(raw::git_patch_get_line_in_hunk( + &mut ret, + self.raw, + hunk_idx, + line_of_hunk + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Get the size of a Patch's diff data in bytes. + pub fn size( + &self, + include_context: bool, + include_hunk_headers: bool, + include_file_headers: bool, + ) -> usize { + unsafe { + raw::git_patch_size( + self.raw, + include_context as c_int, + include_hunk_headers as c_int, + include_file_headers as c_int, + ) + } + } + + /// Print the Patch to text via a callback. + pub fn print(&mut self, mut line_cb: &mut LineCb<'_>) -> Result<(), Error> { + let ptr = &mut line_cb as *mut _ as *mut c_void; + unsafe { + let cb: raw::git_diff_line_cb = Some(print_cb); + try_call!(raw::git_patch_print(self.raw, cb, ptr)); + Ok(()) + } + } + + /// Get the Patch text as a Buf. + pub fn to_buf(&mut self) -> Result<Buf, Error> { + let buf = Buf::new(); + unsafe { + try_call!(raw::git_patch_to_buf(buf.raw(), self.raw)); + } + Ok(buf) + } +} + +impl<'buffers> std::fmt::Debug for Patch<'buffers> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut ds = f.debug_struct("Patch"); + ds.field("delta", &self.delta()) + .field("num_hunks", &self.num_hunks()); + if let Ok(line_stats) = &self.line_stats() { + ds.field("line_stats", line_stats); + } + ds.finish() + } +} diff --git a/extra/git2/src/pathspec.rs b/extra/git2/src/pathspec.rs new file mode 100644 index 000000000..48174fcc1 --- /dev/null +++ b/extra/git2/src/pathspec.rs @@ -0,0 +1,368 @@ +use libc::size_t; +use std::iter::{FusedIterator, IntoIterator}; +use std::marker; +use std::ops::Range; +use std::path::Path; +use std::ptr; + +use crate::util::{path_to_repo_path, Binding}; +use crate::{raw, Diff, DiffDelta, Error, Index, IntoCString, PathspecFlags, Repository, Tree}; + +/// Structure representing a compiled pathspec used for matching against various +/// structures. +pub struct Pathspec { + raw: *mut raw::git_pathspec, +} + +/// List of filenames matching a pathspec. +pub struct PathspecMatchList<'ps> { + raw: *mut raw::git_pathspec_match_list, + _marker: marker::PhantomData<&'ps Pathspec>, +} + +/// Iterator over the matched paths in a pathspec. +pub struct PathspecEntries<'list> { + range: Range<usize>, + list: &'list PathspecMatchList<'list>, +} + +/// Iterator over the matching diff deltas. +pub struct PathspecDiffEntries<'list> { + range: Range<usize>, + list: &'list PathspecMatchList<'list>, +} + +/// Iterator over the failed list of pathspec items that did not match. +pub struct PathspecFailedEntries<'list> { + range: Range<usize>, + list: &'list PathspecMatchList<'list>, +} + +impl Pathspec { + /// Creates a new pathspec from a list of specs to match against. + pub fn new<I, T>(specs: I) -> Result<Pathspec, Error> + where + T: IntoCString, + I: IntoIterator<Item = T>, + { + crate::init(); + let (_a, _b, arr) = crate::util::iter2cstrs_paths(specs)?; + unsafe { + let mut ret = ptr::null_mut(); + try_call!(raw::git_pathspec_new(&mut ret, &arr)); + Ok(Binding::from_raw(ret)) + } + } + + /// Match a pathspec against files in a diff. + /// + /// The list returned contains the list of all matched filenames (unless you + /// pass `PATHSPEC_FAILURES_ONLY` in the flags) and may also contain the + /// list of pathspecs with no match if the `PATHSPEC_FIND_FAILURES` flag is + /// specified. + pub fn match_diff( + &self, + diff: &Diff<'_>, + flags: PathspecFlags, + ) -> Result<PathspecMatchList<'_>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_pathspec_match_diff( + &mut ret, + diff.raw(), + flags.bits(), + self.raw + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Match a pathspec against files in a tree. + /// + /// The list returned contains the list of all matched filenames (unless you + /// pass `PATHSPEC_FAILURES_ONLY` in the flags) and may also contain the + /// list of pathspecs with no match if the `PATHSPEC_FIND_FAILURES` flag is + /// specified. + pub fn match_tree( + &self, + tree: &Tree<'_>, + flags: PathspecFlags, + ) -> Result<PathspecMatchList<'_>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_pathspec_match_tree( + &mut ret, + tree.raw(), + flags.bits(), + self.raw + )); + Ok(Binding::from_raw(ret)) + } + } + + /// This matches the pathspec against the files in the repository index. + /// + /// The list returned contains the list of all matched filenames (unless you + /// pass `PATHSPEC_FAILURES_ONLY` in the flags) and may also contain the + /// list of pathspecs with no match if the `PATHSPEC_FIND_FAILURES` flag is + /// specified. + pub fn match_index( + &self, + index: &Index, + flags: PathspecFlags, + ) -> Result<PathspecMatchList<'_>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_pathspec_match_index( + &mut ret, + index.raw(), + flags.bits(), + self.raw + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Match a pathspec against the working directory of a repository. + /// + /// This matches the pathspec against the current files in the working + /// directory of the repository. It is an error to invoke this on a bare + /// repo. This handles git ignores (i.e. ignored files will not be + /// considered to match the pathspec unless the file is tracked in the + /// index). + /// + /// The list returned contains the list of all matched filenames (unless you + /// pass `PATHSPEC_FAILURES_ONLY` in the flags) and may also contain the + /// list of pathspecs with no match if the `PATHSPEC_FIND_FAILURES` flag is + /// specified. + pub fn match_workdir( + &self, + repo: &Repository, + flags: PathspecFlags, + ) -> Result<PathspecMatchList<'_>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_pathspec_match_workdir( + &mut ret, + repo.raw(), + flags.bits(), + self.raw + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Try to match a path against a pathspec + /// + /// Unlike most of the other pathspec matching functions, this will not fall + /// back on the native case-sensitivity for your platform. You must + /// explicitly pass flags to control case sensitivity or else this will fall + /// back on being case sensitive. + pub fn matches_path(&self, path: &Path, flags: PathspecFlags) -> bool { + let path = path_to_repo_path(path).unwrap(); + unsafe { raw::git_pathspec_matches_path(&*self.raw, flags.bits(), path.as_ptr()) == 1 } + } +} + +impl Binding for Pathspec { + type Raw = *mut raw::git_pathspec; + + unsafe fn from_raw(raw: *mut raw::git_pathspec) -> Pathspec { + Pathspec { raw } + } + fn raw(&self) -> *mut raw::git_pathspec { + self.raw + } +} + +impl Drop for Pathspec { + fn drop(&mut self) { + unsafe { raw::git_pathspec_free(self.raw) } + } +} + +impl<'ps> PathspecMatchList<'ps> { + fn entrycount(&self) -> usize { + unsafe { raw::git_pathspec_match_list_entrycount(&*self.raw) as usize } + } + + fn failed_entrycount(&self) -> usize { + unsafe { raw::git_pathspec_match_list_failed_entrycount(&*self.raw) as usize } + } + + /// Returns an iterator over the matching filenames in this list. + pub fn entries(&self) -> PathspecEntries<'_> { + let n = self.entrycount(); + let n = if n > 0 && self.entry(0).is_none() { + 0 + } else { + n + }; + PathspecEntries { + range: 0..n, + list: self, + } + } + + /// Get a matching filename by position. + /// + /// If this list was generated from a diff, then the return value will + /// always be `None. + pub fn entry(&self, i: usize) -> Option<&[u8]> { + unsafe { + let ptr = raw::git_pathspec_match_list_entry(&*self.raw, i as size_t); + crate::opt_bytes(self, ptr) + } + } + + /// Returns an iterator over the matching diff entries in this list. + pub fn diff_entries(&self) -> PathspecDiffEntries<'_> { + let n = self.entrycount(); + let n = if n > 0 && self.diff_entry(0).is_none() { + 0 + } else { + n + }; + PathspecDiffEntries { + range: 0..n, + list: self, + } + } + + /// Get a matching diff delta by position. + /// + /// If the list was not generated from a diff, then the return value will + /// always be `None`. + pub fn diff_entry(&self, i: usize) -> Option<DiffDelta<'_>> { + unsafe { + let ptr = raw::git_pathspec_match_list_diff_entry(&*self.raw, i as size_t); + Binding::from_raw_opt(ptr as *mut _) + } + } + + /// Returns an iterator over the non-matching entries in this list. + pub fn failed_entries(&self) -> PathspecFailedEntries<'_> { + let n = self.failed_entrycount(); + let n = if n > 0 && self.failed_entry(0).is_none() { + 0 + } else { + n + }; + PathspecFailedEntries { + range: 0..n, + list: self, + } + } + + /// Get an original pathspec string that had no matches. + pub fn failed_entry(&self, i: usize) -> Option<&[u8]> { + unsafe { + let ptr = raw::git_pathspec_match_list_failed_entry(&*self.raw, i as size_t); + crate::opt_bytes(self, ptr) + } + } +} + +impl<'ps> Binding for PathspecMatchList<'ps> { + type Raw = *mut raw::git_pathspec_match_list; + + unsafe fn from_raw(raw: *mut raw::git_pathspec_match_list) -> PathspecMatchList<'ps> { + PathspecMatchList { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_pathspec_match_list { + self.raw + } +} + +impl<'ps> Drop for PathspecMatchList<'ps> { + fn drop(&mut self) { + unsafe { raw::git_pathspec_match_list_free(self.raw) } + } +} + +impl<'list> Iterator for PathspecEntries<'list> { + type Item = &'list [u8]; + fn next(&mut self) -> Option<&'list [u8]> { + self.range.next().and_then(|i| self.list.entry(i)) + } + fn size_hint(&self) -> (usize, Option<usize>) { + self.range.size_hint() + } +} +impl<'list> DoubleEndedIterator for PathspecEntries<'list> { + fn next_back(&mut self) -> Option<&'list [u8]> { + self.range.next_back().and_then(|i| self.list.entry(i)) + } +} +impl<'list> FusedIterator for PathspecEntries<'list> {} +impl<'list> ExactSizeIterator for PathspecEntries<'list> {} + +impl<'list> Iterator for PathspecDiffEntries<'list> { + type Item = DiffDelta<'list>; + fn next(&mut self) -> Option<DiffDelta<'list>> { + self.range.next().and_then(|i| self.list.diff_entry(i)) + } + fn size_hint(&self) -> (usize, Option<usize>) { + self.range.size_hint() + } +} +impl<'list> DoubleEndedIterator for PathspecDiffEntries<'list> { + fn next_back(&mut self) -> Option<DiffDelta<'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> { + type Item = &'list [u8]; + fn next(&mut self) -> Option<&'list [u8]> { + self.range.next().and_then(|i| self.list.failed_entry(i)) + } + fn size_hint(&self) -> (usize, Option<usize>) { + self.range.size_hint() + } +} +impl<'list> DoubleEndedIterator for PathspecFailedEntries<'list> { + fn next_back(&mut self) -> Option<&'list [u8]> { + self.range + .next_back() + .and_then(|i| self.list.failed_entry(i)) + } +} +impl<'list> FusedIterator for PathspecFailedEntries<'list> {} +impl<'list> ExactSizeIterator for PathspecFailedEntries<'list> {} + +#[cfg(test)] +mod tests { + use super::Pathspec; + use crate::PathspecFlags; + use std::fs::File; + use std::path::Path; + + #[test] + fn smoke() { + let ps = Pathspec::new(["a"].iter()).unwrap(); + assert!(ps.matches_path(Path::new("a"), PathspecFlags::DEFAULT)); + assert!(ps.matches_path(Path::new("a/b"), PathspecFlags::DEFAULT)); + assert!(!ps.matches_path(Path::new("b"), PathspecFlags::DEFAULT)); + assert!(!ps.matches_path(Path::new("ab/c"), PathspecFlags::DEFAULT)); + + let (td, repo) = crate::test::repo_init(); + let list = ps.match_workdir(&repo, PathspecFlags::DEFAULT).unwrap(); + assert_eq!(list.entries().len(), 0); + assert_eq!(list.diff_entries().len(), 0); + assert_eq!(list.failed_entries().len(), 0); + + File::create(&td.path().join("a")).unwrap(); + + let list = ps + .match_workdir(&repo, crate::PathspecFlags::FIND_FAILURES) + .unwrap(); + assert_eq!(list.entries().len(), 1); + assert_eq!(list.entries().next(), Some("a".as_bytes())); + } +} diff --git a/extra/git2/src/proxy_options.rs b/extra/git2/src/proxy_options.rs new file mode 100644 index 000000000..b19ba3a52 --- /dev/null +++ b/extra/git2/src/proxy_options.rs @@ -0,0 +1,56 @@ +use std::ffi::CString; +use std::marker; +use std::ptr; + +use crate::raw; +use crate::util::Binding; + +/// Options which can be specified to various fetch operations. +#[derive(Default)] +pub struct ProxyOptions<'a> { + url: Option<CString>, + proxy_kind: raw::git_proxy_t, + _marker: marker::PhantomData<&'a i32>, +} + +impl<'a> ProxyOptions<'a> { + /// Creates a new set of proxy options ready to be configured. + pub fn new() -> ProxyOptions<'a> { + Default::default() + } + + /// Try to auto-detect the proxy from the git configuration. + /// + /// Note that this will override `url` specified before. + pub fn auto(&mut self) -> &mut Self { + self.proxy_kind = raw::GIT_PROXY_AUTO; + self + } + + /// Specify the exact URL of the proxy to use. + /// + /// Note that this will override `auto` specified before. + pub fn url(&mut self, url: &str) -> &mut Self { + self.proxy_kind = raw::GIT_PROXY_SPECIFIED; + self.url = Some(CString::new(url).unwrap()); + self + } +} + +impl<'a> Binding for ProxyOptions<'a> { + type Raw = raw::git_proxy_options; + unsafe fn from_raw(_raw: raw::git_proxy_options) -> ProxyOptions<'a> { + panic!("can't create proxy from raw options") + } + + fn raw(&self) -> raw::git_proxy_options { + raw::git_proxy_options { + version: raw::GIT_PROXY_OPTIONS_VERSION, + kind: self.proxy_kind, + url: self.url.as_ref().map(|s| s.as_ptr()).unwrap_or(ptr::null()), + credentials: None, + certificate_check: None, + payload: ptr::null_mut(), + } + } +} diff --git a/extra/git2/src/push_update.rs b/extra/git2/src/push_update.rs new file mode 100644 index 000000000..3f74a2506 --- /dev/null +++ b/extra/git2/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. + pub fn src_refname(&self) -> Option<&str> { + str::from_utf8(self.src_refname_bytes()).ok() + } + + /// Returns the destination name of the reference as a byte slice. + pub fn dst_refname_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, (*self.raw).dst_refname).unwrap() } + } + + /// Returns the destination name of the reference. + 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/extra/git2/src/rebase.rs b/extra/git2/src/rebase.rs new file mode 100644 index 000000000..2bf8fe3e8 --- /dev/null +++ b/extra/git2/src/rebase.rs @@ -0,0 +1,441 @@ +use std::ffi::CString; +use std::{marker, mem, ptr, str}; + +use crate::build::CheckoutBuilder; +use crate::util::Binding; +use crate::{raw, Error, Index, MergeOptions, Oid, Signature}; + +/// Rebase options +/// +/// Use to tell the rebase machinery how to operate. +pub struct RebaseOptions<'cb> { + raw: raw::git_rebase_options, + rewrite_notes_ref: Option<CString>, + merge_options: Option<MergeOptions>, + checkout_options: Option<CheckoutBuilder<'cb>>, +} + +impl<'cb> Default for RebaseOptions<'cb> { + fn default() -> Self { + Self::new() + } +} + +impl<'cb> RebaseOptions<'cb> { + /// Creates a new default set of rebase options. + pub fn new() -> RebaseOptions<'cb> { + let mut opts = RebaseOptions { + raw: unsafe { mem::zeroed() }, + rewrite_notes_ref: None, + merge_options: None, + checkout_options: None, + }; + assert_eq!(unsafe { raw::git_rebase_init_options(&mut opts.raw, 1) }, 0); + opts + } + + /// Used by `Repository::rebase`, this will instruct other clients working on this + /// rebase that you want a quiet rebase experience, which they may choose to + /// provide in an application-specific manner. This has no effect upon + /// libgit2 directly, but is provided for interoperability between Git + /// tools. + pub fn quiet(&mut self, quiet: bool) -> &mut RebaseOptions<'cb> { + self.raw.quiet = quiet as i32; + self + } + + /// Used by `Repository::rebase`, this will begin an in-memory rebase, + /// which will allow callers to step through the rebase operations and + /// commit the rebased changes, but will not rewind HEAD or update the + /// repository to be in a rebasing state. This will not interfere with + /// the working directory (if there is one). + pub fn inmemory(&mut self, inmemory: bool) -> &mut RebaseOptions<'cb> { + self.raw.inmemory = inmemory as i32; + self + } + + /// Used by `finish()`, this is the name of the notes reference + /// used to rewrite notes for rebased commits when finishing the rebase; + /// if NULL, the contents of the configuration option `notes.rewriteRef` + /// is examined, unless the configuration option `notes.rewrite.rebase` + /// is set to false. If `notes.rewriteRef` is also NULL, notes will + /// not be rewritten. + pub fn rewrite_notes_ref(&mut self, rewrite_notes_ref: &str) -> &mut RebaseOptions<'cb> { + self.rewrite_notes_ref = Some(CString::new(rewrite_notes_ref).unwrap()); + self + } + + /// Options to control how trees are merged during `next()`. + pub fn merge_options(&mut self, opts: MergeOptions) -> &mut RebaseOptions<'cb> { + self.merge_options = Some(opts); + self + } + + /// Options to control how files are written during `Repository::rebase`, + /// `next()` and `abort()`. Note that a minimum strategy of + /// `GIT_CHECKOUT_SAFE` is defaulted in `init` and `next`, and a minimum + /// strategy of `GIT_CHECKOUT_FORCE` is defaulted in `abort` to match git + /// semantics. + pub fn checkout_options(&mut self, opts: CheckoutBuilder<'cb>) -> &mut RebaseOptions<'cb> { + self.checkout_options = Some(opts); + self + } + + /// Acquire a pointer to the underlying raw options. + pub fn raw(&mut self) -> *const raw::git_rebase_options { + unsafe { + if let Some(opts) = self.merge_options.as_mut().take() { + ptr::copy_nonoverlapping(opts.raw(), &mut self.raw.merge_options, 1); + } + if let Some(opts) = self.checkout_options.as_mut() { + opts.configure(&mut self.raw.checkout_options); + } + self.raw.rewrite_notes_ref = self + .rewrite_notes_ref + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(ptr::null()); + } + &self.raw + } +} + +/// Representation of a rebase +pub struct Rebase<'repo> { + raw: *mut raw::git_rebase, + _marker: marker::PhantomData<&'repo raw::git_rebase>, +} + +impl<'repo> Rebase<'repo> { + /// Gets the count of rebase operations that are to be applied. + pub fn len(&self) -> usize { + unsafe { raw::git_rebase_operation_entrycount(self.raw) } + } + + /// Gets the original `HEAD` ref name for merge rebases. + pub fn orig_head_name(&self) -> Option<&str> { + let name_bytes = + unsafe { crate::opt_bytes(self, raw::git_rebase_orig_head_name(self.raw)) }; + name_bytes.and_then(|s| str::from_utf8(s).ok()) + } + + /// Gets the original HEAD id for merge rebases. + pub fn orig_head_id(&self) -> Option<Oid> { + unsafe { Oid::from_raw_opt(raw::git_rebase_orig_head_id(self.raw)) } + } + + /// Gets the rebase operation specified by the given index. + pub fn nth(&mut self, n: usize) -> Option<RebaseOperation<'_>> { + unsafe { + let op = raw::git_rebase_operation_byindex(self.raw, n); + if op.is_null() { + None + } else { + Some(RebaseOperation::from_raw(op)) + } + } + } + + /// Gets the index of the rebase operation that is currently being applied. + /// If the first operation has not yet been applied (because you have called + /// `init` but not yet `next`) then this returns None. + pub fn operation_current(&mut self) -> Option<usize> { + let cur = unsafe { raw::git_rebase_operation_current(self.raw) }; + if cur == raw::GIT_REBASE_NO_OPERATION { + None + } else { + Some(cur) + } + } + + /// Gets the index produced by the last operation, which is the result of + /// `next()` and which will be committed by the next invocation of + /// `commit()`. This is useful for resolving conflicts in an in-memory + /// rebase before committing them. + /// + /// This is only applicable for in-memory rebases; for rebases within a + /// working directory, the changes were applied to the repository's index. + pub fn inmemory_index(&mut self) -> Result<Index, Error> { + let mut idx = ptr::null_mut(); + unsafe { + try_call!(raw::git_rebase_inmemory_index(&mut idx, self.raw)); + Ok(Binding::from_raw(idx)) + } + } + + /// Commits the current patch. You must have resolved any conflicts that + /// were introduced during the patch application from the `git_rebase_next` + /// invocation. To keep the author and message from the original commit leave + /// them as None + pub fn commit( + &mut self, + author: Option<&Signature<'_>>, + committer: &Signature<'_>, + message: Option<&str>, + ) -> Result<Oid, Error> { + let mut id: raw::git_oid = unsafe { mem::zeroed() }; + let message = crate::opt_cstr(message)?; + unsafe { + try_call!(raw::git_rebase_commit( + &mut id, + self.raw, + author.map(|a| a.raw()), + committer.raw(), + ptr::null(), + message + )); + Ok(Binding::from_raw(&id as *const _)) + } + } + + /// Aborts a rebase that is currently in progress, resetting the repository + /// and working directory to their state before rebase began. + pub fn abort(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_rebase_abort(self.raw)); + } + + Ok(()) + } + + /// Finishes a rebase that is currently in progress once all patches have + /// been applied. + pub fn finish(&mut self, signature: Option<&Signature<'_>>) -> Result<(), Error> { + unsafe { + try_call!(raw::git_rebase_finish(self.raw, signature.map(|s| s.raw()))); + } + + Ok(()) + } +} + +impl<'rebase> Iterator for Rebase<'rebase> { + type Item = Result<RebaseOperation<'rebase>, Error>; + + /// Performs the next rebase operation and returns the information about it. + /// If the operation is one that applies a patch (which is any operation except + /// GitRebaseOperation::Exec) then the patch will be applied and the index and + /// working directory will be updated with the changes. If there are conflicts, + /// you will need to address those before committing the changes. + fn next(&mut self) -> Option<Result<RebaseOperation<'rebase>, Error>> { + let mut out = ptr::null_mut(); + unsafe { + try_call_iter!(raw::git_rebase_next(&mut out, self.raw)); + Some(Ok(RebaseOperation::from_raw(out))) + } + } +} + +impl<'repo> Binding for Rebase<'repo> { + type Raw = *mut raw::git_rebase; + unsafe fn from_raw(raw: *mut raw::git_rebase) -> Rebase<'repo> { + Rebase { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_rebase { + self.raw + } +} + +impl<'repo> Drop for Rebase<'repo> { + fn drop(&mut self) { + unsafe { raw::git_rebase_free(self.raw) } + } +} + +/// A rebase operation +/// +/// Describes a single instruction/operation to be performed during the +/// rebase. +#[derive(Debug, PartialEq)] +pub enum RebaseOperationType { + /// The given commit is to be cherry-picked. The client should commit the + /// changes and continue if there are no conflicts. + Pick, + + /// The given commit is to be cherry-picked, but the client should prompt + /// the user to provide an updated commit message. + Reword, + + /// The given commit is to be cherry-picked, but the client should stop to + /// allow the user to edit the changes before committing them. + Edit, + + /// The given commit is to be squashed into the previous commit. The commit + /// message will be merged with the previous message. + Squash, + + /// The given commit is to be squashed into the previous commit. The commit + /// message from this commit will be discarded. + Fixup, + + /// No commit will be cherry-picked. The client should run the given command + /// and (if successful) continue. + Exec, +} + +impl RebaseOperationType { + /// Convert from the int into an enum. Returns None if invalid. + pub fn from_raw(raw: raw::git_rebase_operation_t) -> Option<RebaseOperationType> { + match raw { + raw::GIT_REBASE_OPERATION_PICK => Some(RebaseOperationType::Pick), + raw::GIT_REBASE_OPERATION_REWORD => Some(RebaseOperationType::Reword), + raw::GIT_REBASE_OPERATION_EDIT => Some(RebaseOperationType::Edit), + raw::GIT_REBASE_OPERATION_SQUASH => Some(RebaseOperationType::Squash), + raw::GIT_REBASE_OPERATION_FIXUP => Some(RebaseOperationType::Fixup), + raw::GIT_REBASE_OPERATION_EXEC => Some(RebaseOperationType::Exec), + _ => None, + } + } +} + +/// A rebase operation +/// +/// Describes a single instruction/operation to be performed during the +/// rebase. +#[derive(Debug)] +pub struct RebaseOperation<'rebase> { + raw: *const raw::git_rebase_operation, + _marker: marker::PhantomData<Rebase<'rebase>>, +} + +impl<'rebase> RebaseOperation<'rebase> { + /// The type of rebase operation + pub fn kind(&self) -> Option<RebaseOperationType> { + unsafe { RebaseOperationType::from_raw((*self.raw).kind) } + } + + /// The commit ID being cherry-picked. This will be populated for all + /// operations except those of type `GIT_REBASE_OPERATION_EXEC`. + pub fn id(&self) -> Oid { + unsafe { Binding::from_raw(&(*self.raw).id as *const _) } + } + + ///The executable the user has requested be run. This will only + /// be populated for operations of type RebaseOperationType::Exec + pub fn exec(&self) -> Option<&str> { + unsafe { str::from_utf8(crate::opt_bytes(self, (*self.raw).exec).unwrap()).ok() } + } +} + +impl<'rebase> Binding for RebaseOperation<'rebase> { + type Raw = *const raw::git_rebase_operation; + unsafe fn from_raw(raw: *const raw::git_rebase_operation) -> RebaseOperation<'rebase> { + RebaseOperation { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *const raw::git_rebase_operation { + self.raw + } +} + +#[cfg(test)] +mod tests { + use crate::{RebaseOperationType, RebaseOptions, Signature}; + use std::{fs, path}; + + #[test] + fn smoke() { + let (_td, repo) = crate::test::repo_init(); + let head_target = repo.head().unwrap().target().unwrap(); + let tip = repo.find_commit(head_target).unwrap(); + let sig = tip.author(); + let tree = tip.tree().unwrap(); + + // We just want to see the iteration work so we can create commits with + // no changes + let c1 = repo + .commit(Some("refs/heads/main"), &sig, &sig, "foo", &tree, &[&tip]) + .unwrap(); + let c1 = repo.find_commit(c1).unwrap(); + let c2 = repo + .commit(Some("refs/heads/main"), &sig, &sig, "foo", &tree, &[&c1]) + .unwrap(); + + let head = repo.find_reference("refs/heads/main").unwrap(); + let branch = repo.reference_to_annotated_commit(&head).unwrap(); + let upstream = repo.find_annotated_commit(tip.id()).unwrap(); + let mut rebase = repo + .rebase(Some(&branch), Some(&upstream), None, None) + .unwrap(); + + assert_eq!(Some("refs/heads/main"), rebase.orig_head_name()); + assert_eq!(Some(c2), rebase.orig_head_id()); + + assert_eq!(rebase.len(), 2); + { + let op = rebase.next().unwrap().unwrap(); + assert_eq!(op.kind(), Some(RebaseOperationType::Pick)); + assert_eq!(op.id(), c1.id()); + } + { + let op = rebase.next().unwrap().unwrap(); + assert_eq!(op.kind(), Some(RebaseOperationType::Pick)); + assert_eq!(op.id(), c2); + } + { + let op = rebase.next(); + assert!(op.is_none()); + } + } + + #[test] + fn keeping_original_author_msg() { + let (td, repo) = crate::test::repo_init(); + let head_target = repo.head().unwrap().target().unwrap(); + let tip = repo.find_commit(head_target).unwrap(); + let sig = Signature::now("testname", "testemail").unwrap(); + let mut index = repo.index().unwrap(); + + fs::File::create(td.path().join("file_a")).unwrap(); + index.add_path(path::Path::new("file_a")).unwrap(); + index.write().unwrap(); + let tree_id_a = index.write_tree().unwrap(); + let tree_a = repo.find_tree(tree_id_a).unwrap(); + let c1 = repo + .commit(Some("refs/heads/main"), &sig, &sig, "A", &tree_a, &[&tip]) + .unwrap(); + let c1 = repo.find_commit(c1).unwrap(); + + fs::File::create(td.path().join("file_b")).unwrap(); + index.add_path(path::Path::new("file_b")).unwrap(); + index.write().unwrap(); + let tree_id_b = index.write_tree().unwrap(); + let tree_b = repo.find_tree(tree_id_b).unwrap(); + let c2 = repo + .commit(Some("refs/heads/main"), &sig, &sig, "B", &tree_b, &[&c1]) + .unwrap(); + + let branch = repo.find_annotated_commit(c2).unwrap(); + let upstream = repo.find_annotated_commit(tip.id()).unwrap(); + let mut opts: RebaseOptions<'_> = Default::default(); + let mut rebase = repo + .rebase(Some(&branch), Some(&upstream), None, Some(&mut opts)) + .unwrap(); + + assert_eq!(rebase.len(), 2); + + { + rebase.next().unwrap().unwrap(); + let id = rebase.commit(None, &sig, None).unwrap(); + let commit = repo.find_commit(id).unwrap(); + assert_eq!(commit.message(), Some("A")); + assert_eq!(commit.author().name(), Some("testname")); + assert_eq!(commit.author().email(), Some("testemail")); + } + + { + rebase.next().unwrap().unwrap(); + let id = rebase.commit(None, &sig, None).unwrap(); + let commit = repo.find_commit(id).unwrap(); + assert_eq!(commit.message(), Some("B")); + assert_eq!(commit.author().name(), Some("testname")); + assert_eq!(commit.author().email(), Some("testemail")); + } + rebase.finish(None).unwrap(); + } +} diff --git a/extra/git2/src/reference.rs b/extra/git2/src/reference.rs new file mode 100644 index 000000000..92eb18c63 --- /dev/null +++ b/extra/git2/src/reference.rs @@ -0,0 +1,586 @@ +use std::cmp::Ordering; +use std::ffi::CString; +use std::marker; +use std::mem; +use std::ptr; +use std::str; + +use crate::object::CastOrPanic; +use crate::util::{c_cmp_to_ordering, Binding}; +use crate::{ + call, raw, Blob, Commit, Error, Object, ObjectType, Oid, ReferenceFormat, ReferenceType, + Repository, Tag, Tree, +}; + +// Not in the public header files (yet?), but a hard limit used by libgit2 +// internally +const GIT_REFNAME_MAX: usize = 1024; + +struct Refdb<'repo>(&'repo Repository); + +/// A structure to represent a git [reference][1]. +/// +/// [1]: http://git-scm.com/book/en/Git-Internals-Git-References +pub struct Reference<'repo> { + raw: *mut raw::git_reference, + _marker: marker::PhantomData<Refdb<'repo>>, +} + +/// An iterator over the references in a repository. +pub struct References<'repo> { + raw: *mut raw::git_reference_iterator, + _marker: marker::PhantomData<Refdb<'repo>>, +} + +/// An iterator over the names of references in a repository. +pub struct ReferenceNames<'repo, 'references> { + inner: &'references mut References<'repo>, +} + +impl<'repo> Reference<'repo> { + /// Ensure the reference name is well-formed. + /// + /// Validation is performed as if [`ReferenceFormat::ALLOW_ONELEVEL`] + /// was given to [`Reference::normalize_name`]. No normalization is + /// performed, however. + /// + /// ```rust + /// use git2::Reference; + /// + /// assert!(Reference::is_valid_name("HEAD")); + /// assert!(Reference::is_valid_name("refs/heads/main")); + /// + /// // But: + /// assert!(!Reference::is_valid_name("main")); + /// assert!(!Reference::is_valid_name("refs/heads/*")); + /// assert!(!Reference::is_valid_name("foo//bar")); + /// ``` + /// + /// [`ReferenceFormat::ALLOW_ONELEVEL`]: + /// struct.ReferenceFormat#associatedconstant.ALLOW_ONELEVEL + /// [`Reference::normalize_name`]: struct.Reference#method.normalize_name + pub fn is_valid_name(refname: &str) -> bool { + crate::init(); + let refname = CString::new(refname).unwrap(); + let mut valid: libc::c_int = 0; + unsafe { + call::c_try(raw::git_reference_name_is_valid( + &mut valid, + refname.as_ptr(), + )) + .unwrap(); + } + valid == 1 + } + + /// Normalize reference name and check validity. + /// + /// This will normalize the reference name by collapsing runs of adjacent + /// slashes between name components into a single slash. It also validates + /// the name according to the following rules: + /// + /// 1. If [`ReferenceFormat::ALLOW_ONELEVEL`] is given, the name may + /// contain only capital letters and underscores, and must begin and end + /// with a letter. (e.g. "HEAD", "ORIG_HEAD"). + /// 2. The flag [`ReferenceFormat::REFSPEC_SHORTHAND`] has an effect + /// only when combined with [`ReferenceFormat::ALLOW_ONELEVEL`]. If + /// it is given, "shorthand" branch names (i.e. those not prefixed by + /// `refs/`, but consisting of a single word without `/` separators) + /// become valid. For example, "main" would be accepted. + /// 3. If [`ReferenceFormat::REFSPEC_PATTERN`] is given, the name may + /// contain a single `*` in place of a full pathname component (e.g. + /// `foo/*/bar`, `foo/bar*`). + /// 4. Names prefixed with "refs/" can be almost anything. You must avoid + /// the characters '~', '^', ':', '\\', '?', '[', and '*', and the + /// sequences ".." and "@{" which have special meaning to revparse. + /// + /// If the reference passes validation, it is returned in normalized form, + /// otherwise an [`Error`] with [`ErrorCode::InvalidSpec`] is returned. + /// + /// ```rust + /// use git2::{Reference, ReferenceFormat}; + /// + /// assert_eq!( + /// Reference::normalize_name( + /// "foo//bar", + /// ReferenceFormat::NORMAL + /// ) + /// .unwrap(), + /// "foo/bar".to_owned() + /// ); + /// + /// assert_eq!( + /// Reference::normalize_name( + /// "HEAD", + /// ReferenceFormat::ALLOW_ONELEVEL + /// ) + /// .unwrap(), + /// "HEAD".to_owned() + /// ); + /// + /// assert_eq!( + /// Reference::normalize_name( + /// "refs/heads/*", + /// ReferenceFormat::REFSPEC_PATTERN + /// ) + /// .unwrap(), + /// "refs/heads/*".to_owned() + /// ); + /// + /// assert_eq!( + /// Reference::normalize_name( + /// "main", + /// ReferenceFormat::ALLOW_ONELEVEL | ReferenceFormat::REFSPEC_SHORTHAND + /// ) + /// .unwrap(), + /// "main".to_owned() + /// ); + /// ``` + /// + /// [`ReferenceFormat::ALLOW_ONELEVEL`]: + /// struct.ReferenceFormat#associatedconstant.ALLOW_ONELEVEL + /// [`ReferenceFormat::REFSPEC_SHORTHAND`]: + /// struct.ReferenceFormat#associatedconstant.REFSPEC_SHORTHAND + /// [`ReferenceFormat::REFSPEC_PATTERN`]: + /// struct.ReferenceFormat#associatedconstant.REFSPEC_PATTERN + /// [`Error`]: struct.Error + /// [`ErrorCode::InvalidSpec`]: enum.ErrorCode#variant.InvalidSpec + pub fn normalize_name(refname: &str, flags: ReferenceFormat) -> Result<String, Error> { + crate::init(); + let mut dst = [0u8; GIT_REFNAME_MAX]; + let refname = CString::new(refname)?; + unsafe { + try_call!(raw::git_reference_normalize_name( + dst.as_mut_ptr() as *mut libc::c_char, + dst.len() as libc::size_t, + refname, + flags.bits() + )); + let s = &dst[..dst.iter().position(|&a| a == 0).unwrap()]; + Ok(str::from_utf8(s).unwrap().to_owned()) + } + } + + /// Get access to the underlying raw pointer. + pub fn raw(&self) -> *mut raw::git_reference { + self.raw + } + + /// Delete an existing reference. + /// + /// This method works for both direct and symbolic references. The reference + /// will be immediately removed on disk. + /// + /// This function will return an error if the reference has changed from the + /// time it was looked up. + pub fn delete(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_reference_delete(self.raw)); + } + Ok(()) + } + + /// Check if a reference is a local branch. + pub fn is_branch(&self) -> bool { + unsafe { raw::git_reference_is_branch(&*self.raw) == 1 } + } + + /// Check if a reference is a note. + pub fn is_note(&self) -> bool { + unsafe { raw::git_reference_is_note(&*self.raw) == 1 } + } + + /// Check if a reference is a remote tracking branch + pub fn is_remote(&self) -> bool { + unsafe { raw::git_reference_is_remote(&*self.raw) == 1 } + } + + /// Check if a reference is a tag + pub fn is_tag(&self) -> bool { + unsafe { raw::git_reference_is_tag(&*self.raw) == 1 } + } + + /// Get the reference type of a reference. + /// + /// If the type is unknown, then `None` is returned. + pub fn kind(&self) -> Option<ReferenceType> { + ReferenceType::from_raw(unsafe { raw::git_reference_type(&*self.raw) }) + } + + /// Get the full name of a reference. + /// + /// Returns `None` if the name is not valid utf-8. + pub fn name(&self) -> Option<&str> { + str::from_utf8(self.name_bytes()).ok() + } + + /// Get the full name of a reference. + pub fn name_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, raw::git_reference_name(&*self.raw)).unwrap() } + } + + /// Get the full shorthand of a reference. + /// + /// This will transform the reference name into a name "human-readable" + /// version. If no shortname is appropriate, it will return the full name. + /// + /// Returns `None` if the shorthand is not valid utf-8. + pub fn shorthand(&self) -> Option<&str> { + str::from_utf8(self.shorthand_bytes()).ok() + } + + /// Get the full shorthand of a reference. + pub fn shorthand_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, raw::git_reference_shorthand(&*self.raw)).unwrap() } + } + + /// Get the OID pointed to by a direct reference. + /// + /// Only available if the reference is direct (i.e. an object id reference, + /// not a symbolic one). + pub fn target(&self) -> Option<Oid> { + unsafe { Binding::from_raw_opt(raw::git_reference_target(&*self.raw)) } + } + + /// Return the peeled OID target of this reference. + /// + /// This peeled OID only applies to direct references that point to a hard + /// Tag object: it is the result of peeling such Tag. + pub fn target_peel(&self) -> Option<Oid> { + unsafe { Binding::from_raw_opt(raw::git_reference_target_peel(&*self.raw)) } + } + + /// Get full name to the reference pointed to by a symbolic reference. + /// + /// May return `None` if the reference is either not symbolic or not a + /// valid utf-8 string. + pub fn symbolic_target(&self) -> Option<&str> { + self.symbolic_target_bytes() + .and_then(|s| str::from_utf8(s).ok()) + } + + /// Get full name to the reference pointed to by a symbolic reference. + /// + /// Only available if the reference is symbolic. + pub fn symbolic_target_bytes(&self) -> Option<&[u8]> { + unsafe { crate::opt_bytes(self, raw::git_reference_symbolic_target(&*self.raw)) } + } + + /// Resolve a symbolic reference to a direct reference. + /// + /// This method iteratively peels a symbolic reference until it resolves to + /// a direct reference to an OID. + /// + /// If a direct reference is passed as an argument, a copy of that + /// reference is returned. + pub fn resolve(&self) -> Result<Reference<'repo>, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_reference_resolve(&mut raw, &*self.raw)); + Ok(Binding::from_raw(raw)) + } + } + + /// Peel a reference to an object + /// + /// This method recursively peels the reference until it reaches + /// an object of the specified type. + pub fn peel(&self, kind: ObjectType) -> Result<Object<'repo>, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_reference_peel(&mut raw, self.raw, kind)); + Ok(Binding::from_raw(raw)) + } + } + + /// Peel a reference to a blob + /// + /// This method recursively peels the reference until it reaches + /// a blob. + pub fn peel_to_blob(&self) -> Result<Blob<'repo>, Error> { + Ok(self.peel(ObjectType::Blob)?.cast_or_panic(ObjectType::Blob)) + } + + /// Peel a reference to a commit + /// + /// This method recursively peels the reference until it reaches + /// a commit. + pub fn peel_to_commit(&self) -> Result<Commit<'repo>, Error> { + Ok(self + .peel(ObjectType::Commit)? + .cast_or_panic(ObjectType::Commit)) + } + + /// Peel a reference to a tree + /// + /// This method recursively peels the reference until it reaches + /// a tree. + pub fn peel_to_tree(&self) -> Result<Tree<'repo>, Error> { + Ok(self.peel(ObjectType::Tree)?.cast_or_panic(ObjectType::Tree)) + } + + /// Peel a reference to a tag + /// + /// This method recursively peels the reference until it reaches + /// a tag. + pub fn peel_to_tag(&self) -> Result<Tag<'repo>, Error> { + Ok(self.peel(ObjectType::Tag)?.cast_or_panic(ObjectType::Tag)) + } + + /// Rename an existing reference. + /// + /// This method works for both direct and symbolic references. + /// + /// If the force flag is not enabled, and there's already a reference with + /// the given name, the renaming will fail. + pub fn rename( + &mut self, + new_name: &str, + force: bool, + msg: &str, + ) -> Result<Reference<'repo>, Error> { + let mut raw = ptr::null_mut(); + let new_name = CString::new(new_name)?; + let msg = CString::new(msg)?; + unsafe { + try_call!(raw::git_reference_rename( + &mut raw, self.raw, new_name, force, msg + )); + Ok(Binding::from_raw(raw)) + } + } + + /// Conditionally create a new reference with the same name as the given + /// reference but a different OID target. The reference must be a direct + /// reference, otherwise this will fail. + /// + /// The new reference will be written to disk, overwriting the given + /// reference. + pub fn set_target(&mut self, id: Oid, reflog_msg: &str) -> Result<Reference<'repo>, Error> { + let mut raw = ptr::null_mut(); + let msg = CString::new(reflog_msg)?; + unsafe { + try_call!(raw::git_reference_set_target( + &mut raw, + self.raw, + id.raw(), + msg + )); + 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<Reference<'repo>, 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> { + fn partial_cmp(&self, other: &Reference<'repo>) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl<'repo> Ord for Reference<'repo> { + fn cmp(&self, other: &Reference<'repo>) -> Ordering { + c_cmp_to_ordering(unsafe { raw::git_reference_cmp(&*self.raw, &*other.raw) }) + } +} + +impl<'repo> PartialEq for Reference<'repo> { + fn eq(&self, other: &Reference<'repo>) -> bool { + self.cmp(other) == Ordering::Equal + } +} + +impl<'repo> Eq for Reference<'repo> {} + +impl<'repo> Binding for Reference<'repo> { + type Raw = *mut raw::git_reference; + unsafe fn from_raw(raw: *mut raw::git_reference) -> Reference<'repo> { + Reference { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_reference { + self.raw + } +} + +impl<'repo> Drop for Reference<'repo> { + fn drop(&mut self) { + unsafe { raw::git_reference_free(self.raw) } + } +} + +impl<'repo> References<'repo> { + /// Consumes a `References` iterator to create an iterator over just the + /// name of some references. + /// + /// This is more efficient if only the names are desired of references as + /// the references themselves don't have to be allocated and deallocated. + /// + /// The returned iterator will yield strings as opposed to a `Reference`. + pub fn names<'a>(&'a mut self) -> ReferenceNames<'repo, 'a> { + ReferenceNames { inner: self } + } +} + +impl<'repo> Binding for References<'repo> { + type Raw = *mut raw::git_reference_iterator; + unsafe fn from_raw(raw: *mut raw::git_reference_iterator) -> References<'repo> { + References { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_reference_iterator { + self.raw + } +} + +impl<'repo> Iterator for References<'repo> { + type Item = Result<Reference<'repo>, Error>; + fn next(&mut self) -> Option<Result<Reference<'repo>, Error>> { + let mut out = ptr::null_mut(); + unsafe { + try_call_iter!(raw::git_reference_next(&mut out, self.raw)); + Some(Ok(Binding::from_raw(out))) + } + } +} + +impl<'repo> Drop for References<'repo> { + fn drop(&mut self) { + unsafe { raw::git_reference_iterator_free(self.raw) } + } +} + +impl<'repo, 'references> Iterator for ReferenceNames<'repo, 'references> { + type Item = Result<&'references str, Error>; + fn next(&mut self) -> Option<Result<&'references str, Error>> { + let mut out = ptr::null(); + unsafe { + try_call_iter!(raw::git_reference_next_name(&mut out, self.inner.raw)); + let bytes = crate::opt_bytes(self, out).unwrap(); + let s = str::from_utf8(bytes).unwrap(); + Some(Ok(mem::transmute::<&str, &'references str>(s))) + } + } +} + +#[cfg(test)] +mod tests { + use crate::{ObjectType, Reference, ReferenceType}; + + #[test] + 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 smoke() { + let (_td, repo) = crate::test::repo_init(); + let mut head = repo.head().unwrap(); + assert!(head.is_branch()); + assert!(!head.is_remote()); + assert!(!head.is_tag()); + assert!(!head.is_note()); + + // HEAD is a symbolic reference but git_repository_head resolves it + // so it is a GIT_REFERENCE_DIRECT. + assert_eq!(head.kind().unwrap(), ReferenceType::Direct); + + assert!(head == repo.head().unwrap()); + assert_eq!(head.name(), Some("refs/heads/main")); + + assert!(head == repo.find_reference("refs/heads/main").unwrap()); + assert_eq!( + repo.refname_to_id("refs/heads/main").unwrap(), + head.target().unwrap() + ); + + assert!(head.symbolic_target().is_none()); + assert!(head.target_peel().is_none()); + + assert_eq!(head.shorthand(), Some("main")); + assert!(head.resolve().unwrap() == head); + + let mut tag1 = repo + .reference("refs/tags/tag1", head.target().unwrap(), false, "test") + .unwrap(); + assert!(tag1.is_tag()); + assert_eq!(tag1.kind().unwrap(), ReferenceType::Direct); + + let peeled_commit = tag1.peel(ObjectType::Commit).unwrap(); + assert_eq!(ObjectType::Commit, peeled_commit.kind().unwrap()); + assert_eq!(tag1.target().unwrap(), peeled_commit.id()); + + tag1.delete().unwrap(); + + let mut sym1 = repo + .reference_symbolic("refs/tags/tag1", "refs/heads/main", false, "test") + .unwrap(); + assert_eq!(sym1.kind().unwrap(), ReferenceType::Symbolic); + let mut sym2 = repo + .reference_symbolic("refs/tags/tag2", "refs/heads/main", false, "test") + .unwrap() + .symbolic_set_target("refs/tags/tag1", "test") + .unwrap(); + assert_eq!(sym2.kind().unwrap(), ReferenceType::Symbolic); + assert_eq!(sym2.symbolic_target().unwrap(), "refs/tags/tag1"); + sym2.delete().unwrap(); + sym1.delete().unwrap(); + + { + assert!(repo.references().unwrap().count() == 1); + assert!(repo.references().unwrap().next().unwrap().unwrap() == head); + let mut names = repo.references().unwrap(); + let mut names = names.names(); + assert_eq!(names.next().unwrap().unwrap(), "refs/heads/main"); + assert!(names.next().is_none()); + assert!(repo.references_glob("foo").unwrap().count() == 0); + assert!(repo.references_glob("refs/heads/*").unwrap().count() == 1); + } + + let mut head = head.rename("refs/foo", true, "test").unwrap(); + head.delete().unwrap(); + } +} diff --git a/extra/git2/src/reflog.rs b/extra/git2/src/reflog.rs new file mode 100644 index 000000000..bbd2140ab --- /dev/null +++ b/extra/git2/src/reflog.rs @@ -0,0 +1,196 @@ +use libc::size_t; +use std::iter::FusedIterator; +use std::marker; +use std::ops::Range; +use std::str; + +use crate::util::Binding; +use crate::{raw, signature, Error, Oid, Signature}; + +/// A reference log of a git repository. +pub struct Reflog { + raw: *mut raw::git_reflog, +} + +/// An entry inside the reflog of a repository +pub struct ReflogEntry<'reflog> { + raw: *const raw::git_reflog_entry, + _marker: marker::PhantomData<&'reflog Reflog>, +} + +/// An iterator over the entries inside of a reflog. +pub struct ReflogIter<'reflog> { + range: Range<usize>, + reflog: &'reflog Reflog, +} + +impl Reflog { + /// Add a new entry to the in-memory reflog. + pub fn append( + &mut self, + new_oid: Oid, + committer: &Signature<'_>, + msg: Option<&str>, + ) -> Result<(), Error> { + let msg = crate::opt_cstr(msg)?; + unsafe { + try_call!(raw::git_reflog_append( + self.raw, + new_oid.raw(), + committer.raw(), + msg + )); + } + Ok(()) + } + + /// Remove an entry from the reflog by its index + /// + /// To ensure there's no gap in the log history, set rewrite_previous_entry + /// param value to `true`. When deleting entry n, member old_oid of entry + /// n-1 (if any) will be updated with the value of member new_oid of entry + /// n+1. + pub fn remove(&mut self, i: usize, rewrite_previous_entry: bool) -> Result<(), Error> { + unsafe { + try_call!(raw::git_reflog_drop( + self.raw, + i as size_t, + rewrite_previous_entry + )); + } + Ok(()) + } + + /// Lookup an entry by its index + /// + /// Requesting the reflog entry with an index of 0 (zero) will return the + /// most recently created entry. + pub fn get(&self, i: usize) -> Option<ReflogEntry<'_>> { + unsafe { + let ptr = raw::git_reflog_entry_byindex(self.raw, i as size_t); + Binding::from_raw_opt(ptr) + } + } + + /// Get the number of log entries in a reflog + pub fn len(&self) -> usize { + unsafe { raw::git_reflog_entrycount(self.raw) as usize } + } + + /// Return `true ` is there is no log entry in a reflog + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Get an iterator to all entries inside of this reflog + pub fn iter(&self) -> ReflogIter<'_> { + ReflogIter { + range: 0..self.len(), + reflog: self, + } + } + + /// Write an existing in-memory reflog object back to disk using an atomic + /// file lock. + pub fn write(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_reflog_write(self.raw)); + } + Ok(()) + } +} + +impl Binding for Reflog { + type Raw = *mut raw::git_reflog; + + unsafe fn from_raw(raw: *mut raw::git_reflog) -> Reflog { + Reflog { raw } + } + fn raw(&self) -> *mut raw::git_reflog { + self.raw + } +} + +impl Drop for Reflog { + fn drop(&mut self) { + unsafe { raw::git_reflog_free(self.raw) } + } +} + +impl<'reflog> ReflogEntry<'reflog> { + /// Get the committer of this entry + pub fn committer(&self) -> Signature<'_> { + unsafe { + let ptr = raw::git_reflog_entry_committer(self.raw); + signature::from_raw_const(self, ptr) + } + } + + /// Get the new oid + pub fn id_new(&self) -> Oid { + unsafe { Binding::from_raw(raw::git_reflog_entry_id_new(self.raw)) } + } + + /// Get the old oid + pub fn id_old(&self) -> Oid { + unsafe { Binding::from_raw(raw::git_reflog_entry_id_old(self.raw)) } + } + + /// Get the log message, returning `None` on invalid UTF-8. + pub fn message(&self) -> Option<&str> { + self.message_bytes().and_then(|s| str::from_utf8(s).ok()) + } + + /// Get the log message as a byte array. + pub fn message_bytes(&self) -> Option<&[u8]> { + unsafe { crate::opt_bytes(self, raw::git_reflog_entry_message(self.raw)) } + } +} + +impl<'reflog> Binding for ReflogEntry<'reflog> { + type Raw = *const raw::git_reflog_entry; + + unsafe fn from_raw(raw: *const raw::git_reflog_entry) -> ReflogEntry<'reflog> { + ReflogEntry { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *const raw::git_reflog_entry { + self.raw + } +} + +impl<'reflog> Iterator for ReflogIter<'reflog> { + type Item = ReflogEntry<'reflog>; + fn next(&mut self) -> Option<ReflogEntry<'reflog>> { + self.range.next().and_then(|i| self.reflog.get(i)) + } + fn size_hint(&self) -> (usize, Option<usize>) { + self.range.size_hint() + } +} +impl<'reflog> DoubleEndedIterator for ReflogIter<'reflog> { + fn next_back(&mut self) -> Option<ReflogEntry<'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)] +mod tests { + #[test] + fn smoke() { + let (_td, repo) = crate::test::repo_init(); + let mut reflog = repo.reflog("HEAD").unwrap(); + assert_eq!(reflog.iter().len(), 1); + reflog.write().unwrap(); + + let entry = reflog.iter().next().unwrap(); + assert!(entry.message().is_some()); + + repo.reflog_rename("HEAD", "refs/heads/foo").unwrap(); + repo.reflog_delete("refs/heads/foo").unwrap(); + } +} diff --git a/extra/git2/src/refspec.rs b/extra/git2/src/refspec.rs new file mode 100644 index 000000000..3f62e991c --- /dev/null +++ b/extra/git2/src/refspec.rs @@ -0,0 +1,122 @@ +use std::ffi::CString; +use std::marker; +use std::str; + +use crate::util::Binding; +use crate::{raw, Buf, Direction, Error}; + +/// A structure to represent a git [refspec][1]. +/// +/// Refspecs are currently mainly accessed/created through a `Remote`. +/// +/// [1]: http://git-scm.com/book/en/Git-Internals-The-Refspec +pub struct Refspec<'remote> { + raw: *const raw::git_refspec, + _marker: marker::PhantomData<&'remote raw::git_remote>, +} + +impl<'remote> Refspec<'remote> { + /// Get the refspec's direction. + pub fn direction(&self) -> Direction { + match unsafe { raw::git_refspec_direction(self.raw) } { + raw::GIT_DIRECTION_FETCH => Direction::Fetch, + raw::GIT_DIRECTION_PUSH => Direction::Push, + n => panic!("unknown refspec direction: {}", n), + } + } + + /// Get the destination specifier. + /// + /// If the destination is not utf-8, None is returned. + pub fn dst(&self) -> Option<&str> { + str::from_utf8(self.dst_bytes()).ok() + } + + /// Get the destination specifier, in bytes. + pub fn dst_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, raw::git_refspec_dst(self.raw)).unwrap() } + } + + /// Check if a refspec's destination descriptor matches a reference + pub fn dst_matches(&self, refname: &str) -> bool { + let refname = CString::new(refname).unwrap(); + unsafe { raw::git_refspec_dst_matches(self.raw, refname.as_ptr()) == 1 } + } + + /// Get the source specifier. + /// + /// If the source is not utf-8, None is returned. + pub fn src(&self) -> Option<&str> { + str::from_utf8(self.src_bytes()).ok() + } + + /// Get the source specifier, in bytes. + pub fn src_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, raw::git_refspec_src(self.raw)).unwrap() } + } + + /// Check if a refspec's source descriptor matches a reference + pub fn src_matches(&self, refname: &str) -> bool { + let refname = CString::new(refname).unwrap(); + unsafe { raw::git_refspec_src_matches(self.raw, refname.as_ptr()) == 1 } + } + + /// Get the force update setting. + pub fn is_force(&self) -> bool { + unsafe { raw::git_refspec_force(self.raw) == 1 } + } + + /// Get the refspec's string. + /// + /// Returns None if the string is not valid utf8. + pub fn str(&self) -> Option<&str> { + str::from_utf8(self.bytes()).ok() + } + + /// Get the refspec's string as a byte array + pub fn bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, raw::git_refspec_string(self.raw)).unwrap() } + } + + /// Transform a reference to its target following the refspec's rules + pub fn transform(&self, name: &str) -> Result<Buf, Error> { + let name = CString::new(name).unwrap(); + unsafe { + let buf = Buf::new(); + try_call!(raw::git_refspec_transform( + buf.raw(), + self.raw, + name.as_ptr() + )); + Ok(buf) + } + } + + /// Transform a target reference to its source reference following the refspec's rules + pub fn rtransform(&self, name: &str) -> Result<Buf, Error> { + let name = CString::new(name).unwrap(); + unsafe { + let buf = Buf::new(); + try_call!(raw::git_refspec_rtransform( + buf.raw(), + self.raw, + name.as_ptr() + )); + Ok(buf) + } + } +} + +impl<'remote> Binding for Refspec<'remote> { + type Raw = *const raw::git_refspec; + + unsafe fn from_raw(raw: *const raw::git_refspec) -> Refspec<'remote> { + Refspec { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *const raw::git_refspec { + self.raw + } +} diff --git a/extra/git2/src/remote.rs b/extra/git2/src/remote.rs new file mode 100644 index 000000000..c8f5a935a --- /dev/null +++ b/extra/git2/src/remote.rs @@ -0,0 +1,1123 @@ +use libc; +use raw::git_strarray; +use std::iter::FusedIterator; +use std::marker; +use std::mem; +use std::ops::Range; +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::{call, raw, Buf, Direction, Error, FetchPrune, Oid, ProxyOptions, Refspec}; +use crate::{AutotagOption, Progress, RemoteCallbacks, Repository}; + +/// A structure representing a [remote][1] of a git repository. +/// +/// [1]: http://git-scm.com/book/en/Git-Basics-Working-with-Remotes +/// +/// The lifetime is the lifetime of the repository that it is attached to. The +/// remote is used to manage fetches and pushes as well as refspecs. +pub struct Remote<'repo> { + raw: *mut raw::git_remote, + _marker: marker::PhantomData<&'repo Repository>, +} + +/// An iterator over the refspecs that a remote contains. +pub struct Refspecs<'remote> { + range: Range<usize>, + remote: &'remote Remote<'remote>, +} + +/// Description of a reference advertised by a remote server, given out on calls +/// to `list`. +pub struct RemoteHead<'remote> { + raw: *const raw::git_remote_head, + _marker: marker::PhantomData<&'remote str>, +} + +/// Options which can be specified to various fetch operations. +pub struct FetchOptions<'cb> { + callbacks: Option<RemoteCallbacks<'cb>>, + depth: i32, + proxy: Option<ProxyOptions<'cb>>, + prune: FetchPrune, + update_fetchhead: bool, + download_tags: AutotagOption, + follow_redirects: RemoteRedirect, + custom_headers: Vec<CString>, + custom_headers_ptrs: Vec<*const c_char>, +} + +/// Options to control the behavior of a git push. +pub struct PushOptions<'cb> { + callbacks: Option<RemoteCallbacks<'cb>>, + proxy: Option<ProxyOptions<'cb>>, + pb_parallelism: u32, + follow_redirects: RemoteRedirect, + custom_headers: Vec<CString>, + custom_headers_ptrs: Vec<*const c_char>, +} + +/// Holds callbacks for a connection to a `Remote`. Disconnects when dropped +pub struct RemoteConnection<'repo, 'connection, 'cb> { + _callbacks: Box<RemoteCallbacks<'cb>>, + _proxy: ProxyOptions<'cb>, + remote: &'connection mut Remote<'repo>, +} + +/// Remote redirection settings; whether redirects to another host are +/// permitted. +/// +/// By default, git will follow a redirect on the initial request +/// (`/info/refs`), but not subsequent requests. +pub enum RemoteRedirect { + /// Do not follow any off-site redirects at any stage of the fetch or push. + None, + /// Allow off-site redirects only upon the initial request. This is the + /// default. + Initial, + /// Allow redirects at any stage in the fetch or push. + All, +} + +pub fn remote_into_raw(remote: Remote<'_>) -> *mut raw::git_remote { + let ret = remote.raw; + mem::forget(remote); + ret +} + +impl<'repo> Remote<'repo> { + /// Ensure the remote name is well-formed. + pub fn is_valid_name(remote_name: &str) -> bool { + crate::init(); + let remote_name = CString::new(remote_name).unwrap(); + let mut valid: libc::c_int = 0; + unsafe { + call::c_try(raw::git_remote_name_is_valid( + &mut valid, + remote_name.as_ptr(), + )) + .unwrap(); + } + valid == 1 + } + + /// Create a detached remote + /// + /// Create a remote with the given URL in-memory. You can use this + /// when you have a URL instead of a remote's name. + /// Contrasted with an anonymous remote, a detached remote will not + /// consider any repo configuration values. + pub fn create_detached<S: Into<Vec<u8>>>(url: S) -> Result<Remote<'repo>, Error> { + crate::init(); + let mut ret = ptr::null_mut(); + let url = CString::new(url)?; + unsafe { + try_call!(raw::git_remote_create_detached(&mut ret, url)); + Ok(Binding::from_raw(ret)) + } + } + + /// Get the remote's name. + /// + /// Returns `None` if this remote has not yet been named 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 remote's name, in bytes. + /// + /// Returns `None` if this remote has not yet been named + pub fn name_bytes(&self) -> Option<&[u8]> { + unsafe { crate::opt_bytes(self, raw::git_remote_name(&*self.raw)) } + } + + /// Get the remote's URL. + /// + /// 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. + pub fn url_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, raw::git_remote_url(&*self.raw)).unwrap() } + } + + /// Get the remote's pushurl. + /// + /// Returns `None` if the pushurl is not valid utf-8 + 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. + pub fn pushurl_bytes(&self) -> Option<&[u8]> { + unsafe { crate::opt_bytes(self, raw::git_remote_pushurl(&*self.raw)) } + } + + /// Get the remote's default branch. + /// + /// The remote (or more exactly its transport) must have connected to the + /// remote repository. This default branch is available as soon as the + /// connection to the remote is initiated and it remains available after + /// disconnecting. + pub fn default_branch(&self) -> Result<Buf, Error> { + unsafe { + let buf = Buf::new(); + try_call!(raw::git_remote_default_branch(buf.raw(), self.raw)); + Ok(buf) + } + } + + /// Open a connection to a remote. + pub fn connect(&mut self, dir: Direction) -> Result<(), Error> { + // TODO: can callbacks be exposed safely? + unsafe { + try_call!(raw::git_remote_connect( + self.raw, + dir, + ptr::null(), + ptr::null(), + ptr::null() + )); + } + Ok(()) + } + + /// Open a connection to a remote with callbacks and proxy settings + /// + /// Returns a `RemoteConnection` that will disconnect once dropped + pub fn connect_auth<'connection, 'cb>( + &'connection mut self, + dir: Direction, + cb: Option<RemoteCallbacks<'cb>>, + proxy_options: Option<ProxyOptions<'cb>>, + ) -> Result<RemoteConnection<'repo, 'connection, 'cb>, Error> { + let cb = Box::new(cb.unwrap_or_else(RemoteCallbacks::new)); + let proxy_options = proxy_options.unwrap_or_else(ProxyOptions::new); + unsafe { + try_call!(raw::git_remote_connect( + self.raw, + dir, + &cb.raw(), + &proxy_options.raw(), + ptr::null() + )); + } + + Ok(RemoteConnection { + _callbacks: cb, + _proxy: proxy_options, + remote: self, + }) + } + + /// Check whether the remote is connected + pub fn connected(&mut self) -> bool { + unsafe { raw::git_remote_connected(self.raw) == 1 } + } + + /// Disconnect from the remote + pub fn disconnect(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_remote_disconnect(self.raw)); + } + Ok(()) + } + + /// Download and index the packfile + /// + /// Connect to the remote if it hasn't been done yet, negotiate with the + /// remote git which objects are missing, download and index the packfile. + /// + /// The .idx file will be created and both it and the packfile with be + /// renamed to their final name. + /// + /// The `specs` argument is a list of refspecs to use for this negotiation + /// and download. Use an empty array to use the base refspecs. + pub fn download<Str: AsRef<str> + crate::IntoCString + Clone>( + &mut self, + specs: &[Str], + opts: Option<&mut FetchOptions<'_>>, + ) -> Result<(), Error> { + let (_a, _b, arr) = crate::util::iter2cstrs(specs.iter())?; + let raw = opts.map(|o| o.raw()); + unsafe { + try_call!(raw::git_remote_download(self.raw, &arr, raw.as_ref())); + } + Ok(()) + } + + /// Cancel the operation + /// + /// At certain points in its operation, the network code checks whether the + /// operation has been canceled and if so stops the operation. + pub fn stop(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_remote_stop(self.raw)); + } + Ok(()) + } + + /// Get the number of refspecs for a remote + pub fn refspecs(&self) -> Refspecs<'_> { + let cnt = unsafe { raw::git_remote_refspec_count(&*self.raw) as usize }; + Refspecs { + range: 0..cnt, + remote: self, + } + } + + /// Get the `nth` refspec from this remote. + /// + /// The `refspecs` iterator can be used to iterate over all refspecs. + pub fn get_refspec(&self, i: usize) -> Option<Refspec<'repo>> { + unsafe { + let ptr = raw::git_remote_get_refspec(&*self.raw, i as libc::size_t); + Binding::from_raw_opt(ptr) + } + } + + /// Download new data and update tips + /// + /// Convenience function to connect to a remote, download the data, + /// disconnect and update the remote-tracking branches. + /// + /// # Examples + /// + /// Example of functionality similar to `git fetch origin/main`: + /// + /// ```no_run + /// fn fetch_origin_main(repo: git2::Repository) -> Result<(), git2::Error> { + /// repo.find_remote("origin")?.fetch(&["main"], None, None) + /// } + /// + /// let repo = git2::Repository::discover("rust").unwrap(); + /// fetch_origin_main(repo).unwrap(); + /// ``` + pub fn fetch<Str: AsRef<str> + crate::IntoCString + Clone>( + &mut self, + refspecs: &[Str], + opts: Option<&mut FetchOptions<'_>>, + reflog_msg: Option<&str>, + ) -> Result<(), Error> { + let (_a, _b, arr) = crate::util::iter2cstrs(refspecs.iter())?; + let msg = crate::opt_cstr(reflog_msg)?; + let raw = opts.map(|o| o.raw()); + unsafe { + try_call!(raw::git_remote_fetch(self.raw, &arr, raw.as_ref(), msg)); + } + Ok(()) + } + + /// Update the tips to the new state + pub fn update_tips( + &mut self, + callbacks: Option<&mut RemoteCallbacks<'_>>, + update_fetchhead: bool, + download_tags: AutotagOption, + msg: Option<&str>, + ) -> Result<(), Error> { + let msg = crate::opt_cstr(msg)?; + let cbs = callbacks.map(|cb| cb.raw()); + unsafe { + try_call!(raw::git_remote_update_tips( + self.raw, + cbs.as_ref(), + update_fetchhead, + download_tags, + msg + )); + } + Ok(()) + } + + /// Perform a push + /// + /// Perform all the steps for a push. If no refspecs are passed then the + /// configured refspecs will be used. + /// + /// Note that you'll likely want to use `RemoteCallbacks` and set + /// `push_update_reference` to test whether all the references were pushed + /// successfully. + pub fn push<Str: AsRef<str> + crate::IntoCString + Clone>( + &mut self, + refspecs: &[Str], + opts: Option<&mut PushOptions<'_>>, + ) -> Result<(), Error> { + let (_a, _b, arr) = crate::util::iter2cstrs(refspecs.iter())?; + let raw = opts.map(|o| o.raw()); + unsafe { + try_call!(raw::git_remote_push(self.raw, &arr, raw.as_ref())); + } + Ok(()) + } + + /// Get the statistics structure that is filled in by the fetch operation. + pub fn stats(&self) -> Progress<'_> { + unsafe { Binding::from_raw(raw::git_remote_stats(self.raw)) } + } + + /// Get the remote repository's reference advertisement list. + /// + /// Get the list of references with which the server responds to a new + /// connection. + /// + /// The remote (or more exactly its transport) must have connected to the + /// remote repository. This list is available as soon as the connection to + /// the remote is initiated and it remains available after disconnecting. + pub fn list(&self) -> Result<&[RemoteHead<'_>], Error> { + let mut size = 0; + let mut base = ptr::null_mut(); + unsafe { + try_call!(raw::git_remote_ls(&mut base, &mut size, self.raw)); + assert_eq!( + mem::size_of::<RemoteHead<'_>>(), + mem::size_of::<*const raw::git_remote_head>() + ); + let slice = slice::from_raw_parts(base as *const _, size as usize); + Ok(mem::transmute::< + &[*const raw::git_remote_head], + &[RemoteHead<'_>], + >(slice)) + } + } + + /// Prune tracking refs that are no longer present on remote + pub fn prune(&mut self, callbacks: Option<RemoteCallbacks<'_>>) -> Result<(), Error> { + let cbs = Box::new(callbacks.unwrap_or_else(RemoteCallbacks::new)); + unsafe { + try_call!(raw::git_remote_prune(self.raw, &cbs.raw())); + } + Ok(()) + } + + /// Get the remote's list of fetch refspecs + pub fn fetch_refspecs(&self) -> Result<StringArray, Error> { + unsafe { + let mut raw: raw::git_strarray = mem::zeroed(); + try_call!(raw::git_remote_get_fetch_refspecs(&mut raw, self.raw)); + Ok(StringArray::from_raw(raw)) + } + } + + /// Get the remote's list of push refspecs + pub fn push_refspecs(&self) -> Result<StringArray, Error> { + unsafe { + let mut raw: raw::git_strarray = mem::zeroed(); + try_call!(raw::git_remote_get_push_refspecs(&mut raw, self.raw)); + Ok(StringArray::from_raw(raw)) + } + } +} + +impl<'repo> Clone for Remote<'repo> { + fn clone(&self) -> Remote<'repo> { + let mut ret = ptr::null_mut(); + let rc = unsafe { call!(raw::git_remote_dup(&mut ret, self.raw)) }; + assert_eq!(rc, 0); + Remote { + raw: ret, + _marker: marker::PhantomData, + } + } +} + +impl<'repo> Binding for Remote<'repo> { + type Raw = *mut raw::git_remote; + + unsafe fn from_raw(raw: *mut raw::git_remote) -> Remote<'repo> { + Remote { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_remote { + self.raw + } +} + +impl<'repo> Drop for Remote<'repo> { + fn drop(&mut self) { + unsafe { raw::git_remote_free(self.raw) } + } +} + +impl<'repo> Iterator for Refspecs<'repo> { + type Item = Refspec<'repo>; + fn next(&mut self) -> Option<Refspec<'repo>> { + self.range.next().and_then(|i| self.remote.get_refspec(i)) + } + fn size_hint(&self) -> (usize, Option<usize>) { + self.range.size_hint() + } +} +impl<'repo> DoubleEndedIterator for Refspecs<'repo> { + fn next_back(&mut self) -> Option<Refspec<'repo>> { + self.range + .next_back() + .and_then(|i| self.remote.get_refspec(i)) + } +} +impl<'repo> FusedIterator for Refspecs<'repo> {} +impl<'repo> ExactSizeIterator for Refspecs<'repo> {} + +#[allow(missing_docs)] // not documented in libgit2 :( +impl<'remote> RemoteHead<'remote> { + /// Flag if this is available locally. + pub fn is_local(&self) -> bool { + unsafe { (*self.raw).local != 0 } + } + + pub fn oid(&self) -> Oid { + unsafe { Binding::from_raw(&(*self.raw).oid as *const _) } + } + pub fn loid(&self) -> Oid { + unsafe { Binding::from_raw(&(*self.raw).loid as *const _) } + } + + pub fn name(&self) -> &str { + let b = unsafe { crate::opt_bytes(self, (*self.raw).name).unwrap() }; + str::from_utf8(b).unwrap() + } + + pub fn symref_target(&self) -> Option<&str> { + let b = unsafe { crate::opt_bytes(self, (*self.raw).symref_target) }; + b.map(|b| str::from_utf8(b).unwrap()) + } +} + +impl<'cb> Default for FetchOptions<'cb> { + fn default() -> Self { + Self::new() + } +} + +impl<'cb> FetchOptions<'cb> { + /// Creates a new blank set of fetch options + pub fn new() -> FetchOptions<'cb> { + FetchOptions { + callbacks: None, + proxy: None, + prune: FetchPrune::Unspecified, + update_fetchhead: true, + download_tags: AutotagOption::Unspecified, + follow_redirects: RemoteRedirect::Initial, + custom_headers: Vec::new(), + custom_headers_ptrs: Vec::new(), + depth: 0, // Not limited depth + } + } + + /// Set the callbacks to use for the fetch 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. + pub fn proxy_options(&mut self, opts: ProxyOptions<'cb>) -> &mut Self { + self.proxy = Some(opts); + self + } + + /// Set whether to perform a prune after the fetch. + pub fn prune(&mut self, prune: FetchPrune) -> &mut Self { + self.prune = prune; + self + } + + /// Set whether to write the results to FETCH_HEAD. + /// + /// Defaults to `true`. + pub fn update_fetchhead(&mut self, update: bool) -> &mut Self { + self.update_fetchhead = 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 + } + + /// Set how to behave regarding tags on the remote, such as auto-downloading + /// tags for objects we're downloading or downloading all of them. + /// + /// The default is to auto-follow tags. + pub fn download_tags(&mut self, opt: AutotagOption) -> &mut Self { + 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> { + type Raw = raw::git_fetch_options; + + unsafe fn from_raw(_raw: raw::git_fetch_options) -> FetchOptions<'cb> { + panic!("unimplemented"); + } + fn raw(&self) -> raw::git_fetch_options { + raw::git_fetch_options { + version: 1, + callbacks: self + .callbacks + .as_ref() + .map(|m| m.raw()) + .unwrap_or_else(|| RemoteCallbacks::new().raw()), + proxy_opts: self + .proxy + .as_ref() + .map(|m| m.raw()) + .unwrap_or_else(|| ProxyOptions::new().raw()), + prune: crate::call::convert(&self.prune), + update_fetchhead: crate::call::convert(&self.update_fetchhead), + download_tags: crate::call::convert(&self.download_tags), + depth: self.depth, + follow_redirects: self.follow_redirects.raw(), + custom_headers: git_strarray { + count: self.custom_headers_ptrs.len(), + strings: self.custom_headers_ptrs.as_ptr() as *mut _, + }, + } + } +} + +impl<'cb> Default for PushOptions<'cb> { + fn default() -> Self { + Self::new() + } +} + +impl<'cb> PushOptions<'cb> { + /// Creates a new blank set of push options + pub fn new() -> PushOptions<'cb> { + PushOptions { + callbacks: None, + proxy: None, + pb_parallelism: 1, + follow_redirects: RemoteRedirect::Initial, + custom_headers: Vec::new(), + custom_headers_ptrs: Vec::new(), + } + } + + /// Set the callbacks to use for the push operation. + pub fn remote_callbacks(&mut self, cbs: RemoteCallbacks<'cb>) -> &mut Self { + self.callbacks = Some(cbs); + self + } + + /// Set the proxy options to use for the push operation. + pub fn proxy_options(&mut self, opts: ProxyOptions<'cb>) -> &mut Self { + self.proxy = Some(opts); + self + } + + /// If the transport being used to push to the remote requires the creation + /// of a pack file, this controls the number of worker threads used by the + /// packbuilder when creating that pack file to be sent to the remote. + /// + /// if set to 0 the packbuilder will auto-detect the number of threads to + /// create, and the default value is 1. + pub fn packbuilder_parallelism(&mut self, parallel: u32) -> &mut Self { + 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 + } +} + +impl<'cb> Binding for PushOptions<'cb> { + type Raw = raw::git_push_options; + + unsafe fn from_raw(_raw: raw::git_push_options) -> PushOptions<'cb> { + panic!("unimplemented"); + } + fn raw(&self) -> raw::git_push_options { + raw::git_push_options { + version: 1, + callbacks: self + .callbacks + .as_ref() + .map(|m| m.raw()) + .unwrap_or_else(|| RemoteCallbacks::new().raw()), + proxy_opts: self + .proxy + .as_ref() + .map(|m| m.raw()) + .unwrap_or_else(|| ProxyOptions::new().raw()), + pb_parallelism: self.pb_parallelism as libc::c_uint, + follow_redirects: self.follow_redirects.raw(), + custom_headers: git_strarray { + count: self.custom_headers_ptrs.len(), + strings: self.custom_headers_ptrs.as_ptr() as *mut _, + }, + } + } +} + +impl<'repo, 'connection, 'cb> RemoteConnection<'repo, 'connection, 'cb> { + /// Check whether the remote is (still) connected + pub fn connected(&mut self) -> bool { + self.remote.connected() + } + + /// Get the remote repository's reference advertisement list. + /// + /// This list is available as soon as the connection to + /// the remote is initiated and it remains available after disconnecting. + pub fn list(&self) -> Result<&[RemoteHead<'_>], Error> { + self.remote.list() + } + + /// Get the remote's default branch. + /// + /// This default branch is available as soon as the connection to the remote + /// is initiated and it remains available after disconnecting. + pub fn default_branch(&self) -> Result<Buf, Error> { + self.remote.default_branch() + } + + /// access remote bound to this connection + pub fn remote(&mut self) -> &mut Remote<'repo> { + self.remote + } +} + +impl<'repo, 'connection, 'cb> Drop for RemoteConnection<'repo, 'connection, 'cb> { + fn drop(&mut self) { + drop(self.remote.disconnect()); + } +} + +impl Default for RemoteRedirect { + fn default() -> Self { + RemoteRedirect::Initial + } +} + +impl RemoteRedirect { + fn raw(&self) -> raw::git_remote_redirect_t { + match self { + RemoteRedirect::None => raw::GIT_REMOTE_REDIRECT_NONE, + RemoteRedirect::Initial => raw::GIT_REMOTE_REDIRECT_INITIAL, + RemoteRedirect::All => raw::GIT_REMOTE_REDIRECT_ALL, + } + } +} + +#[cfg(test)] +mod tests { + use crate::{AutotagOption, PushOptions}; + use crate::{Direction, FetchOptions, Remote, RemoteCallbacks, Repository}; + use std::cell::Cell; + use tempfile::TempDir; + + #[test] + fn smoke() { + let (td, repo) = crate::test::repo_init(); + t!(repo.remote("origin", "/path/to/nowhere")); + drop(repo); + + let repo = t!(Repository::init(td.path())); + let mut origin = t!(repo.find_remote("origin")); + assert_eq!(origin.name(), Some("origin")); + assert_eq!(origin.url(), Some("/path/to/nowhere")); + assert_eq!(origin.pushurl(), None); + + t!(repo.remote_set_url("origin", "/path/to/elsewhere")); + t!(repo.remote_set_pushurl("origin", Some("/path/to/elsewhere"))); + + let stats = origin.stats(); + assert_eq!(stats.total_objects(), 0); + + t!(origin.stop()); + } + + #[test] + fn create_remote() { + let td = TempDir::new().unwrap(); + let remote = td.path().join("remote"); + Repository::init_bare(&remote).unwrap(); + + let (_td, repo) = crate::test::repo_init(); + let url = if cfg!(unix) { + format!("file://{}", remote.display()) + } else { + format!( + "file:///{}", + remote.display().to_string().replace("\\", "/") + ) + }; + + let mut origin = repo.remote("origin", &url).unwrap(); + assert_eq!(origin.name(), Some("origin")); + assert_eq!(origin.url(), Some(&url[..])); + assert_eq!(origin.pushurl(), None); + + { + let mut specs = origin.refspecs(); + let spec = specs.next().unwrap(); + assert!(specs.next().is_none()); + assert_eq!(spec.str(), Some("+refs/heads/*:refs/remotes/origin/*")); + assert_eq!(spec.dst(), Some("refs/remotes/origin/*")); + assert_eq!(spec.src(), Some("refs/heads/*")); + assert!(spec.is_force()); + } + assert!(origin.refspecs().next_back().is_some()); + { + let remotes = repo.remotes().unwrap(); + assert_eq!(remotes.len(), 1); + assert_eq!(remotes.get(0), Some("origin")); + assert_eq!(remotes.iter().count(), 1); + assert_eq!(remotes.iter().next().unwrap(), Some("origin")); + } + + origin.connect(Direction::Push).unwrap(); + assert!(origin.connected()); + origin.disconnect().unwrap(); + + origin.connect(Direction::Fetch).unwrap(); + assert!(origin.connected()); + origin.download(&[] as &[&str], None).unwrap(); + origin.disconnect().unwrap(); + + { + let mut connection = origin.connect_auth(Direction::Push, None, None).unwrap(); + assert!(connection.connected()); + } + assert!(!origin.connected()); + + { + let mut connection = origin.connect_auth(Direction::Fetch, None, None).unwrap(); + assert!(connection.connected()); + } + assert!(!origin.connected()); + + origin.fetch(&[] as &[&str], None, None).unwrap(); + origin.fetch(&[] as &[&str], None, Some("foo")).unwrap(); + origin + .update_tips(None, true, AutotagOption::Unspecified, None) + .unwrap(); + origin + .update_tips(None, true, AutotagOption::All, Some("foo")) + .unwrap(); + + t!(repo.remote_add_fetch("origin", "foo")); + t!(repo.remote_add_fetch("origin", "bar")); + } + + #[test] + fn rename_remote() { + let (_td, repo) = crate::test::repo_init(); + repo.remote("origin", "foo").unwrap(); + drop(repo.remote_rename("origin", "foo")); + drop(repo.remote_delete("foo")); + } + + #[test] + fn create_remote_anonymous() { + let td = TempDir::new().unwrap(); + let repo = Repository::init(td.path()).unwrap(); + + let origin = repo.remote_anonymous("/path/to/nowhere").unwrap(); + assert_eq!(origin.name(), None); + drop(origin.clone()); + } + + #[test] + 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(); + let td2 = TempDir::new().unwrap(); + let url = crate::test::path2url(&td.path()); + + let repo = Repository::init(td2.path()).unwrap(); + let progress_hit = Cell::new(false); + { + let mut callbacks = RemoteCallbacks::new(); + let mut origin = repo.remote("origin", &url).unwrap(); + + callbacks.transfer_progress(|_progress| { + progress_hit.set(true); + true + }); + origin + .fetch( + &[] as &[&str], + Some(FetchOptions::new().remote_callbacks(callbacks)), + None, + ) + .unwrap(); + + let list = t!(origin.list()); + assert_eq!(list.len(), 2); + assert_eq!(list[0].name(), "HEAD"); + assert!(!list[0].is_local()); + assert_eq!(list[1].name(), "refs/heads/main"); + assert!(!list[1].is_local()); + } + assert!(progress_hit.get()); + } + + /// This test is meant to assure that the callbacks provided to connect will not cause + /// segfaults + #[test] + fn connect_list() { + let (td, _repo) = crate::test::repo_init(); + let td2 = TempDir::new().unwrap(); + let url = crate::test::path2url(&td.path()); + + let repo = Repository::init(td2.path()).unwrap(); + let mut callbacks = RemoteCallbacks::new(); + callbacks.sideband_progress(|_progress| { + // no-op + true + }); + + let mut origin = repo.remote("origin", &url).unwrap(); + + { + let mut connection = origin + .connect_auth(Direction::Fetch, Some(callbacks), None) + .unwrap(); + assert!(connection.connected()); + + let list = t!(connection.list()); + assert_eq!(list.len(), 2); + assert_eq!(list[0].name(), "HEAD"); + assert!(!list[0].is_local()); + assert_eq!(list[1].name(), "refs/heads/main"); + assert!(!list[1].is_local()); + } + assert!(!origin.connected()); + } + + #[test] + fn push() { + let (_td, repo) = crate::test::repo_init(); + let td2 = TempDir::new().unwrap(); + let td3 = TempDir::new().unwrap(); + let url = crate::test::path2url(&td2.path()); + + let mut opts = crate::RepositoryInitOptions::new(); + opts.bare(true); + opts.initial_head("main"); + Repository::init_opts(td2.path(), &opts).unwrap(); + // git push + let mut remote = repo.remote("origin", &url).unwrap(); + let mut updated = false; + { + let mut callbacks = RemoteCallbacks::new(); + callbacks.push_update_reference(|refname, status| { + updated = true; + assert_eq!(refname, "refs/heads/main"); + assert_eq!(status, None); + Ok(()) + }); + let mut options = PushOptions::new(); + options.remote_callbacks(callbacks); + remote + .push(&["refs/heads/main"], Some(&mut options)) + .unwrap(); + } + assert!(updated); + + let repo = Repository::clone(&url, td3.path()).unwrap(); + let commit = repo.head().unwrap().target().unwrap(); + let commit = repo.find_commit(commit).unwrap(); + assert_eq!(commit.message(), Some("initial\n\nbody")); + } + + #[test] + fn prune() { + let (td, remote_repo) = crate::test::repo_init(); + let oid = remote_repo.head().unwrap().target().unwrap(); + let commit = remote_repo.find_commit(oid).unwrap(); + remote_repo.branch("stale", &commit, true).unwrap(); + + let td2 = TempDir::new().unwrap(); + let url = crate::test::path2url(&td.path()); + let repo = Repository::clone(&url, &td2).unwrap(); + + fn assert_branch_count(repo: &Repository, count: usize) { + assert_eq!( + repo.branches(Some(crate::BranchType::Remote)) + .unwrap() + .filter(|b| b.as_ref().unwrap().0.name().unwrap() == Some("origin/stale")) + .count(), + count, + ); + } + + assert_branch_count(&repo, 1); + + // delete `stale` branch on remote repo + let mut stale_branch = remote_repo + .find_branch("stale", crate::BranchType::Local) + .unwrap(); + stale_branch.delete().unwrap(); + + // prune + let mut remote = repo.find_remote("origin").unwrap(); + remote.connect(Direction::Push).unwrap(); + let mut callbacks = RemoteCallbacks::new(); + callbacks.update_tips(|refname, _a, b| { + assert_eq!(refname, "refs/remotes/origin/stale"); + assert!(b.is_zero()); + true + }); + remote.prune(Some(callbacks)).unwrap(); + assert_branch_count(&repo, 0); + } + + #[test] + fn push_negotiation() { + let (_td, repo) = crate::test::repo_init(); + let oid = repo.head().unwrap().target().unwrap(); + + let td2 = TempDir::new().unwrap(); + let url = crate::test::path2url(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/extra/git2/src/remote_callbacks.rs b/extra/git2/src/remote_callbacks.rs new file mode 100644 index 000000000..1169420bd --- /dev/null +++ b/extra/git2/src/remote_callbacks.rs @@ -0,0 +1,518 @@ +use libc::{c_char, c_int, c_uint, c_void, size_t}; +use std::ffi::{CStr, CString}; +use std::mem; +use std::ptr; +use std::slice; +use std::str; + +use crate::cert::Cert; +use crate::util::Binding; +use crate::{ + panic, raw, Cred, CredentialType, Error, IndexerProgress, Oid, PackBuilderStage, Progress, + PushUpdate, +}; + +/// A structure to contain the callbacks which are invoked when a repository is +/// being updated or downloaded. +/// +/// These callbacks are used to manage facilities such as authentication, +/// transfer progress, etc. +pub struct RemoteCallbacks<'a> { + push_progress: Option<Box<PushTransferProgress<'a>>>, + progress: Option<Box<IndexerProgress<'a>>>, + pack_progress: Option<Box<PackProgress<'a>>>, + credentials: Option<Box<Credentials<'a>>>, + sideband_progress: Option<Box<TransportMessage<'a>>>, + update_tips: Option<Box<UpdateTips<'a>>>, + certificate_check: Option<Box<CertificateCheck<'a>>>, + push_update_reference: Option<Box<PushUpdateReference<'a>>>, + push_negotiation: Option<Box<PushNegotiation<'a>>>, +} + +/// 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` +/// if it was not included. +/// * `allowed_types` - a bitmask stating which cred types are OK to return. +pub type Credentials<'a> = + dyn FnMut(&str, Option<&str>, CredentialType) -> Result<Cred, Error> + 'a; + +/// Callback for receiving messages delivered by the transport. +/// +/// The return value indicates whether the network operation should continue. +pub type TransportMessage<'a> = dyn FnMut(&[u8]) -> bool + 'a; + +/// Callback for whenever a reference is updated locally. +pub type UpdateTips<'a> = dyn FnMut(&str, Oid, Oid) -> bool + 'a; + +/// Callback for a custom certificate check. +/// +/// 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) -> Result<CertificateCheckStatus, Error> + 'a; + +/// The return value for the [`RemoteCallbacks::certificate_check`] callback. +pub enum CertificateCheckStatus { + /// Indicates that the certificate should be accepted. + CertificateOk, + /// Indicates that the certificate callback is neither accepting nor + /// rejecting the certificate. The result of the certificate checks + /// built-in to libgit2 will be used instead. + CertificatePassthrough, +} + +/// Callback for each updated reference on push. +/// +/// The first argument here is the `refname` of the reference, and the second is +/// the status message sent by a server. If the status is `Some` then the update +/// was rejected by the remote server with a reason why. +pub type PushUpdateReference<'a> = dyn FnMut(&str, Option<&str>) -> Result<(), Error> + 'a; + +/// Callback for push transfer progress +/// +/// Parameters: +/// * current +/// * total +/// * bytes +pub type PushTransferProgress<'a> = dyn FnMut(usize, usize, usize) + 'a; + +/// Callback for pack progress +/// +/// Parameters: +/// * stage +/// * current +/// * total +pub type PackProgress<'a> = dyn FnMut(PackBuilderStage, usize, usize) + 'a; + +/// Callback used to inform of upcoming updates. +/// +/// The argument is a slice containing the updates which will be sent as +/// commands to the destination. +/// +/// The push is cancelled if an error is returned. +pub type PushNegotiation<'a> = dyn FnMut(&[PushUpdate<'_>]) -> Result<(), Error> + 'a; + +impl<'a> Default for RemoteCallbacks<'a> { + fn default() -> Self { + Self::new() + } +} + +impl<'a> RemoteCallbacks<'a> { + /// Creates a new set of empty callbacks + pub fn new() -> RemoteCallbacks<'a> { + RemoteCallbacks { + credentials: None, + progress: None, + pack_progress: None, + sideband_progress: None, + update_tips: None, + certificate_check: None, + push_update_reference: None, + push_progress: None, + push_negotiation: None, + } + } + + /// The callback through which to fetch credentials if required. + /// + /// # Example + /// + /// Prepare a callback to authenticate using the `$HOME/.ssh/id_rsa` SSH key, and + /// extracting the username from the URL (i.e. git@github.com:rust-lang/git2-rs.git): + /// + /// ```no_run + /// use git2::{Cred, RemoteCallbacks}; + /// use std::env; + /// + /// let mut callbacks = RemoteCallbacks::new(); + /// callbacks.credentials(|_url, username_from_url, _allowed_types| { + /// Cred::ssh_key( + /// username_from_url.unwrap(), + /// None, + /// std::path::Path::new(&format!("{}/.ssh/id_rsa", env::var("HOME").unwrap())), + /// None, + /// ) + /// }); + /// ``` + pub fn credentials<F>(&mut self, cb: F) -> &mut RemoteCallbacks<'a> + where + F: FnMut(&str, Option<&str>, CredentialType) -> Result<Cred, Error> + 'a, + { + self.credentials = Some(Box::new(cb) as Box<Credentials<'a>>); + self + } + + /// The callback through which progress is monitored. + pub fn transfer_progress<F>(&mut self, cb: F) -> &mut RemoteCallbacks<'a> + where + F: FnMut(Progress<'_>) -> bool + 'a, + { + self.progress = Some(Box::new(cb) as Box<IndexerProgress<'a>>); + self + } + + /// Textual progress from the remote. + /// + /// Text sent over the progress side-band will be passed to this function + /// (this is the 'counting objects' output). + pub fn sideband_progress<F>(&mut self, cb: F) -> &mut RemoteCallbacks<'a> + where + F: FnMut(&[u8]) -> bool + 'a, + { + self.sideband_progress = Some(Box::new(cb) as Box<TransportMessage<'a>>); + self + } + + /// Each time a reference is updated locally, the callback will be called + /// with information about it. + pub fn update_tips<F>(&mut self, cb: F) -> &mut RemoteCallbacks<'a> + where + F: FnMut(&str, Oid, Oid) -> bool + 'a, + { + self.update_tips = Some(Box::new(cb) as Box<UpdateTips<'a>>); + self + } + + /// If certificate verification fails, then this callback will be invoked to + /// let the caller make the final decision of whether to allow the + /// connection to proceed. + pub fn certificate_check<F>(&mut self, cb: F) -> &mut RemoteCallbacks<'a> + where + F: FnMut(&Cert<'_>, &str) -> Result<CertificateCheckStatus, Error> + 'a, + { + self.certificate_check = Some(Box::new(cb) as Box<CertificateCheck<'a>>); + self + } + + /// Set a callback to get invoked for each updated reference on a push. + /// + /// The first argument to the callback is the name of the reference and the + /// second is a status message sent by the server. If the status is `Some` + /// then the push was rejected. + pub fn push_update_reference<F>(&mut self, cb: F) -> &mut RemoteCallbacks<'a> + where + F: FnMut(&str, Option<&str>) -> Result<(), Error> + 'a, + { + self.push_update_reference = Some(Box::new(cb) as Box<PushUpdateReference<'a>>); + self + } + + /// The callback through which progress of push transfer is monitored + pub fn push_transfer_progress<F>(&mut self, cb: F) -> &mut RemoteCallbacks<'a> + where + F: FnMut(usize, usize, usize) + 'a, + { + self.push_progress = Some(Box::new(cb) as Box<PushTransferProgress<'a>>); + self + } + + /// Function to call with progress information during pack building. + /// Be aware that this is called inline with pack building operations, + /// so performance may be affected. + pub fn pack_progress<F>(&mut self, cb: F) -> &mut RemoteCallbacks<'a> + where + F: FnMut(PackBuilderStage, usize, usize) + 'a, + { + self.pack_progress = Some(Box::new(cb) as Box<PackProgress<'a>>); + self + } + + /// The callback is called once between the negotiation step and the upload. + /// It provides information about what updates will be performed. + pub fn push_negotiation<F>(&mut self, cb: F) -> &mut RemoteCallbacks<'a> + where + F: FnMut(&[PushUpdate<'_>]) -> Result<(), Error> + 'a, + { + self.push_negotiation = Some(Box::new(cb) as Box<PushNegotiation<'a>>); + self + } +} + +impl<'a> Binding for RemoteCallbacks<'a> { + type Raw = raw::git_remote_callbacks; + unsafe fn from_raw(_raw: raw::git_remote_callbacks) -> RemoteCallbacks<'a> { + panic!("unimplemented"); + } + + fn raw(&self) -> raw::git_remote_callbacks { + unsafe { + let mut callbacks: raw::git_remote_callbacks = mem::zeroed(); + assert_eq!( + raw::git_remote_init_callbacks(&mut callbacks, raw::GIT_REMOTE_CALLBACKS_VERSION), + 0 + ); + if self.progress.is_some() { + callbacks.transfer_progress = Some(transfer_progress_cb); + } + if self.credentials.is_some() { + callbacks.credentials = Some(credentials_cb); + } + if self.sideband_progress.is_some() { + callbacks.sideband_progress = Some(sideband_progress_cb); + } + if self.certificate_check.is_some() { + callbacks.certificate_check = Some(certificate_check_cb); + } + if self.push_update_reference.is_some() { + callbacks.push_update_reference = Some(push_update_reference_cb); + } + if self.push_progress.is_some() { + callbacks.push_transfer_progress = Some(push_transfer_progress_cb); + } + if self.pack_progress.is_some() { + callbacks.pack_progress = Some(pack_progress_cb); + } + if self.update_tips.is_some() { + let f: extern "C" fn( + *const c_char, + *const raw::git_oid, + *const raw::git_oid, + *mut c_void, + ) -> c_int = update_tips_cb; + callbacks.update_tips = Some(f); + } + if self.push_negotiation.is_some() { + callbacks.push_negotiation = Some(push_negotiation_cb); + } + callbacks.payload = self as *const _ as *mut _; + callbacks + } + } +} + +extern "C" fn credentials_cb( + ret: *mut *mut raw::git_cred, + url: *const c_char, + username_from_url: *const c_char, + allowed_types: c_uint, + payload: *mut c_void, +) -> c_int { + unsafe { + let ok = panic::wrap(|| { + let payload = &mut *(payload as *mut RemoteCallbacks<'_>); + let callback = payload + .credentials + .as_mut() + .ok_or(raw::GIT_PASSTHROUGH as c_int)?; + *ret = ptr::null_mut(); + let url = str::from_utf8(CStr::from_ptr(url).to_bytes()) + .map_err(|_| raw::GIT_PASSTHROUGH as c_int)?; + let username_from_url = match crate::opt_bytes(&url, username_from_url) { + Some(username) => { + Some(str::from_utf8(username).map_err(|_| raw::GIT_PASSTHROUGH as c_int)?) + } + None => None, + }; + + let cred_type = CredentialType::from_bits_truncate(allowed_types as u32); + + callback(url, username_from_url, cred_type).map_err(|e| { + let s = CString::new(e.to_string()).unwrap(); + raw::git_error_set_str(e.class() as c_int, s.as_ptr()); + e.raw_code() as c_int + }) + }); + match ok { + Some(Ok(cred)) => { + // Turns out it's a memory safety issue if we pass through any + // and all credentials into libgit2 + if allowed_types & (cred.credtype() as c_uint) != 0 { + *ret = cred.unwrap(); + 0 + } else { + raw::GIT_PASSTHROUGH as c_int + } + } + Some(Err(e)) => e, + None => -1, + } + } +} + +extern "C" fn transfer_progress_cb( + stats: *const raw::git_indexer_progress, + payload: *mut c_void, +) -> c_int { + let ok = panic::wrap(|| unsafe { + let payload = &mut *(payload as *mut RemoteCallbacks<'_>); + let callback = match payload.progress { + Some(ref mut c) => c, + None => return true, + }; + let progress = Binding::from_raw(stats); + callback(progress) + }); + if ok == Some(true) { + 0 + } else { + -1 + } +} + +extern "C" fn sideband_progress_cb(str: *const c_char, len: c_int, payload: *mut c_void) -> c_int { + let ok = panic::wrap(|| unsafe { + let payload = &mut *(payload as *mut RemoteCallbacks<'_>); + let callback = match payload.sideband_progress { + Some(ref mut c) => c, + None => return true, + }; + let buf = slice::from_raw_parts(str as *const u8, len as usize); + callback(buf) + }); + if ok == Some(true) { + 0 + } else { + -1 + } +} + +extern "C" fn update_tips_cb( + refname: *const c_char, + a: *const raw::git_oid, + b: *const raw::git_oid, + data: *mut c_void, +) -> c_int { + let ok = panic::wrap(|| unsafe { + let payload = &mut *(data as *mut RemoteCallbacks<'_>); + let callback = match payload.update_tips { + Some(ref mut c) => c, + None => return true, + }; + let refname = str::from_utf8(CStr::from_ptr(refname).to_bytes()).unwrap(); + let a = Binding::from_raw(a); + let b = Binding::from_raw(b); + callback(refname, a, b) + }); + if ok == Some(true) { + 0 + } else { + -1 + } +} + +extern "C" fn certificate_check_cb( + cert: *mut raw::git_cert, + _valid: c_int, + hostname: *const c_char, + data: *mut c_void, +) -> c_int { + let ok = panic::wrap(|| unsafe { + let payload = &mut *(data as *mut RemoteCallbacks<'_>); + let callback = match payload.certificate_check { + Some(ref mut c) => c, + 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) + }); + match ok { + Some(Ok(CertificateCheckStatus::CertificateOk)) => 0, + Some(Ok(CertificateCheckStatus::CertificatePassthrough)) => raw::GIT_PASSTHROUGH as c_int, + Some(Err(e)) => { + let s = CString::new(e.message()).unwrap(); + unsafe { + raw::git_error_set_str(e.class() as c_int, s.as_ptr()); + } + e.raw_code() as c_int + } + None => { + // Panic. The *should* get resumed by some future call to check(). + -1 + } + } +} + +extern "C" fn push_update_reference_cb( + refname: *const c_char, + status: *const c_char, + data: *mut c_void, +) -> c_int { + panic::wrap(|| unsafe { + let payload = &mut *(data as *mut RemoteCallbacks<'_>); + let callback = match payload.push_update_reference { + Some(ref mut c) => c, + None => return 0, + }; + let refname = str::from_utf8(CStr::from_ptr(refname).to_bytes()).unwrap(); + let status = if status.is_null() { + None + } else { + Some(str::from_utf8(CStr::from_ptr(status).to_bytes()).unwrap()) + }; + match callback(refname, status) { + Ok(()) => 0, + Err(e) => e.raw_code(), + } + }) + .unwrap_or(-1) +} + +extern "C" fn push_transfer_progress_cb( + progress: c_uint, + total: c_uint, + bytes: size_t, + data: *mut c_void, +) -> c_int { + panic::wrap(|| unsafe { + let payload = &mut *(data as *mut RemoteCallbacks<'_>); + let callback = match payload.push_progress { + Some(ref mut c) => c, + None => return 0, + }; + + callback(progress as usize, total as usize, bytes as usize); + + 0 + }) + .unwrap_or(-1) +} + +extern "C" fn pack_progress_cb( + stage: raw::git_packbuilder_stage_t, + current: c_uint, + total: c_uint, + data: *mut c_void, +) -> c_int { + panic::wrap(|| unsafe { + let payload = &mut *(data as *mut RemoteCallbacks<'_>); + let callback = match payload.pack_progress { + Some(ref mut c) => c, + None => return 0, + }; + + let stage = Binding::from_raw(stage); + + callback(stage, current as usize, total as usize); + + 0 + }) + .unwrap_or(-1) +} + +extern "C" fn push_negotiation_cb( + updates: *mut *const raw::git_push_update, + len: size_t, + payload: *mut c_void, +) -> c_int { + panic::wrap(|| unsafe { + let payload = &mut *(payload as *mut RemoteCallbacks<'_>); + let callback = match payload.push_negotiation { + Some(ref mut c) => c, + None => return 0, + }; + + let updates = slice::from_raw_parts(updates as *mut PushUpdate<'_>, len); + match callback(updates) { + Ok(()) => 0, + Err(e) => e.raw_code(), + } + }) + .unwrap_or(-1) +} diff --git a/extra/git2/src/repo.rs b/extra/git2/src/repo.rs new file mode 100644 index 000000000..921e2b30e --- /dev/null +++ b/extra/git2/src/repo.rs @@ -0,0 +1,4226 @@ +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, PathBuf}; +use std::ptr; +use std::str; + +use crate::build::{CheckoutBuilder, RepoBuilder}; +use crate::diff::{ + binary_cb_c, file_cb_c, hunk_cb_c, line_cb_c, BinaryCb, DiffCallbacks, FileCb, HunkCb, LineCb, +}; +use crate::oid_array::OidArray; +use crate::stash::{stash_cb, StashApplyOptions, StashCbData, StashSaveOptions}; +use crate::string_array::StringArray; +use crate::tagforeach::{tag_foreach_cb, TagForeachCB, TagForeachData}; +use crate::util::{self, path_to_repo_path, Binding}; +use crate::worktree::{Worktree, WorktreeAddOptions}; +use crate::CherrypickOptions; +use crate::RevertOptions; +use crate::{mailmap::Mailmap, panic}; +use crate::{ + raw, AttrCheckFlags, Buf, Error, Object, Remote, RepositoryOpenFlags, RepositoryState, Revspec, + StashFlags, +}; +use crate::{ + AnnotatedCommit, MergeAnalysis, 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::{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. +/// +/// This structure corresponds to a `git_repository` in libgit2. Many other +/// types in git2-rs are derivative from this structure and are attached to its +/// lifetime. +/// +/// When a repository goes out of scope it is freed in memory but not deleted +/// from the filesystem. +pub struct Repository { + raw: *mut raw::git_repository, +} + +// It is the current belief that a `Repository` can be sent among threads, or +// even shared among threads in a mutex. +unsafe impl Send for Repository {} + +/// Options which can be used to configure how a repository is initialized +pub struct RepositoryInitOptions { + flags: u32, + mode: u32, + workdir_path: Option<CString>, + description: Option<CString>, + template_path: Option<CString>, + initial_head: Option<CString>, + origin_url: Option<CString>, +} + +impl Repository { + /// Attempt to open an already-existing repository at `path`. + /// + /// The path can point to either a normal or bare repository. + pub fn open<P: AsRef<Path>>(path: P) -> Result<Repository, Error> { + crate::init(); + // Normal file path OK (does not need Windows conversion). + let path = path.as_ref().into_c_string()?; + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_repository_open(&mut ret, path)); + Ok(Binding::from_raw(ret)) + } + } + + /// Attempt to open an already-existing bare repository at `path`. + /// + /// The path can point to only a bare repository. + pub fn open_bare<P: AsRef<Path>>(path: P) -> Result<Repository, Error> { + crate::init(); + // Normal file path OK (does not need Windows conversion). + let path = path.as_ref().into_c_string()?; + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_repository_open_bare(&mut ret, path)); + Ok(Binding::from_raw(ret)) + } + } + + /// Find and open an existing repository, respecting git environment + /// variables. This acts like `open_ext` with the + /// [FROM_ENV](RepositoryOpenFlags::FROM_ENV) flag, but additionally respects `$GIT_DIR`. + /// With `$GIT_DIR` unset, this will search for a repository starting in + /// the current directory. + pub fn open_from_env() -> Result<Repository, Error> { + crate::init(); + let mut ret = ptr::null_mut(); + let flags = raw::GIT_REPOSITORY_OPEN_FROM_ENV; + unsafe { + try_call!(raw::git_repository_open_ext( + &mut ret, + ptr::null(), + flags as c_uint, + ptr::null() + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Find and open an existing repository, with additional options. + /// + /// If flags contains [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 [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 [BARE](RepositoryOpenFlags::BARE), force opening the repository as + /// bare even if it isn't, ignoring any working directory, and defer + /// loading the repository configuration for performance. + /// + /// If flags contains [NO_DOTGIT](RepositoryOpenFlags::NO_DOTGIT), don't try appending + /// `/.git` to `path`. + /// + /// If flags contains [FROM_ENV](RepositoryOpenFlags::FROM_ENV), `open_ext` will ignore + /// other flags and `ceiling_dirs`, and respect the same environment + /// variables git does. Note, however, that `path` overrides `$GIT_DIR`; to + /// respect `$GIT_DIR` as well, use `open_from_env`. + /// + /// ceiling_dirs specifies a list of paths that the search through parent + /// directories will stop before entering. Use the functions in std::env + /// to construct or manipulate such a path list. (You can use `&[] as + /// &[&std::ffi::OsStr]` as an argument if there are no ceiling + /// directories.) + pub fn open_ext<P, O, I>( + path: P, + flags: RepositoryOpenFlags, + ceiling_dirs: I, + ) -> Result<Repository, Error> + where + P: AsRef<Path>, + O: AsRef<OsStr>, + I: IntoIterator<Item = O>, + { + crate::init(); + // Normal file path OK (does not need Windows conversion). + let path = path.as_ref().into_c_string()?; + let ceiling_dirs_os = env::join_paths(ceiling_dirs)?; + let ceiling_dirs = ceiling_dirs_os.into_c_string()?; + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_repository_open_ext( + &mut ret, + path, + flags.bits() as c_uint, + ceiling_dirs + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Attempt to open an already-existing repository from a worktree. + pub fn open_from_worktree(worktree: &Worktree) -> Result<Repository, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_repository_open_from_worktree( + &mut ret, + worktree.raw() + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Attempt to open an already-existing repository at or above `path` + /// + /// This starts at `path` and looks up the filesystem hierarchy + /// until it finds a repository. + pub fn discover<P: AsRef<Path>>(path: P) -> Result<Repository, Error> { + // TODO: this diverges significantly from the libgit2 API + crate::init(); + let buf = Buf::new(); + // Normal file path OK (does not need Windows conversion). + let path = path.as_ref().into_c_string()?; + unsafe { + try_call!(raw::git_repository_discover( + buf.raw(), + path, + 1, + ptr::null() + )); + } + Repository::open(util::bytes2path(&*buf)) + } + + /// Attempt to find the path to a git repo for a given path + /// + /// This starts at `path` and looks up the filesystem hierarchy + /// until it finds a repository, stopping if it finds a member of ceiling_dirs + pub fn discover_path<P: AsRef<Path>, I, O>(path: P, ceiling_dirs: I) -> Result<PathBuf, Error> + where + O: AsRef<OsStr>, + I: IntoIterator<Item = O>, + { + 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 + /// repository, and it will read any user-specified templates when creating + /// the repository. This behavior can be configured through `init_opts`. + pub fn init<P: AsRef<Path>>(path: P) -> Result<Repository, Error> { + Repository::init_opts(path, &RepositoryInitOptions::new()) + } + + /// Creates a new `--bare` repository in the specified folder. + /// + /// The folder must exist prior to invoking this function. + pub fn init_bare<P: AsRef<Path>>(path: P) -> Result<Repository, Error> { + Repository::init_opts(path, RepositoryInitOptions::new().bare(true)) + } + + /// Creates a new repository in the specified folder with the given options. + /// + /// See `RepositoryInitOptions` struct for more information. + pub fn init_opts<P: AsRef<Path>>( + path: P, + opts: &RepositoryInitOptions, + ) -> Result<Repository, Error> { + crate::init(); + // Normal file path OK (does not need Windows conversion). + let path = path.as_ref().into_c_string()?; + let mut ret = ptr::null_mut(); + unsafe { + let mut opts = opts.raw(); + try_call!(raw::git_repository_init_ext(&mut ret, path, &mut opts)); + Ok(Binding::from_raw(ret)) + } + } + + /// Clone a remote repository. + /// + /// See the `RepoBuilder` struct for more information. This function will + /// delegate to a fresh `RepoBuilder` + pub fn clone<P: AsRef<Path>>(url: &str, into: P) -> Result<Repository, Error> { + crate::init(); + RepoBuilder::new().clone(url, into.as_ref()) + } + + /// Clone a remote repository, initialize and update its submodules + /// recursively. + /// + /// This is similar to `git clone --recursive`. + pub fn clone_recurse<P: AsRef<Path>>(url: &str, into: P) -> Result<Repository, Error> { + let repo = Repository::clone(url, into)?; + repo.update_submodules()?; + Ok(repo) + } + + /// Attempt to wrap an object database as a repository. + pub fn from_odb(odb: Odb<'_>) -> Result<Repository, Error> { + crate::init(); + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_repository_wrap_odb(&mut ret, odb.raw())); + Ok(Binding::from_raw(ret)) + } + } + + /// Update submodules recursively. + /// + /// Uninitialized submodules will be initialized. + fn update_submodules(&self) -> Result<(), Error> { + fn add_subrepos(repo: &Repository, list: &mut Vec<Repository>) -> Result<(), Error> { + for mut subm in repo.submodules()? { + subm.update(true, None)?; + list.push(subm.open()?); + } + Ok(()) + } + + let mut repos = Vec::new(); + add_subrepos(self, &mut repos)?; + while let Some(repo) = repos.pop() { + add_subrepos(&repo, &mut repos)?; + } + Ok(()) + } + + /// Execute a rev-parse operation against the `spec` listed. + /// + /// The resulting revision specification is returned, or an error is + /// returned if one occurs. + pub fn revparse(&self, spec: &str) -> Result<Revspec<'_>, Error> { + let mut raw = raw::git_revspec { + from: ptr::null_mut(), + to: ptr::null_mut(), + flags: 0, + }; + let spec = CString::new(spec)?; + unsafe { + try_call!(raw::git_revparse(&mut raw, self.raw, spec)); + let to = Binding::from_raw_opt(raw.to); + let from = Binding::from_raw_opt(raw.from); + let mode = RevparseMode::from_bits_truncate(raw.flags as u32); + Ok(Revspec::from_objects(from, to, mode)) + } + } + + /// Find a single object, as specified by a revision string. + pub fn revparse_single(&self, spec: &str) -> Result<Object<'_>, Error> { + let spec = CString::new(spec)?; + let mut obj = ptr::null_mut(); + unsafe { + try_call!(raw::git_revparse_single(&mut obj, self.raw, spec)); + assert!(!obj.is_null()); + Ok(Binding::from_raw(obj)) + } + } + + /// Find a single object and intermediate reference by a revision string. + /// + /// See `man gitrevisions`, or + /// <http://git-scm.com/docs/git-rev-parse.html#_specifying_revisions> for + /// information on the syntax accepted. + /// + /// In some cases (`@{<-n>}` or `<branchname>@{upstream}`), the expression + /// may point to an intermediate reference. When such expressions are being + /// passed in, this intermediate reference is returned. + pub fn revparse_ext(&self, spec: &str) -> Result<(Object<'_>, Option<Reference<'_>>), Error> { + let spec = CString::new(spec)?; + let mut git_obj = ptr::null_mut(); + let mut git_ref = ptr::null_mut(); + unsafe { + try_call!(raw::git_revparse_ext( + &mut git_obj, + &mut git_ref, + self.raw, + spec + )); + assert!(!git_obj.is_null()); + Ok((Binding::from_raw(git_obj), Binding::from_raw_opt(git_ref))) + } + } + + /// Tests whether this repository is a bare repository or not. + pub fn is_bare(&self) -> bool { + unsafe { raw::git_repository_is_bare(self.raw) == 1 } + } + + /// Tests whether this repository is a shallow clone. + pub fn is_shallow(&self) -> bool { + unsafe { raw::git_repository_is_shallow(self.raw) == 1 } + } + + /// Tests whether this repository is a worktree. + pub fn is_worktree(&self) -> bool { + unsafe { raw::git_repository_is_worktree(self.raw) == 1 } + } + + /// Tests whether this repository is empty. + pub fn is_empty(&self) -> Result<bool, Error> { + let empty = unsafe { try_call!(raw::git_repository_is_empty(self.raw)) }; + Ok(empty == 1) + } + + /// Returns the path to the `.git` folder for normal repositories or the + /// repository itself for bare repositories. + pub fn path(&self) -> &Path { + unsafe { + let ptr = raw::git_repository_path(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) }; + macro_rules! check( ($($raw:ident => $real:ident),*) => ( + $(if state == raw::$raw as c_int { + super::RepositoryState::$real + }) else * + else { + panic!("unknown repository state: {}", state) + } + ) ); + + check!( + GIT_REPOSITORY_STATE_NONE => Clean, + GIT_REPOSITORY_STATE_MERGE => Merge, + GIT_REPOSITORY_STATE_REVERT => Revert, + GIT_REPOSITORY_STATE_REVERT_SEQUENCE => RevertSequence, + GIT_REPOSITORY_STATE_CHERRYPICK => CherryPick, + GIT_REPOSITORY_STATE_CHERRYPICK_SEQUENCE => CherryPickSequence, + GIT_REPOSITORY_STATE_BISECT => Bisect, + GIT_REPOSITORY_STATE_REBASE => Rebase, + GIT_REPOSITORY_STATE_REBASE_INTERACTIVE => RebaseInteractive, + GIT_REPOSITORY_STATE_REBASE_MERGE => RebaseMerge, + GIT_REPOSITORY_STATE_APPLY_MAILBOX => ApplyMailbox, + GIT_REPOSITORY_STATE_APPLY_MAILBOX_OR_REBASE => ApplyMailboxOrRebase + ) + } + + /// Get the path of the working directory for this repository. + /// + /// If this repository is bare, then `None` is returned. + pub fn workdir(&self) -> Option<&Path> { + unsafe { + let ptr = raw::git_repository_workdir(self.raw); + if ptr.is_null() { + None + } else { + Some(util::bytes2path(CStr::from_ptr(ptr).to_bytes())) + } + } + } + + /// Set the path to the working directory for this repository. + /// + /// If `update_link` is true, create/update the gitlink file in the workdir + /// and set config "core.worktree" (if workdir is not the parent of the .git + /// directory). + pub fn set_workdir(&self, path: &Path, update_gitlink: bool) -> Result<(), Error> { + // Normal file path OK (does not need Windows conversion). + let path = path.into_c_string()?; + unsafe { + try_call!(raw::git_repository_set_workdir( + self.raw(), + path, + update_gitlink + )); + } + Ok(()) + } + + /// Get the currently active namespace for this repository. + /// + /// If there is no namespace, or the namespace is not a valid utf8 string, + /// `None` is returned. + pub fn namespace(&self) -> Option<&str> { + self.namespace_bytes().and_then(|s| str::from_utf8(s).ok()) + } + + /// Get the currently active namespace for this repository as a byte array. + /// + /// If there is no namespace, `None` is returned. + pub fn namespace_bytes(&self) -> Option<&[u8]> { + unsafe { crate::opt_bytes(self, raw::git_repository_get_namespace(self.raw)) } + } + + /// Set the active namespace for this repository. + pub fn set_namespace(&self, namespace: &str) -> Result<(), Error> { + self.set_namespace_bytes(namespace.as_bytes()) + } + + /// Set the active namespace for this repository as a byte array. + pub fn set_namespace_bytes(&self, namespace: &[u8]) -> Result<(), Error> { + unsafe { + let namespace = CString::new(namespace)?; + try_call!(raw::git_repository_set_namespace(self.raw, namespace)); + Ok(()) + } + } + + /// Remove the active namespace for this repository. + pub fn remove_namespace(&self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_repository_set_namespace(self.raw, ptr::null())); + Ok(()) + } + } + + /// Retrieves the Git merge message. + /// Remember to remove the message when finished. + pub fn message(&self) -> Result<String, Error> { + unsafe { + let buf = Buf::new(); + try_call!(raw::git_repository_message(buf.raw(), self.raw)); + Ok(str::from_utf8(&buf).unwrap().to_string()) + } + } + + /// Remove the Git merge message. + pub fn remove_message(&self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_repository_message_remove(self.raw)); + Ok(()) + } + } + + /// List all remotes for a given repository + pub fn remotes(&self) -> Result<StringArray, Error> { + let mut arr = raw::git_strarray { + strings: ptr::null_mut(), + count: 0, + }; + unsafe { + try_call!(raw::git_remote_list(&mut arr, self.raw)); + Ok(Binding::from_raw(arr)) + } + } + + /// Get the information for a particular remote + pub fn find_remote(&self, name: &str) -> Result<Remote<'_>, Error> { + let mut ret = ptr::null_mut(); + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_remote_lookup(&mut ret, self.raw, name)); + Ok(Binding::from_raw(ret)) + } + } + + /// Add a remote with the default fetch refspec to the repository's + /// configuration. + pub fn remote(&self, name: &str, url: &str) -> Result<Remote<'_>, Error> { + let mut ret = ptr::null_mut(); + let name = CString::new(name)?; + let url = CString::new(url)?; + unsafe { + try_call!(raw::git_remote_create(&mut ret, self.raw, name, url)); + Ok(Binding::from_raw(ret)) + } + } + + /// Add a remote with the provided fetch refspec to the repository's + /// configuration. + pub fn remote_with_fetch( + &self, + name: &str, + url: &str, + fetch: &str, + ) -> Result<Remote<'_>, Error> { + let mut ret = ptr::null_mut(); + let name = CString::new(name)?; + let url = CString::new(url)?; + let fetch = CString::new(fetch)?; + unsafe { + try_call!(raw::git_remote_create_with_fetchspec( + &mut ret, self.raw, name, url, fetch + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Create an anonymous remote + /// + /// Create a remote with the given URL and refspec in memory. You can use + /// 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<Remote<'_>, Error> { + let mut ret = ptr::null_mut(); + let url = CString::new(url)?; + unsafe { + try_call!(raw::git_remote_create_anonymous(&mut ret, self.raw, url)); + Ok(Binding::from_raw(ret)) + } + } + + /// Give a remote a new name + /// + /// All remote-tracking branches and configuration settings for the remote + /// are updated. + /// + /// A temporary in-memory remote cannot be given a name with this method. + /// + /// No loaded instances of the remote with the old name will change their + /// name or their list of refspecs. + /// + /// The returned array of strings is a list of the non-default refspecs + /// which cannot be renamed and are returned for further processing by the + /// caller. + pub fn remote_rename(&self, name: &str, new_name: &str) -> Result<StringArray, Error> { + let name = CString::new(name)?; + let new_name = CString::new(new_name)?; + let mut problems = raw::git_strarray { + count: 0, + strings: ptr::null_mut(), + }; + unsafe { + try_call!(raw::git_remote_rename( + &mut problems, + self.raw, + name, + new_name + )); + Ok(Binding::from_raw(problems)) + } + } + + /// Delete an existing persisted remote. + /// + /// All remote-tracking branches and configuration settings for the remote + /// will be removed. + pub fn remote_delete(&self, name: &str) -> Result<(), Error> { + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_remote_delete(self.raw, name)); + } + Ok(()) + } + + /// Add a fetch refspec to the remote's configuration + /// + /// Add the given refspec to the fetch list in the configuration. No loaded + /// remote instances will be affected. + pub fn remote_add_fetch(&self, name: &str, spec: &str) -> Result<(), Error> { + let name = CString::new(name)?; + let spec = CString::new(spec)?; + unsafe { + try_call!(raw::git_remote_add_fetch(self.raw, name, spec)); + } + Ok(()) + } + + /// Add a push refspec to the remote's configuration. + /// + /// Add the given refspec to the push list in the configuration. No + /// loaded remote instances will be affected. + pub fn remote_add_push(&self, name: &str, spec: &str) -> Result<(), Error> { + let name = CString::new(name)?; + let spec = CString::new(spec)?; + unsafe { + try_call!(raw::git_remote_add_push(self.raw, name, spec)); + } + Ok(()) + } + + /// Set the remote's URL in the configuration + /// + /// Remote objects already in memory will not be affected. This assumes + /// the common case of a single-url remote and will otherwise return an + /// error. + pub fn remote_set_url(&self, name: &str, url: &str) -> Result<(), Error> { + let name = CString::new(name)?; + let url = CString::new(url)?; + unsafe { + try_call!(raw::git_remote_set_url(self.raw, name, url)); + } + Ok(()) + } + + /// Set the remote's URL for pushing in the configuration. + /// + /// Remote objects already in memory will not be affected. This assumes + /// the common case of a single-url remote and will otherwise return an + /// error. + /// + /// `None` indicates that it should be cleared. + pub fn remote_set_pushurl(&self, name: &str, pushurl: Option<&str>) -> Result<(), Error> { + let name = CString::new(name)?; + let pushurl = crate::opt_cstr(pushurl)?; + unsafe { + try_call!(raw::git_remote_set_pushurl(self.raw, name, pushurl)); + } + Ok(()) + } + + /// Sets the current head to the specified object and optionally resets + /// the index and working tree to match. + /// + /// A soft reset means the head will be moved to the commit. + /// + /// A mixed reset will trigger a soft reset, plus the index will be + /// replaced with the content of the commit tree. + /// + /// A hard reset will trigger a mixed reset and the working directory will + /// be replaced with the content of the index. (Untracked and ignored files + /// will be left alone, however.) + /// + /// The `target` is a commit-ish to which the head should be moved to. The + /// object can either be a commit or a tag, but tags must be dereferenceable + /// to a commit. + /// + /// The `checkout` options will only be used for a hard reset. + pub fn reset( + &self, + target: &Object<'_>, + kind: ResetType, + checkout: Option<&mut CheckoutBuilder<'_>>, + ) -> Result<(), Error> { + unsafe { + let mut opts: raw::git_checkout_options = mem::zeroed(); + try_call!(raw::git_checkout_init_options( + &mut opts, + raw::GIT_CHECKOUT_OPTIONS_VERSION + )); + let opts = checkout.map(|c| { + c.configure(&mut opts); + &mut opts + }); + try_call!(raw::git_reset(self.raw, target.raw(), kind, opts)); + } + Ok(()) + } + + /// Updates some entries in the index from the target commit tree. + /// + /// The scope of the updated entries is determined by the paths being + /// in the iterator provided. + /// + /// Passing a `None` target will result in removing entries in the index + /// matching the provided pathspecs. + pub fn reset_default<T, I>(&self, target: Option<&Object<'_>>, paths: I) -> Result<(), Error> + where + T: IntoCString, + I: IntoIterator<Item = T>, + { + let (_a, _b, mut arr) = crate::util::iter2cstrs_paths(paths)?; + let target = target.map(|t| t.raw()); + unsafe { + try_call!(raw::git_reset_default(self.raw, target, &mut arr)); + } + Ok(()) + } + + /// Retrieve and resolve the reference pointed at by HEAD. + pub fn head(&self) -> Result<Reference<'_>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_repository_head(&mut ret, self.raw)); + Ok(Binding::from_raw(ret)) + } + } + + /// Make the repository HEAD point to the specified reference. + /// + /// 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(&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)); + } + Ok(()) + } + + /// Determines whether the repository HEAD is detached. + pub fn head_detached(&self) -> Result<bool, Error> { + unsafe { + let value = raw::git_repository_head_detached(self.raw); + match value { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(Error::last_error(value).unwrap()), + } + } + } + + /// Make the repository HEAD directly point to the commit. + /// + /// If the provided commitish cannot be found in the repository, the HEAD + /// is unaltered and an error is returned. + /// + /// If the provided commitish cannot be peeled into a commit, the HEAD is + /// unaltered and an error is returned. + /// + /// Otherwise, the HEAD will eventually be detached and will directly point + /// to the peeled commit. + pub fn set_head_detached(&self, commitish: Oid) -> Result<(), Error> { + unsafe { + try_call!(raw::git_repository_set_head_detached( + self.raw, + commitish.raw() + )); + } + Ok(()) + } + + /// Make the repository HEAD directly point to the commit. + /// + /// If the provided commitish cannot be found in the repository, the HEAD + /// is unaltered and an error is returned. + /// If the provided commitish cannot be peeled into a commit, the HEAD is + /// unaltered and an error is returned. + /// Otherwise, the HEAD will eventually be detached and will directly point + /// to the peeled commit. + pub fn set_head_detached_from_annotated( + &self, + commitish: AnnotatedCommit<'_>, + ) -> Result<(), Error> { + unsafe { + try_call!(raw::git_repository_set_head_detached_from_annotated( + self.raw, + commitish.raw() + )); + } + Ok(()) + } + + /// Create an iterator for the repo's references + pub fn references(&self) -> Result<References<'_>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_reference_iterator_new(&mut ret, self.raw)); + Ok(Binding::from_raw(ret)) + } + } + + /// Create an iterator for the repo's references that match the specified + /// glob + pub fn references_glob(&self, glob: &str) -> Result<References<'_>, Error> { + let mut ret = ptr::null_mut(); + let glob = CString::new(glob)?; + unsafe { + try_call!(raw::git_reference_iterator_glob_new( + &mut ret, self.raw, glob + )); + + Ok(Binding::from_raw(ret)) + } + } + + /// Load all submodules for this repository and return them. + pub fn submodules(&self) -> Result<Vec<Submodule<'_>>, Error> { + struct Data<'a, 'b> { + repo: &'b Repository, + ret: &'a mut Vec<Submodule<'b>>, + } + let mut ret = Vec::new(); + + unsafe { + let mut data = Data { + repo: self, + ret: &mut ret, + }; + let cb: raw::git_submodule_cb = Some(append); + try_call!(raw::git_submodule_foreach( + self.raw, + cb, + &mut data as *mut _ as *mut c_void + )); + } + + return Ok(ret); + + extern "C" fn append( + _repo: *mut raw::git_submodule, + name: *const c_char, + data: *mut c_void, + ) -> c_int { + unsafe { + let data = &mut *(data as *mut Data<'_, '_>); + let mut raw = ptr::null_mut(); + let rc = raw::git_submodule_lookup(&mut raw, data.repo.raw(), name); + assert_eq!(rc, 0); + data.ret.push(Binding::from_raw(raw)); + } + 0 + } + } + + /// Gather file status information and populate the returned structure. + /// + /// Note that if a pathspec is given in the options to filter the + /// status, then the results from rename detection (if you enable it) may + /// not be accurate. To do rename detection properly, this must be called + /// with no pathspec so that all files can be considered. + pub fn statuses(&self, options: Option<&mut StatusOptions>) -> Result<Statuses<'_>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_status_list_new( + &mut ret, + self.raw, + options.map(|s| s.raw()).unwrap_or(ptr::null()) + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Test if the ignore rules apply to a given file. + /// + /// This function checks the ignore rules to see if they would apply to the + /// given file. This indicates if the file would be ignored regardless of + /// whether the file is already in the index or committed to the repository. + /// + /// One way to think of this is if you were to do "git add ." on the + /// directory containing the file, would it be added or not? + pub fn status_should_ignore(&self, path: &Path) -> Result<bool, Error> { + let mut ret = 0 as c_int; + let path = util::cstring_to_repo_path(path)?; + unsafe { + try_call!(raw::git_status_should_ignore(&mut ret, self.raw, path)); + } + Ok(ret != 0) + } + + /// Get file status for a single file. + /// + /// This tries to get status for the filename that you give. If no files + /// match that name (in either the HEAD, index, or working directory), this + /// returns NotFound. + /// + /// If the name matches multiple files (for example, if the path names a + /// directory or if running on a case- insensitive filesystem and yet the + /// HEAD has two entries that both match the path), then this returns + /// Ambiguous because it cannot give correct results. + /// + /// This does not do any sort of rename detection. Renames require a set of + /// targets and because of the path filtering, there is not enough + /// information to check renames correctly. To check file status with rename + /// detection, there is no choice but to do a full `statuses` and scan + /// through looking for the path that you are interested in. + pub fn status_file(&self, path: &Path) -> Result<Status, Error> { + let mut ret = 0 as c_uint; + let path = path_to_repo_path(path)?; + unsafe { + try_call!(raw::git_status_file(&mut ret, self.raw, path)); + } + Ok(Status::from_bits_truncate(ret as u32)) + } + + /// Create an iterator which loops over the requested branches. + pub fn branches(&self, filter: Option<BranchType>) -> Result<Branches<'_>, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_branch_iterator_new(&mut raw, self.raw(), filter)); + Ok(Branches::from_raw(raw)) + } + } + + /// Get the Index file for this 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<Index, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_repository_index(&mut raw, self.raw())); + Ok(Binding::from_raw(raw)) + } + } + + /// Set the Index file for this repository. + pub fn set_index(&self, index: &mut Index) -> Result<(), Error> { + unsafe { + try_call!(raw::git_repository_set_index(self.raw(), index.raw())); + } + Ok(()) + } + + /// Get the configuration file for this repository. + /// + /// If a configuration file has not been set, the default config set for the + /// repository will be returned, including global and system configurations + /// (if they are available). + pub fn config(&self) -> Result<Config, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_repository_config(&mut raw, self.raw())); + Ok(Binding::from_raw(raw)) + } + } + + /// Get the value of a git attribute for a path as a string. + /// + /// This function will return a special string if the attribute is set to a special value. + /// Interpreting the special string is discouraged. You should always use + /// [`AttrValue::from_string`](crate::AttrValue::from_string) to interpret the return value + /// and avoid the special string. + /// + /// As such, the return type of this function will probably be changed in the next major version + /// to prevent interpreting the returned string without checking whether it's special. + pub fn get_attr( + &self, + path: &Path, + name: &str, + flags: AttrCheckFlags, + ) -> Result<Option<&str>, Error> { + Ok(self + .get_attr_bytes(path, name, flags)? + .and_then(|a| str::from_utf8(a).ok())) + } + + /// Get the value of a git attribute for a path as a byte slice. + /// + /// This function will return a special byte slice if the attribute is set to a special value. + /// Interpreting the special byte slice is discouraged. You should always use + /// [`AttrValue::from_bytes`](crate::AttrValue::from_bytes) to interpret the return value and + /// avoid the special string. + /// + /// As such, the return type of this function will probably be changed in the next major version + /// to prevent interpreting the returned byte slice without checking whether it's special. + pub fn get_attr_bytes( + &self, + path: &Path, + name: &str, + flags: AttrCheckFlags, + ) -> Result<Option<&[u8]>, Error> { + let mut ret = ptr::null(); + let path = util::cstring_to_repo_path(path)?; + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_attr_get( + &mut ret, + self.raw(), + flags.bits(), + path, + name + )); + Ok(crate::opt_bytes(self, ret)) + } + } + + /// Write an in-memory buffer to the ODB as a blob. + /// + /// The Oid returned can in turn be passed to `find_blob` to get a handle to + /// the blob. + pub fn blob(&self, data: &[u8]) -> Result<Oid, Error> { + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + let ptr = data.as_ptr() as *const c_void; + let len = data.len() as size_t; + try_call!(raw::git_blob_create_frombuffer( + &mut raw, + self.raw(), + ptr, + len + )); + Ok(Binding::from_raw(&raw as *const _)) + } + } + + /// Read a file from the filesystem and write its content to the Object + /// Database as a loose blob + /// + /// The Oid returned can in turn be passed to `find_blob` to get a handle to + /// the blob. + pub fn blob_path(&self, path: &Path) -> Result<Oid, Error> { + // Normal file path OK (does not need Windows conversion). + let path = path.into_c_string()?; + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_blob_create_fromdisk(&mut raw, self.raw(), path)); + Ok(Binding::from_raw(&raw as *const _)) + } + } + + /// Create a stream to write blob + /// + /// This function may need to buffer the data on disk and will in general + /// not be the right choice if you know the size of the data to write. + /// + /// Use `BlobWriter::commit()` to commit the write to the object db + /// and get the object id. + /// + /// If the `hintpath` parameter is filled, it will be used to determine + /// what git filters should be applied to the object before it is written + /// to the object database. + pub fn blob_writer(&self, hintpath: Option<&Path>) -> Result<BlobWriter<'_>, Error> { + let path_str = match hintpath { + Some(path) => Some(path.into_c_string()?), + None => None, + }; + let path = match path_str { + Some(ref path) => path.as_ptr(), + None => ptr::null(), + }; + let mut out = ptr::null_mut(); + unsafe { + try_call!(raw::git_blob_create_fromstream(&mut out, self.raw(), path)); + Ok(BlobWriter::from_raw(out)) + } + } + + /// Lookup a reference to one of the objects in a repository. + pub fn find_blob(&self, oid: Oid) -> Result<Blob<'_>, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_blob_lookup(&mut raw, self.raw(), oid.raw())); + Ok(Binding::from_raw(raw)) + } + } + + /// Get the object database for this repository + pub fn odb(&self) -> Result<Odb<'_>, Error> { + let mut odb = ptr::null_mut(); + unsafe { + try_call!(raw::git_repository_odb(&mut odb, self.raw())); + Ok(Odb::from_raw(odb)) + } + } + + /// Override the object database for this repository + pub fn set_odb(&self, odb: &Odb<'_>) -> Result<(), Error> { + unsafe { + try_call!(raw::git_repository_set_odb(self.raw(), odb.raw())); + } + Ok(()) + } + + /// Create a new branch pointing at a target commit + /// + /// A new direct reference will be created pointing to this target commit. + /// If `force` is true and a reference already exists with the given name, + /// it'll be replaced. + pub fn branch( + &self, + branch_name: &str, + target: &Commit<'_>, + force: bool, + ) -> Result<Branch<'_>, Error> { + let branch_name = CString::new(branch_name)?; + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_branch_create( + &mut raw, + self.raw(), + branch_name, + target.raw(), + force + )); + Ok(Branch::wrap(Binding::from_raw(raw))) + } + } + + /// Create a new branch pointing at a target commit + /// + /// This behaves like `Repository::branch()` but takes + /// an annotated commit, which lets you specify which + /// extended SHA syntax string was specified by a user, + /// allowing for more exact reflog messages. + /// + /// See the documentation for `Repository::branch()` + pub fn branch_from_annotated_commit( + &self, + branch_name: &str, + target: &AnnotatedCommit<'_>, + force: bool, + ) -> Result<Branch<'_>, Error> { + let branch_name = CString::new(branch_name)?; + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_branch_create_from_annotated( + &mut raw, + self.raw(), + branch_name, + target.raw(), + force + )); + Ok(Branch::wrap(Binding::from_raw(raw))) + } + } + + /// Lookup a branch by its name in a repository. + pub fn find_branch(&self, name: &str, branch_type: BranchType) -> Result<Branch<'_>, Error> { + let name = CString::new(name)?; + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_branch_lookup( + &mut ret, + self.raw(), + name, + branch_type + )); + Ok(Branch::wrap(Binding::from_raw(ret))) + } + } + + /// Create new commit in the repository + /// + /// If the `update_ref` is not `None`, name of the reference that will be + /// updated to point to this commit. If the reference is not direct, it will + /// be resolved to a direct reference. Use "HEAD" to update the HEAD of the + /// current branch and make it point to this commit. If the reference + /// doesn't exist yet, it will be created. If it does exist, the first + /// parent must be the tip of this branch. + pub fn commit( + &self, + update_ref: Option<&str>, + author: &Signature<'_>, + committer: &Signature<'_>, + message: &str, + tree: &Tree<'_>, + parents: &[&Commit<'_>], + ) -> Result<Oid, Error> { + let update_ref = crate::opt_cstr(update_ref)?; + let mut parent_ptrs = parents + .iter() + .map(|p| p.raw() as *const raw::git_commit) + .collect::<Vec<_>>(); + let message = CString::new(message)?; + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_commit_create( + &mut raw, + self.raw(), + update_ref, + author.raw(), + committer.raw(), + ptr::null(), + message, + tree.raw(), + parents.len() as size_t, + parent_ptrs.as_mut_ptr() + )); + Ok(Binding::from_raw(&raw as *const _)) + } + } + + /// Create a commit object and return that as a Buf. + /// + /// That can be converted to a string like this `str::from_utf8(&buf).unwrap().to_string()`. + /// And that string can be passed to the `commit_signed` function, + /// the arguments behave the same as in the `commit` function. + pub fn commit_create_buffer( + &self, + author: &Signature<'_>, + committer: &Signature<'_>, + message: &str, + tree: &Tree<'_>, + parents: &[&Commit<'_>], + ) -> Result<Buf, Error> { + let mut parent_ptrs = parents + .iter() + .map(|p| p.raw() as *const raw::git_commit) + .collect::<Vec<_>>(); + let message = CString::new(message)?; + let buf = Buf::new(); + unsafe { + try_call!(raw::git_commit_create_buffer( + buf.raw(), + self.raw(), + author.raw(), + committer.raw(), + ptr::null(), + message, + tree.raw(), + parents.len() as size_t, + parent_ptrs.as_mut_ptr() + )); + Ok(buf) + } + } + + /// Create a commit object from the given buffer and signature + /// + /// Given the unsigned commit object's contents, its signature and the + /// header field in which to store the signature, attach the signature to + /// the commit and write it into the given repository. + /// + /// Use `None` in `signature_field` to use the default of `gpgsig`, which is + /// almost certainly what you want. + /// + /// Returns the resulting (signed) commit id. + pub fn commit_signed( + &self, + commit_content: &str, + signature: &str, + signature_field: Option<&str>, + ) -> Result<Oid, Error> { + let commit_content = CString::new(commit_content)?; + let signature = CString::new(signature)?; + let signature_field = crate::opt_cstr(signature_field)?; + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_commit_create_with_signature( + &mut raw, + self.raw(), + commit_content, + signature, + signature_field + )); + Ok(Binding::from_raw(&raw as *const _)) + } + } + + /// Extract the signature from a commit + /// + /// Returns a tuple containing the signature in the first value and the + /// signed data in the second. + pub fn extract_signature( + &self, + commit_id: &Oid, + signature_field: Option<&str>, + ) -> Result<(Buf, Buf), Error> { + let signature_field = crate::opt_cstr(signature_field)?; + let signature = Buf::new(); + let content = Buf::new(); + unsafe { + try_call!(raw::git_commit_extract_signature( + signature.raw(), + content.raw(), + self.raw(), + commit_id.raw() as *mut _, + signature_field + )); + Ok((signature, content)) + } + } + + /// Lookup a reference to one of the commits in a repository. + pub fn find_commit(&self, oid: Oid) -> Result<Commit<'_>, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_commit_lookup(&mut raw, self.raw(), oid.raw())); + Ok(Binding::from_raw(raw)) + } + } + + /// Creates an `AnnotatedCommit` from the given commit id. + pub fn find_annotated_commit(&self, id: Oid) -> Result<AnnotatedCommit<'_>, Error> { + unsafe { + let mut raw = ptr::null_mut(); + try_call!(raw::git_annotated_commit_lookup( + &mut raw, + self.raw(), + id.raw() + )); + Ok(Binding::from_raw(raw)) + } + } + + /// Lookup a reference to one of the objects in a repository. + pub fn find_object(&self, oid: Oid, kind: Option<ObjectType>) -> Result<Object<'_>, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_object_lookup( + &mut raw, + self.raw(), + oid.raw(), + kind + )); + Ok(Binding::from_raw(raw)) + } + } + + /// Create a new direct reference. + /// + /// This function will return an error if a reference already exists with + /// the given name unless force is true, in which case it will be + /// overwritten. + pub fn reference( + &self, + name: &str, + id: Oid, + force: bool, + log_message: &str, + ) -> Result<Reference<'_>, Error> { + let name = CString::new(name)?; + let log_message = CString::new(log_message)?; + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_reference_create( + &mut raw, + self.raw(), + name, + id.raw(), + force, + log_message + )); + Ok(Binding::from_raw(raw)) + } + } + + /// Conditionally create new direct reference. + /// + /// A direct reference (also called an object id reference) refers directly + /// to a specific object id (a.k.a. OID or SHA) in the repository. The id + /// permanently refers to the object (although the reference itself can be + /// moved). For example, in libgit2 the direct ref "refs/tags/v0.17.0" + /// refers to OID 5b9fac39d8a76b9139667c26a63e6b3f204b3977. + /// + /// The direct reference will be created in the repository and written to + /// the disk. + /// + /// 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. + /// + /// 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. + /// + /// It will return GIT_EMODIFIED if the reference's value at the time of + /// updating does not match the one passed through `current_id` (i.e. if the + /// ref has changed since the user read it). + pub fn reference_matching( + &self, + name: &str, + id: Oid, + force: bool, + current_id: Oid, + log_message: &str, + ) -> Result<Reference<'_>, Error> { + let name = CString::new(name)?; + let log_message = CString::new(log_message)?; + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_reference_create_matching( + &mut raw, + self.raw(), + name, + id.raw(), + force, + current_id.raw(), + log_message + )); + Ok(Binding::from_raw(raw)) + } + } + + /// Create a new symbolic reference. + /// + /// A symbolic reference is a reference name that refers to another + /// reference name. If the other name moves, the symbolic name will move, + /// too. As a simple example, the "HEAD" reference might refer to + /// "refs/heads/master" while on the "master" branch of a repository. + /// + /// Valid reference names must follow one of two patterns: + /// + /// 1. Top-level names must contain only capital letters and underscores, + /// and must begin and end with a letter. (e.g. "HEAD", "ORIG_HEAD"). + /// 2. Names prefixed with "refs/" can be almost anything. You must avoid + /// the characters '~', '^', ':', '\\', '?', '[', and '*', and the + /// sequences ".." and "@{" which have special meaning to revparse. + /// + /// This function will return an error if a reference already exists with + /// the given name unless force is true, in which case it will be + /// overwritten. + pub fn reference_symbolic( + &self, + name: &str, + target: &str, + force: bool, + log_message: &str, + ) -> Result<Reference<'_>, Error> { + let name = CString::new(name)?; + let target = CString::new(target)?; + let log_message = CString::new(log_message)?; + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_reference_symbolic_create( + &mut raw, + self.raw(), + name, + target, + force, + log_message + )); + Ok(Binding::from_raw(raw)) + } + } + + /// Create a new symbolic reference. + /// + /// 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. + /// + /// It will return GIT_EMODIFIED if the reference's value at the time of + /// updating does not match the one passed through current_value (i.e. if + /// the ref has changed since the user read it). + pub fn reference_symbolic_matching( + &self, + name: &str, + target: &str, + force: bool, + current_value: &str, + log_message: &str, + ) -> Result<Reference<'_>, Error> { + let name = CString::new(name)?; + let target = CString::new(target)?; + let current_value = CString::new(current_value)?; + let log_message = CString::new(log_message)?; + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_reference_symbolic_create_matching( + &mut raw, + self.raw(), + name, + target, + force, + current_value, + log_message + )); + Ok(Binding::from_raw(raw)) + } + } + + /// Lookup a reference to one of the objects in a repository. + pub fn find_reference(&self, name: &str) -> Result<Reference<'_>, Error> { + let name = CString::new(name)?; + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_reference_lookup(&mut raw, self.raw(), name)); + Ok(Binding::from_raw(raw)) + } + } + + /// Lookup a reference to one of the objects in a repository. + /// `Repository::find_reference` with teeth; give the method your reference in + /// human-readable format e.g. 'main' instead of 'refs/heads/main', and it + /// will do-what-you-mean, returning the `Reference`. + pub fn resolve_reference_from_short_name(&self, refname: &str) -> Result<Reference<'_>, Error> { + let refname = CString::new(refname)?; + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_reference_dwim(&mut raw, self.raw(), refname)); + Ok(Binding::from_raw(raw)) + } + } + + /// Lookup a reference by name and resolve immediately to OID. + /// + /// This function provides a quick way to resolve a reference name straight + /// through to the object id that it refers to. This avoids having to + /// allocate or free any `Reference` objects for simple situations. + pub fn refname_to_id(&self, name: &str) -> Result<Oid, Error> { + let name = CString::new(name)?; + let mut ret = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_reference_name_to_id(&mut ret, self.raw(), name)); + Ok(Binding::from_raw(&ret as *const _)) + } + } + + /// Creates a git_annotated_commit from the given reference. + pub fn reference_to_annotated_commit( + &self, + reference: &Reference<'_>, + ) -> Result<AnnotatedCommit<'_>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_annotated_commit_from_ref( + &mut ret, + self.raw(), + reference.raw() + )); + Ok(AnnotatedCommit::from_raw(ret)) + } + } + + /// Creates a git_annotated_commit from FETCH_HEAD. + pub fn annotated_commit_from_fetchhead( + &self, + branch_name: &str, + remote_url: &str, + id: &Oid, + ) -> Result<AnnotatedCommit<'_>, Error> { + let branch_name = CString::new(branch_name)?; + let remote_url = CString::new(remote_url)?; + + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_annotated_commit_from_fetchhead( + &mut ret, + self.raw(), + branch_name, + remote_url, + id.raw() + )); + Ok(AnnotatedCommit::from_raw(ret)) + } + } + + /// Create a new action signature with default user and now timestamp. + /// + /// This looks up the user.name and user.email from the configuration and + /// uses the current time as the timestamp, and creates a new signature + /// based on that information. It will return `NotFound` if either the + /// user.name or user.email are not set. + pub fn signature(&self) -> Result<Signature<'static>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_signature_default(&mut ret, self.raw())); + Ok(Binding::from_raw(ret)) + } + } + + /// Set up a new git submodule for checkout. + /// + /// This does "git submodule add" up to the fetch and checkout of the + /// submodule contents. It preps a new submodule, creates an entry in + /// `.gitmodules` and creates an empty initialized repository either at the + /// given path in the working directory or in `.git/modules` with a gitlink + /// from the working directory to the new repo. + /// + /// To fully emulate "git submodule add" call this function, then `open()` + /// the submodule repo and perform the clone step as needed. Lastly, call + /// `add_finalize()` to wrap up adding the new submodule and `.gitmodules` + /// to the index to be ready to commit. + pub fn submodule( + &self, + url: &str, + path: &Path, + use_gitlink: bool, + ) -> Result<Submodule<'_>, Error> { + let url = CString::new(url)?; + let path = path_to_repo_path(path)?; + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_submodule_add_setup( + &mut raw, + self.raw(), + url, + path, + use_gitlink + )); + Ok(Binding::from_raw(raw)) + } + } + + /// Lookup submodule information by name or path. + /// + /// Given either the submodule name or path (they are usually the same), + /// this returns a structure describing the submodule. + pub fn find_submodule(&self, name: &str) -> Result<Submodule<'_>, Error> { + let name = CString::new(name)?; + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_submodule_lookup(&mut raw, self.raw(), name)); + Ok(Binding::from_raw(raw)) + } + } + + /// Get the status for a submodule. + /// + /// This looks at a submodule and tries to determine the status. It + /// will return a combination of the `SubmoduleStatus` values. + pub fn submodule_status( + &self, + name: &str, + ignore: SubmoduleIgnore, + ) -> Result<SubmoduleStatus, Error> { + let mut ret = 0; + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_submodule_status(&mut ret, self.raw, name, ignore)); + } + Ok(SubmoduleStatus::from_bits_truncate(ret as u32)) + } + + /// Set the ignore rule for the submodule in the configuration + /// + /// This does not affect any currently-loaded instances. + pub fn submodule_set_ignore( + &mut self, + name: &str, + ignore: SubmoduleIgnore, + ) -> Result<(), Error> { + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_submodule_set_ignore(self.raw(), name, ignore)); + } + Ok(()) + } + + /// Set the update rule for the submodule in the configuration + /// + /// This setting won't affect any existing instances. + pub fn submodule_set_update( + &mut self, + name: &str, + update: SubmoduleUpdate, + ) -> Result<(), Error> { + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_submodule_set_update(self.raw(), name, update)); + } + Ok(()) + } + + /// Set the URL for the submodule in the configuration + /// + /// After calling this, you may wish to call [`Submodule::sync`] to write + /// the changes to the checked out submodule repository. + pub fn submodule_set_url(&mut self, name: &str, url: &str) -> Result<(), Error> { + let name = CString::new(name)?; + let url = CString::new(url)?; + unsafe { + try_call!(raw::git_submodule_set_url(self.raw(), name, url)); + } + Ok(()) + } + + /// Set the branch for the submodule in the configuration + /// + /// After calling this, you may wish to call [`Submodule::sync`] to write + /// the changes to the checked out submodule repository. + pub fn submodule_set_branch(&mut self, name: &str, branch_name: &str) -> Result<(), Error> { + let name = CString::new(name)?; + let branch_name = CString::new(branch_name)?; + unsafe { + try_call!(raw::git_submodule_set_branch(self.raw(), name, branch_name)); + } + Ok(()) + } + + /// Lookup a reference to one of the objects in a repository. + pub fn find_tree(&self, oid: Oid) -> Result<Tree<'_>, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_tree_lookup(&mut raw, self.raw(), oid.raw())); + Ok(Binding::from_raw(raw)) + } + } + + /// Create a new TreeBuilder, optionally initialized with the + /// entries of the given Tree. + /// + /// The tree builder can be used to create or modify trees in memory and + /// write them as tree objects to the database. + pub fn treebuilder(&self, tree: Option<&Tree<'_>>) -> Result<TreeBuilder<'_>, Error> { + unsafe { + let mut ret = ptr::null_mut(); + let tree = match tree { + Some(tree) => tree.raw(), + None => ptr::null_mut(), + }; + try_call!(raw::git_treebuilder_new(&mut ret, self.raw, tree)); + Ok(Binding::from_raw(ret)) + } + } + + /// Create a new tag in the repository from an object + /// + /// A new reference will also be created pointing to this tag object. If + /// `force` is true and a reference already exists with the given name, + /// it'll be replaced. + /// + /// The message will not be cleaned up. + /// + /// The tag name will be checked for validity. You must avoid the characters + /// '~', '^', ':', ' \ ', '?', '[', and '*', and the sequences ".." and " @ + /// {" which have special meaning to revparse. + pub fn tag( + &self, + name: &str, + target: &Object<'_>, + tagger: &Signature<'_>, + message: &str, + force: bool, + ) -> Result<Oid, Error> { + let name = CString::new(name)?; + let message = CString::new(message)?; + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_tag_create( + &mut raw, + self.raw, + name, + target.raw(), + tagger.raw(), + message, + force + )); + Ok(Binding::from_raw(&raw as *const _)) + } + } + + /// Create a new tag in the repository from an object without creating a reference. + /// + /// The message will not be cleaned up. + /// + /// The tag name will be checked for validity. You must avoid the characters + /// '~', '^', ':', ' \ ', '?', '[', and '*', and the sequences ".." and " @ + /// {" which have special meaning to revparse. + pub fn tag_annotation_create( + &self, + name: &str, + target: &Object<'_>, + tagger: &Signature<'_>, + message: &str, + ) -> Result<Oid, Error> { + let name = CString::new(name)?; + let message = CString::new(message)?; + let mut raw_oid = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_tag_annotation_create( + &mut raw_oid, + self.raw, + name, + target.raw(), + tagger.raw(), + message + )); + Ok(Binding::from_raw(&raw_oid as *const _)) + } + } + + /// Create a new lightweight tag pointing at a target object + /// + /// A new direct reference will be created pointing to this target object. + /// If force is true and a reference already exists with the given name, + /// it'll be replaced. + pub fn tag_lightweight( + &self, + name: &str, + target: &Object<'_>, + force: bool, + ) -> Result<Oid, Error> { + let name = CString::new(name)?; + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_tag_create_lightweight( + &mut raw, + self.raw, + name, + target.raw(), + force + )); + Ok(Binding::from_raw(&raw as *const _)) + } + } + + /// Lookup a tag object from the repository. + pub fn find_tag(&self, id: Oid) -> Result<Tag<'_>, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_tag_lookup(&mut raw, self.raw, id.raw())); + Ok(Binding::from_raw(raw)) + } + } + + /// Delete an existing tag reference. + /// + /// The tag name will be checked for validity, see `tag` for some rules + /// about valid names. + pub fn tag_delete(&self, name: &str) -> Result<(), Error> { + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_tag_delete(self.raw, name)); + Ok(()) + } + } + + /// Get a list with all the tags in the repository. + /// + /// An optional fnmatch pattern can also be specified. + pub fn tag_names(&self, pattern: Option<&str>) -> Result<StringArray, Error> { + let mut arr = raw::git_strarray { + strings: ptr::null_mut(), + count: 0, + }; + unsafe { + match pattern { + Some(s) => { + let s = CString::new(s)?; + try_call!(raw::git_tag_list_match(&mut arr, s, self.raw)); + } + None => { + try_call!(raw::git_tag_list(&mut arr, self.raw)); + } + } + Ok(Binding::from_raw(arr)) + } + } + + /// iterate over all tags calling `cb` on each. + /// the callback is provided the tag id and name + pub fn tag_foreach<T>(&self, cb: T) -> Result<(), Error> + where + T: FnMut(Oid, &[u8]) -> bool, + { + let mut data = TagForeachData { + cb: Box::new(cb) as TagForeachCB<'_>, + }; + + unsafe { + raw::git_tag_foreach( + self.raw, + Some(tag_foreach_cb), + (&mut data) as *mut _ as *mut _, + ); + } + Ok(()) + } + + /// Updates files in the index and the working tree to match the content of + /// the commit pointed at by HEAD. + pub fn checkout_head(&self, opts: Option<&mut CheckoutBuilder<'_>>) -> Result<(), Error> { + unsafe { + let mut raw_opts = mem::zeroed(); + try_call!(raw::git_checkout_init_options( + &mut raw_opts, + raw::GIT_CHECKOUT_OPTIONS_VERSION + )); + if let Some(c) = opts { + c.configure(&mut raw_opts); + } + + try_call!(raw::git_checkout_head(self.raw, &raw_opts)); + } + Ok(()) + } + + /// Updates files in the working tree to match the content of the index. + /// + /// If the index is `None`, the repository's index will be used. + pub fn checkout_index( + &self, + index: Option<&mut Index>, + opts: Option<&mut CheckoutBuilder<'_>>, + ) -> Result<(), Error> { + unsafe { + let mut raw_opts = mem::zeroed(); + try_call!(raw::git_checkout_init_options( + &mut raw_opts, + raw::GIT_CHECKOUT_OPTIONS_VERSION + )); + if let Some(c) = opts { + c.configure(&mut raw_opts); + } + + try_call!(raw::git_checkout_index( + self.raw, + index.map(|i| &mut *i.raw()), + &raw_opts + )); + } + Ok(()) + } + + /// Updates files in the index and working tree to match the content of the + /// tree pointed at by the treeish. + pub fn checkout_tree( + &self, + treeish: &Object<'_>, + opts: Option<&mut CheckoutBuilder<'_>>, + ) -> Result<(), Error> { + unsafe { + let mut raw_opts = mem::zeroed(); + try_call!(raw::git_checkout_init_options( + &mut raw_opts, + raw::GIT_CHECKOUT_OPTIONS_VERSION + )); + if let Some(c) = opts { + c.configure(&mut raw_opts); + } + + try_call!(raw::git_checkout_tree(self.raw, &*treeish.raw(), &raw_opts)); + } + Ok(()) + } + + /// Merges the given commit(s) into HEAD, writing the results into the + /// working directory. Any changes are staged for commit and any conflicts + /// are written to the index. Callers should inspect the repository's index + /// after this completes, resolve any conflicts and prepare a commit. + /// + /// For compatibility with git, the repository is put into a merging state. + /// Once the commit is done (or if the user wishes to abort), you should + /// clear this state by calling cleanup_state(). + pub fn merge( + &self, + annotated_commits: &[&AnnotatedCommit<'_>], + merge_opts: Option<&mut MergeOptions>, + checkout_opts: Option<&mut CheckoutBuilder<'_>>, + ) -> Result<(), Error> { + unsafe { + let mut raw_checkout_opts = mem::zeroed(); + try_call!(raw::git_checkout_init_options( + &mut raw_checkout_opts, + raw::GIT_CHECKOUT_OPTIONS_VERSION + )); + if let Some(c) = checkout_opts { + c.configure(&mut raw_checkout_opts); + } + + let mut commit_ptrs = annotated_commits + .iter() + .map(|c| c.raw() as *const raw::git_annotated_commit) + .collect::<Vec<_>>(); + + try_call!(raw::git_merge( + self.raw, + commit_ptrs.as_mut_ptr(), + annotated_commits.len() as size_t, + merge_opts.map(|o| o.raw()).unwrap_or(ptr::null()), + &raw_checkout_opts + )); + } + Ok(()) + } + + /// Merge two commits, producing an index that reflects the result of + /// the merge. The index may be written as-is to the working directory or + /// checked out. If the index is to be converted to a tree, the caller + /// should resolve any conflicts that arose as part of the merge. + pub fn merge_commits( + &self, + our_commit: &Commit<'_>, + their_commit: &Commit<'_>, + opts: Option<&MergeOptions>, + ) -> Result<Index, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_merge_commits( + &mut raw, + self.raw, + our_commit.raw(), + their_commit.raw(), + opts.map(|o| o.raw()) + )); + Ok(Binding::from_raw(raw)) + } + } + + /// Merge two trees, producing an index that reflects the result of + /// the merge. The index may be written as-is to the working directory or + /// checked out. If the index is to be converted to a tree, the caller + /// should resolve any conflicts that arose as part of the merge. + pub fn merge_trees( + &self, + ancestor_tree: &Tree<'_>, + our_tree: &Tree<'_>, + their_tree: &Tree<'_>, + opts: Option<&MergeOptions>, + ) -> Result<Index, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_merge_trees( + &mut raw, + self.raw, + ancestor_tree.raw(), + our_tree.raw(), + their_tree.raw(), + opts.map(|o| o.raw()) + )); + Ok(Binding::from_raw(raw)) + } + } + + /// Remove all the metadata associated with an ongoing command like merge, + /// revert, cherry-pick, etc. For example: MERGE_HEAD, MERGE_MSG, etc. + pub fn cleanup_state(&self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_repository_state_cleanup(self.raw)); + } + Ok(()) + } + + /// Analyzes the given branch(es) and determines the opportunities for + /// merging them into the HEAD of the repository. + pub fn merge_analysis( + &self, + their_heads: &[&AnnotatedCommit<'_>], + ) -> Result<(MergeAnalysis, MergePreference), Error> { + unsafe { + let mut raw_merge_analysis = 0 as raw::git_merge_analysis_t; + let mut raw_merge_preference = 0 as raw::git_merge_preference_t; + let mut their_heads = their_heads + .iter() + .map(|v| v.raw() as *const _) + .collect::<Vec<_>>(); + try_call!(raw::git_merge_analysis( + &mut raw_merge_analysis, + &mut raw_merge_preference, + self.raw, + their_heads.as_mut_ptr() as *mut _, + their_heads.len() + )); + Ok(( + MergeAnalysis::from_bits_truncate(raw_merge_analysis as u32), + MergePreference::from_bits_truncate(raw_merge_preference as u32), + )) + } + } + + /// Analyzes the given branch(es) and determines the opportunities for + /// merging them into a reference. + pub fn merge_analysis_for_ref( + &self, + our_ref: &Reference<'_>, + their_heads: &[&AnnotatedCommit<'_>], + ) -> Result<(MergeAnalysis, MergePreference), Error> { + unsafe { + let mut raw_merge_analysis = 0 as raw::git_merge_analysis_t; + let mut raw_merge_preference = 0 as raw::git_merge_preference_t; + let mut their_heads = their_heads + .iter() + .map(|v| v.raw() as *const _) + .collect::<Vec<_>>(); + try_call!(raw::git_merge_analysis_for_ref( + &mut raw_merge_analysis, + &mut raw_merge_preference, + self.raw, + our_ref.raw(), + their_heads.as_mut_ptr() as *mut _, + their_heads.len() + )); + Ok(( + MergeAnalysis::from_bits_truncate(raw_merge_analysis as u32), + MergePreference::from_bits_truncate(raw_merge_preference as u32), + )) + } + } + + /// Initializes a rebase operation to rebase the changes in `branch` + /// relative to `upstream` onto another branch. To begin the rebase process, + /// call `next()`. + pub fn rebase( + &self, + branch: Option<&AnnotatedCommit<'_>>, + upstream: Option<&AnnotatedCommit<'_>>, + onto: Option<&AnnotatedCommit<'_>>, + opts: Option<&mut RebaseOptions<'_>>, + ) -> Result<Rebase<'_>, Error> { + let mut rebase: *mut raw::git_rebase = ptr::null_mut(); + unsafe { + try_call!(raw::git_rebase_init( + &mut rebase, + self.raw(), + branch.map(|c| c.raw()), + upstream.map(|c| c.raw()), + onto.map(|c| c.raw()), + opts.map(|o| o.raw()).unwrap_or(ptr::null()) + )); + + Ok(Rebase::from_raw(rebase)) + } + } + + /// Opens an existing rebase that was previously started by either an + /// invocation of `rebase()` or by another client. + pub fn open_rebase(&self, opts: Option<&mut RebaseOptions<'_>>) -> Result<Rebase<'_>, Error> { + let mut rebase: *mut raw::git_rebase = ptr::null_mut(); + unsafe { + try_call!(raw::git_rebase_open( + &mut rebase, + self.raw(), + opts.map(|o| o.raw()).unwrap_or(ptr::null()) + )); + Ok(Rebase::from_raw(rebase)) + } + } + + /// Add a note for an object + /// + /// The `notes_ref` argument is the canonical name of the reference to use, + /// defaulting to "refs/notes/commits". If `force` is specified then + /// previous notes are overwritten. + pub fn note( + &self, + author: &Signature<'_>, + committer: &Signature<'_>, + notes_ref: Option<&str>, + oid: Oid, + note: &str, + force: bool, + ) -> Result<Oid, Error> { + let notes_ref = crate::opt_cstr(notes_ref)?; + let note = CString::new(note)?; + let mut ret = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_note_create( + &mut ret, + self.raw, + notes_ref, + author.raw(), + committer.raw(), + oid.raw(), + note, + force + )); + Ok(Binding::from_raw(&ret as *const _)) + } + } + + /// Get the default notes reference for this repository + pub fn note_default_ref(&self) -> Result<String, Error> { + let ret = Buf::new(); + unsafe { + try_call!(raw::git_note_default_ref(ret.raw(), self.raw)); + } + Ok(str::from_utf8(&ret).unwrap().to_string()) + } + + /// Creates a new iterator for notes in this repository. + /// + /// The `notes_ref` argument is the canonical name of the reference to use, + /// defaulting to "refs/notes/commits". + /// + /// The iterator returned yields pairs of (Oid, Oid) where the first element + /// is the id of the note and the second id is the id the note is + /// annotating. + pub fn notes(&self, notes_ref: Option<&str>) -> Result<Notes<'_>, Error> { + let notes_ref = crate::opt_cstr(notes_ref)?; + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_note_iterator_new(&mut ret, self.raw, notes_ref)); + Ok(Binding::from_raw(ret)) + } + } + + /// Read the note for an object. + /// + /// The `notes_ref` argument is the canonical name of the reference to use, + /// defaulting to "refs/notes/commits". + /// + /// The id specified is the Oid of the git object to read the note from. + pub fn find_note(&self, notes_ref: Option<&str>, id: Oid) -> Result<Note<'_>, Error> { + let notes_ref = crate::opt_cstr(notes_ref)?; + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_note_read(&mut ret, self.raw, notes_ref, id.raw())); + Ok(Binding::from_raw(ret)) + } + } + + /// Remove the note for an object. + /// + /// The `notes_ref` argument is the canonical name of the reference to use, + /// defaulting to "refs/notes/commits". + /// + /// The id specified is the Oid of the git object to remove the note from. + pub fn note_delete( + &self, + id: Oid, + notes_ref: Option<&str>, + author: &Signature<'_>, + committer: &Signature<'_>, + ) -> Result<(), Error> { + let notes_ref = crate::opt_cstr(notes_ref)?; + unsafe { + try_call!(raw::git_note_remove( + self.raw, + notes_ref, + author.raw(), + committer.raw(), + id.raw() + )); + Ok(()) + } + } + + /// Create a revwalk that can be used to traverse the commit graph. + pub fn revwalk(&self) -> Result<Revwalk<'_>, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_revwalk_new(&mut raw, self.raw())); + Ok(Binding::from_raw(raw)) + } + } + + /// Get the blame for a single file. + pub fn blame_file( + &self, + path: &Path, + opts: Option<&mut BlameOptions>, + ) -> Result<Blame<'_>, Error> { + let path = path_to_repo_path(path)?; + let mut raw = ptr::null_mut(); + + unsafe { + try_call!(raw::git_blame_file( + &mut raw, + self.raw(), + path, + opts.map(|s| s.raw()) + )); + Ok(Binding::from_raw(raw)) + } + } + + /// Find a merge base between two commits + pub fn merge_base(&self, one: Oid, two: Oid) -> Result<Oid, Error> { + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_merge_base( + &mut raw, + self.raw, + one.raw(), + two.raw() + )); + Ok(Binding::from_raw(&raw as *const _)) + } + } + + /// Find a merge base given a list of commits + pub fn merge_base_many(&self, oids: &[Oid]) -> Result<Oid, Error> { + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + + unsafe { + try_call!(raw::git_merge_base_many( + &mut raw, + self.raw, + oids.len() as size_t, + oids.as_ptr() as *const raw::git_oid + )); + Ok(Binding::from_raw(&raw as *const _)) + } + } + + /// Find all merge bases between two commits + pub fn merge_bases(&self, one: Oid, two: Oid) -> Result<OidArray, Error> { + let mut arr = raw::git_oidarray { + ids: ptr::null_mut(), + count: 0, + }; + unsafe { + try_call!(raw::git_merge_bases( + &mut arr, + self.raw, + one.raw(), + two.raw() + )); + Ok(Binding::from_raw(arr)) + } + } + + /// Find all merge bases given a list of commits + pub fn merge_bases_many(&self, oids: &[Oid]) -> Result<OidArray, Error> { + let mut arr = raw::git_oidarray { + ids: ptr::null_mut(), + count: 0, + }; + unsafe { + try_call!(raw::git_merge_bases_many( + &mut arr, + self.raw, + oids.len() as size_t, + oids.as_ptr() as *const raw::git_oid + )); + Ok(Binding::from_raw(arr)) + } + } + + /// Count the number of unique commits between two commit objects + /// + /// There is no need for branches containing the commits to have any + /// upstream relationship, but it helps to think of one as a branch and the + /// other as its upstream, the ahead and behind values will be what git + /// would report for the branches. + pub fn graph_ahead_behind(&self, local: Oid, upstream: Oid) -> Result<(usize, usize), Error> { + unsafe { + let mut ahead: size_t = 0; + let mut behind: size_t = 0; + try_call!(raw::git_graph_ahead_behind( + &mut ahead, + &mut behind, + self.raw(), + local.raw(), + upstream.raw() + )); + Ok((ahead as usize, behind as usize)) + } + } + + /// 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<bool, Error> { + unsafe { + let rv = try_call!(raw::git_graph_descendant_of( + self.raw(), + commit.raw(), + ancestor.raw() + )); + Ok(rv != 0) + } + } + + /// Read the reflog for the given reference + /// + /// If there is no reflog file for the given reference yet, an empty reflog + /// object will be returned. + pub fn reflog(&self, name: &str) -> Result<Reflog, Error> { + let name = CString::new(name)?; + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_reflog_read(&mut ret, self.raw, name)); + Ok(Binding::from_raw(ret)) + } + } + + /// Delete the reflog for the given reference + pub fn reflog_delete(&self, name: &str) -> Result<(), Error> { + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_reflog_delete(self.raw, name)); + } + Ok(()) + } + + /// Rename a reflog + /// + /// The reflog to be renamed is expected to already exist. + pub fn reflog_rename(&self, old_name: &str, new_name: &str) -> Result<(), Error> { + let old_name = CString::new(old_name)?; + let new_name = CString::new(new_name)?; + unsafe { + try_call!(raw::git_reflog_rename(self.raw, old_name, new_name)); + } + Ok(()) + } + + /// Check if the given reference has a reflog. + pub fn reference_has_log(&self, name: &str) -> Result<bool, Error> { + let name = CString::new(name)?; + let ret = unsafe { try_call!(raw::git_reference_has_log(self.raw, name)) }; + Ok(ret != 0) + } + + /// Ensure that the given reference has a reflog. + pub fn reference_ensure_log(&self, name: &str) -> Result<(), Error> { + let name = CString::new(name)?; + unsafe { + try_call!(raw::git_reference_ensure_log(self.raw, name)); + } + Ok(()) + } + + /// Describes a commit + /// + /// Performs a describe operation on the current commit and the worktree. + /// After performing a describe on HEAD, a status is run and description is + /// considered to be dirty if there are. + pub fn describe(&self, opts: &DescribeOptions) -> Result<Describe<'_>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_describe_workdir(&mut ret, self.raw, opts.raw())); + Ok(Binding::from_raw(ret)) + } + } + + /// Directly run a diff on two blobs. + /// + /// Compared to a file, a blob lacks some contextual information. As such, the + /// `DiffFile` given to the callback will have some fake data; i.e. mode will be + /// 0 and path will be `None`. + /// + /// `None` is allowed for either `old_blob` or `new_blob` and will be treated + /// as an empty blob, with the oid set to zero in the `DiffFile`. Passing `None` + /// for both blobs is a noop; no callbacks will be made at all. + /// + /// We do run a binary content check on the blob content and if either blob looks + /// like binary data, the `DiffFile` binary attribute will be set to 1 and no call to + /// the `hunk_cb` nor `line_cb` will be made (unless you set the `force_text` + /// option). + pub fn diff_blobs( + &self, + old_blob: Option<&Blob<'_>>, + old_as_path: Option<&str>, + new_blob: Option<&Blob<'_>>, + new_as_path: Option<&str>, + opts: Option<&mut DiffOptions>, + file_cb: Option<&mut FileCb<'_>>, + binary_cb: Option<&mut BinaryCb<'_>>, + hunk_cb: Option<&mut HunkCb<'_>>, + line_cb: Option<&mut LineCb<'_>>, + ) -> Result<(), Error> { + let old_as_path = crate::opt_cstr(old_as_path)?; + let new_as_path = crate::opt_cstr(new_as_path)?; + let mut cbs = DiffCallbacks { + file: file_cb, + binary: binary_cb, + hunk: hunk_cb, + line: line_cb, + }; + let ptr = &mut cbs as *mut _; + unsafe { + let file_cb_c: raw::git_diff_file_cb = if cbs.file.is_some() { + Some(file_cb_c) + } else { + None + }; + let binary_cb_c: raw::git_diff_binary_cb = if cbs.binary.is_some() { + Some(binary_cb_c) + } else { + None + }; + let hunk_cb_c: raw::git_diff_hunk_cb = if cbs.hunk.is_some() { + Some(hunk_cb_c) + } else { + None + }; + let line_cb_c: raw::git_diff_line_cb = if cbs.line.is_some() { + Some(line_cb_c) + } else { + None + }; + try_call!(raw::git_diff_blobs( + old_blob.map(|s| s.raw()), + old_as_path, + new_blob.map(|s| s.raw()), + new_as_path, + opts.map(|s| s.raw()), + file_cb_c, + binary_cb_c, + hunk_cb_c, + line_cb_c, + ptr as *mut _ + )); + Ok(()) + } + } + + /// Create a diff with the difference between two tree objects. + /// + /// This is equivalent to `git diff <old-tree> <new-tree>` + /// + /// The first tree will be used for the "old_file" side of the delta and the + /// second tree will be used for the "new_file" side of the delta. You can + /// pass `None` to indicate an empty tree, although it is an error to pass + /// `None` for both the `old_tree` and `new_tree`. + pub fn diff_tree_to_tree( + &self, + old_tree: Option<&Tree<'_>>, + new_tree: Option<&Tree<'_>>, + opts: Option<&mut DiffOptions>, + ) -> Result<Diff<'_>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_diff_tree_to_tree( + &mut ret, + self.raw(), + old_tree.map(|s| s.raw()), + new_tree.map(|s| s.raw()), + opts.map(|s| s.raw()) + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Create a diff between a tree and repository index. + /// + /// This is equivalent to `git diff --cached <treeish>` or if you pass + /// the HEAD tree, then like `git diff --cached`. + /// + /// The tree you pass will be used for the "old_file" side of the delta, and + /// the index will be used for the "new_file" side of the delta. + /// + /// If you pass `None` for the index, then the existing index of the `repo` + /// will be used. In this case, the index will be refreshed from disk + /// (if it has changed) before the diff is generated. + /// + /// If the tree is `None`, then it is considered an empty tree. + pub fn diff_tree_to_index( + &self, + old_tree: Option<&Tree<'_>>, + index: Option<&Index>, + opts: Option<&mut DiffOptions>, + ) -> Result<Diff<'_>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_diff_tree_to_index( + &mut ret, + self.raw(), + old_tree.map(|s| s.raw()), + index.map(|s| s.raw()), + opts.map(|s| s.raw()) + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Create a diff between two index objects. + /// + /// The first index will be used for the "old_file" side of the delta, and + /// the second index will be used for the "new_file" side of the delta. + pub fn diff_index_to_index( + &self, + old_index: &Index, + new_index: &Index, + opts: Option<&mut DiffOptions>, + ) -> Result<Diff<'_>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_diff_index_to_index( + &mut ret, + self.raw(), + old_index.raw(), + new_index.raw(), + opts.map(|s| s.raw()) + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Create a diff between the repository index and the workdir directory. + /// + /// This matches the `git diff` command. See the note below on + /// `tree_to_workdir` for a discussion of the difference between + /// `git diff` and `git diff HEAD` and how to emulate a `git diff <treeish>` + /// using libgit2. + /// + /// The index will be used for the "old_file" side of the delta, and the + /// working directory will be used for the "new_file" side of the delta. + /// + /// If you pass `None` for the index, then the existing index of the `repo` + /// will be used. In this case, the index will be refreshed from disk + /// (if it has changed) before the diff is generated. + pub fn diff_index_to_workdir( + &self, + index: Option<&Index>, + opts: Option<&mut DiffOptions>, + ) -> Result<Diff<'_>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_diff_index_to_workdir( + &mut ret, + self.raw(), + index.map(|s| s.raw()), + opts.map(|s| s.raw()) + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Create a diff between a tree and the working directory. + /// + /// The tree you provide will be used for the "old_file" side of the delta, + /// and the working directory will be used for the "new_file" side. + /// + /// This is not the same as `git diff <treeish>` or `git diff-index + /// <treeish>`. Those commands use information from the index, whereas this + /// function strictly returns the differences between the tree and the files + /// in the working directory, regardless of the state of the index. Use + /// `tree_to_workdir_with_index` to emulate those commands. + /// + /// To see difference between this and `tree_to_workdir_with_index`, + /// consider the example of a staged file deletion where the file has then + /// been put back into the working dir and further modified. The + /// tree-to-workdir diff for that file is 'modified', but `git diff` would + /// show status 'deleted' since there is a staged delete. + /// + /// If `None` is passed for `tree`, then an empty tree is used. + pub fn diff_tree_to_workdir( + &self, + old_tree: Option<&Tree<'_>>, + opts: Option<&mut DiffOptions>, + ) -> Result<Diff<'_>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_diff_tree_to_workdir( + &mut ret, + self.raw(), + old_tree.map(|s| s.raw()), + opts.map(|s| s.raw()) + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Create a diff between a tree and the working directory using index data + /// to account for staged deletes, tracked files, etc. + /// + /// This emulates `git diff <tree>` by diffing the tree to the index and + /// the index to the working directory and blending the results into a + /// single diff that includes staged deleted, etc. + pub fn diff_tree_to_workdir_with_index( + &self, + old_tree: Option<&Tree<'_>>, + opts: Option<&mut DiffOptions>, + ) -> Result<Diff<'_>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_diff_tree_to_workdir_with_index( + &mut ret, + self.raw(), + old_tree.map(|s| s.raw()), + opts.map(|s| s.raw()) + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Create a PackBuilder + pub fn packbuilder(&self) -> Result<PackBuilder<'_>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_packbuilder_new(&mut ret, self.raw())); + Ok(Binding::from_raw(ret)) + } + } + + /// Save the local modifications to a new stash. + pub fn stash_save( + &mut self, + stasher: &Signature<'_>, + message: &str, + flags: Option<StashFlags>, + ) -> Result<Oid, Error> { + self.stash_save2(stasher, Some(message), flags) + } + + /// Save the local modifications to a new stash. + /// unlike `stash_save` it allows to pass a null `message` + pub fn stash_save2( + &mut self, + stasher: &Signature<'_>, + message: Option<&str>, + flags: Option<StashFlags>, + ) -> Result<Oid, Error> { + unsafe { + let mut raw_oid = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + let message = crate::opt_cstr(message)?; + let flags = flags.unwrap_or_else(StashFlags::empty); + try_call!(raw::git_stash_save( + &mut raw_oid, + self.raw(), + stasher.raw(), + message, + flags.bits() as c_uint + )); + Ok(Binding::from_raw(&raw_oid as *const _)) + } + } + + /// Like `stash_save` but with more options like selective statshing via path patterns. + pub fn stash_save_ext( + &mut self, + opts: Option<&mut StashSaveOptions<'_>>, + ) -> Result<Oid, Error> { + unsafe { + let mut raw_oid = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + let opts = opts.map(|opts| opts.raw()); + try_call!(raw::git_stash_save_with_opts( + &mut raw_oid, + self.raw(), + opts + )); + Ok(Binding::from_raw(&raw_oid as *const _)) + } + } + + /// Apply a single stashed state from the stash list. + pub fn stash_apply( + &mut self, + index: usize, + opts: Option<&mut StashApplyOptions<'_>>, + ) -> Result<(), Error> { + unsafe { + let opts = opts.map(|opts| opts.raw()); + try_call!(raw::git_stash_apply(self.raw(), index, opts)); + Ok(()) + } + } + + /// Loop over all the stashed states and issue a callback for each one. + /// + /// Return `true` to continue iterating or `false` to stop. + pub fn stash_foreach<C>(&mut self, mut callback: C) -> Result<(), Error> + where + C: FnMut(usize, &str, &Oid) -> bool, + { + unsafe { + let mut data = StashCbData { + callback: &mut callback, + }; + let cb: raw::git_stash_cb = Some(stash_cb); + try_call!(raw::git_stash_foreach( + self.raw(), + cb, + &mut data as *mut _ as *mut _ + )); + Ok(()) + } + } + + /// Remove a single stashed state from the stash list. + pub fn stash_drop(&mut self, index: usize) -> Result<(), Error> { + unsafe { + try_call!(raw::git_stash_drop(self.raw(), index)); + Ok(()) + } + } + + /// Apply a single stashed state from the stash list and remove it from the list if successful. + pub fn stash_pop( + &mut self, + index: usize, + opts: Option<&mut StashApplyOptions<'_>>, + ) -> Result<(), Error> { + unsafe { + let opts = opts.map(|opts| opts.raw()); + try_call!(raw::git_stash_pop(self.raw(), index, opts)); + Ok(()) + } + } + + /// Add ignore rules for a repository. + /// + /// The format of the rules is the same one of the .gitignore file. + pub fn add_ignore_rule(&self, rules: &str) -> Result<(), Error> { + let rules = CString::new(rules)?; + unsafe { + try_call!(raw::git_ignore_add_rule(self.raw, rules)); + } + Ok(()) + } + + /// Clear ignore rules that were explicitly added. + pub fn clear_ignore_rules(&self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_ignore_clear_internal_rules(self.raw)); + } + Ok(()) + } + + /// Test if the ignore rules apply to a given path. + pub fn is_path_ignored<P: AsRef<Path>>(&self, path: P) -> Result<bool, Error> { + let path = util::cstring_to_repo_path(path.as_ref())?; + let mut ignored: c_int = 0; + unsafe { + try_call!(raw::git_ignore_path_is_ignored( + &mut ignored, + self.raw, + path + )); + } + Ok(ignored == 1) + } + + /// Perform a cherrypick + pub fn cherrypick( + &self, + commit: &Commit<'_>, + options: Option<&mut CherrypickOptions<'_>>, + ) -> Result<(), Error> { + let raw_opts = options.map(|o| o.raw()); + let ptr_raw_opts = match raw_opts.as_ref() { + Some(v) => v, + None => std::ptr::null(), + }; + unsafe { + try_call!(raw::git_cherrypick(self.raw(), commit.raw(), ptr_raw_opts)); + + Ok(()) + } + } + + /// Create an index of uncommitted changes, representing the result of + /// cherry-picking. + pub fn cherrypick_commit( + &self, + cherrypick_commit: &Commit<'_>, + our_commit: &Commit<'_>, + mainline: u32, + options: Option<&MergeOptions>, + ) -> Result<Index, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_cherrypick_commit( + &mut ret, + self.raw(), + cherrypick_commit.raw(), + our_commit.raw(), + mainline, + options.map(|o| o.raw()) + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Find the remote name of a remote-tracking branch + pub fn branch_remote_name(&self, refname: &str) -> Result<Buf, Error> { + let refname = CString::new(refname)?; + unsafe { + let buf = Buf::new(); + try_call!(raw::git_branch_remote_name(buf.raw(), self.raw, refname)); + Ok(buf) + } + } + + /// Retrieves the name of the reference supporting the remote tracking branch, + /// given the name of a local branch reference. + pub fn branch_upstream_name(&self, refname: &str) -> Result<Buf, Error> { + let refname = CString::new(refname)?; + unsafe { + let buf = Buf::new(); + try_call!(raw::git_branch_upstream_name(buf.raw(), self.raw, refname)); + Ok(buf) + } + } + + /// Retrieve the name of the upstream remote of a local branch. + pub fn branch_upstream_remote(&self, refname: &str) -> Result<Buf, Error> { + let refname = CString::new(refname)?; + unsafe { + let buf = Buf::new(); + try_call!(raw::git_branch_upstream_remote( + buf.raw(), + self.raw, + refname + )); + Ok(buf) + } + } + + /// Apply a Diff to the given repo, making changes directly in the working directory, the index, or both. + pub fn apply( + &self, + diff: &Diff<'_>, + location: ApplyLocation, + options: Option<&mut ApplyOptions<'_>>, + ) -> Result<(), Error> { + unsafe { + try_call!(raw::git_apply( + self.raw, + diff.raw(), + location.raw(), + options.map(|s| s.raw()).unwrap_or(ptr::null()) + )); + + Ok(()) + } + } + + /// Apply a Diff to the provided tree, and return the resulting Index. + pub fn apply_to_tree( + &self, + tree: &Tree<'_>, + diff: &Diff<'_>, + options: Option<&mut ApplyOptions<'_>>, + ) -> Result<Index, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_apply_to_tree( + &mut ret, + self.raw, + tree.raw(), + diff.raw(), + options.map(|s| s.raw()).unwrap_or(ptr::null()) + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Reverts the given commit, producing changes in the index and working directory. + pub fn revert( + &self, + commit: &Commit<'_>, + options: Option<&mut RevertOptions<'_>>, + ) -> Result<(), Error> { + let raw_opts = options.map(|o| o.raw()); + let ptr_raw_opts = match raw_opts.as_ref() { + Some(v) => v, + None => 0 as *const _, + }; + unsafe { + try_call!(raw::git_revert(self.raw(), commit.raw(), ptr_raw_opts)); + Ok(()) + } + } + + /// Reverts the given commit against the given "our" commit, + /// producing an index that reflects the result of the revert. + pub fn revert_commit( + &self, + revert_commit: &Commit<'_>, + our_commit: &Commit<'_>, + mainline: u32, + options: Option<&MergeOptions>, + ) -> Result<Index, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_revert_commit( + &mut ret, + self.raw(), + revert_commit.raw(), + our_commit.raw(), + mainline, + options.map(|o| o.raw()) + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Lists all the worktrees for the repository + pub fn worktrees(&self) -> Result<StringArray, Error> { + let mut arr = raw::git_strarray { + strings: ptr::null_mut(), + count: 0, + }; + unsafe { + try_call!(raw::git_worktree_list(&mut arr, self.raw)); + Ok(Binding::from_raw(arr)) + } + } + + /// Opens a worktree by name for the given repository + /// + /// This can open any worktree that the worktrees method returns. + pub fn find_worktree(&self, name: &str) -> Result<Worktree, Error> { + let mut raw = ptr::null_mut(); + let raw_name = CString::new(name)?; + unsafe { + try_call!(raw::git_worktree_lookup(&mut raw, self.raw, raw_name)); + Ok(Binding::from_raw(raw)) + } + } + + /// Creates a new worktree for the repository + pub fn worktree<'a>( + &'a self, + name: &str, + path: &Path, + opts: Option<&WorktreeAddOptions<'a>>, + ) -> Result<Worktree, Error> { + let mut raw = ptr::null_mut(); + let raw_name = CString::new(name)?; + let raw_path = path.into_c_string()?; + + unsafe { + try_call!(raw::git_worktree_add( + &mut raw, + self.raw, + raw_name, + raw_path, + opts.map(|o| o.raw()) + )); + Ok(Binding::from_raw(raw)) + } + } + + /// Create a new transaction + pub fn transaction<'a>(&'a self) -> Result<Transaction<'a>, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_transaction_new(&mut raw, self.raw)); + Ok(Binding::from_raw(raw)) + } + } + + /// Gets this repository's mailmap. + pub fn mailmap(&self) -> Result<Mailmap, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_mailmap_from_repository(&mut ret, self.raw)); + Ok(Binding::from_raw(ret)) + } + } + + /// If a merge is in progress, invoke 'callback' for each commit ID in the + /// MERGE_HEAD file. + pub fn mergehead_foreach<C>(&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<C>(&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 { + type Raw = *mut raw::git_repository; + unsafe fn from_raw(ptr: *mut raw::git_repository) -> Repository { + Repository { raw: ptr } + } + fn raw(&self) -> *mut raw::git_repository { + self.raw + } +} + +impl Drop for Repository { + fn drop(&mut self) { + unsafe { raw::git_repository_free(self.raw) } + } +} + +impl RepositoryInitOptions { + /// Creates a default set of initialization options. + /// + /// By default this will set flags for creating all necessary directories + /// and initializing a directory from the user-configured templates path. + pub fn new() -> RepositoryInitOptions { + RepositoryInitOptions { + flags: raw::GIT_REPOSITORY_INIT_MKDIR as u32 + | raw::GIT_REPOSITORY_INIT_MKPATH as u32 + | raw::GIT_REPOSITORY_INIT_EXTERNAL_TEMPLATE as u32, + mode: 0, + workdir_path: None, + description: None, + template_path: None, + initial_head: None, + origin_url: None, + } + } + + /// Create a bare repository with no working directory. + /// + /// Defaults to false. + pub fn bare(&mut self, bare: bool) -> &mut RepositoryInitOptions { + self.flag(raw::GIT_REPOSITORY_INIT_BARE, bare) + } + + /// Return an error if the repository path appears to already be a git + /// repository. + /// + /// Defaults to false. + pub fn no_reinit(&mut self, enabled: bool) -> &mut RepositoryInitOptions { + self.flag(raw::GIT_REPOSITORY_INIT_NO_REINIT, enabled) + } + + /// Normally a '/.git/' will be appended to the repo path for non-bare repos + /// (if it is not already there), but passing this flag prevents that + /// behavior. + /// + /// Defaults to false. + pub fn no_dotgit_dir(&mut self, enabled: bool) -> &mut RepositoryInitOptions { + self.flag(raw::GIT_REPOSITORY_INIT_NO_DOTGIT_DIR, enabled) + } + + /// Make the repo path (and workdir path) as needed. The ".git" directory + /// will always be created regardless of this flag. + /// + /// Defaults to true. + pub fn mkdir(&mut self, enabled: bool) -> &mut RepositoryInitOptions { + self.flag(raw::GIT_REPOSITORY_INIT_MKDIR, enabled) + } + + /// Recursively make all components of the repo and workdir path as + /// necessary. + /// + /// Defaults to true. + pub fn mkpath(&mut self, enabled: bool) -> &mut RepositoryInitOptions { + self.flag(raw::GIT_REPOSITORY_INIT_MKPATH, enabled) + } + + /// Set to one of the `RepositoryInit` constants, or a custom value. + pub fn mode(&mut self, mode: RepositoryInitMode) -> &mut RepositoryInitOptions { + self.mode = mode.bits(); + self + } + + /// Enable or disable using external templates. + /// + /// If enabled, then the `template_path` option will be queried first, then + /// `init.templatedir` from the global config, and finally + /// `/usr/share/git-core-templates` will be used (if it exists). + /// + /// Defaults to true. + pub fn external_template(&mut self, enabled: bool) -> &mut RepositoryInitOptions { + self.flag(raw::GIT_REPOSITORY_INIT_EXTERNAL_TEMPLATE, enabled) + } + + fn flag( + &mut self, + flag: raw::git_repository_init_flag_t, + on: bool, + ) -> &mut RepositoryInitOptions { + if on { + self.flags |= flag as u32; + } else { + self.flags &= !(flag as u32); + } + self + } + + /// The path to the working directory. + /// + /// If this is a relative path it will be evaluated relative to the repo + /// path. If this is not the "natural" working directory, a .git gitlink + /// file will be created here linking to the repo path. + pub fn workdir_path(&mut self, path: &Path) -> &mut RepositoryInitOptions { + // Normal file path OK (does not need Windows conversion). + self.workdir_path = Some(path.into_c_string().unwrap()); + self + } + + /// If set, this will be used to initialize the "description" file in the + /// repository instead of using the template content. + pub fn description(&mut self, desc: &str) -> &mut RepositoryInitOptions { + self.description = Some(CString::new(desc).unwrap()); + self + } + + /// When the `external_template` option is set, this is the first location + /// to check for the template directory. + /// + /// If this is not configured, then the default locations will be searched + /// instead. + pub fn template_path(&mut self, path: &Path) -> &mut RepositoryInitOptions { + // Normal file path OK (does not need Windows conversion). + self.template_path = Some(path.into_c_string().unwrap()); + self + } + + /// The name of the head to point HEAD at. + /// + /// If not configured, this will be taken from your git configuration. + /// If this begins with `refs/` it will be used verbatim; + /// otherwise `refs/heads/` will be prefixed + pub fn initial_head(&mut self, head: &str) -> &mut RepositoryInitOptions { + self.initial_head = Some(CString::new(head).unwrap()); + self + } + + /// If set, then after the rest of the repository initialization is + /// completed an `origin` remote will be added pointing to this URL. + pub fn origin_url(&mut self, url: &str) -> &mut RepositoryInitOptions { + self.origin_url = Some(CString::new(url).unwrap()); + self + } + + /// Creates a set of raw init options to be used with + /// `git_repository_init_ext`. + /// + /// This method is unsafe as the returned value may have pointers to the + /// interior of this structure. + pub unsafe fn raw(&self) -> raw::git_repository_init_options { + let mut opts = mem::zeroed(); + assert_eq!( + raw::git_repository_init_init_options( + &mut opts, + raw::GIT_REPOSITORY_INIT_OPTIONS_VERSION + ), + 0 + ); + opts.flags = self.flags; + opts.mode = self.mode; + opts.workdir_path = crate::call::convert(&self.workdir_path); + opts.description = crate::call::convert(&self.description); + opts.template_path = crate::call::convert(&self.template_path); + opts.initial_head = crate::call::convert(&self.initial_head); + opts.origin_url = crate::call::convert(&self.origin_url); + opts + } +} + +#[cfg(test)] +mod tests { + use crate::build::CheckoutBuilder; + use crate::CherrypickOptions; + use crate::{ + ObjectType, Oid, Repository, ResetType, Signature, SubmoduleIgnore, SubmoduleUpdate, + }; + use std::ffi::OsStr; + use std::fs; + use std::path::Path; + use tempfile::TempDir; + + #[test] + fn smoke_init() { + let td = TempDir::new().unwrap(); + let path = td.path(); + + let repo = Repository::init(path).unwrap(); + assert!(!repo.is_bare()); + } + + #[test] + fn smoke_init_bare() { + let td = TempDir::new().unwrap(); + let path = td.path(); + + let repo = Repository::init_bare(path).unwrap(); + assert!(repo.is_bare()); + assert!(repo.namespace().is_none()); + } + + #[test] + fn smoke_open() { + let td = TempDir::new().unwrap(); + let path = td.path(); + Repository::init(td.path()).unwrap(); + let repo = Repository::open(path).unwrap(); + assert!(!repo.is_bare()); + assert!(!repo.is_shallow()); + assert!(repo.is_empty().unwrap()); + assert_eq!( + crate::test::realpath(&repo.path()).unwrap(), + crate::test::realpath(&td.path().join(".git/")).unwrap() + ); + assert_eq!(repo.state(), crate::RepositoryState::Clean); + } + + #[test] + fn smoke_open_bare() { + let td = TempDir::new().unwrap(); + let path = td.path(); + Repository::init_bare(td.path()).unwrap(); + + let repo = Repository::open(path).unwrap(); + assert!(repo.is_bare()); + assert_eq!( + crate::test::realpath(&repo.path()).unwrap(), + crate::test::realpath(&td.path().join("")).unwrap() + ); + } + + #[test] + fn smoke_checkout() { + let (_td, repo) = crate::test::repo_init(); + repo.checkout_head(None).unwrap(); + } + + #[test] + fn smoke_revparse() { + let (_td, repo) = crate::test::repo_init(); + let rev = repo.revparse("HEAD").unwrap(); + assert!(rev.to().is_none()); + let from = rev.from().unwrap(); + assert!(rev.from().is_some()); + + assert_eq!(repo.revparse_single("HEAD").unwrap().id(), from.id()); + let obj = repo.find_object(from.id(), None).unwrap().clone(); + obj.peel(ObjectType::Any).unwrap(); + obj.short_id().unwrap(); + repo.reset(&obj, ResetType::Hard, None).unwrap(); + let mut opts = CheckoutBuilder::new(); + t!(repo.reset(&obj, ResetType::Soft, Some(&mut opts))); + } + + #[test] + fn makes_dirs() { + let td = TempDir::new().unwrap(); + Repository::init(&td.path().join("a/b/c/d")).unwrap(); + } + + #[test] + fn smoke_discover() { + let td = TempDir::new().unwrap(); + let subdir = td.path().join("subdi"); + fs::create_dir(&subdir).unwrap(); + Repository::init_bare(td.path()).unwrap(); + let repo = Repository::discover(&subdir).unwrap(); + assert_eq!( + crate::test::realpath(&repo.path()).unwrap(), + crate::test::realpath(&td.path().join("")).unwrap() + ); + } + + #[test] + fn smoke_discover_path() { + let td = TempDir::new().unwrap(); + let subdir = td.path().join("subdi"); + fs::create_dir(&subdir).unwrap(); + Repository::init_bare(td.path()).unwrap(); + let path = Repository::discover_path(&subdir, &[] as &[&OsStr]).unwrap(); + assert_eq!( + crate::test::realpath(&path).unwrap(), + crate::test::realpath(&td.path().join("")).unwrap() + ); + } + + #[test] + fn smoke_discover_path_ceiling_dir() { + let td = TempDir::new().unwrap(); + let subdir = td.path().join("subdi"); + fs::create_dir(&subdir).unwrap(); + let ceilingdir = subdir.join("ceiling"); + fs::create_dir(&ceilingdir).unwrap(); + let testdir = ceilingdir.join("testdi"); + fs::create_dir(&testdir).unwrap(); + Repository::init_bare(td.path()).unwrap(); + let path = Repository::discover_path(&testdir, &[ceilingdir.as_os_str()]); + + assert!(path.is_err()); + } + + #[test] + fn smoke_open_ext() { + let td = TempDir::new().unwrap(); + let subdir = td.path().join("subdir"); + fs::create_dir(&subdir).unwrap(); + Repository::init(td.path()).unwrap(); + + let repo = Repository::open_ext( + &subdir, + crate::RepositoryOpenFlags::empty(), + &[] as &[&OsStr], + ) + .unwrap(); + assert!(!repo.is_bare()); + assert_eq!( + crate::test::realpath(&repo.path()).unwrap(), + crate::test::realpath(&td.path().join(".git")).unwrap() + ); + + let repo = + Repository::open_ext(&subdir, crate::RepositoryOpenFlags::BARE, &[] as &[&OsStr]) + .unwrap(); + assert!(repo.is_bare()); + assert_eq!( + crate::test::realpath(&repo.path()).unwrap(), + crate::test::realpath(&td.path().join(".git")).unwrap() + ); + + let err = Repository::open_ext( + &subdir, + crate::RepositoryOpenFlags::NO_SEARCH, + &[] as &[&OsStr], + ) + .err() + .unwrap(); + assert_eq!(err.code(), crate::ErrorCode::NotFound); + + assert!( + Repository::open_ext(&subdir, crate::RepositoryOpenFlags::empty(), &[&subdir]).is_ok() + ); + } + + fn graph_repo_init() -> (TempDir, Repository) { + let (_td, repo) = crate::test::repo_init(); + { + let head = repo.head().unwrap().target().unwrap(); + let head = repo.find_commit(head).unwrap(); + + let mut index = repo.index().unwrap(); + let id = index.write_tree().unwrap(); + + let tree = repo.find_tree(id).unwrap(); + let sig = repo.signature().unwrap(); + repo.commit(Some("HEAD"), &sig, &sig, "second", &tree, &[&head]) + .unwrap(); + } + (_td, repo) + } + + #[test] + fn smoke_graph_ahead_behind() { + let (_td, repo) = graph_repo_init(); + let head = repo.head().unwrap().target().unwrap(); + let head = repo.find_commit(head).unwrap(); + let head_id = head.id(); + let head_parent_id = head.parent(0).unwrap().id(); + let (ahead, behind) = repo.graph_ahead_behind(head_id, head_parent_id).unwrap(); + assert_eq!(ahead, 1); + assert_eq!(behind, 0); + let (ahead, behind) = repo.graph_ahead_behind(head_parent_id, head_id).unwrap(); + assert_eq!(ahead, 0); + assert_eq!(behind, 1); + } + + #[test] + fn smoke_graph_descendant_of() { + let (_td, repo) = graph_repo_init(); + let head = repo.head().unwrap().target().unwrap(); + let head = repo.find_commit(head).unwrap(); + let head_id = head.id(); + let head_parent_id = head.parent(0).unwrap().id(); + assert!(repo.graph_descendant_of(head_id, head_parent_id).unwrap()); + assert!(!repo.graph_descendant_of(head_parent_id, head_id).unwrap()); + } + + #[test] + fn smoke_reference_has_log_ensure_log() { + let (_td, repo) = crate::test::repo_init(); + + assert_eq!(repo.reference_has_log("HEAD").unwrap(), true); + assert_eq!(repo.reference_has_log("refs/heads/main").unwrap(), true); + assert_eq!(repo.reference_has_log("NOT_HEAD").unwrap(), false); + let main_oid = repo.revparse_single("main").unwrap().id(); + assert!(repo + .reference("NOT_HEAD", main_oid, false, "creating a new branch") + .is_ok()); + assert_eq!(repo.reference_has_log("NOT_HEAD").unwrap(), false); + assert!(repo.reference_ensure_log("NOT_HEAD").is_ok()); + assert_eq!(repo.reference_has_log("NOT_HEAD").unwrap(), true); + } + + #[test] + fn smoke_set_head() { + let (_td, repo) = crate::test::repo_init(); + + assert!(repo.set_head("refs/heads/does-not-exist").is_ok()); + assert!(repo.head().is_err()); + + assert!(repo.set_head("refs/heads/main").is_ok()); + assert!(repo.head().is_ok()); + + assert!(repo.set_head("*").is_err()); + } + + #[test] + fn smoke_set_head_bytes() { + let (_td, repo) = crate::test::repo_init(); + + assert!(repo.set_head_bytes(b"refs/heads/does-not-exist").is_ok()); + assert!(repo.head().is_err()); + + assert!(repo.set_head_bytes(b"refs/heads/main").is_ok()); + assert!(repo.head().is_ok()); + + assert!(repo.set_head_bytes(b"*").is_err()); + } + + #[test] + fn smoke_set_head_detached() { + let (_td, repo) = crate::test::repo_init(); + + let void_oid = Oid::from_bytes(b"00000000000000000000").unwrap(); + assert!(repo.set_head_detached(void_oid).is_err()); + + let main_oid = repo.revparse_single("main").unwrap().id(); + assert!(repo.set_head_detached(main_oid).is_ok()); + assert_eq!(repo.head().unwrap().target().unwrap(), main_oid); + } + + /// create the following: + /// /---o4 + /// /---o3 + /// o1---o2 + #[test] + fn smoke_merge_base() { + let (_td, repo) = graph_repo_init(); + let sig = repo.signature().unwrap(); + + // let oid1 = head + let oid1 = repo.head().unwrap().target().unwrap(); + let commit1 = repo.find_commit(oid1).unwrap(); + println!("created oid1 {:?}", oid1); + + repo.branch("branch_a", &commit1, true).unwrap(); + repo.branch("branch_b", &commit1, true).unwrap(); + repo.branch("branch_c", &commit1, true).unwrap(); + + // create commit oid2 on branch_a + let mut index = repo.index().unwrap(); + let p = Path::new(repo.workdir().unwrap()).join("file_a"); + println!("using path {:?}", p); + fs::File::create(&p).unwrap(); + index.add_path(Path::new("file_a")).unwrap(); + let id_a = index.write_tree().unwrap(); + let tree_a = repo.find_tree(id_a).unwrap(); + let oid2 = repo + .commit( + Some("refs/heads/branch_a"), + &sig, + &sig, + "commit 2", + &tree_a, + &[&commit1], + ) + .unwrap(); + repo.find_commit(oid2).unwrap(); + println!("created oid2 {:?}", oid2); + + t!(repo.reset(commit1.as_object(), ResetType::Hard, None)); + + // create commit oid3 on branch_b + let mut index = repo.index().unwrap(); + let p = Path::new(repo.workdir().unwrap()).join("file_b"); + fs::File::create(&p).unwrap(); + index.add_path(Path::new("file_b")).unwrap(); + let id_b = index.write_tree().unwrap(); + let tree_b = repo.find_tree(id_b).unwrap(); + let oid3 = repo + .commit( + Some("refs/heads/branch_b"), + &sig, + &sig, + "commit 3", + &tree_b, + &[&commit1], + ) + .unwrap(); + repo.find_commit(oid3).unwrap(); + println!("created oid3 {:?}", oid3); + + t!(repo.reset(commit1.as_object(), ResetType::Hard, None)); + + // create commit oid4 on branch_c + let mut index = repo.index().unwrap(); + let p = Path::new(repo.workdir().unwrap()).join("file_c"); + fs::File::create(&p).unwrap(); + index.add_path(Path::new("file_c")).unwrap(); + let id_c = index.write_tree().unwrap(); + let tree_c = repo.find_tree(id_c).unwrap(); + let oid4 = repo + .commit( + Some("refs/heads/branch_c"), + &sig, + &sig, + "commit 3", + &tree_c, + &[&commit1], + ) + .unwrap(); + repo.find_commit(oid4).unwrap(); + println!("created oid4 {:?}", oid4); + + // the merge base of (oid2,oid3) should be oid1 + let merge_base = repo.merge_base(oid2, oid3).unwrap(); + assert_eq!(merge_base, oid1); + + // the merge base of (oid2,oid3,oid4) should be oid1 + let merge_base = repo.merge_base_many(&[oid2, oid3, oid4]).unwrap(); + assert_eq!(merge_base, oid1); + } + + /// create an octopus: + /// /---o2-o4 + /// o1 X + /// \---o3-o5 + /// and checks that the merge bases of (o4,o5) are (o2,o3) + #[test] + fn smoke_merge_bases() { + let (_td, repo) = graph_repo_init(); + let sig = repo.signature().unwrap(); + + // let oid1 = head + let oid1 = repo.head().unwrap().target().unwrap(); + let commit1 = repo.find_commit(oid1).unwrap(); + println!("created oid1 {:?}", oid1); + + repo.branch("branch_a", &commit1, true).unwrap(); + repo.branch("branch_b", &commit1, true).unwrap(); + + // create commit oid2 on branchA + let mut index = repo.index().unwrap(); + let p = Path::new(repo.workdir().unwrap()).join("file_a"); + println!("using path {:?}", p); + fs::File::create(&p).unwrap(); + index.add_path(Path::new("file_a")).unwrap(); + let id_a = index.write_tree().unwrap(); + let tree_a = repo.find_tree(id_a).unwrap(); + let oid2 = repo + .commit( + Some("refs/heads/branch_a"), + &sig, + &sig, + "commit 2", + &tree_a, + &[&commit1], + ) + .unwrap(); + let commit2 = repo.find_commit(oid2).unwrap(); + println!("created oid2 {:?}", oid2); + + t!(repo.reset(commit1.as_object(), ResetType::Hard, None)); + + // create commit oid3 on branchB + let mut index = repo.index().unwrap(); + let p = Path::new(repo.workdir().unwrap()).join("file_b"); + fs::File::create(&p).unwrap(); + index.add_path(Path::new("file_b")).unwrap(); + let id_b = index.write_tree().unwrap(); + let tree_b = repo.find_tree(id_b).unwrap(); + let oid3 = repo + .commit( + Some("refs/heads/branch_b"), + &sig, + &sig, + "commit 3", + &tree_b, + &[&commit1], + ) + .unwrap(); + let commit3 = repo.find_commit(oid3).unwrap(); + println!("created oid3 {:?}", oid3); + + // create merge commit oid4 on branchA with parents oid2 and oid3 + //let mut index4 = repo.merge_commits(&commit2, &commit3, None).unwrap(); + repo.set_head("refs/heads/branch_a").unwrap(); + repo.checkout_head(None).unwrap(); + let oid4 = repo + .commit( + Some("refs/heads/branch_a"), + &sig, + &sig, + "commit 4", + &tree_a, + &[&commit2, &commit3], + ) + .unwrap(); + //index4.write_tree_to(&repo).unwrap(); + println!("created oid4 {:?}", oid4); + + // create merge commit oid5 on branchB with parents oid2 and oid3 + //let mut index5 = repo.merge_commits(&commit3, &commit2, None).unwrap(); + repo.set_head("refs/heads/branch_b").unwrap(); + repo.checkout_head(None).unwrap(); + let oid5 = repo + .commit( + Some("refs/heads/branch_b"), + &sig, + &sig, + "commit 5", + &tree_a, + &[&commit3, &commit2], + ) + .unwrap(); + //index5.write_tree_to(&repo).unwrap(); + println!("created oid5 {:?}", oid5); + + // merge bases of (oid4,oid5) should be (oid2,oid3) + let merge_bases = repo.merge_bases(oid4, oid5).unwrap(); + let mut found_oid2 = false; + let mut found_oid3 = false; + for mg in merge_bases.iter() { + println!("found merge base {:?}", mg); + if mg == &oid2 { + found_oid2 = true; + } else if mg == &oid3 { + found_oid3 = true; + } else { + assert!(false); + } + } + assert!(found_oid2); + assert!(found_oid3); + assert_eq!(merge_bases.len(), 2); + + // merge bases of (oid4,oid5) should be (oid2,oid3) + let merge_bases = repo.merge_bases_many(&[oid4, oid5]).unwrap(); + let mut found_oid2 = false; + let mut found_oid3 = false; + for mg in merge_bases.iter() { + println!("found merge base {:?}", mg); + if mg == &oid2 { + found_oid2 = true; + } else if mg == &oid3 { + found_oid3 = true; + } else { + assert!(false); + } + } + assert!(found_oid2); + assert!(found_oid3); + assert_eq!(merge_bases.len(), 2); + } + + #[test] + fn smoke_revparse_ext() { + let (_td, repo) = graph_repo_init(); + + { + let short_refname = "main"; + let expected_refname = "refs/heads/main"; + let (obj, reference) = repo.revparse_ext(short_refname).unwrap(); + let expected_obj = repo.revparse_single(expected_refname).unwrap(); + assert_eq!(obj.id(), expected_obj.id()); + assert_eq!(reference.unwrap().name().unwrap(), expected_refname); + } + { + let missing_refname = "refs/heads/does-not-exist"; + assert!(repo.revparse_ext(missing_refname).is_err()); + } + { + let (_obj, reference) = repo.revparse_ext("HEAD^").unwrap(); + assert!(reference.is_none()); + } + } + + #[test] + fn smoke_is_path_ignored() { + let (_td, repo) = graph_repo_init(); + + assert!(!repo.is_path_ignored(Path::new("foo")).unwrap()); + + let _ = repo.add_ignore_rule("/foo"); + assert!(repo.is_path_ignored(Path::new("foo")).unwrap()); + if cfg!(windows) { + assert!(repo.is_path_ignored(Path::new("foo\\thing")).unwrap()); + } + + let _ = repo.clear_ignore_rules(); + assert!(!repo.is_path_ignored(Path::new("foo")).unwrap()); + if cfg!(windows) { + assert!(!repo.is_path_ignored(Path::new("foo\\thing")).unwrap()); + } + } + + #[test] + fn smoke_cherrypick() { + let (_td, repo) = crate::test::repo_init(); + let sig = repo.signature().unwrap(); + + let oid1 = repo.head().unwrap().target().unwrap(); + let commit1 = repo.find_commit(oid1).unwrap(); + + repo.branch("branch_a", &commit1, true).unwrap(); + + // Add 2 commits on top of the initial one in branch_a + let mut index = repo.index().unwrap(); + let p1 = Path::new(repo.workdir().unwrap()).join("file_c"); + fs::File::create(&p1).unwrap(); + index.add_path(Path::new("file_c")).unwrap(); + let id = index.write_tree().unwrap(); + let tree_c = repo.find_tree(id).unwrap(); + let oid2 = repo + .commit( + Some("refs/heads/branch_a"), + &sig, + &sig, + "commit 2", + &tree_c, + &[&commit1], + ) + .unwrap(); + let commit2 = repo.find_commit(oid2).unwrap(); + println!("created oid2 {:?}", oid2); + assert!(p1.exists()); + + let mut index = repo.index().unwrap(); + let p2 = Path::new(repo.workdir().unwrap()).join("file_d"); + fs::File::create(&p2).unwrap(); + index.add_path(Path::new("file_d")).unwrap(); + let id = index.write_tree().unwrap(); + let tree_d = repo.find_tree(id).unwrap(); + let oid3 = repo + .commit( + Some("refs/heads/branch_a"), + &sig, + &sig, + "commit 3", + &tree_d, + &[&commit2], + ) + .unwrap(); + let commit3 = repo.find_commit(oid3).unwrap(); + println!("created oid3 {:?}", oid3); + assert!(p1.exists()); + assert!(p2.exists()); + + // cherry-pick commit3 on top of commit1 in branch b + repo.reset(commit1.as_object(), ResetType::Hard, None) + .unwrap(); + let mut cherrypick_opts = CherrypickOptions::new(); + repo.cherrypick(&commit3, Some(&mut cherrypick_opts)) + .unwrap(); + let id = repo.index().unwrap().write_tree().unwrap(); + let tree_d = repo.find_tree(id).unwrap(); + let oid4 = repo + .commit(Some("HEAD"), &sig, &sig, "commit 4", &tree_d, &[&commit1]) + .unwrap(); + let commit4 = repo.find_commit(oid4).unwrap(); + // should have file from commit3, but not the file from commit2 + assert_eq!(commit4.parent(0).unwrap().id(), commit1.id()); + assert!(!p1.exists()); + assert!(p2.exists()); + } + + #[test] + fn smoke_revert() { + let (_td, repo) = crate::test::repo_init(); + let foo_file = Path::new(repo.workdir().unwrap()).join("foo"); + assert!(!foo_file.exists()); + + let (oid1, _id) = crate::test::commit(&repo); + let commit1 = repo.find_commit(oid1).unwrap(); + t!(repo.reset(commit1.as_object(), ResetType::Hard, None)); + assert!(foo_file.exists()); + + repo.revert(&commit1, None).unwrap(); + let id = repo.index().unwrap().write_tree().unwrap(); + let tree2 = repo.find_tree(id).unwrap(); + let sig = repo.signature().unwrap(); + repo.commit(Some("HEAD"), &sig, &sig, "commit 1", &tree2, &[&commit1]) + .unwrap(); + // reverting once removes `foo` file + assert!(!foo_file.exists()); + + let oid2 = repo.head().unwrap().target().unwrap(); + let commit2 = repo.find_commit(oid2).unwrap(); + repo.revert(&commit2, None).unwrap(); + let id = repo.index().unwrap().write_tree().unwrap(); + let tree3 = repo.find_tree(id).unwrap(); + repo.commit(Some("HEAD"), &sig, &sig, "commit 2", &tree3, &[&commit2]) + .unwrap(); + // reverting twice restores `foo` file + assert!(foo_file.exists()); + } + + #[test] + fn smoke_config_write_and_read() { + let (td, repo) = crate::test::repo_init(); + + let mut config = repo.config().unwrap(); + + config.set_bool("commit.gpgsign", false).unwrap(); + + let c = fs::read_to_string(td.path().join(".git").join("config")).unwrap(); + + assert!(c.contains("[commit]")); + assert!(c.contains("gpgsign = false")); + + let config = repo.config().unwrap(); + + assert!(!config.get_bool("commit.gpgsign").unwrap()); + } + + #[test] + fn smoke_merge_analysis_for_ref() -> Result<(), crate::Error> { + let (_td, repo) = graph_repo_init(); + + // Set up this repo state: + // * second (their-branch) + // * initial (HEAD -> main) + // + // We expect that their-branch can be fast-forward merged into main. + + // git checkout --detach HEAD + let head_commit = repo.head()?.peel_to_commit()?; + repo.set_head_detached(head_commit.id())?; + + // git branch their-branch HEAD + let their_branch = repo.branch("their-branch", &head_commit, false)?; + + // git branch -f main HEAD~ + let mut parents_iter = head_commit.parents(); + let parent = parents_iter.next().unwrap(); + assert!(parents_iter.next().is_none()); + + let main = repo.branch("main", &parent, true)?; + + // git checkout main + repo.set_head(main.get().name().expect("should be utf-8"))?; + + let (merge_analysis, _merge_preference) = repo.merge_analysis_for_ref( + main.get(), + &[&repo.reference_to_annotated_commit(their_branch.get())?], + )?; + + assert!(merge_analysis.contains(crate::MergeAnalysis::ANALYSIS_FASTFORWARD)); + + Ok(()) + } + + #[test] + fn smoke_submodule_set() -> Result<(), crate::Error> { + let (td1, _repo) = crate::test::repo_init(); + let (td2, mut repo2) = crate::test::repo_init(); + let url = crate::test::path2url(td1.path()); + let name = "bar"; + { + let mut s = repo2.submodule(&url, Path::new(name), true)?; + fs::remove_dir_all(td2.path().join("bar")).unwrap(); + Repository::clone(&url, td2.path().join("bar"))?; + s.add_to_index(false)?; + s.add_finalize()?; + } + + // update strategy + repo2.submodule_set_update(name, SubmoduleUpdate::None)?; + assert!(matches!( + repo2.find_submodule(name)?.update_strategy(), + SubmoduleUpdate::None + )); + repo2.submodule_set_update(name, SubmoduleUpdate::Rebase)?; + assert!(matches!( + repo2.find_submodule(name)?.update_strategy(), + SubmoduleUpdate::Rebase + )); + + // ignore rule + repo2.submodule_set_ignore(name, SubmoduleIgnore::Untracked)?; + assert!(matches!( + repo2.find_submodule(name)?.ignore_rule(), + SubmoduleIgnore::Untracked + )); + repo2.submodule_set_ignore(name, SubmoduleIgnore::Dirty)?; + assert!(matches!( + repo2.find_submodule(name)?.ignore_rule(), + SubmoduleIgnore::Dirty + )); + + // url + repo2.submodule_set_url(name, "fake-url")?; + assert_eq!(repo2.find_submodule(name)?.url(), Some("fake-url")); + + // branch + repo2.submodule_set_branch(name, "fake-branch")?; + assert_eq!(repo2.find_submodule(name)?.branch(), Some("fake-branch")); + + Ok(()) + } + + #[test] + fn smoke_mailmap_from_repository() { + let (_td, repo) = crate::test::repo_init(); + + let commit = { + let head = t!(repo.head()).target().unwrap(); + t!(repo.find_commit(head)) + }; + + // This is our baseline for HEAD. + let author = commit.author(); + let committer = commit.committer(); + assert_eq!(author.name(), Some("name")); + assert_eq!(author.email(), Some("email")); + assert_eq!(committer.name(), Some("name")); + assert_eq!(committer.email(), Some("email")); + + // There is no .mailmap file in the test repo so all signature identities are equal. + let mailmap = t!(repo.mailmap()); + let mailmapped_author = t!(commit.author_with_mailmap(&mailmap)); + let mailmapped_committer = t!(commit.committer_with_mailmap(&mailmap)); + assert_eq!(mailmapped_author.name(), author.name()); + assert_eq!(mailmapped_author.email(), author.email()); + assert_eq!(mailmapped_committer.name(), committer.name()); + assert_eq!(mailmapped_committer.email(), committer.email()); + + let commit = { + // - Add a .mailmap file to the repository. + // - Commit with a signature identity different from the author's. + // - Include entries for both author and committer to prove we call + // the right raw functions. + let mailmap_file = Path::new(".mailmap"); + let p = Path::new(repo.workdir().unwrap()).join(&mailmap_file); + t!(fs::write( + p, + r#" +Author Name <author.proper@email> name <email> +Committer Name <committer.proper@email> <committer@email>"#, + )); + let mut index = t!(repo.index()); + t!(index.add_path(&mailmap_file)); + let id_mailmap = t!(index.write_tree()); + let tree_mailmap = t!(repo.find_tree(id_mailmap)); + + let head = t!(repo.commit( + Some("HEAD"), + &author, + t!(&Signature::now("committer", "committer@email")), + "Add mailmap", + &tree_mailmap, + &[&commit], + )); + t!(repo.find_commit(head)) + }; + + // Sanity check that we're working with the right commit and that its + // author and committer identities differ. + let author = commit.author(); + let committer = commit.committer(); + assert_ne!(author.name(), committer.name()); + assert_ne!(author.email(), committer.email()); + assert_eq!(author.name(), Some("name")); + assert_eq!(author.email(), Some("email")); + assert_eq!(committer.name(), Some("committer")); + assert_eq!(committer.email(), Some("committer@email")); + + // Fetch the newly added .mailmap from the repository. + let mailmap = t!(repo.mailmap()); + let mailmapped_author = t!(commit.author_with_mailmap(&mailmap)); + let mailmapped_committer = t!(commit.committer_with_mailmap(&mailmap)); + + let mm_resolve_author = t!(mailmap.resolve_signature(&author)); + let mm_resolve_committer = t!(mailmap.resolve_signature(&committer)); + + // Mailmap Signature lifetime is independent of Commit lifetime. + drop(author); + drop(committer); + drop(commit); + + // author_with_mailmap() + committer_with_mailmap() work + assert_eq!(mailmapped_author.name(), Some("Author Name")); + assert_eq!(mailmapped_author.email(), Some("author.proper@email")); + assert_eq!(mailmapped_committer.name(), Some("Committer Name")); + assert_eq!(mailmapped_committer.email(), Some("committer.proper@email")); + + // resolve_signature() works + assert_eq!(mm_resolve_author.email(), mailmapped_author.email()); + assert_eq!(mm_resolve_committer.email(), mailmapped_committer.email()); + } +} diff --git a/extra/git2/src/revert.rs b/extra/git2/src/revert.rs new file mode 100644 index 000000000..55d702600 --- /dev/null +++ b/extra/git2/src/revert.rs @@ -0,0 +1,69 @@ +use std::mem; + +use crate::build::CheckoutBuilder; +use crate::merge::MergeOptions; +use crate::raw; +use std::ptr; + +/// Options to specify when reverting +pub struct RevertOptions<'cb> { + mainline: u32, + checkout_builder: Option<CheckoutBuilder<'cb>>, + merge_opts: Option<MergeOptions>, +} + +impl<'cb> RevertOptions<'cb> { + /// Creates a default set of revert options + pub fn new() -> RevertOptions<'cb> { + RevertOptions { + mainline: 0, + checkout_builder: None, + merge_opts: None, + } + } + + /// Set the mainline value + /// + /// For merge commits, the "mainline" is treated as the parent. + pub fn mainline(&mut self, mainline: u32) -> &mut Self { + self.mainline = mainline; + self + } + + /// Set the checkout builder + pub fn checkout_builder(&mut self, cb: CheckoutBuilder<'cb>) -> &mut Self { + self.checkout_builder = Some(cb); + self + } + + /// Set the merge options + pub fn merge_opts(&mut self, merge_opts: MergeOptions) -> &mut Self { + self.merge_opts = Some(merge_opts); + self + } + + /// Obtain the raw struct + pub fn raw(&mut self) -> raw::git_revert_options { + unsafe { + let mut checkout_opts: raw::git_checkout_options = mem::zeroed(); + raw::git_checkout_init_options(&mut checkout_opts, raw::GIT_CHECKOUT_OPTIONS_VERSION); + if let Some(ref mut cb) = self.checkout_builder { + cb.configure(&mut checkout_opts); + } + + let mut merge_opts: raw::git_merge_options = mem::zeroed(); + raw::git_merge_init_options(&mut merge_opts, raw::GIT_MERGE_OPTIONS_VERSION); + if let Some(ref opts) = self.merge_opts { + ptr::copy(opts.raw(), &mut merge_opts, 1); + } + + let mut revert_opts: raw::git_revert_options = mem::zeroed(); + raw::git_revert_options_init(&mut revert_opts, raw::GIT_REVERT_OPTIONS_VERSION); + revert_opts.mainline = self.mainline; + revert_opts.checkout_opts = checkout_opts; + revert_opts.merge_opts = merge_opts; + + revert_opts + } + } +} diff --git a/extra/git2/src/revspec.rs b/extra/git2/src/revspec.rs new file mode 100644 index 000000000..d2e08670a --- /dev/null +++ b/extra/git2/src/revspec.rs @@ -0,0 +1,34 @@ +use crate::{Object, RevparseMode}; + +/// A revspec represents a range of revisions within a repository. +pub struct Revspec<'repo> { + from: Option<Object<'repo>>, + to: Option<Object<'repo>>, + mode: RevparseMode, +} + +impl<'repo> Revspec<'repo> { + /// Assembles a new revspec from the from/to components. + pub fn from_objects( + from: Option<Object<'repo>>, + to: Option<Object<'repo>>, + mode: RevparseMode, + ) -> Revspec<'repo> { + Revspec { from, to, mode } + } + + /// Access the `from` range of this revspec. + pub fn from(&self) -> Option<&Object<'repo>> { + self.from.as_ref() + } + + /// Access the `to` range of this revspec. + pub fn to(&self) -> Option<&Object<'repo>> { + self.to.as_ref() + } + + /// Returns the intent of the revspec. + pub fn mode(&self) -> RevparseMode { + self.mode + } +} diff --git a/extra/git2/src/revwalk.rs b/extra/git2/src/revwalk.rs new file mode 100644 index 000000000..7837f00d6 --- /dev/null +++ b/extra/git2/src/revwalk.rs @@ -0,0 +1,316 @@ +use libc::{c_int, c_uint, c_void}; +use std::ffi::CString; +use std::marker; + +use crate::util::Binding; +use crate::{panic, raw, Error, Oid, Repository, Sort}; + +/// A revwalk allows traversal of the commit graph defined by including one or +/// more leaves and excluding one or more roots. +pub struct Revwalk<'repo> { + raw: *mut raw::git_revwalk, + _marker: marker::PhantomData<&'repo Repository>, +} + +/// A `Revwalk` with an associated "hide callback", see `with_hide_callback` +pub struct RevwalkWithHideCb<'repo, 'cb, C> +where + C: FnMut(Oid) -> bool, +{ + revwalk: Revwalk<'repo>, + _marker: marker::PhantomData<&'cb C>, +} + +extern "C" fn revwalk_hide_cb<C>(commit_id: *const raw::git_oid, payload: *mut c_void) -> c_int +where + C: FnMut(Oid) -> bool, +{ + panic::wrap(|| unsafe { + let hide_cb = payload as *mut C; + if (*hide_cb)(Oid::from_raw(commit_id)) { + 1 + } else { + 0 + } + }) + .unwrap_or(-1) +} + +impl<'repo, 'cb, C: FnMut(Oid) -> bool> RevwalkWithHideCb<'repo, 'cb, C> { + /// Consumes the `RevwalkWithHideCb` and returns the contained `Revwalk`. + /// + /// Note that this will reset the `Revwalk`. + pub fn into_inner(mut self) -> Result<Revwalk<'repo>, Error> { + self.revwalk.reset()?; + Ok(self.revwalk) + } +} + +impl<'repo> Revwalk<'repo> { + /// Reset a revwalk to allow re-configuring it. + /// + /// The revwalk is automatically reset when iteration of its commits + /// completes. + pub fn reset(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_revwalk_reset(self.raw())); + } + Ok(()) + } + + /// Set the order in which commits are visited. + pub fn set_sorting(&mut self, sort_mode: Sort) -> Result<(), Error> { + unsafe { + try_call!(raw::git_revwalk_sorting( + self.raw(), + sort_mode.bits() as c_uint + )); + } + Ok(()) + } + + /// Simplify the history by first-parent + /// + /// No parents other than the first for each commit will be enqueued. + pub fn simplify_first_parent(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_revwalk_simplify_first_parent(self.raw)); + } + Ok(()) + } + + /// Mark a commit to start traversal from. + /// + /// The given OID must belong to a 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 + /// a walk can be started. + pub fn push(&mut self, oid: Oid) -> Result<(), Error> { + unsafe { + try_call!(raw::git_revwalk_push(self.raw(), oid.raw())); + } + Ok(()) + } + + /// Push the repository's HEAD + /// + /// For more information, see `push`. + pub fn push_head(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_revwalk_push_head(self.raw())); + } + Ok(()) + } + + /// Push matching references + /// + /// The OIDs pointed to by the references that match the given glob pattern + /// will be pushed to the revision walker. + /// + /// 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 commitish + /// will be ignored. + pub fn push_glob(&mut self, glob: &str) -> Result<(), Error> { + let glob = CString::new(glob)?; + unsafe { + try_call!(raw::git_revwalk_push_glob(self.raw, glob)); + } + Ok(()) + } + + /// Push and hide the respective endpoints of the given range. + /// + /// The range should be of the form `<commit>..<commit>` where each + /// `<commit>` is in the form accepted by `revparse_single`. The left-hand + /// commit will be hidden and the right-hand commit pushed. + pub fn push_range(&mut self, range: &str) -> Result<(), Error> { + let range = CString::new(range)?; + unsafe { + try_call!(raw::git_revwalk_push_range(self.raw, range)); + } + Ok(()) + } + + /// Push the OID pointed to by a reference + /// + /// The reference must point to a commitish. + pub fn push_ref(&mut self, reference: &str) -> Result<(), Error> { + let reference = CString::new(reference)?; + unsafe { + try_call!(raw::git_revwalk_push_ref(self.raw, reference)); + } + Ok(()) + } + + /// Mark a commit as not of interest to this revwalk. + pub fn hide(&mut self, oid: Oid) -> Result<(), Error> { + unsafe { + try_call!(raw::git_revwalk_hide(self.raw(), oid.raw())); + } + Ok(()) + } + + /// Hide all commits for which the callback returns true from + /// the walk. + pub fn with_hide_callback<'cb, C>( + self, + callback: &'cb mut C, + ) -> Result<RevwalkWithHideCb<'repo, 'cb, C>, Error> + where + C: FnMut(Oid) -> bool, + { + let r = RevwalkWithHideCb { + revwalk: self, + _marker: marker::PhantomData, + }; + unsafe { + raw::git_revwalk_add_hide_cb( + r.revwalk.raw(), + Some(revwalk_hide_cb::<C>), + callback as *mut _ as *mut c_void, + ); + }; + Ok(r) + } + + /// Hide the repository's HEAD + /// + /// For more information, see `hide`. + pub fn hide_head(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_revwalk_hide_head(self.raw())); + } + Ok(()) + } + + /// Hide matching references. + /// + /// The OIDs pointed to by the references that match the given glob pattern + /// and their ancestors will be hidden from the output on the revision walk. + /// + /// 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 commitish + /// will be ignored. + pub fn hide_glob(&mut self, glob: &str) -> Result<(), Error> { + let glob = CString::new(glob)?; + unsafe { + try_call!(raw::git_revwalk_hide_glob(self.raw, glob)); + } + Ok(()) + } + + /// Hide the OID pointed to by a reference. + /// + /// The reference must point to a commitish. + pub fn hide_ref(&mut self, reference: &str) -> Result<(), Error> { + let reference = CString::new(reference)?; + unsafe { + try_call!(raw::git_revwalk_hide_ref(self.raw, reference)); + } + Ok(()) + } +} + +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, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_revwalk { + self.raw + } +} + +impl<'repo> Drop for Revwalk<'repo> { + fn drop(&mut self) { + unsafe { raw::git_revwalk_free(self.raw) } + } +} + +impl<'repo> Iterator for Revwalk<'repo> { + type Item = Result<Oid, Error>; + fn next(&mut self) -> Option<Result<Oid, Error>> { + let mut out: raw::git_oid = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call_iter!(raw::git_revwalk_next(&mut out, self.raw())); + Some(Ok(Binding::from_raw(&out as *const _))) + } + } +} + +impl<'repo, 'cb, C: FnMut(Oid) -> bool> Iterator for RevwalkWithHideCb<'repo, 'cb, C> { + type Item = Result<Oid, Error>; + fn next(&mut self) -> Option<Result<Oid, Error>> { + let out = self.revwalk.next(); + crate::panic::check(); + out + } +} + +#[cfg(test)] +mod tests { + #[test] + fn smoke() { + let (_td, repo) = crate::test::repo_init(); + let head = repo.head().unwrap(); + let target = head.target().unwrap(); + + let mut walk = repo.revwalk().unwrap(); + walk.push(target).unwrap(); + + let oids: Vec<crate::Oid> = walk.by_ref().collect::<Result<Vec<_>, _>>().unwrap(); + + assert_eq!(oids.len(), 1); + assert_eq!(oids[0], target); + + walk.reset().unwrap(); + walk.push_head().unwrap(); + assert_eq!(walk.by_ref().count(), 1); + + walk.reset().unwrap(); + walk.push_head().unwrap(); + walk.hide_head().unwrap(); + assert_eq!(walk.by_ref().count(), 0); + } + + #[test] + fn smoke_hide_cb() { + let (_td, repo) = crate::test::repo_init(); + let head = repo.head().unwrap(); + let target = head.target().unwrap(); + + let mut walk = repo.revwalk().unwrap(); + walk.push(target).unwrap(); + + let oids: Vec<crate::Oid> = walk.by_ref().collect::<Result<Vec<_>, _>>().unwrap(); + + assert_eq!(oids.len(), 1); + assert_eq!(oids[0], target); + + walk.reset().unwrap(); + walk.push_head().unwrap(); + assert_eq!(walk.by_ref().count(), 1); + + walk.reset().unwrap(); + walk.push_head().unwrap(); + + let mut hide_cb = |oid| oid == target; + let mut walk = walk.with_hide_callback(&mut hide_cb).unwrap(); + + assert_eq!(walk.by_ref().count(), 0); + + let mut walk = walk.into_inner().unwrap(); + walk.push_head().unwrap(); + assert_eq!(walk.by_ref().count(), 1); + } +} diff --git a/extra/git2/src/signature.rs b/extra/git2/src/signature.rs new file mode 100644 index 000000000..83fbbf593 --- /dev/null +++ b/extra/git2/src/signature.rs @@ -0,0 +1,189 @@ +use libc; +use std::ffi::CString; +use std::fmt; +use std::marker; +use std::mem; +use std::ptr; +use std::str; + +use crate::util::Binding; +use crate::{raw, Error, Time}; + +/// A Signature is used to indicate authorship of various actions throughout the +/// library. +/// +/// Signatures contain a name, email, and timestamp. All fields can be specified +/// with `new` while the `now` constructor omits the timestamp. The +/// [`Repository::signature`] method can be used to create a default signature +/// with name and email values read from the configuration. +/// +/// [`Repository::signature`]: struct.Repository.html#method.signature +pub struct Signature<'a> { + raw: *mut raw::git_signature, + _marker: marker::PhantomData<&'a str>, + owned: bool, +} + +impl<'a> Signature<'a> { + /// Create a new action signature with a timestamp of 'now'. + /// + /// See `new` for more information + pub fn now(name: &str, email: &str) -> Result<Signature<'static>, Error> { + crate::init(); + let mut ret = ptr::null_mut(); + let name = CString::new(name)?; + let email = CString::new(email)?; + unsafe { + try_call!(raw::git_signature_now(&mut ret, name, email)); + Ok(Binding::from_raw(ret)) + } + } + + /// Create a new action signature. + /// + /// The `time` specified is in seconds since the epoch, and the `offset` is + /// the time zone offset in minutes. + /// + /// Returns error if either `name` or `email` contain angle brackets. + pub fn new(name: &str, email: &str, time: &Time) -> Result<Signature<'static>, Error> { + crate::init(); + let mut ret = ptr::null_mut(); + let name = CString::new(name)?; + let email = CString::new(email)?; + unsafe { + try_call!(raw::git_signature_new( + &mut ret, + name, + email, + time.seconds() as raw::git_time_t, + time.offset_minutes() as libc::c_int + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Gets the name on the signature. + /// + /// Returns `None` if the name is not valid utf-8 + pub fn name(&self) -> Option<&str> { + str::from_utf8(self.name_bytes()).ok() + } + + /// Gets the name on the signature as a byte slice. + pub fn name_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, (*self.raw).name).unwrap() } + } + + /// Gets the email on the signature. + /// + /// Returns `None` if the email is not valid utf-8 + pub fn email(&self) -> Option<&str> { + str::from_utf8(self.email_bytes()).ok() + } + + /// Gets the email on the signature as a byte slice. + pub fn email_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, (*self.raw).email).unwrap() } + } + + /// Get the `when` of this signature. + pub fn when(&self) -> Time { + unsafe { Binding::from_raw((*self.raw).when) } + } + + /// Convert a signature of any lifetime into an owned signature with a + /// static lifetime. + pub fn to_owned(&self) -> Signature<'static> { + unsafe { + let me = mem::transmute::<&Signature<'a>, &Signature<'static>>(self); + me.clone() + } + } +} + +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, + _marker: marker::PhantomData, + owned: true, + } + } + fn raw(&self) -> *mut raw::git_signature { + self.raw + } +} + +/// Creates a new signature from the give raw pointer, tied to the lifetime +/// of the given object. +/// +/// This function is unsafe as there is no guarantee that `raw` is valid for +/// `'a` nor if it's a valid pointer. +pub unsafe fn from_raw_const<'b, T>(_lt: &'b T, raw: *const raw::git_signature) -> Signature<'b> { + Signature { + raw: raw as *mut raw::git_signature, + _marker: marker::PhantomData, + owned: false, + } +} + +impl Clone for Signature<'static> { + fn clone(&self) -> Signature<'static> { + // TODO: can this be defined for 'a and just do a plain old copy if the + // lifetime isn't static? + let mut raw = ptr::null_mut(); + let rc = unsafe { raw::git_signature_dup(&mut raw, &*self.raw) }; + assert_eq!(rc, 0); + unsafe { Binding::from_raw(raw) } + } +} + +impl<'a> Drop for Signature<'a> { + fn drop(&mut self) { + if self.owned { + unsafe { raw::git_signature_free(self.raw) } + } + } +} + +impl<'a> fmt::Display for Signature<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} <{}>", + String::from_utf8_lossy(self.name_bytes()), + String::from_utf8_lossy(self.email_bytes()) + ) + } +} + +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}; + + #[test] + fn smoke() { + Signature::new("foo", "bar", &Time::new(89, 0)).unwrap(); + Signature::now("foo", "bar").unwrap(); + assert!(Signature::new("<foo>", "bar", &Time::new(89, 0)).is_err()); + assert!(Signature::now("<foo>", "bar").is_err()); + + let s = Signature::now("foo", "bar").unwrap(); + assert_eq!(s.name(), Some("foo")); + assert_eq!(s.email(), Some("bar")); + + drop(s.clone()); + drop(s.to_owned()); + } +} diff --git a/extra/git2/src/stash.rs b/extra/git2/src/stash.rs new file mode 100644 index 000000000..ea898e46b --- /dev/null +++ b/extra/git2/src/stash.rs @@ -0,0 +1,348 @@ +use crate::build::CheckoutBuilder; +use crate::util::{self, Binding}; +use crate::{panic, raw, IntoCString, Oid, Signature, StashApplyProgress, StashFlags}; +use libc::{c_char, c_int, c_void, size_t}; +use std::ffi::{c_uint, CStr, CString}; +use std::mem; + +/// Stash application options structure +pub struct StashSaveOptions<'a> { + message: Option<CString>, + flags: Option<StashFlags>, + stasher: Signature<'a>, + pathspec: Vec<CString>, + 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<StashFlags>) -> &mut Self { + self.flags = flags; + self + } + + /// Add to the array of paths patterns to build the stash. + pub fn pathspec<T: IntoCString>(&mut self, pathspec: T) -> &mut Self { + let s = util::cstring_to_repo_path(pathspec).unwrap(); + self.pathspec_ptrs.push(s.as_ptr()); + self.pathspec.push(s); + self + } + + /// Acquire a pointer to the underlying raw options. + /// + /// This function is unsafe as the pointer is only valid so long as this + /// structure is not moved, modified, or used elsewhere. + pub unsafe fn raw(&mut self) -> *const raw::git_stash_save_options { + self.raw_opts.flags = self.flags.unwrap_or_else(StashFlags::empty).bits() as c_uint; + self.raw_opts.message = crate::call::convert(&self.message); + self.raw_opts.paths.count = self.pathspec_ptrs.len() as size_t; + self.raw_opts.paths.strings = self.pathspec_ptrs.as_ptr() as *mut _; + self.raw_opts.stasher = self.stasher.raw(); + + &self.raw_opts as *const _ + } +} + +/// Stash application progress notification function. +/// +/// Return `true` to continue processing, or `false` to +/// abort the stash application. +// FIXME: This probably should have been pub(crate) since it is not used anywhere. +pub type StashApplyProgressCb<'a> = dyn FnMut(StashApplyProgress) -> bool + 'a; + +/// This is a callback function you can provide to iterate over all the +/// stashed states that will be invoked per entry. +// FIXME: This probably should have been pub(crate) since it is not used anywhere. +pub type StashCb<'a> = dyn FnMut(usize, &str, &Oid) -> bool + 'a; + +/// Stash application options structure +pub struct StashApplyOptions<'cb> { + progress: Option<Box<StashApplyProgressCb<'cb>>>, + checkout_options: Option<CheckoutBuilder<'cb>>, + raw_opts: raw::git_stash_apply_options, +} + +impl<'cb> Default for StashApplyOptions<'cb> { + fn default() -> Self { + Self::new() + } +} + +impl<'cb> StashApplyOptions<'cb> { + /// Creates a default set of merge options. + pub fn new() -> StashApplyOptions<'cb> { + let mut opts = StashApplyOptions { + progress: None, + checkout_options: None, + raw_opts: unsafe { mem::zeroed() }, + }; + assert_eq!( + unsafe { raw::git_stash_apply_init_options(&mut opts.raw_opts, 1) }, + 0 + ); + opts + } + + /// Set stash application flag to GIT_STASH_APPLY_REINSTATE_INDEX + pub fn reinstantiate_index(&mut self) -> &mut StashApplyOptions<'cb> { + self.raw_opts.flags = raw::GIT_STASH_APPLY_REINSTATE_INDEX as u32; + self + } + + /// Options to use when writing files to the working directory + pub fn checkout_options(&mut self, opts: CheckoutBuilder<'cb>) -> &mut StashApplyOptions<'cb> { + self.checkout_options = Some(opts); + self + } + + /// Optional callback to notify the consumer of application progress. + /// + /// Return `true` to continue processing, or `false` to + /// abort the stash application. + pub fn progress_cb<C>(&mut self, callback: C) -> &mut StashApplyOptions<'cb> + where + C: FnMut(StashApplyProgress) -> bool + 'cb, + { + self.progress = Some(Box::new(callback) as Box<StashApplyProgressCb<'cb>>); + self.raw_opts.progress_cb = Some(stash_apply_progress_cb); + self.raw_opts.progress_payload = self as *mut _ as *mut _; + self + } + + /// Pointer to a raw git_stash_apply_options + pub fn raw(&mut self) -> &raw::git_stash_apply_options { + unsafe { + if let Some(opts) = self.checkout_options.as_mut() { + opts.configure(&mut self.raw_opts.checkout_options); + } + } + &self.raw_opts + } +} + +pub(crate) struct StashCbData<'a> { + pub callback: &'a mut StashCb<'a>, +} + +pub(crate) extern "C" fn stash_cb( + index: size_t, + message: *const c_char, + stash_id: *const raw::git_oid, + payload: *mut c_void, +) -> c_int { + panic::wrap(|| unsafe { + let data = &mut *(payload as *mut StashCbData<'_>); + let res = { + let callback = &mut data.callback; + callback( + index, + CStr::from_ptr(message).to_str().unwrap(), + &Binding::from_raw(stash_id), + ) + }; + + if res { + 0 + } else { + 1 + } + }) + .unwrap_or(1) +} + +fn convert_progress(progress: raw::git_stash_apply_progress_t) -> StashApplyProgress { + match progress { + raw::GIT_STASH_APPLY_PROGRESS_NONE => StashApplyProgress::None, + raw::GIT_STASH_APPLY_PROGRESS_LOADING_STASH => StashApplyProgress::LoadingStash, + raw::GIT_STASH_APPLY_PROGRESS_ANALYZE_INDEX => StashApplyProgress::AnalyzeIndex, + raw::GIT_STASH_APPLY_PROGRESS_ANALYZE_MODIFIED => StashApplyProgress::AnalyzeModified, + raw::GIT_STASH_APPLY_PROGRESS_ANALYZE_UNTRACKED => StashApplyProgress::AnalyzeUntracked, + raw::GIT_STASH_APPLY_PROGRESS_CHECKOUT_UNTRACKED => StashApplyProgress::CheckoutUntracked, + raw::GIT_STASH_APPLY_PROGRESS_CHECKOUT_MODIFIED => StashApplyProgress::CheckoutModified, + raw::GIT_STASH_APPLY_PROGRESS_DONE => StashApplyProgress::Done, + + _ => StashApplyProgress::None, + } +} + +extern "C" fn stash_apply_progress_cb( + progress: raw::git_stash_apply_progress_t, + payload: *mut c_void, +) -> c_int { + panic::wrap(|| unsafe { + let options = &mut *(payload as *mut StashApplyOptions<'_>); + let res = { + let callback = options.progress.as_mut().unwrap(); + callback(convert_progress(progress)) + }; + + if res { + 0 + } else { + -1 + } + }) + .unwrap_or(-1) +} + +#[cfg(test)] +mod tests { + use crate::stash::{StashApplyOptions, StashSaveOptions}; + use crate::test::repo_init; + use crate::{IndexAddOption, Repository, StashFlags, Status}; + use std::fs; + use std::path::{Path, PathBuf}; + + fn make_stash<C>(next: C) + where + C: FnOnce(&mut Repository), + { + let (_td, mut repo) = repo_init(); + let signature = repo.signature().unwrap(); + + let p = Path::new(repo.workdir().unwrap()).join("file_b.txt"); + println!("using path {:?}", p); + + fs::write(&p, "data".as_bytes()).unwrap(); + + let rel_p = Path::new("file_b.txt"); + assert!(repo.status_file(&rel_p).unwrap() == Status::WT_NEW); + + repo.stash_save(&signature, "msg1", Some(StashFlags::INCLUDE_UNTRACKED)) + .unwrap(); + + assert!(repo.status_file(&rel_p).is_err()); + + let mut count = 0; + repo.stash_foreach(|index, name, _oid| { + count += 1; + assert!(index == 0); + assert!(name == "On main: msg1"); + true + }) + .unwrap(); + + assert!(count == 1); + next(&mut repo); + } + + fn count_stash(repo: &mut Repository) -> usize { + let mut count = 0; + repo.stash_foreach(|_, _, _| { + count += 1; + true + }) + .unwrap(); + count + } + + #[test] + fn smoke_stash_save_drop() { + make_stash(|repo| { + repo.stash_drop(0).unwrap(); + assert!(count_stash(repo) == 0) + }) + } + + #[test] + fn smoke_stash_save_pop() { + make_stash(|repo| { + repo.stash_pop(0, None).unwrap(); + assert!(count_stash(repo) == 0) + }) + } + + #[test] + fn smoke_stash_save_apply() { + make_stash(|repo| { + let mut options = StashApplyOptions::new(); + options.progress_cb(|progress| { + println!("{:?}", progress); + true + }); + + repo.stash_apply(0, Some(&mut options)).unwrap(); + assert!(count_stash(repo) == 1) + }) + } + + #[test] + fn test_stash_save2_msg_none() { + let (_td, mut repo) = repo_init(); + let signature = repo.signature().unwrap(); + + let p = Path::new(repo.workdir().unwrap()).join("file_b.txt"); + + fs::write(&p, "data".as_bytes()).unwrap(); + + repo.stash_save2(&signature, None, Some(StashFlags::INCLUDE_UNTRACKED)) + .unwrap(); + + let mut stash_name = String::new(); + repo.stash_foreach(|index, name, _oid| { + assert_eq!(index, 0); + stash_name = name.to_string(); + true + }) + .unwrap(); + + assert!(stash_name.starts_with("WIP on main:")); + } + + fn create_file(r: &Repository, name: &str, data: &str) -> PathBuf { + let p = Path::new(r.workdir().unwrap()).join(name); + fs::write(&p, data).unwrap(); + p + } + + #[test] + fn test_stash_save_ext() { + let (_td, mut repo) = repo_init(); + let signature = repo.signature().unwrap(); + + create_file(&repo, "file_a", "foo"); + create_file(&repo, "file_b", "foo"); + + let mut index = repo.index().unwrap(); + index + .add_all(["*"].iter(), IndexAddOption::DEFAULT, None) + .unwrap(); + index.write().unwrap(); + + assert_eq!(repo.statuses(None).unwrap().len(), 2); + + let mut opt = StashSaveOptions::new(signature); + opt.pathspec("file_a"); + repo.stash_save_ext(Some(&mut opt)).unwrap(); + + assert_eq!(repo.statuses(None).unwrap().len(), 0); + + repo.stash_pop(0, None).unwrap(); + + assert_eq!(repo.statuses(None).unwrap().len(), 1); + } +} diff --git a/extra/git2/src/status.rs b/extra/git2/src/status.rs new file mode 100644 index 000000000..a5a8cffd3 --- /dev/null +++ b/extra/git2/src/status.rs @@ -0,0 +1,435 @@ +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; +use std::str; + +use crate::util::{self, Binding}; +use crate::{raw, DiffDelta, IntoCString, Repository, Status}; + +/// Options that can be provided to `repo.statuses()` to control how the status +/// information is gathered. +pub struct StatusOptions { + raw: raw::git_status_options, + pathspec: Vec<CString>, + ptrs: Vec<*const c_char>, +} + +/// Enumeration of possible methods of what can be shown through a status +/// operation. +#[derive(Copy, Clone)] +pub enum StatusShow { + /// Only gives status based on HEAD to index comparison, not looking at + /// working directory changes. + Index, + + /// Only gives status based on index to working directory comparison, not + /// comparing the index to the HEAD. + Workdir, + + /// The default, this roughly matches `git status --porcelain` regarding + /// which files are included and in what order. + IndexAndWorkdir, +} + +/// A container for a list of status information about a repository. +/// +/// Each instance appears as if it were a collection, having a length and +/// allowing indexing, as well as providing an iterator. +pub struct Statuses<'repo> { + raw: *mut raw::git_status_list, + + // Hm, not currently present, but can't hurt? + _marker: marker::PhantomData<&'repo Repository>, +} + +/// An iterator over the statuses in a `Statuses` instance. +pub struct StatusIter<'statuses> { + statuses: &'statuses Statuses<'statuses>, + range: Range<usize>, +} + +/// A structure representing an entry in the `Statuses` structure. +/// +/// Instances are created through the `.iter()` method or the `.get()` method. +pub struct StatusEntry<'statuses> { + raw: *const raw::git_status_entry, + _marker: marker::PhantomData<&'statuses DiffDelta<'statuses>>, +} + +impl Default for StatusOptions { + fn default() -> Self { + Self::new() + } +} + +impl StatusOptions { + /// Creates a new blank set of status options. + pub fn new() -> StatusOptions { + unsafe { + let mut raw = mem::zeroed(); + let r = raw::git_status_init_options(&mut raw, raw::GIT_STATUS_OPTIONS_VERSION); + assert_eq!(r, 0); + StatusOptions { + raw, + pathspec: Vec::new(), + ptrs: Vec::new(), + } + } + } + + /// Select the files on which to report status. + /// + /// The default, if unspecified, is to show the index and the working + /// directory. + pub fn show(&mut self, show: StatusShow) -> &mut StatusOptions { + self.raw.show = match show { + StatusShow::Index => raw::GIT_STATUS_SHOW_INDEX_ONLY, + StatusShow::Workdir => raw::GIT_STATUS_SHOW_WORKDIR_ONLY, + StatusShow::IndexAndWorkdir => raw::GIT_STATUS_SHOW_INDEX_AND_WORKDIR, + }; + self + } + + /// Add a path pattern to match (using fnmatch-style matching). + /// + /// If the `disable_pathspec_match` option is given, then this is a literal + /// path to match. If this is not called, then there will be no patterns to + /// match and the entire directory will be used. + pub fn pathspec<T: IntoCString>(&mut self, pathspec: T) -> &mut StatusOptions { + let s = util::cstring_to_repo_path(pathspec).unwrap(); + self.ptrs.push(s.as_ptr()); + self.pathspec.push(s); + self + } + + fn flag(&mut self, flag: raw::git_status_opt_t, val: bool) -> &mut StatusOptions { + if val { + self.raw.flags |= flag as c_uint; + } else { + self.raw.flags &= !(flag as c_uint); + } + self + } + + /// Flag whether untracked files will be included. + /// + /// Untracked files will only be included if the workdir files are included + /// in the status "show" option. + pub fn include_untracked(&mut self, include: bool) -> &mut StatusOptions { + self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNTRACKED, include) + } + + /// Flag whether ignored files will be included. + /// + /// The files will only be included if the workdir files are included + /// in the status "show" option. + pub fn include_ignored(&mut self, include: bool) -> &mut StatusOptions { + self.flag(raw::GIT_STATUS_OPT_INCLUDE_IGNORED, include) + } + + /// Flag to include unmodified files. + pub fn include_unmodified(&mut self, include: bool) -> &mut StatusOptions { + self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNMODIFIED, include) + } + + /// Flag that submodules should be skipped. + /// + /// This only applies if there are no pending typechanges to the submodule + /// (either from or to another type). + pub fn exclude_submodules(&mut self, exclude: bool) -> &mut StatusOptions { + self.flag(raw::GIT_STATUS_OPT_EXCLUDE_SUBMODULES, exclude) + } + + /// Flag that all files in untracked directories should be included. + /// + /// Normally if an entire directory is new then just the top-level directory + /// is included (with a trailing slash on the entry name). + pub fn recurse_untracked_dirs(&mut self, include: bool) -> &mut StatusOptions { + self.flag(raw::GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS, include) + } + + /// Indicates that the given paths should be treated as literals paths, note + /// patterns. + pub fn disable_pathspec_match(&mut self, include: bool) -> &mut StatusOptions { + self.flag(raw::GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH, include) + } + + /// Indicates that the contents of ignored directories should be included in + /// the status. + pub fn recurse_ignored_dirs(&mut self, include: bool) -> &mut StatusOptions { + self.flag(raw::GIT_STATUS_OPT_RECURSE_IGNORED_DIRS, include) + } + + /// Indicates that rename detection should be processed between the head. + pub fn renames_head_to_index(&mut self, include: bool) -> &mut StatusOptions { + self.flag(raw::GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX, include) + } + + /// Indicates that rename detection should be run between the index and the + /// working directory. + pub fn renames_index_to_workdir(&mut self, include: bool) -> &mut StatusOptions { + self.flag(raw::GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR, include) + } + + /// Override the native case sensitivity for the file system and force the + /// output to be in case sensitive order. + pub fn sort_case_sensitively(&mut self, include: bool) -> &mut StatusOptions { + self.flag(raw::GIT_STATUS_OPT_SORT_CASE_SENSITIVELY, include) + } + + /// Override the native case sensitivity for the file system and force the + /// output to be in case-insensitive order. + pub fn sort_case_insensitively(&mut self, include: bool) -> &mut StatusOptions { + self.flag(raw::GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY, include) + } + + /// Indicates that rename detection should include rewritten files. + pub fn renames_from_rewrites(&mut self, include: bool) -> &mut StatusOptions { + self.flag(raw::GIT_STATUS_OPT_RENAMES_FROM_REWRITES, include) + } + + /// Bypasses the default status behavior of doing a "soft" index reload. + pub fn no_refresh(&mut self, include: bool) -> &mut StatusOptions { + self.flag(raw::GIT_STATUS_OPT_NO_REFRESH, include) + } + + /// Refresh the stat cache in the index for files are unchanged but have + /// out of date stat information in the index. + /// + /// This will result in less work being done on subsequent calls to fetching + /// the status. + pub fn update_index(&mut self, include: bool) -> &mut StatusOptions { + self.flag(raw::GIT_STATUS_OPT_UPDATE_INDEX, include) + } + + // erm... + #[allow(missing_docs)] + pub fn include_unreadable(&mut self, include: bool) -> &mut StatusOptions { + self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNREADABLE, include) + } + + // erm... + #[allow(missing_docs)] + pub fn include_unreadable_as_untracked(&mut self, include: bool) -> &mut StatusOptions { + self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNREADABLE_AS_UNTRACKED, include) + } + + /// Set threshold above which similar files will be considered renames. + /// + /// This is equivalent to the `-M` option. Defaults to 50. + pub fn rename_threshold(&mut self, threshold: u16) -> &mut StatusOptions { + self.raw.rename_threshold = threshold; + self + } + + /// Get a pointer to the inner list of status options. + /// + /// This function is unsafe as the returned structure has interior pointers + /// and may no longer be valid if these options continue to be mutated. + pub unsafe fn raw(&mut self) -> *const raw::git_status_options { + self.raw.pathspec.strings = self.ptrs.as_ptr() as *mut _; + self.raw.pathspec.count = self.ptrs.len() as size_t; + &self.raw + } +} + +impl<'repo> Statuses<'repo> { + /// Gets a status entry from this list at the specified index. + /// + /// Returns `None` if the index is out of bounds. + pub fn get(&self, index: usize) -> Option<StatusEntry<'_>> { + unsafe { + let p = raw::git_status_byindex(self.raw, index as size_t); + Binding::from_raw_opt(p) + } + } + + /// Gets the count of status entries in this list. + /// + /// If there are no changes in status (according to the options given + /// when the status list was created), this should return 0. + pub fn len(&self) -> usize { + unsafe { raw::git_status_list_entrycount(self.raw) as usize } + } + + /// Return `true` if there is no status entry in this list. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns an iterator over the statuses in this list. + pub fn iter(&self) -> StatusIter<'_> { + StatusIter { + statuses: self, + range: 0..self.len(), + } + } +} + +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, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_status_list { + self.raw + } +} + +impl<'repo> Drop for Statuses<'repo> { + fn drop(&mut self) { + unsafe { + raw::git_status_list_free(self.raw); + } + } +} + +impl<'a> Iterator for StatusIter<'a> { + type Item = StatusEntry<'a>; + fn next(&mut self) -> Option<StatusEntry<'a>> { + self.range.next().and_then(|i| self.statuses.get(i)) + } + fn size_hint(&self) -> (usize, Option<usize>) { + self.range.size_hint() + } +} +impl<'a> DoubleEndedIterator for StatusIter<'a> { + fn next_back(&mut self) -> Option<StatusEntry<'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] { + unsafe { + if (*self.raw).head_to_index.is_null() { + crate::opt_bytes(self, (*(*self.raw).index_to_workdir).old_file.path) + } else { + crate::opt_bytes(self, (*(*self.raw).head_to_index).old_file.path) + } + .unwrap() + } + } + + /// Access this entry's path name as a string. + /// + /// Returns `None` if the path is not valid utf-8. + pub fn path(&self) -> Option<&str> { + str::from_utf8(self.path_bytes()).ok() + } + + /// Access the status flags for this file + pub fn status(&self) -> Status { + Status::from_bits_truncate(unsafe { (*self.raw).status as u32 }) + } + + /// Access detailed information about the differences between the file in + /// HEAD and the file in the index. + pub fn head_to_index(&self) -> Option<DiffDelta<'statuses>> { + unsafe { Binding::from_raw_opt((*self.raw).head_to_index) } + } + + /// Access detailed information about the differences between the file in + /// the index and the file in the working directory. + pub fn index_to_workdir(&self) -> Option<DiffDelta<'statuses>> { + unsafe { Binding::from_raw_opt((*self.raw).index_to_workdir) } + } +} + +impl<'statuses> Binding for StatusEntry<'statuses> { + type Raw = *const raw::git_status_entry; + + unsafe fn from_raw(raw: *const raw::git_status_entry) -> StatusEntry<'statuses> { + StatusEntry { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *const raw::git_status_entry { + self.raw + } +} + +#[cfg(test)] +mod tests { + use super::StatusOptions; + use std::fs::File; + use std::io::prelude::*; + use std::path::Path; + + #[test] + fn smoke() { + let (td, repo) = crate::test::repo_init(); + assert_eq!(repo.statuses(None).unwrap().len(), 0); + File::create(&td.path().join("foo")).unwrap(); + let statuses = repo.statuses(None).unwrap(); + assert_eq!(statuses.iter().count(), 1); + let status = statuses.iter().next().unwrap(); + assert_eq!(status.path(), Some("foo")); + assert!(status.status().contains(crate::Status::WT_NEW)); + assert!(!status.status().contains(crate::Status::INDEX_NEW)); + assert!(status.head_to_index().is_none()); + let diff = status.index_to_workdir().unwrap(); + assert_eq!(diff.old_file().path_bytes().unwrap(), b"foo"); + assert_eq!(diff.new_file().path_bytes().unwrap(), b"foo"); + } + + #[test] + fn filter() { + let (td, repo) = crate::test::repo_init(); + t!(File::create(&td.path().join("foo"))); + t!(File::create(&td.path().join("bar"))); + let mut opts = StatusOptions::new(); + opts.include_untracked(true).pathspec("foo"); + + let statuses = t!(repo.statuses(Some(&mut opts))); + assert_eq!(statuses.iter().count(), 1); + let status = statuses.iter().next().unwrap(); + assert_eq!(status.path(), Some("foo")); + } + + #[test] + fn gitignore() { + let (td, repo) = crate::test::repo_init(); + t!(t!(File::create(td.path().join(".gitignore"))).write_all(b"foo\n")); + assert!(!t!(repo.status_should_ignore(Path::new("bar")))); + assert!(t!(repo.status_should_ignore(Path::new("foo")))); + } + + #[test] + fn status_file() { + let (td, repo) = crate::test::repo_init(); + assert!(repo.status_file(Path::new("foo")).is_err()); + if cfg!(windows) { + assert!(repo.status_file(Path::new("bar\\foo.txt")).is_err()); + } + t!(File::create(td.path().join("foo"))); + if cfg!(windows) { + t!(::std::fs::create_dir_all(td.path().join("bar"))); + t!(File::create(td.path().join("bar").join("foo.txt"))); + } + let status = t!(repo.status_file(Path::new("foo"))); + assert!(status.contains(crate::Status::WT_NEW)); + if cfg!(windows) { + let status = t!(repo.status_file(Path::new("bar\\foo.txt"))); + assert!(status.contains(crate::Status::WT_NEW)); + } + } +} diff --git a/extra/git2/src/string_array.rs b/extra/git2/src/string_array.rs new file mode 100644 index 000000000..c77ccdab9 --- /dev/null +++ b/extra/git2/src/string_array.rs @@ -0,0 +1,136 @@ +//! Bindings to libgit2's raw `git_strarray` type + +use std::iter::FusedIterator; +use std::ops::Range; +use std::str; + +use crate::raw; +use crate::util::Binding; + +/// A string array structure used by libgit2 +/// +/// Some APIs return arrays of strings which originate from libgit2. This +/// wrapper type behaves a little like `Vec<&str>` but does so without copying +/// the underlying strings until necessary. +pub struct StringArray { + raw: raw::git_strarray, +} + +/// A forward iterator over the strings of an array, casted to `&str`. +pub struct Iter<'a> { + range: Range<usize>, + arr: &'a StringArray, +} + +/// A forward iterator over the strings of an array, casted to `&[u8]`. +pub struct IterBytes<'a> { + range: Range<usize>, + arr: &'a StringArray, +} + +impl StringArray { + /// Returns None if the i'th string is not utf8 or if i is out of bounds. + pub fn get(&self, i: usize) -> Option<&str> { + self.get_bytes(i).and_then(|s| str::from_utf8(s).ok()) + } + + /// Returns None if `i` is out of bounds. + pub fn get_bytes(&self, i: usize) -> Option<&[u8]> { + if i < self.raw.count as usize { + unsafe { + let ptr = *self.raw.strings.add(i) as *const _; + Some(crate::opt_bytes(self, ptr).unwrap()) + } + } else { + None + } + } + + /// Returns an iterator over the strings contained within this array. + /// + /// The iterator yields `Option<&str>` as it is unknown whether the contents + /// are utf-8 or not. + pub fn iter(&self) -> Iter<'_> { + Iter { + range: 0..self.len(), + arr: self, + } + } + + /// Returns an iterator over the strings contained within this array, + /// yielding byte slices. + pub fn iter_bytes(&self) -> IterBytes<'_> { + IterBytes { + range: 0..self.len(), + arr: self, + } + } + + /// Returns the number of strings in this array. + pub fn len(&self) -> usize { + self.raw.count as usize + } + + /// Return `true` if this array is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +impl Binding for StringArray { + type Raw = raw::git_strarray; + unsafe fn from_raw(raw: raw::git_strarray) -> StringArray { + StringArray { raw } + } + fn raw(&self) -> raw::git_strarray { + self.raw + } +} + +impl<'a> IntoIterator for &'a StringArray { + type Item = Option<&'a str>; + type IntoIter = Iter<'a>; + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl<'a> Iterator for Iter<'a> { + type Item = Option<&'a str>; + fn next(&mut self) -> Option<Option<&'a str>> { + self.range.next().map(|i| self.arr.get(i)) + } + fn size_hint(&self) -> (usize, Option<usize>) { + self.range.size_hint() + } +} +impl<'a> DoubleEndedIterator for Iter<'a> { + fn next_back(&mut self) -> Option<Option<&'a str>> { + 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> { + type Item = &'a [u8]; + fn next(&mut self) -> Option<&'a [u8]> { + self.range.next().and_then(|i| self.arr.get_bytes(i)) + } + fn size_hint(&self) -> (usize, Option<usize>) { + self.range.size_hint() + } +} +impl<'a> DoubleEndedIterator for IterBytes<'a> { + fn next_back(&mut self) -> Option<&'a [u8]> { + self.range.next_back().and_then(|i| self.arr.get_bytes(i)) + } +} +impl<'a> FusedIterator for IterBytes<'a> {} +impl<'a> ExactSizeIterator for IterBytes<'a> {} + +impl Drop for StringArray { + fn drop(&mut self) { + unsafe { raw::git_strarray_free(&mut self.raw) } + } +} diff --git a/extra/git2/src/submodule.rs b/extra/git2/src/submodule.rs new file mode 100644 index 000000000..06a635940 --- /dev/null +++ b/extra/git2/src/submodule.rs @@ -0,0 +1,471 @@ +use std::marker; +use std::mem; +use std::os::raw::c_int; +use std::path::Path; +use std::ptr; +use std::str; + +use crate::util::{self, Binding}; +use crate::{build::CheckoutBuilder, SubmoduleIgnore, SubmoduleUpdate}; +use crate::{raw, Error, FetchOptions, Oid, Repository}; + +/// A structure to represent a git [submodule][1] +/// +/// [1]: http://git-scm.com/book/en/Git-Tools-Submodules +pub struct Submodule<'repo> { + raw: *mut raw::git_submodule, + _marker: marker::PhantomData<&'repo Repository>, +} + +impl<'repo> Submodule<'repo> { + /// Get the submodule's branch. + /// + /// Returns `None` if the branch is not valid utf-8 or if the branch is not + /// yet available. + pub fn branch(&self) -> Option<&str> { + self.branch_bytes().and_then(|s| str::from_utf8(s).ok()) + } + + /// Get the branch for the submodule. + /// + /// Returns `None` if the branch is not yet available. + pub fn branch_bytes(&self) -> Option<&[u8]> { + unsafe { crate::opt_bytes(self, raw::git_submodule_branch(self.raw)) } + } + + /// Perform the clone step for a newly created submodule. + /// + /// This performs the necessary `git_clone` to setup a newly-created submodule. + pub fn clone( + &mut self, + opts: Option<&mut SubmoduleUpdateOptions<'_>>, + ) -> Result<Repository, Error> { + unsafe { + let raw_opts = opts.map(|o| o.raw()); + let mut raw_repo = ptr::null_mut(); + try_call!(raw::git_submodule_clone( + &mut raw_repo, + self.raw, + raw_opts.as_ref() + )); + Ok(Binding::from_raw(raw_repo)) + } + } + + /// Get the submodule's URL. + /// + /// 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. + #[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. + /// + /// Returns `None` if the URL isn't present + // TODO: delete this method and fix the signature of `url_bytes` on next + // major version bump + pub fn opt_url_bytes(&self) -> Option<&[u8]> { + unsafe { crate::opt_bytes(self, raw::git_submodule_url(self.raw)) } + } + + /// Get the submodule's name. + /// + /// Returns `None` if the name is not valid utf-8 + pub fn name(&self) -> Option<&str> { + str::from_utf8(self.name_bytes()).ok() + } + + /// Get the name for the submodule. + pub fn name_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, raw::git_submodule_name(self.raw)).unwrap() } + } + + /// Get the path for the submodule. + pub fn path(&self) -> &Path { + util::bytes2path(unsafe { + crate::opt_bytes(self, raw::git_submodule_path(self.raw)).unwrap() + }) + } + + /// Get the OID for the submodule in the current HEAD tree. + pub fn head_id(&self) -> Option<Oid> { + unsafe { Binding::from_raw_opt(raw::git_submodule_head_id(self.raw)) } + } + + /// Get the OID for the submodule in the index. + pub fn index_id(&self) -> Option<Oid> { + unsafe { Binding::from_raw_opt(raw::git_submodule_index_id(self.raw)) } + } + + /// Get the OID for the submodule in the current working directory. + /// + /// This returns the OID that corresponds to looking up 'HEAD' in the + /// checked out submodule. If there are pending changes in the index or + /// anything else, this won't notice that. + pub fn workdir_id(&self) -> Option<Oid> { + unsafe { Binding::from_raw_opt(raw::git_submodule_wd_id(self.raw)) } + } + + /// Get the ignore rule that will be used for the submodule. + pub fn ignore_rule(&self) -> SubmoduleIgnore { + SubmoduleIgnore::from_raw(unsafe { raw::git_submodule_ignore(self.raw) }) + } + + /// Get the update rule that will be used for the submodule. + pub fn update_strategy(&self) -> SubmoduleUpdate { + SubmoduleUpdate::from_raw(unsafe { raw::git_submodule_update_strategy(self.raw) }) + } + + /// Copy submodule info into ".git/config" file. + /// + /// Just like "git submodule init", this copies information about the + /// submodule into ".git/config". You can use the accessor functions above + /// to alter the in-memory git_submodule object and control what is written + /// to the config, overriding what is in .gitmodules. + /// + /// By default, existing entries will not be overwritten, but passing `true` + /// for `overwrite` forces them to be updated. + pub fn init(&mut self, overwrite: bool) -> Result<(), Error> { + unsafe { + try_call!(raw::git_submodule_init(self.raw, overwrite)); + } + 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<Repository, Error> { + unsafe { + let mut raw_repo = ptr::null_mut(); + try_call!(raw::git_submodule_repo_init( + &mut raw_repo, + self.raw, + use_gitlink + )); + Ok(Binding::from_raw(raw_repo)) + } + } + + /// Open the repository for a submodule. + /// + /// This will only work if the submodule is checked out into the working + /// directory. + pub fn open(&self) -> Result<Repository, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_submodule_open(&mut raw, self.raw)); + Ok(Binding::from_raw(raw)) + } + } + + /// Reread submodule info from config, index, and HEAD. + /// + /// Call this to reread cached submodule information for this submodule if + /// you have reason to believe that it has changed. + /// + /// If `force` is `true`, then data will be reloaded even if it doesn't seem + /// out of date + pub fn reload(&mut self, force: bool) -> Result<(), Error> { + unsafe { + try_call!(raw::git_submodule_reload(self.raw, force)); + } + Ok(()) + } + + /// Copy submodule remote info into submodule repo. + /// + /// This copies the information about the submodules URL into the checked + /// out submodule config, acting like "git submodule sync". This is useful + /// if you have altered the URL for the submodule (or it has been altered + /// by a fetch of upstream changes) and you need to update your local repo. + pub fn sync(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_submodule_sync(self.raw)); + } + Ok(()) + } + + /// Add current submodule HEAD commit to index of superproject. + /// + /// If `write_index` is true, then the index file will be immediately + /// written. Otherwise you must explicitly call `write()` on an `Index` + /// later on. + pub fn add_to_index(&mut self, write_index: bool) -> Result<(), Error> { + unsafe { + try_call!(raw::git_submodule_add_to_index(self.raw, write_index)); + } + Ok(()) + } + + /// Resolve the setup of a new git submodule. + /// + /// This should be called on a submodule once you have called add setup and + /// done the clone of the submodule. This adds the .gitmodules file and the + /// newly cloned submodule to the index to be ready to be committed (but + /// doesn't actually do the commit). + pub fn add_finalize(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_submodule_add_finalize(self.raw)); + } + Ok(()) + } + + /// Update submodule. + /// + /// This will clone a missing submodule and check out the subrepository to + /// the commit specified in the index of the containing repository. If + /// the submodule repository doesn't contain the target commit, then the + /// submodule is fetched using the fetch options supplied in `opts`. + /// + /// `init` indicates if the submodule should be initialized first if it has + /// not been initialized yet. + pub fn update( + &mut self, + init: bool, + opts: Option<&mut SubmoduleUpdateOptions<'_>>, + ) -> Result<(), Error> { + unsafe { + let mut raw_opts = opts.map(|o| o.raw()); + try_call!(raw::git_submodule_update( + self.raw, + init as c_int, + raw_opts.as_mut().map_or(ptr::null_mut(), |o| o) + )); + } + Ok(()) + } +} + +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, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_submodule { + self.raw + } +} + +impl<'repo> Drop for Submodule<'repo> { + fn drop(&mut self) { + unsafe { raw::git_submodule_free(self.raw) } + } +} + +/// Options to update a submodule. +pub struct SubmoduleUpdateOptions<'cb> { + checkout_builder: CheckoutBuilder<'cb>, + fetch_opts: FetchOptions<'cb>, + allow_fetch: bool, +} + +impl<'cb> SubmoduleUpdateOptions<'cb> { + /// Return default options. + pub fn new() -> Self { + SubmoduleUpdateOptions { + checkout_builder: CheckoutBuilder::new(), + fetch_opts: FetchOptions::new(), + allow_fetch: true, + } + } + + unsafe fn raw(&mut self) -> raw::git_submodule_update_options { + let mut checkout_opts: raw::git_checkout_options = mem::zeroed(); + let init_res = + raw::git_checkout_init_options(&mut checkout_opts, raw::GIT_CHECKOUT_OPTIONS_VERSION); + assert_eq!(0, init_res); + self.checkout_builder.configure(&mut checkout_opts); + let opts = raw::git_submodule_update_options { + version: raw::GIT_SUBMODULE_UPDATE_OPTIONS_VERSION, + checkout_opts, + fetch_opts: self.fetch_opts.raw(), + allow_fetch: self.allow_fetch as c_int, + }; + opts + } + + /// Set checkout options. + pub fn checkout(&mut self, opts: CheckoutBuilder<'cb>) -> &mut Self { + self.checkout_builder = opts; + self + } + + /// Set fetch options and allow fetching. + pub fn fetch(&mut self, opts: FetchOptions<'cb>) -> &mut Self { + self.fetch_opts = opts; + self.allow_fetch = true; + self + } + + /// Allow or disallow fetching. + pub fn allow_fetch(&mut self, b: bool) -> &mut Self { + self.allow_fetch = b; + self + } +} + +impl<'cb> Default for SubmoduleUpdateOptions<'cb> { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::path::Path; + use tempfile::TempDir; + use url::Url; + + use crate::Repository; + use crate::SubmoduleUpdateOptions; + + #[test] + fn smoke() { + let td = TempDir::new().unwrap(); + let repo = Repository::init(td.path()).unwrap(); + let mut s1 = repo + .submodule("/path/to/nowhere", Path::new("foo"), true) + .unwrap(); + s1.init(false).unwrap(); + s1.sync().unwrap(); + + let s2 = repo + .submodule("/path/to/nowhere", Path::new("bar"), true) + .unwrap(); + drop((s1, s2)); + + let mut submodules = repo.submodules().unwrap(); + assert_eq!(submodules.len(), 2); + let mut s = submodules.remove(0); + assert_eq!(s.name(), Some("bar")); + assert_eq!(s.url(), Some("/path/to/nowhere")); + assert_eq!(s.branch(), None); + assert!(s.head_id().is_none()); + assert!(s.index_id().is_none()); + assert!(s.workdir_id().is_none()); + + repo.find_submodule("bar").unwrap(); + s.open().unwrap(); + assert!(s.path() == Path::new("bar")); + s.reload(true).unwrap(); + } + + #[test] + fn add_a_submodule() { + let (_td, repo1) = crate::test::repo_init(); + let (td, repo2) = crate::test::repo_init(); + + let url = Url::from_file_path(&repo1.workdir().unwrap()).unwrap(); + let mut s = repo2 + .submodule(&url.to_string(), Path::new("bar"), true) + .unwrap(); + t!(fs::remove_dir_all(td.path().join("bar"))); + t!(Repository::clone(&url.to_string(), td.path().join("bar"))); + t!(s.add_to_index(false)); + t!(s.add_finalize()); + } + + #[test] + fn update_submodule() { + // ----------------------------------- + // Same as `add_a_submodule()` + let (_td, repo1) = crate::test::repo_init(); + let (td, repo2) = crate::test::repo_init(); + + let url = Url::from_file_path(&repo1.workdir().unwrap()).unwrap(); + let mut s = repo2 + .submodule(&url.to_string(), Path::new("bar"), true) + .unwrap(); + t!(fs::remove_dir_all(td.path().join("bar"))); + t!(Repository::clone(&url.to_string(), td.path().join("bar"))); + t!(s.add_to_index(false)); + t!(s.add_finalize()); + // ----------------------------------- + + // Attempt to update submodule + let submodules = t!(repo1.submodules()); + for mut submodule in submodules { + let mut submodule_options = SubmoduleUpdateOptions::new(); + let init = true; + let opts = Some(&mut submodule_options); + + t!(submodule.update(init, opts)); + } + } + + #[test] + fn clone_submodule() { + // ----------------------------------- + // Same as `add_a_submodule()` + let (_td, repo1) = crate::test::repo_init(); + let (_td, repo2) = crate::test::repo_init(); + let (_td, parent) = crate::test::repo_init(); + + let url1 = Url::from_file_path(&repo1.workdir().unwrap()).unwrap(); + let url2 = Url::from_file_path(&repo2.workdir().unwrap()).unwrap(); + let mut s1 = parent + .submodule(&url1.to_string(), Path::new("bar"), true) + .unwrap(); + let mut s2 = parent + .submodule(&url2.to_string(), Path::new("bar2"), true) + .unwrap(); + // ----------------------------------- + + t!(s1.clone(Some(&mut SubmoduleUpdateOptions::default()))); + t!(s2.clone(None)); + } + + #[test] + fn repo_init_submodule() { + // ----------------------------------- + // Same as `clone_submodule()` + let (_td, child) = crate::test::repo_init(); + let (_td, parent) = crate::test::repo_init(); + + let url_child = Url::from_file_path(&child.workdir().unwrap()).unwrap(); + let url_parent = Url::from_file_path(&parent.workdir().unwrap()).unwrap(); + let mut sub = parent + .submodule(&url_child.to_string(), Path::new("bar"), true) + .unwrap(); + + // ----------------------------------- + // Let's commit the submodule for later clone + t!(sub.clone(None)); + t!(sub.add_to_index(true)); + t!(sub.add_finalize()); + + crate::test::commit(&parent); + + // Clone the parent to init its submodules + let td = TempDir::new().unwrap(); + let new_parent = Repository::clone(&url_parent.to_string(), &td).unwrap(); + + let mut submodules = new_parent.submodules().unwrap(); + let child = submodules.first_mut().unwrap(); + + // First init child + t!(child.init(false)); + assert_eq!(child.url().unwrap(), url_child.as_str()); + + // open() is not possible before initializing the repo + assert!(child.open().is_err()); + t!(child.repo_init(true)); + assert!(child.open().is_ok()); + } +} diff --git a/extra/git2/src/tag.rs b/extra/git2/src/tag.rs new file mode 100644 index 000000000..6986c7c16 --- /dev/null +++ b/extra/git2/src/tag.rs @@ -0,0 +1,234 @@ +use std::ffi::CString; +use std::marker; +use std::mem; +use std::ptr; +use std::str; + +use crate::util::Binding; +use crate::{call, raw, signature, Error, Object, ObjectType, Oid, Signature}; + +/// A structure to represent a git [tag][1] +/// +/// [1]: http://git-scm.com/book/en/Git-Basics-Tagging +pub struct Tag<'repo> { + raw: *mut raw::git_tag, + _marker: marker::PhantomData<Object<'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)) } + } + + /// Get the message of a tag + /// + /// Returns None if there is no message or if it is not valid utf8 + pub fn message(&self) -> Option<&str> { + self.message_bytes().and_then(|s| str::from_utf8(s).ok()) + } + + /// Get the message of a tag + /// + /// Returns None if there is no message + pub fn message_bytes(&self) -> Option<&[u8]> { + unsafe { crate::opt_bytes(self, raw::git_tag_message(&*self.raw)) } + } + + /// Get the name of a tag + /// + /// Returns None if it is not valid utf8 + pub fn name(&self) -> Option<&str> { + str::from_utf8(self.name_bytes()).ok() + } + + /// Get the name of a tag + pub fn name_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, raw::git_tag_name(&*self.raw)).unwrap() } + } + + /// Recursively peel a tag until a non tag git_object is found + pub fn peel(&self) -> Result<Object<'repo>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_tag_peel(&mut ret, &*self.raw)); + Ok(Binding::from_raw(ret)) + } + } + + /// Get the tagger (author) of a tag + /// + /// If the author is unspecified, then `None` is returned. + pub fn tagger(&self) -> Option<Signature<'_>> { + unsafe { + let ptr = raw::git_tag_tagger(&*self.raw); + if ptr.is_null() { + None + } else { + Some(signature::from_raw_const(self, ptr)) + } + } + } + + /// Get the tagged object of a tag + /// + /// This method performs a repository lookup for the given object and + /// returns it + pub fn target(&self) -> Result<Object<'repo>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_tag_target(&mut ret, &*self.raw)); + Ok(Binding::from_raw(ret)) + } + } + + /// Get the OID of the tagged object of a tag + pub fn target_id(&self) -> Oid { + unsafe { Binding::from_raw(raw::git_tag_target_id(&*self.raw)) } + } + + /// Get the ObjectType of the tagged object of a tag + pub fn target_type(&self) -> Option<ObjectType> { + unsafe { ObjectType::from_raw(raw::git_tag_target_type(&*self.raw)) } + } + + /// Casts this Tag to be usable as an `Object` + pub fn as_object(&self) -> &Object<'repo> { + unsafe { &*(self as *const _ as *const Object<'repo>) } + } + + /// Consumes Tag to be returned as an `Object` + pub fn into_object(self) -> Object<'repo> { + assert_eq!(mem::size_of_val(&self), mem::size_of::<Object<'_>>()); + unsafe { mem::transmute(self) } + } +} + +impl<'repo> std::fmt::Debug for Tag<'repo> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut ds = f.debug_struct("Tag"); + if let Some(name) = self.name() { + ds.field("name", &name); + } + ds.field("id", &self.id()); + ds.finish() + } +} + +impl<'repo> Binding for Tag<'repo> { + type Raw = *mut raw::git_tag; + unsafe fn from_raw(raw: *mut raw::git_tag) -> Tag<'repo> { + Tag { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_tag { + self.raw + } +} + +impl<'repo> Clone for Tag<'repo> { + fn clone(&self) -> Self { + self.as_object().clone().into_tag().ok().unwrap() + } +} + +impl<'repo> Drop for Tag<'repo> { + fn drop(&mut self) { + unsafe { raw::git_tag_free(self.raw) } + } +} + +#[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(); + let head = repo.head().unwrap(); + let id = head.target().unwrap(); + assert!(repo.find_tag(id).is_err()); + + let obj = repo.find_object(id, None).unwrap(); + let sig = repo.signature().unwrap(); + let tag_id = repo.tag("foo", &obj, &sig, "msg", false).unwrap(); + let tag = repo.find_tag(tag_id).unwrap(); + assert_eq!(tag.id(), tag_id); + + let tags = repo.tag_names(None).unwrap(); + assert_eq!(tags.len(), 1); + assert_eq!(tags.get(0), Some("foo")); + + assert_eq!(tag.name(), Some("foo")); + assert_eq!(tag.message(), Some("msg")); + assert_eq!(tag.peel().unwrap().id(), obj.id()); + assert_eq!(tag.target_id(), obj.id()); + assert_eq!(tag.target_type(), Some(crate::ObjectType::Commit)); + + assert_eq!(tag.tagger().unwrap().name(), sig.name()); + tag.target().unwrap(); + tag.into_object(); + + repo.find_object(tag_id, None).unwrap().as_tag().unwrap(); + repo.find_object(tag_id, None) + .unwrap() + .into_tag() + .ok() + .unwrap(); + + repo.tag_delete("foo").unwrap(); + } + + #[test] + fn lite() { + let (_td, repo) = crate::test::repo_init(); + let head = t!(repo.head()); + let id = head.target().unwrap(); + let obj = t!(repo.find_object(id, None)); + let tag_id = t!(repo.tag_lightweight("foo", &obj, false)); + assert!(repo.find_tag(tag_id).is_err()); + assert_eq!(t!(repo.refname_to_id("refs/tags/foo")), id); + + let tags = t!(repo.tag_names(Some("f*"))); + assert_eq!(tags.len(), 1); + let tags = t!(repo.tag_names(Some("b*"))); + assert_eq!(tags.len(), 0); + } +} diff --git a/extra/git2/src/tagforeach.rs b/extra/git2/src/tagforeach.rs new file mode 100644 index 000000000..425eea5a4 --- /dev/null +++ b/extra/git2/src/tagforeach.rs @@ -0,0 +1,69 @@ +//! git_tag_foreach support +//! see original: <https://libgit2.org/libgit2/#HEAD/group/tag/git_tag_foreach> + +use crate::{panic, raw, util::Binding, Oid}; +use libc::{c_char, c_int}; +use raw::git_oid; +use std::ffi::{c_void, CStr}; + +/// boxed callback type +pub(crate) type TagForeachCB<'a> = Box<dyn FnMut(Oid, &[u8]) -> bool + 'a>; + +/// helper type to be able to pass callback to payload +pub(crate) struct TagForeachData<'a> { + /// callback + pub(crate) cb: TagForeachCB<'a>, +} + +/// c callback forwarding to rust callback inside `TagForeachData` +/// see original: <https://libgit2.org/libgit2/#HEAD/group/callback/git_tag_foreach_cb> +pub(crate) extern "C" fn tag_foreach_cb( + name: *const c_char, + oid: *mut git_oid, + payload: *mut c_void, +) -> c_int { + panic::wrap(|| unsafe { + let id: Oid = Binding::from_raw(oid as *const _); + + let name = CStr::from_ptr(name); + let name = name.to_bytes(); + + let payload = &mut *(payload as *mut TagForeachData<'_>); + let cb = &mut payload.cb; + + let res = cb(id, name); + + if res { + 0 + } else { + -1 + } + }) + .unwrap_or(-1) +} + +#[cfg(test)] +mod tests { + + #[test] + fn smoke() { + let (_td, repo) = crate::test::repo_init(); + let head = repo.head().unwrap(); + let id = head.target().unwrap(); + assert!(repo.find_tag(id).is_err()); + + let obj = repo.find_object(id, None).unwrap(); + let sig = repo.signature().unwrap(); + let tag_id = repo.tag("foo", &obj, &sig, "msg", false).unwrap(); + + let mut tags = Vec::new(); + repo.tag_foreach(|id, name| { + tags.push((id, String::from_utf8(name.into()).unwrap())); + true + }) + .unwrap(); + + assert_eq!(tags[0].0, tag_id); + assert_eq!(tags[0].1, "refs/tags/foo"); + } +} diff --git a/extra/git2/src/test.rs b/extra/git2/src/test.rs new file mode 100644 index 000000000..c1ff1de21 --- /dev/null +++ b/extra/git2/src/test.rs @@ -0,0 +1,89 @@ +use std::fs::File; +use std::io; +use std::path::{Path, PathBuf}; +#[cfg(unix)] +use std::ptr; +use tempfile::TempDir; +use url::Url; + +use crate::{Branch, Oid, Repository, RepositoryInitOptions}; + +macro_rules! t { + ($e:expr) => { + match $e { + Ok(e) => e, + Err(e) => panic!("{} failed with {}", stringify!($e), e), + } + }; +} + +pub fn repo_init() -> (TempDir, Repository) { + let td = TempDir::new().unwrap(); + let mut opts = RepositoryInitOptions::new(); + opts.initial_head("main"); + let repo = Repository::init_opts(td.path(), &opts).unwrap(); + { + let mut config = repo.config().unwrap(); + config.set_str("user.name", "name").unwrap(); + config.set_str("user.email", "email").unwrap(); + let mut index = repo.index().unwrap(); + let id = index.write_tree().unwrap(); + + let tree = repo.find_tree(id).unwrap(); + let sig = repo.signature().unwrap(); + repo.commit(Some("HEAD"), &sig, &sig, "initial\n\nbody", &tree, &[]) + .unwrap(); + } + (td, repo) +} + +pub fn commit(repo: &Repository) -> (Oid, Oid) { + let mut index = t!(repo.index()); + let root = repo.path().parent().unwrap(); + t!(File::create(&root.join("foo"))); + t!(index.add_path(Path::new("foo"))); + + let tree_id = t!(index.write_tree()); + let tree = t!(repo.find_tree(tree_id)); + let sig = t!(repo.signature()); + let head_id = t!(repo.refname_to_id("HEAD")); + let parent = t!(repo.find_commit(head_id)); + let commit = t!(repo.commit(Some("HEAD"), &sig, &sig, "commit", &tree, &[&parent])); + (commit, tree_id) +} + +pub fn path2url(path: &Path) -> String { + Url::from_file_path(path).unwrap().to_string() +} + +pub fn worktrees_env_init(repo: &Repository) -> (TempDir, Branch<'_>) { + let oid = repo.head().unwrap().target().unwrap(); + let commit = repo.find_commit(oid).unwrap(); + let branch = repo.branch("wt-branch", &commit, true).unwrap(); + let wtdir = TempDir::new().unwrap(); + (wtdir, branch) +} + +#[cfg(windows)] +pub fn realpath(original: &Path) -> io::Result<PathBuf> { + Ok(original.to_path_buf()) +} +#[cfg(unix)] +pub fn realpath(original: &Path) -> io::Result<PathBuf> { + use libc::c_char; + use std::ffi::{CStr, CString, OsString}; + use std::os::unix::prelude::*; + extern "C" { + fn realpath(name: *const c_char, resolved: *mut c_char) -> *mut c_char; + } + unsafe { + let cstr = CString::new(original.as_os_str().as_bytes())?; + let ptr = realpath(cstr.as_ptr(), ptr::null_mut()); + if ptr.is_null() { + return Err(io::Error::last_os_error()); + } + let bytes = CStr::from_ptr(ptr).to_bytes().to_vec(); + libc::free(ptr as *mut _); + Ok(PathBuf::from(OsString::from_vec(bytes))) + } +} diff --git a/extra/git2/src/time.rs b/extra/git2/src/time.rs new file mode 100644 index 000000000..46b5bd3f9 --- /dev/null +++ b/extra/git2/src/time.rs @@ -0,0 +1,127 @@ +use std::cmp::Ordering; + +use libc::{c_char, c_int}; + +use crate::raw; +use crate::util::Binding; + +/// Time in a signature +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct Time { + raw: raw::git_time, +} + +/// Time structure used in a git index entry. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct IndexTime { + raw: raw::git_index_time, +} + +impl Time { + /// Creates a new time structure from its components. + pub fn new(time: i64, offset: i32) -> Time { + unsafe { + Binding::from_raw(raw::git_time { + time: time as raw::git_time_t, + offset: offset as c_int, + sign: if offset < 0 { '-' } else { '+' } as c_char, + }) + } + } + + /// Return the time, in seconds, from epoch + pub fn seconds(&self) -> i64 { + self.raw.time as i64 + } + + /// Return the timezone offset, in minutes + pub fn offset_minutes(&self) -> i32 { + self.raw.offset as i32 + } + + /// Return whether the offset was positive or negative. Primarily useful + /// in case the offset is specified as a negative zero. + pub fn sign(&self) -> char { + self.raw.sign as u8 as char + } +} + +impl PartialOrd for Time { + fn partial_cmp(&self, other: &Time) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for Time { + fn cmp(&self, other: &Time) -> Ordering { + (self.raw.time, self.raw.offset).cmp(&(other.raw.time, other.raw.offset)) + } +} + +impl Binding for Time { + type Raw = raw::git_time; + unsafe fn from_raw(raw: raw::git_time) -> Time { + Time { raw } + } + fn raw(&self) -> raw::git_time { + self.raw + } +} + +impl IndexTime { + /// Creates a new time structure from its components. + pub fn new(seconds: i32, nanoseconds: u32) -> IndexTime { + unsafe { + Binding::from_raw(raw::git_index_time { + seconds, + nanoseconds, + }) + } + } + + /// Returns the number of seconds in the second component of this time. + pub fn seconds(&self) -> i32 { + self.raw.seconds + } + /// Returns the nanosecond component of this time. + pub fn nanoseconds(&self) -> u32 { + self.raw.nanoseconds + } +} + +impl Binding for IndexTime { + type Raw = raw::git_index_time; + unsafe fn from_raw(raw: raw::git_index_time) -> IndexTime { + IndexTime { raw } + } + fn raw(&self) -> raw::git_index_time { + self.raw + } +} + +impl PartialOrd for IndexTime { + fn partial_cmp(&self, other: &IndexTime) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for IndexTime { + fn cmp(&self, other: &IndexTime) -> Ordering { + let me = (self.raw.seconds, self.raw.nanoseconds); + let other = (other.raw.seconds, other.raw.nanoseconds); + me.cmp(&other) + } +} + +#[cfg(test)] +mod tests { + use crate::Time; + + #[test] + fn smoke() { + assert_eq!(Time::new(1608839587, -300).seconds(), 1608839587); + assert_eq!(Time::new(1608839587, -300).offset_minutes(), -300); + assert_eq!(Time::new(1608839587, -300).sign(), '-'); + assert_eq!(Time::new(1608839587, 300).sign(), '+'); + } +} diff --git a/extra/git2/src/tracing.rs b/extra/git2/src/tracing.rs new file mode 100644 index 000000000..5acae8a85 --- /dev/null +++ b/extra/git2/src/tracing.rs @@ -0,0 +1,85 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; + +use libc::c_char; + +use crate::{panic, raw, util::Binding}; + +/// 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, + } + } +} + +//TODO: pass raw &[u8] and leave conversion to consumer (breaking API) +/// Callback type used to pass tracing events to the subscriber. +/// see `trace_set` to register a subscriber. +pub type TracingCb = fn(TraceLevel, &str); + +static CALLBACK: AtomicUsize = AtomicUsize::new(0); + +/// +pub fn trace_set(level: TraceLevel, cb: TracingCb) -> bool { + CALLBACK.store(cb as usize, Ordering::SeqCst); + + unsafe { + raw::git_trace_set(level.raw(), Some(tracing_cb_c)); + } + + return true; +} + +extern "C" fn tracing_cb_c(level: raw::git_trace_level_t, msg: *const c_char) { + let cb = CALLBACK.load(Ordering::SeqCst); + panic::wrap(|| unsafe { + let cb: TracingCb = std::mem::transmute(cb); + let msg = std::ffi::CStr::from_ptr(msg).to_string_lossy(); + cb(Binding::from_raw(level), msg.as_ref()); + }); +} diff --git a/extra/git2/src/transaction.rs b/extra/git2/src/transaction.rs new file mode 100644 index 000000000..4f661f1d4 --- /dev/null +++ b/extra/git2/src/transaction.rs @@ -0,0 +1,285 @@ +use std::ffi::CString; +use std::marker; + +use crate::{raw, util::Binding, Error, Oid, Reflog, Repository, Signature}; + +/// A structure representing a transactional update of a repository's references. +/// +/// Transactions work by locking loose refs for as long as the [`Transaction`] +/// is held, and committing all changes to disk when [`Transaction::commit`] is +/// called. Note that committing is not atomic: if an operation fails, the +/// transaction aborts, but previous successful operations are not rolled back. +pub struct Transaction<'repo> { + raw: *mut raw::git_transaction, + _marker: marker::PhantomData<&'repo Repository>, +} + +impl Drop for Transaction<'_> { + fn drop(&mut self) { + unsafe { raw::git_transaction_free(self.raw) } + } +} + +impl<'repo> Binding for Transaction<'repo> { + type Raw = *mut raw::git_transaction; + + unsafe fn from_raw(ptr: *mut raw::git_transaction) -> Transaction<'repo> { + Transaction { + raw: ptr, + _marker: marker::PhantomData, + } + } + + fn raw(&self) -> *mut raw::git_transaction { + self.raw + } +} + +impl<'repo> Transaction<'repo> { + /// Lock the specified reference by name. + pub fn lock_ref(&mut self, refname: &str) -> Result<(), Error> { + let refname = CString::new(refname).unwrap(); + unsafe { + try_call!(raw::git_transaction_lock_ref(self.raw, refname)); + } + + Ok(()) + } + + /// Set the target of the specified reference. + /// + /// The reference must have been locked via `lock_ref`. + /// + /// If `reflog_signature` is `None`, the [`Signature`] is read from the + /// repository config. + pub fn set_target( + &mut self, + refname: &str, + target: Oid, + reflog_signature: Option<&Signature<'_>>, + reflog_message: &str, + ) -> Result<(), Error> { + let refname = CString::new(refname).unwrap(); + let reflog_message = CString::new(reflog_message).unwrap(); + unsafe { + try_call!(raw::git_transaction_set_target( + self.raw, + refname, + target.raw(), + reflog_signature.map(|s| s.raw()), + reflog_message + )); + } + + Ok(()) + } + + /// Set the target of the specified symbolic reference. + /// + /// The reference must have been locked via `lock_ref`. + /// + /// If `reflog_signature` is `None`, the [`Signature`] is read from the + /// repository config. + pub fn set_symbolic_target( + &mut self, + refname: &str, + target: &str, + reflog_signature: Option<&Signature<'_>>, + reflog_message: &str, + ) -> Result<(), Error> { + let refname = CString::new(refname).unwrap(); + let target = CString::new(target).unwrap(); + let reflog_message = CString::new(reflog_message).unwrap(); + unsafe { + try_call!(raw::git_transaction_set_symbolic_target( + self.raw, + refname, + target, + reflog_signature.map(|s| s.raw()), + reflog_message + )); + } + + Ok(()) + } + + /// Add a [`Reflog`] to the transaction. + /// + /// This commit the in-memory [`Reflog`] to disk when the transaction commits. + /// Note that atomicity is **not* guaranteed: if the transaction fails to + /// modify `refname`, the reflog may still have been committed to disk. + /// + /// If this is combined with setting the target, that update won't be + /// written to the log (i.e. the `reflog_signature` and `reflog_message` + /// parameters will be ignored). + pub fn set_reflog(&mut self, refname: &str, reflog: Reflog) -> Result<(), Error> { + let refname = CString::new(refname).unwrap(); + unsafe { + try_call!(raw::git_transaction_set_reflog( + self.raw, + refname, + reflog.raw() + )); + } + + Ok(()) + } + + /// Remove a reference. + /// + /// The reference must have been locked via `lock_ref`. + pub fn remove(&mut self, refname: &str) -> Result<(), Error> { + let refname = CString::new(refname).unwrap(); + unsafe { + try_call!(raw::git_transaction_remove(self.raw, refname)); + } + + Ok(()) + } + + /// Commit the changes from the transaction. + /// + /// The updates will be made one by one, and the first failure will stop the + /// processing. + pub fn commit(self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_transaction_commit(self.raw)); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::{Error, ErrorClass, ErrorCode, Oid, Repository}; + + #[test] + fn smoke() { + let (_td, repo) = crate::test::repo_init(); + + let mut tx = t!(repo.transaction()); + + t!(tx.lock_ref("refs/heads/main")); + t!(tx.lock_ref("refs/heads/next")); + + t!(tx.set_target("refs/heads/main", Oid::zero(), None, "set main to zero")); + t!(tx.set_symbolic_target( + "refs/heads/next", + "refs/heads/main", + None, + "set next to main", + )); + + t!(tx.commit()); + + assert_eq!(repo.refname_to_id("refs/heads/main").unwrap(), Oid::zero()); + assert_eq!( + repo.find_reference("refs/heads/next") + .unwrap() + .symbolic_target() + .unwrap(), + "refs/heads/main" + ); + } + + #[test] + fn locks_same_repo_handle() { + let (_td, repo) = crate::test::repo_init(); + + let mut tx1 = t!(repo.transaction()); + t!(tx1.lock_ref("refs/heads/seen")); + + let mut tx2 = t!(repo.transaction()); + assert!(matches!(tx2.lock_ref("refs/heads/seen"), Err(e) if e.code() == ErrorCode::Locked)) + } + + #[test] + fn locks_across_repo_handles() { + let (td, repo1) = crate::test::repo_init(); + let repo2 = t!(Repository::open(&td)); + + let mut tx1 = t!(repo1.transaction()); + t!(tx1.lock_ref("refs/heads/seen")); + + let mut tx2 = t!(repo2.transaction()); + assert!(matches!(tx2.lock_ref("refs/heads/seen"), Err(e) if e.code() == ErrorCode::Locked)) + } + + #[test] + fn drop_unlocks() { + let (_td, repo) = crate::test::repo_init(); + + let mut tx = t!(repo.transaction()); + t!(tx.lock_ref("refs/heads/seen")); + drop(tx); + + let mut tx2 = t!(repo.transaction()); + t!(tx2.lock_ref("refs/heads/seen")) + } + + #[test] + fn commit_unlocks() { + let (_td, repo) = crate::test::repo_init(); + + let mut tx = t!(repo.transaction()); + t!(tx.lock_ref("refs/heads/seen")); + t!(tx.commit()); + + let mut tx2 = t!(repo.transaction()); + t!(tx2.lock_ref("refs/heads/seen")); + } + + #[test] + fn prevents_non_transactional_updates() { + let (_td, repo) = crate::test::repo_init(); + let head = t!(repo.refname_to_id("HEAD")); + + let mut tx = t!(repo.transaction()); + t!(tx.lock_ref("refs/heads/seen")); + + assert!(matches!( + repo.reference("refs/heads/seen", head, true, "competing with lock"), + Err(e) if e.code() == ErrorCode::Locked + )); + } + + #[test] + fn remove() { + let (_td, repo) = crate::test::repo_init(); + let head = t!(repo.refname_to_id("HEAD")); + let next = "refs/heads/next"; + + t!(repo.reference( + next, + head, + true, + "refs/heads/next@{0}: branch: Created from HEAD" + )); + + { + let mut tx = t!(repo.transaction()); + t!(tx.lock_ref(next)); + t!(tx.remove(next)); + t!(tx.commit()); + } + assert!(matches!(repo.refname_to_id(next), Err(e) if e.code() == ErrorCode::NotFound)) + } + + #[test] + fn must_lock_ref() { + let (_td, repo) = crate::test::repo_init(); + + // 🤷 + fn is_not_locked_err(e: &Error) -> bool { + e.code() == ErrorCode::NotFound + && e.class() == ErrorClass::Reference + && e.message() == "the specified reference is not locked" + } + + let mut tx = t!(repo.transaction()); + assert!(matches!( + tx.set_target("refs/heads/main", Oid::zero(), None, "set main to zero"), + Err(e) if is_not_locked_err(&e) + )) + } +} diff --git a/extra/git2/src/transport.rs b/extra/git2/src/transport.rs new file mode 100644 index 000000000..74446d0ca --- /dev/null +++ b/extra/git2/src/transport.rs @@ -0,0 +1,429 @@ +//! Interfaces for adding custom transports to libgit2 + +use libc::{c_char, c_int, c_uint, c_void, size_t}; +use std::ffi::{CStr, CString}; +use std::io; +use std::io::prelude::*; +use std::mem; +use std::ptr; +use std::slice; +use std::str; + +use crate::util::Binding; +use crate::{panic, raw, Error, Remote}; + +/// A transport is a structure which knows how to transfer data to and from a +/// remote. +/// +/// This transport is a representation of the raw transport underneath it, which +/// is similar to a trait object in Rust. +#[allow(missing_copy_implementations)] +pub struct Transport { + raw: *mut raw::git_transport, + owned: bool, +} + +/// Interface used by smart transports. +/// +/// 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. +/// +/// A smart subtransport is contained within an instance of a smart transport +/// and is delegated to in order to actually conduct network activity to push or +/// pull data from a remote. +pub trait SmartSubtransport: Send + 'static { + /// Indicates that this subtransport will be performing the specified action + /// on the specified URL. + /// + /// This function is responsible for making any network connections and + /// returns a stream which can be read and written from in order to + /// negotiate the git protocol. + fn action(&self, url: &str, action: Service) + -> Result<Box<dyn SmartSubtransportStream>, Error>; + + /// Terminates a connection with the remote. + /// + /// Each subtransport is guaranteed a call to close() between calls to + /// action(), except for the following two natural progressions of actions + /// against a constant URL. + /// + /// 1. UploadPackLs -> UploadPack + /// 2. ReceivePackLs -> ReceivePack + fn close(&self) -> Result<(), Error>; +} + +/// Actions that a smart transport can ask a subtransport to perform +#[derive(Copy, Clone, PartialEq)] +#[allow(missing_docs)] +pub enum Service { + UploadPackLs, + UploadPack, + ReceivePackLs, + ReceivePack, +} + +/// An instance of a stream over which a smart transport will communicate with a +/// remote. +/// +/// Currently this only requires the standard `Read` and `Write` traits. This +/// trait also does not need to be implemented manually as long as the `Read` +/// and `Write` traits are implemented. +pub trait SmartSubtransportStream: Read + Write + Send + 'static {} + +impl<T: Read + Write + Send + 'static> SmartSubtransportStream for T {} + +type TransportFactory = dyn Fn(&Remote<'_>) -> Result<Transport, Error> + Send + Sync + 'static; + +/// Boxed data payload used for registering new transports. +/// +/// Currently only contains a field which knows how to create transports. +struct TransportData { + factory: Box<TransportFactory>, +} + +/// Instance of a `git_smart_subtransport`, must use `#[repr(C)]` to ensure that +/// the C fields come first. +#[repr(C)] +struct RawSmartSubtransport { + raw: raw::git_smart_subtransport, + stream: Option<*mut raw::git_smart_subtransport_stream>, + rpc: bool, + obj: Box<dyn SmartSubtransport>, +} + +/// Instance of a `git_smart_subtransport_stream`, must use `#[repr(C)]` to +/// ensure that the C fields come first. +#[repr(C)] +struct RawSmartSubtransportStream { + raw: raw::git_smart_subtransport_stream, + obj: Box<dyn SmartSubtransportStream>, +} + +/// Add a custom transport definition, to be used in addition to the built-in +/// set of transports that come with libgit2. +/// +/// This function is unsafe as it needs to be externally synchronized with calls +/// to creation of other transports. +pub unsafe fn register<F>(prefix: &str, factory: F) -> Result<(), Error> +where + F: Fn(&Remote<'_>) -> Result<Transport, Error> + Send + Sync + 'static, +{ + crate::init(); + let mut data = Box::new(TransportData { + factory: Box::new(factory), + }); + let prefix = CString::new(prefix)?; + let datap = (&mut *data) as *mut TransportData as *mut c_void; + let factory: raw::git_transport_cb = Some(transport_factory); + try_call!(raw::git_transport_register(prefix, factory, datap)); + mem::forget(data); + Ok(()) +} + +impl Transport { + /// Creates a new transport which will use the "smart" transport protocol + /// for transferring data. + /// + /// A smart transport requires a *subtransport* over which data is actually + /// communicated, but this subtransport largely just needs to be able to + /// read() and write(). The subtransport provided will be used to make + /// connections which can then be read/written from. + /// + /// The `rpc` argument is `true` if the protocol is stateless, false + /// otherwise. For example `http://` is stateless but `git://` is not. + pub fn smart<S>(remote: &Remote<'_>, rpc: bool, subtransport: S) -> Result<Transport, Error> + where + S: SmartSubtransport, + { + let mut ret = ptr::null_mut(); + + let mut raw = Box::new(RawSmartSubtransport { + raw: raw::git_smart_subtransport { + action: Some(subtransport_action), + close: Some(subtransport_close), + free: Some(subtransport_free), + }, + stream: None, + rpc, + obj: Box::new(subtransport), + }); + let mut defn = raw::git_smart_subtransport_definition { + callback: Some(smart_factory), + rpc: rpc as c_uint, + param: &mut *raw as *mut _ as *mut _, + }; + + // Currently there's no way to pass a payload via the + // git_smart_subtransport_definition structure, but it's only used as a + // configuration for the initial creation of the smart transport (verified + // by reading the current code, hopefully it doesn't change!). + // + // We, however, need some state (gotta pass in our + // `RawSmartSubtransport`). This also means that this block must be + // entirely synchronized with a lock (boo!) + unsafe { + try_call!(raw::git_transport_smart( + &mut ret, + remote.raw(), + &mut defn as *mut _ as *mut _ + )); + mem::forget(raw); // ownership transport to `ret` + } + return Ok(Transport { + raw: ret, + owned: true, + }); + + extern "C" fn smart_factory( + out: *mut *mut raw::git_smart_subtransport, + _owner: *mut raw::git_transport, + ptr: *mut c_void, + ) -> c_int { + unsafe { + *out = ptr as *mut raw::git_smart_subtransport; + 0 + } + } + } +} + +impl Drop for Transport { + fn drop(&mut self) { + if self.owned { + unsafe { (*self.raw).free.unwrap()(self.raw) } + } + } +} + +// callback used by register() to create new transports +extern "C" fn transport_factory( + out: *mut *mut raw::git_transport, + owner: *mut raw::git_remote, + param: *mut c_void, +) -> c_int { + struct Bomb<'a> { + remote: Option<Remote<'a>>, + } + impl<'a> Drop for Bomb<'a> { + fn drop(&mut self) { + // TODO: maybe a method instead? + mem::forget(self.remote.take()); + } + } + + panic::wrap(|| unsafe { + let remote = Bomb { + remote: Some(Binding::from_raw(owner)), + }; + let data = &mut *(param as *mut TransportData); + match (data.factory)(remote.remote.as_ref().unwrap()) { + Ok(mut transport) => { + *out = transport.raw; + transport.owned = false; + 0 + } + Err(e) => e.raw_code() as c_int, + } + }) + .unwrap_or(-1) +} + +// callback used by smart transports to delegate an action to a +// `SmartSubtransport` trait object. +extern "C" fn subtransport_action( + stream: *mut *mut raw::git_smart_subtransport_stream, + raw_transport: *mut raw::git_smart_subtransport, + url: *const c_char, + action: raw::git_smart_service_t, +) -> c_int { + panic::wrap(|| unsafe { + let url = CStr::from_ptr(url).to_bytes(); + let url = match str::from_utf8(url).ok() { + Some(s) => s, + None => return -1, + }; + let action = match action { + raw::GIT_SERVICE_UPLOADPACK_LS => Service::UploadPackLs, + raw::GIT_SERVICE_UPLOADPACK => Service::UploadPack, + raw::GIT_SERVICE_RECEIVEPACK_LS => Service::ReceivePackLs, + raw::GIT_SERVICE_RECEIVEPACK => Service::ReceivePack, + n => panic!("unknown action: {}", n), + }; + + 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 = + transport.rpc || action == Service::UploadPackLs || action == Service::ReceivePackLs; + 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; + } + }; + *stream = mem::transmute(Box::new(RawSmartSubtransportStream { + raw: raw::git_smart_subtransport_stream { + subtransport: raw_transport, + read: Some(stream_read), + write: Some(stream_write), + free: Some(stream_free), + }, + obj, + })); + transport.stream = Some(*stream); + } else { + if transport.stream.is_none() { + return -1; + } + *stream = transport.stream.unwrap(); + } + 0 + }) + .unwrap_or(-1) +} + +// callback used by smart transports to close a `SmartSubtransport` trait +// object. +extern "C" fn subtransport_close(transport: *mut raw::git_smart_subtransport) -> c_int { + let ret = panic::wrap(|| unsafe { + let transport = &mut *(transport as *mut RawSmartSubtransport); + transport.obj.close() + }); + match ret { + Some(Ok(())) => 0, + Some(Err(e)) => e.raw_code() as c_int, + None => -1, + } +} + +// callback used by smart transports to free a `SmartSubtransport` trait +// object. +extern "C" fn subtransport_free(transport: *mut raw::git_smart_subtransport) { + let _ = panic::wrap(|| unsafe { + mem::transmute::<_, Box<RawSmartSubtransport>>(transport); + }); +} + +// callback used by smart transports to read from a `SmartSubtransportStream` +// object. +extern "C" fn stream_read( + stream: *mut raw::git_smart_subtransport_stream, + buffer: *mut c_char, + buf_size: size_t, + bytes_read: *mut size_t, +) -> c_int { + let ret = panic::wrap(|| unsafe { + let transport = &mut *(stream as *mut RawSmartSubtransportStream); + let buf = slice::from_raw_parts_mut(buffer as *mut u8, buf_size as usize); + match transport.obj.read(buf) { + Ok(n) => { + *bytes_read = n as size_t; + Ok(n) + } + e => e, + } + }); + match ret { + Some(Ok(_)) => 0, + Some(Err(e)) => unsafe { + set_err_io(&e); + -2 + }, + None => -1, + } +} + +// callback used by smart transports to write to a `SmartSubtransportStream` +// object. +extern "C" fn stream_write( + stream: *mut raw::git_smart_subtransport_stream, + buffer: *const c_char, + len: size_t, +) -> c_int { + let ret = panic::wrap(|| unsafe { + let transport = &mut *(stream as *mut RawSmartSubtransportStream); + let buf = slice::from_raw_parts(buffer as *const u8, len as usize); + transport.obj.write_all(buf) + }); + match ret { + Some(Ok(())) => 0, + Some(Err(e)) => unsafe { + set_err_io(&e); + -2 + }, + None => -1, + } +} + +unsafe fn set_err_io(e: &io::Error) { + let s = CString::new(e.to_string()).unwrap(); + 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) { + let _ = panic::wrap(|| unsafe { + mem::transmute::<_, Box<RawSmartSubtransportStream>>(stream); + }); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ErrorClass, ErrorCode}; + use std::sync::Once; + + struct DummyTransport; + + // in lieu of lazy_static + fn dummy_error() -> Error { + Error::new(ErrorCode::Ambiguous, ErrorClass::Net, "bleh") + } + + impl SmartSubtransport for DummyTransport { + fn action( + &self, + _url: &str, + _service: Service, + ) -> Result<Box<dyn SmartSubtransportStream>, Error> { + Err(dummy_error()) + } + + fn close(&self) -> Result<(), Error> { + Ok(()) + } + } + + #[test] + fn transport_error_propagates() { + static INIT: Once = Once::new(); + + unsafe { + INIT.call_once(|| { + register("dummy", move |remote| { + Transport::smart(&remote, true, DummyTransport) + }) + .unwrap(); + }) + } + + let (_td, repo) = crate::test::repo_init(); + t!(repo.remote("origin", "dummy://ball")); + + let mut origin = t!(repo.find_remote("origin")); + + match origin.fetch(&["main"], None, None) { + Ok(()) => unreachable!(), + Err(e) => assert_eq!(e, dummy_error()), + } + } +} diff --git a/extra/git2/src/tree.rs b/extra/git2/src/tree.rs new file mode 100644 index 000000000..9a38244cf --- /dev/null +++ b/extra/git2/src/tree.rs @@ -0,0 +1,570 @@ +use libc::{self, 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; +use std::path::Path; +use std::ptr; +use std::str; + +use crate::util::{c_cmp_to_ordering, path_to_repo_path, Binding}; +use crate::{panic, raw, Error, Object, ObjectType, Oid, Repository}; + +/// A structure to represent a git [tree][1] +/// +/// [1]: http://git-scm.com/book/en/Git-Internals-Git-Objects +pub struct Tree<'repo> { + raw: *mut raw::git_tree, + _marker: marker::PhantomData<Object<'repo>>, +} + +/// A structure representing an entry inside of a tree. An entry is borrowed +/// from a tree. +pub struct TreeEntry<'tree> { + raw: *mut raw::git_tree_entry, + owned: bool, + _marker: marker::PhantomData<&'tree raw::git_tree_entry>, +} + +/// An iterator over the entries in a tree. +pub struct TreeIter<'tree> { + range: Range<usize>, + tree: &'tree Tree<'tree>, +} + +/// A binary indicator of whether a tree walk should be performed in pre-order +/// or post-order. +pub enum TreeWalkMode { + /// Runs the traversal in pre-order. + PreOrder = 0, + /// Runs the traversal in post-order. + PostOrder = 1, +} + +/// Possible return codes for tree walking callback functions. +#[repr(i32)] +pub enum TreeWalkResult { + /// Continue with the traversal as normal. + Ok = 0, + /// Skip the current node (in pre-order mode). + Skip = 1, + /// Completely stop the traversal. + Abort = raw::GIT_EUSER, +} + +impl Into<i32> for TreeWalkResult { + fn into(self) -> i32 { + self as i32 + } +} + +impl Into<raw::git_treewalk_mode> for TreeWalkMode { + #[cfg(target_env = "msvc")] + fn into(self) -> raw::git_treewalk_mode { + self as i32 + } + #[cfg(not(target_env = "msvc"))] + fn into(self) -> raw::git_treewalk_mode { + self as u32 + } +} + +impl<'repo> Tree<'repo> { + /// Get the id (SHA1) of a repository object + pub fn id(&self) -> Oid { + unsafe { Binding::from_raw(raw::git_tree_id(&*self.raw)) } + } + + /// Get the number of entries listed in this tree. + pub fn len(&self) -> usize { + unsafe { raw::git_tree_entrycount(&*self.raw) as usize } + } + + /// Return `true` if there is not entry + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns an iterator over the entries in this tree. + pub fn iter(&self) -> TreeIter<'_> { + TreeIter { + range: 0..self.len(), + tree: self, + } + } + + /// Traverse the entries in a tree and its subtrees in post or pre-order. + /// The callback function will be run on each node of the tree that's + /// walked. The return code of this function will determine how the walk + /// continues. + /// + /// libgit2 requires that the callback be an integer, where 0 indicates a + /// successful visit, 1 skips the node, and -1 aborts the traversal completely. + /// You may opt to use the enum [`TreeWalkResult`](TreeWalkResult) instead. + /// + /// ```ignore + /// let mut ct = 0; + /// tree.walk(TreeWalkMode::PreOrder, |_, entry| { + /// assert_eq!(entry.name(), Some("foo")); + /// ct += 1; + /// TreeWalkResult::Ok + /// }).unwrap(); + /// assert_eq!(ct, 1); + /// ``` + /// + /// See [libgit2 documentation][1] for more information. + /// + /// [1]: https://libgit2.org/libgit2/#HEAD/group/tree/git_tree_walk + pub fn walk<C, T>(&self, mode: TreeWalkMode, mut callback: C) -> Result<(), Error> + where + C: FnMut(&str, &TreeEntry<'_>) -> T, + T: Into<i32>, + { + unsafe { + let mut data = TreeWalkCbData { + callback: &mut callback, + }; + raw::git_tree_walk( + self.raw(), + mode.into(), + Some(treewalk_cb::<T>), + &mut data as *mut _ as *mut c_void, + ); + Ok(()) + } + } + + /// Lookup a tree entry by SHA value. + pub fn get_id(&self, id: Oid) -> Option<TreeEntry<'_>> { + unsafe { + let ptr = raw::git_tree_entry_byid(&*self.raw(), &*id.raw()); + if ptr.is_null() { + None + } else { + Some(entry_from_raw_const(ptr)) + } + } + } + + /// Lookup a tree entry by its position in the tree + pub fn get(&self, n: usize) -> Option<TreeEntry<'_>> { + unsafe { + let ptr = raw::git_tree_entry_byindex(&*self.raw(), n as libc::size_t); + if ptr.is_null() { + None + } else { + Some(entry_from_raw_const(ptr)) + } + } + } + + /// Lookup a tree entry by its filename + pub fn get_name(&self, filename: &str) -> Option<TreeEntry<'_>> { + 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<TreeEntry<'_>> { + let filename = CString::new(filename).unwrap(); + unsafe { + let ptr = call!(raw::git_tree_entry_byname(&*self.raw(), filename)); + if ptr.is_null() { + None + } else { + Some(entry_from_raw_const(ptr)) + } + } + } + + /// Retrieve a tree entry contained in a tree or in any of its subtrees, + /// given its relative path. + pub fn get_path(&self, path: &Path) -> Result<TreeEntry<'static>, Error> { + let path = path_to_repo_path(path)?; + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_tree_entry_bypath(&mut ret, &*self.raw(), path)); + Ok(Binding::from_raw(ret)) + } + } + + /// Casts this Tree to be usable as an `Object` + pub fn as_object(&self) -> &Object<'repo> { + unsafe { &*(self as *const _ as *const Object<'repo>) } + } + + /// 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::<Object<'_>>()); + unsafe { mem::transmute(self) } + } +} + +type TreeWalkCb<'a, T> = dyn FnMut(&str, &TreeEntry<'_>) -> T + 'a; + +struct TreeWalkCbData<'a, T> { + callback: &'a mut TreeWalkCb<'a, T>, +} + +extern "C" fn treewalk_cb<T: Into<i32>>( + root: *const c_char, + entry: *const raw::git_tree_entry, + payload: *mut c_void, +) -> c_int { + match panic::wrap(|| unsafe { + let root = match CStr::from_ptr(root).to_str() { + Ok(value) => value, + _ => return -1, + }; + let entry = entry_from_raw_const(entry); + let payload = &mut *(payload as *mut TreeWalkCbData<'_, T>); + let callback = &mut payload.callback; + callback(root, &entry).into() + }) { + Some(value) => value, + None => -1, + } +} + +impl<'repo> Binding for Tree<'repo> { + type Raw = *mut raw::git_tree; + + unsafe fn from_raw(raw: *mut raw::git_tree) -> Tree<'repo> { + Tree { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_tree { + self.raw + } +} + +impl<'repo> std::fmt::Debug for Tree<'repo> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_struct("Tree").field("id", &self.id()).finish() + } +} + +impl<'repo> Clone for Tree<'repo> { + fn clone(&self) -> Self { + self.as_object().clone().into_tree().ok().unwrap() + } +} + +impl<'repo> Drop for Tree<'repo> { + fn drop(&mut self) { + unsafe { raw::git_tree_free(self.raw) } + } +} + +impl<'repo, 'iter> IntoIterator for &'iter Tree<'repo> { + type Item = TreeEntry<'iter>; + type IntoIter = TreeIter<'iter>; + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +/// Create a new tree entry from the raw pointer provided. +/// +/// The lifetime of the entry is tied to the tree provided and the function +/// is unsafe because the validity of the pointer cannot be guaranteed. +pub unsafe fn entry_from_raw_const<'tree>(raw: *const raw::git_tree_entry) -> TreeEntry<'tree> { + TreeEntry { + raw: raw as *mut raw::git_tree_entry, + owned: false, + _marker: marker::PhantomData, + } +} + +impl<'tree> TreeEntry<'tree> { + /// Get the id of the object pointed by the entry + pub fn id(&self) -> Oid { + unsafe { Binding::from_raw(raw::git_tree_entry_id(&*self.raw)) } + } + + /// Get the filename of a tree entry + /// + /// Returns `None` if the name is not valid utf-8 + pub fn name(&self) -> Option<&str> { + str::from_utf8(self.name_bytes()).ok() + } + + /// Get the filename of a tree entry + pub fn name_bytes(&self) -> &[u8] { + unsafe { crate::opt_bytes(self, raw::git_tree_entry_name(&*self.raw())).unwrap() } + } + + /// Convert a tree entry to the object it points to. + pub fn to_object<'a>(&self, repo: &'a Repository) -> Result<Object<'a>, Error> { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_tree_entry_to_object( + &mut ret, + repo.raw(), + &*self.raw() + )); + Ok(Binding::from_raw(ret)) + } + } + + /// Get the type of the object pointed by the entry + pub fn kind(&self) -> Option<ObjectType> { + ObjectType::from_raw(unsafe { raw::git_tree_entry_type(&*self.raw) }) + } + + /// Get the UNIX file attributes of a tree entry + pub fn filemode(&self) -> i32 { + unsafe { raw::git_tree_entry_filemode(&*self.raw) as i32 } + } + + /// Get the raw UNIX file attributes of a tree entry + pub fn filemode_raw(&self) -> i32 { + unsafe { raw::git_tree_entry_filemode_raw(&*self.raw) as i32 } + } + + /// Convert this entry of any lifetime into an owned signature with a static + /// lifetime. + /// + /// This will use the `Clone::clone` implementation under the hood. + pub fn to_owned(&self) -> TreeEntry<'static> { + unsafe { + let me = mem::transmute::<&TreeEntry<'tree>, &TreeEntry<'static>>(self); + me.clone() + } + } +} + +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, + owned: true, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_tree_entry { + self.raw + } +} + +impl<'a> Clone for TreeEntry<'a> { + fn clone(&self) -> TreeEntry<'a> { + let mut ret = ptr::null_mut(); + unsafe { + assert_eq!(raw::git_tree_entry_dup(&mut ret, &*self.raw()), 0); + Binding::from_raw(ret) + } + } +} + +impl<'a> PartialOrd for TreeEntry<'a> { + fn partial_cmp(&self, other: &TreeEntry<'a>) -> Option<Ordering> { + Some(self.cmp(other)) + } +} +impl<'a> Ord for TreeEntry<'a> { + fn cmp(&self, other: &TreeEntry<'a>) -> Ordering { + c_cmp_to_ordering(unsafe { raw::git_tree_entry_cmp(&*self.raw(), &*other.raw()) }) + } +} + +impl<'a> PartialEq for TreeEntry<'a> { + fn eq(&self, other: &TreeEntry<'a>) -> bool { + self.cmp(other) == Ordering::Equal + } +} +impl<'a> Eq for TreeEntry<'a> {} + +impl<'a> Drop for TreeEntry<'a> { + fn drop(&mut self) { + if self.owned { + unsafe { raw::git_tree_entry_free(self.raw) } + } + } +} + +impl<'tree> Iterator for TreeIter<'tree> { + type Item = TreeEntry<'tree>; + fn next(&mut self) -> Option<TreeEntry<'tree>> { + self.range.next().and_then(|i| self.tree.get(i)) + } + fn size_hint(&self) -> (usize, Option<usize>) { + self.range.size_hint() + } +} +impl<'tree> DoubleEndedIterator for TreeIter<'tree> { + fn next_back(&mut self) -> Option<TreeEntry<'tree>> { + self.range.next_back().and_then(|i| self.tree.get(i)) + } +} +impl<'tree> FusedIterator for TreeIter<'tree> {} +impl<'tree> ExactSizeIterator for TreeIter<'tree> {} + +#[cfg(test)] +mod tests { + use super::{TreeWalkMode, TreeWalkResult}; + use crate::{Object, ObjectType, Repository, Tree, TreeEntry}; + use std::fs::File; + use std::io::prelude::*; + use std::path::Path; + use tempfile::TempDir; + + pub struct TestTreeIter<'a> { + entries: Vec<TreeEntry<'a>>, + repo: &'a Repository, + } + + impl<'a> Iterator for TestTreeIter<'a> { + type Item = TreeEntry<'a>; + + fn next(&mut self) -> Option<TreeEntry<'a>> { + if self.entries.is_empty() { + None + } else { + let entry = self.entries.remove(0); + + match entry.kind() { + Some(ObjectType::Tree) => { + let obj: Object<'a> = entry.to_object(self.repo).unwrap(); + + let tree: &Tree<'a> = obj.as_tree().unwrap(); + + for entry in tree.iter() { + self.entries.push(entry.to_owned()); + } + } + _ => {} + } + + Some(entry) + } + } + } + + fn tree_iter<'repo>(tree: &Tree<'repo>, repo: &'repo Repository) -> TestTreeIter<'repo> { + let mut initial = vec![]; + + for entry in tree.iter() { + initial.push(entry.to_owned()); + } + + TestTreeIter { + entries: initial, + repo: repo, + } + } + + #[test] + fn smoke_tree_iter() { + 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(), 1); + + for entry in tree_iter(&tree, &repo) { + println!("iter entry {:?}", entry.name()); + } + } + + 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(); + let id = index.write_tree().unwrap(); + let sig = repo.signature().unwrap(); + let tree = repo.find_tree(id).unwrap(); + let parent = repo + .find_commit(repo.head().unwrap().target().unwrap()) + .unwrap(); + repo.commit( + Some("HEAD"), + &sig, + &sig, + "another commit", + &tree, + &[&parent], + ) + .unwrap(); + } + + #[test] + fn smoke() { + 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(), 1); + { + let e1 = tree.get(0).unwrap(); + assert!(e1 == tree.get_id(e1.id()).unwrap()); + assert!(e1 == tree.get_name("foo").unwrap()); + assert!(e1 == tree.get_name_bytes(b"foo").unwrap()); + assert!(e1 == tree.get_path(Path::new("foo")).unwrap()); + assert_eq!(e1.name(), Some("foo")); + e1.to_object(&repo).unwrap(); + } + tree.into_object(); + + repo.find_object(commit.tree_id(), None) + .unwrap() + .as_tree() + .unwrap(); + repo.find_object(commit.tree_id(), None) + .unwrap() + .into_tree() + .ok() + .unwrap(); + } + + #[test] + fn tree_walk() { + let (td, repo) = crate::test::repo_init(); + + setup_repo(&td, &repo); + + let head = repo.head().unwrap(); + let target = head.target().unwrap(); + let commit = repo.find_commit(target).unwrap(); + let tree = repo.find_tree(commit.tree_id()).unwrap(); + + let mut ct = 0; + tree.walk(TreeWalkMode::PreOrder, |_, entry| { + assert_eq!(entry.name(), Some("foo")); + ct += 1; + 0 + }) + .unwrap(); + assert_eq!(ct, 1); + + let mut ct = 0; + tree.walk(TreeWalkMode::PreOrder, |_, entry| { + assert_eq!(entry.name(), Some("foo")); + ct += 1; + TreeWalkResult::Ok + }) + .unwrap(); + assert_eq!(ct, 1); + } +} diff --git a/extra/git2/src/treebuilder.rs b/extra/git2/src/treebuilder.rs new file mode 100644 index 000000000..1548a048c --- /dev/null +++ b/extra/git2/src/treebuilder.rs @@ -0,0 +1,234 @@ +use std::marker; +use std::ptr; + +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 (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>, +} + +impl<'repo> TreeBuilder<'repo> { + /// Clear all the entries in the builder + pub fn clear(&mut self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_treebuilder_clear(self.raw)); + } + Ok(()) + } + + /// Get the number of entries + pub fn len(&self) -> usize { + unsafe { raw::git_treebuilder_entrycount(self.raw) as usize } + } + + /// Return `true` if there is no entry + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Get en entry from the builder from its filename + pub fn get<P>(&self, filename: P) -> Result<Option<TreeEntry<'_>>, Error> + where + P: IntoCString, + { + let filename = filename.into_c_string()?; + unsafe { + let ret = raw::git_treebuilder_get(self.raw, filename.as_ptr()); + if ret.is_null() { + Ok(None) + } else { + Ok(Some(tree::entry_from_raw_const(ret))) + } + } + } + + /// Add or update an entry in the builder + /// + /// No attempt is made to ensure that the provided Oid points to + /// an object of a reasonable type (or any object at all). + /// + /// The mode given must be one of 0o040000, 0o100644, 0o100755, 0o120000 or + /// 0o160000 currently. + pub fn insert<P: IntoCString>( + &mut self, + filename: P, + oid: Oid, + filemode: i32, + ) -> Result<TreeEntry<'_>, Error> { + let filename = filename.into_c_string()?; + let filemode = filemode as raw::git_filemode_t; + + let mut ret = ptr::null(); + unsafe { + try_call!(raw::git_treebuilder_insert( + &mut ret, + self.raw, + filename, + oid.raw(), + filemode + )); + Ok(tree::entry_from_raw_const(ret)) + } + } + + /// Remove an entry from the builder by its filename + pub fn remove<P: IntoCString>(&mut self, filename: P) -> Result<(), Error> { + let filename = filename.into_c_string()?; + unsafe { + try_call!(raw::git_treebuilder_remove(self.raw, filename)); + } + Ok(()) + } + + /// Selectively remove entries from the tree + /// + /// Values for which the filter returns `true` will be kept. Note + /// that this behavior is different from the libgit2 C interface. + pub fn filter<F>(&mut self, mut filter: F) -> Result<(), Error> + where + F: FnMut(&TreeEntry<'_>) -> bool, + { + let mut cb: &mut FilterCb<'_> = &mut filter; + let ptr = &mut cb as *mut _; + let cb: raw::git_treebuilder_filter_cb = Some(filter_cb); + unsafe { + try_call!(raw::git_treebuilder_filter(self.raw, cb, ptr as *mut _)); + panic::check(); + } + Ok(()) + } + + /// Write the contents of the TreeBuilder as a Tree object and + /// return its Oid + pub fn write(&self) -> Result<Oid, Error> { + let mut raw = raw::git_oid { + id: [0; raw::GIT_OID_RAWSZ], + }; + unsafe { + try_call!(raw::git_treebuilder_write(&mut raw, self.raw())); + Ok(Binding::from_raw(&raw as *const _)) + } + } +} + +type FilterCb<'a> = dyn FnMut(&TreeEntry<'_>) -> bool + 'a; + +extern "C" fn filter_cb(entry: *const raw::git_tree_entry, payload: *mut c_void) -> c_int { + let ret = panic::wrap(|| unsafe { + // There's no way to return early from git_treebuilder_filter. + if panic::panicked() { + true + } else { + let entry = tree::entry_from_raw_const(entry); + let payload = payload as *mut &mut FilterCb<'_>; + (*payload)(&entry) + } + }); + if ret == Some(false) { + 1 + } else { + 0 + } +} + +impl<'repo> Binding for TreeBuilder<'repo> { + type Raw = *mut raw::git_treebuilder; + + unsafe fn from_raw(raw: *mut raw::git_treebuilder) -> TreeBuilder<'repo> { + TreeBuilder { + raw, + _marker: marker::PhantomData, + } + } + fn raw(&self) -> *mut raw::git_treebuilder { + self.raw + } +} + +impl<'repo> Drop for TreeBuilder<'repo> { + fn drop(&mut self) { + unsafe { raw::git_treebuilder_free(self.raw) } + } +} + +#[cfg(test)] +mod tests { + use crate::ObjectType; + + #[test] + fn smoke() { + let (_td, repo) = crate::test::repo_init(); + + let mut builder = repo.treebuilder(None).unwrap(); + assert_eq!(builder.len(), 0); + let blob = repo.blob(b"data").unwrap(); + { + let entry = builder.insert("a", blob, 0o100644).unwrap(); + assert_eq!(entry.kind(), Some(ObjectType::Blob)); + } + builder.insert("b", blob, 0o100644).unwrap(); + assert_eq!(builder.len(), 2); + builder.remove("a").unwrap(); + assert_eq!(builder.len(), 1); + assert_eq!(builder.get("b").unwrap().unwrap().id(), blob); + builder.clear().unwrap(); + assert_eq!(builder.len(), 0); + } + + #[test] + fn write() { + let (_td, repo) = crate::test::repo_init(); + + let mut builder = repo.treebuilder(None).unwrap(); + let data = repo.blob(b"data").unwrap(); + builder.insert("name", data, 0o100644).unwrap(); + let tree = builder.write().unwrap(); + let tree = repo.find_tree(tree).unwrap(); + let entry = tree.get(0).unwrap(); + assert_eq!(entry.name(), Some("name")); + let blob = entry.to_object(&repo).unwrap(); + let blob = blob.as_blob().unwrap(); + assert_eq!(blob.content(), b"data"); + + let builder = repo.treebuilder(Some(&tree)).unwrap(); + assert_eq!(builder.len(), 1); + } + + #[test] + fn filter() { + let (_td, repo) = crate::test::repo_init(); + + let mut builder = repo.treebuilder(None).unwrap(); + let blob = repo.blob(b"data").unwrap(); + let tree = { + let head = repo.head().unwrap().peel(ObjectType::Commit).unwrap(); + let head = head.as_commit().unwrap(); + head.tree_id() + }; + builder.insert("blob", blob, 0o100644).unwrap(); + builder.insert("dir", tree, 0o040000).unwrap(); + builder.insert("dir2", tree, 0o040000).unwrap(); + + builder.filter(|_| true).unwrap(); + assert_eq!(builder.len(), 3); + builder + .filter(|e| e.kind().unwrap() != ObjectType::Blob) + .unwrap(); + assert_eq!(builder.len(), 2); + builder.filter(|_| false).unwrap(); + assert_eq!(builder.len(), 0); + } +} diff --git a/extra/git2/src/util.rs b/extra/git2/src/util.rs new file mode 100644 index 000000000..5f735bc00 --- /dev/null +++ b/extra/git2/src/util.rs @@ -0,0 +1,342 @@ +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}; + +#[doc(hidden)] +pub trait IsNull { + fn is_ptr_null(&self) -> bool; +} +impl<T> IsNull for *const T { + fn is_ptr_null(&self) -> bool { + self.is_null() + } +} +impl<T> IsNull for *mut T { + fn is_ptr_null(&self) -> bool { + self.is_null() + } +} + +#[doc(hidden)] +pub trait Binding: Sized { + type Raw; + + unsafe fn from_raw(raw: Self::Raw) -> Self; + fn raw(&self) -> Self::Raw; + + unsafe fn from_raw_opt<T>(raw: T) -> Option<Self> + where + T: Copy + IsNull, + Self: Binding<Raw = T>, + { + if raw.is_ptr_null() { + None + } else { + Some(Binding::from_raw(raw)) + } + } +} + +/// Converts an iterator of repo paths into a git2-compatible array of cstrings. +/// +/// Only use this for repo-relative paths or pathspecs. +/// +/// See `iter2cstrs` for more details. +pub fn iter2cstrs_paths<T, I>( + iter: I, +) -> Result<(Vec<CString>, Vec<*const c_char>, raw::git_strarray), Error> +where + T: IntoCString, + I: IntoIterator<Item = T>, +{ + let cstrs = iter + .into_iter() + .map(|i| fixup_windows_path(i.into_c_string()?)) + .collect::<Result<Vec<CString>, _>>()?; + iter2cstrs(cstrs) +} + +/// Converts an iterator of things into a git array of c-strings. +/// +/// Returns a tuple `(cstrings, pointers, git_strarray)`. The first two values +/// should not be dropped before `git_strarray`. +pub fn iter2cstrs<T, I>( + iter: I, +) -> Result<(Vec<CString>, Vec<*const c_char>, raw::git_strarray), Error> +where + T: IntoCString, + I: IntoIterator<Item = T>, +{ + let cstrs = iter + .into_iter() + .map(|i| i.into_c_string()) + .collect::<Result<Vec<CString>, _>>()?; + let ptrs = cstrs.iter().map(|i| i.as_ptr()).collect::<Vec<_>>(); + let raw = raw::git_strarray { + strings: ptrs.as_ptr() as *mut _, + count: ptrs.len() as size_t, + }; + Ok((cstrs, ptrs, raw)) +} + +#[cfg(unix)] +pub fn bytes2path(b: &[u8]) -> &Path { + use std::os::unix::prelude::*; + Path::new(OsStr::from_bytes(b)) +} +#[cfg(windows)] +pub fn bytes2path(b: &[u8]) -> &Path { + use std::str; + Path::new(str::from_utf8(b).unwrap()) +} + +/// A class of types that can be converted to C strings. +/// +/// These types are represented internally as byte slices and it is quite rare +/// for them to contain an interior 0 byte. +pub trait IntoCString { + /// Consume this container, converting it into a CString + fn into_c_string(self) -> Result<CString, Error>; +} + +impl<'a, T: IntoCString + Clone> IntoCString for &'a T { + fn into_c_string(self) -> Result<CString, Error> { + self.clone().into_c_string() + } +} + +impl<'a> IntoCString for &'a str { + fn into_c_string(self) -> Result<CString, Error> { + Ok(CString::new(self)?) + } +} + +impl IntoCString for String { + fn into_c_string(self) -> Result<CString, Error> { + Ok(CString::new(self.into_bytes())?) + } +} + +impl IntoCString for CString { + fn into_c_string(self) -> Result<CString, Error> { + Ok(self) + } +} + +impl<'a> IntoCString for &'a Path { + fn into_c_string(self) -> Result<CString, Error> { + let s: &OsStr = self.as_ref(); + s.into_c_string() + } +} + +impl IntoCString for PathBuf { + fn into_c_string(self) -> Result<CString, Error> { + let s: OsString = self.into(); + s.into_c_string() + } +} + +impl<'a> IntoCString for &'a OsStr { + fn into_c_string(self) -> Result<CString, Error> { + self.to_os_string().into_c_string() + } +} + +impl IntoCString for OsString { + #[cfg(unix)] + fn into_c_string(self) -> Result<CString, Error> { + use std::os::unix::prelude::*; + let s: &OsStr = self.as_ref(); + Ok(CString::new(s.as_bytes())?) + } + #[cfg(windows)] + fn into_c_string(self) -> Result<CString, Error> { + match self.to_str() { + Some(s) => s.into_c_string(), + None => Err(Error::from_str( + "only valid unicode paths are accepted on windows", + )), + } + } +} + +impl<'a> IntoCString for &'a [u8] { + fn into_c_string(self) -> Result<CString, Error> { + Ok(CString::new(self)?) + } +} + +impl IntoCString for Vec<u8> { + fn into_c_string(self) -> Result<CString, Error> { + Ok(CString::new(self)?) + } +} + +pub fn into_opt_c_string<S>(opt_s: Option<S>) -> Result<Option<CString>, Error> +where + S: IntoCString, +{ + match opt_s { + None => Ok(None), + Some(s) => Ok(Some(s.into_c_string()?)), + } +} + +pub fn c_cmp_to_ordering(cmp: c_int) -> Ordering { + match cmp { + 0 => Ordering::Equal, + n if n < 0 => Ordering::Less, + _ => Ordering::Greater, + } +} + +/// Converts a path to a CString that is usable by the libgit2 API. +/// +/// Checks if it is a relative path. +/// +/// On Windows, this also requires the path to be valid Unicode, and translates +/// back slashes to forward slashes. +pub fn path_to_repo_path(path: &Path) -> Result<CString, Error> { + macro_rules! err { + ($msg:literal, $path:expr) => { + return Err(Error::from_str(&format!($msg, $path.display()))) + }; + } + match path.components().next() { + None => return Err(Error::from_str("repo path should not be empty")), + Some(Component::Prefix(_)) => err!( + "repo path `{}` should be relative, not a windows prefix", + path + ), + Some(Component::RootDir) => err!("repo path `{}` should be relative", path), + Some(Component::CurDir) => err!("repo path `{}` should not start with `.`", path), + Some(Component::ParentDir) => err!("repo path `{}` should not start with `..`", path), + Some(Component::Normal(_)) => {} + } + #[cfg(windows)] + { + match path.to_str() { + None => { + return Err(Error::from_str( + "only valid unicode paths are accepted on windows", + )) + } + Some(s) => return fixup_windows_path(s), + } + } + #[cfg(not(windows))] + { + path.into_c_string() + } +} + +pub fn cstring_to_repo_path<T: IntoCString>(path: T) -> Result<CString, Error> { + fixup_windows_path(path.into_c_string()?) +} + +#[cfg(windows)] +fn fixup_windows_path<P: Into<Vec<u8>>>(path: P) -> Result<CString, Error> { + let mut bytes: Vec<u8> = path.into(); + for i in 0..bytes.len() { + if bytes[i] == b'\\' { + bytes[i] = b'/'; + } + } + Ok(CString::new(bytes)?) +} + +#[cfg(not(windows))] +fn fixup_windows_path(path: CString) -> Result<CString, Error> { + Ok(path) +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! assert_err { + ($path:expr, $msg:expr) => { + match path_to_repo_path(Path::new($path)) { + Ok(_) => panic!("expected `{}` to err", $path), + Err(e) => assert_eq!(e.message(), $msg), + } + }; + } + + macro_rules! assert_repo_path_ok { + ($path:expr) => { + assert_repo_path_ok!($path, $path) + }; + ($path:expr, $expect:expr) => { + assert_eq!( + path_to_repo_path(Path::new($path)), + Ok(CString::new($expect).unwrap()) + ); + }; + } + + #[test] + #[cfg(windows)] + fn path_to_repo_path_translate() { + assert_repo_path_ok!("foo"); + assert_repo_path_ok!("foo/bar"); + assert_repo_path_ok!(r"foo\bar", "foo/bar"); + assert_repo_path_ok!(r"foo\bar\", "foo/bar/"); + } + + #[test] + fn path_to_repo_path_no_weird() { + assert_err!("", "repo path should not be empty"); + assert_err!("./foo", "repo path `./foo` should not start with `.`"); + assert_err!("../foo", "repo path `../foo` should not start with `..`"); + } + + #[test] + #[cfg(not(windows))] + fn path_to_repo_path_no_absolute() { + assert_err!("/", "repo path `/` should be relative"); + assert_repo_path_ok!("foo/bar"); + } + + #[test] + #[cfg(windows)] + fn path_to_repo_path_no_absolute() { + assert_err!( + r"c:", + r"repo path `c:` should be relative, not a windows prefix" + ); + assert_err!( + r"c:\", + r"repo path `c:\` should be relative, not a windows prefix" + ); + assert_err!( + r"c:temp", + r"repo path `c:temp` should be relative, not a windows prefix" + ); + assert_err!( + r"\\?\UNC\a\b\c", + r"repo path `\\?\UNC\a\b\c` should be relative, not a windows prefix" + ); + assert_err!( + r"\\?\c:\foo", + r"repo path `\\?\c:\foo` should be relative, not a windows prefix" + ); + assert_err!( + r"\\.\COM42", + r"repo path `\\.\COM42` should be relative, not a windows prefix" + ); + assert_err!( + r"\\a\b", + r"repo path `\\a\b` should be relative, not a windows prefix" + ); + assert_err!(r"\", r"repo path `\` should be relative"); + assert_err!(r"/", r"repo path `/` should be relative"); + assert_err!(r"\foo", r"repo path `\foo` should be relative"); + assert_err!(r"/foo", r"repo path `/foo` should be relative"); + } +} diff --git a/extra/git2/src/version.rs b/extra/git2/src/version.rs new file mode 100644 index 000000000..b5dd4fb12 --- /dev/null +++ b/extra/git2/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/extra/git2/src/worktree.rs b/extra/git2/src/worktree.rs new file mode 100644 index 000000000..569b639cf --- /dev/null +++ b/extra/git2/src/worktree.rs @@ -0,0 +1,331 @@ +use crate::buf::Buf; +use crate::reference::Reference; +use crate::repo::Repository; +use crate::util::{self, Binding}; +use crate::{raw, Error}; +use std::os::raw::c_int; +use std::path::Path; +use std::ptr; +use std::str; +use std::{marker, mem}; + +/// An owned git worktree +/// +/// This structure corresponds to a `git_worktree` in libgit2. +// +pub struct Worktree { + raw: *mut raw::git_worktree, +} + +/// Options which can be used to configure how a worktree is initialized +pub struct WorktreeAddOptions<'a> { + raw: raw::git_worktree_add_options, + _marker: marker::PhantomData<Reference<'a>>, +} + +/// Options to configure how worktree pruning is performed +pub struct WorktreePruneOptions { + raw: raw::git_worktree_prune_options, +} + +/// Lock Status of a worktree +#[derive(PartialEq, Debug)] +pub enum WorktreeLockStatus { + /// Worktree is Unlocked + Unlocked, + /// Worktree is locked with the optional message + Locked(Option<String>), +} + +impl Worktree { + /// Open a worktree of a the repository + /// + /// If a repository is not the main tree but a worktree, this + /// function will look up the worktree inside the parent + /// repository and create a new `git_worktree` structure. + pub fn open_from_repository(repo: &Repository) -> Result<Worktree, Error> { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_worktree_open_from_repository(&mut raw, repo.raw())); + Ok(Binding::from_raw(raw)) + } + } + + /// Retrieves the name of the worktree + /// + /// This is the name that can be passed to repo::Repository::find_worktree + /// to reopen the worktree. This is also the name that would appear in the + /// list returned by repo::Repository::worktrees + pub fn name(&self) -> Option<&str> { + unsafe { + crate::opt_bytes(self, raw::git_worktree_name(self.raw)) + .and_then(|s| str::from_utf8(s).ok()) + } + } + + /// Retrieves the path to the worktree + /// + /// This is the path to the top-level of the source and not the path to the + /// .git file within the worktree. This path can be passed to + /// repo::Repository::open. + pub fn path(&self) -> &Path { + unsafe { + util::bytes2path(crate::opt_bytes(self, raw::git_worktree_path(self.raw)).unwrap()) + } + } + + /// Validates the worktree + /// + /// This checks that it still exists on the + /// filesystem and that the metadata is correct + pub fn validate(&self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_worktree_validate(self.raw)); + } + Ok(()) + } + + /// Locks the worktree + pub fn lock(&self, reason: Option<&str>) -> Result<(), Error> { + let reason = crate::opt_cstr(reason)?; + unsafe { + try_call!(raw::git_worktree_lock(self.raw, reason)); + } + Ok(()) + } + + /// Unlocks the worktree + pub fn unlock(&self) -> Result<(), Error> { + unsafe { + try_call!(raw::git_worktree_unlock(self.raw)); + } + Ok(()) + } + + /// Checks if worktree is locked + pub fn is_locked(&self) -> Result<WorktreeLockStatus, Error> { + let buf = Buf::new(); + unsafe { + match try_call!(raw::git_worktree_is_locked(buf.raw(), self.raw)) { + 0 => Ok(WorktreeLockStatus::Unlocked), + _ => { + let v = buf.to_vec(); + Ok(WorktreeLockStatus::Locked(match v.len() { + 0 => None, + _ => Some(String::from_utf8(v).unwrap()), + })) + } + } + } + } + + /// Prunes the worktree + pub fn prune(&self, opts: Option<&mut WorktreePruneOptions>) -> Result<(), Error> { + // When successful the worktree should be removed however the backing structure + // of the git_worktree should still be valid. + unsafe { + try_call!(raw::git_worktree_prune(self.raw, opts.map(|o| o.raw()))); + } + Ok(()) + } + + /// Checks if the worktree is prunable + pub fn is_prunable(&self, opts: Option<&mut WorktreePruneOptions>) -> Result<bool, Error> { + unsafe { + let rv = try_call!(raw::git_worktree_is_prunable( + self.raw, + opts.map(|o| o.raw()) + )); + Ok(rv != 0) + } + } +} + +impl<'a> WorktreeAddOptions<'a> { + /// Creates a default set of add options. + /// + /// By default this will not lock the worktree + pub fn new() -> WorktreeAddOptions<'a> { + unsafe { + let mut raw = mem::zeroed(); + assert_eq!( + raw::git_worktree_add_options_init(&mut raw, raw::GIT_WORKTREE_ADD_OPTIONS_VERSION), + 0 + ); + WorktreeAddOptions { + raw, + _marker: marker::PhantomData, + } + } + } + + /// If enabled, this will cause the newly added worktree to be locked + pub fn lock(&mut self, enabled: bool) -> &mut WorktreeAddOptions<'a> { + self.raw.lock = enabled as c_int; + self + } + + /// reference to use for the new worktree HEAD + pub fn reference( + &mut self, + reference: Option<&'a Reference<'_>>, + ) -> &mut WorktreeAddOptions<'a> { + self.raw.reference = if let Some(reference) = reference { + reference.raw() + } else { + ptr::null_mut() + }; + self + } + + /// Get a set of raw add options to be used with `git_worktree_add` + pub fn raw(&self) -> *const raw::git_worktree_add_options { + &self.raw + } +} + +impl WorktreePruneOptions { + /// Creates a default set of pruning options + /// + /// By defaults this will prune only worktrees that are no longer valid + /// unlocked and not checked out + pub fn new() -> WorktreePruneOptions { + unsafe { + let mut raw = mem::zeroed(); + assert_eq!( + raw::git_worktree_prune_options_init( + &mut raw, + raw::GIT_WORKTREE_PRUNE_OPTIONS_VERSION + ), + 0 + ); + WorktreePruneOptions { raw } + } + } + + /// Controls whether valid (still existing on the filesystem) worktrees + /// will be pruned + /// + /// Defaults to false + pub fn valid(&mut self, valid: bool) -> &mut WorktreePruneOptions { + self.flag(raw::GIT_WORKTREE_PRUNE_VALID, valid) + } + + /// Controls whether locked worktrees will be pruned + /// + /// Defaults to false + pub fn locked(&mut self, locked: bool) -> &mut WorktreePruneOptions { + self.flag(raw::GIT_WORKTREE_PRUNE_LOCKED, locked) + } + + /// Controls whether the actual working tree on the filesystem is recursively removed + /// + /// Defaults to false + pub fn working_tree(&mut self, working_tree: bool) -> &mut WorktreePruneOptions { + self.flag(raw::GIT_WORKTREE_PRUNE_WORKING_TREE, working_tree) + } + + fn flag(&mut self, flag: raw::git_worktree_prune_t, on: bool) -> &mut WorktreePruneOptions { + if on { + self.raw.flags |= flag as u32; + } else { + self.raw.flags &= !(flag as u32); + } + self + } + + /// Get a set of raw prune options to be used with `git_worktree_prune` + pub fn raw(&mut self) -> *mut raw::git_worktree_prune_options { + &mut self.raw + } +} + +impl Binding for Worktree { + type Raw = *mut raw::git_worktree; + unsafe fn from_raw(ptr: *mut raw::git_worktree) -> Worktree { + Worktree { raw: ptr } + } + fn raw(&self) -> *mut raw::git_worktree { + self.raw + } +} + +impl Drop for Worktree { + fn drop(&mut self) { + unsafe { raw::git_worktree_free(self.raw) } + } +} + +#[cfg(test)] +mod tests { + use crate::WorktreeAddOptions; + use crate::WorktreeLockStatus; + + use tempfile::TempDir; + + #[test] + fn smoke_add_no_ref() { + let (_td, repo) = crate::test::repo_init(); + + let wtdir = TempDir::new().unwrap(); + let wt_path = wtdir.path().join("tree-no-ref-dir"); + let opts = WorktreeAddOptions::new(); + + let wt = repo.worktree("tree-no-ref", &wt_path, Some(&opts)).unwrap(); + assert_eq!(wt.name(), Some("tree-no-ref")); + assert_eq!( + wt.path().canonicalize().unwrap(), + wt_path.canonicalize().unwrap() + ); + let status = wt.is_locked().unwrap(); + assert_eq!(status, WorktreeLockStatus::Unlocked); + } + + #[test] + fn smoke_add_locked() { + let (_td, repo) = crate::test::repo_init(); + + let wtdir = TempDir::new().unwrap(); + let wt_path = wtdir.path().join("locked-tree"); + let mut opts = WorktreeAddOptions::new(); + opts.lock(true); + + let wt = repo.worktree("locked-tree", &wt_path, Some(&opts)).unwrap(); + // shouldn't be able to lock a worktree that was created locked + assert!(wt.lock(Some("my reason")).is_err()); + assert_eq!(wt.name(), Some("locked-tree")); + assert_eq!( + wt.path().canonicalize().unwrap(), + wt_path.canonicalize().unwrap() + ); + assert_eq!(wt.is_locked().unwrap(), WorktreeLockStatus::Locked(None)); + assert!(wt.unlock().is_ok()); + assert!(wt.lock(Some("my reason")).is_ok()); + assert_eq!( + wt.is_locked().unwrap(), + WorktreeLockStatus::Locked(Some("my reason".to_string())) + ); + } + + #[test] + fn smoke_add_from_branch() { + let (_td, repo) = crate::test::repo_init(); + + let (wt_top, branch) = crate::test::worktrees_env_init(&repo); + let wt_path = wt_top.path().join("test"); + let mut opts = WorktreeAddOptions::new(); + let reference = branch.into_reference(); + opts.reference(Some(&reference)); + + let wt = repo + .worktree("test-worktree", &wt_path, Some(&opts)) + .unwrap(); + assert_eq!(wt.name(), Some("test-worktree")); + assert_eq!( + wt.path().canonicalize().unwrap(), + wt_path.canonicalize().unwrap() + ); + let status = wt.is_locked().unwrap(); + assert_eq!(status, WorktreeLockStatus::Unlocked); + } +} diff --git a/extra/git2/tests/add_extensions.rs b/extra/git2/tests/add_extensions.rs new file mode 100644 index 000000000..57c0eb976 --- /dev/null +++ b/extra/git2/tests/add_extensions.rs @@ -0,0 +1,21 @@ +//! 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() }?; + + assert_eq!(extensions.len(), 3); + assert_eq!(extensions.get(0), Some("custom")); + // The objectformat extension was added in 1.6 + assert_eq!(extensions.get(1), Some("noop")); + assert_eq!(extensions.get(2), Some("objectformat")); + + Ok(()) +} diff --git a/extra/git2/tests/get_extensions.rs b/extra/git2/tests/get_extensions.rs new file mode 100644 index 000000000..d8dd55fe0 --- /dev/null +++ b/extra/git2/tests/get_extensions.rs @@ -0,0 +1,16 @@ +//! 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() }?; + + assert_eq!(extensions.len(), 2); + assert_eq!(extensions.get(0), Some("noop")); + // The objectformat extension was added in 1.6 + assert_eq!(extensions.get(1), Some("objectformat")); + + Ok(()) +} diff --git a/extra/git2/tests/global_state.rs b/extra/git2/tests/global_state.rs new file mode 100644 index 000000000..192acdbd3 --- /dev/null +++ b/extra/git2/tests/global_state.rs @@ -0,0 +1,47 @@ +//! Test for some global state set up by libgit2's `git_libgit2_init` function +//! that need to be synchronized within a single process. + +use git2::opts; +use git2::{ConfigLevel, IntoCString}; + +// Test for mutating configuration file search path which is set during +// initialization in libgit2's `git_sysdir_global_init` function. +#[test] +fn search_path() -> Result<(), Box<dyn std::error::Error>> { + use std::env::join_paths; + + let path = "fake_path"; + let original = unsafe { opts::get_search_path(ConfigLevel::Global) }; + assert_ne!(original, Ok(path.into_c_string()?)); + + // Set + unsafe { + opts::set_search_path(ConfigLevel::Global, &path)?; + } + assert_eq!( + unsafe { opts::get_search_path(ConfigLevel::Global) }, + Ok(path.into_c_string()?) + ); + + // Append + let paths = join_paths(["$PATH", path].iter())?; + let expected_paths = join_paths([path, path].iter())?.into_c_string()?; + unsafe { + opts::set_search_path(ConfigLevel::Global, paths)?; + } + assert_eq!( + unsafe { opts::get_search_path(ConfigLevel::Global) }, + Ok(expected_paths) + ); + + // Reset + unsafe { + opts::reset_search_path(ConfigLevel::Global)?; + } + assert_eq!( + unsafe { opts::get_search_path(ConfigLevel::Global) }, + original + ); + + Ok(()) +} diff --git a/extra/git2/tests/remove_extensions.rs b/extra/git2/tests/remove_extensions.rs new file mode 100644 index 000000000..5f632a880 --- /dev/null +++ b/extra/git2/tests/remove_extensions.rs @@ -0,0 +1,19 @@ +//! 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", "other"])?; + } + + let extensions = unsafe { get_extensions() }?; + + assert_eq!(extensions.len(), 2); + assert_eq!(extensions.get(0), Some("custom")); + assert_eq!(extensions.get(1), Some("other")); + + Ok(()) +} |