use git2::{Cred, CredentialType, RemoteCallbacks, Repository};
use std::path::{Path, PathBuf};
#[derive(Clone, Debug)]
pub struct CommitAuthor {
pub name: String,
pub email: String,
}
impl CommitAuthor {
pub fn new<T: AsRef<str>>(name: T, email: T) -> Self {
Self {
name: name.as_ref().to_owned(),
email: email.as_ref().to_owned(),
}
}
}
#[allow(dead_code)]
pub struct Branch {
pub name: String,
pub refspec: String,
pub remote_refspec: String,
}
impl Branch {
pub fn new<T: AsRef<str>>(name: T) -> Self {
Self {
name: name.as_ref().to_owned(),
refspec: format!("refs/heads/{}", name.as_ref()),
remote_refspec: format!("refs/remotes/origin/{}", name.as_ref()),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn refspec(&self) -> &str {
&self.refspec
}
pub fn remote_refspec(&self) -> &str {
&self.refspec
}
}
impl From<&str> for Branch {
fn from(name: &str) -> Self {
Self::new(name)
}
}
impl From<String> for Branch {
fn from(name: String) -> Self {
Self::new(name)
}
}
impl From<&String> for Branch {
fn from(name: &String) -> Self {
Self::new(name)
}
}
#[allow(dead_code)]
pub struct PublicIndexRepository {
pub repository: git2::Repository,
pub cloned_branch: Branch,
pub ssh_private_key_path: Option<PathBuf>,
pub ssh_private_key: Option<String>,
pub local_repository_path: PathBuf,
}
impl std::fmt::Debug for PublicIndexRepository {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PublicIndexRepository")
.field("ssh_private_key_path", &self.ssh_private_key_path)
.field("local_repository_path", &self.local_repository_path)
.finish()
}
}
impl PublicIndexRepository {
#[allow(clippy::type_complexity)]
fn get_auth_callback(
ssh_private_key_path: &Path,
) -> Box<dyn FnMut(&str, Option<&str>, CredentialType) -> Result<Cred, git2::Error>> {
let key_path =
std::fs::canonicalize(PathBuf::from(ssh_private_key_path)).unwrap_or_else(|_| {
panic!(
"Failed to canonicalize path: {}",
ssh_private_key_path.display()
)
});
Box::new(
move |_url, username_from_url: Option<&str>, _allowed_types| {
Cred::ssh_key(
username_from_url.expect("No username specified for remote. We right now expect the registry to lie on github with a username of `git`."),
None,
&key_path,
None)
},
)
}
#[allow(clippy::type_complexity)]
fn get_auth_callback_with_key_from_memory(
ssh_private_key: String,
) -> Box<dyn FnMut(&str, Option<&str>, CredentialType) -> Result<Cred, git2::Error>> {
Box::new(
move |_url, username_from_url: Option<&str>, _allowed_types| {
Cred::ssh_key_from_memory(
username_from_url.expect("No username specified for remote. We right now expect the registry to lie on github with a username of `git`."),
None,
&ssh_private_key,
None)
},
)
}
pub fn auth(&self) -> anyhow::Result<RemoteCallbacks> {
let Self {
ssh_private_key_path,
ssh_private_key,
..
} = self;
let mut callbacks = RemoteCallbacks::new();
if let Some(ssh_private_key_path) = ssh_private_key_path {
callbacks.credentials(Self::get_auth_callback(ssh_private_key_path));
return Ok(callbacks);
}
if let Some(ssh_private_key) = ssh_private_key {
callbacks.credentials(Self::get_auth_callback_with_key_from_memory(
ssh_private_key.clone(),
));
return Ok(callbacks);
}
Err(anyhow::anyhow!(
"Couldn't find a private ssh key to authenticate with."
))
}
pub fn clone_or_open<T: AsRef<camino::Utf8Path>, S: AsRef<str>>(
try_cloning_from: S,
try_cloning_to: T,
branch: S,
ssh_private_key_path: Option<T>,
ssh_private_key: Option<S>,
) -> anyhow::Result<Self> {
let mut callbacks = RemoteCallbacks::new();
if let Some(ref ssh_private_key_path) = ssh_private_key_path {
callbacks.credentials(Self::get_auth_callback(
ssh_private_key_path.as_ref().as_std_path(),
));
}
if let Some(ref ssh_private_key) = ssh_private_key {
callbacks.credentials(Self::get_auth_callback_with_key_from_memory(
ssh_private_key.as_ref().to_owned(),
));
}
let mut fetch_options = git2::FetchOptions::new();
fetch_options.remote_callbacks(callbacks);
let mut repo_builder = git2::build::RepoBuilder::new();
repo_builder.fetch_options(fetch_options);
let local_repository_path = try_cloning_to.as_ref();
#[allow(clippy::if_not_else)]
let repository = if local_repository_path.exists() {
if !local_repository_path.join(".git").exists() {
repo_builder.clone(
try_cloning_from.as_ref(),
local_repository_path.as_std_path(),
)?
} else {
Repository::open(local_repository_path)?
}
} else {
repo_builder.clone(
try_cloning_from.as_ref(),
local_repository_path.as_std_path(),
)?
};
Ok(Self {
repository,
cloned_branch: Branch::new(branch.as_ref()),
ssh_private_key_path: ssh_private_key_path
.map(|p| p.as_ref().as_std_path().to_path_buf()),
ssh_private_key: ssh_private_key.map(|s| s.as_ref().to_owned()),
local_repository_path: try_cloning_to.as_ref().as_std_path().to_path_buf(),
})
}
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::unwrap_in_result)]
pub fn pull_from_origin_fast_forward(&self) -> anyhow::Result<()> {
let callbacks = self.auth()?;
let mut fetch_options = git2::FetchOptions::new();
fetch_options.remote_callbacks(callbacks);
fetch_options.download_tags(git2::AutotagOption::All);
let Self {
repository,
cloned_branch,
..
} = self;
let origin_head_ref = repository.find_reference("refs/remotes/origin/HEAD")?;
let annotated_commit_ref = repository.reference_to_annotated_commit(&origin_head_ref)?;
let annotated_commit_id = annotated_commit_ref.id();
let commit = repository.find_commit(annotated_commit_id)?;
repository.reset(
commit.as_object(),
git2::ResetType::Hard,
Some(git2::build::CheckoutBuilder::default().force()),
)?;
let statuses = repository.statuses(None)?;
let mut index = repository.index()?;
let mut path_specs_to_remove = vec![];
for i in 0..statuses.len() {
let status_entry = statuses.get(i).unwrap();
let path = status_entry.path();
let status = status_entry.status();
if status.is_wt_new() {
if let Some(path) = path {
path_specs_to_remove.push(path.to_owned());
}
}
}
index.remove_all(&path_specs_to_remove, None)?;
index.write()?;
for path_spec in path_specs_to_remove {
std::fs::remove_file(self.local_repository_path.join(path_spec))?;
}
let mut remote = repository.find_remote("origin")?;
remote.fetch(&[cloned_branch.name()], Some(&mut fetch_options), None)?;
let fetch_head = repository.find_reference("FETCH_HEAD")?;
let annotated_commit_ref = repository.reference_to_annotated_commit(&fetch_head)?;
let annotated_commit_id = annotated_commit_ref.id();
let (merge_analysis, _) = repository.merge_analysis(&[&annotated_commit_ref])?;
if merge_analysis.is_normal() {
}
if let Ok(mut reference) = repository.find_reference(cloned_branch.refspec()) {
let name = match reference.name() {
Some(s) => s.to_owned(),
None => String::from_utf8_lossy(reference.name_bytes()).to_string(),
};
let msg = format!(
"Fast-Forward: Setting {} to id: {}",
name, annotated_commit_id
);
reference.set_target(annotated_commit_id, &msg)?;
repository.set_head(&name)?;
repository.checkout_head(Some(
git2::build::CheckoutBuilder::default()
.force(),
))?;
return Ok(());
}
repository.reference(
cloned_branch.refspec(),
annotated_commit_id,
true,
&format!(
"Setting {} to {}",
cloned_branch.name(),
annotated_commit_id
),
)?;
repository.set_head(cloned_branch.refspec())?;
repository.checkout_head(Some(
git2::build::CheckoutBuilder::default()
.allow_conflicts(true)
.conflict_style_merge(true)
.force(),
))?;
Ok(())
}
}