//! Definition of an iris [repo][Repository]. use crate::{error, Result}; use log::{debug, trace}; use std::{path::PathBuf, sync::OnceLock}; use url::Url; /// A handle to a locally stored git repository. pub struct Repository { /// The remote url that the git repository is located at. pub url: Url, /// The path on disk that the this repository is/will be located at. pub path: PathBuf, /// The git2 lib handle to the repo inner: OnceLock, } impl Repository { /// Creates a new handle to a local git repository. pub const fn new(url: Url, path: PathBuf) -> Self { Self { url, path, inner: OnceLock::new(), } } /// Gets the handle to the local repository fn repo(&self) -> Result<&git2::Repository> { // we only need to initalize once if let Some(repo) = self.inner.get() { return Ok(repo); } trace!("loading repo"); // repository not loaded, try loading it let repo = if self.path.exists() { // repo exists on file system, just open it git2::Repository::open(&self.path)? } else { // repo needs to be cloned git2::Repository::clone(self.url.as_str(), &self.path)? }; // save and return repo Ok(self.inner.get_or_init(|| repo)) } /// Gets the current remote in the git repository fn remote(&self) -> Result { let repo = self.repo()?; let remote_name = "origin"; let mut remote = repo.find_remote(remote_name)?; // make sure that out remote is connected before // we do anything with it. otherwise things will // fail! if !remote.connected() { trace!("connecting to remote"); remote.connect(git2::Direction::Fetch)?; } Ok(remote) } /// Fetches the latest commits in the provided branch. pub fn fetch(&self, branch: &str) -> Result<()> { let mut remote = self.remote()?; trace!("fetching '{branch}"); remote.fetch(&[branch], None, None)?; Ok(()) } /// Returns the default branch of the git repositroy. pub fn default_branch(&self) -> Result { let remote = self.remote()?; let buf = remote.default_branch()?; let name = buf .as_str() .and_then(|s| s.strip_prefix("refs/heads/")) .map(String::from) .ok_or_else(|| error::error!("cannot get default branch"))?; debug!("default branch is '{name}'"); Ok(name) } /// Returns the latest fetched commit in `branch`. pub fn latest_commit(&self, branch: &str) -> Result { let repo = self.repo()?; let refr_name = format!("refs/remotes/origin/{branch}"); let refr = repo.find_reference(&refr_name)?; let commit = refr.peel_to_commit()?; let oid = commit.id().to_string(); Ok(oid) } /// Checks out the local repository to the given commit and detaches /// HEAD to that commit. pub fn checkout(&self, commit: &str) -> Result<()> { let repo = self.repo()?; trace!("checkout to '{commit}'"); let oid = git2::Oid::from_str(commit)?; let commit = repo.find_commit(oid)?; repo.reset(commit.as_object(), git2::ResetType::Hard, None)?; Ok(()) } } impl std::fmt::Debug for Repository { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "_") } } impl PartialEq for Repository { fn eq(&self, other: &Self) -> bool { self.url.eq(&other.url) && self.path.eq(&other.path) } } impl Eq for Repository {}