From e94340be68a087ce9e2e1f176498776623be2f43 Mon Sep 17 00:00:00 2001
From: Fady Philip
Date: Thu, 25 Jun 2026 14:28:35 +0300
Subject: [PATCH 1/5] refactor: eliminate implicit global state via dependency
injection
The core engine (object, refs, tree, commit) previously relied on the OS's global Current Working Directory (CWD) by utilizing hardcoded relative paths (e.g., '.git/objects/'). This implicit coupling to the execution environment rendered the library impure, tightly bound to the terminal state, and inherently thread-unsafe for parallel testing.
Transitioned the entire core architecture to explicit Dependency Injection. All filesystem-touching functions now require a `repo_dir: &Path` parameter. `main.rs` has been elevated to a strict 'application boundary' that captures `std::env::current_dir()` exactly once and injects it down the call chain.
Architectural Impact:
- Eradicated all hidden environmental dependencies and hardcoded '.git/' literals from the library layer.
- Transformed the engine from an environment-dependent script into a pure, isolated library capable of manipulating any repository programmatically.
- Eliminated global state mutation, establishing a thread-safe foundation that enables parallel, race-condition-free unit testing.
---
src/commit.rs | 8 +++---
src/main.rs | 67 ++++++++++++++++++++++++++++++---------------------
src/object.rs | 17 +++++++------
src/refs.rs | 17 +++++++------
src/tree.rs | 8 +++---
5 files changed, 68 insertions(+), 49 deletions(-)
diff --git a/src/commit.rs b/src/commit.rs
index 200f98a..83a4314 100644
--- a/src/commit.rs
+++ b/src/commit.rs
@@ -1,6 +1,7 @@
use crate::{config::get_author, object::write_object};
use std::{
io::Write,
+ path::Path,
time::{SystemTime, UNIX_EPOCH},
};
use thiserror::Error;
@@ -66,7 +67,7 @@ fn create_commit(
Ok(commit)
}
-fn write_commit(commit: &Commit) -> Result {
+fn write_commit(commit: &Commit, dir: &Path) -> Result {
let mut serialized = Vec::new();
writeln!(&mut serialized, "tree {}", commit.tree)?;
if let Some(parent_hash) = &commit.parent {
@@ -86,7 +87,7 @@ fn write_commit(commit: &Commit) -> Result {
commit.committer.timezone
)?;
write!(&mut serialized, "\n{}\n", commit.message)?;
- let oid = write_object("commit", &serialized)?;
+ let oid = write_object("commit", &serialized, dir)?;
Ok(oid)
}
@@ -94,8 +95,9 @@ pub fn write_commit_object(
tree_hash: &str,
commit_message: &str,
parent_hash: Option<&str>,
+ dir: &Path,
) -> Result {
let commit = create_commit(tree_hash, commit_message, parent_hash)?;
- let commit_hash = write_commit(&commit)?;
+ let commit_hash = write_commit(&commit, dir)?;
Ok(commit_hash)
}
diff --git a/src/main.rs b/src/main.rs
index b0f5222..e0e0333 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -76,36 +76,38 @@ fn main() -> anyhow::Result<()> {
// The magic happens here! clap reads env::args(), validates everything,
// and populates the Cli struct.
let cli = Cli::parse();
+ let repodir =
+ std::env::current_dir().context("Failed to determine the current working directory")?;
match cli.command {
- Commands::Init => cmd_init(),
+ Commands::Init => cmd_init(&repodir),
Commands::CatFile {
pretty,
r#type,
size,
hash,
- } => cmd_cat_file(pretty, r#type, size, &hash),
+ } => cmd_cat_file(pretty, r#type, size, &hash, &repodir),
- Commands::HashObject { write, file } => cmd_hash_object(&file, write),
+ Commands::HashObject { write, file } => cmd_hash_object(&file, write, &repodir),
Commands::WriteTree => {
- let tree_hash = cmd_write_tree(Path::new("."))?;
+ let tree_hash = cmd_write_tree(Path::new("."), &repodir)?;
println!("{}", tree_hash);
Ok(())
}
// FIXED: Used the correct destructured variables, added `?` and `println!`
Commands::CommitTree { tree_hash, message } => {
- let commit_hash = cmd_write_commit(&tree_hash, &message, None)?;
+ let commit_hash = cmd_write_commit(&tree_hash, &message, None, &repodir)?;
println!("{}", commit_hash);
Ok(())
}
// ADDED: The missing Commit match arm
Commands::Commit { message } => {
- let new_commit_hash = cmd_commit(&message)?;
- update_current_ref(&new_commit_hash)?;
+ let new_commit_hash = cmd_commit(&message, &repodir)?;
+ update_current_ref(&new_commit_hash, &repodir)?;
println!("{}", new_commit_hash);
Ok(())
}
@@ -114,23 +116,32 @@ fn main() -> anyhow::Result<()> {
// DELETED: expect_args and run functions are no longer needed!
-fn cmd_init() -> anyhow::Result<()> {
- fs::create_dir_all(".git/objects/info")?;
- fs::create_dir_all(".git/objects/pack/")?;
- fs::create_dir_all(".git/refs/heads/")?;
- fs::create_dir_all(".git/refs/tags/")?;
- fs::write(".git/HEAD", "ref: refs/heads/main\n")?;
- if !Path::new(".git/config").exists() {
+fn cmd_init(repo_dir: &Path) -> anyhow::Result<()> {
+ let git_dir = repo_dir.join(".git");
+ fs::create_dir_all(git_dir.join("objects/info"))?;
+ fs::create_dir_all(git_dir.join("objects/pack/"))?;
+ fs::create_dir_all(git_dir.join("refs/heads/"))?;
+ fs::create_dir_all(git_dir.join("refs/tags/"))?;
+ fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n")?;
+
+ let config_path = git_dir.join("config");
+ if !config_path.exists() {
fs::write(
- ".git/config",
+ config_path,
"[user]\nname = \"Your Name\"\nemail = \"you@example.com\"\n",
)?;
}
Ok(())
}
-fn cmd_cat_file(pretty: bool, show_type: bool, show_size: bool, hash: &str) -> anyhow::Result<()> {
- let (kind, content) = read_object(hash).context("Failed to read git object from disk")?;
+fn cmd_cat_file(
+ pretty: bool,
+ show_type: bool,
+ show_size: bool,
+ hash: &str,
+ dir: &Path,
+) -> anyhow::Result<()> {
+ let (kind, content) = read_object(hash, dir).context("Failed to read git object from disk")?;
if show_type {
println!("{}", kind);
@@ -144,10 +155,10 @@ fn cmd_cat_file(pretty: bool, show_type: bool, show_size: bool, hash: &str) -> a
Ok(())
}
-fn cmd_hash_object(file: &str, write: bool) -> anyhow::Result<()> {
+fn cmd_hash_object(file: &str, write: bool, dir: &Path) -> anyhow::Result<()> {
let content = fs::read(file)?;
if write {
- let hash = write_object("blob", &content).context("Failed to write object")?;
+ let hash = write_object("blob", &content, dir).context("Failed to write object")?;
println!("{}", hash);
} else {
bail!("Without -w, hashing without writing is not yet implemented. Please use -w.");
@@ -155,8 +166,8 @@ fn cmd_hash_object(file: &str, write: bool) -> anyhow::Result<()> {
Ok(())
}
-fn cmd_write_tree(path: &Path) -> anyhow::Result {
- let tree_hash = write_tree(path).context("Failed to write tree")?;
+fn cmd_write_tree(path: &Path, dir: &Path) -> anyhow::Result {
+ let tree_hash = write_tree(path, dir).context("Failed to write tree")?;
Ok(tree_hash)
}
@@ -165,19 +176,21 @@ fn cmd_write_commit(
tree_hash: &str,
commit_message: &str,
parent_hash: Option<&str>,
+ dir: &Path,
) -> anyhow::Result {
- let commit_hash = write_commit_object(tree_hash, commit_message, parent_hash)
+ let commit_hash = write_commit_object(tree_hash, commit_message, parent_hash, dir)
.context("Failed to write commit to disk")?;
Ok(commit_hash)
}
-fn cmd_commit(commit_message: &str) -> anyhow::Result {
+fn cmd_commit(commit_message: &str, dir: &Path) -> anyhow::Result {
let current_path = Path::new(".");
- let tree_hash = write_tree(current_path).context("Failed to snapshot working directory")?;
- let path = read_head().context("Failed to read HEAD pointer")?;
- let ref_content = read_ref(&path).context("Failed to read current branch reference")?;
+ let tree_hash =
+ write_tree(current_path, dir).context("Failed to snapshot working directory")?;
+ let path = read_head(dir).context("Failed to read HEAD pointer")?;
+ let ref_content = read_ref(&path, dir).context("Failed to read current branch reference")?;
- let commit_hash = write_commit_object(&tree_hash, commit_message, ref_content.as_deref())
+ let commit_hash = write_commit_object(&tree_hash, commit_message, ref_content.as_deref(), dir)
.context("Failed to write commit to disk")?;
Ok(commit_hash)
}
diff --git a/src/object.rs b/src/object.rs
index c172150..959be2f 100644
--- a/src/object.rs
+++ b/src/object.rs
@@ -3,7 +3,7 @@ use sha1::{Digest, Sha1};
use std::{
fs,
io::{Read, Write},
- path::PathBuf,
+ path::{Path, PathBuf},
};
use thiserror::Error;
@@ -64,9 +64,9 @@ fn compress_object(object: &[u8]) -> Result, ObjectError> {
Ok(compressed?)
}
-pub fn read_object(hash: &str) -> Result<(String, Vec), ObjectError> {
+pub fn read_object(hash: &str, dir: &Path) -> Result<(String, Vec), ObjectError> {
// 1. Resolve the filesystem path for this hash
- let path = object_path(hash)?;
+ let path = object_path(hash, dir)?;
// 2. Read the compressed bytes from disk
let compressed = fs::read(&path)?;
@@ -105,19 +105,22 @@ pub fn read_object(hash: &str) -> Result<(String, Vec), ObjectError> {
Ok((kind.to_string(), content))
}
-fn object_path(hash: &str) -> Result {
- let base = ".git/objects/";
+fn object_path(hash: &str, repo_dir: &Path) -> Result {
+ if hash.len() != 40 {
+ return Err(ObjectError::InvalidHashLength);
+ }
+ let base = repo_dir.join(".git").join("objects");
let file_name = hash.get(2..).ok_or(ObjectError::InvalidHashLength)?;
let dir = hash.get(..2).ok_or(ObjectError::InvalidHashLength)?;
let path = PathBuf::from(base).join(dir).join(file_name);
Ok(path)
}
-pub fn write_object(kind: &str, content: &[u8]) -> Result {
+pub fn write_object(kind: &str, content: &[u8], dir: &Path) -> Result {
let object = create_object(kind, content)?;
let hashed_object = hash_object(&object);
let compressed_object = compress_object(&object)?;
- let path = object_path(&hashed_object)?;
+ let path = object_path(&hashed_object, dir)?;
fs::create_dir_all(path.parent().ok_or(ObjectError::InvalidObjectPath)?)?;
fs::write(path, compressed_object)?;
Ok(hashed_object)
diff --git a/src/refs.rs b/src/refs.rs
index 2795ed8..7cddf4f 100644
--- a/src/refs.rs
+++ b/src/refs.rs
@@ -1,4 +1,3 @@
-use std::path::PathBuf;
use std::{fs, path::Path};
use thiserror::Error;
@@ -13,17 +12,18 @@ pub enum RefsError {
DetachedHead,
}
-pub fn read_head() -> Result {
- let path = fs::read_to_string(".git/HEAD")?;
- let clean_path = path
+pub fn read_head(dir: &Path) -> Result {
+ let path = dir.join(".git").join("HEAD");
+ let contents = fs::read_to_string(path)?;
+ let clean_path = contents
.trim()
.strip_prefix("ref: ")
.ok_or(RefsError::DetachedHead)?;
Ok(clean_path.to_string())
}
-pub fn read_ref(path: &str) -> Result
+[](https://github.com//git-rs/actions/workflows/ci.yml)
+
[Overview](#overview) • [Architecture](#architecture) • [Quick Start](#quick-start) • [Roadmap](#roadmap)
From 8e1d68298b1877a9c3ed7218275ed24a503f1b62 Mon Sep 17 00:00:00 2001
From: Fady Philip
Date: Thu, 25 Jun 2026 15:34:40 +0300
Subject: [PATCH 5/5] fix(minor): remove unnecessary PathBuf::from(base)
---
src/object.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/object.rs b/src/object.rs
index 91c9ed7..8e8d2c1 100644
--- a/src/object.rs
+++ b/src/object.rs
@@ -112,7 +112,7 @@ fn object_path(hash: &str, repo_dir: &Path) -> Result {
let base = repo_dir.join(".git").join("objects");
let file_name = hash.get(2..).ok_or(ObjectError::InvalidHashLength)?;
let dir = hash.get(..2).ok_or(ObjectError::InvalidHashLength)?;
- let path = PathBuf::from(base).join(dir).join(file_name);
+ let path = base.join(dir).join(file_name);
Ok(path)
}