326 lines
9.1 KiB
Rust
326 lines
9.1 KiB
Rust
extern crate git2;
|
||
extern crate pretty_env_logger;
|
||
#[macro_use]
|
||
extern crate log;
|
||
extern crate indicatif;
|
||
|
||
use git2::{Cred, Error, FetchOptions, Index, IndexEntry, IndexTime, Progress, PushOptions, RemoteCallbacks, ResetType, Repository, Signature, TreeBuilder};
|
||
use git2::build::{CheckoutBuilder, RepoBuilder};
|
||
use std::fs::read;
|
||
use std::path::Path;
|
||
use std::string::String;
|
||
use std::str;
|
||
use indicatif::{ProgressBar, ProgressStyle};
|
||
|
||
pub struct BenchmarkRepository
|
||
{
|
||
repository: Repository,
|
||
ssh_user: String,
|
||
user_name: String,
|
||
user_email: String,
|
||
}
|
||
|
||
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!("Couldn’t 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 update_branch_storage(&self, branch_name: &str) -> &Self
|
||
{
|
||
self.fetch_branch(branch_name);
|
||
|
||
let mut progress_bar = ProgressBar::new(0);
|
||
progress_bar.set_style(BenchmarkRepository::progress_bar_style());
|
||
|
||
{
|
||
let mut checkout_builder = CheckoutBuilder::new();
|
||
|
||
let workdir = self.repository.path().parent().expect("invalid repository storage path");
|
||
let branch_storage_path = workdir.join(branch_name);
|
||
|
||
checkout_builder.force();
|
||
checkout_builder.target_dir(&branch_storage_path);
|
||
checkout_builder.progress(
|
||
|path, current, total| BenchmarkRepository::checkout_progress_callback(path, current, total, &mut progress_bar));
|
||
|
||
let object_id = match self.repository.refname_to_id(&format!("refs/remotes/origin/{}", branch_name))
|
||
{
|
||
Ok(object_id) => object_id,
|
||
Err(error) => panic!("could not look up branch “{}” from remote “origin”: {}", branch_name, error),
|
||
};
|
||
|
||
let object = match self.repository.find_object(object_id, None)
|
||
{
|
||
Ok(object) => object,
|
||
Err(error) => panic!("could not retrieve branch “{}” from remote “origin”: {}", branch_name, error),
|
||
};
|
||
|
||
info!("Updating branch storage “{}”", branch_name);
|
||
|
||
if let Err(error) = self.repository.checkout_tree(&object, Some(&mut checkout_builder))
|
||
{
|
||
panic!("failed to update branch storage for “{}”: {}", branch_name, error);
|
||
}
|
||
}
|
||
|
||
progress_bar.finish_and_clear();
|
||
|
||
trace!("Branch storage “{}” 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(),
|
||
};
|
||
|
||
benchmark_repository
|
||
.reset_origin(remote_url)
|
||
.fetch_branch("test-config")
|
||
.fetch_branch("test-results")
|
||
.fetch_branch("test-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_file(&self, file_path: &Path, result_file_path: &Path, 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),
|
||
};
|
||
|
||
let index_entry = self.read_file_as_index_entry(file_path, result_file_path);
|
||
|
||
if let Err(error) = index.add(&index_entry)
|
||
{
|
||
panic!("Could not add index entry: {}", 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 file “{}”", result_file_path.display());
|
||
|
||
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("couldn’t push");
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests
|
||
{
|
||
}
|