From 4461b171f6a988f1b9a37e30addb82f7bf139454 Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Mon, 18 Oct 2021 17:20:30 +0200 Subject: [PATCH] Add binding for `git_message_trailers` (#749) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add binding for `git_message_trailers` * `cargo fmt` * Fix pointer mismatch error Fix error from `cargo run --manifest-path systest/Cargo.toml;`. You can’t just mix and match `*mut` and `*const` like that. * Remove a lot of unnecessary lifetimes Suggested-by: Alex Crichton See: https://github.com/rust-lang/git2-rs/pull/749#discussion_r726550603 * Remove another unnecessary lifetime Suggested-by: Alex Crichton See: https://github.com/rust-lang/git2-rs/pull/749#discussion_r726550919 * Use `Range` instead of `usize` I love it. Suggested-by: Alex Crichton See: https://github.com/rust-lang/git2-rs/pull/749#discussion_r726551294 * `cargo fmt` * Inline one-off struct Suggested-by: Alex Crichton See: https://github.com/rust-lang/git2-rs/pull/749#discussion_r726551473 * Implement more iterators Also change `to_str_tuple(…)` in order to share more code between two of the iterators. Suggested-by: Alex Crichton See: https://github.com/rust-lang/git2-rs/pull/749#discussion_r726551780 * Undo accidental and unrelated edit * Less explicit lifetimes See: https://github.com/rust-lang/git2-rs/pull/749#discussion_r729899152 * Don’t need `std::marker` any more See: https://github.com/rust-lang/git2-rs/pull/749#discussion_r729899304 * Correct `len(…)` See: https://github.com/rust-lang/git2-rs/pull/749#discussion_r729900328 * Remove unnecessary annotation See: https://github.com/rust-lang/git2-rs/pull/749#discussion_r729900889 * Implement `size_hint()` Better than the default implementation. See: https://github.com/rust-lang/git2-rs/pull/749#discussion_r729900533 * Split into “bytes” and “string” iterators Support both raw bytes messages as well as normal (UTF-8) messages by making two iterators. See: https://github.com/rust-lang/git2-rs/pull/749#discussion_r729951586 * Remove more lifetimes * Docs * `cargo fmt` * Undo accidental and unrelated edit (cherry picked from commit cd4c2dbee5f91610011913d3cd10e5786fbceef5) --- libgit2-sys/lib.rs | 21 ++++ src/lib.rs | 6 +- src/message.rs | 285 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 310 insertions(+), 2 deletions(-) diff --git a/libgit2-sys/lib.rs b/libgit2-sys/lib.rs index 5e277b31ef..7f128cd0db 100644 --- a/libgit2-sys/lib.rs +++ b/libgit2-sys/lib.rs @@ -1953,6 +1953,20 @@ git_enum! { } } +#[repr(C)] +pub struct git_message_trailer { + pub key: *const c_char, + pub value: *const c_char, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct git_message_trailer_array { + pub trailers: *mut git_message_trailer, + pub count: size_t, + pub _trailer_block: *mut c_char, +} + extern "C" { // threads pub fn git_libgit2_init() -> c_int; @@ -3678,6 +3692,13 @@ extern "C" { comment_char: c_char, ) -> c_int; + pub fn git_message_trailers( + out: *mut git_message_trailer_array, + message: *const c_char, + ) -> c_int; + + pub fn git_message_trailer_array_free(trailer: *mut git_message_trailer_array); + // packbuilder pub fn git_packbuilder_new(out: *mut *mut git_packbuilder, repo: *mut git_repository) -> c_int; pub fn git_packbuilder_set_threads(pb: *mut git_packbuilder, n: c_uint) -> c_uint; diff --git a/src/lib.rs b/src/lib.rs index bcabcdf044..2456877f6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -101,7 +101,11 @@ pub use crate::indexer::{IndexerProgress, Progress}; pub use crate::mailmap::Mailmap; pub use crate::mempack::Mempack; pub use crate::merge::{AnnotatedCommit, MergeOptions}; -pub use crate::message::{message_prettify, DEFAULT_COMMENT_CHAR}; +pub use crate::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}; diff --git a/src/message.rs b/src/message.rs index 7c17eeffe0..398f11659f 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,4 +1,7 @@ +use core::ops::Range; +use std::ffi::CStr; use std::ffi::CString; +use std::ptr; use libc::{c_char, c_int}; @@ -31,12 +34,216 @@ fn _message_prettify(message: CString, comment_char: Option) -> Result = Some(b'#'); +/// Get the trailers for the given message. +/// +/// Use this function when you are dealing with a UTF-8-encoded message. +pub fn message_trailers_strs(message: &str) -> Result { + _message_trailers(message.into_c_string()?).map(|res| MessageTrailersStrs(res)) +} + +/// Get the trailers for the given message. +/// +/// Use this function when the message might not be UTF-8-encoded, +/// or if you want to handle the returned trailer key–value pairs +/// as bytes. +pub fn message_trailers_bytes(message: S) -> Result { + _message_trailers(message.into_c_string()?).map(|res| MessageTrailersBytes(res)) +} + +fn _message_trailers(message: CString) -> Result { + let ret = MessageTrailers::new(); + unsafe { + try_call!(raw::git_message_trailers(ret.raw(), message)); + } + Ok(ret) +} + +/// Collection of UTF-8-encoded trailers. +/// +/// Use `iter()` to get access to the values. +pub struct MessageTrailersStrs(MessageTrailers); + +impl MessageTrailersStrs { + /// Create a borrowed iterator. + pub fn iter(&self) -> MessageTrailersStrsIterator<'_> { + MessageTrailersStrsIterator(self.0.iter()) + } + /// The number of trailer key–value pairs. + pub fn len(&self) -> usize { + self.0.len() + } + /// Convert to the “bytes” variant. + pub fn to_bytes(self) -> MessageTrailersBytes { + MessageTrailersBytes(self.0) + } +} + +/// Collection of unencoded (bytes) trailers. +/// +/// Use `iter()` to get access to the values. +pub struct MessageTrailersBytes(MessageTrailers); + +impl MessageTrailersBytes { + /// Create a borrowed iterator. + pub fn iter(&self) -> MessageTrailersBytesIterator<'_> { + MessageTrailersBytesIterator(self.0.iter()) + } + /// The number of trailer key–value pairs. + pub fn len(&self) -> usize { + self.0.len() + } +} + +struct MessageTrailers { + raw: raw::git_message_trailer_array, +} + +impl MessageTrailers { + fn new() -> MessageTrailers { + crate::init(); + unsafe { + Binding::from_raw(&mut raw::git_message_trailer_array { + trailers: ptr::null_mut(), + count: 0, + _trailer_block: ptr::null_mut(), + } as *mut _) + } + } + fn iter(&self) -> MessageTrailersIterator<'_> { + MessageTrailersIterator { + trailers: self, + range: Range { + start: 0, + end: self.raw.count, + }, + } + } + fn len(&self) -> usize { + self.raw.count + } +} + +impl Drop for MessageTrailers { + fn drop(&mut self) { + unsafe { + raw::git_message_trailer_array_free(&mut self.raw); + } + } +} + +impl Binding for MessageTrailers { + type Raw = *mut raw::git_message_trailer_array; + unsafe fn from_raw(raw: *mut raw::git_message_trailer_array) -> MessageTrailers { + MessageTrailers { raw: *raw } + } + fn raw(&self) -> *mut raw::git_message_trailer_array { + &self.raw as *const _ as *mut _ + } +} + +struct MessageTrailersIterator<'a> { + trailers: &'a MessageTrailers, + range: Range, +} + +fn to_raw_tuple(trailers: &MessageTrailers, index: usize) -> (*const c_char, *const c_char) { + unsafe { + let addr = trailers.raw.trailers.wrapping_add(index); + ((*addr).key, (*addr).value) + } +} + +/// Borrowed iterator over the UTF-8-encoded trailers. +pub struct MessageTrailersStrsIterator<'a>(MessageTrailersIterator<'a>); + +impl<'pair> Iterator for MessageTrailersStrsIterator<'pair> { + type Item = (&'pair str, &'pair str); + + fn next(&mut self) -> Option { + self.0 + .range + .next() + .map(|index| to_str_tuple(&self.0.trailers, index)) + } + + fn size_hint(&self) -> (usize, Option) { + self.0.range.size_hint() + } +} + +impl ExactSizeIterator for MessageTrailersStrsIterator<'_> { + fn len(&self) -> usize { + self.0.range.len() + } +} + +impl DoubleEndedIterator for MessageTrailersStrsIterator<'_> { + fn next_back(&mut self) -> Option { + self.0 + .range + .next_back() + .map(|index| to_str_tuple(&self.0.trailers, index)) + } +} + +fn to_str_tuple(trailers: &MessageTrailers, index: usize) -> (&str, &str) { + unsafe { + let (rkey, rvalue) = to_raw_tuple(&trailers, index); + let key = CStr::from_ptr(rkey).to_str().unwrap(); + let value = CStr::from_ptr(rvalue).to_str().unwrap(); + (key, value) + } +} + +/// Borrowed iterator over the raw (bytes) trailers. +pub struct MessageTrailersBytesIterator<'a>(MessageTrailersIterator<'a>); + +impl<'pair> Iterator for MessageTrailersBytesIterator<'pair> { + type Item = (&'pair [u8], &'pair [u8]); + + fn next(&mut self) -> Option { + self.0 + .range + .next() + .map(|index| to_bytes_tuple(&self.0.trailers, index)) + } + + fn size_hint(&self) -> (usize, Option) { + self.0.range.size_hint() + } +} + +impl ExactSizeIterator for MessageTrailersBytesIterator<'_> { + fn len(&self) -> usize { + self.0.range.len() + } +} + +impl DoubleEndedIterator for MessageTrailersBytesIterator<'_> { + fn next_back(&mut self) -> Option { + self.0 + .range + .next_back() + .map(|index| to_bytes_tuple(&self.0.trailers, index)) + } +} + +fn to_bytes_tuple(trailers: &MessageTrailers, index: usize) -> (&[u8], &[u8]) { + unsafe { + let (rkey, rvalue) = to_raw_tuple(&trailers, index); + let key = CStr::from_ptr(rkey).to_bytes(); + let value = CStr::from_ptr(rvalue).to_bytes(); + (key, value) + } +} + #[cfg(test)] mod tests { - use crate::{message_prettify, DEFAULT_COMMENT_CHAR}; #[test] fn prettify() { + use crate::{message_prettify, DEFAULT_COMMENT_CHAR}; + // This does not attempt to duplicate the extensive tests for // git_message_prettify in libgit2, just a few representative values to // make sure the interface works as expected. @@ -58,4 +265,80 @@ mod tests { "1\n" ); } + + #[test] + fn trailers() { + use crate::{message_trailers_bytes, message_trailers_strs, MessageTrailersStrs}; + use std::collections::HashMap; + + // no trailers + let message1 = " +WHAT ARE WE HERE FOR + +What are we here for? + +Just to be eaten? +"; + let expected: HashMap<&str, &str> = HashMap::new(); + assert_eq!(expected, to_map(&message_trailers_strs(message1).unwrap())); + + // standard PSA + let message2 = " +Attention all + +We are out of tomatoes. + +Spoken-by: Major Turnips +Transcribed-by: Seargant Persimmons +Signed-off-by: Colonel Kale +"; + let expected: HashMap<&str, &str> = vec![ + ("Spoken-by", "Major Turnips"), + ("Transcribed-by", "Seargant Persimmons"), + ("Signed-off-by", "Colonel Kale"), + ] + .into_iter() + .collect(); + assert_eq!(expected, to_map(&message_trailers_strs(message2).unwrap())); + + // ignore everything after `---` + let message3 = " +The fate of Seargant Green-Peppers + +Seargant Green-Peppers was killed by Caterpillar Battalion 44. + +Signed-off-by: Colonel Kale +--- +I never liked that guy, anyway. + +Opined-by: Corporal Garlic +"; + let expected: HashMap<&str, &str> = vec![("Signed-off-by", "Colonel Kale")] + .into_iter() + .collect(); + assert_eq!(expected, to_map(&message_trailers_strs(message3).unwrap())); + + // Raw bytes message; not valid UTF-8 + // Source: https://stackoverflow.com/a/3886015/1725151 + let message4 = b" +Be honest guys + +Am I a malformed brussels sprout? + +Signed-off-by: Lieutenant \xe2\x28\xa1prout +"; + + let trailer = message_trailers_bytes(&message4[..]).unwrap(); + let expected = (&b"Signed-off-by"[..], &b"Lieutenant \xe2\x28\xa1prout"[..]); + let actual = trailer.iter().next().unwrap(); + assert_eq!(expected, actual); + + fn to_map(trailers: &MessageTrailersStrs) -> HashMap<&str, &str> { + let mut map = HashMap::with_capacity(trailers.len()); + for (key, value) in trailers.iter() { + map.insert(key, value); + } + map + } + } }