Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 183 additions & 2 deletions src/git/mod.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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::AnnotatedCommit<'a>, 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<Oid, Error> {
let repository = &repo.repo;
let mut index:Index = repository.index()?;
Expand Down
43 changes: 32 additions & 11 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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();
Expand All @@ -205,20 +210,36 @@ fn watch(config: BackerConfig) -> notify::Result<()> {
let mut _guard: Option<Guard> = 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")
Expand Down