From 17e649650211e6575369af8ea501c7666efccea8 Mon Sep 17 00:00:00 2001 From: Johan Norberg Date: Sun, 9 Mar 2025 18:18:13 +0100 Subject: [PATCH 1/3] Add function to resolve git references --- src/lib.rs | 1 + src/refs.rs | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 src/refs.rs diff --git a/src/lib.rs b/src/lib.rs index 3372b20..0b39802 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ use object::Object; use repo::Repo; pub mod object; +pub mod refs; pub mod repo; pub fn init_repo(repo: &Repo, branch_name: &str) -> Result<()> { diff --git a/src/refs.rs b/src/refs.rs new file mode 100644 index 0000000..5ac9bf2 --- /dev/null +++ b/src/refs.rs @@ -0,0 +1,81 @@ +use std::fs; + +use crate::repo::Repo; + +use anyhow::{Result, anyhow}; + +/// Finds and resolves a Git reference to its commit hash. +/// +/// # Arguments +/// * reference - The reference path to look up, prefixed with the type of reference (e.g. refs/heads) +/// * repo - The repository to search in +/// +/// # Returns +/// * The commit hash as a string if found +pub fn find_ref(reference: &str, repo: &Repo) -> Result { + let path = repo.git_dir().join(reference); + if !path.exists() { + return Err(anyhow!("Reference not found: {reference}")); + } + let content = fs::read_to_string(path)?; + if content.starts_with("ref: ") { + let target = content.trim_start_matches("ref: ").trim_end(); + return find_ref(target, repo); + } + Ok(content.trim_end().to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use tempfile::tempdir; + + struct TestRepo { + dir: tempfile::TempDir, + repo: Repo, + } + + impl TestRepo { + fn new() -> Self { + let dir = tempdir().unwrap(); + fs::create_dir_all(dir.path().join(".git").join("refs").join("heads")).unwrap(); + let repo = Repo::new(dir.path()); + TestRepo { dir, repo } + } + + fn create_ref(&self, name: &str, content: &str) { + let ref_path = self.dir.path().join(".git/refs/heads").join(name); + fs::write(ref_path, content).unwrap(); + } + } + + #[test] + fn test_find_ref_existing() { + let test_repo = TestRepo::new(); + test_repo.create_ref("main", "commit_hash\n"); + + let result = find_ref("refs/heads/main", &test_repo.repo); + assert_eq!(result.unwrap(), "commit_hash"); + } + + #[test] + fn test_find_ref_non_existing() { + let test_repo = TestRepo::new(); + + let result = find_ref("non_existing", &test_repo.repo); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert_eq!(err, "Reference not found: non_existing"); + } + + #[test] + fn test_find_ref_with_reference() { + let test_repo = TestRepo::new(); + test_repo.create_ref("main", "ref: refs/heads/feature\n"); + test_repo.create_ref("feature", "commit_hash"); + + let result = find_ref("refs/heads/main", &test_repo.repo); + assert_eq!(result.unwrap(), "commit_hash"); + } +} From ab9291c21cf7b4637cf5bff92bda882a4152cccb Mon Sep 17 00:00:00 2001 From: Johan Norberg Date: Sun, 9 Mar 2025 18:18:36 +0100 Subject: [PATCH 2/3] Add show-ref command --- src/lib.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 11 +++++++++++ tests/git_test.rs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 0b39802..68d1280 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -123,6 +123,52 @@ pub fn log(repo: &Repo, object_rev: &str, stdout: &mut dyn io::Write) -> Result< Ok(()) } +fn iter_subdirs( + path: &std::path::PathBuf, + callback: &mut dyn FnMut(&std::path::PathBuf) -> Result<()>, +) -> Result<()> { + for entry in fs::read_dir(path)? { + let p = entry?.path(); + if p.is_dir() { + iter_subdirs(&p, callback)?; + } else { + callback(&p)?; + } + } + Ok(()) +} + +pub fn show_ref(repo: &Repo, stdout: &mut dyn io::Write) -> Result<()> { + let mut found_refs: Vec<(String, String)> = vec![]; + + let mut add_ref_to_found = |path: &std::path::PathBuf| -> Result<()> { + let d = path.strip_prefix(repo.git_dir())?; + let ref_name = &d + .to_str() + .ok_or_else(|| anyhow::anyhow!("Invalid ref name"))?; + // Normalize path separators to handle Windows + let ref_name = ref_name.replace("\\", "/"); + let hash = refs::find_ref(&ref_name, repo)?; + found_refs.push((ref_name.to_string(), hash)); + Ok(()) + }; + + for folder in &["heads", "remotes", "tags"] { + let path = repo.git_dir().join("refs").join(folder); + if path.is_dir() { + iter_subdirs(&path, &mut add_ref_to_found)?; + } + } + + // Sort by ref name + found_refs.sort_by(|a, b| a.0.cmp(&b.0)); + for (ref_name, hash) in found_refs { + writeln!(stdout, "{hash} {ref_name}")?; + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/main.rs b/src/main.rs index 3ed8ecb..3ad2b50 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,9 @@ enum Commands { /// Show a log of the history. Log(LogArgs), + + /// Show a list of references + ShowRef(ShowRefArgs), } #[derive(Args)] @@ -61,6 +64,9 @@ struct LogArgs { object: String, } +#[derive(Args)] +struct ShowRefArgs {} + fn main() -> Result<()> { let cli = Cli::parse(); @@ -100,6 +106,11 @@ fn main() -> Result<()> { .ok_or_else(|| anyhow!("Could not find a valid git repository"))?; good_git::log(&repo, &log_args.object, &mut io::stdout())?; } + Commands::ShowRef(_show_ref_args) => { + let repo = Repo::from_dir(Path::new(".")) + .ok_or_else(|| anyhow!("Could not find a valid git repository"))?; + good_git::show_ref(&repo, &mut io::stdout())?; + } } Ok(()) } diff --git a/tests/git_test.rs b/tests/git_test.rs index d241c01..3882c03 100644 --- a/tests/git_test.rs +++ b/tests/git_test.rs @@ -78,6 +78,11 @@ parent {} write_compressed_object(dir, hash, &full_bytes); } +fn create_ref(dir: PathBuf, name: &str, content: &str) { + let ref_path = dir.join(".git/refs/heads").join(name); + std::fs::write(ref_path, content).unwrap(); +} + #[fixture] fn test_repo() -> tempfile::TempDir { let tmpdir = tempfile::tempdir().unwrap(); @@ -144,6 +149,16 @@ fn test_repo() -> tempfile::TempDir { "ccccccccccccccccccccdddddddddddddddddddd", &commit, ); + create_ref( + git_dir.clone(), + "main", + "aaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbb\n", + ); + create_ref( + git_dir.clone(), + "better_commit", + "ccccccccccccccccccccdddddddddddddddddddd\n", + ); tmpdir } @@ -293,4 +308,19 @@ aaaaaa - This is a good commit - \"Alice \" assert_eq!(stdout, b"test content\n\n"); } + + #[rstest] + fn test_show_ref(test_repo: tempfile::TempDir) { + let repo = Repo::new(test_repo.path()); + let mut stdout = Vec::new(); + + good_git::show_ref(&repo, &mut stdout).unwrap(); + assert_eq!( + std::str::from_utf8(&stdout).unwrap(), + "\ +ccccccccccccccccccccdddddddddddddddddddd refs/heads/better_commit +aaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbb refs/heads/main +" + ); + } } From 456829b02dcd1d5163e836618153a47f3f8c77cf Mon Sep 17 00:00:00 2001 From: Johan Norberg Date: Sun, 9 Mar 2025 18:20:18 +0100 Subject: [PATCH 3/3] Use find_ref in Object::from_rev --- src/object.rs | 8 ++++++-- tests/git_test.rs | 11 ++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/object.rs b/src/object.rs index b880142..56ac8e8 100644 --- a/src/object.rs +++ b/src/object.rs @@ -3,7 +3,7 @@ use flate2::read::ZlibDecoder; use sha1::{Digest, Sha1}; use std::{fs, io::prelude::*}; -use crate::repo::Repo; +use crate::{refs, repo::Repo}; #[derive(Debug)] pub struct Blob { @@ -243,7 +243,11 @@ impl Object { } } - // TODO: Check if this is a branch or a tag + for reference in &[format!("refs/heads/{rev}"), format!("refs/tags/{rev}")] { + if let Ok(hash) = refs::find_ref(reference, repo) { + candidates.push(hash); + } + } match candidates.len() { 1 => Ok(Object::from_hash(repo, &candidates[0])?), diff --git a/tests/git_test.rs b/tests/git_test.rs index 3882c03..b9769e9 100644 --- a/tests/git_test.rs +++ b/tests/git_test.rs @@ -244,16 +244,13 @@ from a good client } #[rstest] - fn test_cat_file_commit(test_repo: tempfile::TempDir) { + #[case("aaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbb")] + #[case("main")] // Points to the same commit + fn test_cat_file_commit(test_repo: tempfile::TempDir, #[case] reference: String) { let repo = Repo::new(test_repo.path()); let mut stdout = Vec::new(); - good_git::cat_file( - &repo, - "aaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbb", - &mut stdout, - ) - .unwrap(); + good_git::cat_file(&repo, &reference, &mut stdout).unwrap(); assert_eq!( stdout, b"\