Compare commits

..

No commits in common. "49ed7400142a39975239fe25ddd6336942d431b2" and "4b0902c6482cbb2f9c8b2d03f90bf47da19d1e4e" have entirely different histories.

4 changed files with 138 additions and 193 deletions

View File

@ -1,6 +1,6 @@
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use repository::default_init_path; use std::env;
use std::path::PathBuf; use std::{fs, path::PathBuf};
use clap::Parser; use clap::Parser;
use clap::Subcommand; use clap::Subcommand;
@ -8,10 +8,10 @@ use clap::Subcommand;
mod error; mod error;
mod kind; mod kind;
mod object; mod object;
mod repository;
mod tree; mod tree;
use crate::repository::Repository; use crate::object::{read_object, write_blob};
use crate::tree::write_tree;
#[derive(Parser)] #[derive(Parser)]
#[command(name = "mg", about = "A simple git clone")] #[command(name = "mg", about = "A simple git clone")]
@ -45,25 +45,42 @@ 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<PathBuf> {
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> { fn main() -> Result<(), Error> {
let cli = Cli::parse(); let cli = Cli::parse();
let repo_path = default_init_path();
let mut repo = Repository::new()?;
match cli.command { match cli.command {
Command::Init { path } => match repo.init_repository(&path) { Command::Init { path } => match init_repository(path) {
Ok(path) => println!("Initialized empty Git repository in {:?}", path), Ok(path) => println!("Initialized empty Git repository in {:?}", path),
Err(e) => eprintln!("Failed to initialize repository: {}", e), Err(e) => eprintln!("Failed to initialize repository: {}", e),
}, },
Command::CatFile { hash } => match repo.read_object(&hash) { Command::CatFile { hash } => match read_object(&repo_path, &hash) {
Ok(mut obj) => print!("{}", obj.string()?), Ok(mut obj) => print!("{}", obj.string()?),
Err(e) => eprintln!("Failed to read object: {}", e), Err(e) => eprintln!("Failed to read object: {}", e),
}, },
Command::WriteBlob { file } => match repo.write_blob(&file) { Command::WriteBlob { file } => match write_blob(&repo_path, &file) {
Ok(hash) => println!("{}", hex::encode(hash)), Ok(hash) => println!("{}", hex::encode(hash)),
Err(e) => eprintln!("Failed to write object: {}", e), Err(e) => eprintln!("Failed to write object: {}", e),
}, },
Command::WriteTree { path } => match repo.write_tree(&path) { Command::WriteTree { path } => match write_tree(&repo_path, &path) {
Ok(hash) => println!("{}", hex::encode(hash)), Ok(hash) => println!("{}", hex::encode(hash)),
Err(e) => eprintln!("Failed to write tree: {}", e), Err(e) => eprintln!("Failed to write tree: {}", e),
}, },

View File

@ -1,4 +1,3 @@
use crate::repository::Repository;
use crate::{error::RuntimeError, kind::Kind}; use crate::{error::RuntimeError, kind::Kind};
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use flate2::{write::ZlibEncoder, Compression}; use flate2::{write::ZlibEncoder, Compression};
@ -25,10 +24,8 @@ pub struct TreeObject {
pub hash: [u8; 20], pub hash: [u8; 20],
} }
impl Repository { pub fn read_object(path: &Path, object: &str) -> Result<Object<impl BufRead>> {
pub fn read_object(&self, object: &str) -> Result<Object<impl BufRead>> { let object_path = path
let object_path = self
.path
.join(".git") .join(".git")
.join("objects") .join("objects")
.join(&object[..2]) .join(&object[..2])
@ -68,47 +65,6 @@ impl Repository {
_size: object_size, _size: object_size,
data: buf_reader, 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<bool> { fn is_path_in_repo(repo_path: &Path, file_path: &Path) -> Result<bool> {
@ -123,6 +79,45 @@ fn is_path_in_repo(repo_path: &Path, file_path: &Path) -> Result<bool> {
Ok(file_canonical.starts_with(repo_canonical)) 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<R: BufRead> Object<R> { impl<R: BufRead> Object<R> {
pub fn string(&mut self) -> Result<String> { pub fn string(&mut self) -> Result<String> {
let mut buf: Vec<u8> = Vec::new(); let mut buf: Vec<u8> = Vec::new();

View File

@ -1,57 +0,0 @@
use anyhow::Result;
use std::{
env,
fs::{create_dir, read_to_string},
path::PathBuf,
};
pub struct Repository {
pub path: PathBuf,
pub ignore: Vec<String>,
}
pub fn default_init_path() -> PathBuf {
env::var("REPO_PATH")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("."))
}
impl Repository {
pub fn new() -> Result<Repository> {
let path = default_init_path();
let mut repo = Repository {
path,
ignore: Vec::new(),
};
repo.load_ignore()?;
Ok(repo)
}
fn load_ignore(&mut self) -> Result<bool> {
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<PathBuf> {
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())
}
}

View File

@ -3,11 +3,9 @@ use std::os::unix::fs::MetadataExt;
use std::path::PathBuf; use std::path::PathBuf;
use crate::kind::Kind; use crate::kind::Kind;
use crate::object::TreeObject; use crate::object::{write_blob, write_object, TreeObject};
use crate::repository::Repository;
impl Repository { pub fn write_tree(repo_path: &PathBuf, path: &PathBuf) -> Result<[u8; 20]> {
pub fn write_tree(&self, path: &PathBuf) -> Result<[u8; 20]> {
let mut entries = Vec::new(); let mut entries = Vec::new();
let files = std::fs::read_dir(path)?; let files = std::fs::read_dir(path)?;
@ -17,21 +15,14 @@ impl Repository {
let file_name = file.file_name(); let file_name = file.file_name();
let file_path = file.path(); let file_path = file.path();
// Skip the .git directory
if file_name == ".git" {
continue;
}
let hash: [u8; 20]; let hash: [u8; 20];
let kind; let kind;
if file_type.is_dir() { if file_type.is_dir() {
hash = self hash = write_tree(repo_path, &file_path).context("could not write_tree of subtree")?;
.write_tree(&file_path)
.context("could not write_tree of subtree")?;
kind = Kind::Tree; kind = Kind::Tree;
} else { } else {
hash = self.write_blob(&file_path).context(format!( hash = write_blob(repo_path, &file_path).context(format!(
"could not write object {:?}", "could not write object {:?}",
file_path.file_name() file_path.file_name()
))?; ))?;
@ -57,6 +48,5 @@ impl Repository {
out.extend_from_slice(&entry.hash); out.extend_from_slice(&entry.hash);
} }
self.write_object(Kind::Tree, &out).context("Write") write_object(repo_path, Kind::Tree, &out).context("Write")
}
} }