Compare commits

..

No commits in common. "c2b6506db0b3326715ac8e1d27c32a851c04ae0a" and "9316290d2898103cc77a23075cee5db0699ed7b5" have entirely different histories.

3 changed files with 67 additions and 238 deletions

View file

@ -2,9 +2,6 @@
## [Unreleased]
### Added
- `sync` subcommand, which performs git pull (fast-forward) and push (#21)
### Fixed
- Git local config is now looked up. (#20)
- Git global config will not be polluted in test by default. (#20)

View file

@ -1,10 +1,7 @@
use std::{
io::{self, Write},
path::{Path, PathBuf},
};
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Context, Result};
use git2::{build::CheckoutBuilder, Cred, FetchOptions, PushOptions, RemoteCallbacks, Repository};
use anyhow::{anyhow, Result};
use git2::{Cred, PushOptions, RemoteCallbacks, Repository};
pub(crate) fn cmd_sync(
config_dir: &PathBuf,
@ -12,7 +9,7 @@ pub(crate) fn cmd_sync(
use_sshagent: bool,
ssh_key: Option<PathBuf>,
) -> Result<()> {
info!("cmd_sync");
warn!("Experimental");
let repo = Repository::open(config_dir)?;
let remote_name = match remote_name {
Some(remote_name) => remote_name,
@ -24,34 +21,12 @@ pub(crate) fn cmd_sync(
remotes.get(0).unwrap().to_string()
}
};
debug!("resolved remote name: {remote_name}");
let mut remote = repo.find_remote(&remote_name)?;
pull(
&repo,
&mut remote,
remote_name,
&use_sshagent,
ssh_key.as_ref(),
)?;
push(&repo, &mut remote, &use_sshagent, ssh_key.as_ref())?;
Ok(())
}
fn remote_callback<'b, 'a>(
use_sshagent: &'a bool,
ssh_key: Option<&'a PathBuf>,
) -> RemoteCallbacks<'a>
where
'b: 'a,
{
// using credentials
let mut callbacks = RemoteCallbacks::new();
callbacks
.credentials(move |_url, username_from_url, _allowed_types| {
if let Some(key) = ssh_key {
.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),
@ -67,7 +42,7 @@ where
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(
@ -79,34 +54,10 @@ where
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}/{total} files sent \t{bytes} bytes");
})
.push_update_reference(|reference_name, status_msg| {
trace!("{current},\t{total},\t{bytes}");
});
callbacks.push_update_reference(|reference_name, status_msg| {
debug!("remote reference_name {reference_name}");
match status_msg {
None => {
@ -123,153 +74,21 @@ where
}
}
});
callbacks
}
fn pull(
repo: &Repository,
remote: &mut git2::Remote,
remote_name: String,
use_sshagent: &bool,
ssh_key: Option<&PathBuf>,
) -> Result<()> {
debug!("pull");
let callbacks = remote_callback(use_sshagent, ssh_key);
let mut fetchoptions = FetchOptions::new();
fetchoptions.remote_callbacks(callbacks);
let fetch_refspec: Vec<String> = 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_none() => {
error!("no merge is possible");
return Err(anyhow!("no merge is possible"));
}
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 => {
error!(
"this code must not reachable: merge_analysis {:?}",
merge_analysis
);
return Err(anyhow!("must not be reachabel (uncovered merge_analysis)"));
}
}
Ok(())
}
fn push(
repo: &Repository,
remote: &mut git2::Remote,
use_sshagent: &bool,
ssh_key: Option<&PathBuf>,
) -> Result<()> {
debug!("push");
let callbacks = remote_callback(&use_sshagent, ssh_key);
let mut push_options = PushOptions::new();
push_options.remote_callbacks(callbacks);
let num_push_refspecs = remote
.refspecs()
.filter(|rs| rs.direction() == git2::Direction::Push)
.count();
if num_push_refspecs > 1 {
warn!("more than one push refspecs are configured");
warn!("using the first one");
let mut remote = repo.find_remote(&remote_name)?;
trace!("remote: {:?}", remote.name());
if remote.refspecs().len() != 1 {
warn!("multiple refspecs found");
}
let head = repo.head().context("Failed to get HEAD")?;
if num_push_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))?;
};
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(())
}

View file

@ -2,7 +2,6 @@ mod integrated_test {
use std::{
fs::{self, DirBuilder, File},
io::{self, BufWriter, Write},
path,
};
use anyhow::{anyhow, Context, Ok, Result};
@ -73,16 +72,6 @@ mod integrated_test {
Ok(())
}
fn run_sync_cmd(config_dir: &path::Path) -> Result<()> {
Command::cargo_bin("xdbm")?
.arg("-c")
.arg(config_dir)
.args(["sync", "-vvvv"])
.assert()
.success();
Ok(())
}
#[test]
fn single_device() -> Result<()> {
let config_dir = assert_fs::TempDir::new()?;
@ -391,8 +380,16 @@ mod integrated_test {
std::fs::read_to_string(config_dir_1.join("storages.yml"))?.contains("parent: gdrive1")
);
run_sync_cmd(&config_dir_1)?;
run_sync_cmd(&config_dir_2)?;
std::process::Command::new("git")
.arg("push")
.current_dir(&config_dir_1)
.assert()
.success();
std::process::Command::new("git")
.arg("pull")
.current_dir(&config_dir_2)
.assert()
.success();
// bind
//
@ -606,8 +603,16 @@ mod integrated_test {
.and(predicate::str::contains("foodoc").not()),
);
run_sync_cmd(&config_dir_2)?;
run_sync_cmd(&config_dir_1)?;
std::process::Command::new("git")
.arg("push")
.current_dir(&config_dir_2)
.assert()
.success();
std::process::Command::new("git")
.arg("pull")
.current_dir(&config_dir_1)
.assert()
.success();
// bind
//
@ -722,8 +727,16 @@ mod integrated_test {
.assert()
.success();
run_sync_cmd(&config_dir_1)?;
run_sync_cmd(&config_dir_2)?;
std::process::Command::new("git")
.arg("push")
.current_dir(&config_dir_1)
.assert()
.success();
std::process::Command::new("git")
.arg("pull")
.current_dir(&config_dir_2)
.assert()
.success();
// backup add
//