diff --git a/src/git/mod.rs b/src/git/mod.rs index e7b6dc8..973478c 100644 --- a/src/git/mod.rs +++ b/src/git/mod.rs @@ -1,8 +1,9 @@ -use git2::{Commit, ObjectType, Repository, Index, Error, Signature, Oid}; +use git2::{Commit, Cred, CredentialType, ObjectType, Repository, Index, Error, Signature, Oid}; use git2::IndexAddOption; use std::path::Path; +use std::io::{self, Write}; -use log::{error, trace}; +use log::{error, trace, warn}; pub struct Repo<'a> { pub repo_path: &'a str, @@ -30,6 +31,186 @@ impl<'a> Repo<'a> { } } + +fn do_fetch<'a>( + repo: &'a git2::Repository, + refs: &[&str], + remote: &'a mut git2::Remote, + ssh_pkey: &str, +) -> Result, git2::Error> { + let mut cb = git2::RemoteCallbacks::new(); + // Print out our transfer progress. + cb.credentials( + move |_url: &str, _uname: Option<&str>, _ctype: CredentialType| { + Cred::ssh_key("git", None, Path::new(ssh_pkey), None) + }, + ); + cb.transfer_progress(|stats| { + if stats.received_objects() == stats.total_objects() { + print!( + "Resolving deltas {}/{}\r", + stats.indexed_deltas(), + stats.total_deltas() + ); + } else if stats.total_objects() > 0 { + print!( + "Received {}/{} objects ({}) in {} bytes\r", + stats.received_objects(), + stats.total_objects(), + stats.indexed_objects(), + stats.received_bytes() + ); + } + io::stdout().flush().unwrap(); + true + }); + + let mut fo = git2::FetchOptions::new(); + fo.remote_callbacks(cb); + // Always fetch all tags. + // Perform a download and also update tips + fo.download_tags(git2::AutotagOption::All); + trace!("Fetching {} for repo", remote.name().unwrap()); + remote.fetch(refs, Some(&mut fo), None)?; + + // If there are local objects (we got a thin pack), then tell the user + // how many objects we saved from having to cross the network. + let stats = remote.stats(); + if stats.local_objects() > 0 { + trace!( + "\rReceived {}/{} objects in {} bytes (used {} local \ + objects)", + stats.indexed_objects(), + stats.total_objects(), + stats.received_bytes(), + stats.local_objects() + ); + } else { + trace!( + "\rReceived {}/{} objects in {} bytes", + stats.indexed_objects(), + stats.total_objects(), + stats.received_bytes() + ); + } + + let fetch_head = repo.find_reference("FETCH_HEAD")?; + Ok(repo.reference_to_annotated_commit(&fetch_head)?) +} + +fn fast_forward( + repo: &Repository, + lb: &mut git2::Reference, + rc: &git2::AnnotatedCommit, +) -> Result<(), git2::Error> { + let name = match lb.name() { + Some(s) => s.to_string(), + None => String::from_utf8_lossy(lb.name_bytes()).to_string(), + }; + let msg = format!("Fast-Forward: Setting {} to id: {}", name, rc.id()); + trace!("{}", msg); + lb.set_target(rc.id(), &msg)?; + repo.set_head(&name)?; + repo.checkout_head(Some( + git2::build::CheckoutBuilder::default() + // For some reason the force is required to make the working directory actually get updated + // I suspect we should be adding some logic to handle dirty working directory states + // but this is just an example so maybe not. + .force(), + ))?; + Ok(()) +} + +fn normal_merge( + repo: &Repository, + local: &git2::AnnotatedCommit, + remote: &git2::AnnotatedCommit, +) -> Result<(), git2::Error> { + let local_tree = repo.find_commit(local.id())?.tree()?; + let remote_tree = repo.find_commit(remote.id())?.tree()?; + let ancestor = repo + .find_commit(repo.merge_base(local.id(), remote.id())?)? + .tree()?; + let mut idx = repo.merge_trees(&ancestor, &local_tree, &remote_tree, None)?; + + if idx.has_conflicts() { + warn!("Merge conficts detected..."); + repo.checkout_index(Some(&mut idx), None)?; + return Ok(()); + } + let result_tree = repo.find_tree(idx.write_tree_to(repo)?)?; + // now create the merge commit + let msg = format!("Merge: {} into {}", remote.id(), local.id()); + let sig = repo.signature()?; + let local_commit = repo.find_commit(local.id())?; + let remote_commit = repo.find_commit(remote.id())?; + // Do our merge commit and set current branch head to that commit. + let _merge_commit = repo.commit( + Some("HEAD"), + &sig, + &sig, + &msg, + &result_tree, + &[&local_commit, &remote_commit], + )?; + // Set working tree to match head. + repo.checkout_head(None)?; + Ok(()) +} + +fn do_merge<'a>( + repo: &'a Repository, + remote_branch: &str, + fetch_commit: git2::AnnotatedCommit<'a>, +) -> Result<(), git2::Error> { + // 1. do a merge analysis + let analysis = repo.merge_analysis(&[&fetch_commit])?; + + // 2. Do the appopriate merge + if analysis.0.is_fast_forward() { + trace!("Doing a fast forward"); + // do a fast forward + let refname = format!("refs/heads/{}", remote_branch); + match repo.find_reference(&refname) { + Ok(mut r) => { + fast_forward(repo, &mut r, &fetch_commit)?; + } + Err(_) => { + // The branch doesn't exist so just set the reference to the + // commit directly. Usually this is because you are pulling + // into an empty repository. + repo.reference( + &refname, + fetch_commit.id(), + true, + &format!("Setting {} to {}", remote_branch, fetch_commit.id()), + )?; + repo.set_head(&refname)?; + repo.checkout_head(Some( + git2::build::CheckoutBuilder::default() + .allow_conflicts(true) + .conflict_style_merge(true) + .force(), + ))?; + } + }; + } else if analysis.0.is_normal() { + // do a normal merge + let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?; + normal_merge(&repo, &head_commit, &fetch_commit)?; + } else { + trace!("Nothing to do..."); + } + Ok(()) +} + +pub fn pull(repo: &Repo, ssh_pkey: &str) -> Result<(), Error> { + let repository = &repo.repo; + let mut remote = repository.find_remote("origin")?; + let fetch_commit = do_fetch(&repository, &["master"], &mut remote, ssh_pkey)?; + do_merge(&repository, &"master", fetch_commit) +} + pub fn add_all_and_commit(repo: &Repo, message: &str, sign_name: &str, sign_email: &str) -> Result { let repository = &repo.repo; let mut index:Index = repository.index()?; diff --git a/src/main.rs b/src/main.rs index aa45432..5fc95d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,8 @@ extern crate notify; mod git; -use notify::{RecommendedWatcher, RecursiveMode, Watcher}; +use std::path::PathBuf; +use notify::{RecommendedWatcher, RecursiveMode, Watcher, DebouncedEvent}; use std::sync::mpsc::{channel}; use std::time::Duration; @@ -134,8 +135,12 @@ fn init_repo(repo_path: String, remote_url: Option<&str>) { fn add_all_changed(repo_path: &str, default_commit_msg: &str, sign_name: &str, sign_email: &str, should_push: bool, ssh_pkey: &str) { let mut repo = git::Repo::open(&repo_path); match git::add_all_and_commit(&mut repo, &default_commit_msg, &sign_name, &sign_email) { - Ok(oid) => { + Ok(oid) => { trace!("Commit id {}",oid); + match git::pull(&mut repo, ssh_pkey) { + Ok(_) => trace!("Pull successful"), + Err(e) => error!("Pull failed {:?}", e), + } if should_push { let callback = move |_url: &str, _uname: Option<&str>, _ctype: CredentialType| { Cred::ssh_key("git", None, Path::new(ssh_pkey), None) @@ -191,7 +196,7 @@ fn watch(config: BackerConfig) -> notify::Result<()> { // Add a path to be watched. All files and directories at that path and // below will be monitored for changes. - watcher.watch(&repo_path, RecursiveMode::NonRecursive)?; + watcher.watch(&repo_path, RecursiveMode::Recursive)?; let time_done = Arc::new(AtomicBool::new(false)); let timer = timer::Timer::new(); @@ -205,20 +210,36 @@ fn watch(config: BackerConfig) -> notify::Result<()> { let mut _guard: Option = None; loop { match rx.recv() { - Ok(event) => { - trace!("{:?}", event); - if ! time_done.load(Ordering::Relaxed) { - //let time_done1 = time_done.clone(); - _guard = Some(timer.schedule_with_delay(chrono::Duration::seconds(commit_delay), callback.clone())); - trace!("Commit timer started, will be committed in {} seconds", commit_delay); - time_done.store(true, Ordering::Relaxed); - } + Ok(event) => match event { + DebouncedEvent::NoticeWrite(path) + | DebouncedEvent::NoticeRemove(path) + | DebouncedEvent::Create(path) + | DebouncedEvent::Write(path) + | DebouncedEvent::Chmod(path) + | DebouncedEvent::Remove(path) => { + if !is_git_folder(path.clone()) && ! time_done.load(Ordering::Relaxed) { + trace!("{:?}", path); + _guard = Some(timer.schedule_with_delay(chrono::Duration::seconds(commit_delay), callback.clone())); + trace!("Commit timer started, will be committed in {} seconds", commit_delay); + time_done.store(true, Ordering::Relaxed); + } + }, + _ => {} }, Err(e) => error!("watch error: {:?}", e), } } } +fn is_git_folder(path: PathBuf) -> bool { + return path + .clone() + .into_os_string() + .into_string() + .unwrap() + .contains(".git"); +} + fn show_desktop_notification(body: &str, timeout: notify_rust::Timeout) { if let Err(e) = Notification::new() .summary("Category: backer-rs")