new(sync): implement sync subcommand (WIP)

TODO
- update CHANGELOG
- refactor sync func
This commit is contained in:
qwjyh 2025-02-23 03:09:45 +09:00
parent 47b3a5e69d
commit 9316290d28
6 changed files with 137 additions and 19 deletions

View file

@ -63,6 +63,12 @@ pub(crate) enum Commands {
Sync {
/// Remote name to sync.
remote_name: Option<String>,
/// Whether to use ssh-agent
#[arg(long)]
use_sshagent: bool,
/// Manually specify ssh key
#[arg(long)]
ssh_key: Option<PathBuf>,
},
/// Check config files validity.

View file

@ -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");

View file

@ -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<String>) -> Result<()> {
pub(crate) fn cmd_sync(
config_dir: &PathBuf,
remote_name: Option<String>,
use_sshagent: bool,
ssh_key: Option<PathBuf>,
) -> 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<String>) -> 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(())
}

40
src/git.rs Normal file
View file

@ -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<PathBuf>,
) -> 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
}

View file

@ -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,

View file

@ -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::<Vec<_>>()));
// 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)