benchmark-repository-rs/src/lib.rs

458 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.

use git2::{Cred, Error, FetchOptions, Index, IndexEntry, IndexTime, Oid, Progress, PushOptions, RemoteCallbacks, Repository, Signature};
use indicatif::{ProgressBar, ProgressStyle};
use log::{info, trace};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::string::String;
use std::str;
use walkdir::WalkDir;
pub struct Job
{
pub id: u32,
pub key: String,
pub result_repository_path: PathBuf,
}
pub struct BenchmarkRepository
{
base_path: PathBuf,
repository: Repository,
jobs: HashMap<u32, Job>,
ssh_user: String,
user_name: String,
user_email: String,
}
pub struct TargetPath
{
pub source: PathBuf,
pub destination: PathBuf,
}
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 reset_origin(&self, remote_url: &str) -> &Self
{
if let Err(error) = self.repository.find_remote("origin")
.or_else(|_| self.repository.remote("origin", remote_url))
{
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_path = base_path.join("upstream");
let repository = match Repository::init_bare(&repository_path)
{
Ok(repository) => repository,
Err(error) => panic!("failed to initialize Git repository in “{}”: {}", repository_path.display(), error),
};
info!("Initialized Git repository in “{}”", repository_path.display());
repository
}
pub fn new(remote_url: &str, base_path: PathBuf, 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
{
base_path: base_path,
repository: repository,
jobs: HashMap::new(),
ssh_user: ssh_user.to_string(),
user_name: user_name.to_string(),
user_email: user_email.to_string(),
};
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_directory(&self, directory_path: &TargetPath, branch_name: &str)
{
let mut file_paths = vec![];
for entry in WalkDir::new(&directory_path.source)
{
let entry = entry.unwrap();
if entry.path().file_name().unwrap() == ".lock" || entry.file_type().is_dir()
{
continue;
}
let relative_path = entry.path().strip_prefix(&directory_path.source).unwrap();
trace!("Adding “{}” (from “{}”) to autocommit list", relative_path.display(), entry.path().display());
file_paths.push(TargetPath{source: entry.path().to_owned(), destination: directory_path.destination.join(&relative_path)});
}
self.commit_files(&file_paths, branch_name);
}
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(_) => return false,
};
if let Err(_) = self.repository.find_blob(object_id)
{
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(_) => return None,
};
let blob = match self.repository.find_blob(object_id)
{
Ok(blob) => blob,
Err(_) => 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())
}
pub fn create_job(&mut self, key: String) -> &Job
{
let id = self.jobs.len() as u32;
let result_repository_path = self.base_path.join(format!("job-{}", id));
if result_repository_path.exists()
{
match std::fs::remove_dir_all(&result_repository_path)
{
Ok(_) => (),
Err(error) => panic!("failed to initialize result Git repository for job {} in “{}”: {}", id, result_repository_path.display(), error),
};
}
match Repository::init_bare(&result_repository_path)
{
Ok(repository) => repository,
Err(error) => panic!("failed to initialize result Git repository for job {} in “{}”: {}", id, result_repository_path.display(), error),
};
info!("Initialized result Git repository for job {} in “{}”", id, result_repository_path.display());
self.jobs.insert(id, Job{id, key, result_repository_path});
self.jobs.get(&id).unwrap()
}
fn tip_commit<'repository>(repository: &'repository git2::Repository, from_remote: bool, branch_name: &str) -> Result<git2::Commit<'repository>, git2::Error>
{
let tip_reference_name = match from_remote
{
true => format!("refs/remotes/origin/{}", branch_name),
false => format!("refs/heads/{}", branch_name),
};
repository.find_reference(&tip_reference_name).and_then(|tip_reference| tip_reference.peel_to_commit())
}
pub fn join(&self)
{
let mut active_job_ids = HashSet::new();
for (job_id, _) in &self.jobs
{
active_job_ids.insert(job_id);
}
while !active_job_ids.is_empty()
{
active_job_ids.retain
(
|job_id|
{
let job = self.jobs.get(job_id).unwrap();
let job_repository = match Repository::open(&job.result_repository_path)
{
Ok(value) => value,
Err(error) => panic!("cannot access result repository for job {}: {}", job_id, error),
};
let job_commit = match Self::tip_commit(&job_repository, false, "master")
{
Ok(value) => value,
// Job is not done yet, so skip it until it is
Err(_) => return true,
};
info!("job {} finished", job_id);
let remote_commit = match Self::tip_commit(&self.repository, true, "results")
{
Ok(value) => value,
Err(error) => panic!("could not access tip commit of “results” branch: {}", error),
};
false
}
);
println!("...");
std::thread::sleep(std::time::Duration::from_secs(2));
}
}
}
#[cfg(test)]
mod tests
{
}