chore: refator in repository module
This commit is contained in:
parent
d1f83c18f3
commit
49ed740014
37
src/main.rs
37
src/main.rs
@ -1,6 +1,6 @@
|
|||||||
use anyhow::{Error, Result};
|
use anyhow::{Error, Result};
|
||||||
use std::env;
|
use repository::default_init_path;
|
||||||
use std::{fs, path::PathBuf};
|
use std::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::object::{read_object, write_blob};
|
use crate::repository::Repository;
|
||||||
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,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<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 init_repository(path) {
|
Command::Init { path } => match repo.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 read_object(&repo_path, &hash) {
|
Command::CatFile { hash } => match repo.read_object(&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 write_blob(&repo_path, &file) {
|
Command::WriteBlob { file } => match repo.write_blob(&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 write_tree(&repo_path, &path) {
|
Command::WriteTree { path } => match repo.write_tree(&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),
|
||||||
},
|
},
|
||||||
|
149
src/object.rs
149
src/object.rs
@ -1,3 +1,4 @@
|
|||||||
|
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};
|
||||||
@ -24,47 +25,90 @@ pub struct TreeObject {
|
|||||||
pub hash: [u8; 20],
|
pub hash: [u8; 20],
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_object(path: &Path, object: &str) -> Result<Object<impl BufRead>> {
|
impl Repository {
|
||||||
let object_path = path
|
pub fn read_object(&self, object: &str) -> Result<Object<impl BufRead>> {
|
||||||
.join(".git")
|
let object_path = self
|
||||||
.join("objects")
|
.path
|
||||||
.join(&object[..2])
|
.join(".git")
|
||||||
.join(&object[2..]);
|
.join("objects")
|
||||||
|
.join(&object[..2])
|
||||||
|
.join(&object[2..]);
|
||||||
|
|
||||||
let fd = File::open(&object_path).context("opening the object")?;
|
let fd = File::open(&object_path).context("opening the object")?;
|
||||||
let zfd = flate2::read::ZlibDecoder::new(fd);
|
let zfd = flate2::read::ZlibDecoder::new(fd);
|
||||||
let mut buf_reader = std::io::BufReader::new(zfd);
|
let mut buf_reader = std::io::BufReader::new(zfd);
|
||||||
|
|
||||||
let mut buf: Vec<u8> = Vec::new();
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
buf_reader
|
buf_reader
|
||||||
.read_until(0, &mut buf)
|
.read_until(0, &mut buf)
|
||||||
.context("read the object")?;
|
.context("read the object")?;
|
||||||
|
|
||||||
match buf.pop() {
|
match buf.pop() {
|
||||||
Some(0) => {}
|
Some(0) => {}
|
||||||
Some(_) | None => return Err(RuntimeError::UnexpectedChar.into()),
|
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 {
|
let Some((object_type, object_size)) = header.split_once(' ') else {
|
||||||
anyhow::bail!("could not parse object header correctly");
|
anyhow::bail!("could not parse object header correctly");
|
||||||
};
|
};
|
||||||
|
|
||||||
let object_type = match object_type {
|
let object_type = match object_type {
|
||||||
"blob" => Kind::Blob(true),
|
"blob" => Kind::Blob(true),
|
||||||
"commit" => Kind::Commit,
|
"commit" => Kind::Commit,
|
||||||
"tree" => Kind::Tree,
|
"tree" => Kind::Tree,
|
||||||
_ => anyhow::bail!("invalid object type found"),
|
_ => anyhow::bail!("invalid object type found"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let object_size = object_size.parse::<usize>()?;
|
let object_size = object_size.parse::<usize>()?;
|
||||||
|
|
||||||
Ok(Object {
|
Ok(Object {
|
||||||
kind: object_type,
|
kind: object_type,
|
||||||
_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> {
|
||||||
@ -79,45 +123,6 @@ 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();
|
||||||
|
57
src/repository.rs
Normal file
57
src/repository.rs
Normal file
@ -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<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())
|
||||||
|
}
|
||||||
|
}
|
89
src/tree.rs
89
src/tree.rs
@ -3,55 +3,60 @@ 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::{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]> {
|
impl Repository {
|
||||||
let mut entries = Vec::new();
|
pub fn write_tree(&self, path: &PathBuf) -> Result<[u8; 20]> {
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
|
||||||
let files = std::fs::read_dir(path)?;
|
let files = std::fs::read_dir(path)?;
|
||||||
for file in files {
|
for file in files {
|
||||||
let file = file?;
|
let file = file?;
|
||||||
let file_type = file.file_type()?;
|
let file_type = file.file_type()?;
|
||||||
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
|
// Skip the .git directory
|
||||||
if file_name == ".git" {
|
if file_name == ".git" {
|
||||||
continue;
|
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];
|
entries.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
let kind;
|
|
||||||
|
|
||||||
if file_type.is_dir() {
|
let mut out: Vec<u8> = Vec::new();
|
||||||
hash = write_tree(repo_path, &file_path).context("could not write_tree of subtree")?;
|
for entry in &entries {
|
||||||
kind = Kind::Tree;
|
out.extend_from_slice(entry.mode.as_bytes());
|
||||||
} else {
|
out.push(b' ');
|
||||||
hash = write_blob(repo_path, &file_path).context(format!(
|
out.extend_from_slice(entry.name.as_bytes());
|
||||||
"could not write object {:?}",
|
out.push(0);
|
||||||
file_path.file_name()
|
out.extend_from_slice(&entry.hash);
|
||||||
))?;
|
|
||||||
kind = Kind::Blob(file_path.metadata()?.mode() & 0o111 != 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entries.push(TreeObject {
|
self.write_object(Kind::Tree, &out).context("Write")
|
||||||
mode: kind.to_mode().to_string(),
|
|
||||||
kind,
|
|
||||||
name: file_name.into_string().unwrap(),
|
|
||||||
hash,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entries.sort_by(|a, b| a.name.cmp(&b.name));
|
|
||||||
|
|
||||||
let mut out: Vec<u8> = 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")
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user