From a2bf7eadba9f1d0601e30a4772a4e11bc3fb865a Mon Sep 17 00:00:00 2001 From: nick <59822256+Archasion@users.noreply.github.com> Date: Mon, 3 Feb 2025 11:55:13 +0000 Subject: [PATCH 1/8] chore(show-ref): Start implementing `show-ref` --- src/commands/init.rs | 4 +- src/commands/mod.rs | 3 ++ src/commands/show_ref.rs | 111 +++++++++++++++++++++++++++++++++++++++ src/main.rs | 2 + 4 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 src/commands/show_ref.rs diff --git a/src/commands/init.rs b/src/commands/init.rs index 95cb08f..019b323 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -23,9 +23,9 @@ impl CommandArgs for InitArgs { std::fs::create_dir_all(object_dir)?; std::fs::create_dir(init_path.join("refs"))?; - // Create the HEAD file with the initial branch. + // Create the main HEAD file. std::fs::write( - init_path.join("HEAD"), + init_path.join("heads").join(&self.initial_branch), get_head_ref_content(&self.initial_branch), )?; diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 4de92f5..9de649b 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -5,6 +5,7 @@ use clap::Subcommand; mod cat_file; mod hash_object; mod init; +mod show_ref; impl Command { pub fn run(self) -> anyhow::Result<()> { @@ -14,6 +15,7 @@ impl Command { Command::HashObject(args) => args.run(&mut stdout), Command::Init(args) => args.run(&mut stdout), Command::CatFile(args) => args.run(&mut stdout), + Command::ShowRef(args) => args.run(&mut stdout), } } } @@ -23,6 +25,7 @@ pub(crate) enum Command { HashObject(hash_object::HashObjectArgs), Init(init::InitArgs), CatFile(cat_file::CatFileArgs), + ShowRef(show_ref::ShowRefArgs), } pub(crate) trait CommandArgs { diff --git a/src/commands/show_ref.rs b/src/commands/show_ref.rs new file mode 100644 index 0000000..81889e7 --- /dev/null +++ b/src/commands/show_ref.rs @@ -0,0 +1,111 @@ +use std::collections::BTreeMap; +use std::fs::File; +use std::io::{Read, Write}; +use std::path::PathBuf; + +use anyhow::Context; +use clap::Args; + +use crate::commands::CommandArgs; +use crate::utils::git_dir; + +impl CommandArgs for ShowRefArgs { + fn run(self, writer: &mut W) -> anyhow::Result<()> + where + W: Write, + { + let git_dir = git_dir()?; + let ref_dir = git_dir.join("refs"); + + // use a BTreeMap to sort the entries by path + // the entries are stored as a key-value pair of the path and the hash + let mut refs = BTreeMap::::new(); + read_refs(&git_dir, ref_dir, &mut refs)?; + + let refs = refs + .into_iter() + .map(|(path, hash)| { + let mut entry = hash.to_vec(); + let path = path.to_string_lossy(); + + // format the entries as " " + entry.push(b' '); + entry.extend_from_slice(path.as_bytes()); + entry + }) + .collect::>() + .join(&b'\n'); + + writer.write_all(refs.as_slice()).context("write to stdout") + } +} + +/// Recursively read all reference files in the given directory. +/// +/// # Arguments +/// +/// * `git_dir` - The path to the git directory +/// * `ref_dir` - The path to the directory containing the references +/// * `refs` - A mutable reference to a [`BTreeMap`] to store the references +fn read_refs( + git_dir: &PathBuf, + ref_dir: PathBuf, + refs: &mut BTreeMap, +) -> anyhow::Result<()> { + let entries = std::fs::read_dir(ref_dir)?; + for entry in entries { + let ref_path = entry?.path(); + // recurse into subdirectories + if ref_path.is_dir() { + read_refs(git_dir, ref_path, refs)?; + continue; + } + + let mut file = File::open(&ref_path)?; + let mut hash = [0; 40]; + // read 40-byte hex hash + file.read_exact(&mut hash)?; + + // remove the git directory prefix from the path + let ref_path = ref_path + .strip_prefix(git_dir.as_path()) + .context("strip prefix")? + .to_path_buf(); + refs.insert(ref_path, hash); + } + Ok(()) +} + +#[derive(Args, Debug)] +pub(crate) struct ShowRefArgs { + /// show the HEAD reference, even if it would be filtered out + #[arg(long)] + head: bool, + /// only show branches (can be combined with tags) + #[arg(long)] + branches: bool, + /// only show tags (can be combined with branches) + #[arg(long)] + tags: bool, + /// stricter reference checking, requires exact ref path + #[arg(long, requires = "pattern")] + verify: bool, + /// dereference tags into object IDs + #[arg(short, long)] + dereference: bool, + /// only show SHA1 hash using digits + #[arg(short = 's', long, value_name = "n")] + hash: Option, + /// use digits to display object names + #[arg(long, value_name = "n")] + abbrev: Option, + /// do not print results to stdout (useful with --verify) + #[arg(short, long)] + quiet: bool, + /// show refs from stdin that aren't in local repository + #[arg(long, value_name = "pattern", conflicts_with = "pattern")] + exclude_existing: Option, + /// only show refs that match the given pattern + #[arg(name = "pattern", required = false)] + patterns: Vec, +} diff --git a/src/main.rs b/src/main.rs index a279165..dd36560 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +extern crate core; + mod commands; mod utils; From b8bc0e6a786682200100f8b9464dabd8133ac697 Mon Sep 17 00:00:00 2001 From: nick <59822256+Archasion@users.noreply.github.com> Date: Mon, 3 Feb 2025 11:56:54 +0000 Subject: [PATCH 2/8] refactor(main): Remove core crate --- src/main.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index dd36560..a279165 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,3 @@ -extern crate core; - mod commands; mod utils; From 04d92e968745518d740d17d28be6fec9c9c1097d Mon Sep 17 00:00:00 2001 From: nick <59822256+Archasion@users.noreply.github.com> Date: Mon, 3 Feb 2025 12:01:03 +0000 Subject: [PATCH 3/8] fix(init): Correct HEAD file path --- src/commands/init.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/init.rs b/src/commands/init.rs index 019b323..c105238 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -25,7 +25,7 @@ impl CommandArgs for InitArgs { // Create the main HEAD file. std::fs::write( - init_path.join("heads").join(&self.initial_branch), + init_path.join("HEAD"), get_head_ref_content(&self.initial_branch), )?; From 1bb102a5610ba3f3b8000fe8f13af69265816d16 Mon Sep 17 00:00:00 2001 From: nick <59822256+Archasion@users.noreply.github.com> Date: Mon, 3 Feb 2025 22:04:46 +0000 Subject: [PATCH 4/8] chore(show-ref): Implement `--head` argument --- src/commands/show_ref.rs | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/commands/show_ref.rs b/src/commands/show_ref.rs index 81889e7..b816cc6 100644 --- a/src/commands/show_ref.rs +++ b/src/commands/show_ref.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use std::fs::File; -use std::io::{Read, Write}; -use std::path::PathBuf; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::path::{Path, PathBuf}; use anyhow::Context; use clap::Args; @@ -22,6 +22,11 @@ impl CommandArgs for ShowRefArgs { let mut refs = BTreeMap::::new(); read_refs(&git_dir, ref_dir, &mut refs)?; + if self.head { + let hash = get_head_hash(&git_dir, &refs)?; + refs.insert(PathBuf::from("HEAD"), hash); + } + let refs = refs .into_iter() .map(|(path, hash)| { @@ -48,7 +53,7 @@ impl CommandArgs for ShowRefArgs { /// * `ref_dir` - The path to the directory containing the references /// * `refs` - A mutable reference to a [`BTreeMap`] to store the references fn read_refs( - git_dir: &PathBuf, + git_dir: &Path, ref_dir: PathBuf, refs: &mut BTreeMap, ) -> anyhow::Result<()> { @@ -68,7 +73,7 @@ fn read_refs( // remove the git directory prefix from the path let ref_path = ref_path - .strip_prefix(git_dir.as_path()) + .strip_prefix(git_dir) .context("strip prefix")? .to_path_buf(); refs.insert(ref_path, hash); @@ -76,6 +81,33 @@ fn read_refs( Ok(()) } +/// Get the hash of the HEAD reference. +/// +/// # Arguments +/// +/// * `git_dir` - The path to the git directory +/// * `refs` - A reference to a [`BTreeMap`] containing the references +/// +/// # Returns +/// +/// SHA1 of the HEAD reference +fn get_head_hash(git_dir: &Path, refs: &BTreeMap) -> anyhow::Result<[u8; 40]> { + // read the HEAD + let head_path = git_dir.join("HEAD"); + let mut head = File::open(head_path)?; + let mut head_path = Vec::new(); + + head.seek(SeekFrom::Start(5))?; // skip "ref: " + head.read_to_end(&mut head_path)?; + + // convert the path to a string and remove the trailing newline + let head_path = std::str::from_utf8(&head_path)?; + let head_path = PathBuf::from(head_path.trim_end()); + let head = refs.get(&head_path).expect("HEAD reference should exist"); + + Ok(*head) +} + #[derive(Args, Debug)] pub(crate) struct ShowRefArgs { /// show the HEAD reference, even if it would be filtered out From 872498a413cd460ce5d8c5a6674b42161a06c9da Mon Sep 17 00:00:00 2001 From: nick <59822256+Archasion@users.noreply.github.com> Date: Mon, 3 Feb 2025 23:52:25 +0000 Subject: [PATCH 5/8] chore(show-ref): Add functionality for flags: `--heads`, `--tags`, `--hash`, and `--abbrev` --- src/commands/show_ref.rs | 168 +++++++++++++++++++++++---------------- 1 file changed, 100 insertions(+), 68 deletions(-) diff --git a/src/commands/show_ref.rs b/src/commands/show_ref.rs index b816cc6..bcdf4f6 100644 --- a/src/commands/show_ref.rs +++ b/src/commands/show_ref.rs @@ -1,5 +1,5 @@ use std::collections::BTreeMap; -use std::fs::File; +use std::fs::{read_dir, File}; use std::io::{Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; @@ -15,97 +15,144 @@ impl CommandArgs for ShowRefArgs { W: Write, { let git_dir = git_dir()?; - let ref_dir = git_dir.join("refs"); - - // use a BTreeMap to sort the entries by path - // the entries are stored as a key-value pair of the path and the hash + // Map of ref paths to their hashes, a BTreeMap is used + // to ensure the output is sorted by the ref paths let mut refs = BTreeMap::::new(); - read_refs(&git_dir, ref_dir, &mut refs)?; + // Clamp the abbrev and hash values to be between 4 and 40 + let abbrev = self.abbrev.clamp(4, 40); + let hash_limit = self.hash.map(|n| n.clamp(4, 40)); + + // Read the refs based on the flags + if self.heads { + read_refs(&git_dir, "refs/heads", &mut refs)?; + } + if self.tags { + read_refs(&git_dir, "refs/tags", &mut refs)?; + } + if !self.heads && !self.tags { + read_refs(&git_dir, "refs/heads", &mut refs)?; + read_refs(&git_dir, "refs/tags", &mut refs)?; + read_refs(&git_dir, "refs/remotes", &mut refs)?; + add_ref_if_exists(&git_dir, "refs/stash", &mut refs)?; + } if self.head { - let hash = get_head_hash(&git_dir, &refs)?; - refs.insert(PathBuf::from("HEAD"), hash); + read_head(&git_dir, &mut refs)?; } let refs = refs .into_iter() .map(|(path, hash)| { - let mut entry = hash.to_vec(); - let path = path.to_string_lossy(); - - // format the entries as " " + // If hash_limit is set, only show the first n characters of the hash + // and nothing else + if let Some(hash_limit) = hash_limit { + return hash[0..hash_limit].to_vec(); + } + // If abbrev is set, show the first n characters of the hash + // followed by a space and the path (from refs) + let mut entry = hash[0..abbrev].to_vec(); entry.push(b' '); - entry.extend_from_slice(path.as_bytes()); + entry.extend_from_slice(path.to_string_lossy().as_bytes()); entry }) - .collect::>() + .collect::>>() .join(&b'\n'); writer.write_all(refs.as_slice()).context("write to stdout") } } -/// Recursively read all reference files in the given directory. +/// Recursively read all refs in a directory +/// and add them to the refs map. /// /// # Arguments /// -/// * `git_dir` - The path to the git directory -/// * `ref_dir` - The path to the directory containing the references -/// * `refs` - A mutable reference to a [`BTreeMap`] to store the references +/// * `git_dir` - The path to the .git directory +/// * `subdir` - The subdirectory to read refs from, relative to `git_dir` +/// * `refs` - The map to add the refs to fn read_refs( git_dir: &Path, - ref_dir: PathBuf, + subdir: &str, refs: &mut BTreeMap, ) -> anyhow::Result<()> { - let entries = std::fs::read_dir(ref_dir)?; - for entry in entries { + for entry in read_dir(git_dir.join(subdir))? { let ref_path = entry?.path(); - // recurse into subdirectories if ref_path.is_dir() { - read_refs(git_dir, ref_path, refs)?; - continue; + read_refs(git_dir, &ref_path.to_string_lossy(), refs)?; + } else { + add_ref(git_dir, &ref_path, refs)?; } + } + Ok(()) +} - let mut file = File::open(&ref_path)?; - let mut hash = [0; 40]; - // read 40-byte hex hash - file.read_exact(&mut hash)?; - - // remove the git directory prefix from the path - let ref_path = ref_path - .strip_prefix(git_dir) - .context("strip prefix")? - .to_path_buf(); - refs.insert(ref_path, hash); +/// Add a ref to the refs map if the file exists. +/// +/// # Arguments +/// +/// * `git_dir` - The path to the .git directory +/// * `sub_path` - The path to the ref file, relative to `git_dir` +/// * `refs` - The map to add the ref to +fn add_ref_if_exists( + git_dir: &Path, + sub_path: &str, + refs: &mut BTreeMap, +) -> anyhow::Result<()> { + let ref_path = git_dir.join(sub_path); + if ref_path.exists() { + add_ref(git_dir, &ref_path, refs)?; } Ok(()) } -/// Get the hash of the HEAD reference. +/// Add a ref to the refs map. /// /// # Arguments /// -/// * `git_dir` - The path to the git directory -/// * `refs` - A reference to a [`BTreeMap`] containing the references +/// * `git_dir` - The path to the .git directory +/// * `path` - The path to the ref file +/// * `refs` - The map to add the ref to +fn add_ref( + git_dir: &Path, + path: &Path, + refs: &mut BTreeMap, +) -> anyhow::Result<()> { + let mut file = File::open(path)?; + let mut hash = [0; 40]; + file.read_exact(&mut hash)?; + + let stripped_path = path.strip_prefix(git_dir)?; + refs.insert(stripped_path.to_path_buf(), hash); + Ok(()) +} + +/// Read the HEAD file and add it to the refs map. /// -/// # Returns +/// # Arguments /// -/// SHA1 of the HEAD reference -fn get_head_hash(git_dir: &Path, refs: &BTreeMap) -> anyhow::Result<[u8; 40]> { - // read the HEAD +/// * `git_dir` - The path to the .git directory +/// * `refs` - The map to add the HEAD ref to +fn read_head(git_dir: &Path, refs: &mut BTreeMap) -> anyhow::Result<()> { let head_path = git_dir.join("HEAD"); let mut head = File::open(head_path)?; let mut head_path = Vec::new(); - head.seek(SeekFrom::Start(5))?; // skip "ref: " + head.seek(SeekFrom::Start(5))?; // Skip the "ref: " prefix head.read_to_end(&mut head_path)?; + head_path.pop(); // Remove the trailing newline - // convert the path to a string and remove the trailing newline - let head_path = std::str::from_utf8(&head_path)?; - let head_path = PathBuf::from(head_path.trim_end()); - let head = refs.get(&head_path).expect("HEAD reference should exist"); + let head_path = PathBuf::from(std::str::from_utf8(&head_path)?); + // If refs/heads was read, we don't need to re-read the HEAD file + if let Some(&hash) = refs.get(&head_path) { + refs.insert(PathBuf::from("HEAD"), hash); + return Ok(()); + } - Ok(*head) + let mut head = File::open(head_path)?; + let mut hash = [0; 40]; + head.read_exact(&mut hash)?; + refs.insert(PathBuf::from("HEAD"), hash); + Ok(()) } #[derive(Args, Debug)] @@ -113,31 +160,16 @@ pub(crate) struct ShowRefArgs { /// show the HEAD reference, even if it would be filtered out #[arg(long)] head: bool, - /// only show branches (can be combined with tags) + /// only show heads (can be combined with tags) #[arg(long)] - branches: bool, - /// only show tags (can be combined with branches) + heads: bool, + /// only show tags (can be combined with heads) #[arg(long)] tags: bool, - /// stricter reference checking, requires exact ref path - #[arg(long, requires = "pattern")] - verify: bool, - /// dereference tags into object IDs - #[arg(short, long)] - dereference: bool, /// only show SHA1 hash using digits #[arg(short = 's', long, value_name = "n")] hash: Option, /// use digits to display object names - #[arg(long, value_name = "n")] - abbrev: Option, - /// do not print results to stdout (useful with --verify) - #[arg(short, long)] - quiet: bool, - /// show refs from stdin that aren't in local repository - #[arg(long, value_name = "pattern", conflicts_with = "pattern")] - exclude_existing: Option, - /// only show refs that match the given pattern - #[arg(name = "pattern", required = false)] - patterns: Vec, + #[arg(long, value_name = "n", default_value = "40")] + abbrev: usize, } From 77e442f45454c5acfaff29a68882e81bc4f10632 Mon Sep 17 00:00:00 2001 From: nick <59822256+Archasion@users.noreply.github.com> Date: Mon, 3 Feb 2025 23:55:02 +0000 Subject: [PATCH 6/8] fix(show-ref): Use correct path to HEAD ref --- src/commands/show_ref.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/show_ref.rs b/src/commands/show_ref.rs index bcdf4f6..198d59a 100644 --- a/src/commands/show_ref.rs +++ b/src/commands/show_ref.rs @@ -148,7 +148,7 @@ fn read_head(git_dir: &Path, refs: &mut BTreeMap) -> anyhow:: return Ok(()); } - let mut head = File::open(head_path)?; + let mut head = File::open(git_dir.join(head_path))?; let mut hash = [0; 40]; head.read_exact(&mut hash)?; refs.insert(PathBuf::from("HEAD"), hash); From 8b25a5f1eb056cf131572b8a04397bda96ae9c92 Mon Sep 17 00:00:00 2001 From: nick <59822256+Archasion@users.noreply.github.com> Date: Mon, 3 Feb 2025 23:58:31 +0000 Subject: [PATCH 7/8] docs(show-ref): Document subcommand in README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index bc3d8e9..2aa4c83 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,12 @@ This is a simple attempt at re-creating some of the functionality of the `git` c - `-p` flag to show the content of the object (pretty-print) - `--allow-unknown-type` flag to allow unknown object types (to be used with `-t` or `-s`). - `` argument to specify the object to show. +- `show-ref` - List references in a local repository. + - `--head` flag to include the HEAD reference. + - `--tags` flag to show only tags. + - `--heads` flag to show only heads. + - `--hash=` flag to only show the reference hashes (`n` is the number of characters to show, 4-40). + - `--abbrev=` flag to abbreviate the hashes to `n` characters (4-40) ## Testing From 54cb09a9e335a5ce63e6cb8aa39bdfc866b2f77f Mon Sep 17 00:00:00 2001 From: nick <59822256+Archasion@users.noreply.github.com> Date: Thu, 6 Feb 2025 10:44:36 +0000 Subject: [PATCH 8/8] test(show-ref): Add tests for the `show-ref` subcommand --- src/commands/show_ref.rs | 593 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 592 insertions(+), 1 deletion(-) diff --git a/src/commands/show_ref.rs b/src/commands/show_ref.rs index 198d59a..dfb9353 100644 --- a/src/commands/show_ref.rs +++ b/src/commands/show_ref.rs @@ -75,7 +75,13 @@ fn read_refs( subdir: &str, refs: &mut BTreeMap, ) -> anyhow::Result<()> { - for entry in read_dir(git_dir.join(subdir))? { + let subdir_path = git_dir.join(subdir); + + if !subdir_path.exists() { + return Ok(()); + } + + for entry in read_dir(subdir_path)? { let ref_path = entry?.path(); if ref_path.is_dir() { read_refs(git_dir, &ref_path.to_string_lossy(), refs)?; @@ -173,3 +179,588 @@ pub(crate) struct ShowRefArgs { #[arg(long, value_name = "n", default_value = "40")] abbrev: usize, } + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::env; + use crate::utils::test::{TempEnv, TempPwd}; + + const HEAD_HASH: &str = "aabbccddeeff00112233445566778899aabbccdd"; + const HEAD_NAME: &str = "main"; + const TAG_HASH: &str = "112233445566778899aabbccddeeff0011223344"; + const TAG_NAME: &str = "v1.0"; + const REMOTE_HASH: &str = "33445566778899aabbccddeeff00112233445566"; + const REMOTE_NAME: &str = "origin"; + const STASH_HASH: &str = "5566778899aabbccddeeff001122334455667788"; + + // Head can be excluded from the enum as it must always be present + struct Ref { + dir: &'static str, + name: &'static str, + hash: &'static [u8], + } + + /// Create a temporary `.git/refs` directory with refs of the specified types. + /// + /// The `stash` and `HEAD` refs are always created. + fn create_temp_refs(refs: [Ref; N]) -> TempPwd { + let _env = TempEnv::new(env::GIT_DIR, None); + let temp_pwd = TempPwd::new(); + let git_dir = temp_pwd.path().join(".git"); + let refs_dir = git_dir.join("refs"); + + std::fs::create_dir_all(&refs_dir).unwrap(); + + for Ref { dir, name, hash } in refs { + let ref_dir = refs_dir.join(dir); + std::fs::create_dir(&ref_dir).unwrap(); + let ref_file = ref_dir.join(name); + std::fs::write(&ref_file, hash).unwrap(); + } + + // Store the HEAD ref in /refs/heads + let heads_dir = refs_dir.join("heads"); + std::fs::create_dir(&heads_dir).unwrap(); + let head_file = heads_dir.join(HEAD_NAME); + std::fs::write(&head_file, HEAD_HASH).unwrap(); + + // Create a HEAD file that points to the main branch + let head_file = git_dir.join("HEAD"); + std::fs::write(&head_file, format!("ref: refs/heads/{}\n", HEAD_NAME)).unwrap(); + + // Create a stash file + let stash_file = refs_dir.join("stash"); + std::fs::write(&stash_file, STASH_HASH).unwrap(); + + temp_pwd + } + + #[test] + fn show_refs() { + let _pwd = create_temp_refs([ + Ref { + dir: "tags", + name: TAG_NAME, + hash: TAG_HASH.as_bytes(), + }, + Ref { + dir: "remotes", + name: REMOTE_NAME, + hash: REMOTE_HASH.as_bytes(), + }, + ]); + + let args = ShowRefArgs { + head: false, + heads: false, + tags: false, + hash: None, + abbrev: 40, + }; + + let mut output = Vec::new(); + let result = args.run(&mut output); + let expected = format!( + "{HEAD_HASH} refs/heads/{HEAD_NAME}\n\ + {REMOTE_HASH} refs/remotes/{REMOTE_NAME}\n\ + {STASH_HASH} refs/stash\n\ + {TAG_HASH} refs/tags/{TAG_NAME}", + ) + .into_bytes(); + + assert!(result.is_ok()); + assert_eq!(output, expected); + } + + #[test] + fn show_refs_with_head() { + let _pwd = create_temp_refs([ + Ref { + dir: "tags", + name: TAG_NAME, + hash: TAG_HASH.as_bytes(), + }, + Ref { + dir: "remotes", + name: REMOTE_NAME, + hash: REMOTE_HASH.as_bytes(), + }, + ]); + + let args = ShowRefArgs { + head: true, + heads: false, + tags: false, + hash: None, + abbrev: 40, + }; + + let mut output = Vec::new(); + let result = args.run(&mut output); + let expected = format!( + "{HEAD_HASH} HEAD\n\ + {HEAD_HASH} refs/heads/{HEAD_NAME}\n\ + {REMOTE_HASH} refs/remotes/{REMOTE_NAME}\n\ + {STASH_HASH} refs/stash\n\ + {TAG_HASH} refs/tags/{TAG_NAME}", + ) + .into_bytes(); + + assert!(result.is_ok()); + assert_eq!(output, expected); + } + + #[test] + fn show_head_refs() { + let _pwd = create_temp_refs([ + Ref { + dir: "tags", + name: TAG_NAME, + hash: TAG_HASH.as_bytes(), + }, + Ref { + dir: "remotes", + name: REMOTE_NAME, + hash: REMOTE_HASH.as_bytes(), + }, + ]); + + let args = ShowRefArgs { + head: false, + heads: true, + tags: false, + hash: None, + abbrev: 40, + }; + + let mut output = Vec::new(); + let result = args.run(&mut output); + let expected = format!("{HEAD_HASH} refs/heads/{HEAD_NAME}"); + + assert!(result.is_ok()); + assert_eq!(output, expected.into_bytes()); + } + + #[test] + fn show_tag_refs() { + let _pwd = create_temp_refs([ + Ref { + dir: "tags", + name: TAG_NAME, + hash: TAG_HASH.as_bytes(), + }, + Ref { + dir: "remotes", + name: REMOTE_NAME, + hash: REMOTE_HASH.as_bytes(), + }, + ]); + + let args = ShowRefArgs { + head: false, + heads: false, + tags: true, + hash: None, + abbrev: 40, + }; + + let mut output = Vec::new(); + let result = args.run(&mut output); + let expected = format!("{TAG_HASH} refs/tags/{TAG_NAME}"); + + assert!(result.is_ok()); + assert_eq!(output, expected.into_bytes()); + } + + #[test] + fn show_tag_and_head_refs() { + let _pwd = create_temp_refs([ + Ref { + dir: "tags", + name: TAG_NAME, + hash: TAG_HASH.as_bytes(), + }, + Ref { + dir: "remotes", + name: REMOTE_NAME, + hash: REMOTE_HASH.as_bytes(), + }, + ]); + + let args = ShowRefArgs { + head: false, + heads: true, + tags: true, + hash: None, + abbrev: 40, + }; + + let mut output = Vec::new(); + let result = args.run(&mut output); + let expected = format!( + "{HEAD_HASH} refs/heads/{HEAD_NAME}\n\ + {TAG_HASH} refs/tags/{TAG_NAME}", + ); + + assert!(result.is_ok()); + assert_eq!(output, expected.into_bytes()); + } + + #[test] + fn show_tag_and_head_refs_with_head() { + let _pwd = create_temp_refs([ + Ref { + dir: "tags", + name: TAG_NAME, + hash: TAG_HASH.as_bytes(), + }, + Ref { + dir: "remotes", + name: REMOTE_NAME, + hash: REMOTE_HASH.as_bytes(), + }, + ]); + + let args = ShowRefArgs { + head: true, + heads: true, + tags: true, + hash: None, + abbrev: 40, + }; + + let mut output = Vec::new(); + let result = args.run(&mut output); + let expected = format!( + "{HEAD_HASH} HEAD\n\ + {HEAD_HASH} refs/heads/{HEAD_NAME}\n\ + {TAG_HASH} refs/tags/{TAG_NAME}", + ); + + assert!(result.is_ok()); + assert_eq!(output, expected.into_bytes()); + } + + #[test] + fn show_tag_refs_with_head() { + let _pwd = create_temp_refs([ + Ref { + dir: "tags", + name: TAG_NAME, + hash: TAG_HASH.as_bytes(), + }, + Ref { + dir: "remotes", + name: REMOTE_NAME, + hash: REMOTE_HASH.as_bytes(), + }, + ]); + + let args = ShowRefArgs { + head: true, + heads: false, + tags: true, + hash: None, + abbrev: 40, + }; + + let mut output = Vec::new(); + let result = args.run(&mut output); + let expected = format!( + "{HEAD_HASH} HEAD\n\ + {TAG_HASH} refs/tags/{TAG_NAME}", + ); + + assert!(result.is_ok()); + assert_eq!(output, expected.into_bytes()); + } + + #[test] + fn show_no_tag_refs() { + let _pwd = create_temp_refs([]); + let args = ShowRefArgs { + head: false, + heads: false, + tags: true, + hash: None, + abbrev: 40, + }; + + let mut output = Vec::new(); + let result = args.run(&mut output); + + assert!(result.is_ok()); + assert_eq!(output, Vec::new()); + } + + #[test] + fn abbreviate_ref_hashes() { + let _pwd = create_temp_refs([ + Ref { + dir: "tags", + name: TAG_NAME, + hash: TAG_HASH.as_bytes(), + }, + Ref { + dir: "remotes", + name: REMOTE_NAME, + hash: REMOTE_HASH.as_bytes(), + }, + ]); + + let args = ShowRefArgs { + head: false, + heads: false, + tags: false, + hash: None, + abbrev: 8, + }; + + let mut output = Vec::new(); + let result = args.run(&mut output); + let expected = format!( + "{} refs/heads/{HEAD_NAME}\n\ + {} refs/remotes/{REMOTE_NAME}\n\ + {} refs/stash\n\ + {} refs/tags/{TAG_NAME}", + &HEAD_HASH[0..8], + &REMOTE_HASH[0..8], + &STASH_HASH[0..8], + &TAG_HASH[0..8], + ) + .into_bytes(); + + assert!(result.is_ok()); + assert_eq!(output, expected); + } + + #[test] + fn abbreviate_ref_hashes_below_min() { + let _pwd = create_temp_refs([ + Ref { + dir: "tags", + name: TAG_NAME, + hash: TAG_HASH.as_bytes(), + }, + Ref { + dir: "remotes", + name: REMOTE_NAME, + hash: REMOTE_HASH.as_bytes(), + }, + ]); + + let args = ShowRefArgs { + head: false, + heads: false, + tags: false, + hash: None, + abbrev: 2, + }; + + let mut output = Vec::new(); + let result = args.run(&mut output); + let expected = format!( + "{} refs/heads/{HEAD_NAME}\n\ + {} refs/remotes/{REMOTE_NAME}\n\ + {} refs/stash\n\ + {} refs/tags/{TAG_NAME}", + &HEAD_HASH[0..4], + &REMOTE_HASH[0..4], + &STASH_HASH[0..4], + &TAG_HASH[0..4], + ) + .into_bytes(); + + assert!(result.is_ok()); + assert_eq!(output, expected); + } + + #[test] + fn abbreviate_ref_hashes_above_max() { + let _pwd = create_temp_refs([ + Ref { + dir: "tags", + name: TAG_NAME, + hash: TAG_HASH.as_bytes(), + }, + Ref { + dir: "remotes", + name: REMOTE_NAME, + hash: REMOTE_HASH.as_bytes(), + }, + ]); + + let args = ShowRefArgs { + head: false, + heads: false, + tags: false, + hash: None, + abbrev: 50, + }; + + let mut output = Vec::new(); + let result = args.run(&mut output); + let expected = format!( + "{} refs/heads/{HEAD_NAME}\n\ + {} refs/remotes/{REMOTE_NAME}\n\ + {} refs/stash\n\ + {} refs/tags/{TAG_NAME}", + &HEAD_HASH, &REMOTE_HASH, &STASH_HASH, &TAG_HASH, + ) + .into_bytes(); + + assert!(result.is_ok()); + assert_eq!(output, expected); + } + + #[test] + fn show_hashes_with_limit() { + let _pwd = create_temp_refs([ + Ref { + dir: "tags", + name: TAG_NAME, + hash: TAG_HASH.as_bytes(), + }, + Ref { + dir: "remotes", + name: REMOTE_NAME, + hash: REMOTE_HASH.as_bytes(), + }, + ]); + + let args = ShowRefArgs { + head: false, + heads: false, + tags: false, + hash: Some(8), + abbrev: 40, + }; + + let mut output = Vec::new(); + let result = args.run(&mut output); + let expected = format!( + "{}\n{}\n{}\n{}", + &HEAD_HASH[0..8], + &REMOTE_HASH[0..8], + &STASH_HASH[0..8], + &TAG_HASH[0..8], + ) + .into_bytes(); + + assert!(result.is_ok()); + assert_eq!(output, expected); + } + + #[test] + fn show_hashes_with_limit_below_min() { + let _pwd = create_temp_refs([ + Ref { + dir: "tags", + name: TAG_NAME, + hash: TAG_HASH.as_bytes(), + }, + Ref { + dir: "remotes", + name: REMOTE_NAME, + hash: REMOTE_HASH.as_bytes(), + }, + ]); + + let args = ShowRefArgs { + head: false, + heads: false, + tags: false, + hash: Some(2), + abbrev: 40, + }; + + let mut output = Vec::new(); + let result = args.run(&mut output); + let expected = format!( + "{}\n{}\n{}\n{}", + &HEAD_HASH[0..4], + &REMOTE_HASH[0..4], + &STASH_HASH[0..4], + &TAG_HASH[0..4], + ) + .into_bytes(); + + assert!(result.is_ok()); + assert_eq!(output, expected); + } + + #[test] + fn show_hashes_with_limit_above_max() { + let _pwd = create_temp_refs([ + Ref { + dir: "tags", + name: TAG_NAME, + hash: TAG_HASH.as_bytes(), + }, + Ref { + dir: "remotes", + name: REMOTE_NAME, + hash: REMOTE_HASH.as_bytes(), + }, + ]); + + let args = ShowRefArgs { + head: false, + heads: false, + tags: false, + hash: Some(50), + abbrev: 40, + }; + + let mut output = Vec::new(); + let result = args.run(&mut output); + let expected = format!( + "{}\n{}\n{}\n{}", + &HEAD_HASH, &REMOTE_HASH, &STASH_HASH, &TAG_HASH, + ) + .into_bytes(); + + assert!(result.is_ok()); + assert_eq!(output, expected); + } + + #[test] + fn allow_invalid_head_path_without_head_arg() { + let pwd = create_temp_refs([]); + let head_file = pwd.path().join(".git/HEAD"); + // Overwrite the HEAD file with an invalid path + std::fs::write(&head_file, "ref: refs/heads/invalid\n").unwrap(); + + let args = ShowRefArgs { + head: false, + heads: false, + tags: false, + hash: None, + abbrev: 40, + }; + + let mut output = Vec::new(); + let result = args.run(&mut output); + assert!(result.is_ok()); + } + + #[test] + fn fail_on_invalid_head_path() { + let pwd = create_temp_refs([]); + let head_file = pwd.path().join(".git/HEAD"); + // Overwrite the HEAD file with an invalid path + std::fs::write(&head_file, "ref: refs/heads/invalid\n").unwrap(); + + let args = ShowRefArgs { + head: true, + heads: false, + tags: false, + hash: None, + abbrev: 40, + }; + + let result = args.run(&mut Vec::new()); + assert!(result.is_err()); + } +}