diff --git a/Cargo.lock b/Cargo.lock index 64fc2c3..fe158ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "anstream" version = "0.6.18" @@ -58,6 +64,12 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.5.27" @@ -104,12 +116,37 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -122,6 +159,18 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "flate2", + "hex", + "thiserror", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +dependencies = [ + "adler2", ] [[package]] @@ -165,6 +214,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.16" diff --git a/Cargo.toml b/Cargo.toml index e35e97f..7ab0529 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,4 +5,7 @@ edition = "2021" [dependencies] anyhow = "1.0.95" -clap = { version = "4.5.27", features = ["derive"] } +clap = { version = "4.5.27", features = ["derive", "string"] } +flate2 = "1.0.35" +hex = "0.4.3" +thiserror = "2.0.11" diff --git a/src/main.rs b/src/main.rs index 9866415..cf04257 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,6 @@ -use anyhow::{Error, Result}; +use anyhow::{Context, Error, Result}; +use std::env; +use std::io::prelude::*; use std::{fs, path::PathBuf}; use clap::Parser; @@ -16,14 +18,72 @@ enum Command { /// Initialize a new Git repository Init { /// The path where to create the repository. Defaults to current directory - #[arg(default_value = ".")] + #[arg(default_value=default_init_path().into_os_string())] path: PathBuf, }, + /// Display a Git object + CatFile { + /// The object to display + hash: String, + }, } -fn init_repository(path: PathBuf) -> Result { - println!("Creating in {:?}", path); +#[derive(thiserror::Error, Debug)] +pub enum RuntimeError { + #[error("Invalid character found")] + UnexpectedChar, +} +#[derive(Debug)] +enum Kind { + Blob, // 100644 or 100755 + Commit, // 120000 + Tree, // 040000 + Symlink, // 120000 +} + +impl Kind { + fn from_mode(mode: &str) -> Result { + match mode { + "100644" | "100755" => Ok(Kind::Blob), + "120000" => Ok(Kind::Commit), + "040000" | "40000" => Ok(Kind::Tree), + _ => Err(anyhow::anyhow!(format!("invalid mode: {}", mode))), + } + } + + fn string(&self) -> &str { + match self { + Kind::Blob => "blob", + Kind::Commit => "commit", + Kind::Tree => "tree", + Kind::Symlink => "symlink", + } + } +} + +#[derive(Debug)] +struct Object { + kind: Kind, + size: usize, + data: Reader, +} + +#[derive(Debug)] +struct TreeObject { + mode: String, + kind: Kind, + name: String, + hash: [u8; 20], +} + +fn default_init_path() -> PathBuf { + env::var("REPO_PATH") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(".")) +} + +fn init_repository(path: PathBuf) -> Result { let git_dir = path.join(".git"); fs::create_dir(&git_dir)?; @@ -35,14 +95,129 @@ fn init_repository(path: PathBuf) -> Result { Ok(path) } +fn read_object(path: &PathBuf, object: &str) -> Result> { + let object_path = path + .join(".git") + .join("objects") + .join(&object[..2]) + .join(&object[2..]); + + let fd = fs::File::open(&object_path).context("opening the object")?; + let zfd = flate2::read::ZlibDecoder::new(fd); + let mut buf_reader = std::io::BufReader::new(zfd); + + let mut buf: Vec = Vec::new(); + buf_reader + .read_until(0, &mut buf) + .context("read the object")?; + + match buf.pop() { + Some(0) => {} + Some(_) | None => return Err(RuntimeError::UnexpectedChar.into()), + }; + + let header = String::from_utf8(buf.clone()).context("converting header to utf-8")?; + + let Some((object_type, object_size)) = header.split_once(' ') else { + anyhow::bail!("could not parse object header correctly"); + }; + + let object_type = match object_type { + "blob" => Kind::Blob, + "commit" => Kind::Commit, + "tree" => Kind::Tree, + _ => anyhow::bail!("invalid object type found"), + }; + + let object_size = object_size.parse::()?; + + Ok(Object { + kind: object_type, + size: object_size, + data: buf_reader, + }) +} + +impl Object { + fn print(&mut self) -> Result<()> { + let mut buf: Vec = Vec::new(); + let mut buf_hash: [u8; 20] = [0; 20]; + + match self.kind { + Kind::Blob | Kind::Commit => { + self.data.read_to_end(&mut buf)?; + println!("{}", String::from_utf8(buf)?); + } + Kind::Tree => { + let mut max_name_len = 0; + let mut entries = Vec::new(); + loop { + let read_bytes_len = self.data.read_until(0, &mut buf)?; + if read_bytes_len <= 0 { + break; + } + + let mode_name = buf.clone(); + buf.clear(); + let mut splits = mode_name.splitn(2, |&b| b == b' '); + + let mode = splits + .next() + .ok_or_else(|| anyhow::anyhow!("could not parse mode"))?; + let mode = std::str::from_utf8(mode)?; + let name = splits + .next() + .ok_or_else(|| anyhow::anyhow!("could not parse name"))?; + let name = std::str::from_utf8(name)?; + + self.data.read_exact(&mut buf_hash)?; + + if name.len() > max_name_len { + max_name_len = name.len(); + } + + entries.push(TreeObject { + name: name.to_string(), + kind: Kind::from_mode(mode)?, + mode: mode.to_string(), + hash: buf_hash.clone(), + }); + } + + entries.sort_by(|a, b| a.name.cmp(&b.name)); + + for entry in entries { + let hash = hex::encode(entry.hash); + println!( + "{:0>6} {} {} {:name_len$}", + entry.mode, + entry.kind.string(), + hash, + entry.name, + name_len = max_name_len + ); + } + } + _ => unimplemented!(), + } + + Ok(()) + } +} + fn main() -> Result<(), Error> { let cli = Cli::parse(); + let path = default_init_path(); match cli.command { Command::Init { path } => match init_repository(path) { Ok(path) => println!("Initialized empty Git repository in {:?}", path), Err(e) => eprintln!("Failed to initialize repository: {}", e), }, + Command::CatFile { hash } => match read_object(&path, &hash) { + Ok(mut obj) => obj.print()?, + Err(e) => eprintln!("Failed to read object: {}", e), + }, } Ok(())