aboutsummaryrefslogtreecommitdiff
path: root/libgitmail/src/patch
diff options
context:
space:
mode:
Diffstat (limited to 'libgitmail/src/patch')
-rw-r--r--libgitmail/src/patch/mod.rs103
-rw-r--r--libgitmail/src/patch/parsers.rs163
2 files changed, 266 insertions, 0 deletions
diff --git a/libgitmail/src/patch/mod.rs b/libgitmail/src/patch/mod.rs
new file mode 100644
index 0000000..b4b09a4
--- /dev/null
+++ b/libgitmail/src/patch/mod.rs
@@ -0,0 +1,103 @@
+use crate::{Error, Result};
+use std::collections::HashMap;
+
+mod parsers;
+use parsers::get_header;
+pub use parsers::{Segment, Subject};
+
+#[doc(hidden)]
+pub type HeaderMap = HashMap<String, String>;
+
+/// Message ID of an email in a thread
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Id(pub String);
+
+impl Id {
+ pub fn from(headers: &HeaderMap, key: &str) -> Result<Self> {
+ get_header(&headers, key).and_then(|h| match h {
+ Header::Single(h) => Ok(Self(h)),
+ _ => Err(Error::FailedParsing),
+ })
+ }
+}
+
+/// A semi typed header value for mail
+pub(crate) enum Header {
+ /// A single header value
+ Single(String),
+ /// A set of values that was separated by `,`
+ Multi(Vec<String>),
+}
+
+impl Header {
+ fn single(self) -> Result<String> {
+ match self {
+ Self::Single(s) => Ok(s),
+ Self::Multi(_) => Err(Error::FailedParsing),
+ }
+ }
+}
+
+/// A single patch, metadata and email body
+///
+/// This type is constructed from a single email in a thread, and can
+/// then be combined into a [PatchSet](struct.PatchSet.html) via the
+/// [PatchTree](struct.PatchTree.html) builder.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Patch<'mail> {
+ pub id: Id,
+ pub reply_to: Option<Id>,
+ pub subject: Subject,
+ pub headers: HeaderMap,
+ pub raw: &'mail str,
+}
+
+impl<'mail> Patch<'mail> {
+ #[doc(hidden)]
+ pub fn preprocess(raw: &'mail str) -> HeaderMap {
+ let mail = mailparse::parse_mail(raw.as_bytes())
+ .map_err(|_| Error::FailedParsing)
+ .unwrap();
+ mail
+ .headers
+ .into_iter()
+ .fold(HashMap::new(), |mut acc, header| {
+ let key = header.get_key().unwrap();
+ let val = header.get_value().unwrap();
+ acc.insert(key, val);
+ acc
+ })
+ }
+
+ pub fn new(raw: &'mail str) -> Result<Self> {
+ let mail = mailparse::parse_mail(raw.as_bytes())
+ .map_err(|_| Error::FailedParsing)?;
+ let headers =
+ mail
+ .headers
+ .into_iter()
+ .fold(HashMap::new(), |mut acc, header| {
+ let key = header.get_key().unwrap();
+ let val = header.get_value().unwrap();
+ acc.insert(key, val);
+ acc
+ });
+
+ get_header(&headers, "X-Mailer").and_then(|h| match h {
+ Header::Single(s) if s.contains("git-send-email") => Ok(()),
+ _ => Err(Error::NotAGitMail),
+ })?;
+
+ Ok(Self {
+ id: Id::from(&headers, "Message-Id")?,
+ reply_to: Id::from(&headers, "In-Reply-To")
+ .map(|id| Some(id))
+ .unwrap_or(None),
+ subject: get_header(&headers, "Subject")
+ .and_then(|h| h.single())
+ .and_then(|s| Subject::from(s))?,
+ headers,
+ raw,
+ })
+ }
+}
diff --git a/libgitmail/src/patch/parsers.rs b/libgitmail/src/patch/parsers.rs
new file mode 100644
index 0000000..9779e2b
--- /dev/null
+++ b/libgitmail/src/patch/parsers.rs
@@ -0,0 +1,163 @@
+//! Contains additional parsers that are required for git patch emails
+//!
+//! The parsers are written in nom, and generate
+//! [Patch](../struct.Patch.html) objects, or an Error. A parser
+//! should neven panic.
+//!
+//! There's also some unit and quick check tests that make sure that
+//! valid git patch emails can be parsed. If you have emails you
+//! don't mind becoming part of the test set, feel free to email in a
+//! patch!
+
+use crate::{
+ patch::{Header, HeaderMap},
+ Error, Result,
+};
+use nom::{
+ bytes::complete::{is_not, tag, take_till},
+ character::complete::char,
+ combinator::opt,
+ sequence::{delimited, preceded},
+ IResult,
+};
+
+/// The segment data for a patch mail
+#[derive(Clone, Default, Debug, PartialEq, Eq)]
+pub struct Segment {
+ /// The number of a patch in it's series
+ pub this: u8,
+ /// The total number of patches in a seriesThe first
+ pub max: u8,
+}
+
+/// The typed subject line of a patch mail
+///
+/// A message might be: "[PATCH v2 1/5], which is parsed into this
+/// structure to be easier to work with
+#[derive(Clone, Default, Debug, PartialEq, Eq)]
+pub struct Subject {
+ /// The patch revision (e.g. `v2`)
+ pub version: u8,
+ /// Patch segment information (e.g. `1/5`)
+ pub segment: Option<Segment>,
+ /// Subject prefix (e.g. `PATCH`)
+ pub prefix: String,
+ /// The actual patch commit message
+ pub message: String,
+}
+
+impl Subject {
+ /// Create a typed subject from a subject string
+ pub fn from(s: String) -> Result<Self> {
+ Ok(Self::parse(s.as_str()).unwrap().1)
+ }
+
+ /// This is a terrible function that does terrible things
+ fn parse(input: &str) -> IResult<&str, Self> {
+ let input = opt(tag("Re:"))(input).map(|(i, _)| i.trim())?;
+ let (msg, input) = Self::parens(input).map(|(a, b)| (a.trim(), b))?;
+ let (input, prefix) =
+ take_till(|c| c == ' ')(input).map(|(a, b)| (a.trim(), b))?;
+
+ let (input, version) =
+ opt(preceded(tag("v"), take_till(|c| c == ' ')))(input)
+ .map(|(i, v)| (i.trim(), v.map_or(1, |v| v.parse::<u8>().unwrap())))?;
+
+ let (max_str, this_opt) = opt(take_till(|c| c == '/'))(input)?;
+
+ let segment = if max_str.len() > 1 {
+ let max = max_str[1..].parse::<u8>().unwrap_or(1);
+ let this = this_opt.unwrap().parse::<u8>().unwrap();
+ Some(Segment { max, this })
+ } else {
+ None
+ };
+
+ Ok((
+ input,
+ Self {
+ message: msg.into(),
+ prefix: prefix.into(),
+ version,
+ segment,
+ },
+ ))
+ }
+
+ /// Grab text between the [ ... ] section of a subject
+ fn parens(input: &str) -> IResult<&str, &str> {
+ delimited(char('['), is_not("]"), char(']'))(input)
+ }
+}
+
+/// A utility function that can get a header into a semi-typed field
+pub(crate) fn get_header(headers: &HeaderMap, key: &str) -> Result<Header> {
+ headers.get(key).map_or(Err(Error::FailedParsing), |h| {
+ match h.split(",").map(|s| s.trim()).collect::<Vec<_>>() {
+ vec if vec.len() == 0 => Err(Error::FailedParsing),
+ vec if vec.len() == 1 => {
+ Ok(Header::Single(String::from(*vec.get(0).unwrap())))
+ }
+ vec => Ok(Header::Multi(
+ vec.into_iter().map(|s| String::from(s)).collect(),
+ )),
+ }
+ })
+}
+
+#[test]
+fn full_test() {
+ let subject =
+ "Re: [PATCH v2 2/3] docs: fix spelling of \"contributors\"".into();
+ assert_eq!(
+ Subject::from(subject).unwrap(),
+ Subject {
+ message: "docs: fix spelling of \"contributors\"".into(),
+ prefix: "PATCH".into(),
+ version: 2,
+ segment: Some(Segment { max: 3, this: 2 })
+ }
+ );
+}
+
+#[test]
+fn minimal_test() {
+ let subject = "Re: [PATCH] docs: fix spelling of \"contributors\"".into();
+ assert_eq!(
+ Subject::from(subject).unwrap(),
+ Subject {
+ message: "docs: fix spelling of \"contributors\"".into(),
+ prefix: "PATCH".into(),
+ version: 1,
+ segment: None
+ }
+ );
+}
+
+#[test]
+fn version_test() {
+ let subject = "Re: [PATCH v4] docs: fix spelling of \"contributors\"".into();
+ assert_eq!(
+ Subject::from(subject).unwrap(),
+ Subject {
+ message: "docs: fix spelling of \"contributors\"".into(),
+ prefix: "PATCH".into(),
+ version: 4,
+ segment: None
+ }
+ );
+}
+
+#[test]
+fn double_digit_version_test() {
+ let subject = "Re: [PATCH v11] docs: fix spelling of \"contributors\"".into();
+ assert_eq!(
+ Subject::from(subject).unwrap(),
+ Subject {
+ message: "docs: fix spelling of \"contributors\"".into(),
+ prefix: "PATCH".into(),
+ version: 11,
+ segment: None
+ }
+ );
+}