Implement show-ref command

What a ref is, and the show-ref command

original-source

Update cli module

src/cli.rs
#[derive(Debug)]
pub enum Command {
    // ...
    ShowRef,
}
src/cli.rs
        .subcommand(ClapCommand::new("show-ref").about("List references."))
src/cli.rs
} else if let Some(_) = matches.subcommand_matches("show-ref") {
        Ok(Command::ShowRef)
    } else {

Update directory_manager

Let's add a PathBuf to DirectoryManager for the refs folder:

src/directory_manager.rs
#[derive(Debug, Clone)]
pub struct DirectoryManager {
    pub work_tree: PathBuf,
    pub dot_git_path: PathBuf,
    pub config_file: PathBuf,
    pub description_file: PathBuf,
    pub head_file: PathBuf,
    pub branches_path: PathBuf,
    pub objects_path: PathBuf,
    pub refs_path: PathBuf,
    pub refs_tags_path: PathBuf,
    pub refs_heads_path: PathBuf,
}
src/directory_manager.rs
    pub fn new<T: Into<PathBuf>>(base_path: T) -> Self {
        let base_path: PathBuf = base_path.into();
        let dot_git_path = base_path.join(".git");

        Self {
            work_tree: base_path,
            config_file: dot_git_path.join("config"),
            description_file: dot_git_path.join("description"),
            head_file: dot_git_path.join("HEAD"),
            branches_path: dot_git_path.join("branches"),
            objects_path: dot_git_path.join("objects"),
            refs_path: dot_git_path.join("refs"),
            refs_tags_path: dot_git_path.join("refs").join("tags"),
            refs_heads_path: dot_git_path.join("refs").join("heads"),
            dot_git_path,
        }
    }

Move repository module to a separate folder

mkdir src/repository
cp src/repository.rs src/repository/

Add new module to repository module

Add a new file, src/repository/refs.rs

This file is going to have our functions and structs to help us deal with refs:

src/repository/refs.rs
#[derive(Debug)]
pub struct Ref {
    pub hash: String,
    pub path: PathBuf,
}

Add resolve_ref function to refs module

getsThis function gets the path to .git folder and and a path to a ref and tries to resolve it to a hash:

src/repository/refs.rs
pub fn resolve_ref(dot_git_path: &Path, ref_path: &Path) -> Result<String, ResolveRefError> {
    if ref_path.is_file() == false {
        return Err(ResolveRefError::RelativePathIsNotAFile(format!(
            "{}",
            ref_path.display()
        )));
    }

    let ref_value = fs::read_to_string(ref_path)?;
    let ref_value = ref_value.trim_end();

    if ref_value.starts_with("ref: ") {
        return resolve_ref(
            dot_git_path,
                                                // Skip ref:
            &dot_git_path.join(&PathBuf::from(&ref_value[5..])),
        );
    }

    Ok(ref_value.to_string())

If the given file contains a hash, it returns it, otherwise, it recursively tries to resolve it again.

Add list_refs function

Given a path(directory), this function list all of the refs in the folder and all of its sub-directories.

src/repository/refs.rs
pub fn list_refs(dot_git_path: &Path, path: &Path) -> Result<Vec<Ref>, ResolveRefError> {
    let mut refs = vec![];

    for entry in std::fs::read_dir(path)? {
        let entry = entry?;
        let entry_path = entry.path();
        if entry_path.is_dir() {
            let out = list_refs(dot_git_path, &entry_path)?;
            refs.extend(out);
        } else {
            let hash = resolve_ref(dot_git_path, &entry_path)?;
            refs.push(Ref {
                hash,
                path: entry_path,
            });
        }
    }

    Ok(refs)
}
src/error/repository.rs
#[derive(Debug, Error)]
pub enum ResolveRefError {
    #[error("Relative path {0} is not a file")]
    RelativePathIsNotAFile(String),

    #[error(transparent)]
    IoError(#[from] std::io::Error),

    #[error(transparent)]
    UnexpectedError(#[from] anyhow::Error),
}

Add resolve_ref and list_refs to GitRepository

The resolve_ref function is simple. We just call refs::resolve_ref function with appropriate arguments.

src/repository/mod.rs
    pub fn resolve_ref(&self, ref_relative_path: &str) -> Result<String, ResolveRefError> {
        let ref_path = self.directory_manager.dot_git_path.join(ref_relative_path);
        refs::resolve_ref(&self.directory_manager.dot_git_path, &ref_path)
    }

In list_refs function, we do one more step. Because the git itself prints the ref paths in the relative manner, we convert the results to relative ones.

src/repository/mod.rs
    pub fn list_refs(&self) -> Result<Vec<refs::Ref>, ResolveRefError> {
        let refs = refs::list_refs(
            &self.directory_manager.dot_git_path,
            &self.directory_manager.refs_path,
        )?;
        refs.into_iter()
            .map(|ref_item| {
                Ok(refs::Ref {
                    hash: ref_item.hash,
                    path: ref_item
                        .path
                        .strip_prefix(&self.directory_manager.dot_git_path)
                        .context("Failed to strip_prefix")?
                        .to_path_buf(),
                })
            })
            .collect()
    }

I also added this function to GitRepository for convenience.

    pub fn new(config: GitConfig, directory_manager: DirectoryManager) -> Self {
        Self {
            config,
            directory_manager,
        }
    }

Update main.rs

src/main.rs
fn main() -> Result<()> {
    let command = parse_args()?;
    match command {
        Command::Init { path } => {
            GitRepository::create(path)?;
        }
        Command::CatFile {
            object_type,
            object_hash,
        } => cmd_cat_file(object_type, object_hash)?,
        Command::HashObject {
            object_type,
            file_path,
            write,
        } => cmd_hash_object(&file_path, object_type, write)?,
        Command::Log { commit, n_logs } => cmd_log(commit, n_logs)?,
        Command::LsTree { recursive, tree } => cmd_ls_tree(&tree, recursive, PathBuf::new())?,
        Command::Checkout { commit, path } => cmd_checkout(commit, PathBuf::from(path))?,
        Command::ShowRef => cmd_show_ref()?,
    };

    Ok(())
}
src/main.rs

fn cmd_show_ref() -> Result<()> {
    let current_directory = std::env::current_dir()?;
    let repo = GitRepository::find(&current_directory)?;
    let refs = repo.list_refs()?;
    for ref_item in refs {
        println!("{}", ref_item);
    }

    Ok(())
}

Implement Display for Ref to be able to write line 7 in the previous code:

src/repository/refs.rs
impl Display for Ref {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} {}", self.hash, self.path.display())
    }
}

Last updated