Implement commit parsing/formatting
To be able to support git commits with multiple signature headers, we have to implement our own parsing and formatting. Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
parent
5b673c7440
commit
fece740fcb
|
|
@ -274,3 +274,180 @@ pub fn run<P: AsRef<Path>, S: AsRef<std::ffi::OsStr>>(
|
||||||
String::from_utf8_lossy(&output.stderr),
|
String::from_utf8_lossy(&output.stderr),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parsing and formatting of commit objects.
|
||||||
|
/// This module exists to work with commits that have multiple signature headers.
|
||||||
|
pub mod commit {
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// A parsed commit object.
|
||||||
|
/// Contains the full commit header and body.
|
||||||
|
///
|
||||||
|
/// Can be created with the [`FromStr`] instance, and formatted with the [`ToString`]
|
||||||
|
/// instance.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CommitObject {
|
||||||
|
headers: Vec<(String, String)>,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommitObject {
|
||||||
|
/// Get the commit message.
|
||||||
|
pub fn message(&self) -> &str {
|
||||||
|
self.message.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over the headers, in order.
|
||||||
|
pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
|
||||||
|
self.headers.iter().map(|(k, v)| (k.as_str(), v.as_str()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over matching header values.
|
||||||
|
pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + '_ {
|
||||||
|
self.headers
|
||||||
|
.iter()
|
||||||
|
.filter(move |(k, _)| k == name)
|
||||||
|
.map(|(_, v)| v.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a header to the end of the headers section.
|
||||||
|
pub fn push_header(&mut self, name: &str, value: &str) {
|
||||||
|
self.headers
|
||||||
|
.push((name.to_owned(), value.trim().to_owned()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum ParseError {
|
||||||
|
#[error("invalid git commit object format")]
|
||||||
|
InvalidFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for CommitObject {
|
||||||
|
type Err = ParseError;
|
||||||
|
|
||||||
|
fn from_str(buffer: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut headers = Vec::new();
|
||||||
|
let (header, message) = buffer.split_once("\n\n").ok_or(ParseError::InvalidFormat)?;
|
||||||
|
|
||||||
|
for line in header.lines() {
|
||||||
|
if let Some(rest) = line.strip_prefix(' ') {
|
||||||
|
let value: &mut String = headers
|
||||||
|
.last_mut()
|
||||||
|
.map(|(_, v)| v)
|
||||||
|
.ok_or(ParseError::InvalidFormat)?;
|
||||||
|
value.push('\n');
|
||||||
|
value.push_str(rest);
|
||||||
|
} else if let Some((name, value)) = line.split_once(' ') {
|
||||||
|
headers.push((name.to_owned(), value.to_owned()));
|
||||||
|
} else {
|
||||||
|
return Err(ParseError::InvalidFormat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
headers,
|
||||||
|
message: message.to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for CommitObject {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
let mut buf = String::new();
|
||||||
|
|
||||||
|
for (name, value) in &self.headers {
|
||||||
|
buf.push_str(name);
|
||||||
|
buf.push(' ');
|
||||||
|
buf.push_str(value.replace('\n', "\n ").as_str());
|
||||||
|
buf.push('\n');
|
||||||
|
}
|
||||||
|
buf.push('\n');
|
||||||
|
buf.push_str(self.message.as_str());
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const UNSIGNED: &str = "\
|
||||||
|
tree c66cc435f83ed0fba90ed4500e9b4b96e9bd001b
|
||||||
|
parent af06ad645133f580a87895353508053c5de60716
|
||||||
|
author Alexis Sellier <alexis@radicle.xyz> 1664467633 +0200
|
||||||
|
committer Alexis Sellier <alexis@radicle.xyz> 1664786099 +0200
|
||||||
|
|
||||||
|
Add SSH functionality with new `radicle-ssh`
|
||||||
|
|
||||||
|
We borrow code from `thrussh`, refactored to be runtime-less.
|
||||||
|
";
|
||||||
|
|
||||||
|
const SIGNATURE: &str = "\
|
||||||
|
-----BEGIN SSH SIGNATURE-----
|
||||||
|
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgvjrQogRxxLjzzWns8+mKJAGzEX
|
||||||
|
4fm2ALoN7pyvD2ttQAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
|
||||||
|
AAAAQIQvhIewOgGfnXLgR5Qe1ZEr2vjekYXTdOfNWICi6ZiosgfZnIqV0enCPC4arVqQg+
|
||||||
|
GPp0HqxaB911OnSAr6bwU=
|
||||||
|
-----END SSH SIGNATURE-----
|
||||||
|
";
|
||||||
|
|
||||||
|
const SIGNED: &str = "\
|
||||||
|
tree c66cc435f83ed0fba90ed4500e9b4b96e9bd001b
|
||||||
|
parent af06ad645133f580a87895353508053c5de60716
|
||||||
|
author Alexis Sellier <alexis@radicle.xyz> 1664467633 +0200
|
||||||
|
committer Alexis Sellier <alexis@radicle.xyz> 1664786099 +0200
|
||||||
|
other e6fe3c97619deb8ab4198620f9a7eb79d98363dd
|
||||||
|
gpgsig -----BEGIN SSH SIGNATURE-----
|
||||||
|
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgvjrQogRxxLjzzWns8+mKJAGzEX
|
||||||
|
4fm2ALoN7pyvD2ttQAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
|
||||||
|
AAAAQIQvhIewOgGfnXLgR5Qe1ZEr2vjekYXTdOfNWICi6ZiosgfZnIqV0enCPC4arVqQg+
|
||||||
|
GPp0HqxaB911OnSAr6bwU=
|
||||||
|
-----END SSH SIGNATURE-----
|
||||||
|
gpgsig -----BEGIN SSH SIGNATURE-----
|
||||||
|
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgvjrQogRxxLjzzWns8+mKJAGzEX
|
||||||
|
4fm2ALoN7pyvD2ttQAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
|
||||||
|
AAAAQIQvhIewOgGfnXLgR5Qe1ZEr2vjekYXTdOfNWICi6ZiosgfZnIqV0enCPC4arVqQg+
|
||||||
|
GPp0HqxaB911OnSAr6bwU=
|
||||||
|
-----END SSH SIGNATURE-----
|
||||||
|
|
||||||
|
Add SSH functionality with new `radicle-ssh`
|
||||||
|
|
||||||
|
We borrow code from `thrussh`, refactored to be runtime-less.
|
||||||
|
";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_push_header() {
|
||||||
|
let mut commit = CommitObject::from_str(UNSIGNED).unwrap();
|
||||||
|
commit.push_header("other", "e6fe3c97619deb8ab4198620f9a7eb79d98363dd");
|
||||||
|
commit.push_header("gpgsig", SIGNATURE);
|
||||||
|
commit.push_header("gpgsig", SIGNATURE);
|
||||||
|
|
||||||
|
assert_eq!(commit.to_string(), SIGNED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_header() {
|
||||||
|
let commit = CommitObject::from_str(SIGNED).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
commit.values("gpgsig").collect::<Vec<_>>(),
|
||||||
|
vec![SIGNATURE.trim(), SIGNATURE.trim()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
commit.values("parent").collect::<Vec<_>>(),
|
||||||
|
vec![String::from("af06ad645133f580a87895353508053c5de60716")],
|
||||||
|
);
|
||||||
|
assert!(commit.values("unknown").next().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_conversion() {
|
||||||
|
assert_eq!(CommitObject::from_str(SIGNED).unwrap().to_string(), SIGNED);
|
||||||
|
assert_eq!(
|
||||||
|
CommitObject::from_str(UNSIGNED).unwrap().to_string(),
|
||||||
|
UNSIGNED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue