Git Implementation in Rust

Since all three types of objects have some common behaviour that only differs in its implementation, we can easily define a trait Object that defines each of these methods.

#[async_trait]
pub trait Object {
    async fn from_object_sha(object_sha: String) -> Result<Self>
    where
        Self: Sized;

    fn sha1_hash(&self) -> [u8; 20];

    fn write_data(&self) -> &Vec<u8>;

    async fn write(&self) -> Result<PathBuf> {
        let mut path = PathBuf::from(".git/objects");

        let blob_hex = hex::encode(self.sha1_hash());
        let (dirname, filename) = blob_hex.split_at(2);

        path.push(dirname);

        fs::create_dir_all(&path).await?;
        path.push(filename);

        let encoded_content = utils::zlib_compress(&self.write_data())?;

        fs::write(&path, encoded_content).await?;

        Ok(path)
    }

    fn encoded_hash(&self) -> String {
        hex::encode(&self.sha1_hash())
    }
}

The from_object_sha, sha1_hash, and the write_data functions have a type specific implementation while the write and the encoded_hash functions have a generic implementation that is defined here.

The #[async_trait] macro is needed because Rust currently doesn’t natively support asynchronous traits. To add asynchronous functions in the Object trait, we have used the async-trait crate.

Now that we have a generic Object type, we can implement the Object trait for each of Blob, Tree, and Commit. Let’s see the impl blocks for the Blob type.

    impl Blob {
        pub async fn new(file: PathBuf) -> Result<Self> {
        // -- snip --
        }
    }

    #[async_trait]
    impl Object for Blob {
        async fn from_object_sha(object_sha: String) -> Result<Self> {
        // -- snip --
        }

        fn sha1_hash(&self) -> [u8; 20] {
            let mut hash: [u8; 20] = [0; 20];
            hash.copy_from_slice(&self.sha1_hash);

            hash
        }

        fn write_data(&self) -> &Vec<u8> {
            &self.write_data
        }
    }



    impl Display for Blob {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            let out = String::from_utf8_lossy(&self.contents);

            f.write_fmt(format_args!("{}", out))
        }
    }

Now that we have a Blob type, we can use it in commands so that the git cat-file and the git hash-object commands can be implemented like this:

pub async fn cat_file(pretty_print: bool, object_sha: String) -> Result<()> {
    let blob = Blob::from_object_sha(object_sha).await?;

    if pretty_print {
        print!("{}", blob);
    }

    Ok(())
}

pub async fn hash_object(file: PathBuf, write: bool) -> Result<()> {
    let blob = Blob::new(file).await?;

    if write {
        blob.write().await?;
    }
    print!("{}", blob.encoded_hash());

    Ok(())
}

The implementation of the Tree and the Commit types is on similar lines.

Next

Move on to [[git/error-handling]] to read about convenient error handling in Rust.