use std::path::PathBuf;
use git2::{Repository, build::RepoBuilder};
use thiserror::Error;
pub struct GitRepository {
repository: Option<Repository>,
local_folder: PathBuf,
}
#[derive(Error, Debug)]
pub enum Error {
#[error("Git2 error: {0}")]
Git2(#[from] git2::Error),
#[error("Git repository is not initialized!")]
RepositoryNotInitialized,
#[error("Invalid branch name!")]
InvalidBranchName,
#[error("Current reference is not a branch!")]
RefIsNotBranch,
}
pub type Result<T> = std::result::Result<T, Error>;
impl GitRepository {
pub fn new(local_folder: PathBuf) -> Self {
Self {
repository: None,
local_folder,
}
}
pub fn init(&mut self) -> Result<()> {
self.repository = Some(Repository::init(&self.local_folder)?);
Ok(())
}
pub fn load(&mut self) -> Result<()> {
self.repository = Some(Repository::open(&self.local_folder).map_err(Error::Git2)?);
Ok(())
}
pub fn clone_and_checkout(&mut self, url: &str, branch: &str) -> Result<()> {
self.repository = Some(
RepoBuilder::new()
.branch(branch)
.clone(url, &self.local_folder)
.map_err(Error::Git2)?,
);
Ok(())
}
pub fn pull_changes(&self, branch: Option<String>) -> Result<()> {
let repo = self.repository()?;
let current_branch_name = if let Some(branch) = branch {
branch
} else {
self.current_branch_name()?
};
let mut remote = repo.find_remote("origin")?;
let mut fetch_opts = git2::FetchOptions::new();
fetch_opts.download_tags(git2::AutotagOption::All);
remote.fetch(&[current_branch_name.as_str()], Some(&mut fetch_opts), None)?;
let fetch_head = repo.find_reference("FETCH_HEAD")?;
let fetch_commit = repo.reference_to_annotated_commit(&fetch_head)?;
let refname = format!("refs/heads/{current_branch_name}");
repo.reference(
&refname,
fetch_commit.id(),
true,
&format!("Setting {} to {}", current_branch_name, fetch_commit.id()),
)?;
repo.set_head(&refname)?;
repo.checkout_head(Some(
git2::build::CheckoutBuilder::default()
.allow_conflicts(false)
.conflict_style_merge(true)
.force(),
))?;
Ok(())
}
fn repository(&self) -> Result<&Repository> {
if self.repository.is_none() {
return Err(Error::RepositoryNotInitialized);
}
Ok(self.repository.as_ref().unwrap())
}
pub fn current_branch_name(&self) -> Result<String> {
let repo = self.repository()?;
let head = repo.head()?;
if head.is_branch() {
if let Some(name) = head.name() {
Ok(name.to_string().replace("refs/heads/", ""))
} else {
Err(Error::InvalidBranchName)
}
} else {
Err(Error::RefIsNotBranch)
}
}
pub fn local_folder(&self) -> &PathBuf {
&self.local_folder
}
}