2024-08-24 00:45:16 +02:00
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
|
|
|
use fs_extra::dir::CopyOptions;
|
2024-08-24 14:55:26 +02:00
|
|
|
use serde::Deserialize;
|
2024-08-24 00:45:16 +02:00
|
|
|
|
|
|
|
use crate::project::{self, ProjectPlan};
|
|
|
|
|
|
|
|
pub const CUDDLE_PLAN_FOLDER: &str = "plan";
|
|
|
|
pub const CUDDLE_PROJECT_WORKSPACE: &str = ".cuddle";
|
2024-08-24 14:55:26 +02:00
|
|
|
pub const CUDDLE_PLAN_FILE: &str = "cuddle.plan.toml";
|
2024-08-24 00:45:16 +02:00
|
|
|
|
|
|
|
pub trait PlanPathExt {
|
|
|
|
fn plan_path(&self) -> PathBuf;
|
|
|
|
}
|
|
|
|
|
|
|
|
impl PlanPathExt for project::ProjectPlan {
|
|
|
|
fn plan_path(&self) -> PathBuf {
|
|
|
|
self.root
|
|
|
|
.join(CUDDLE_PROJECT_WORKSPACE)
|
|
|
|
.join(CUDDLE_PLAN_FOLDER)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-24 14:55:26 +02:00
|
|
|
pub struct RawPlan {
|
|
|
|
pub config: RawPlanConfig,
|
|
|
|
pub root: PathBuf,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl RawPlan {
|
|
|
|
pub fn new(config: RawPlanConfig, root: &Path) -> Self {
|
|
|
|
Self {
|
|
|
|
config,
|
|
|
|
root: root.to_path_buf(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn from_file(content: &str, root: &Path) -> anyhow::Result<Self> {
|
|
|
|
let config: RawPlanConfig = toml::from_str(content)?;
|
|
|
|
|
|
|
|
Ok(Self::new(config, root))
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn from_path(path: &Path) -> anyhow::Result<Self> {
|
|
|
|
let cuddle_file = path.join(CUDDLE_PLAN_FILE);
|
|
|
|
|
|
|
|
tracing::trace!(
|
|
|
|
path = cuddle_file.display().to_string(),
|
|
|
|
"searching for cuddle.toml project file"
|
|
|
|
);
|
|
|
|
|
|
|
|
if !cuddle_file.exists() {
|
|
|
|
anyhow::bail!("no cuddle.toml project file found");
|
|
|
|
}
|
|
|
|
|
|
|
|
let cuddle_plan_file = tokio::fs::read_to_string(cuddle_file).await?;
|
|
|
|
|
|
|
|
Self::from_file(&cuddle_plan_file, path)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
|
|
|
pub struct RawPlanConfig {
|
|
|
|
pub plan: RawPlanConfigSection,
|
|
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
|
|
|
pub struct RawPlanConfigSection {
|
|
|
|
pub schema: Option<RawPlanSchema>,
|
|
|
|
}
|
2024-08-24 00:45:16 +02:00
|
|
|
|
2024-08-24 14:55:26 +02:00
|
|
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
|
|
|
#[serde(untagged)]
|
|
|
|
pub enum RawPlanSchema {
|
|
|
|
Nickel { nickel: PathBuf },
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct Plan {}
|
2024-08-24 00:45:16 +02:00
|
|
|
impl Plan {
|
|
|
|
pub fn new() -> Self {
|
|
|
|
Self {}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn clone_from_project(
|
|
|
|
&self,
|
|
|
|
project: &ProjectPlan,
|
|
|
|
) -> anyhow::Result<Option<ClonedPlan>> {
|
|
|
|
if !project.plan_path().exists() {
|
|
|
|
if project.has_plan() {
|
|
|
|
self.prepare_plan(project).await?;
|
|
|
|
}
|
|
|
|
|
|
|
|
match project.get_plan() {
|
|
|
|
project::Plan::None => Ok(None),
|
|
|
|
project::Plan::Git(url) => Ok(Some(self.git_plan(project, url).await?)),
|
|
|
|
project::Plan::Folder(folder) => {
|
|
|
|
Ok(Some(self.folder_plan(project, &folder).await?))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
match project.get_plan() {
|
|
|
|
project::Plan::Folder(folder) => {
|
|
|
|
self.clean_plan(project).await?;
|
|
|
|
self.prepare_plan(project).await?;
|
|
|
|
|
|
|
|
Ok(Some(self.folder_plan(project, &folder).await?))
|
|
|
|
}
|
|
|
|
project::Plan::Git(_git) => Ok(Some(ClonedPlan {})),
|
|
|
|
project::Plan::None => Ok(None),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn prepare_plan(&self, project: &ProjectPlan) -> anyhow::Result<()> {
|
|
|
|
tracing::trace!("preparing workspace");
|
|
|
|
|
|
|
|
tokio::fs::create_dir_all(project.plan_path()).await?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn clean_plan(&self, project: &ProjectPlan) -> anyhow::Result<()> {
|
|
|
|
tracing::trace!("clean plan");
|
|
|
|
|
|
|
|
tokio::fs::remove_dir_all(project.plan_path()).await?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn git_plan(&self, project: &ProjectPlan, url: String) -> anyhow::Result<ClonedPlan> {
|
|
|
|
let mut cmd = tokio::process::Command::new("git");
|
|
|
|
cmd.args(["clone", &url, &project.plan_path().display().to_string()]);
|
|
|
|
|
|
|
|
tracing::debug!(url = url, "cloning git plan");
|
|
|
|
|
|
|
|
let output = cmd.output().await?;
|
|
|
|
if !output.status.success() {
|
|
|
|
anyhow::bail!(
|
|
|
|
"failed to clone: {}, output: {} {}",
|
|
|
|
url,
|
|
|
|
std::str::from_utf8(&output.stdout)?,
|
|
|
|
std::str::from_utf8(&output.stderr)?,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(ClonedPlan {})
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn folder_plan(&self, project: &ProjectPlan, path: &Path) -> anyhow::Result<ClonedPlan> {
|
|
|
|
tracing::trace!(
|
|
|
|
src = path.display().to_string(),
|
|
|
|
dest = project.plan_path().display().to_string(),
|
|
|
|
"copying src into plan dest"
|
|
|
|
);
|
|
|
|
|
|
|
|
let mut items_stream = tokio::fs::read_dir(path).await?;
|
|
|
|
let mut items = Vec::new();
|
|
|
|
while let Some(item) = items_stream.next_entry().await? {
|
|
|
|
items.push(item.path());
|
|
|
|
}
|
|
|
|
|
|
|
|
fs_extra::copy_items(
|
|
|
|
&items,
|
|
|
|
project.plan_path(),
|
|
|
|
&CopyOptions::default()
|
|
|
|
.overwrite(true)
|
|
|
|
.depth(0)
|
|
|
|
.copy_inside(false),
|
|
|
|
)?;
|
|
|
|
|
2024-08-24 14:55:26 +02:00
|
|
|
Ok(ClonedPlan {})
|
2024-08-24 00:45:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct ClonedPlan {}
|
2024-08-24 14:55:26 +02:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_can_parse_schema_plan() -> anyhow::Result<()> {
|
|
|
|
let plan = RawPlan::from_file(
|
|
|
|
r##"
|
|
|
|
[plan]
|
|
|
|
schema = {nickel = "contract.ncl"}
|
|
|
|
"##,
|
|
|
|
&PathBuf::new(),
|
|
|
|
)?;
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
RawPlanConfig {
|
|
|
|
plan: RawPlanConfigSection {
|
|
|
|
schema: Some(RawPlanSchema::Nickel {
|
|
|
|
nickel: "contract.ncl".into()
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
plan.config,
|
|
|
|
);
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|