Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
Implement commit parsing/formatting
Alexis Sellier committed 3 years ago
commit fece740fcbf4871bad2c233e8d95a6e4a3b007cb
parent 5b673c7440efc032807378e7fb555623b867f5fe
1 file changed +177 -0
modified radicle/src/git.rs
@@ -274,3 +274,180 @@ pub fn run<P: AsRef<Path>, S: AsRef<std::ffi::OsStr>>(
        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
+
            );
+
        }
+
    }
+
}