feat: add basic plan and project clone
Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
parent
6cb65e55c1
commit
531ea225f3
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "basic"
|
|
0
crates/cuddle/examples/basic/plan/cuddle.plan.toml
Normal file
0
crates/cuddle/examples/basic/plan/cuddle.plan.toml
Normal file
0
crates/cuddle/examples/basic/plan/cuddle.toml
Normal file
0
crates/cuddle/examples/basic/plan/cuddle.toml
Normal file
2
crates/cuddle/examples/basic/project/cuddle.toml
Normal file
2
crates/cuddle/examples/basic/project/cuddle.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[plan]
|
||||||
|
path = "../plan"
|
@ -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
120
crates/cuddle/src/plan.rs
Normal 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 {}
|
@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user