Implement tag command

Update cli module

src/cli.rs

#[derive(Debug)]
pub enum TagSubCommand {
    ListTags,
    CreateTagObject { name: String, object: String },
    CreateLightweightTag { name: String, object: String },
}
src/cli.rs
#[derive(Debug)]
pub enum Command {
    ...
    Tag {
        command: TagSubCommand,
    },
}
        .subcommand(
            ClapCommand::new("tag")
                .about("List and create tags")
                .arg(
                    Arg::new("name")
                        .value_name("NAME")
                        .help("The new tag's name"),
                )
                .arg(
                    Arg::new("object")
                        .value_name("OBJECT")
                        .help("The object the new tag will point to"),
                )
                .arg(
                    Arg::new("tag_object")
                        .short('a')
                        .long("add-tag-object")
                        .requires("name")
                        .help("Whether to create a tag object")
                        .action(ArgAction::SetTrue),
                ),
        )
  } else if let Some(subcommand) = matches.subcommand_matches("tag") {
        let name = subcommand.get_one::<String>("name");
        let object = subcommand.get_one::<String>("object");
        let add_tag_object = subcommand.get_flag("tag_object");
        let add_lightweight_tag = add_tag_object == false && name.is_some();

        if add_tag_object {
            Ok(Command::Tag {
                command: TagSubCommand::CreateTagObject {
                    name: name.unwrap().clone(), // Safe to call unwrap, we specified that if -a presents, name must too.
                    object: object.map(|val| val.clone()).unwrap_or("HEAD".to_string()),
                },
            })
        } else if add_lightweight_tag {
            Ok(Command::Tag {
                command: TagSubCommand::CreateLightweightTag {
                    name: name.unwrap().clone(), // Safe to call unwrap, add_lightweight_tag has a check for presence of name
                    object: object.map(|val| val.clone()).unwrap_or("HEAD".to_string()),
                },
            })
        } else {
            Ok(Command::Tag {
                command: TagSubCommand::ListTags,
            })
        }
    } else {

Update Main

Let's move all cmd_ functions to a new module

src/lib.rs
pub mod cli;
pub mod directory_manager;
pub mod error;
pub mod executer;
pub mod git_config;
pub mod git_object;
pub mod repository;

pub use cli::*;
pub use directory_manager::DirectoryManager;
pub use git_object::GitObject;

the Move all cmd_ functions from src/main.rs to src/executer.rs. After that, the main will be like this:

src/main.rs
fn main() -> Result<()> {
    let command = parse_args()?;
    match command {
        Command::Init { path } => cmd_init(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(),
        Command::Tag { command } => cmd_tag(command),
    }

Add cmd_tag to executer module

fn find_repo_in_current_directory() -> Result<GitRepository, anyhow::Error> {
    let current_directory = std::env::current_dir()?;
    GitRepository::find(&current_directory).context("Failed to create the repo")
}

pub fn cmd_tag(command: TagSubCommand) -> Result<()> {
    let repo = find_repo_in_current_directory()?;
    match command {
        TagSubCommand::ListTags => {
            let tags = repo.list_refs_in(&PathBuf::from("tags"))?;
            for tag in tags {
                println!("{}", tag);
            }
        }
        TagSubCommand::CreateTagObject { name, object } => repo.create_tag_object(name, object)?,
        TagSubCommand::CreateLightweightTag { name, object } => {
            repo.create_lightweight_tag(name, object)?
        }
    };

    Ok(())
}

Update repository module

Add list_refs_in

This function returns all refs in a sub-folder of refs folder, specified using the passed argument

   pub fn list_refs(&self) -> Result<Vec<refs::Ref>, ResolveRefError> {
        self.list_refs_in_absolute(&self.directory_manager.refs_path)
    }

    pub fn list_refs_in(&self, path: &Path) -> Result<Vec<refs::Ref>, ResolveRefError> {
        self.list_refs_in_absolute(&self.directory_manager.refs_path.join(path))
    }

    pub fn list_refs_in_absolute(&self, path: &Path) -> Result<Vec<refs::Ref>, ResolveRefError> {
        let refs = refs::list_refs(&self.directory_manager.dot_git_path, 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()
    }
}
// Tag methods
impl GitRepository {
    pub fn create_lightweight_tag(
        &self,
        name: String,
        object: String,
    ) -> Result<(), anyhow::Error> {
        let object = self.find_object(git_object::Type::Tag, object)?;
        std::fs::write(self.directory_manager.refs_tags_path.join(name), object)?;

        Ok(())
    }

    pub fn create_tag_object(&self, name: String, object: String) -> Result<(), anyhow::Error> {
        let object = self.find_object(git_object::Type::Tag, object)?;
        let kvl = BTreeMap::from([
            ("object".to_string(), object),
            ("type".to_string(), "commit".to_string()),
            ("tag".to_string(), name.clone()),
            (
                "tagger".to_string(),
                "Saeed <saeed@zilliqa.com>".to_string(),
            ),
            ("message".to_string(), "This is the message".to_string()),
        ]);

        let tag = Tag {
            kvl: KeyValueList::new(kvl),
        };

        let serialized = SerializedGitObject::try_from(GitObject::Tag(tag))?;

        self.write_object(&serialized)?;
        std::fs::write(
            self.directory_manager.refs_tags_path.join(name),
            serialized.hash,
        )?;
        Ok(())
    }
}

Refactor create_object and write_object

Remove both functions and add this:

src/repository.rs
    pub fn create_object(
        file_path: &Path,
        object_type: Type,
    ) -> Result<SerializedGitObject, ObjectCreateError> {
        let mut buf_reader = BufReader::new(File::open(file_path)?);
        let mut buffer = String::new();
        buf_reader.read_to_string(&mut buffer)?;

        let object = match object_type {
            Type::Commit => todo!(),
            Type::Tree => todo!(),
            Type::Tag => todo!(),
            Type::Blob => GitObject::Blob(Blob { blob: buffer }),
        };

        object.try_into()
    }

    pub fn write_object(
        &self,
        serialized_object: &SerializedGitObject,
    ) -> Result<(), anyhow::Error> {
        let file_path = self
            .directory_manager
            .sha_to_file_path(&serialized_object.hash, true)?;

        std::fs::write(file_path, CompressedGitObject::try_from(serialized_object)?)?;

        Ok(())
    }

To make the compiler happy, remove the serialize function from SerializedGitObject and add this function:

src/git_object/serialized.rs
impl TryFrom<GitObject> for SerializedGitObject {
    type Error = ObjectCreateError;

    fn try_from(value: GitObject) -> Result<Self, Self::Error> {
        let (serialized_object, object_type) = match value {
            GitObject::Commit(commit) => (commit.serialize(), git_object::Type::Commit),
            GitObject::Blob(blob) => (blob.serialize(), git_object::Type::Blob),
            GitObject::Tag(tag) => (tag.serialize(), git_object::Type::Tag),
            GitObject::Tree(tree) => (tree.serialize(), git_object::Type::Tree),
        };

        let buffer = Vec::<u8>::new();
        let mut buf_writer = BufWriter::new(buffer);

        write!(
            buf_writer,
            "{}{}",
            Header::new(object_type, serialized_object.len()),
            serialized_object
        )?;

        buf_writer.flush()?;
        let buffer = buf_writer
            .into_inner()
            .context("Failed to take buffer out of buf writer")?;

        Ok(SerializedGitObject::new(buffer))
    }
}

Now cmd_cat_file will be:

src/executer.rs
pub fn cmd_hash_object(file_path: &Path, object_type: git_object::Type, write: bool) -> Result<()> {
    let current_directory = std::env::current_dir()?;
    let repo = GitRepository::find(&current_directory)?;
    let object = GitRepository::create_object(file_path, object_type)?;
    if write {
        repo.write_object(&object)?;
    }

    println!("{}", object.hash);

    Ok(())
}

Add tag object

Tag object is completely like commit, so it can be defined like:

src/git_object/tag.rs
// Tag objects are like commits
pub type Tag = super::Commit;

Last but not least

src/git_object/mod.rs
impl GitObject {
    pub fn serialize(&self) -> String {
        match self {
            GitObject::Commit(commit) => commit.serialize(),
            GitObject::Blob(blob) => blob.serialize(),
            GitObject::Tag(tag) => tag.serialize(),
            GitObject::Tree(tree) => tree.serialize(),
        }
    }

    fn deserialize(
        buf_reader: &mut impl std::io::BufRead,
        object_header: Header,
    ) -> Result<Self, ObjectParseError> {
        match object_header.object_type {
            Type::Commit => Ok(Self::Commit(Commit::deserialize(
                buf_reader,
                object_header,
            )?)),
            Type::Tree => Ok(Self::Tree(Tree::deserialize(buf_reader, object_header)?)),
            Type::Tag => Ok(Self::Tag(Tag::deserialize(buf_reader, object_header)?)),
            Type::Blob => Ok(Self::Blob(Blob::deserialize(buf_reader, object_header)?)),
        }
    }
}

Last updated