Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand Down Expand Up @@ -122,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::*;
Expand Down
11 changes: 11 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ enum Commands {

/// Show a log of the history.
Log(LogArgs),

/// Show a list of references
ShowRef(ShowRefArgs),
}

#[derive(Args)]
Expand Down Expand Up @@ -61,6 +64,9 @@ struct LogArgs {
object: String,
}

#[derive(Args)]
struct ShowRefArgs {}

fn main() -> Result<()> {
let cli = Cli::parse();

Expand Down Expand Up @@ -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(())
}
8 changes: 6 additions & 2 deletions src/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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])?),
Expand Down
81 changes: 81 additions & 0 deletions src/refs.rs
Original file line number Diff line number Diff line change
@@ -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<String> {
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");
}
}
41 changes: 34 additions & 7 deletions tests/git_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -229,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"\
Expand Down Expand Up @@ -293,4 +305,19 @@ aaaaaa - This is a good commit - \"Alice <bye@alice.test>\"

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
"
);
}
}