diff --git a/src/cmd_sync.rs b/src/cmd_sync.rs index 2c2a213..c462c87 100644 --- a/src/cmd_sync.rs +++ b/src/cmd_sync.rs @@ -1,7 +1,10 @@ -use std::path::{Path, PathBuf}; +use std::{ + io::{self, Write}, + path::{Path, PathBuf}, +}; -use anyhow::{anyhow, Result}; -use git2::{Cred, PushOptions, RemoteCallbacks, Repository}; +use anyhow::{anyhow, Context, Result}; +use git2::{build::CheckoutBuilder, Cred, FetchOptions, PushOptions, RemoteCallbacks, Repository}; pub(crate) fn cmd_sync( config_dir: &PathBuf, @@ -21,12 +24,154 @@ pub(crate) fn cmd_sync( remotes.get(0).unwrap().to_string() } }; + debug!("remote name: {remote_name}"); + let mut remote = repo.find_remote(&remote_name)?; + let callbacks = remote_callback(&use_sshagent, &ssh_key); + let mut fetchoptions = FetchOptions::new(); + fetchoptions.remote_callbacks(callbacks); + let fetch_refspec: Vec = remote + .refspecs() + .filter_map(|rs| match rs.direction() { + git2::Direction::Fetch => rs.str().map(|s| s.to_string()), + git2::Direction::Push => None, + }) + .collect(); + remote + .fetch(&fetch_refspec, Some(&mut fetchoptions), None) + .context("Failed to fetch (pull)")?; + let stats = remote.stats(); + if stats.local_objects() > 0 { + println!( + "\rReceived {}/{} objects in {} bytes (used {} local objects)", + stats.indexed_objects(), + stats.total_objects(), + stats.received_bytes(), + stats.local_objects(), + ); + } else { + println!( + "\rReceived {}/{} objects in {} bytes", + stats.indexed_objects(), + stats.total_objects(), + stats.received_bytes(), + ); + } + let fetch_head = repo + .reference_to_annotated_commit( + &repo + .resolve_reference_from_short_name(&remote_name) + .context("failed to get reference from fetch refspec")?, + ) + .context("failed to get annotated commit")?; + let (merge_analysis, merge_preference) = repo + .merge_analysis(&[&fetch_head]) + .context("failed to do merge_analysis")?; + + trace!("merge analysis: {:?}", merge_analysis); + trace!("merge preference: {:?}", merge_preference); + match merge_analysis { + ma if ma.is_up_to_date() => { + info!("HEAD is up to date. skip merging"); + } + ma if ma.is_fast_forward() => { + // https://github.com/rust-lang/git2-rs/blob/master/examples/pull.rs + info!("fast forward is available"); + let mut ref_remote = repo + .find_reference( + remote + .default_branch() + .context("failed to get remote default branch")? + .as_str() + .unwrap(), + ) + .context("failed to get remote reference")?; + let name = match ref_remote.name() { + Some(s) => s.to_string(), + None => String::from_utf8_lossy(ref_remote.name_bytes()).to_string(), + }; + let msg = format!("Fast-Forward: Setting {} to id: {}", name, fetch_head.id()); + println!("{}", msg); + ref_remote + .set_target(fetch_head.id(), &msg) + .context("failed to set target")?; + repo.checkout_head(Some(CheckoutBuilder::default().force())) + .context("failed to checkout")?; + } + ma if ma.is_unborn() => { + warn!("HEAD is invalid (unborn)"); + return Err(anyhow!( + "HEAD is invalid: merge_analysis: {:?}", + merge_analysis + )); + } + ma if ma.is_normal() => { + error!("unable to fast-forward. manual merge is required"); + return Err(anyhow!("unable to fast-forward. manual merge is required")); + } + ma if ma.is_none() => { + error!("no merge is possible"); + return Err(anyhow!("no merge is possible")); + } + _ma => { + error!( + "this code must not reachable: merge_analysis {:?}", + merge_analysis + ); + return Err(anyhow!("must not be reachabel (uncovered merge_analysis)")); + } + } + + // push + let callbacks = remote_callback(&use_sshagent, &ssh_key); + let mut push_options = PushOptions::new(); + push_options.remote_callbacks(callbacks); + trace!("remote: {:?}", remote.name()); + let num_refspecs = remote + .refspecs() + .filter(|rs| rs.direction() == git2::Direction::Push) + .count(); + if num_refspecs > 1 { + warn!("more than one push refspecs are configured"); + warn!("using the first one"); + } + let head = repo.head().context("Failed to get HEAD")?; + if num_refspecs >= 1 { + trace!("using push refspec"); + let push_refspec = remote + .refspecs() + .filter_map(|rs| match rs.direction() { + git2::Direction::Fetch => None, + git2::Direction::Push => Some(rs), + }) + .next() + .expect("this must be unreachabe") + .str() + .context("failed to get valid utf8 push refspec")? + .to_string(); + remote.push(&[push_refspec.as_str()] as &[&str], Some(&mut push_options))?; + } else { + trace!("using head as push refspec"); + trace!("head is branch: {:?}", head.is_branch()); + trace!("head is remote: {:?}", head.is_remote()); + let push_refspec = head.name().context("failed to get head name")?; + remote.push(&[push_refspec] as &[&str], Some(&mut push_options))?; + }; + Ok(()) +} + +fn remote_callback<'b, 'a>( + use_sshagent: &'a bool, + ssh_key: &'b Option, +) -> RemoteCallbacks<'a> +where + 'b: 'a, +{ // using credentials let mut callbacks = RemoteCallbacks::new(); callbacks - .credentials(|_url, username_from_url, _allowed_types| { - if let Some(key) = &ssh_key { + .credentials(move |_url, username_from_url, _allowed_types| { + if let Some(ref key) = ssh_key { info!("Using provided ssh key to access the repository"); let passwd = match inquire::Password::new("SSH passphrase").prompt() { std::result::Result::Ok(s) => Some(s), @@ -42,7 +187,7 @@ pub(crate) fn cmd_sync( key as &Path, passwd.as_deref(), ) - } else if use_sshagent { + } else if *use_sshagent { // use ssh agent info!("Using ssh agent to access the repository"); Cred::ssh_key_from_agent( @@ -54,41 +199,49 @@ pub(crate) fn cmd_sync( panic!("This option must be unreachable.") } }) + .transfer_progress(|progress| { + if progress.received_objects() == progress.total_objects() { + print!( + "Resolving deltas {}/{}\r", + progress.indexed_deltas(), + progress.total_deltas() + ); + } else { + print!( + "Received {}/{} objects ({}) in {} bytes\r", + progress.received_objects(), + progress.total_objects(), + progress.indexed_objects(), + progress.received_bytes(), + ); + } + io::stdout().flush().unwrap(); + true + }) + .sideband_progress(|text| { + let msg = String::from_utf8_lossy(text); + eprintln!("remote: {msg}"); + true + }) .push_transfer_progress(|current, total, bytes| { - trace!("{current},\t{total},\t{bytes}"); + trace!("{current}/{total} files sent \t{bytes} bytes"); + }) + .push_update_reference(|reference_name, status_msg| { + debug!("remote reference_name {reference_name}"); + match status_msg { + None => { + info!("successfully pushed"); + eprintln!("successfully pushed to {}", reference_name); + Ok(()) + } + Some(status) => { + error!("failed to push: {}", status); + Err(git2::Error::from_str(&format!( + "failed to push to {}: {}", + reference_name, status + ))) + } + } }); - callbacks.push_update_reference(|reference_name, status_msg| { - debug!("remote reference_name {reference_name}"); - match status_msg { - None => { - info!("successfully pushed"); - eprintln!("successfully pushed to {}", reference_name); - Ok(()) - } - Some(status) => { - error!("failed to push: {}", status); - Err(git2::Error::from_str(&format!( - "failed to push to {}: {}", - reference_name, status - ))) - } - } - }); - let mut push_options = PushOptions::new(); - push_options.remote_callbacks(callbacks); - let mut remote = repo.find_remote(&remote_name)?; - trace!("remote: {:?}", remote.name()); - if remote.refspecs().len() != 1 { - warn!("multiple refspecs found"); - } - trace!("refspec: {:?}", remote.get_refspec(0).unwrap().str()); - trace!("refspec: {:?}", remote.get_refspec(0).unwrap().direction()); - trace!("refspec: {:?}", repo.head().unwrap().name()); - trace!("head is branch: {:?}", repo.head().unwrap().is_branch()); - trace!("head is remote: {:?}", repo.head().unwrap().is_remote()); - remote.push( - &[repo.head().unwrap().name().unwrap()] as &[&str], - Some(&mut push_options), - )?; - Ok(()) + callbacks }