From 4688694213a81a8e158d1c521a9556611262309b Mon Sep 17 00:00:00 2001 From: NobodyForNothing <82763757+NobodyForNothing@users.noreply.github.com> Date: Thu, 4 Jul 2024 09:00:13 +0200 Subject: [PATCH] feat: git commit --- rust/vcs/src/git/objects.rs | 105 +++++++++++++++++++++++++++++------- rust/vcs/src/git/repo.rs | 8 ++- 2 files changed, 90 insertions(+), 23 deletions(-) diff --git a/rust/vcs/src/git/objects.rs b/rust/vcs/src/git/objects.rs index 0d38897..ac7f5bf 100644 --- a/rust/vcs/src/git/objects.rs +++ b/rust/vcs/src/git/objects.rs @@ -1,6 +1,4 @@ -use std::collections::HashMap; -use std::fmt::format; -use std::io::Bytes; +use std::io::{Bytes, Read}; pub(crate) trait BinSerializable { /// Read git object contents without header or compression. @@ -9,7 +7,7 @@ pub(crate) trait BinSerializable { } pub enum GitObject { - Commit, + Commit(GitCommit), Tree, Tag, Blob(GitBlob), @@ -26,7 +24,17 @@ pub enum GitObjectType { impl GitObject { /// Serialize a git object including the header and compression. - pub fn serialize(&self) -> Vec { + pub fn serialize(self) -> Vec { + match self { + GitObject::Commit(commit) => { + commit.serialize(); + } + GitObject::Tree => {} + GitObject::Tag => {} + GitObject::Blob(blob) => { + blob.serialize(); + } + } todo!() } } @@ -50,17 +58,66 @@ impl BinSerializable for GitBlob { // https://wyag.thb.lt/#orgfe2859f pub struct GitCommit { + kvlm: Vec<(String, String)>, +} + +impl GitCommit { /// Reference to a tree object. - tree: String, + pub fn get_tree(&self) -> Option { + self.kvlm + .iter().filter(|(k,_)| k == "tree") + .map(|(_k,v)| v.trim_matches(|e| e == '\n').to_string()) + .next() + } /// References to commits this commit is based on. /// /// - merge commits may have multiple /// - the first commit may have none - parent: Vec, - author: String, - commiter: String, + pub fn get_parents(&self) -> Vec { + self.kvlm + .iter().filter(|(k,_)| k == "parent") + .map(|(_k, v)| v.trim_matches(|e| e == '\n').to_string()) + .collect::>() + } + /// Like: `Scott Chacon 1243040974 -0700` + pub fn get_author(&self) -> Option { + self.kvlm + .iter().filter(|(k,_)| k == "author") + .map(|(_k,v)| v.trim_matches(|e| e == '\n').to_string()) + .next() + } + pub fn get_commiter(&self) -> Option { + self.kvlm + .iter().filter(|(k,_)| k == "committer") + .map(|(_k,v)| v.trim_matches(|e| e == '\n').to_string()) + .next() + } /// PGP signature of the object. - gpgsig: String, + pub fn get_gpgsig(&self) -> Option { + self.kvlm + .iter().filter(|(k,_)| k == "gpgsig") + .map(|(_k,v)| v.trim_matches(|e| e == '\n').to_string()) + .next() + } + pub fn get_message(&self) -> Option { + self.kvlm + .iter().filter(|(k,_)| k == "__message__") + .map(|(_k,v)| v.trim_matches(|e| e == '\n').to_string()) + .next() + } + +} + +impl BinSerializable for GitCommit { + fn deserialize(data: Vec) -> Self { + GitCommit { + kvlm: kvlm_parse(data.bytes()), + } + } + + fn serialize(self) -> Vec { + kvlm_serialize(self.kvlm) + } } /// Recursively parse a Key-Value List with Message. @@ -88,7 +145,6 @@ fn kvlm_parse(mut raw: Bytes<&[u8]>) -> Vec<(String, String)> { let mut key: Option = None; let mut value = String::new(); for line in lines { - print!("{}, {}", in_message_block, &line); if in_message_block { value.push_str(line.as_str()); } else if line.starts_with(" ") || line.is_empty() { @@ -99,7 +155,6 @@ fn kvlm_parse(mut raw: Bytes<&[u8]>) -> Vec<(String, String)> { if let Some(last_key) = key { let last_value = value.replace("\n ", "\n"); kv_entries.push((last_key, last_value)); - value = String::new(); key = None; } in_message_block = true; @@ -133,10 +188,10 @@ fn kvlm_parse(mut raw: Bytes<&[u8]>) -> Vec<(String, String)> { fn kvlm_serialize(kvlm: Vec<(String, String)>) -> Vec { // TODO: test let mut out = String::new(); for (k, v) in kvlm { - let v = v.replace("\n", "\n "); if k == "__message__" { out.push_str(format!("\n{v}").as_str()) } else { + let v = v.replace("\n", "\n "); out.push_str(format!("{k} {v} \n").as_str()); } } @@ -146,11 +201,10 @@ fn kvlm_serialize(kvlm: Vec<(String, String)>) -> Vec { // TODO: test #[cfg(test)] mod tests { use std::io::Read; - use crate::git::objects::kvlm_parse; - #[test] - fn kvlm_parses() { - let parsed = kvlm_parse("tree 29ff16c9c14e2652b22f8b78bb08a5a07930c147 + use crate::git::objects::{BinSerializable, GitCommit, kvlm_parse}; + + const SAMPLE_COMMIT: &str = "tree 29ff16c9c14e2652b22f8b78bb08a5a07930c147 parent 206941306e8a8af65b66eaaaea388a7ae24d49a0 author Thibault Polge 1527025023 +0200 committer Thibault Polge 1527025044 +0200 @@ -169,7 +223,11 @@ gpgsig -----BEGIN PGP SIGNATURE-----\n \n iQIzBAABCAAdFiEExwXquOM8bWb4Q2zVGxM2Fx =lgTX -----END PGP SIGNATURE----- -Create first draft".as_bytes().bytes()); +Create first draft"; + + #[test] + fn kvlm_parses() { + let parsed = kvlm_parse(SAMPLE_COMMIT.as_bytes().bytes()); // TODO: verify \n is wanted assert_eq!(parsed.get(0).unwrap().0, "tree"); assert_eq!(parsed.get(0).unwrap().1, "29ff16c9c14e2652b22f8b78bb08a5a07930c147\n"); @@ -187,4 +245,15 @@ Create first draft".as_bytes().bytes()); assert_eq!(parsed.get(5).unwrap().0, "__message__"); assert_eq!(parsed.get(5).unwrap().1, "Create first draft"); } + + #[test] + fn git_commit_deserialize() { + let commit = GitCommit::deserialize(SAMPLE_COMMIT.as_bytes().to_vec()); + assert_eq!(commit.get_tree(), Some(String::from("29ff16c9c14e2652b22f8b78bb08a5a07930c147"))); + assert_eq!(commit.get_parents().iter().next().unwrap().clone(), String::from("206941306e8a8af65b66eaaaea388a7ae24d49a0")); + assert_eq!(commit.get_author(), Some(String::from("Thibault Polge 1527025023 +0200"))); + assert_eq!(commit.get_commiter(), Some(String::from("Thibault Polge 1527025044 +0200"))); + assert!(commit.get_gpgsig().unwrap().contains("iQIzBAABCAAdFiEExwXquOM8bWb4Q2zVGxM2FxoLkGQFAlsEjZQACgkQGxM2FxoL")); + assert_eq!(commit.get_message(), Some(String::from("Create first draft"))); + } } diff --git a/rust/vcs/src/git/repo.rs b/rust/vcs/src/git/repo.rs index d6528c4..4479d2b 100644 --- a/rust/vcs/src/git/repo.rs +++ b/rust/vcs/src/git/repo.rs @@ -1,12 +1,10 @@ -use std::ffi::OsStr; use iniconf::{IniFile, IniFileOpenError}; use std::fs; use std::io::Read; use std::path::{Path, PathBuf}; use log::warn; use sha1::{Sha1, Digest}; -use crate::git; -use crate::git::objects::{GitBlob, GitObject, BinSerializable, GitObjectType}; +use crate::git::objects::{GitBlob, GitObject, BinSerializable, GitObjectType, GitCommit}; pub struct Repository { /// Where the files meant to be in version control live. @@ -142,7 +140,7 @@ impl Repository { let path = self.repo_path(vec!["objects", &sha[0..2], &sha[2..sha.len()]], None, Some(true)); if path.as_ref().is_some_and(|p| p.is_file()) { if let Ok(data) = fs::read(path.unwrap()) { - let mut data = flate2::read::ZlibDecoder::new(&data[..]); + let data = flate2::read::ZlibDecoder::new(&data[..]); let mut data = data.bytes(); let mut obj_type = String::new(); while let Some(Ok(byte)) = data.next() { @@ -165,7 +163,7 @@ impl Repository { assert_eq!(obj_len as usize, remaining_bits.len()); let obj = match obj_type.as_str() { - "commit" => GitObject::Commit, + "commit" => GitObject::Commit(GitCommit::deserialize(remaining_bits)), "tree" => GitObject::Tree, "tag" => GitObject::Tag, "blob" => GitObject::Blob(GitBlob::deserialize(remaining_bits)),