From 89c560dd721bb8de4cef0446c6fab62e2ddd03a2 Mon Sep 17 00:00:00 2001 From: Thinking-Dragon Date: Mon, 25 Aug 2025 19:19:33 -0400 Subject: [PATCH 1/7] Format long instructions on multiple lines --- cli/src/commands/replication/plan.rs | 39 ++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/cli/src/commands/replication/plan.rs b/cli/src/commands/replication/plan.rs index 30aeb6c..c5816e0 100644 --- a/cli/src/commands/replication/plan.rs +++ b/cli/src/commands/replication/plan.rs @@ -49,13 +49,38 @@ pub fn plan_replication() -> Result { let mut packages: Vec = Vec::new(); - let build_dependencies: serde_json::Value = serde_json::from_str(top_level.run_shell("go list -json -m all | jq -s".to_string())?.as_str())?; + let build_dependencies: serde_json::Value = serde_json::from_str( + top_level.run_shell( + "go list -json -m all | jq -s".to_string() + )?.as_str() + )?; + if let Value::Array(build_dependencies) = build_dependencies { for build_dependency in build_dependencies { if let Value::Object(build_dependency) = build_dependency { - let name: String = build_dependency.get("Path").unwrap().as_str().unwrap_or_default().replace("/", "-"); - let version: String = build_dependency.get("Version").unwrap_or(&Value::String(origin.reference.split('/').last().unwrap_or_default().to_string())).as_str().unwrap_or_default().to_string(); - let cache_path: String = build_dependency.get("Dir").unwrap_or(&Value::String(String::new())).as_str().unwrap_or_default().to_string(); + let name: String = build_dependency.get("Path") + .unwrap() + .as_str() + .unwrap_or_default() + .replace("/", "-"); + + let version: String = build_dependency.get("Version") + .unwrap_or( + &Value::String(origin.reference.split('/') + .last() + .unwrap_or_default() + .to_string() + ) + ) + .as_str() + .unwrap_or_default() + .to_string(); + + let cache_path: String = build_dependency.get("Dir") + .unwrap_or(&Value::String(String::new())) + .as_str() + .unwrap_or_default() + .to_string(); let environment: Environment = { let mut major: String = String::new(); @@ -104,7 +129,11 @@ pub fn plan_replication() -> Result { let package: Package = Package::new( 0, - PackageOriginGoCache::new(environment.name, environment.version, cache_path), + PackageOriginGoCache::new( + environment.name, + environment.version, + cache_path + ), PackageDestinationGit::new( package_destination_url, package_destination_reference, From e0d89dc308ef1c3d59befc2036d1f5eb3765d134 Mon Sep 17 00:00:00 2001 From: Thinking-Dragon Date: Mon, 25 Aug 2025 19:40:24 -0400 Subject: [PATCH 2/7] Add dependencies to replication plan packages --- cli/src/commands/replication/plan.rs | 44 ++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/cli/src/commands/replication/plan.rs b/cli/src/commands/replication/plan.rs index c5816e0..348d4a3 100644 --- a/cli/src/commands/replication/plan.rs +++ b/cli/src/commands/replication/plan.rs @@ -12,7 +12,13 @@ use source_wand_common::{ read_yaml_file::read_yaml_file } }; +use source_wand_dependency_analysis::{ + dependency_tree_node::DependencyTreeNode, + dependency_tree_request::DependencyTreeRequest, + find_dependency_tree +}; use source_wand_replication::model::{ + dependency::Dependency, package::Package, package_destination::PackageDestination, package_destination_git::PackageDestinationGit, @@ -49,6 +55,13 @@ pub fn plan_replication() -> Result { let mut packages: Vec = Vec::new(); + let dependency_tree: DependencyTreeNode = find_dependency_tree( + DependencyTreeRequest::from_git_project( + origin.git, + Some(origin.reference.clone()) + ) + )?; + let build_dependencies: serde_json::Value = serde_json::from_str( top_level.run_shell( "go list -json -m all | jq -s".to_string() @@ -120,13 +133,18 @@ pub fn plan_replication() -> Result { format!("{}.{}.{}-{}", major, minor, patch, suffix) }; - Environment::new(name, version, major, minor, patch, suffix, retrocompatible) + Environment::new(name.clone(), version, major, minor, patch, suffix, retrocompatible) }; let PackageDestination::Git(package_destination) = &replication_manifest.destination_template; let package_destination_url: String = environment.apply(&package_destination.git); let package_destination_reference: String = environment.apply(&package_destination.reference); + let dependencies: Vec = find_dependencies_for_package( + &dependency_tree, + &name, + ); + let package: Package = Package::new( 0, PackageOriginGoCache::new( @@ -138,7 +156,7 @@ pub fn plan_replication() -> Result { package_destination_url, package_destination_reference, ), - Vec::new(), + dependencies, ); packages.push(package); @@ -148,6 +166,7 @@ pub fn plan_replication() -> Result { top_level.cleanup(); let replication_plan: ReplicationPlan = ReplicationPlan::new(replication_manifest.project, replication_manifest.hooks, packages); + Ok(replication_plan) }, PackageOrigin::GoCache(_origin) => { todo!() }, @@ -196,3 +215,24 @@ impl Environment { .replace("$VERSION", &self.version) } } + +fn find_dependencies_for_package(root: &DependencyTreeNode, package_name: &str) -> Vec { + if root.project.name.replace("/", "-") == package_name { + return root.dependencies + .iter() + .map(|dep| Dependency { + name: dep.project.name.replace("/", "-"), + version: dep.project.version.clone(), + }) + .collect(); + } + + for child in &root.dependencies { + let found = find_dependencies_for_package(child, package_name); + if !found.is_empty() { + return found; + } + } + + Vec::new() +} From 490932d1b1b362d9e8cd185e4669fa9773b89a33 Mon Sep 17 00:00:00 2001 From: Thinking-Dragon Date: Tue, 26 Aug 2025 09:48:35 -0400 Subject: [PATCH 3/7] Remove level from replication plan packages --- cli/src/commands/replication/plan.rs | 1 - replication/src/model/package.rs | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/cli/src/commands/replication/plan.rs b/cli/src/commands/replication/plan.rs index 348d4a3..1d3c273 100644 --- a/cli/src/commands/replication/plan.rs +++ b/cli/src/commands/replication/plan.rs @@ -146,7 +146,6 @@ pub fn plan_replication() -> Result { ); let package: Package = Package::new( - 0, PackageOriginGoCache::new( environment.name, environment.version, diff --git a/replication/src/model/package.rs b/replication/src/model/package.rs index c524499..ea79f62 100644 --- a/replication/src/model/package.rs +++ b/replication/src/model/package.rs @@ -4,14 +4,13 @@ use crate::model::{dependency::Dependency, package_destination::PackageDestinati #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Package { - pub level: u32, pub origin: PackageOrigin, pub destination: PackageDestination, pub dependencies: Vec, } impl Package { - pub fn new(level: u32, origin: PackageOrigin, destination: PackageDestination, dependencies: Vec) -> Self { - Package { level, origin, destination, dependencies } + pub fn new(origin: PackageOrigin, destination: PackageDestination, dependencies: Vec) -> Self { + Package { origin, destination, dependencies } } } From 100a0326a1f8ef9f6262281532a9156ff39872be Mon Sep 17 00:00:00 2001 From: Thinking-Dragon Date: Wed, 27 Aug 2025 13:59:18 -0400 Subject: [PATCH 4/7] Generate sourcecraft.yaml with the parts for the dependencies of each project --- cli/src/commands/replication/init.rs | 1 + cli/src/commands/replication/plan.rs | 114 +++-------- replication/Cargo.toml | 1 + .../src/apply/plan_to_execution_graph.rs | 147 ++++----------- replication/src/model/mod.rs | 1 + replication/src/model/replication_config.rs | 12 ++ replication/src/model/replication_manifest.rs | 14 +- replication/src/model/replication_plan.rs | 13 +- replication/src/plan/environment.rs | 83 ++++++++ replication/src/plan/mod.rs | 1 + .../src/plan/transformations/git/git_init.rs | 17 +- .../src/plan/transformations/git/git_push.rs | 5 +- replication/src/plan/transformations/mod.rs | 1 + .../transformations/sourcecraft/initialize.rs | 178 ++++++++++++++++++ .../plan/transformations/sourcecraft/mod.rs | 1 + snapcraft.yaml | 2 +- 16 files changed, 374 insertions(+), 217 deletions(-) create mode 100644 replication/src/model/replication_config.rs create mode 100644 replication/src/plan/environment.rs create mode 100644 replication/src/plan/transformations/sourcecraft/initialize.rs create mode 100644 replication/src/plan/transformations/sourcecraft/mod.rs diff --git a/cli/src/commands/replication/init.rs b/cli/src/commands/replication/init.rs index 565f4c3..ffb7352 100644 --- a/cli/src/commands/replication/init.rs +++ b/cli/src/commands/replication/init.rs @@ -21,6 +21,7 @@ pub fn replicate_init_command(_args: &ReplicationInitArgs) -> Result<()> { Some(Hooks { before_all: None, before_each: None, after_each: None, after_all: None }), PackageOriginGit::new("".to_string(), "".to_string()), PackageDestinationGit::new("".to_string(), "".to_string()), + None, ); write_yaml_file(&replication_manifest, "replication.yaml")?; diff --git a/cli/src/commands/replication/plan.rs b/cli/src/commands/replication/plan.rs index 1d3c273..1d26e1b 100644 --- a/cli/src/commands/replication/plan.rs +++ b/cli/src/commands/replication/plan.rs @@ -17,7 +17,7 @@ use source_wand_dependency_analysis::{ dependency_tree_request::DependencyTreeRequest, find_dependency_tree }; -use source_wand_replication::model::{ +use source_wand_replication::{model::{ dependency::Dependency, package::Package, package_destination::PackageDestination, @@ -26,7 +26,7 @@ use source_wand_replication::model::{ package_origin_go_cache::PackageOriginGoCache, replication_manifest::ReplicationManifest, replication_plan::ReplicationPlan -}; +}, plan::environment::Environment}; use uuid::Uuid; #[derive(Debug, Parser)] @@ -75,7 +75,8 @@ pub fn plan_replication() -> Result { .unwrap() .as_str() .unwrap_or_default() - .replace("/", "-"); + .replace("/", "-") + .replace(".", "-"); let version: String = build_dependency.get("Version") .unwrap_or( @@ -95,46 +96,7 @@ pub fn plan_replication() -> Result { .unwrap_or_default() .to_string(); - let environment: Environment = { - let mut major: String = String::new(); - let mut minor: String = String::new(); - let mut patch: String = String::new(); - let mut suffix: String = String::new(); - - if version.starts_with('v') { - let parts: Vec<&str> = version.trim_start_matches('v').split('-').collect(); - let semantic_version_parts: Vec<&str> = parts[0].split('.').collect(); - - if semantic_version_parts.len() > 0 { - major = semantic_version_parts[0].to_string(); - } - if semantic_version_parts.len() > 1 { - minor = semantic_version_parts[1].to_string(); - } - if semantic_version_parts.len() > 2 { - patch = semantic_version_parts[2].to_string(); - } - - if parts.len() > 1 { - suffix = format!("-{}", parts[1..].join("-")); - } - } - - let retrocompatible: String = - if suffix.is_empty() { - if major == "0".to_string() { - format!("{}.{}.{}", major.clone(), minor, patch) - } - else { - major.clone() - } - } - else { - format!("{}.{}.{}-{}", major, minor, patch, suffix) - }; - - Environment::new(name.clone(), version, major, minor, patch, suffix, retrocompatible) - }; + let environment: Environment = Environment::new(&name, &version); let PackageDestination::Git(package_destination) = &replication_manifest.destination_template; let package_destination_url: String = environment.apply(&package_destination.git); @@ -147,8 +109,8 @@ pub fn plan_replication() -> Result { let package: Package = Package::new( PackageOriginGoCache::new( - environment.name, - environment.version, + environment.name.clone(), + environment.version.clone(), cache_path ), PackageDestinationGit::new( @@ -164,7 +126,12 @@ pub fn plan_replication() -> Result { } top_level.cleanup(); - let replication_plan: ReplicationPlan = ReplicationPlan::new(replication_manifest.project, replication_manifest.hooks, packages); + let replication_plan: ReplicationPlan = ReplicationPlan::new( + replication_manifest.project, + replication_manifest.hooks, + packages, + replication_manifest.config, + ); Ok(replication_plan) }, @@ -172,56 +139,17 @@ pub fn plan_replication() -> Result { } } -struct Environment { - name: String, - version: String, - version_major: String, - version_minor: String, - version_patch: String, - version_suffix: String, - version_retrocompatible: String, -} - -impl Environment { - pub fn new( - name: String, - version: String, - version_major: String, - version_minor: String, - version_patch: String, - version_suffix: String, - version_retrocompatible: String, - ) -> Self { - Environment { - name, - version, - version_major, - version_minor, - version_patch, - version_suffix, - version_retrocompatible - } - } - - pub fn apply(&self, template: &String) -> String { - template - .replace("$NAME", &self.name) - .replace("$VERSION_MAJOR", &self.version_major) - .replace("$VERSION_MINOR", &self.version_minor) - .replace("$VERSION_PATCH", &self.version_patch) - .replace("$VERSION_SUFFIX", &self.version_suffix) - .replace("$VERSION_RETROCOMPATIBLE", &self.version_retrocompatible) - .replace("$VERSION", &self.version) - } -} - fn find_dependencies_for_package(root: &DependencyTreeNode, package_name: &str) -> Vec { - if root.project.name.replace("/", "-") == package_name { + if root.project.name.replace("/", "-").replace(".", "-") == package_name { return root.dependencies .iter() - .map(|dep| Dependency { - name: dep.project.name.replace("/", "-"), - version: dep.project.version.clone(), + .map(|dep| { + let environment: Environment = Environment::new(&dep.project.name, &dep.project.version); + + Dependency { + name: environment.name.replace("/", "-").replace(".", "-"), + version: format!("{}-24.04", environment.version_retrocompatible), + } }) .collect(); } diff --git a/replication/Cargo.toml b/replication/Cargo.toml index 3f7c862..84425c3 100644 --- a/replication/Cargo.toml +++ b/replication/Cargo.toml @@ -12,3 +12,4 @@ serde = { version = "1.0.219", features = ["derive"] } uuid = { version = "1.18.0", features = ["v4"] } source-wand-common = { path = "../common" } +readonly = "0.2.13" diff --git a/replication/src/apply/plan_to_execution_graph.rs b/replication/src/apply/plan_to_execution_graph.rs index 0680865..8967acf 100644 --- a/replication/src/apply/plan_to_execution_graph.rs +++ b/replication/src/apply/plan_to_execution_graph.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::{model::{package_destination::PackageDestination, package_origin::PackageOrigin, replication_plan::ReplicationPlan}, plan::{transformation_node::{NodeId, TransformationNode}, transformations::{git::{git_init::GitInit, git_push::GitPush}, golang::fetch_source::GolangFetchSource}}}; +use crate::{model::{package_destination::PackageDestination, package_origin::PackageOrigin, replication_plan::ReplicationPlan}, plan::{environment::Environment, transformation_node::{NodeId, TransformationNode}, transformations::{git::{git_init::GitInit, git_push::GitPush}, golang::fetch_source::GolangFetchSource, sourcecraft::initialize::SourcecraftInitialize}}}; impl ReplicationPlan { pub fn to_execution_graph(&self) -> Vec> { @@ -11,7 +11,8 @@ impl ReplicationPlan { if let PackageOrigin::GoCache(origin) = &package.origin { let PackageDestination::Git(destination) = &package.destination; - let workdesk: String = format!("{} ({})", origin.name, origin.version); + let environment: Environment = Environment::new(&origin.name, &origin.version); + let workdesk: String = format!("{} ({}-24.04/edge)", environment.name, environment.version_retrocompatible); let fetch: TransformationNode = TransformationNode { id, @@ -24,134 +25,54 @@ impl ReplicationPlan { let init: TransformationNode = TransformationNode { id: id + 1, workdesk: workdesk.clone(), - transformation: Arc::new(GitInit::new(destination.git.clone(), destination.reference.clone())), + transformation: Arc::new(GitInit::new( + destination.git.clone(), + destination.reference.clone(), + if let Some(config) = &self.config { + config.git_identity.clone() + } + else { + None + }, + )), dependencies: vec![id], dependents: vec![id + 2] }; let push: TransformationNode = TransformationNode { id: id + 2, - workdesk, - transformation: Arc::new(GitPush::new(destination.git.clone(), destination.reference.clone())), + workdesk: workdesk.clone(), + transformation: Arc::new(GitPush::new( + destination.git.clone(), + destination.reference.clone(), + )), dependencies: vec![id + 1], + dependents: vec![id + 3] + }; + + let initialize_sourcecraft: TransformationNode = TransformationNode { + id: id + 3, + workdesk, + transformation: Arc::new(SourcecraftInitialize::new( + environment.name.clone(), + format!("{}-24.04", environment.version_retrocompatible.clone()), + "ubuntu@24.04".to_string(), + vec!["amd64".to_string()], + package.dependencies.clone(), + )), + dependencies: vec![id + 2], dependents: vec![] }; execution_graph.push(Arc::new(fetch)); execution_graph.push(Arc::new(init)); execution_graph.push(Arc::new(push)); + execution_graph.push(Arc::new(initialize_sourcecraft)); - id += 3; + id += 4; } } execution_graph } } - -// use std::{collections::HashMap, sync::Arc}; - -// use crate::{model::{package::Package, replication_plan::ReplicationPlan}, plan::{transformation_node::{NodeId, TransformationNode}, transformations::{git::{git_init::GitInit, git_push::GitPush}, golang::fetch_source::GolangFetchSource}}}; - - -// impl ReplicationPlan { -// pub fn to_execution_graph(&self) -> Vec> { -// let mut nodes: Vec> = Vec::new(); -// let mut node_id_counter = 0; - -// let mut package_nodes: HashMap = HashMap::new(); - -// for package in &self.packages { -// let golang_fetch = Arc::new(GolangFetchSource::new(match &package.origin { -// crate::model::package_origin::PackageOrigin::GoCache(origin) => origin.path.clone(), -// crate::model::package_origin::PackageOrigin::Git(origin) => origin.git.clone(), -// })); - -// let golang_fetch_node_id = node_id_counter; -// node_id_counter += 1; - -// let golang_fetch_node = TransformationNode { -// id: golang_fetch_node_id, -// transformation: golang_fetch, -// dependencies: Vec::new(), -// dependents: Vec::new(), -// }; - -// let (repo_url, reference) = match &package.destination { -// crate::model::package_destination::PackageDestination::Git(dest) => (&dest.git, &dest.reference), -// }; -// let git_init = Arc::new(GitInit::new(repo_url.clone(), reference.clone())); - -// let git_init_node_id = node_id_counter; -// node_id_counter += 1; - -// let git_init_node = TransformationNode { -// id: git_init_node_id, -// transformation: git_init, -// dependencies: vec![golang_fetch_node_id], -// dependents: Vec::new(), -// }; - -// let git_push = Arc::new(GitPush::new(repo_url.clone(), reference.clone())); - -// let git_push_node_id = node_id_counter; -// node_id_counter += 1; - -// let git_push_node = TransformationNode { -// id: git_push_node_id, -// transformation: git_push, -// dependencies: vec![git_init_node_id], -// dependents: Vec::new(), -// }; - -// let mut golang_fetch_node = Arc::try_unwrap(golang_fetch_node.into()) -// .unwrap_or_else(|arc| (*arc).clone()); -// golang_fetch_node.dependents.push(git_init_node_id); -// let mut git_init_node = Arc::try_unwrap(git_init_node.into()) -// .unwrap_or_else(|arc| (*arc).clone()); -// git_init_node.dependents.push(git_push_node_id); - -// nodes.push(Arc::new(golang_fetch_node)); -// nodes.push(Arc::new(git_init_node)); -// nodes.push(Arc::new(git_push_node)); - -// package_nodes.insert(package.origin_key(), (golang_fetch_node_id, git_push_node_id)); -// } - -// let mut id_to_node = nodes.iter().map(|n| (n.id, Arc::clone(n))).collect::>(); - -// for package in &self.packages { -// let (pkg_golang_fetch_id, _) = package_nodes.get(&package.origin_key()).unwrap(); - -// for dependency in &package.dependencies { -// if let Some((_, dep_git_push_id)) = package_nodes.get(&dependency.name) { -// let mut pkg_golang_fetch_node = Arc::try_unwrap(id_to_node[pkg_golang_fetch_id].clone()) -// .unwrap_or_else(|arc| (*arc).clone()); -// if !pkg_golang_fetch_node.dependencies.contains(dep_git_push_id) { -// pkg_golang_fetch_node.dependencies.push(*dep_git_push_id); -// } - -// let mut dep_git_push_node = Arc::try_unwrap(id_to_node[dep_git_push_id].clone()) -// .unwrap_or_else(|arc| (*arc).clone()); -// if !dep_git_push_node.dependents.contains(pkg_golang_fetch_id) { -// dep_git_push_node.dependents.push(*pkg_golang_fetch_id); -// } - -// id_to_node.insert(*pkg_golang_fetch_id, Arc::new(pkg_golang_fetch_node)); -// id_to_node.insert(*dep_git_push_id, Arc::new(dep_git_push_node)); -// } -// } -// } - -// id_to_node.values().cloned().collect() -// } -// } - -// impl Package { -// fn origin_key(&self) -> String { -// match &self.origin { -// crate::model::package_origin::PackageOrigin::Git(origin) => origin.git.clone(), -// crate::model::package_origin::PackageOrigin::GoCache(origin) => origin.name.clone(), -// } -// } -// } diff --git a/replication/src/model/mod.rs b/replication/src/model/mod.rs index d563fd6..854b1a5 100644 --- a/replication/src/model/mod.rs +++ b/replication/src/model/mod.rs @@ -1,3 +1,4 @@ +pub mod replication_config; pub mod replication_manifest; pub mod replication_plan; diff --git a/replication/src/model/replication_config.rs b/replication/src/model/replication_config.rs new file mode 100644 index 0000000..2cc01cc --- /dev/null +++ b/replication/src/model/replication_config.rs @@ -0,0 +1,12 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReplicationConfig { + pub git_identity: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GitIdentity { + pub username: String, + pub email: String, +} diff --git a/replication/src/model/replication_manifest.rs b/replication/src/model/replication_manifest.rs index f6cdd9f..4b0bbf6 100644 --- a/replication/src/model/replication_manifest.rs +++ b/replication/src/model/replication_manifest.rs @@ -1,6 +1,6 @@ use serde::{Serialize, Deserialize}; -use crate::model::{hooks::Hooks, package_destination::PackageDestination, package_origin::PackageOrigin}; +use crate::model::{hooks::Hooks, package_destination::PackageDestination, package_origin::PackageOrigin, replication_config::ReplicationConfig}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReplicationManifest { @@ -8,10 +8,18 @@ pub struct ReplicationManifest { pub hooks: Option, pub origin: PackageOrigin, pub destination_template: PackageDestination, + + pub config: Option, } impl ReplicationManifest { - pub fn new(project: String, hooks: Option, origin: PackageOrigin, destination_template: PackageDestination) -> Self { - ReplicationManifest { project, hooks, origin, destination_template } + pub fn new( + project: String, + hooks: Option, + origin: PackageOrigin, + destination_template: PackageDestination, + config: Option, + ) -> Self { + ReplicationManifest { project, hooks, origin, destination_template, config } } } diff --git a/replication/src/model/replication_plan.rs b/replication/src/model/replication_plan.rs index 7f1829f..00dcecd 100644 --- a/replication/src/model/replication_plan.rs +++ b/replication/src/model/replication_plan.rs @@ -1,16 +1,23 @@ use serde::{Serialize, Deserialize}; -use crate::model::{hooks::Hooks, package::Package}; +use crate::model::{hooks::Hooks, package::Package, replication_config::ReplicationConfig}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReplicationPlan { pub project: String, pub hooks: Option, pub packages: Vec, + + pub config: Option, } impl ReplicationPlan { - pub fn new(project: String, hooks: Option, packages: Vec) -> Self { - ReplicationPlan { project, hooks, packages } + pub fn new( + project: String, + hooks: Option, + packages: Vec, + config: Option, + ) -> Self { + ReplicationPlan { project, hooks, packages, config } } } diff --git a/replication/src/plan/environment.rs b/replication/src/plan/environment.rs new file mode 100644 index 0000000..7c300d7 --- /dev/null +++ b/replication/src/plan/environment.rs @@ -0,0 +1,83 @@ +use regex::Regex; + +#[readonly::make] +pub struct Environment { + pub name: String, + pub version: String, + pub version_major: String, + pub version_minor: String, + pub version_patch: String, + pub version_suffix: String, + pub version_retrocompatible: String, +} + +impl Environment { + pub fn new(name: &String, version: &String) -> Self { + let mut major: String = String::new(); + let mut minor: String = String::new(); + let mut patch: String = String::new(); + let mut suffix: String = String::new(); + + if version.starts_with('v') { + let parts: Vec<&str> = version.trim_start_matches('v').split('-').collect(); + let semantic_version_parts: Vec<&str> = parts[0].split('.').collect(); + + if semantic_version_parts.len() > 0 { + major = semantic_version_parts[0].to_string(); + } + if semantic_version_parts.len() > 1 { + minor = semantic_version_parts[1].to_string(); + } + if semantic_version_parts.len() > 2 { + patch = semantic_version_parts[2].to_string(); + } + + if parts.len() > 1 { + suffix = format!("-{}", parts[1..].join("-")); + } + } + + let retrocompatible = if !suffix.is_empty() { + let re: Regex = Regex::new(r"(\d{14})-([a-f0-9]{12,40})$").unwrap(); + if let Some(caps) = re.captures(&suffix) { + let datetime_str: &str = caps.get(1).unwrap().as_str(); + let hash: &str = caps.get(2).unwrap().as_str(); + + let year: &str = &datetime_str[0..4]; + let month: &str = &datetime_str[4..6]; + let day: &str = &datetime_str[6..8]; + + format!("{}{}{}-{}", year, month, day, &hash[0..7]) + } else { + format!("{}.{}.{}-{}", major, minor, patch, suffix) + } + } else { + if major == "0".to_string() { + format!("{}.{}.{}", major.clone(), minor, patch) + } else { + major.clone() + } + }; + + Environment { + name: name.clone(), + version: version.clone(), + version_major: major, + version_minor: minor, + version_patch: patch, + version_suffix: suffix, + version_retrocompatible: retrocompatible, + } + } + + pub fn apply(&self, template: &String) -> String { + template + .replace("$NAME", &self.name) + .replace("$VERSION_MAJOR", &self.version_major) + .replace("$VERSION_MINOR", &self.version_minor) + .replace("$VERSION_PATCH", &self.version_patch) + .replace("$VERSION_SUFFIX", &self.version_suffix) + .replace("$VERSION_RETROCOMPATIBLE", &self.version_retrocompatible) + .replace("$VERSION", &self.version) + } +} diff --git a/replication/src/plan/mod.rs b/replication/src/plan/mod.rs index bb3ca15..b3a8172 100644 --- a/replication/src/plan/mod.rs +++ b/replication/src/plan/mod.rs @@ -4,3 +4,4 @@ pub mod transformations; pub mod transformation_node; pub mod context; +pub mod environment; diff --git a/replication/src/plan/transformations/git/git_init.rs b/replication/src/plan/transformations/git/git_init.rs index 1f49500..e7a9a57 100644 --- a/replication/src/plan/transformations/git/git_init.rs +++ b/replication/src/plan/transformations/git/git_init.rs @@ -1,23 +1,34 @@ use anyhow::Result; use source_wand_common::project_manipulator::project_manipulator::ProjectManipulator; -use crate::plan::{context::Context, transformation::Transformation}; +use crate::{model::replication_config::GitIdentity, plan::{context::Context, transformation::Transformation}}; #[derive(Debug, Clone)] pub struct GitInit { repository_url: String, reference: String, + git_identity: Option, } impl GitInit { - pub fn new(repository_url: String, reference: String) -> Self { - GitInit { repository_url, reference } + pub fn new( + repository_url: String, + reference: String, + git_identity: Option, + ) -> Self { + GitInit { repository_url, reference, git_identity } } } impl Transformation for GitInit { fn apply(&self, ctx: Context) -> Result { ctx.sh.run_shell("git init".to_string())?; + + if let Some(git_identity) = &self.git_identity { + ctx.sh.run_shell(format!("git config --local user.name {}", git_identity.username))?; + ctx.sh.run_shell(format!("git config --local user.email {}", git_identity.email))?; + } + ctx.sh.run_shell(format!("git remote add origin {}", self.repository_url))?; ctx.sh.run_shell(format!("git checkout --orphan {}", self.reference))?; diff --git a/replication/src/plan/transformations/git/git_push.rs b/replication/src/plan/transformations/git/git_push.rs index 584ece6..3a51c18 100644 --- a/replication/src/plan/transformations/git/git_push.rs +++ b/replication/src/plan/transformations/git/git_push.rs @@ -10,7 +10,10 @@ pub struct GitPush { } impl GitPush { - pub fn new(repository_url: String, reference: String) -> Self { + pub fn new( + repository_url: String, + reference: String, + ) -> Self { GitPush { repository_url, reference } } } diff --git a/replication/src/plan/transformations/mod.rs b/replication/src/plan/transformations/mod.rs index e9645c5..e01bc20 100644 --- a/replication/src/plan/transformations/mod.rs +++ b/replication/src/plan/transformations/mod.rs @@ -1,2 +1,3 @@ pub mod golang; pub mod git; +pub mod sourcecraft; diff --git a/replication/src/plan/transformations/sourcecraft/initialize.rs b/replication/src/plan/transformations/sourcecraft/initialize.rs new file mode 100644 index 0000000..be422db --- /dev/null +++ b/replication/src/plan/transformations/sourcecraft/initialize.rs @@ -0,0 +1,178 @@ +use std::collections::HashMap; + +use anyhow::Result; +use serde::Serialize; +use source_wand_common::{project_manipulator::project_manipulator::ProjectManipulator, utils::write_yaml_file::write_yaml_file}; + +use crate::{model::dependency::Dependency, plan::{context::Context, transformation::Transformation}}; + +#[derive(Debug, Clone)] +pub struct SourcecraftInitialize { + pub name: String, + pub version: String, + pub base: String, + pub platforms: Vec, + pub dependencies: Vec, +} + +impl SourcecraftInitialize { + pub fn new( + name: String, + version: String, + base: String, + platforms: Vec, + dependencies: Vec, + ) -> Self { + SourcecraftInitialize { + name, + version, + base, + platforms, + dependencies, + } + } +} + +#[derive(Debug, Clone, Serialize)] +struct SourcecraftMetadata { + pub name: String, + pub version: String, + pub base: String, + pub summary: String, + pub description: String, + pub platforms: HashMap, + pub parts: HashMap +} + +#[derive(Debug, Clone, Serialize)] +#[serde(untagged)] +enum Part { + Go(GoPart), + GoUse(GoUsePart), +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "kebab-case")] +struct GoPart { + pub plugin: String, + pub source: String, + pub build_snaps: Option>, + pub build_environment: Option>>, + pub after: Option>, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "kebab-case")] +struct GoUsePart { + pub plugin: String, + pub source: String, + pub source_channel: Option, +} + +impl Part { + pub fn with_go_plugin( + source: String, + build_snaps: Vec, + build_environment: Vec>, + after: Vec, + ) -> Self { + Part::Go( + GoPart { + plugin: "go".to_string(), + source, + build_snaps: Some(build_snaps), + build_environment: Some(build_environment), + after: Some(after), + } + ) + } + + pub fn with_go_use_plugin( + name: String, + track: String, + ) -> Self { + Part::GoUse( + GoUsePart { + plugin: "go-use".to_string(), + source: format!("sourcecraft:{}", name), + source_channel: Some(format!("{}/edge", track)), + } + ) + } +} + +impl SourcecraftMetadata { + pub fn from_args(args: &SourcecraftInitialize) -> Self { + let mut parts: HashMap = HashMap::new(); + + for dependency in &args.dependencies { + parts.insert( + dependency.name.clone(), + Part::with_go_use_plugin( + dependency.name.clone(), + dependency.version.clone() + ) + ); + } + + parts.insert( + args.name.clone(), + Part::with_go_plugin( + ".".to_string(), + vec!["go".to_string()], + [ + ("GOFLAGS".to_string(), "-json".to_string()), + ("GOPROXY".to_string(), "False".to_string()), + ].iter() + .map(|(key, value)| { + let mut map: HashMap = HashMap::new(); + map.insert(key.clone(), value.clone()); + map + }).collect(), + args.dependencies + .iter() + .map(|dependency| dependency.name.clone()) + .collect() + ) + ); + + SourcecraftMetadata { + name: args.name.clone(), + version: args.version.clone(), + base: args.base.clone(), + summary: format!("{} version {} (Golang program)", args.name.clone(), args.version.clone()), + description: format!("{} version {} (Golang program), onboarded by source-wand", args.name.clone(), args.version.clone()), + platforms: args.platforms.clone() + .into_iter() + .map(|platform| (platform.clone(), ())) + .collect(), + parts + } + } +} + +impl Transformation for SourcecraftInitialize { + fn apply(&self, ctx: Context) -> Result { + let sourcecraft_metadata: SourcecraftMetadata = SourcecraftMetadata::from_args(&self); + write_yaml_file( + &sourcecraft_metadata, + format!("{}/sourcecraft.yaml", ctx.sh.project_root.to_str().unwrap()).as_str(), + )?; + Ok(ctx) + } + + fn should_skip(&self, ctx: &Context) -> Option { + if ctx.sh.run_shell( + "ls | grep \"^sourcecraft.yaml$\"".to_string() + ).unwrap_or_default().trim() == "sourcecraft.yaml" { + Some("sourcecraft.yaml already exists".to_string()) + } + else { + None + } + } + + fn get_name(&self) -> String { + "initialize sourcecraft project".to_string() + } +} diff --git a/replication/src/plan/transformations/sourcecraft/mod.rs b/replication/src/plan/transformations/sourcecraft/mod.rs new file mode 100644 index 0000000..c570ae3 --- /dev/null +++ b/replication/src/plan/transformations/sourcecraft/mod.rs @@ -0,0 +1 @@ +pub mod initialize; diff --git a/snapcraft.yaml b/snapcraft.yaml index e484b35..bf41e49 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -1,5 +1,5 @@ name: source-wand -version: 0.16.0 +version: 0.17.0 summary: CLI tool that helps with large scale manipulations of source code description: | `source-wand` is a CLI tool that helps with large scale manipulations of source code. From fe67751196353d286ad228a1e5c690632e5f7ea2 Mon Sep 17 00:00:00 2001 From: Thinking-Dragon Date: Wed, 27 Aug 2025 15:19:53 -0400 Subject: [PATCH 5/7] Create nil parts for to onboard golang libraries --- cli/src/commands/replication/plan.rs | 3 +- .../src/apply/plan_to_execution_graph.rs | 1 + replication/src/model/mod.rs | 3 +- replication/src/model/package.rs | 10 +- replication/src/model/sourcecraft/go_part.rs | 13 ++ .../src/model/sourcecraft/go_use_part.rs | 9 + replication/src/model/sourcecraft/mod.rs | 7 + replication/src/model/sourcecraft/nil_part.rs | 8 + replication/src/model/sourcecraft/part.rs | 49 +++++ .../model/sourcecraft/sourcecraft_metadata.rs | 16 ++ .../transformations/sourcecraft/initialize.rs | 192 +++++++----------- 11 files changed, 191 insertions(+), 120 deletions(-) create mode 100644 replication/src/model/sourcecraft/go_part.rs create mode 100644 replication/src/model/sourcecraft/go_use_part.rs create mode 100644 replication/src/model/sourcecraft/mod.rs create mode 100644 replication/src/model/sourcecraft/nil_part.rs create mode 100644 replication/src/model/sourcecraft/part.rs create mode 100644 replication/src/model/sourcecraft/sourcecraft_metadata.rs diff --git a/cli/src/commands/replication/plan.rs b/cli/src/commands/replication/plan.rs index 1d26e1b..6a94899 100644 --- a/cli/src/commands/replication/plan.rs +++ b/cli/src/commands/replication/plan.rs @@ -57,7 +57,7 @@ pub fn plan_replication() -> Result { let dependency_tree: DependencyTreeNode = find_dependency_tree( DependencyTreeRequest::from_git_project( - origin.git, + origin.git.clone(), Some(origin.reference.clone()) ) )?; @@ -118,6 +118,7 @@ pub fn plan_replication() -> Result { package_destination_reference, ), dependencies, + !origin.git.clone().replace("/", "-").replace(".", "-").ends_with(&environment.name) ); packages.push(package); diff --git a/replication/src/apply/plan_to_execution_graph.rs b/replication/src/apply/plan_to_execution_graph.rs index 8967acf..bfc9655 100644 --- a/replication/src/apply/plan_to_execution_graph.rs +++ b/replication/src/apply/plan_to_execution_graph.rs @@ -59,6 +59,7 @@ impl ReplicationPlan { "ubuntu@24.04".to_string(), vec!["amd64".to_string()], package.dependencies.clone(), + package.is_library, )), dependencies: vec![id + 2], dependents: vec![] diff --git a/replication/src/model/mod.rs b/replication/src/model/mod.rs index 854b1a5..a2e5c95 100644 --- a/replication/src/model/mod.rs +++ b/replication/src/model/mod.rs @@ -5,6 +5,7 @@ pub mod replication_plan; pub mod hooks; pub mod package; +pub mod dependency; pub mod package_origin; pub mod package_origin_git; @@ -13,4 +14,4 @@ pub mod package_origin_go_cache; pub mod package_destination; pub mod package_destination_git; -pub mod dependency; +pub mod sourcecraft; diff --git a/replication/src/model/package.rs b/replication/src/model/package.rs index ea79f62..d3b317b 100644 --- a/replication/src/model/package.rs +++ b/replication/src/model/package.rs @@ -7,10 +7,16 @@ pub struct Package { pub origin: PackageOrigin, pub destination: PackageDestination, pub dependencies: Vec, + pub is_library: bool, } impl Package { - pub fn new(origin: PackageOrigin, destination: PackageDestination, dependencies: Vec) -> Self { - Package { origin, destination, dependencies } + pub fn new( + origin: PackageOrigin, + destination: PackageDestination, + dependencies: Vec, + is_library: bool, + ) -> Self { + Package { origin, destination, dependencies, is_library } } } diff --git a/replication/src/model/sourcecraft/go_part.rs b/replication/src/model/sourcecraft/go_part.rs new file mode 100644 index 0000000..0261bad --- /dev/null +++ b/replication/src/model/sourcecraft/go_part.rs @@ -0,0 +1,13 @@ +use std::collections::HashMap; + +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct GoPart { + pub plugin: String, + pub source: String, + pub build_snaps: Option>, + pub build_environment: Option>>, + pub after: Option>, +} diff --git a/replication/src/model/sourcecraft/go_use_part.rs b/replication/src/model/sourcecraft/go_use_part.rs new file mode 100644 index 0000000..32dab03 --- /dev/null +++ b/replication/src/model/sourcecraft/go_use_part.rs @@ -0,0 +1,9 @@ +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct GoUsePart { + pub plugin: String, + pub source: String, + pub source_channel: Option, +} diff --git a/replication/src/model/sourcecraft/mod.rs b/replication/src/model/sourcecraft/mod.rs new file mode 100644 index 0000000..0e8d192 --- /dev/null +++ b/replication/src/model/sourcecraft/mod.rs @@ -0,0 +1,7 @@ +pub mod sourcecraft_metadata; + +pub mod part; + +pub mod nil_part; +pub mod go_part; +pub mod go_use_part; diff --git a/replication/src/model/sourcecraft/nil_part.rs b/replication/src/model/sourcecraft/nil_part.rs new file mode 100644 index 0000000..385783b --- /dev/null +++ b/replication/src/model/sourcecraft/nil_part.rs @@ -0,0 +1,8 @@ +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct NilPart { + pub plugin: String, + pub source: String, +} diff --git a/replication/src/model/sourcecraft/part.rs b/replication/src/model/sourcecraft/part.rs new file mode 100644 index 0000000..bc21b8c --- /dev/null +++ b/replication/src/model/sourcecraft/part.rs @@ -0,0 +1,49 @@ +use std::collections::HashMap; + +use serde::Serialize; + +use crate::model::sourcecraft::{go_part::GoPart, go_use_part::GoUsePart, nil_part::NilPart}; + +#[derive(Debug, Clone, Serialize)] +#[serde(untagged)] +pub enum Part { + Nil(NilPart), + Go(GoPart), + GoUse(GoUsePart), +} + +impl Part { + pub fn with_nil_plugin() -> Self { + Part::Nil(NilPart { plugin: "nil".to_string(), source: ".".to_string() }) + } + + pub fn with_go_plugin( + source: String, + build_snaps: Vec, + build_environment: Vec>, + after: Vec, + ) -> Self { + Part::Go( + GoPart { + plugin: "go".to_string(), + source, + build_snaps: Some(build_snaps), + build_environment: Some(build_environment), + after: Some(after), + } + ) + } + + pub fn with_go_use_plugin( + name: String, + track: String, + ) -> Self { + Part::GoUse( + GoUsePart { + plugin: "go-use".to_string(), + source: format!("sourcecraft:{}", name), + source_channel: Some(format!("{}/edge", track)), + } + ) + } +} diff --git a/replication/src/model/sourcecraft/sourcecraft_metadata.rs b/replication/src/model/sourcecraft/sourcecraft_metadata.rs new file mode 100644 index 0000000..a073d1e --- /dev/null +++ b/replication/src/model/sourcecraft/sourcecraft_metadata.rs @@ -0,0 +1,16 @@ +use std::collections::HashMap; + +use serde::Serialize; + +use crate::model::sourcecraft::part::Part; + +#[derive(Debug, Clone, Serialize)] +pub struct SourcecraftMetadata { + pub name: String, + pub version: String, + pub base: String, + pub summary: String, + pub description: String, + pub platforms: HashMap, + pub parts: HashMap +} diff --git a/replication/src/plan/transformations/sourcecraft/initialize.rs b/replication/src/plan/transformations/sourcecraft/initialize.rs index be422db..65e727c 100644 --- a/replication/src/plan/transformations/sourcecraft/initialize.rs +++ b/replication/src/plan/transformations/sourcecraft/initialize.rs @@ -1,10 +1,25 @@ use std::collections::HashMap; use anyhow::Result; -use serde::Serialize; -use source_wand_common::{project_manipulator::project_manipulator::ProjectManipulator, utils::write_yaml_file::write_yaml_file}; -use crate::{model::dependency::Dependency, plan::{context::Context, transformation::Transformation}}; +use source_wand_common::{ + project_manipulator::project_manipulator::ProjectManipulator, + utils::write_yaml_file::write_yaml_file +}; + +use crate::{ + model::{ + dependency::Dependency, + sourcecraft::{ + part::Part, + sourcecraft_metadata::SourcecraftMetadata + } + }, + plan::{ + context::Context, + transformation::Transformation + } +}; #[derive(Debug, Clone)] pub struct SourcecraftInitialize { @@ -13,6 +28,8 @@ pub struct SourcecraftInitialize { pub base: String, pub platforms: Vec, pub dependencies: Vec, + + pub is_library: bool, } impl SourcecraftInitialize { @@ -22,6 +39,7 @@ impl SourcecraftInitialize { base: String, platforms: Vec, dependencies: Vec, + is_library: bool, ) -> Self { SourcecraftInitialize { name, @@ -29,113 +47,81 @@ impl SourcecraftInitialize { base, platforms, dependencies, + is_library, } } } -#[derive(Debug, Clone, Serialize)] -struct SourcecraftMetadata { - pub name: String, - pub version: String, - pub base: String, - pub summary: String, - pub description: String, - pub platforms: HashMap, - pub parts: HashMap -} - -#[derive(Debug, Clone, Serialize)] -#[serde(untagged)] -enum Part { - Go(GoPart), - GoUse(GoUsePart), -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "kebab-case")] -struct GoPart { - pub plugin: String, - pub source: String, - pub build_snaps: Option>, - pub build_environment: Option>>, - pub after: Option>, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "kebab-case")] -struct GoUsePart { - pub plugin: String, - pub source: String, - pub source_channel: Option, -} +impl Transformation for SourcecraftInitialize { + fn apply(&self, ctx: Context) -> Result { + let sourcecraft_metadata: SourcecraftMetadata = SourcecraftMetadata::from_args(&self); + write_yaml_file( + &sourcecraft_metadata, + format!("{}/sourcecraft.yaml", ctx.sh.project_root.to_str().unwrap()).as_str(), + )?; + Ok(ctx) + } -impl Part { - pub fn with_go_plugin( - source: String, - build_snaps: Vec, - build_environment: Vec>, - after: Vec, - ) -> Self { - Part::Go( - GoPart { - plugin: "go".to_string(), - source, - build_snaps: Some(build_snaps), - build_environment: Some(build_environment), - after: Some(after), - } - ) + fn should_skip(&self, ctx: &Context) -> Option { + if ctx.sh.run_shell( + "ls | grep \"^sourcecraft.yaml$\"".to_string() + ).unwrap_or_default().trim() == "sourcecraft.yaml" { + Some("sourcecraft.yaml already exists".to_string()) + } + else { + None + } } - pub fn with_go_use_plugin( - name: String, - track: String, - ) -> Self { - Part::GoUse( - GoUsePart { - plugin: "go-use".to_string(), - source: format!("sourcecraft:{}", name), - source_channel: Some(format!("{}/edge", track)), - } - ) + fn get_name(&self) -> String { + "initialize sourcecraft project".to_string() } } + impl SourcecraftMetadata { pub fn from_args(args: &SourcecraftInitialize) -> Self { let mut parts: HashMap = HashMap::new(); - for dependency in &args.dependencies { + if args.is_library { parts.insert( - dependency.name.clone(), - Part::with_go_use_plugin( + args.name.clone(), + Part::with_nil_plugin(), + ); + } + else { + for dependency in &args.dependencies { + parts.insert( dependency.name.clone(), - dependency.version.clone() + Part::with_go_use_plugin( + dependency.name.clone(), + dependency.version.clone() + ) + ); + } + + parts.insert( + args.name.clone(), + Part::with_go_plugin( + ".".to_string(), + vec!["go".to_string()], + [ + ("GOFLAGS".to_string(), "-json".to_string()), + ("GOPROXY".to_string(), "False".to_string()), + ].iter() + .map(|(key, value)| { + let mut map: HashMap = HashMap::new(); + map.insert(key.clone(), value.clone()); + map + }).collect(), + args.dependencies + .iter() + .map(|dependency| dependency.name.clone()) + .collect() ) ); } - parts.insert( - args.name.clone(), - Part::with_go_plugin( - ".".to_string(), - vec!["go".to_string()], - [ - ("GOFLAGS".to_string(), "-json".to_string()), - ("GOPROXY".to_string(), "False".to_string()), - ].iter() - .map(|(key, value)| { - let mut map: HashMap = HashMap::new(); - map.insert(key.clone(), value.clone()); - map - }).collect(), - args.dependencies - .iter() - .map(|dependency| dependency.name.clone()) - .collect() - ) - ); - SourcecraftMetadata { name: args.name.clone(), version: args.version.clone(), @@ -150,29 +136,3 @@ impl SourcecraftMetadata { } } } - -impl Transformation for SourcecraftInitialize { - fn apply(&self, ctx: Context) -> Result { - let sourcecraft_metadata: SourcecraftMetadata = SourcecraftMetadata::from_args(&self); - write_yaml_file( - &sourcecraft_metadata, - format!("{}/sourcecraft.yaml", ctx.sh.project_root.to_str().unwrap()).as_str(), - )?; - Ok(ctx) - } - - fn should_skip(&self, ctx: &Context) -> Option { - if ctx.sh.run_shell( - "ls | grep \"^sourcecraft.yaml$\"".to_string() - ).unwrap_or_default().trim() == "sourcecraft.yaml" { - Some("sourcecraft.yaml already exists".to_string()) - } - else { - None - } - } - - fn get_name(&self) -> String { - "initialize sourcecraft project".to_string() - } -} From e84b89ada000ddd2dfcd7a228f94513f2b5a73d8 Mon Sep 17 00:00:00 2001 From: Thinking-Dragon Date: Thu, 28 Aug 2025 15:25:55 -0400 Subject: [PATCH 6/7] Make initializing projects idempotent --- .../src/apply/plan_to_execution_graph.rs | 90 ++++++++++++------- .../src/plan/transformations/git/git_init.rs | 45 +++++----- .../src/plan/transformations/git/git_push.rs | 26 +++--- .../transformations/initialize_project.rs | 49 ++++++++++ replication/src/plan/transformations/mod.rs | 2 + 5 files changed, 149 insertions(+), 63 deletions(-) create mode 100644 replication/src/plan/transformations/initialize_project.rs diff --git a/replication/src/apply/plan_to_execution_graph.rs b/replication/src/apply/plan_to_execution_graph.rs index bfc9655..dd07a9e 100644 --- a/replication/src/apply/plan_to_execution_graph.rs +++ b/replication/src/apply/plan_to_execution_graph.rs @@ -1,6 +1,28 @@ use std::sync::Arc; -use crate::{model::{package_destination::PackageDestination, package_origin::PackageOrigin, replication_plan::ReplicationPlan}, plan::{environment::Environment, transformation_node::{NodeId, TransformationNode}, transformations::{git::{git_init::GitInit, git_push::GitPush}, golang::fetch_source::GolangFetchSource, sourcecraft::initialize::SourcecraftInitialize}}}; +use crate::{ + model::{ + package_destination::PackageDestination, + package_origin::PackageOrigin, + replication_plan::ReplicationPlan + }, + plan::{ + environment::Environment, + transformation_node::{ + NodeId, + TransformationNode + }, + transformations::{ + git::{ + git_init::GitInit, + git_push::GitPush + }, + golang::fetch_source::GolangFetchSource, + initialize_project::InitializeProject, + sourcecraft::initialize::SourcecraftInitialize + } + } +}; impl ReplicationPlan { pub fn to_execution_graph(&self) -> Vec> { @@ -14,45 +36,42 @@ impl ReplicationPlan { let environment: Environment = Environment::new(&origin.name, &origin.version); let workdesk: String = format!("{} ({}-24.04/edge)", environment.name, environment.version_retrocompatible); - let fetch: TransformationNode = TransformationNode { - id, + let initialize_project: TransformationNode = TransformationNode { + id: id, workdesk: workdesk.clone(), - transformation: Arc::new(GolangFetchSource::new(origin.path.clone())), + transformation: Arc::new( + InitializeProject::new( + GitInit::new( + destination.git.clone(), + destination.reference.clone(), + if let Some(config) = &self.config { + config.git_identity.clone() + } + else { + None + }, + ), + GolangFetchSource::new(origin.path.clone()), + ) + ), dependencies: vec![], - dependents: vec![id + 1] + dependents: vec![id + 1], }; - let init: TransformationNode = TransformationNode { + let push_code: TransformationNode = TransformationNode { id: id + 1, workdesk: workdesk.clone(), - transformation: Arc::new(GitInit::new( - destination.git.clone(), + transformation: Arc::new(GitPush::new( destination.reference.clone(), - if let Some(config) = &self.config { - config.git_identity.clone() - } - else { - None - }, + "Replicate source code".to_string(), )), dependencies: vec![id], dependents: vec![id + 2] }; - - let push: TransformationNode = TransformationNode { - id: id + 2, - workdesk: workdesk.clone(), - transformation: Arc::new(GitPush::new( - destination.git.clone(), - destination.reference.clone(), - )), - dependencies: vec![id + 1], - dependents: vec![id + 3] - }; let initialize_sourcecraft: TransformationNode = TransformationNode { - id: id + 3, - workdesk, + id: id + 2, + workdesk: workdesk.clone(), transformation: Arc::new(SourcecraftInitialize::new( environment.name.clone(), format!("{}-24.04", environment.version_retrocompatible.clone()), @@ -61,14 +80,25 @@ impl ReplicationPlan { package.dependencies.clone(), package.is_library, )), + dependencies: vec![id + 1], + dependents: vec![id + 3] + }; + + let push_sourcecraft_metadata: TransformationNode = TransformationNode { + id: id + 3, + workdesk: workdesk, + transformation: Arc::new(GitPush::new( + destination.reference.clone(), + "Initialize sourcecraft".to_string(), + )), dependencies: vec![id + 2], dependents: vec![] }; - execution_graph.push(Arc::new(fetch)); - execution_graph.push(Arc::new(init)); - execution_graph.push(Arc::new(push)); + execution_graph.push(Arc::new(initialize_project)); + execution_graph.push(Arc::new(push_code)); execution_graph.push(Arc::new(initialize_sourcecraft)); + execution_graph.push(Arc::new(push_sourcecraft_metadata)); id += 4; } diff --git a/replication/src/plan/transformations/git/git_init.rs b/replication/src/plan/transformations/git/git_init.rs index e7a9a57..495e079 100644 --- a/replication/src/plan/transformations/git/git_init.rs +++ b/replication/src/plan/transformations/git/git_init.rs @@ -18,40 +18,45 @@ impl GitInit { ) -> Self { GitInit { repository_url, reference, git_identity } } + + pub fn reference_exists(&self, ctx: &Context) -> bool { + let ls_remote: Result = ctx.sh.run_shell( + format!( + "git ls-remote --exit-code --heads {} {}", + self.repository_url, + self.reference + ) + ); + + ls_remote.is_ok() + } } impl Transformation for GitInit { fn apply(&self, ctx: Context) -> Result { - ctx.sh.run_shell("git init".to_string())?; + if self.reference_exists(&ctx) { + ctx.sh.run_shell(format!("git clone {} .", self.repository_url))?; + ctx.sh.run_shell(format!("git checkout {}", self.reference))?; + ctx.sh.run_shell("git pull".to_string())?; + } + else { + ctx.sh.run_shell("git init".to_string())?; + ctx.sh.run_shell(format!("git remote add origin {}", self.repository_url))?; + ctx.sh.run_shell(format!("git checkout --orphan {}", self.reference))?; + } if let Some(git_identity) = &self.git_identity { ctx.sh.run_shell(format!("git config --local user.name {}", git_identity.username))?; ctx.sh.run_shell(format!("git config --local user.email {}", git_identity.email))?; } - ctx.sh.run_shell(format!("git remote add origin {}", self.repository_url))?; - ctx.sh.run_shell(format!("git checkout --orphan {}", self.reference))?; - Ok(ctx) } - fn should_skip(&self, ctx: &Context) -> Option { - let ls_remote: Result = ctx.sh.run_shell( - format!( - "git ls-remote --exit-code --heads {} {}", - self.repository_url, - self.reference - ) - ); - - if ls_remote.is_ok() { - Some("reference already exists on remote".to_string()) - } - else { - None - } + fn should_skip(&self, _: &Context) -> Option { + None } - + fn get_name(&self) -> String { "initialize git repository".to_string() } diff --git a/replication/src/plan/transformations/git/git_push.rs b/replication/src/plan/transformations/git/git_push.rs index 3a51c18..7e3f8ac 100644 --- a/replication/src/plan/transformations/git/git_push.rs +++ b/replication/src/plan/transformations/git/git_push.rs @@ -5,45 +5,45 @@ use crate::plan::{context::Context, transformation::Transformation}; #[derive(Debug, Clone)] pub struct GitPush { - repository_url: String, reference: String, + commit_text: String, } impl GitPush { pub fn new( - repository_url: String, reference: String, + commit_text: String, ) -> Self { - GitPush { repository_url, reference } + GitPush { reference, commit_text } } } impl Transformation for GitPush { fn apply(&self, ctx: Context) -> Result { ctx.sh.run_shell("git add .".to_string())?; - ctx.sh.run_shell("git commit -m 'Replicate source code'".to_string())?; + ctx.sh.run_shell(format!("git commit -m '{}'", self.commit_text))?; ctx.sh.run_shell(format!("git push -u origin {}", self.reference))?; Ok(ctx) } fn should_skip(&self, ctx: &Context) -> Option { - let ls_remote: Result = ctx.sh.run_shell( - format!( - "git ls-remote --exit-code --heads {} {}", - self.repository_url, - self.reference - ) + if ctx.sh.run_shell("git rev-parse --is-inside-work-tree".to_string()).is_err() { + return Some("local is not a git repository".to_string()); + } + + let clean_tree: Result = ctx.sh.run_shell( + "git diff --quiet && git diff --cached --quiet && [ -z \"$(git ls-files --others --exclude-standard)\" ]".to_string() ); - if ls_remote.is_ok() { - Some("reference already exists on remote".to_string()) + if clean_tree.is_ok() { + Some("there is nothing to push".to_string()) } else { None } } - + fn get_name(&self) -> String { "push to git".to_string() } diff --git a/replication/src/plan/transformations/initialize_project.rs b/replication/src/plan/transformations/initialize_project.rs new file mode 100644 index 0000000..18c3675 --- /dev/null +++ b/replication/src/plan/transformations/initialize_project.rs @@ -0,0 +1,49 @@ +use anyhow::Result; + +use crate::{ + plan::{ + context::Context, + transformation::Transformation, + transformations::{ + git::git_init::GitInit, + golang::fetch_source::GolangFetchSource + } + } +}; + +#[derive(Debug, Clone)] +pub struct InitializeProject { + git_init: GitInit, + fetch_source: GolangFetchSource, +} + +impl InitializeProject { + pub fn new( + git_init: GitInit, + fetch_source: GolangFetchSource, + ) -> Self { + InitializeProject { git_init, fetch_source } + } +} + +impl Transformation for InitializeProject { + fn apply(&self, ctx: Context) -> Result { + if self.git_init.reference_exists(&ctx) { + self.git_init.apply(ctx.clone())?; + } + else { + self.fetch_source.apply(ctx.clone())?; + self.git_init.apply(ctx.clone())?; + } + + Ok(ctx) + } + + fn should_skip(&self, _: &Context) -> Option { + None + } + + fn get_name(&self) -> String { + "initialize project".to_string() + } +} diff --git a/replication/src/plan/transformations/mod.rs b/replication/src/plan/transformations/mod.rs index e01bc20..8aed69d 100644 --- a/replication/src/plan/transformations/mod.rs +++ b/replication/src/plan/transformations/mod.rs @@ -1,3 +1,5 @@ pub mod golang; pub mod git; pub mod sourcecraft; + +pub mod initialize_project; From 6fb845fb6fa101be7aefbfd5efbc3966846ffda4 Mon Sep 17 00:00:00 2001 From: Thinking-Dragon Date: Thu, 28 Aug 2025 15:45:40 -0400 Subject: [PATCH 7/7] Add option to include messages in [execute] logs --- replication/src/apply/plan_executor.rs | 35 +++++++++++-------- replication/src/plan/transformation.rs | 2 +- .../src/plan/transformations/git/git_init.rs | 4 +-- .../src/plan/transformations/git/git_push.rs | 4 +-- .../transformations/golang/fetch_source.rs | 4 +-- .../transformations/initialize_project.rs | 6 ++-- .../transformations/sourcecraft/initialize.rs | 4 +-- 7 files changed, 32 insertions(+), 27 deletions(-) diff --git a/replication/src/apply/plan_executor.rs b/replication/src/apply/plan_executor.rs index c652b4a..706076d 100644 --- a/replication/src/apply/plan_executor.rs +++ b/replication/src/apply/plan_executor.rs @@ -86,21 +86,26 @@ pub fn execute_plan(nodes: Vec>) -> Result<()> { ); } else { - let transformation_result: Result = node.transformation.apply(ctx); - if let Err(e) = transformation_result { - *error.lock().unwrap() = Err(e); - return; - } - else { - println!( - "{:<106} context: {}", - format!( - "{} {}", - "[execute]".to_string().green(), - node.transformation.get_name().blue(), - ), - node.workdesk, - ); + let transformation_result: Result> = node.transformation.apply(ctx); + match transformation_result { + Ok(message) => { + let message: String = message.unwrap_or_default(); + + println!( + "{:<120} context: {}", + format!( + "{} {} {}", + "[execute]".to_string().green(), + node.transformation.get_name().blue(), + message.italic(), + ), + node.workdesk, + ); + }, + Err(e) => { + *error.lock().unwrap() = Err(e); + return; + } } } } else { diff --git a/replication/src/plan/transformation.rs b/replication/src/plan/transformation.rs index 1d916b6..4d11a38 100644 --- a/replication/src/plan/transformation.rs +++ b/replication/src/plan/transformation.rs @@ -3,7 +3,7 @@ use anyhow::Result; use crate::plan::context::Context; pub trait Transformation: Send + Sync + TransformationClone { - fn apply(&self, ctx: Context) -> Result; + fn apply(&self, ctx: Context) -> Result>; fn should_skip(&self, ctx: &Context) -> Option; fn get_name(&self) -> String; } diff --git a/replication/src/plan/transformations/git/git_init.rs b/replication/src/plan/transformations/git/git_init.rs index 495e079..ee22339 100644 --- a/replication/src/plan/transformations/git/git_init.rs +++ b/replication/src/plan/transformations/git/git_init.rs @@ -33,7 +33,7 @@ impl GitInit { } impl Transformation for GitInit { - fn apply(&self, ctx: Context) -> Result { + fn apply(&self, ctx: Context) -> Result> { if self.reference_exists(&ctx) { ctx.sh.run_shell(format!("git clone {} .", self.repository_url))?; ctx.sh.run_shell(format!("git checkout {}", self.reference))?; @@ -50,7 +50,7 @@ impl Transformation for GitInit { ctx.sh.run_shell(format!("git config --local user.email {}", git_identity.email))?; } - Ok(ctx) + Ok(None) } fn should_skip(&self, _: &Context) -> Option { diff --git a/replication/src/plan/transformations/git/git_push.rs b/replication/src/plan/transformations/git/git_push.rs index 7e3f8ac..9e2bd67 100644 --- a/replication/src/plan/transformations/git/git_push.rs +++ b/replication/src/plan/transformations/git/git_push.rs @@ -19,12 +19,12 @@ impl GitPush { } impl Transformation for GitPush { - fn apply(&self, ctx: Context) -> Result { + fn apply(&self, ctx: Context) -> Result> { ctx.sh.run_shell("git add .".to_string())?; ctx.sh.run_shell(format!("git commit -m '{}'", self.commit_text))?; ctx.sh.run_shell(format!("git push -u origin {}", self.reference))?; - Ok(ctx) + Ok(Some(format!("commit \"{}\"", self.commit_text))) } fn should_skip(&self, ctx: &Context) -> Option { diff --git a/replication/src/plan/transformations/golang/fetch_source.rs b/replication/src/plan/transformations/golang/fetch_source.rs index 7b5c1b4..365e291 100644 --- a/replication/src/plan/transformations/golang/fetch_source.rs +++ b/replication/src/plan/transformations/golang/fetch_source.rs @@ -15,9 +15,9 @@ impl GolangFetchSource { } impl Transformation for GolangFetchSource { - fn apply(&self, ctx: Context) -> Result { + fn apply(&self, ctx: Context) -> Result> { ctx.sh.run_shell(format!("cp -r {}/* .", self.origin))?; - Ok(ctx) + Ok(None) } fn should_skip(&self, _: &Context) -> Option { diff --git a/replication/src/plan/transformations/initialize_project.rs b/replication/src/plan/transformations/initialize_project.rs index 18c3675..debb37a 100644 --- a/replication/src/plan/transformations/initialize_project.rs +++ b/replication/src/plan/transformations/initialize_project.rs @@ -27,16 +27,16 @@ impl InitializeProject { } impl Transformation for InitializeProject { - fn apply(&self, ctx: Context) -> Result { + fn apply(&self, ctx: Context) -> Result> { if self.git_init.reference_exists(&ctx) { self.git_init.apply(ctx.clone())?; + Ok(Some("fetched back from mirror".to_string())) } else { self.fetch_source.apply(ctx.clone())?; self.git_init.apply(ctx.clone())?; + Ok(Some("fetched from Go proxy".to_string())) } - - Ok(ctx) } fn should_skip(&self, _: &Context) -> Option { diff --git a/replication/src/plan/transformations/sourcecraft/initialize.rs b/replication/src/plan/transformations/sourcecraft/initialize.rs index 65e727c..09f5b14 100644 --- a/replication/src/plan/transformations/sourcecraft/initialize.rs +++ b/replication/src/plan/transformations/sourcecraft/initialize.rs @@ -53,13 +53,13 @@ impl SourcecraftInitialize { } impl Transformation for SourcecraftInitialize { - fn apply(&self, ctx: Context) -> Result { + fn apply(&self, ctx: Context) -> Result> { let sourcecraft_metadata: SourcecraftMetadata = SourcecraftMetadata::from_args(&self); write_yaml_file( &sourcecraft_metadata, format!("{}/sourcecraft.yaml", ctx.sh.project_root.to_str().unwrap()).as_str(), )?; - Ok(ctx) + Ok(None) } fn should_skip(&self, ctx: &Context) -> Option {