diff --git a/Cargo.lock b/Cargo.lock index bf7e84c..b269ee4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -173,6 +173,7 @@ dependencies = [ "anyhow", "clap", "dotenv", + "fs_extra", "serde", "tokio", "toml", @@ -193,6 +194,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "getrandom" version = "0.2.15" diff --git a/crates/cuddle/Cargo.toml b/crates/cuddle/Cargo.toml index be021a1..26f0826 100644 --- a/crates/cuddle/Cargo.toml +++ b/crates/cuddle/Cargo.toml @@ -15,3 +15,4 @@ dotenv.workspace = true serde = { version = "1.0.197", features = ["derive"] } uuid = { version = "1.7.0", features = ["v4"] } toml = "0.8.19" +fs_extra = "1.3.0" diff --git a/crates/cuddle/examples/basic/cuddle.toml b/crates/cuddle/examples/basic/cuddle.toml deleted file mode 100644 index e6dd06e..0000000 --- a/crates/cuddle/examples/basic/cuddle.toml +++ /dev/null @@ -1,2 +0,0 @@ -[project] -name = "basic" diff --git a/crates/cuddle/examples/basic/plan/cuddle.plan.toml b/crates/cuddle/examples/basic/plan/cuddle.plan.toml new file mode 100644 index 0000000..e69de29 diff --git a/crates/cuddle/examples/basic/plan/cuddle.toml b/crates/cuddle/examples/basic/plan/cuddle.toml new file mode 100644 index 0000000..e69de29 diff --git a/crates/cuddle/examples/basic/plan/folder/folder/.gitkeep b/crates/cuddle/examples/basic/plan/folder/folder/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/crates/cuddle/examples/basic/project/cuddle.toml b/crates/cuddle/examples/basic/project/cuddle.toml new file mode 100644 index 0000000..244b7b7 --- /dev/null +++ b/crates/cuddle/examples/basic/project/cuddle.toml @@ -0,0 +1,2 @@ +[plan] +path = "../plan" diff --git a/crates/cuddle/src/main.rs b/crates/cuddle/src/main.rs index f96336d..5e42e86 100644 --- a/crates/cuddle/src/main.rs +++ b/crates/cuddle/src/main.rs @@ -1,25 +1,66 @@ -mod project; +use plan::Plan; +use project::ProjectPlan; -use project::Project; +mod plan; +mod project; #[tokio::main] async fn main() -> anyhow::Result<()> { dotenv::dotenv().ok(); tracing_subscriber::fmt::init(); - Cuddle::new().await?; + let _cuddle = Cuddle::default() + .prepare_project() + .await? + .prepare_plan() + .await?; Ok(()) } -struct Cuddle { - project: Option, +struct Start {} +struct PrepareProject { + project: Option, } -impl Cuddle { - pub async fn new() -> anyhow::Result { - let project = Project::from_current_path().await?; +struct PreparePlan { + project: Option, + plan: Option, +} - Ok(Self { project }) +struct Cuddle { + state: S, +} + +// Cuddle maintains the context for cuddle to use +// Stage 1 figure out which state to display +// Stage 2 prepare plan +// Stage 3 validate settings, build actions, prepare +impl Cuddle {} + +impl Cuddle { + pub fn default() -> Self { + Self { state: Start {} } + } + + pub async fn prepare_project(&self) -> anyhow::Result> { + let project = ProjectPlan::from_current_path().await?; + + Ok(Cuddle { + state: PrepareProject { project }, + }) + } +} + +impl Cuddle { + pub async fn prepare_plan(&self) -> anyhow::Result> { + if let Some(project) = &self.state.project { + match Plan::new().clone_from_project(project).await? { + Some(plan) => todo!(), + None => todo!(), + } + } + + todo!() } } diff --git a/crates/cuddle/src/plan.rs b/crates/cuddle/src/plan.rs new file mode 100644 index 0000000..66c0183 --- /dev/null +++ b/crates/cuddle/src/plan.rs @@ -0,0 +1,120 @@ +use std::path::{Path, PathBuf}; + +use fs_extra::dir::CopyOptions; + +use crate::project::{self, ProjectPlan}; + +pub const CUDDLE_PLAN_FOLDER: &str = "plan"; +pub const CUDDLE_PROJECT_WORKSPACE: &str = ".cuddle"; + +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) + } +} + +pub struct Plan {} + +impl Plan { + pub fn new() -> Self { + Self {} + } + + pub async fn clone_from_project( + &self, + project: &ProjectPlan, + ) -> anyhow::Result> { + 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 { + 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 { + 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), + )?; + + todo!() + } +} + +pub struct ClonedPlan {} diff --git a/crates/cuddle/src/project.rs b/crates/cuddle/src/project.rs index f806108..9443ce4 100644 --- a/crates/cuddle/src/project.rs +++ b/crates/cuddle/src/project.rs @@ -1,47 +1,75 @@ -use std::env::current_dir; +use std::{env::current_dir, path::PathBuf}; use serde::Deserialize; const CUDDLE_FILE_NAME: &str = "cuddle.toml"; -pub struct Project { +pub struct ProjectPlan { config: Config, + pub root: PathBuf, } -impl Project { - pub fn new(config: Config) -> Self { - Self { config } +impl ProjectPlan { + pub fn new(config: Config, root: PathBuf) -> Self { + Self { config, root } } - pub fn from_file(content: &str) -> anyhow::Result { + pub fn from_file(content: &str, root: PathBuf) -> anyhow::Result { let config: Config = toml::from_str(&content)?; - Ok(Self::new(config)) + Ok(Self::new(config, root)) } pub async fn from_current_path() -> anyhow::Result> { let cur_dir = current_dir()?; let cuddle_file = cur_dir.join(CUDDLE_FILE_NAME); + tracing::trace!( + path = cuddle_file.display().to_string(), + "searching for cuddle.toml project file" + ); + if !cuddle_file.exists() { + tracing::debug!("no cuddle.toml project file found"); // We may want to recursively search for the file (towards root) return Ok(None); } let cuddle_project_file = tokio::fs::read_to_string(cuddle_file).await?; - Ok(Some(Self::from_file(&cuddle_project_file)?)) + Ok(Some(Self::from_file(&cuddle_project_file, cur_dir)?)) } + + pub fn has_plan(&self) -> bool { + self.config.plan.is_some() + } + + pub fn get_plan(&self) -> Plan { + match &self.config.plan { + Some(PlanConfig::Bare(git)) | Some(PlanConfig::Git { git }) => Plan::Git(git.clone()), + Some(PlanConfig::Folder { path }) => Plan::Folder(path.clone()), + None => Plan::None, + } + } +} + +pub enum Plan { + None, + Git(String), + Folder(PathBuf), } #[derive(Debug, Clone, Deserialize, PartialEq)] pub struct Config { - project: ProjectConfig, + plan: Option, } #[derive(Debug, Clone, Deserialize, PartialEq)] -pub struct ProjectConfig { - name: String, +#[serde(untagged)] +pub enum PlanConfig { + Bare(String), + Git { git: String }, + Folder { path: PathBuf }, } #[cfg(test)] @@ -50,22 +78,53 @@ mod tests { #[test] fn test_can_parse_simple_file() -> anyhow::Result<()> { - let project = Project::from_file( + let project = ProjectPlan::from_file( r##" -[project] -name = "simple_file" +[plan] +git = "https://github.com/kjuulh/some-cuddle-project" "##, + PathBuf::new(), )?; assert_eq!( Config { - project: ProjectConfig { - name: "simple_file".into() - } + plan: Some(PlanConfig::Git { + git: "https://github.com/kjuulh/some-cuddle-project".into() + }) }, project.config ); Ok(()) } + + #[test] + fn test_can_parse_simple_file_bare() -> anyhow::Result<()> { + let project = ProjectPlan::from_file( + r##" +plan = "https://github.com/kjuulh/some-cuddle-project" +"##, + PathBuf::new(), + )?; + + assert_eq!( + Config { + plan: Some(PlanConfig::Bare( + "https://github.com/kjuulh/some-cuddle-project".into() + )) + }, + project.config + ); + + Ok(()) + } + + #[test] + fn test_can_parse_simple_file_none() -> anyhow::Result<()> { + let project = ProjectPlan::from_file(r##""##, PathBuf::new())?; + + assert_eq!(Config { plan: None }, project.config); + + Ok(()) + } }