From 8db6c791fc3ba98ccbeec6c78c184545bae74e5d Mon Sep 17 00:00:00 2001 From: Patrick Marie Date: Tue, 4 Feb 2025 23:05:08 +0100 Subject: [PATCH] feat: prepare tree objects --- src/main.rs | 22 +++++++++++++++------ src/object.rs | 22 ++++++++++----------- src/tree.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 17 deletions(-) create mode 100644 src/tree.rs diff --git a/src/main.rs b/src/main.rs index 714aa30..ab76d9f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,9 +8,10 @@ use clap::Subcommand; mod error; mod kind; mod object; +mod tree; -use crate::object::read_object; -use crate::object::write_object; +use crate::object::{read_object, write_object}; +use crate::tree::write_tree; #[derive(Parser)] #[command(name = "mg", about = "A simple git clone")] @@ -37,6 +38,11 @@ enum Command { /// The file to write file: PathBuf, }, + /// Write a tree object + WriteTree { + /// The path to write + path: PathBuf, + }, } fn default_init_path() -> PathBuf { @@ -59,21 +65,25 @@ fn init_repository(path: PathBuf) -> Result { fn main() -> Result<(), Error> { let cli = Cli::parse(); - let path = default_init_path(); + let repo_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) { + Command::CatFile { hash } => match read_object(&repo_path, &hash) { 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), + Command::WriteBlob { file } => match write_object(&repo_path, &file) { + Ok(hash) => println!("{}", hex::encode(hash)), Err(e) => eprintln!("Failed to write object: {}", e), }, + Command::WriteTree { path } => match write_tree(&repo_path, &path) { + Ok(hash) => println!("{}", hex::encode(hash)), + Err(e) => eprintln!("Failed to write tree: {}", e), + }, } Ok(()) diff --git a/src/object.rs b/src/object.rs index ee91bb5..24bd05c 100644 --- a/src/object.rs +++ b/src/object.rs @@ -18,11 +18,11 @@ pub struct Object { } #[derive(Debug)] -struct TreeObject { - mode: String, - kind: Kind, - name: String, - hash: [u8; 20], +pub struct TreeObject { + pub mode: String, + pub kind: Kind, + pub name: String, + pub hash: [u8; 20], } pub fn read_object(path: &Path, object: &str) -> Result> { @@ -80,7 +80,7 @@ fn is_path_in_repo(repo_path: &Path, file_path: &Path) -> Result { Ok(file_canonical.starts_with(repo_canonical)) } -pub fn hash_file(file: &Path) -> Result { +pub fn hash_file(file: &Path) -> Result<[u8; 20]> { let fd = File::open(file).context("opening file for sha1")?; let mut buf_reader = std::io::BufReader::new(fd); @@ -91,27 +91,27 @@ pub fn hash_file(file: &Path) -> Result { 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)) + Ok(hasher.finalize().into()) } -pub fn write_object(repo_path: &Path, file: &Path) -> Result { +pub fn write_object(repo_path: &Path, file: &Path) -> Result<[u8; 20]> { if !file.exists() || !is_path_in_repo(repo_path, file)? { return Err(anyhow!("path does not exist")); } let hash = hash_file(file)?; + let hash_str = hex::encode(hash); 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]); + let target_dir = repo_path.join(".git").join("objects").join(&hash_str[..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 target_file = target_dir.join(&hash_str[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()); diff --git a/src/tree.rs b/src/tree.rs new file mode 100644 index 0000000..6d870a7 --- /dev/null +++ b/src/tree.rs @@ -0,0 +1,55 @@ +use anyhow::{Context, Result}; +use sha1::{Digest, Sha1}; +use std::path::PathBuf; + +use crate::kind::Kind; +use crate::object::{hash_file, TreeObject}; + +pub fn write_tree(_repo_path: &PathBuf, path: &PathBuf) -> Result<[u8; 20]> { + let mut entries = Vec::new(); + + let files = std::fs::read_dir(path)?; + for file in files { + let file = file?; + let file_type = file.file_type()?; + let file_name = file.file_name(); + let file_path = file.path(); + + let hash: [u8; 20]; + let kind; + + if file_type.is_dir() { + hash = write_tree(_repo_path, &file_path).context("could not write_tree of subtree")?; + kind = Kind::Tree; + } else { + hash = hash_file(&file_path)?; + kind = Kind::Blob; + } + + entries.push(TreeObject { + mode: "100644".to_string(), + kind, + name: file_name.into_string().unwrap(), + hash, + }) + } + + entries.sort_by(|a, b| a.name.cmp(&b.name)); + + let mut out: Vec = Vec::new(); + for entry in &entries { + out.extend_from_slice(entry.mode.as_bytes()); + out.push(b' '); + out.extend_from_slice(entry.name.as_bytes()); + out.push(0); + out.extend_from_slice(&entry.hash); + } + + let header = format!("tree {}\0", out.len()); + + let mut hasher = Sha1::new(); + hasher.update(header); + hasher.update(out); + + Ok(hasher.finalize().into()) +}