From 82f4381d20bcae88a2a49b81ee6f3b1be99bc7ea Mon Sep 17 00:00:00 2001 From: Patrick Marie Date: Tue, 4 Feb 2025 22:03:26 +0100 Subject: [PATCH] feat: create a blob object --- Cargo.lock | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/main.rs | 12 +++++++- src/object.rs | 67 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 154 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe158ed..9b9b6ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,15 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -116,6 +125,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -125,6 +143,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "flate2" version = "1.0.35" @@ -135,6 +173,16 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "heck" version = "0.5.0" @@ -153,6 +201,12 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + [[package]] name = "mg" version = "0.1.0" @@ -161,6 +215,7 @@ dependencies = [ "clap", "flate2", "hex", + "sha1", "thiserror", ] @@ -197,6 +252,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "strsim" version = "0.11.1" @@ -234,6 +300,12 @@ dependencies = [ "syn", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" version = "1.0.16" @@ -246,6 +318,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/Cargo.toml b/Cargo.toml index 7ab0529..d77c58d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,5 @@ anyhow = "1.0.95" clap = { version = "4.5.27", features = ["derive", "string"] } flate2 = "1.0.35" hex = "0.4.3" +sha1 = "0.10.6" thiserror = "2.0.11" diff --git a/src/main.rs b/src/main.rs index 18f7419..714aa30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ mod kind; mod object; use crate::object::read_object; +use crate::object::write_object; #[derive(Parser)] #[command(name = "mg", about = "A simple git clone")] @@ -31,6 +32,11 @@ enum Command { /// The object to display hash: String, }, + /// Write a blob object + WriteBlob { + /// The file to write + file: PathBuf, + }, } fn default_init_path() -> PathBuf { @@ -61,9 +67,13 @@ fn main() -> Result<(), Error> { Err(e) => eprintln!("Failed to initialize repository: {}", e), }, Command::CatFile { hash } => match read_object(&path, &hash) { - Ok(mut obj) => println!("{}", obj.string()?), + Ok(mut obj) => print!("{}", obj.string()?), Err(e) => eprintln!("Failed to read object: {}", e), }, + Command::WriteBlob { file } => match write_object(&path, &file) { + Ok(hash) => println!("{}", hash), + Err(e) => eprintln!("Failed to write object: {}", e), + }, } Ok(()) diff --git a/src/object.rs b/src/object.rs index ffaef3c..ee91bb5 100644 --- a/src/object.rs +++ b/src/object.rs @@ -1,7 +1,14 @@ -use anyhow::{Context, Result}; -use std::{fs::File, io::BufRead, path::Path}; - use crate::{error::RuntimeError, kind::Kind}; +use anyhow::{anyhow, Context, Result}; +use flate2::{write::ZlibEncoder, Compression}; +use sha1::{Digest, Sha1}; +use std::io::Write; +use std::{ + fs::{create_dir, File}, + io::{copy, BufRead}, + os::unix::fs::MetadataExt, + path::Path, +}; #[derive(Debug)] pub struct Object { @@ -61,6 +68,60 @@ pub fn read_object(path: &Path, object: &str) -> Result> { }) } +fn is_path_in_repo(repo_path: &Path, file_path: &Path) -> Result { + // Convert both paths to absolute paths + let repo_canonical = repo_path.canonicalize()?; + let file_canonical = match file_path.canonicalize() { + Ok(path) => path, + Err(_) => return Ok(false), + }; + + // Check if file_path starts with repo_path + Ok(file_canonical.starts_with(repo_canonical)) +} + +pub fn hash_file(file: &Path) -> Result { + let fd = File::open(file).context("opening file for sha1")?; + let mut buf_reader = std::io::BufReader::new(fd); + + let stat = file.metadata().context("stat on file")?; + + let header = format!("blob {}\0", stat.size()); + + let mut hasher = Sha1::new(); + hasher.update(header.as_bytes()); + copy(&mut buf_reader, &mut hasher)?; + let hash_bytes = hasher.finalize(); + + Ok(hex::encode(hash_bytes)) +} + +pub fn write_object(repo_path: &Path, file: &Path) -> Result { + if !file.exists() || !is_path_in_repo(repo_path, file)? { + return Err(anyhow!("path does not exist")); + } + + let hash = hash_file(file)?; + let bytes_num = file.metadata()?.len(); + let mut buf_reader = std::io::BufReader::new(File::open(file)?); + + let target_dir = repo_path.join(".git").join("objects").join(&hash[..2]); + + if !target_dir.exists() { + create_dir(&target_dir).context("could not create directory in .git/objects")?; + } + + let target_file = target_dir.join(&hash[2..]); + let file_out_fd = File::create(target_file).context("could not open target file")?; + + let mut zlib_out = ZlibEncoder::new(file_out_fd, Compression::default()); + write!(zlib_out, "blob {}\0", bytes_num).context("could not write header")?; + + std::io::copy(&mut buf_reader, &mut zlib_out).context("could not compress or write file")?; + + Ok(hash) +} + impl Object { pub fn string(&mut self) -> Result { let mut buf: Vec = Vec::new();