aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKatharina Fey <kookie@spacekookie.de>2020-01-09 21:30:25 +0000
committerKatharina Fey <kookie@spacekookie.de>2020-01-10 01:05:26 +0000
commitfdb464b380dcda0f9afeee080ebe988e3934e02b (patch)
treeb46ebdea03117f2f6f82e65a2542ae4b6e8f2e5d
parentad1ad814ab2c4c89a3e8b6538f93c6c2b455efb3 (diff)
Refactoring libgitmail core to work on complete emails, with tests
-rw-r--r--libgitmail/README18
-rw-r--r--libgitmail/email.txt86
-rw-r--r--libgitmail/src/lib.rs10
-rw-r--r--libgitmail/src/patch/mod.rs103
-rw-r--r--libgitmail/src/patch/parsers.rs163
-rw-r--r--libgitmail/tests/basic.rs26
6 files changed, 406 insertions, 0 deletions
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: <xms:OakIXj9KSSuHqRfGLe6tUc0G3UgzkdLMKEwQ09Mv9Mtug1erzbaCKA>
+X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedufedrvdeffedgheduucetufdoteggodetrfdotf
+ fvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfqfgfvpdfurfetoffkrfgpnffqhgen
+ uceurghilhhouhhtmecufedttdenucenucfjughrpefhvffufffkofgggfestdekredtre
+ dttdenucfhrhhomheptehlhihsshgrucftohhsshcuoehhihesrghlhihsshgrrdhisheq
+ necuffhomhgrihhnpegvmhgsvghrqdgtlhhirdgtohhmpdhgohhoghhlvgdrtghomhdpfh
+ hinhgvrdguohgtshdpnhhouggvjhhsrdhorhhgpdihrghrnhhpkhhgrdgtohhmpdhgihht
+ qdhstghmrdgtohhmnecukfhppeduhedurddvudejrddvudejrdduheegnecurfgrrhgrmh
+ epmhgrihhlfhhrohhmpehqhihlihhsshesgidvvddtrdhqhihlihhsshdrnhgvthenucev
+ lhhushhtvghrufhiiigvpedt
+X-ME-Proxy: <xmx:OakIXu9us3-tg5ttMt9F2aiP33qpkoccDHkUI6wgKBwjRBEOv1KQtA>
+ <xmx:OakIXj3qwzXAlA5-K5KvaNTRMDcPQ83Spc8FsTXreEkvZTkOg_oSog>
+ <xmx:OakIXm9h6ztAwRW9tHaBXehYrCR55Q-F9jD1NXrfwEB4iRuT_Vkjkg>
+ <xmx:OqkIXi95Uuou4hos4yeU2YHX4DogCTC9OISbfvyBX9mxhKulMTAuUg>
+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 <hi@alyssa.is>
+To: ~qaul/community@lists.sr.ht
+Cc: Katharina Fey <kookie@spacekookie.de>,
+ Mathias Jud <jud@open-communication.net>,
+ Lux <lux@lux.name>,
+ Alyssa Ross <hi@alyssa.is>
+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<T> = std::result::Result<T, Error>;
+
/// 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<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
+ }
+ );
+}
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()
+ }
+ }
+ )
+}