use crate::{self as git, repository::OpenRepositoryLike};
use config::BranchName;
use derive_more::Constructor;
use git_next_config::{self as config, RemoteUrl};
use gix::bstr::BStr;
use std::{
path::Path,
sync::{Arc, RwLock},
};
use tracing::{info, warn};
#[derive(Clone, Debug, Constructor)]
pub struct RealOpenRepository(Arc<RwLock<gix::ThreadSafeRepository>>);
impl super::OpenRepositoryLike for RealOpenRepository {
fn remote_branches(&self) -> git::push::Result<Vec<config::BranchName>> {
let refs = self
.0
.read()
.map_err(|_| git::push::Error::Lock)
.and_then(|repo| {
Ok(repo.to_thread_local().references()?).and_then(|refs| {
Ok(refs.remote_branches().map(|rb| {
rb.filter_map(|rbi| rbi.ok())
.map(|r| r.name().to_owned())
.map(|n| n.to_string())
.filter_map(|p| {
p.strip_prefix("refs/remotes/origin/").map(|v| v.to_owned())
})
.filter(|b| b.as_str() != "HEAD")
.map(BranchName::new)
.collect::<Vec<_>>()
})?)
})
})?;
Ok(refs)
}
#[tracing::instrument]
fn find_default_remote(&self, direction: git::repository::Direction) -> Option<RemoteUrl> {
let Ok(repository) = self.0.read() else {
#[cfg(not(tarpaulin_include))] tracing::debug!("no repository");
return None;
};
let thread_local = repository.to_thread_local();
let Some(Ok(remote)) = thread_local.find_default_remote(direction.into()) else {
#[cfg(not(tarpaulin_include))] tracing::debug!("no remote");
return None;
};
remote
.url(direction.into())
.cloned()
.and_then(|url| RemoteUrl::try_from(url).ok())
}
#[tracing::instrument(skip_all)]
#[cfg(not(tarpaulin_include))] fn fetch(&self) -> Result<(), git::fetch::Error> {
let Ok(repository) = self.0.read() else {
#[cfg(not(tarpaulin_include))] return Err(git::fetch::Error::Lock);
};
let thread_local = repository.to_thread_local();
let Some(Ok(remote)) =
thread_local.find_default_remote(git::repository::Direction::Fetch.into())
else {
#[cfg(not(tarpaulin_include))] return Err(git::fetch::Error::NoFetchRemoteFound);
};
remote
.connect(gix::remote::Direction::Fetch)
.map_err(|gix| git::fetch::Error::Connect(gix.to_string()))?
.prepare_fetch(gix::progress::Discard, Default::default())
.map_err(|gix| git::fetch::Error::Prepare(gix.to_string()))?
.receive(gix::progress::Discard, &Default::default())
.map_err(|gix| git::fetch::Error::Receive(gix.to_string()))?;
info!("Fetch okay");
Ok(())
}
#[cfg(not(tarpaulin_include))] #[tracing::instrument(skip_all)]
fn push(
&self,
repo_details: &git::RepoDetails,
branch_name: &config::BranchName,
to_commit: &git::GitRef,
force: &git::push::Force,
) -> Result<(), git::push::Error> {
let origin = repo_details.origin();
let force = match force {
git::push::Force::No => "".to_string(),
git::push::Force::From(old_ref) => {
format!("--force-with-lease={branch_name}:{old_ref}")
}
};
use secrecy::ExposeSecret;
let command: secrecy::Secret<String> = format!(
"/usr/bin/git push {} {to_commit}:{branch_name} {force}",
origin.expose_secret()
)
.into();
let git_dir = self
.0
.read()
.map_err(|_| git::push::Error::Lock)
.map(|r| r.git_dir().to_path_buf())?;
let ctx = gix::diff::command::Context {
git_dir: Some(git_dir),
..Default::default()
};
gix::command::prepare(command.expose_secret())
.with_context(ctx)
.with_shell_allow_argument_splitting()
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()?
.wait()?;
Ok(())
}
fn commit_log(
&self,
branch_name: &config::BranchName,
find_commits: &[git::Commit],
) -> Result<Vec<crate::Commit>, git::commit::log::Error> {
let limit = match find_commits.is_empty() {
true => 1,
false => 50,
};
self.0
.read()
.map_err(|_| git::commit::log::Error::Lock)
.map(|repo| {
let branch = format!("remotes/origin/{branch_name}");
let branch = BStr::new(&branch);
let thread_local = repo.to_thread_local();
let branch_head = thread_local
.rev_parse_single(branch)
.map_err(|e| e.to_string())
.map_err(as_gix_error(branch_name.clone()))?;
let object = branch_head
.object()
.map_err(|e| e.to_string())
.map_err(as_gix_error(branch_name.clone()))?;
let commit = object
.try_into_commit()
.map_err(|e| e.to_string())
.map_err(as_gix_error(branch_name.clone()))?;
let walk = thread_local
.rev_walk([commit.id])
.all()
.map_err(|e| e.to_string())
.map_err(as_gix_error(branch_name.clone()))?;
let mut commits = vec![];
for item in walk.take(limit) {
let item = item
.map_err(|e| e.to_string())
.map_err(as_gix_error(branch_name.clone()))?;
let commit = item
.object()
.map_err(|e| e.to_string())
.map_err(as_gix_error(branch_name.clone()))?;
let id = commit.id().to_string();
let message = commit
.message_raw()
.map_err(|e| e.to_string())
.map_err(as_gix_error(branch_name.clone()))?
.to_string();
let commit = git::Commit::new(
git::commit::Sha::new(id),
git::commit::Message::new(message),
);
if find_commits.contains(&commit) {
commits.push(commit);
break;
}
commits.push(commit);
}
Ok(commits)
})?
}
#[tracing::instrument(skip_all, fields(%branch_name, ?file_name))]
fn read_file(
&self,
branch_name: &config::BranchName,
file_name: &Path,
) -> git::file::Result<String> {
self.0
.read()
.map_err(|_| git::file::Error::Lock)
.and_then(|repo| {
let thread_local = repo.to_thread_local();
let fref =
thread_local.find_reference(format!("origin/{}", branch_name).as_str())?;
let id = fref.try_id().ok_or(git::file::Error::TryId)?;
let oid = id.detach();
let obj = thread_local.find_object(oid)?;
let commit = obj.into_commit();
let tree = commit.tree()?;
let ent = tree
.find_entry(file_name.to_string_lossy().to_string())
.ok_or(git::file::Error::FileNotFound)?;
let fobj = ent.object()?;
let blob = fobj.into_blob().take_data();
let content = String::from_utf8(blob)?;
Ok(content)
})
}
fn duplicate(&self) -> Box<dyn OpenRepositoryLike> {
Box::new(self.clone())
}
}
fn as_gix_error(branch: BranchName) -> impl FnOnce(String) -> git::commit::log::Error {
|error| git::commit::log::Error::Gix { branch, error }
}
impl From<&RemoteUrl> for git::GitRemote {
fn from(url: &RemoteUrl) -> Self {
let host = url.host.clone().unwrap_or_default();
let path = url.path.to_string();
let path = path.strip_prefix('/').map_or(path.as_str(), |path| path);
let path = path.strip_suffix(".git").map_or(path, |path| path);
Self::new(
config::Hostname::new(host),
config::RepoPath::new(path.to_string()),
)
}
}