use gix::{Repository, Url};
use std::io::Write;
use std::path::Path;
use tracing::info;
use crate::config::GitAuthentication;
fn token_credentials(
token: &str,
) -> impl FnMut(gix::credentials::helper::Action) -> gix::credentials::protocol::Result + 'static {
let token = token.to_owned();
move |action| match action {
gix::credentials::helper::Action::Get(context) => {
Ok(Some(gix::credentials::protocol::Outcome {
identity: gix::sec::identity::Account {
username: "oauth2".to_string(),
password: token.clone(),
oauth_refresh_token: None,
},
next: context.into(),
}))
}
gix::credentials::helper::Action::Store(_) | gix::credentials::helper::Action::Erase(_) => {
Ok(None)
}
}
}
pub fn git_clone(
repository_url: Url,
local_path: &str,
branch: &str,
auth: &GitAuthentication,
) -> Result<Repository, String> {
std::fs::create_dir_all(local_path).map_err(|details| format!("{}", details))?;
let local_path = Path::new(local_path);
let ref_name = format!("refs/heads/{}", branch);
let mut prepare_clone = gix::prepare_clone(repository_url, local_path)
.map_err(|details| format!("{}", details))?
.with_ref_name(Some(ref_name.as_str()))
.map_err(|details| format!("{}", details))?;
if let GitAuthentication::WithToken(token) = auth {
let token = token.clone();
prepare_clone = prepare_clone.configure_connection(move |connection| {
connection.set_credentials(token_credentials(&token));
Ok(())
});
}
let (mut prepare_checkout, _) = prepare_clone
.fetch_then_checkout(gix::progress::Discard, &gix::interrupt::IS_INTERRUPTED)
.map_err(|details| format!("{}", details))?;
let (repo, _) = prepare_checkout
.main_worktree(gix::progress::Discard, &gix::interrupt::IS_INTERRUPTED)
.map_err(|details| format!("{}", details))?;
let config_path = repo.git_dir().join("config");
if let Ok(mut file) = std::fs::OpenOptions::new().append(true).open(&config_path) {
let _ = writeln!(file, "\n[core]\n attributesFile = \"\"");
}
let head: String = match repo.head_commit() {
Ok(commit) => commit.id().to_string(),
Err(_details) => "unknown".to_string(),
};
info!(head, "Initial cloning done");
Ok(repo)
}
pub fn git_pull(local_path: &str, auth: &GitAuthentication) -> Result<(), String> {
let config_path = Path::new(local_path).join(".git").join("config");
if let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&config_path)
{
let _ = writeln!(
file,
"\n[user]\n name = Local User\n email = user@local.invalid"
);
}
let repo = gix::open(local_path).map_err(|details| format!("{}", details))?;
let head = repo.head().map_err(|details| format!("{}", details))?;
let local_branch_ref = head
.referent_name()
.ok_or("Repository is in a detached HEAD state")?;
let branch_short_name = local_branch_ref
.as_bstr()
.strip_prefix(b"refs/heads/")
.ok_or("HEAD does not point to a local branch under refs/heads/")?;
let branch_name_str =
std::str::from_utf8(branch_short_name).map_err(|details| format!("{}", details))?;
let (remote_name, remote_branch_name) = repo
.branch_remote_ref_name(local_branch_ref, gix::remote::Direction::Fetch)
.and_then(|remote_ref| {
let name_str = remote_ref.unwrap().to_string();
if let Some(stripped) = name_str.strip_prefix("refs/remotes/") {
let mut parts = stripped.splitn(2, '/');
let r = parts.next()?;
let b = parts.next()?;
Some((r.to_string(), b.to_string()))
} else {
None
}
})
.unwrap_or_else(|| ("origin".to_string(), branch_name_str.to_string()));
let remote = repo
.find_remote(&remote_name)
.map_err(|details| format!("{}", details))?;
let mut connection = remote
.connect(gix::remote::Direction::Fetch)
.map_err(|details| format!("{}", details))?;
if let GitAuthentication::WithToken(token) = auth {
connection.set_credentials(token_credentials(token));
}
connection
.prepare_fetch(gix::progress::Discard, Default::default())
.map_err(|details| format!("{}", details))?
.receive(gix::progress::Discard, &gix::interrupt::IS_INTERRUPTED)
.map_err(|details| format!("{}", details))?;
let remote_ref_name = format!("refs/remotes/{}/{}", remote_name, remote_branch_name);
let mut remote_ref = repo
.find_reference(&remote_ref_name)
.map_err(|details| format!("{}", details))?;
let target_commit_id = remote_ref
.peel_to_id()
.map_err(|details| format!("{}", details))?;
let mut local_ref = repo
.find_reference(local_branch_ref.as_bstr())
.map_err(|details| format!("{}", details))?;
local_ref
.set_target_id(target_commit_id.detach(), "gix auto-pull: Fast-forward")
.map_err(|details| format!("{}", details))?;
let commit = target_commit_id
.object()
.map_err(|details| format!("{}", details))?
.peel_to_commit()
.map_err(|details| format!("{}", details))?;
let tree_id = commit.tree_id().map_err(|details| format!("{}", details))?;
let mut index = repo
.index_from_tree(&tree_id)
.map_err(|details| format!("{}", details))?;
index
.write(gix::index::write::Options::default())
.map_err(|details| format!("{}", details))?;
if let Some(workdir) = repo.workdir() {
gix::worktree::state::checkout(
&mut index,
workdir,
repo.objects.clone(),
&gix::progress::Discard,
&gix::progress::Discard,
&Default::default(),
Default::default(),
)
.map_err(|details| format!("{}", details))?;
}
let head: String = match repo.head_commit() {
Ok(commit) => commit.id().to_string(),
Err(_details) => "unknown".to_string(),
};
info!(head, "Git pull done");
Ok(())
}