From 9316290d2898103cc77a23075cee5db0699ed7b5 Mon Sep 17 00:00:00 2001 From: qwjyh Date: Sun, 23 Feb 2025 03:09:45 +0900 Subject: [PATCH] new(sync): implement sync subcommand (WIP) TODO - update CHANGELOG - refactor sync func --- src/cmd_args.rs | 6 ++++ src/cmd_init.rs | 8 ++--- src/cmd_sync.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++--- src/git.rs | 40 +++++++++++++++++++++++++ src/main.rs | 7 ++++- tests/cli.rs | 15 +++++----- 6 files changed, 137 insertions(+), 19 deletions(-) create mode 100644 src/git.rs diff --git a/src/cmd_args.rs b/src/cmd_args.rs index c0b46b7..1c3eb3c 100644 --- a/src/cmd_args.rs +++ b/src/cmd_args.rs @@ -63,6 +63,12 @@ pub(crate) enum Commands { Sync { /// Remote name to sync. remote_name: Option, + /// Whether to use ssh-agent + #[arg(long)] + use_sshagent: bool, + /// Manually specify ssh key + #[arg(long)] + ssh_key: Option, }, /// Check config files validity. diff --git a/src/cmd_init.rs b/src/cmd_init.rs index 415489a..7207202 100644 --- a/src/cmd_init.rs +++ b/src/cmd_init.rs @@ -40,9 +40,7 @@ fn clone_repo( } }; Cred::ssh_key( - username_from_url - .context("No username found from the url") - .unwrap(), + username_from_url.ok_or(git2::Error::from_str("No username found from the url"))?, None, key as &Path, passwd.as_deref(), @@ -51,9 +49,7 @@ fn clone_repo( // use ssh agent info!("Using ssh agent to access the repository"); Cred::ssh_key_from_agent( - username_from_url - .context("No username found from the url") - .unwrap(), + username_from_url.ok_or(git2::Error::from_str("No username found from the url"))?, ) } else { error!("no ssh_key and use_sshagent"); diff --git a/src/cmd_sync.rs b/src/cmd_sync.rs index eb477ca..2c2a213 100644 --- a/src/cmd_sync.rs +++ b/src/cmd_sync.rs @@ -1,9 +1,14 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use anyhow::{anyhow, Result}; -use git2::Repository; +use git2::{Cred, PushOptions, RemoteCallbacks, Repository}; -pub(crate) fn cmd_sync(config_dir: &PathBuf, remote_name: Option) -> Result<()> { +pub(crate) fn cmd_sync( + config_dir: &PathBuf, + remote_name: Option, + use_sshagent: bool, + ssh_key: Option, +) -> Result<()> { warn!("Experimental"); let repo = Repository::open(config_dir)?; let remote_name = match remote_name { @@ -16,7 +21,74 @@ pub(crate) fn cmd_sync(config_dir: &PathBuf, remote_name: Option) -> Res remotes.get(0).unwrap().to_string() } }; + + // using credentials + let mut callbacks = RemoteCallbacks::new(); + callbacks + .credentials(|_url, username_from_url, _allowed_types| { + if let Some(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), + Err(err) => { + error!("Failed to get ssh passphrase: {:?}", err); + None + } + }; + Cred::ssh_key( + username_from_url + .ok_or(git2::Error::from_str("No username found from the url"))?, + None, + key as &Path, + passwd.as_deref(), + ) + } else if use_sshagent { + // use ssh agent + info!("Using ssh agent to access the repository"); + Cred::ssh_key_from_agent( + username_from_url + .ok_or(git2::Error::from_str("No username found from the url"))?, + ) + } else { + error!("no ssh_key and use_sshagent"); + panic!("This option must be unreachable.") + } + }) + .push_transfer_progress(|current, total, bytes| { + trace!("{current},\t{total},\t{bytes}"); + }); + 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)?; - remote.push(&[] as &[&str], None)?; + 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(()) } diff --git a/src/git.rs b/src/git.rs new file mode 100644 index 0000000..4ff8970 --- /dev/null +++ b/src/git.rs @@ -0,0 +1,40 @@ +use std::path::{Path, PathBuf}; + +use git2::{Cred, RemoteCallbacks}; +use inquire::Password; + +pub(crate) fn get_credential<'a>( + use_sshagent: bool, + ssh_key: Option, +) -> RemoteCallbacks<'a> { + // using credentials + let mut callbacks = RemoteCallbacks::new(); + callbacks.credentials(move |_url, username_from_url, _allowed_types| { + if let Some(key) = &ssh_key { + info!("Using provided ssh key to access the repository"); + let passwd = match Password::new("SSH passphrase").prompt() { + std::result::Result::Ok(s) => Some(s), + Err(err) => { + error!("Failed to get ssh passphrase: {:?}", err); + None + } + }; + Cred::ssh_key( + username_from_url.ok_or(git2::Error::from_str("No username found from the url"))?, + None, + key as &Path, + passwd.as_deref(), + ) + } else if use_sshagent { + // use ssh agent + info!("Using ssh agent to access the repository"); + Cred::ssh_key_from_agent( + username_from_url.ok_or(git2::Error::from_str("No username found from the url"))?, + ) + } else { + error!("no ssh_key and use_sshagent"); + panic!("This option must be unreachable.") + } + }); + callbacks +} diff --git a/src/main.rs b/src/main.rs index 32cf7d6..41aab33 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,6 +35,7 @@ mod cmd_status; mod cmd_storage; mod cmd_sync; mod devices; +mod git; mod inquire_filepath_completer; mod storages; mod util; @@ -91,7 +92,11 @@ fn main() -> Result<()> { Commands::Path {} => { println!("{}", &config_dir.display()); } - Commands::Sync { remote_name } => cmd_sync::cmd_sync(&config_dir, remote_name)?, + Commands::Sync { + remote_name, + use_sshagent, + ssh_key, + } => cmd_sync::cmd_sync(&config_dir, remote_name, use_sshagent, ssh_key)?, Commands::Status { path, storage, diff --git a/tests/cli.rs b/tests/cli.rs index 06e3890..44e93cd 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -313,15 +313,14 @@ mod integrated_test { assert!(config_dir_2.join("backups").join("second.yml").exists()); // sync - std::process::Command::new("git") - .arg("push") - .current_dir(&config_dir_2) + Command::cargo_bin("xdbm")? + .arg("-c") + .arg(config_dir_2.path()) + .arg("sync") + .arg("-vvvv") .assert() - .success(); - // let repo_2 = Repository::open(config_dir_2)?; - // // return Err(anyhow!("{:?}", repo_2.remotes()?.iter().collect::>())); - // let mut repo_2_remote = repo_2.find_remote(repo_2.remotes()?.get(0).unwrap())?; - // repo_2_remote.push(&[] as &[&str], None)?; + .success() + .stderr(predicate::str::contains("successfully pushed")); std::process::Command::new("git") .arg("pull") .current_dir(&config_dir_1)