feat: add basic plan and project clone

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
Kasper Juul Hermansen 2024-08-24 00:45:16 +02:00
parent 6cb65e55c1
commit 531ea225f3
Signed by: kjuulh
GPG Key ID: D85D7535F18F35FA
10 changed files with 256 additions and 28 deletions

7
Cargo.lock generated
View File

@ -173,6 +173,7 @@ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"dotenv", "dotenv",
"fs_extra",
"serde", "serde",
"tokio", "tokio",
"toml", "toml",
@ -193,6 +194,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.15" version = "0.2.15"

View File

@ -15,3 +15,4 @@ dotenv.workspace = true
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }
uuid = { version = "1.7.0", features = ["v4"] } uuid = { version = "1.7.0", features = ["v4"] }
toml = "0.8.19" toml = "0.8.19"
fs_extra = "1.3.0"

View File

@ -1,2 +0,0 @@
[project]
name = "basic"

View File

@ -0,0 +1,2 @@
[plan]
path = "../plan"

View File

@ -1,25 +1,66 @@
mod project; use plan::Plan;
use project::ProjectPlan;
use project::Project; mod plan;
mod project;
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
dotenv::dotenv().ok(); dotenv::dotenv().ok();
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
Cuddle::new().await?; let _cuddle = Cuddle::default()
.prepare_project()
.await?
.prepare_plan()
.await?;
Ok(()) Ok(())
} }
struct Cuddle { struct Start {}
project: Option<Project>, struct PrepareProject {
project: Option<ProjectPlan>,
} }
impl Cuddle { struct PreparePlan {
pub async fn new() -> anyhow::Result<Self> { project: Option<ProjectPlan>,
let project = Project::from_current_path().await?; plan: Option<Plan>,
}
Ok(Self { project }) struct Cuddle<S = Start> {
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<Start> {
pub fn default() -> Self {
Self { state: Start {} }
}
pub async fn prepare_project(&self) -> anyhow::Result<Cuddle<PrepareProject>> {
let project = ProjectPlan::from_current_path().await?;
Ok(Cuddle {
state: PrepareProject { project },
})
}
}
impl Cuddle<PrepareProject> {
pub async fn prepare_plan(&self) -> anyhow::Result<Cuddle<PreparePlan>> {
if let Some(project) = &self.state.project {
match Plan::new().clone_from_project(project).await? {
Some(plan) => todo!(),
None => todo!(),
}
}
todo!()
} }
} }

120
crates/cuddle/src/plan.rs Normal file
View File

@ -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<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),
)?;
todo!()
}
}
pub struct ClonedPlan {}

View File

@ -1,47 +1,75 @@
use std::env::current_dir; use std::{env::current_dir, path::PathBuf};
use serde::Deserialize; use serde::Deserialize;
const CUDDLE_FILE_NAME: &str = "cuddle.toml"; const CUDDLE_FILE_NAME: &str = "cuddle.toml";
pub struct Project { pub struct ProjectPlan {
config: Config, config: Config,
pub root: PathBuf,
} }
impl Project { impl ProjectPlan {
pub fn new(config: Config) -> Self { pub fn new(config: Config, root: PathBuf) -> Self {
Self { config } Self { config, root }
} }
pub fn from_file(content: &str) -> anyhow::Result<Self> { pub fn from_file(content: &str, root: PathBuf) -> anyhow::Result<Self> {
let config: Config = toml::from_str(&content)?; let config: Config = toml::from_str(&content)?;
Ok(Self::new(config)) Ok(Self::new(config, root))
} }
pub async fn from_current_path() -> anyhow::Result<Option<Self>> { pub async fn from_current_path() -> anyhow::Result<Option<Self>> {
let cur_dir = current_dir()?; let cur_dir = current_dir()?;
let cuddle_file = cur_dir.join(CUDDLE_FILE_NAME); 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() { if !cuddle_file.exists() {
tracing::debug!("no cuddle.toml project file found");
// We may want to recursively search for the file (towards root) // We may want to recursively search for the file (towards root)
return Ok(None); return Ok(None);
} }
let cuddle_project_file = tokio::fs::read_to_string(cuddle_file).await?; 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)] #[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct Config { pub struct Config {
project: ProjectConfig, plan: Option<PlanConfig>,
} }
#[derive(Debug, Clone, Deserialize, PartialEq)] #[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct ProjectConfig { #[serde(untagged)]
name: String, pub enum PlanConfig {
Bare(String),
Git { git: String },
Folder { path: PathBuf },
} }
#[cfg(test)] #[cfg(test)]
@ -50,22 +78,53 @@ mod tests {
#[test] #[test]
fn test_can_parse_simple_file() -> anyhow::Result<()> { fn test_can_parse_simple_file() -> anyhow::Result<()> {
let project = Project::from_file( let project = ProjectPlan::from_file(
r##" r##"
[project] [plan]
name = "simple_file" git = "https://github.com/kjuulh/some-cuddle-project"
"##, "##,
PathBuf::new(),
)?; )?;
assert_eq!( assert_eq!(
Config { Config {
project: ProjectConfig { plan: Some(PlanConfig::Git {
name: "simple_file".into() git: "https://github.com/kjuulh/some-cuddle-project".into()
} })
}, },
project.config project.config
); );
Ok(()) 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(())
}
} }