From 49ed7400142a39975239fe25ddd6336942d431b2 Mon Sep 17 00:00:00 2001 From: Patrick Marie Date: Wed, 5 Feb 2025 20:17:21 +0100 Subject: [PATCH] chore: refator in repository module --- src/main.rs | 37 ++++-------- src/object.rs | 149 ++++++++++++++++++++++++---------------------- src/repository.rs | 57 ++++++++++++++++++ src/tree.rs | 89 ++++++++++++++------------- 4 files changed, 191 insertions(+), 141 deletions(-) create mode 100644 src/repository.rs diff --git a/src/main.rs b/src/main.rs index fedca65..098c747 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use anyhow::{Error, Result}; -use std::env; -use std::{fs, path::PathBuf}; +use repository::default_init_path; +use std::path::PathBuf; use clap::Parser; use clap::Subcommand; @@ -8,10 +8,10 @@ use clap::Subcommand; mod error; mod kind; mod object; +mod repository; mod tree; -use crate::object::{read_object, write_blob}; -use crate::tree::write_tree; +use crate::repository::Repository; #[derive(Parser)] #[command(name = "mg", about = "A simple git clone")] @@ -45,42 +45,25 @@ enum Command { }, } -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)?; - fs::create_dir(git_dir.join("objects"))?; - fs::create_dir(git_dir.join("refs"))?; - - fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n")?; - - Ok(path) -} - fn main() -> Result<(), Error> { let cli = Cli::parse(); - let repo_path = default_init_path(); + + let mut repo = Repository::new()?; match cli.command { - Command::Init { path } => match init_repository(path) { + Command::Init { path } => match repo.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(&repo_path, &hash) { + Command::CatFile { hash } => match repo.read_object(&hash) { Ok(mut obj) => print!("{}", obj.string()?), Err(e) => eprintln!("Failed to read object: {}", e), }, - Command::WriteBlob { file } => match write_blob(&repo_path, &file) { + Command::WriteBlob { file } => match repo.write_blob(&file) { Ok(hash) => println!("{}", hex::encode(hash)), Err(e) => eprintln!("Failed to write object: {}", e), }, - Command::WriteTree { path } => match write_tree(&repo_path, &path) { + Command::WriteTree { path } => match repo.write_tree(&path) { Ok(hash) => println!("{}", hex::encode(hash)), Err(e) => eprintln!("Failed to write tree: {}", e), }, diff --git a/src/object.rs b/src/object.rs index d43bf0d..b6e5d0f 100644 --- a/src/object.rs +++ b/src/object.rs @@ -1,3 +1,4 @@ +use crate::repository::Repository; use crate::{error::RuntimeError, kind::Kind}; use anyhow::{anyhow, Context, Result}; use flate2::{write::ZlibEncoder, Compression}; @@ -24,47 +25,90 @@ pub struct TreeObject { pub hash: [u8; 20], } -pub fn read_object(path: &Path, object: &str) -> Result> { - let object_path = path - .join(".git") - .join("objects") - .join(&object[..2]) - .join(&object[2..]); +impl Repository { + pub fn read_object(&self, object: &str) -> Result> { + let object_path = self + .path + .join(".git") + .join("objects") + .join(&object[..2]) + .join(&object[2..]); - let fd = 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 fd = 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")?; + 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()), - }; + 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 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 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(true), - "commit" => Kind::Commit, - "tree" => Kind::Tree, - _ => anyhow::bail!("invalid object type found"), - }; + let object_type = match object_type { + "blob" => Kind::Blob(true), + "commit" => Kind::Commit, + "tree" => Kind::Tree, + _ => anyhow::bail!("invalid object type found"), + }; - let object_size = object_size.parse::()?; + let object_size = object_size.parse::()?; - Ok(Object { - kind: object_type, - _size: object_size, - data: buf_reader, - }) + Ok(Object { + kind: object_type, + _size: object_size, + data: buf_reader, + }) + } + + pub fn write_blob(&self, file: &Path) -> Result<[u8; 20]> { + if !file.exists() || !is_path_in_repo(&self.path, file)? { + return Err(anyhow!("path does not exist")); + } + + let content = std::fs::read(file)?; + + Ok(self.write_object(Kind::Blob(false), &content)?) + } + + pub fn write_object(&self, kind: Kind, content: &[u8]) -> Result<[u8; 20]> { + let mut hasher = Sha1::new(); + hasher.update(format!("{} {}\0", kind.string(), content.len()).as_bytes()); + hasher.update(content); + let hash = hasher.finalize().into(); + let hash_str = hex::encode(hash); + + let target_dir = self.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_str[2..]); + if target_file.exists() { + return Ok(hash); + } + + 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, "{} {}\0", kind.string(), content.len()) + .context("could not write header")?; + zlib_out.write(content)?; + zlib_out + .finish() + .context("could not compress or write file")?; + + Ok(hash) + } } fn is_path_in_repo(repo_path: &Path, file_path: &Path) -> Result { @@ -79,45 +123,6 @@ fn is_path_in_repo(repo_path: &Path, file_path: &Path) -> Result { Ok(file_canonical.starts_with(repo_canonical)) } -pub fn write_blob(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 content = std::fs::read(file)?; - - Ok(write_object(repo_path, Kind::Blob(false), &content)?) -} - -pub fn write_object(repo_path: &Path, kind: Kind, content: &[u8]) -> Result<[u8; 20]> { - let mut hasher = Sha1::new(); - hasher.update(format!("{} {}\0", kind.string(), content.len()).as_bytes()); - hasher.update(content); - let hash = hasher.finalize().into(); - let hash_str = hex::encode(hash); - - 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_str[2..]); - if target_file.exists() { - return Ok(hash); - } - - 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, "{} {}\0", kind.string(), content.len()).context("could not write header")?; - zlib_out.write(content)?; - zlib_out - .finish() - .context("could not compress or write file")?; - - Ok(hash) -} - impl Object { pub fn string(&mut self) -> Result { let mut buf: Vec = Vec::new(); diff --git a/src/repository.rs b/src/repository.rs new file mode 100644 index 0000000..6d7e226 --- /dev/null +++ b/src/repository.rs @@ -0,0 +1,57 @@ +use anyhow::Result; +use std::{ + env, + fs::{create_dir, read_to_string}, + path::PathBuf, +}; + +pub struct Repository { + pub path: PathBuf, + pub ignore: Vec, +} + +pub fn default_init_path() -> PathBuf { + env::var("REPO_PATH") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(".")) +} + +impl Repository { + pub fn new() -> Result { + let path = default_init_path(); + + let mut repo = Repository { + path, + ignore: Vec::new(), + }; + + repo.load_ignore()?; + + Ok(repo) + } + + fn load_ignore(&mut self) -> Result { + let ignore_path = self.path.join(".gitignore"); + if !ignore_path.exists() { + return Ok(false); + } + + let ignore_content = read_to_string(ignore_path)?; + self.ignore = ignore_content.lines().map(String::from).collect(); + + Ok(true) + } + + pub fn init_repository(&mut self, path: &PathBuf) -> Result { + self.path = path.clone(); + let git_dir = self.path.join(".git"); + + create_dir(&git_dir)?; + create_dir(git_dir.join("objects"))?; + create_dir(git_dir.join("refs"))?; + + std::fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n")?; + + Ok(self.path.clone()) + } +} diff --git a/src/tree.rs b/src/tree.rs index 7116e24..d323ec6 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -3,55 +3,60 @@ use std::os::unix::fs::MetadataExt; use std::path::PathBuf; use crate::kind::Kind; -use crate::object::{write_blob, write_object, TreeObject}; +use crate::object::TreeObject; +use crate::repository::Repository; -pub fn write_tree(repo_path: &PathBuf, path: &PathBuf) -> Result<[u8; 20]> { - let mut entries = Vec::new(); +impl Repository { + pub fn write_tree(&self, 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 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(); - // Skip the .git directory - if file_name == ".git" { - continue; + // Skip the .git directory + if file_name == ".git" { + continue; + } + + let hash: [u8; 20]; + let kind; + + if file_type.is_dir() { + hash = self + .write_tree(&file_path) + .context("could not write_tree of subtree")?; + kind = Kind::Tree; + } else { + hash = self.write_blob(&file_path).context(format!( + "could not write object {:?}", + file_path.file_name() + ))?; + kind = Kind::Blob(file_path.metadata()?.mode() & 0o111 != 0); + } + + entries.push(TreeObject { + mode: kind.to_mode().to_string(), + kind, + name: file_name.into_string().unwrap(), + hash, + }) } - let hash: [u8; 20]; - let kind; + entries.sort_by(|a, b| a.name.cmp(&b.name)); - if file_type.is_dir() { - hash = write_tree(repo_path, &file_path).context("could not write_tree of subtree")?; - kind = Kind::Tree; - } else { - hash = write_blob(repo_path, &file_path).context(format!( - "could not write object {:?}", - file_path.file_name() - ))?; - kind = Kind::Blob(file_path.metadata()?.mode() & 0o111 != 0); + 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); } - entries.push(TreeObject { - mode: kind.to_mode().to_string(), - kind, - name: file_name.into_string().unwrap(), - hash, - }) + self.write_object(Kind::Tree, &out).context("Write") } - - 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); - } - - write_object(repo_path, Kind::Tree, &out).context("Write") }