benchmark-repository-rs/src/lib.rs

475 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

extern crate git2;
extern crate pretty_env_logger;
#[macro_use]
extern crate log;
extern crate indicatif;
extern crate notify;
extern crate tempfile;
use git2::{Cred, Error, FetchOptions, Index, IndexEntry, IndexTime, Progress, PushOptions, RemoteCallbacks, ResetType, Repository, Signature, TreeBuilder};
use git2::build::{CheckoutBuilder, RepoBuilder};
use std::fs::{File, create_dir_all, read};
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::string::String;
use std::str;
use std::sync::mpsc::{channel, Sender, Receiver};
use std::thread::{spawn, JoinHandle};
use indicatif::{ProgressBar, ProgressStyle};
use notify::{raw_watcher, RecommendedWatcher, RawEvent, Op};
use tempfile::TempDir;
pub struct BenchmarkRepository
{
repository: Repository,
ssh_user: String,
user_name: String,
user_email: String,
autocommit_channel: (Sender<RawEvent>, Receiver<RawEvent>),
autocommit_directory: Option<TempDir>,
autocommit_thread: Option<JoinHandle<()>>,
autocommit_watcher: Option<RecommendedWatcher>,
}
pub struct TargetPath<'a>
{
pub source: &'a Path,
pub destination: &'a Path,
}
impl BenchmarkRepository
{
fn progress_bar_style() -> ProgressStyle
{
ProgressStyle::default_bar()
.template("{bar:74.on_black} {percent:>3} %")
.progress_chars("█▉▊▋▌▍▎▏ ")
}
fn transfer_progress_callback(progress: Progress, progress_bar: &mut ProgressBar) -> bool
{
progress_bar.set_length(progress.total_objects() as u64);
progress_bar.set_position(progress.received_objects() as u64);
// continue the transfer
true
}
fn push_update_reference_callback(reference: &str, status: Option<&str>) -> Result<(), Error>
{
match status
{
None => trace!("Reference “{}” pushed successfully to remote “origin”", reference),
Some(error) => panic!("Couldnt push reference “{}” to remote “origin”: {}", reference, error),
};
Ok(())
}
fn checkout_progress_callback(path: Option<&Path>, current: usize, total: usize, progress_bar: &mut ProgressBar)
{
progress_bar.set_length(total as u64);
progress_bar.set_position(current as u64);
}
fn reset_origin(&self, remote_url: &str) -> &Self
{
let remote = match self.repository.find_remote("origin")
{
Ok(remote) => remote,
Err(_) =>
match self.repository.remote("origin", remote_url)
{
Ok(remote) => remote,
Err(error) => panic!("Could not reset remote “origin”: {}", error),
},
};
info!("Reset origin to “{}”", remote_url);
self
}
fn fetch_branch(&self, branch_name: &str) -> &Self
{
let mut progress_bar = ProgressBar::new(0);
progress_bar.set_style(BenchmarkRepository::progress_bar_style());
{
let mut remote_callbacks = RemoteCallbacks::new();
remote_callbacks.credentials(
|_, _, _|
{
match Cred::ssh_key_from_agent(&self.ssh_user)
{
Ok(credentials) => Ok(credentials),
Err(error) => panic!("could not retrieve key pair for SSH authentication as user “{}”: {}", self.ssh_user, error),
}
});
remote_callbacks.transfer_progress(
|progress| BenchmarkRepository::transfer_progress_callback(progress, &mut progress_bar));
let mut fetch_options = FetchOptions::new();
fetch_options.remote_callbacks(remote_callbacks);
let mut origin = self.repository.find_remote("origin").expect("could not find remote “origin”");
info!("Updating branch “{}”", branch_name);
if let Err(error) = origin.fetch(&[branch_name], Some(&mut fetch_options), None)
{
panic!("failed to fetch branch “{}” from remote “origin”: {}", branch_name, error);
}
}
progress_bar.finish_and_clear();
trace!("Branch “{}” is up-to-date", branch_name);
self
}
fn init(base_path: &Path) -> Repository
{
let repository = match Repository::init_bare(base_path)
{
Ok(repository) => repository,
Err(error) => panic!("failed to initialize Git repository in “{}”: {}", base_path.display(), error),
};
info!("Initialized Git repository in “{}”", base_path.display());
repository
}
pub fn new(remote_url: &str, base_path: &Path, ssh_user: &str, user_name: &str, user_email: &str) -> BenchmarkRepository
{
let repository = match Repository::open(base_path)
{
Ok(repository) =>
{
info!("Using existing Git repository");
repository
},
Err(_) => BenchmarkRepository::init(base_path),
};
let benchmark_repository =
BenchmarkRepository
{
repository: repository,
ssh_user: ssh_user.to_string(),
user_name: user_name.to_string(),
user_email: user_email.to_string(),
autocommit_channel: channel(),
autocommit_directory: None,
autocommit_thread: None,
autocommit_watcher: None,
};
benchmark_repository
.reset_origin(remote_url)
.fetch_branch("config")
.fetch_branch("results")
.fetch_branch("status");
benchmark_repository
}
pub fn read_file_as_index_entry(&self, file_path: &Path, result_file_path: &Path) -> IndexEntry
{
// create a new blob with the file contents
let object_id = match self.repository.blob_path(file_path)
{
Ok(object_id) => object_id,
Err(error) => panic!("Could not write blob for “{}”: {}", file_path.display(), error),
};
info!("Created object “{}” from “{}”", object_id, file_path.display());
IndexEntry
{
ctime: IndexTime::new(0, 0),
mtime: IndexTime::new(0, 0),
dev: 0,
ino: 0,
mode: 0o100644,
uid: 0,
gid: 0,
file_size: 0,
id: object_id,
flags: 0,
flags_extended: 0,
path: result_file_path.to_string_lossy().as_bytes().to_owned(),
}
}
pub fn commit_files(&self, file_paths: &[TargetPath], branch_name: &str)
{
let tip_reference_name = format!("refs/remotes/origin/{}", branch_name);
let tip_reference = match self.repository.find_reference(&tip_reference_name)
{
Ok(value) => value,
Err(error) => panic!("Could not find reference “{}”: {}", tip_reference_name, error),
};
let parent_tree = match tip_reference.peel_to_tree()
{
Ok(value) => value,
Err(error) => panic!("Could not peel reference to tree: {}", error),
};
let mut index = match Index::new()
{
Ok(value) => value,
Err(error) => panic!("Could not create index: {}", error),
};
match index.read_tree(&parent_tree)
{
Ok(value) => value,
Err(error) => panic!("Could not read parent tree into index: {}", error),
};
for target_path in file_paths
{
let index_entry = self.read_file_as_index_entry(target_path.source, target_path.destination);
if let Err(error) = index.add(&index_entry)
{
panic!("Could not add index entry for “{}”: {}", target_path.destination.display(), error);
}
}
let tree_object_id = match index.write_tree_to(&self.repository)
{
Ok(value) => value,
Err(error) => panic!("Could not write index to tree: {}", error),
};
let tree = match self.repository.find_tree(tree_object_id)
{
Ok(value) => value,
Err(error) => panic!("Could obtain tree: {}", error),
};
info!("Created tree object “{}”", tree_object_id);
let signature = Signature::now(&self.user_name, &self.user_email).expect("Could not create signature");
let message = format!("Add files");
let parent = match tip_reference.peel_to_commit()
{
Ok(value) => value,
Err(error) => panic!("Could not peel reference: {}", error),
};
let commit_id = match self.repository.commit(Some(&tip_reference_name), &signature, &signature, &message, &tree, &[&parent])
{
Ok(value) => value,
Err(error) => panic!("Could not write commit: {}", error),
};
let push_refspec = format!("refs/remotes/origin/{}:refs/heads/{}", branch_name, branch_name);
trace!("Created commit “{}”, using refspec “{}”", commit_id, push_refspec);
let mut remote_callbacks = RemoteCallbacks::new();
remote_callbacks.credentials(
|_, _, _|
{
match Cred::ssh_key_from_agent(&self.ssh_user)
{
Ok(value) => Ok(value),
Err(error) => panic!("could not retrieve key pair for SSH authentication as user “{}”: {}", self.ssh_user, error),
}
});
remote_callbacks.push_update_reference(
|reference, status| BenchmarkRepository::push_update_reference_callback(reference, status));
let mut push_options = PushOptions::new();
push_options.remote_callbacks(remote_callbacks);
let mut remote = self.repository.find_remote("origin").expect("");
remote.push(&[&push_refspec], Some(&mut push_options)).expect("couldnt push");
}
pub fn file_exists(&self, file_path: &Path, branch_name: &str) -> bool
{
let tip_reference_name = format!("refs/remotes/origin/{}", branch_name);
let tip_reference = match self.repository.find_reference(&tip_reference_name)
{
Ok(value) => value,
Err(error) => panic!("Could not find reference “{}”: {}", tip_reference_name, error),
};
let tree = match tip_reference.peel_to_tree()
{
Ok(value) => value,
Err(error) => panic!("Could not peel reference to tree: {}", error),
};
let object_id = match tree.get_path(file_path)
{
Ok(tree_entry) => tree_entry.id(),
Err(error) => return false,
};
let blob = match self.repository.find_blob(object_id)
{
Ok(blob) => blob,
Err(error) => return false,
};
true
}
pub fn read_file(&self, file_path: &Path, branch_name: &str) -> Option<String>
{
let tip_reference_name = format!("refs/remotes/origin/{}", branch_name);
let tip_reference = match self.repository.find_reference(&tip_reference_name)
{
Ok(value) => value,
Err(error) => panic!("Could not find reference “{}”: {}", tip_reference_name, error),
};
let tree = match tip_reference.peel_to_tree()
{
Ok(value) => value,
Err(error) => panic!("Could not peel reference to tree: {}", error),
};
let object_id = match tree.get_path(file_path)
{
Ok(tree_entry) => tree_entry.id(),
Err(error) => return None,
};
let blob = match self.repository.find_blob(object_id)
{
Ok(blob) => blob,
Err(error) => return None,
};
let content = match std::str::from_utf8(blob.content())
{
Ok(content) => content,
Err(error) => panic!("Could not interpret file “{}” as UTF-8: {}", file_path.display(), error),
};
Some(content.to_owned())
}
fn prepare_autocommit_directory(&mut self)
{
match self.autocommit_thread
{
Some(_) => (),
None =>
{
let tmp_dir = match TempDir::new()
{
Ok(value) => value,
Err(error) => panic!("Could not create autocommit directory: {}", error),
};
let lock_file_path = tmp_dir.path().join(".lock");
if let Err(error) = File::create(&lock_file_path)
{
panic!("Could not create lock file “{}”: {}", lock_file_path.display(), error);
}
let (sender, receiver) = channel();
let watcher = match raw_watcher(sender)
{
Ok(value) => value,
Err(error) => panic!("Could not create filesystem watcher: {}", error),
};
self.autocommit_watcher = Some(watcher);
self.autocommit_directory = Some(tmp_dir);
trace!("{:#?}", receiver);
let thread = spawn(
move ||
{
trace!("Autocommit thread running");
trace!("{:#?}", receiver);
loop
{
match receiver.recv()
{
Ok(RawEvent{path: Some(path), op: Ok(Op::REMOVE), cookie: _}) =>
{
trace!("Received “remove” event for path “{}”", path.display());
},
Err(error) => panic!("Error handling notify event: {}", error),
_ => (),
}
}
});
self.autocommit_thread = Some(thread);
}
}
}
pub fn create_autocommit_directory(&mut self, directory_path: &Path, branch_name: &str) -> Result<PathBuf, String>
{
self.prepare_autocommit_directory();
let result_directory_path = self.autocommit_directory.as_ref().unwrap().path().join(branch_name).join(directory_path);
match create_dir_all(&result_directory_path)
{
Ok(_) => (),
Err(error) => panic!("Could not create result directory: {}", error),
}
{
let lock_file_path = result_directory_path.join(".lock");
if lock_file_path.exists()
{
panic!("Autocommit directory “{}” already locked", lock_file_path.display());
}
if let Err(error) = File::create(&lock_file_path)
{
panic!("Could not create temporary file “{}”: {}", lock_file_path.display(), error);
}
}
Ok(result_directory_path)
}
pub fn wait_for_autocommit_thread(mut self)
{
if let None = self.autocommit_thread
{
panic!("No autocommit thread started");
}
let autocommit_thread = self.autocommit_thread;
if let Err(RecvError) = autocommit_thread.unwrap().join()
{
panic!("Could not join autocommit thread");
}
self.autocommit_thread = None;
}
}
#[cfg(test)]
mod tests
{
}