From dfe2ac62e33d170b7fb518d8832cb45d4e2e3fa4 Mon Sep 17 00:00:00 2001 From: kjuulh Date: Fri, 28 Feb 2025 23:08:40 +0100 Subject: [PATCH] feat: can clone using git --- crates/forest/src/model.rs | 20 ++++++ crates/forest/src/plan_reconciler.rs | 62 +++++++++++++++++++ examples/project_git/forest.kdl | 33 ++++++++++ examples/project_git/scripts/hello.sh | 5 ++ .../templates/something.yaml.jinja2 | 3 + 5 files changed, 123 insertions(+) create mode 100644 examples/project_git/forest.kdl create mode 100755 examples/project_git/scripts/hello.sh create mode 100644 examples/project_git/templates/something.yaml.jinja2 diff --git a/crates/forest/src/model.rs b/crates/forest/src/model.rs index 2971e7a..d73b3b9 100644 --- a/crates/forest/src/model.rs +++ b/crates/forest/src/model.rs @@ -1,5 +1,6 @@ use std::{collections::BTreeMap, fmt::Debug, path::PathBuf}; +use colored_json::Paint; use kdl::{KdlDocument, KdlNode, KdlValue}; use serde::Serialize; @@ -53,6 +54,7 @@ impl TryFrom for Plan { #[serde(untagged)] pub enum ProjectPlan { Local { path: PathBuf }, + Git { url: String, path: Option }, NoPlan, } @@ -74,6 +76,24 @@ impl TryFrom<&KdlNode> for ProjectPlan { }); } + if let Some(git) = children.get_arg("git") { + return Ok(Self::Git { + url: git + .as_string() + .map(|l| l.to_string()) + .ok_or(anyhow::anyhow!("a git url is required"))?, + path: children + .get("git") + .and_then(|git| { + git.entries() + .iter() + .filter(|i| i.name().is_some()) + .find(|i| i.name().expect("to have a value").to_string() == "path") + }) + .and_then(|i| i.value().as_string().map(|p| p.to_string().into())), + }); + } + Ok(Self::NoPlan) } } diff --git a/crates/forest/src/plan_reconciler.rs b/crates/forest/src/plan_reconciler.rs index b6bcfb2..df69d7b 100644 --- a/crates/forest/src/plan_reconciler.rs +++ b/crates/forest/src/plan_reconciler.rs @@ -5,6 +5,65 @@ use anyhow::Context; use crate::model::Project; pub mod local; +pub mod git { + use std::{ + env::temp_dir, + path::{Path, PathBuf}, + }; + + use super::local; + + pub async fn reconcile( + url: &str, + path: &Option, + plan_dir: &Path, + ) -> anyhow::Result<()> { + let temp = TempDir::new(); + tokio::fs::create_dir_all(&temp.0).await?; + + let mut cmd = tokio::process::Command::new("git"); + cmd.args(["clone", url, &temp.0.display().to_string(), "--depth=1"]); + + tracing::info!("cloning plan: {}", url); + let out = cmd.output().await?; + if !out.status.success() { + let stdout = std::str::from_utf8(&out.stdout).unwrap_or_default(); + let stderr = std::str::from_utf8(&out.stderr).unwrap_or_default(); + + anyhow::bail!("failed to process git plan: {}, {}", stdout, stderr) + } + + let temp_plan_dir = if let Some(path) = path { + temp.0.join(path) + } else { + temp.0.to_path_buf() + }; + + local::reconcile(&temp_plan_dir, plan_dir).await?; + + drop(temp); + + Ok(()) + } + + struct TempDir(PathBuf); + + impl TempDir { + fn new() -> Self { + Self( + temp_dir() + .join("forest") + .join(uuid::Uuid::new_v4().to_string()), + ) + } + } + + impl Drop for TempDir { + fn drop(&mut self) { + std::fs::remove_dir_all(&self.0).expect("to be able to remove temp dir"); + } + } +} #[derive(Default)] pub struct PlanReconciler {} @@ -43,6 +102,9 @@ impl PlanReconciler { let source = &destination.join(path); local::reconcile(source, &plan_dir).await?; } + crate::model::ProjectPlan::Git { url, path } => { + git::reconcile(url, path, &plan_dir).await?; + } crate::model::ProjectPlan::NoPlan => { tracing::debug!("no plan, returning"); return Ok(None); diff --git a/examples/project_git/forest.kdl b/examples/project_git/forest.kdl new file mode 100644 index 0000000..1dece96 --- /dev/null +++ b/examples/project_git/forest.kdl @@ -0,0 +1,33 @@ +project { + name local + description """ + A simple local project that depends on ../plan for its utility scripts + """ + + plan { + git "ssh://git@git.front.kjuulh.io/kjuulh/forest" path="examples/plan" + } + + global { + someName "name" + someKey { + someNestedKey "somevalue" + some { + key { + val + val + } + } + } + } + + templates type=jinja2 { + path "templates/*.jinja2" + output "output/" + } + + scripts { + hello type=shell {} + } +} + diff --git a/examples/project_git/scripts/hello.sh b/examples/project_git/scripts/hello.sh new file mode 100755 index 0000000..6b22ac4 --- /dev/null +++ b/examples/project_git/scripts/hello.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env zsh + +set -e + +echo "hello world" diff --git a/examples/project_git/templates/something.yaml.jinja2 b/examples/project_git/templates/something.yaml.jinja2 new file mode 100644 index 0000000..8f64ed9 --- /dev/null +++ b/examples/project_git/templates/something.yaml.jinja2 @@ -0,0 +1,3 @@ +something + +val is mapping: {{ global.someKey.some.key.val is mapping }}