use std::{
path::{Path, PathBuf},
sync::Arc,
};
use anyhow::{anyhow, Error, Result};
use git2::{Oid, Signature};
use parking_lot::Mutex;
use crate::{commit_diff_loader::CommitDiffLoader, Commit, CommitDiff, CommitDiffLoaderOptions, Config, Reference};
#[derive(Clone)]
pub struct Repository {
repository: Arc<Mutex<git2::Repository>>,
}
impl Repository {
#[inline]
pub fn open_from_env() -> Result<Self> {
let repository = git2::Repository::open_from_env()
.map_err(|e| anyhow!(String::from(e.message())).context("Could not open repository from environment"))?;
Ok(Self {
repository: Arc::new(Mutex::new(repository)),
})
}
#[inline]
pub fn open_from_path(path: &Path) -> Result<Self> {
let repository = git2::Repository::open(path)
.map_err(|e| anyhow!(String::from(e.message())).context("Could not open repository from path"))?;
Ok(Self {
repository: Arc::new(Mutex::new(repository)),
})
}
#[inline]
pub fn load_config(&self) -> Result<Config> {
self.repository
.lock()
.config()
.map_err(|e| anyhow!(String::from(e.message())))
}
#[inline]
pub fn load_commit_diff(&self, hash: &str, config: &CommitDiffLoaderOptions) -> Result<CommitDiff> {
let oid = self.repository.lock().revparse_single(hash)?.id();
let diff_loader_repository = Arc::clone(&self.repository);
let loader = CommitDiffLoader::new(diff_loader_repository, config);
Ok(loader.load_from_hash(oid).map_err(|e| anyhow!("{}", e))?.remove(0))
}
#[inline]
pub fn find_reference(&self, reference: &str) -> Result<Reference> {
let repo = self.repository.lock();
let git2_reference = repo.find_reference(reference)?;
Ok(Reference::from(&git2_reference))
}
#[inline]
pub fn find_commit(&self, reference: &str) -> Result<Commit> {
let repo = self.repository.lock();
let git2_reference = repo.find_reference(reference)?;
Commit::try_from(&git2_reference)
}
pub(crate) fn repo_path(&self) -> PathBuf {
self.repository.lock().path().to_path_buf()
}
pub(crate) fn head_id(&self, head_name: &str) -> Result<Oid> {
let repo = self.repository.lock();
let ref_name = format!("refs/heads/{}", head_name);
let revision = repo.revparse_single(ref_name.as_str())?;
Ok(revision.id())
}
pub(crate) fn commit_id_from_ref(&self, reference: &str) -> Result<Oid> {
let repo = self.repository.lock();
let commit = repo.find_reference(reference)?.peel_to_commit()?;
Ok(commit.id())
}
pub(crate) fn add_path_to_index(&self, path: &Path) -> Result<()> {
let repo = self.repository.lock();
let mut index = repo.index()?;
index.add_path(path).map_err(Error::from)
}
pub(crate) fn remove_path_from_index(&self, path: &Path) -> Result<()> {
let repo = self.repository.lock();
let mut index = repo.index()?;
index.remove_path(path).map_err(Error::from)
}
pub(crate) fn create_commit_on_index(
&self,
reference: &str,
author: &Signature<'_>,
committer: &Signature<'_>,
message: &str,
) -> Result<()> {
let repo = self.repository.lock();
let tree = repo.find_tree(repo.index()?.write_tree()?)?;
let head = repo.find_reference(reference)?.peel_to_commit()?;
let _ = repo.commit(Some("HEAD"), author, committer, message, &tree, &[&head])?;
Ok(())
}
#[cfg(test)]
pub(crate) fn repository(&self) -> Arc<Mutex<git2::Repository>> {
self.repository.clone()
}
}
impl From<git2::Repository> for Repository {
#[inline]
fn from(repository: git2::Repository) -> Self {
Self {
repository: Arc::new(Mutex::new(repository)),
}
}
}
impl ::std::fmt::Debug for Repository {
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> {
f.debug_struct("Repository")
.field("[path]", &self.repository.lock().path())
.finish()
}
}
#[cfg(all(unix, test))]
mod tests {
use std::env::set_var;
use super::*;
use crate::testutil::{commit_id_from_ref, create_commit, with_temp_bare_repository, with_temp_repository};
#[test]
#[serial_test::serial]
fn open_from_env() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("test")
.join("fixtures")
.join("simple");
set_var("GIT_DIR", path.to_str().unwrap());
assert!(Repository::open_from_env().is_ok());
}
#[test]
#[serial_test::serial]
fn open_from_env_error() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("test")
.join("fixtures")
.join("does-not-exist");
set_var("GIT_DIR", path.to_str().unwrap());
assert_eq!(
format!("{:#}", Repository::open_from_env().err().unwrap()),
format!(
"Could not open repository from environment: failed to resolve path '{}': No such file or directory",
path.to_str().unwrap()
)
);
}
#[test]
fn open_from_path() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("test")
.join("fixtures")
.join("simple");
assert!(Repository::open_from_path(&path).is_ok());
}
#[test]
fn open_from_path_error() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("test")
.join("fixtures")
.join("does-not-exist");
assert_eq!(
format!("{:#}", Repository::open_from_path(&path).err().unwrap()),
format!(
"Could not open repository from path: failed to resolve path '{}': No such file or directory",
path.to_str().unwrap()
)
);
}
#[test]
fn load_config() {
with_temp_bare_repository(|repo| {
let _repo = repo.load_config()?;
Ok(())
});
}
#[test]
fn load_commit_diff() {
with_temp_repository(|repository| {
create_commit(&repository, None);
let id = commit_id_from_ref(&repository, "refs/heads/main");
let _diff = repository
.load_commit_diff(id.to_string().as_str(), &CommitDiffLoaderOptions::new())
.unwrap();
Ok(())
});
}
#[test]
fn fmt() {
with_temp_bare_repository(|repository| {
let formatted = format!("{:?}", repository);
let path = repository.repo_path().canonicalize().unwrap();
assert_eq!(
formatted,
format!("Repository {{ [path]: \"{}/\" }}", path.to_str().unwrap())
);
Ok(())
});
}
}