From fdb464b380dcda0f9afeee080ebe988e3934e02b Mon Sep 17 00:00:00 2001 From: Katharina Fey Date: Thu, 9 Jan 2020 21:30:25 +0000 Subject: Refactoring libgitmail core to work on complete emails, with tests --- libgitmail/README | 18 +++++ libgitmail/email.txt | 86 +++++++++++++++++++++ libgitmail/src/lib.rs | 10 +++ libgitmail/src/patch/mod.rs | 103 +++++++++++++++++++++++++ libgitmail/src/patch/parsers.rs | 163 ++++++++++++++++++++++++++++++++++++++++ libgitmail/tests/basic.rs | 26 +++++++ 6 files changed, 406 insertions(+) create mode 100644 libgitmail/README create mode 100644 libgitmail/email.txt create mode 100644 libgitmail/src/patch/mod.rs create mode 100644 libgitmail/src/patch/parsers.rs create mode 100644 libgitmail/tests/basic.rs diff --git a/libgitmail/README b/libgitmail/README new file mode 100644 index 0000000..0d3ba07 --- /dev/null +++ b/libgitmail/README @@ -0,0 +1,18 @@ +libgitmail +========== + +This library provides an abstraction over git sent email patches. It +parses incoming mail and provides a strongly typed interface for +interacting with patches, patch trees, as well as building patch sets +from a tree thread with many revisions. + +It then compiles a new thread of mail that can be piped directly into +git am - by some user interface. + + +License +------- + +This library is part of dev-suite and as such licensed under the GNU +General Public License 3.0 or, (at your choosing), any later version. +See the LICENSE.md in the parent repository for more details. diff --git a/libgitmail/email.txt b/libgitmail/email.txt new file mode 100644 index 0000000..ac950ad --- /dev/null +++ b/libgitmail/email.txt @@ -0,0 +1,86 @@ +Received: from wout5-smtp.messagingengine.com (wout5-smtp.messagingengine.com [64.147.123.21]) + by mail.sr.ht (Postfix) with ESMTPS id 9C1CE402A6 + for <~qaul/community@lists.sr.ht>; Sun, 29 Dec 2019 13:25:16 +0000 (UTC) +Authentication-Results: mail.sr.ht; + dkim=pass (2048-bit key) header.d=alyssa.is header.i=@alyssa.is header.b=bK0bsSUp; + dkim=pass (2048-bit key) header.d=messagingengine.com header.i=@messagingengine.com header.b=HzRR6htT +Received: from compute5.internal (compute5.nyi.internal [10.202.2.45]) + by mailout.west.internal (Postfix) with ESMTP id 226173D1; + Sun, 29 Dec 2019 08:25:15 -0500 (EST) +Received: from mailfrontend2 ([10.202.2.163]) + by compute5.internal (MEProxy); Sun, 29 Dec 2019 08:25:15 -0500 +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=alyssa.is; h= + from:to:cc:subject:date:message-id:mime-version + :content-transfer-encoding; s=fm2; bh=pcHi92xLPx2ZwvL3WD16XEuRb2 + iD1LAkLhnN11wIKrE=; b=bK0bsSUpdzMvscbhhfMuAzxVdz/+/N9ceeiAXxJoBG + 7Ley+qOF9lHXtXgJg8+cP32f51P871M+X9L5qzKTFKwYZE3PUl1sO0Xhj0FAHBqa + enMXkCvxZbSn3yz8Y/v+zk0GPIcvd99CkPELkkTusR19EME5hV3AymYZKiln42UV + Tc6NYsGzaEvdgH8Lq3xGz1mhcC5knHYelKZz2LZu2D6aQy8WZOBxZDRkwlfdfb85 + yQ7faBRaxfiw6jO/8aspKr1DLiGbE+2OWOwj/kEcIyD1gbMwjJb8NGHHrYWxj32q + juSdIHdWZgmO0Y397wAi2CeBvch/03g9dkyy6LQEEvDQ== +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= + messagingengine.com; h=cc:content-transfer-encoding:date:from + :message-id:mime-version:subject:to:x-me-proxy:x-me-proxy + :x-me-sender:x-me-sender:x-sasl-enc; s=fm1; bh=pcHi92xLPx2ZwvL3W + D16XEuRb2iD1LAkLhnN11wIKrE=; b=HzRR6htTKCdXVTf44qHumeH7nU/wgi9Ic + NFdHpWpMW1eH24huf4K4bhRf5Zl+9UVcWLOhYgYwQcRUkBlOcKrGwmxeL+0/ZmZR + AJqONj6xNJAZatmKsPuk9ZWd/59YlDw6CwxCw7De5qPYS6xaYTKIlkC67OuftPIa + wykga/tK9r0cG6nYwiEKwNR2hhUi8VrfNAMhwGbaNAHvGy4UY8tn4fhzmJKc1hn1 + s9LHzZUOriwFFE2PmCLEooNLosN47xp3T2g2lBCsT//SiJTv8Rm+Qq5kRMG2iUgp + Dh3Vx/OoYzmXUATFfG2BCWoiwQlCmC3/2N6ph4Pcjs17DtmOk8y4g== +X-ME-Sender: +X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedufedrvdeffedgheduucetufdoteggodetrfdotf + fvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfqfgfvpdfurfetoffkrfgpnffqhgen + uceurghilhhouhhtmecufedttdenucenucfjughrpefhvffufffkofgggfestdekredtre + dttdenucfhrhhomheptehlhihsshgrucftohhsshcuoehhihesrghlhihsshgrrdhisheq + necuffhomhgrihhnpegvmhgsvghrqdgtlhhirdgtohhmpdhgohhoghhlvgdrtghomhdpfh + hinhgvrdguohgtshdpnhhouggvjhhsrdhorhhgpdihrghrnhhpkhhgrdgtohhmpdhgihht + qdhstghmrdgtohhmnecukfhppeduhedurddvudejrddvudejrdduheegnecurfgrrhgrmh + epmhgrihhlfhhrohhmpehqhihlihhsshesgidvvddtrdhqhihlihhsshdrnhgvthenucev + lhhushhtvghrufhiiigvpedt +X-ME-Proxy: + + + +Received: from x220.qyliss.net (unknown [151.217.217.154]) + by mail.messagingengine.com (Postfix) with ESMTPA id A5F8B30608D7; + Sun, 29 Dec 2019 08:25:13 -0500 (EST) +Received: by x220.qyliss.net (Postfix, from userid 1000) + id E0BA68D7; Sun, 29 Dec 2019 13:25:10 +0000 (UTC) +From: Alyssa Ross +To: ~qaul/community@lists.sr.ht +Cc: Katharina Fey , + Mathias Jud , + Lux , + Alyssa Ross +Subject: [PATCH] docs: Node.js 10 works fine (now?) +Date: Sun, 29 Dec 2019 13:24:05 +0000 +Message-Id: <20191229132404.14579-1-hi@alyssa.is> +X-Mailer: git-send-email 2.23.0 +MIME-Version: 1.0 +Content-Transfer-Encoding: quoted-printable + +--- +I tested with Node 10 on master and feature/webgui/responsive, and +both worked fine. + + docs/contributors/src/technical/webgui/install.md | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/docs/contributors/src/technical/webgui/install.md b/docs/con= +tributors/src/technical/webgui/install.md +index db197ff..6501d91 100644 +--- a/docs/contributors/src/technical/webgui/install.md ++++ b/docs/contributors/src/technical/webgui/install.md +@@ -10,7 +10,7 @@ to get you up and running. + You will need the following things properly installed on your computer. +=20 + * [Git](https://git-scm.com/) +-* [Node.js](https://nodejs.org/) *(you need node version <10 due depende= +ncies..., 2018/05/07)* ++* [Node.js](https://nodejs.org/) + * [Yarn](https://yarnpkg.com/) + * [Ember CLI](https://ember-cli.com/) + * [Google Chrome](https://google.com/chrome/) +--=20 +2.23.0 diff --git a/libgitmail/src/lib.rs b/libgitmail/src/lib.rs index 81de51d..53bb6b8 100644 --- a/libgitmail/src/lib.rs +++ b/libgitmail/src/lib.rs @@ -10,6 +10,14 @@ //! structure of a pathset with a way to select specific parts of the //! set to export. Look at the `PatchSet` type for more information. +mod patch; +pub use patch::*; + +pub mod set; +pub mod tree; + +pub type Result = std::result::Result; + /// A mail error type #[derive(Debug)] pub enum Error { @@ -17,4 +25,6 @@ pub enum Error { FailedParsing, /// The provided email is not a valid get-sent mail NotAGitMail, + /// Generally a git email, but malformed + InvalidGitFormat, } 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; + +/// 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 { + 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), +} + +impl Header { + fn single(self) -> Result { + 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, + 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 { + 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, + /// 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 { + 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::().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::().unwrap_or(1); + let this = this_opt.unwrap().parse::().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
{ + headers.get(key).map_or(Err(Error::FailedParsing), |h| { + match h.split(",").map(|s| s.trim()).collect::>() { + 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 + } + ); +} diff --git a/libgitmail/tests/basic.rs b/libgitmail/tests/basic.rs new file mode 100644 index 0000000..0271784 --- /dev/null +++ b/libgitmail/tests/basic.rs @@ -0,0 +1,26 @@ +#![allow(warnings, unused)] + +use libgitmail::{Id, Patch, Segment, Subject}; + +const TEST_MAIL: &'static str = include_str!("../email.txt"); + +/// Parse an e-mail that was generated by git-send-email in a real projectf +#[test] +fn parse_mail() { + let headers = Patch::preprocess(TEST_MAIL); + assert_eq!( + Patch::new(TEST_MAIL).unwrap(), + Patch { + id: Id("<20191229132404.14579-1-hi@alyssa.is>".into()), + headers, + raw: TEST_MAIL, + reply_to: None, + subject: Subject { + version: 1, + segment: None, + prefix: "PATCH".into(), + message: "docs: Node.js 10 works fine (now?)".into() + } + } + ) +} -- cgit v1.2.3