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.