Parsing commits

Read the source doc, to get familiar with the commit object format.

Parse key value list of commit object

Add KeyValueList struct to commit.rs

src/git_object/commit.rs
#[derive(Debug)]
pub struct KeyValueList {
    map: BTreeMap<String, String>,
}

We need to have a map as a BTreeMap, because we need to have entries sorted by their keys.

Add parse function to KeyValueList

src/git_object/commit.rs
impl KeyValueList {
    pub fn new(map: BTreeMap<String, String>) -> Self {
        KeyValueList { map }
    }

    pub fn parse(buf_reader: impl BufRead) -> Result<Self, KvlParseError> {
        let mut map: BTreeMap<String, String> = BTreeMap::new();

        let mut the_rest_is_message = false;
        let mut message = String::new();

        let mut prev_key = String::new();

        for line in buf_reader.lines() {
            let line = line.map_err(|_| KvlParseError::FailedToGetNextLine)?;
            if line.is_empty() {
                the_rest_is_message = true;
            } else if the_rest_is_message {
                message.push_str(line.as_str());
            } else if let Some(char) = line.chars().next() {
                if char == ' ' {
                    // Continuation of previous value
                    map.get_mut(&prev_key)
                        .unwrap()
                        .push_str(format!("\n{}", &line[1..]).as_str());
                } else {
                    // New key value
                    let index = line.find(' ').ok_or(KvlParseError::KeyDelimiterNotFound)?;
                    let key = &line[0..index];
                    let value = &line[index + 1..];
                    map.insert(key.to_string(), value.to_string());
                    prev_key = key.to_string();
                }
            }
        }

        if the_rest_is_message {
            map.insert("message".to_string(), message);
        }

        Ok(Self { map })
    }
src/error/git_object.rs

#[derive(Debug, Error)]
pub enum KvlParseError {
    #[error("Failed to get the next line of the kvl")]
    FailedToGetNextLine,

    #[error("Failed to find the delimiter fo key/value")]
    KeyDelimiterNotFound,
}
pub enum ObjectParseError {
    // ...
    #[error(transparent)]
    KvlParseError(#[from] KvlParseError),

Add serialize function to KeyValueList

src/git_object/commit.rs
impl KeyValueList {
    //...
     pub fn serialize(&self) -> String {
        let mut buffer = String::new();
        let mut message_value = None;
        for (key, value) in &self.map {
            if key == "message" {
                message_value = Some(value);
                continue;
            }
            if value.contains('\n') {
                let multi_line_value = value
                    .lines()
                    .take(1)
                    .map(|v| format!("{}\n", v))
                    .chain(value.lines().skip(1).map(|line| format!(" {}\n", line)))
                    .collect::<String>();
                buffer.push_str(format!("{} {}", key, &multi_line_value).as_str());
            } else {
                buffer.push_str(format!("{} {}\n", key, value).as_str());
            }
        }

        if let Some(message_value) = message_value {
            buffer.push_str(&format!("\n{}", &message_value));
        }
        buffer
    }
}

Implement AsRef for KeyValueList

src/git_object/commit.rs
impl AsRef<BTreeMap<String, String>> for KeyValueList {
    fn as_ref(&self) -> &BTreeMap<String, String> {
        &self.map
    }
}

Add unit tests

src/git_object/commit.rs
#[cfg(test)]
mod tests {
    use std::{collections::BTreeMap, io::BufReader};

    use super::KeyValueList;

    #[test]
    pub fn kvl_parse_should_parse_single_line_key_values_correctly() {
        let raw = r#"tree 29ff16c9c14e2652b22f8b78bb08a5a07930c147
parent 206941306e8a8af65b66eaaaea388a7ae24d49a0
author Thibault Polge <thibault@thb.lt> 1527025023 +0200
committer Thibault Polge <thibault@thb.lt> 1527025044 +0200
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCAAdFiEExwXquOM8bWb4Q2zVGxM2FxoLkGQFAlsEjZQACgkQGxM2FxoL
 kGQdcBAAqPP+ln4nGDd2gETXjvOpOxLzIMEw4A9gU6CzWzm+oB8mEIKyaH0UFIPh
 rNUZ1j7/ZGFNeBDtT55LPdPIQw4KKlcf6kC8MPWP3qSu3xHqx12C5zyai2duFZUU
 wqOt9iCFCscFQYqKs3xsHI+ncQb+PGjVZA8+jPw7nrPIkeSXQV2aZb1E68wa2YIL
 3eYgTUKz34cB6tAq9YwHnZpyPx8UJCZGkshpJmgtZ3mCbtQaO17LoihnqPn4UOMr
 V75R/7FjSuPLS8NaZF4wfi52btXMSxO/u7GuoJkzJscP3p4qtwe6Rl9dc1XC8P7k
 NIbGZ5Yg5cEPcfmhgXFOhQZkD0yxcJqBUcoFpnp2vu5XJl2E5I/quIyVxUXi6O6c
 /obspcvace4wy8uO0bdVhc4nJ+Rla4InVSJaUaBeiHTW8kReSFYyMmDCzLjGIu1q
 doU61OM3Zv1ptsLu3gUE6GU27iWYj2RWN3e3HE4Sbd89IFwLXNdSuM0ifDLZk7AQ
 WBhRhipCCgZhkj9g2NEk7jRVslti1NdN5zoQLaJNqSwO1MtxTmJ15Ksk3QP6kfLB
 Q52UWybBzpaP9HEd4XnR+HuQ4k2K0ns2KgNImsNvIyFwbpMUyUWLMPimaV1DWUXo
 5SBjDB/V/W2JBFR+XKHFJeFwYhj7DD/ocsGr4ZMx/lgc8rjIBkI=
 =lgTX
 -----END PGP SIGNATURE-----

Create first draft"#;

        let parsed_key_values = BTreeMap::from([
            (
                "tree".to_string(),
                "29ff16c9c14e2652b22f8b78bb08a5a07930c147".to_string(),
            ),
            (
                "parent".to_string(),
                "206941306e8a8af65b66eaaaea388a7ae24d49a0".to_string(),
            ),
            (
                "author".to_string(),
                "Thibault Polge <thibault@thb.lt> 1527025023 +0200".to_string(),
            ),
            (
                "committer".to_string(),
                "Thibault Polge <thibault@thb.lt> 1527025044 +0200".to_string(),
            ),
            (
                "gpgsig".to_string(),
                "-----BEGIN PGP SIGNATURE-----\n\
                \n\
            iQIzBAABCAAdFiEExwXquOM8bWb4Q2zVGxM2FxoLkGQFAlsEjZQACgkQGxM2FxoL\n\
            kGQdcBAAqPP+ln4nGDd2gETXjvOpOxLzIMEw4A9gU6CzWzm+oB8mEIKyaH0UFIPh\n\
            rNUZ1j7/ZGFNeBDtT55LPdPIQw4KKlcf6kC8MPWP3qSu3xHqx12C5zyai2duFZUU\n\
            wqOt9iCFCscFQYqKs3xsHI+ncQb+PGjVZA8+jPw7nrPIkeSXQV2aZb1E68wa2YIL\n\
            3eYgTUKz34cB6tAq9YwHnZpyPx8UJCZGkshpJmgtZ3mCbtQaO17LoihnqPn4UOMr\n\
            V75R/7FjSuPLS8NaZF4wfi52btXMSxO/u7GuoJkzJscP3p4qtwe6Rl9dc1XC8P7k\n\
            NIbGZ5Yg5cEPcfmhgXFOhQZkD0yxcJqBUcoFpnp2vu5XJl2E5I/quIyVxUXi6O6c\n\
            /obspcvace4wy8uO0bdVhc4nJ+Rla4InVSJaUaBeiHTW8kReSFYyMmDCzLjGIu1q\n\
            doU61OM3Zv1ptsLu3gUE6GU27iWYj2RWN3e3HE4Sbd89IFwLXNdSuM0ifDLZk7AQ\n\
            WBhRhipCCgZhkj9g2NEk7jRVslti1NdN5zoQLaJNqSwO1MtxTmJ15Ksk3QP6kfLB\n\
            Q52UWybBzpaP9HEd4XnR+HuQ4k2K0ns2KgNImsNvIyFwbpMUyUWLMPimaV1DWUXo\n\
            5SBjDB/V/W2JBFR+XKHFJeFwYhj7DD/ocsGr4ZMx/lgc8rjIBkI=\n\
            =lgTX\n\
            -----END PGP SIGNATURE-----"
                    .to_string(),
            ),
            ("message".to_string(), "Create first draft".to_string()),
        ]);

        let kvl = KeyValueList::parse(BufReader::new(raw.as_bytes())).unwrap();

        assert_eq!(parsed_key_values, *kvl.as_ref());
    }

    #[test]
    fn serialize_should_work_if_it_doesnt_contain_message() {
        let map = BTreeMap::from([
            ("key1".to_string(), "value1".to_string()),
            ("key2".to_string(), "value2".to_string()),
            ("key3".to_string(), "value3\nvalue3-1\nvalue3-2".to_string()),
        ]);

        let kvl = KeyValueList::new(map);
        let serialized = kvl.serialize();

        let expected = r#"key1 value1
key2 value2
key3 value3
 value3-1
 value3-2
"#;

        assert_eq!(expected, serialized);
    }

    #[test]
    fn serialize_should_work_if_it_contains_message() {
        let map = BTreeMap::from([
            ("key1".to_string(), "value1".to_string()),
            ("key2".to_string(), "value2".to_string()),
            ("key3".to_string(), "value3\nvalue3-1\nvalue3-2".to_string()),
            ("message".to_string(), "khar gav".to_string()),
        ]);

        let kvl = KeyValueList::new(map);
        let serialized = kvl.serialize();

        let expected = r#"key1 value1
key2 value2
key3 value3
 value3-1
 value3-2

khar gav"#;

        assert_eq!(expected, serialized);
    }
}

Use KeyValueList in Commit object

src/git_object/commit.rs
#[derive(Debug)]
pub struct Commit {
    pub kvl: KeyValueList,
}

impl Commit {
    pub fn serialize(&self) -> String {
        self.kvl.serialize()
    }

    pub fn deserialize(
        buf_reader: &mut impl std::io::BufRead,
        _object_header: super::Header,
    ) -> Result<Self, crate::error::ObjectParseError> {
        Ok(Self {
            kvl: KeyValueList::parse(buf_reader)?,
        })
    }

    pub fn get_value(&self, key: &str) -> Option<&String> {
        self.kvl.map.get(key)
    }
}

Last updated