use std::ffi::OsStr;
use std::io;
use std::path::PathBuf;
use git2::build::CheckoutBuilder;
use git2::{ObjectType, Repository, ResetType};
use super::email::EmailMessage;
use crate::utils;
pub struct BulkPatchApply<'repo> {
logger: slog::Logger,
target_repo: &'repo Repository,
patch_dir: PathBuf,
}
impl<'repo> BulkPatchApply<'repo> {
pub fn new(logger: &slog::Logger, target_repo: &'repo Repository, patch_dir: PathBuf) -> Self {
use utils::log::LogPathValue;
let logger = logger.new(slog::o!(
"target_repo" => LogPathValue::from(target_repo.workdir().unwrap_or(target_repo.path())),
"patch_dir" => LogPathValue::from(&*patch_dir)
));
BulkPatchApply {
logger,
target_repo,
patch_dir,
}
}
pub fn reset_upstream(&self, upstream_name: &str) -> Result<(), ResetUpstreamError> {
let obj = self
.target_repo
.resolve_reference_from_short_name(upstream_name)
.and_then(|reference| reference.peel(ObjectType::Any))
.map_err(|cause| ResetUpstreamError::InvalidReference {
upstream_name: upstream_name.into(),
cause,
})?;
let mut checkout = CheckoutBuilder::new();
checkout.remove_untracked(true);
self.target_repo
.reset(&obj, ResetType::Hard, Some(&mut checkout))
.map_err(|cause| ResetUpstreamError::FailedReset {
upstream_name: upstream_name.into(),
cause,
})?;
slog::info!(
self.logger, "Reset upstream";
"upstream" => upstream_name,
);
Ok(())
}
pub fn apply_all(self) -> Result<(), BulkApplyError> {
let entries = std::fs::read_dir(&self.patch_dir).map_err(|cause| {
BulkApplyError::ErrorAccessPatchDir {
cause,
patch_dir: self.patch_dir.clone(),
}
})?;
struct BufferedPatch {
patch_name: String,
patch_file: PathBuf,
email: EmailMessage,
}
let mut patch_files = Vec::new();
for entry in entries {
let entry = entry.map_err(|cause| BulkApplyError::ErrorAccessPatchDir {
cause,
patch_dir: self.patch_dir.clone(),
})?;
let full_patch_path = entry.path();
if full_patch_path.extension() != Some(OsStr::new("patch")) {
slog::debug!(
self.logger,
"Skipping non-patch directory entry";
"full_path" => full_patch_path.display(),
);
continue;
}
let file_name = entry
.file_name()
.into_string()
.map_err(|invalid_file_name| BulkApplyError::PatchNameInvalidUtf8 {
raw_entry: PathBuf::from(invalid_file_name),
})?;
let patch_name = file_name
.strip_suffix(".patch")
.unwrap_or_else(|| panic!("Patch file doesn't end with `.patch`: {file_name:?}"));
let patch_file_contents =
String::from_utf8(std::fs::read(&full_patch_path).map_err(|cause| {
BulkApplyError::FailedReadPatch {
cause,
patch_file: full_patch_path.clone(),
}
})?)
.map_err(|cause| BulkApplyError::PatchContentsInvalidUtf8 {
cause,
patch_file: full_patch_path.clone(),
})?;
let email = EmailMessage::parse(&patch_file_contents).map_err(|cause| {
BulkApplyError::FailedParsePatch {
patch_file: full_patch_path.clone(),
cause,
}
})?;
patch_files.push(BufferedPatch {
email,
patch_file: full_patch_path,
patch_name: patch_name.into(),
});
}
patch_files.sort_by(|first, second| first.patch_name.cmp(&second.patch_name));
for patch in &patch_files {
slog::info!(
self.logger,
"Applying patch";
"patch_name" => &patch.patch_name,
"patch_file" => patch.patch_file.display()
);
patch
.email
.apply_commit(self.target_repo)
.map_err(|cause| BulkApplyError::FailedApplyPatch {
cause,
name: patch.patch_name.clone(),
})?;
}
slog::info!(
self.logger,
"Successfully applied {} patches!",
patch_files.len()
);
Ok(())
}
}
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum BulkApplyError {
#[error("Error accessing patch directory: {}", patch_dir.display())]
ErrorAccessPatchDir {
patch_dir: PathBuf,
#[source]
cause: io::Error,
},
#[error("Patch name must be valid UTF8: {}", raw_entry.display())]
PatchNameInvalidUtf8 { raw_entry: PathBuf },
#[error("Failed to read patch file: {}", patch_file.display())]
FailedReadPatch {
patch_file: PathBuf,
#[source]
cause: io::Error,
},
#[error("Patch contents are not valid UTF8: {}", patch_file.display())]
PatchContentsInvalidUtf8 {
patch_file: PathBuf,
#[source]
cause: std::string::FromUtf8Error,
},
#[error("Failed parse patch file: {}", patch_file.display())]
FailedParsePatch {
patch_file: PathBuf,
#[source]
cause: super::email::InvalidEmailMessage,
},
#[error("Failed to apply patch: {name:?}")]
FailedApplyPatch {
name: String,
#[source]
cause: super::email::PatchApplyError,
},
}
#[derive(thiserror::Error, Debug)]
pub enum ResetUpstreamError {
#[error("Unable to resolve reference: {upstream_name:?}")]
InvalidReference {
upstream_name: String,
#[source]
cause: git2::Error,
},
#[error("Failed to reset to {upstream_name:?}")]
FailedReset {
upstream_name: String,
#[source]
cause: git2::Error,
},
}