mod callbacks;
pub(crate) mod push;
pub(crate) mod tags;
use crate::{
error::{Error, Result},
sync::{
cred::BasicAuthCredential,
remotes::push::ProgressNotification, repository::repo, utils,
},
ProgressPercent,
};
use crossbeam_channel::Sender;
use git2::{
BranchType, FetchOptions, ProxyOptions, Remote, Repository,
};
use scopetime::scope_time;
use utils::bytes2string;
pub use callbacks::Callbacks;
pub use tags::tags_missing_remote;
use super::RepoPath;
pub const DEFAULT_REMOTE_NAME: &str = "origin";
pub fn proxy_auto<'a>() -> ProxyOptions<'a> {
let mut proxy = ProxyOptions::new();
proxy.auto();
proxy
}
pub fn add_remote(
repo_path: &RepoPath,
name: &str,
url: &str,
) -> Result<()> {
let repo = repo(repo_path)?;
repo.remote(name, url)?;
Ok(())
}
pub fn rename_remote(
repo_path: &RepoPath,
name: &str,
new_name: &str,
) -> Result<()> {
let repo = repo(repo_path)?;
repo.remote_rename(name, new_name)?;
Ok(())
}
pub fn update_remote_url(
repo_path: &RepoPath,
name: &str,
new_url: &str,
) -> Result<()> {
let repo = repo(repo_path)?;
repo.remote_set_url(name, new_url)?;
Ok(())
}
pub fn delete_remote(
repo_path: &RepoPath,
remote_name: &str,
) -> Result<()> {
let repo = repo(repo_path)?;
repo.remote_delete(remote_name)?;
Ok(())
}
pub fn validate_remote_name(name: &str) -> bool {
Remote::is_valid_name(name)
}
pub fn get_remotes(repo_path: &RepoPath) -> Result<Vec<String>> {
scope_time!("get_remotes");
let repo = repo(repo_path)?;
let remotes = repo.remotes()?;
let remotes: Vec<String> =
remotes.iter().flatten().map(String::from).collect();
Ok(remotes)
}
pub fn get_remote_url(
repo_path: &RepoPath,
remote_name: &str,
) -> Result<Option<String>> {
let repo = repo(repo_path)?;
let remote = repo.find_remote(remote_name)?.clone();
let url = remote.url();
if let Some(u) = url {
return Ok(Some(u.to_string()));
}
Ok(None)
}
pub fn get_default_remote(repo_path: &RepoPath) -> Result<String> {
let repo = repo(repo_path)?;
get_default_remote_in_repo(&repo)
}
fn get_current_branch(
repo: &Repository,
) -> Result<Option<git2::Branch<'_>>> {
for b in repo.branches(None)? {
let branch = b?.0;
if branch.is_head() {
return Ok(Some(branch));
}
}
Ok(None)
}
pub fn get_default_remote_for_fetch(
repo_path: &RepoPath,
) -> Result<String> {
let repo = repo(repo_path)?;
get_default_remote_for_fetch_in_repo(&repo)
}
pub(crate) fn get_default_remote_for_fetch_in_repo(
repo: &Repository,
) -> Result<String> {
scope_time!("get_default_remote_for_fetch_in_repo");
let config = repo.config()?;
let branch = get_current_branch(repo)?;
if let Some(branch) = branch {
let remote_name = bytes2string(branch.name_bytes()?)?;
let entry_name = format!("branch.{}.remote", &remote_name);
if let Ok(entry) = config.get_entry(&entry_name) {
return bytes2string(entry.value_bytes());
}
}
get_default_remote_in_repo(repo)
}
pub fn get_default_remote_for_push(
repo_path: &RepoPath,
) -> Result<String> {
let repo = repo(repo_path)?;
get_default_remote_for_push_in_repo(&repo)
}
pub(crate) fn get_default_remote_for_push_in_repo(
repo: &Repository,
) -> Result<String> {
scope_time!("get_default_remote_for_push_in_repo");
let config = repo.config()?;
let branch = get_current_branch(repo)?;
if let Some(branch) = branch {
let remote_name = bytes2string(branch.name_bytes()?)?;
let entry_name =
format!("branch.{}.pushRemote", &remote_name);
if let Ok(entry) = config.get_entry(&entry_name) {
return bytes2string(entry.value_bytes());
}
if let Ok(entry) = config.get_entry("remote.pushDefault") {
return bytes2string(entry.value_bytes());
}
let entry_name = format!("branch.{}.remote", &remote_name);
if let Ok(entry) = config.get_entry(&entry_name) {
return bytes2string(entry.value_bytes());
}
}
get_default_remote_in_repo(repo)
}
pub(crate) fn get_default_remote_in_repo(
repo: &Repository,
) -> Result<String> {
scope_time!("get_default_remote_in_repo");
let remotes = repo.remotes()?;
let found_origin = remotes
.iter()
.any(|r| r.is_some_and(|r| r == DEFAULT_REMOTE_NAME));
if found_origin {
return Ok(DEFAULT_REMOTE_NAME.into());
}
if remotes.len() == 1 {
let first_remote = remotes
.iter()
.next()
.flatten()
.map(String::from)
.ok_or_else(|| {
Error::Generic("no remote found".into())
})?;
return Ok(first_remote);
}
Err(Error::NoDefaultRemoteFound)
}
fn fetch_from_remote(
repo_path: &RepoPath,
remote: &str,
basic_credential: Option<BasicAuthCredential>,
progress_sender: Option<Sender<ProgressNotification>>,
) -> Result<()> {
let repo = repo(repo_path)?;
let mut remote = repo.find_remote(remote)?;
let mut options = FetchOptions::new();
let callbacks = Callbacks::new(progress_sender, basic_credential);
options.prune(git2::FetchPrune::On);
options.proxy_options(proxy_auto());
options.download_tags(git2::AutotagOption::All);
options.remote_callbacks(callbacks.callbacks());
remote.fetch(&[] as &[&str], Some(&mut options), None)?;
remote.fetch(
&["refs/tags/*:refs/tags/*"],
Some(&mut options),
None,
)?;
Ok(())
}
pub fn fetch_all(
repo_path: &RepoPath,
basic_credential: &Option<BasicAuthCredential>,
progress_sender: &Option<Sender<ProgressPercent>>,
) -> Result<()> {
scope_time!("fetch_all");
let repo = repo(repo_path)?;
let remotes = repo
.remotes()?
.iter()
.flatten()
.map(String::from)
.collect::<Vec<_>>();
let remotes_count = remotes.len();
for (idx, remote) in remotes.into_iter().enumerate() {
fetch_from_remote(
repo_path,
&remote,
basic_credential.clone(),
None,
)?;
if let Some(sender) = progress_sender {
let progress = ProgressPercent::new(idx, remotes_count);
sender.send(progress)?;
}
}
Ok(())
}
pub(crate) fn fetch(
repo_path: &RepoPath,
branch: &str,
basic_credential: Option<BasicAuthCredential>,
progress_sender: Option<Sender<ProgressNotification>>,
) -> Result<usize> {
scope_time!("fetch");
let repo = repo(repo_path)?;
let branch_ref = repo
.find_branch(branch, BranchType::Local)?
.into_reference();
let branch_ref = bytes2string(branch_ref.name_bytes())?;
let remote_name = repo.branch_upstream_remote(&branch_ref)?;
let remote_name = bytes2string(&remote_name)?;
let mut remote = repo.find_remote(&remote_name)?;
let mut options = FetchOptions::new();
options.download_tags(git2::AutotagOption::All);
let callbacks = Callbacks::new(progress_sender, basic_credential);
options.remote_callbacks(callbacks.callbacks());
options.proxy_options(proxy_auto());
remote.fetch(&[branch], Some(&mut options), None)?;
Ok(remote.stats().received_bytes())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sync::tests::{
debug_cmd_print, repo_clone, repo_init,
};
#[test]
fn test_smoke() {
let (remote_dir, _remote) = repo_init().unwrap();
let remote_path = remote_dir.path().to_str().unwrap();
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
let repo_path: &RepoPath =
&repo_dir.keep().as_os_str().to_str().unwrap().into();
let remotes = get_remotes(repo_path).unwrap();
assert_eq!(remotes, vec![String::from("origin")]);
fetch(repo_path, "master", None, None).unwrap();
}
#[test]
fn test_default_remote() {
let (remote_dir, _remote) = repo_init().unwrap();
let remote_path = remote_dir.path().to_str().unwrap();
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
let repo_path: &RepoPath =
&repo_dir.keep().as_os_str().to_str().unwrap().into();
debug_cmd_print(
repo_path,
&format!("git remote add second {remote_path}")[..],
);
let remotes = get_remotes(repo_path).unwrap();
assert_eq!(
remotes,
vec![String::from("origin"), String::from("second")]
);
let first =
get_default_remote_in_repo(&repo(repo_path).unwrap())
.unwrap();
assert_eq!(first, String::from("origin"));
}
#[test]
fn test_default_remote_out_of_order() {
let (remote_dir, _remote) = repo_init().unwrap();
let remote_path = remote_dir.path().to_str().unwrap();
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
let repo_path: &RepoPath =
&repo_dir.keep().as_os_str().to_str().unwrap().into();
debug_cmd_print(
repo_path,
"git remote rename origin alternate",
);
debug_cmd_print(
repo_path,
&format!("git remote add origin {remote_path}")[..],
);
let remotes = get_remotes(repo_path).unwrap();
assert_eq!(
remotes,
vec![String::from("alternate"), String::from("origin")]
);
let first =
get_default_remote_in_repo(&repo(repo_path).unwrap())
.unwrap();
assert_eq!(first, String::from("origin"));
}
#[test]
fn test_default_remote_inconclusive() {
let (remote_dir, _remote) = repo_init().unwrap();
let remote_path = remote_dir.path().to_str().unwrap();
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
let repo_path: &RepoPath =
&repo_dir.keep().as_os_str().to_str().unwrap().into();
debug_cmd_print(
repo_path,
"git remote rename origin alternate",
);
debug_cmd_print(
repo_path,
&format!("git remote add someremote {remote_path}")[..],
);
let remotes = get_remotes(repo_path).unwrap();
assert_eq!(
remotes,
vec![
String::from("alternate"),
String::from("someremote")
]
);
let default_remote =
get_default_remote_in_repo(&repo(repo_path).unwrap());
assert!(matches!(
default_remote,
Err(Error::NoDefaultRemoteFound)
));
}
#[test]
fn test_default_remote_for_fetch() {
let (remote_dir, _remote) = repo_init().unwrap();
let remote_path = remote_dir.path().to_str().unwrap();
let (repo_dir, repo) = repo_clone(remote_path).unwrap();
let repo_path: &RepoPath =
&repo_dir.keep().as_os_str().to_str().unwrap().into();
debug_cmd_print(
repo_path,
"git remote rename origin alternate",
);
debug_cmd_print(
repo_path,
&format!("git remote add someremote {remote_path}")[..],
);
let mut config = repo.config().unwrap();
config
.set_str("branch.master.remote", "branchremote")
.unwrap();
let default_fetch_remote =
get_default_remote_for_fetch_in_repo(&repo);
assert!(
matches!(default_fetch_remote, Ok(remote_name) if remote_name == "branchremote")
);
}
#[test]
fn test_default_remote_for_push() {
let (remote_dir, _remote) = repo_init().unwrap();
let remote_path = remote_dir.path().to_str().unwrap();
let (repo_dir, repo) = repo_clone(remote_path).unwrap();
let repo_path: &RepoPath =
&repo_dir.keep().as_os_str().to_str().unwrap().into();
debug_cmd_print(
repo_path,
"git remote rename origin alternate",
);
debug_cmd_print(
repo_path,
&format!("git remote add someremote {remote_path}")[..],
);
let mut config = repo.config().unwrap();
config
.set_str("branch.master.remote", "branchremote")
.unwrap();
let default_push_remote =
get_default_remote_for_push_in_repo(&repo);
assert!(
matches!(default_push_remote, Ok(remote_name) if remote_name == "branchremote")
);
config.set_str("remote.pushDefault", "pushdefault").unwrap();
let default_push_remote =
get_default_remote_for_push_in_repo(&repo);
assert!(
matches!(default_push_remote, Ok(remote_name) if remote_name == "pushdefault")
);
config
.set_str("branch.master.pushRemote", "branchpushremote")
.unwrap();
let default_push_remote =
get_default_remote_for_push_in_repo(&repo);
assert!(
matches!(default_push_remote, Ok(remote_name) if remote_name == "branchpushremote")
);
}
}