use git2::SubmoduleUpdateOptions;
use crate::Error;
use std::fmt::{self, Display, Formatter};
use std::path::{Path, PathBuf};
use futures::future::try_join_all;
use git2::Repository;
use serde_json::{Value, to_string_pretty};
use tokio::fs;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkItem {
Empty,
NewFile {
path: PathBuf,
content: String
},
GitInit,
GitAddSubmodule {
path: PathBuf,
url: String,
},
}
const MAX_FILE_CONTENT_DISPLAY: usize = 60;
const FILE_CONTENT_DISPLAY: usize = 40;
impl Display for WorkItem {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => {
write!(f, "empty work item")
}
Self::NewFile { path, content } => {
let content = if content.len() > MAX_FILE_CONTENT_DISPLAY {
content.get(0..FILE_CONTENT_DISPLAY)
.unwrap_or_else(|| {
(0..FILE_CONTENT_DISPLAY).into_iter()
.rev()
.find(|index| content.is_char_boundary(*index))
.and_then(|index| content.get(0..index))
.unwrap_or("")
})
} else {
content
};
write!(f, "new file {}: {}", path.display(), content)
}
Self::GitAddSubmodule { path, url } => {
write!(f, "add git submodule {} ({})", path.display(), url)
}
Self::GitInit => {
write!(f, "init git repository")
}
}
}
}
impl WorkItem {
pub fn write_string<T: Into<PathBuf>, U: Into<String>>(path: T, content: U) -> Self {
Self::NewFile {
path: path.into(),
content: content.into(),
}
}
pub fn write_multi_line_string<T: Into<PathBuf>, U>(path: T, lines: U) -> Self
where U: IntoIterator, U::Item: Into<String>
{
let content_lines: Vec<String> = lines.into_iter()
.map(Into::into)
.collect();
Self::NewFile {
path: path.into(),
content: content_lines.join("\n"),
}
}
pub fn write_json<T: Into<PathBuf>>(path: T, value: Value) -> Self {
Self::NewFile {
path: path.into(),
content: to_string_pretty(&value)
.expect("value should serialize to JSON"),
}
}
pub fn add_git_submodule<T: Into<PathBuf>, U: Into<String>>(path: T, url: U) -> Self {
Self::GitAddSubmodule {
path: path.into(),
url: url.into(),
}
}
pub async fn execute(&self, project_root: &Path) -> Result<(), Error> {
match self {
Self::Empty => Ok(()),
Self::NewFile { path, content } => {
if let Some(parent) = path.parent() {
let directory = project_root.join(parent);
log::trace!("creating parent directory: {}", parent.display());
fs::create_dir_all(&directory).await
.map_err(|io_error| {
Error::CannotCreateDirectory {
path: directory,
reason: format!("{}", io_error),
}
})?;
log::trace!("parent directory {} created", parent.display());
}
log::debug!("writing file at {}", path.display());
fs::write(project_root.join(path), content).await
.map_err(|io_error| {
Error::CannotWriteFile {
path: path.clone(),
reason: format!("{}", io_error),
}
})?;
log::trace!("file {} written", path.display());
Ok(())
}
Self::GitInit => {
log::debug!("init git repository at {}", project_root.display());
Repository::init(&project_root)
.map(|_| ())
.map_err(|git_error| {
Error::GitError {
reason: format!("{}", git_error),
}
})?;
log::trace!("repository at {} initialized", project_root.display());
Ok(())
}
Self::GitAddSubmodule { path, url } => {
log::debug!("adding submodule at {} ({})", path.display(), url);
Repository::open(&project_root)
.and_then(|repository| {
repository.submodule(url, path, true)
.and_then(|mut submodule| {
let mut options = SubmoduleUpdateOptions::new();
submodule.clone(Some(&mut options))
.and_then(|_| {
submodule.add_finalize()
})
})
})
.map_err(|git_error| {
Error::GitError {
reason: format!("{}", git_error),
}
})?;
Ok(())
}
}
}
}
impl Default for WorkItem {
fn default() -> Self {
Self::Empty
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Work {
primary_work: Vec<WorkItem>,
secondary_work: Vec<WorkItem>,
}
impl From<WorkItem> for Work {
fn from(item: WorkItem) -> Self {
let mut work = Self::new();
work.add_item(item);
work
}
}
impl Work {
pub fn new() -> Self {
Self {
primary_work: Vec::new(),
secondary_work: Vec::new(),
}
}
pub fn merge(&mut self, mut work: Self) -> &mut Self {
self.primary_work.extend(work.primary_work.drain(..));
self.secondary_work.extend(work.secondary_work.drain(..));
self
}
pub fn add_item(&mut self, item: WorkItem) -> &mut Self {
self.secondary_work.push(item);
self
}
pub fn add_items<I>(&mut self, items: I) -> &mut Self
where I: IntoIterator<Item=WorkItem>
{
self.secondary_work.extend(items);
self
}
pub fn add_primary_item(&mut self, item: WorkItem) -> &mut Self {
self.secondary_work.push(item);
self
}
#[tokio::main]
pub async fn execute(&self, project_root: &Path) -> Result<(), String> {
Self::execute_work(project_root, &self.primary_work).await?;
Self::execute_work(project_root, &self.secondary_work).await
}
async fn execute_work(project_root: &Path, work: &Vec<WorkItem>) -> Result<(), String> {
let work_futures: Vec<_> = work.iter()
.map(|work_item| {
work_item.execute(project_root)
})
.collect();
try_join_all(work_futures).await
.map(|_| ())
.map_err(|error| format!("{}", error))
}
}