feat: adding cat-file
Some checks failed
CI checks / Format (push) Successful in 29s
CI checks / Clippy (push) Failing after 38s

This commit is contained in:
Patrick MARIE 2025-02-04 09:07:59 +01:00
parent bba1db7e2f
commit bb731239f4
Signed by: mycroft
GPG Key ID: BB519E5CD8E7BFA7
3 changed files with 252 additions and 5 deletions

69
Cargo.lock generated
View File

@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "anstream"
version = "0.6.18"
@ -58,6 +64,12 @@ version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.27"
@ -104,12 +116,37 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
[[package]]
name = "flate2"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
@ -122,6 +159,18 @@ version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"flate2",
"hex",
"thiserror",
]
[[package]]
name = "miniz_oxide"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
dependencies = [
"adler2",
]
[[package]]
@ -165,6 +214,26 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.16"

View File

@ -5,4 +5,7 @@ edition = "2021"
[dependencies]
anyhow = "1.0.95"
clap = { version = "4.5.27", features = ["derive"] }
clap = { version = "4.5.27", features = ["derive", "string"] }
flate2 = "1.0.35"
hex = "0.4.3"
thiserror = "2.0.11"

View File

@ -1,4 +1,6 @@
use anyhow::{Error, Result};
use anyhow::{Context, Error, Result};
use std::env;
use std::io::prelude::*;
use std::{fs, path::PathBuf};
use clap::Parser;
@ -16,14 +18,72 @@ enum Command {
/// Initialize a new Git repository
Init {
/// The path where to create the repository. Defaults to current directory
#[arg(default_value = ".")]
#[arg(default_value=default_init_path().into_os_string())]
path: PathBuf,
},
/// Display a Git object
CatFile {
/// The object to display
hash: String,
},
}
fn init_repository(path: PathBuf) -> Result<PathBuf> {
println!("Creating in {:?}", path);
#[derive(thiserror::Error, Debug)]
pub enum RuntimeError {
#[error("Invalid character found")]
UnexpectedChar,
}
#[derive(Debug)]
enum Kind {
Blob, // 100644 or 100755
Commit, // 120000
Tree, // 040000
Symlink, // 120000
}
impl Kind {
fn from_mode(mode: &str) -> Result<Self> {
match mode {
"100644" | "100755" => Ok(Kind::Blob),
"120000" => Ok(Kind::Commit),
"040000" | "40000" => Ok(Kind::Tree),
_ => Err(anyhow::anyhow!(format!("invalid mode: {}", mode))),
}
}
fn string(&self) -> &str {
match self {
Kind::Blob => "blob",
Kind::Commit => "commit",
Kind::Tree => "tree",
Kind::Symlink => "symlink",
}
}
}
#[derive(Debug)]
struct Object<Reader> {
kind: Kind,
size: usize,
data: Reader,
}
#[derive(Debug)]
struct TreeObject {
mode: String,
kind: Kind,
name: String,
hash: [u8; 20],
}
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)?;
@ -35,14 +95,129 @@ fn init_repository(path: PathBuf) -> Result<PathBuf> {
Ok(path)
}
fn read_object(path: &PathBuf, object: &str) -> Result<Object<impl BufRead>> {
let object_path = path
.join(".git")
.join("objects")
.join(&object[..2])
.join(&object[2..]);
let fd = fs::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<u8> = 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()),
};
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 object_type = match object_type {
"blob" => Kind::Blob,
"commit" => Kind::Commit,
"tree" => Kind::Tree,
_ => anyhow::bail!("invalid object type found"),
};
let object_size = object_size.parse::<usize>()?;
Ok(Object {
kind: object_type,
size: object_size,
data: buf_reader,
})
}
impl<R: BufRead> Object<R> {
fn print(&mut self) -> Result<()> {
let mut buf: Vec<u8> = Vec::new();
let mut buf_hash: [u8; 20] = [0; 20];
match self.kind {
Kind::Blob | Kind::Commit => {
self.data.read_to_end(&mut buf)?;
println!("{}", String::from_utf8(buf)?);
}
Kind::Tree => {
let mut max_name_len = 0;
let mut entries = Vec::new();
loop {
let read_bytes_len = self.data.read_until(0, &mut buf)?;
if read_bytes_len <= 0 {
break;
}
let mode_name = buf.clone();
buf.clear();
let mut splits = mode_name.splitn(2, |&b| b == b' ');
let mode = splits
.next()
.ok_or_else(|| anyhow::anyhow!("could not parse mode"))?;
let mode = std::str::from_utf8(mode)?;
let name = splits
.next()
.ok_or_else(|| anyhow::anyhow!("could not parse name"))?;
let name = std::str::from_utf8(name)?;
self.data.read_exact(&mut buf_hash)?;
if name.len() > max_name_len {
max_name_len = name.len();
}
entries.push(TreeObject {
name: name.to_string(),
kind: Kind::from_mode(mode)?,
mode: mode.to_string(),
hash: buf_hash.clone(),
});
}
entries.sort_by(|a, b| a.name.cmp(&b.name));
for entry in entries {
let hash = hex::encode(entry.hash);
println!(
"{:0>6} {} {} {:name_len$}",
entry.mode,
entry.kind.string(),
hash,
entry.name,
name_len = max_name_len
);
}
}
_ => unimplemented!(),
}
Ok(())
}
}
fn main() -> Result<(), Error> {
let cli = Cli::parse();
let 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) {
Ok(mut obj) => obj.print()?,
Err(e) => eprintln!("Failed to read object: {}", e),
},
}
Ok(())