From c9de04482df40d202ac2d28fb2b6298d7c82b3ba Mon Sep 17 00:00:00 2001 From: Perelyn <64838956+Perelyn-sama@users.noreply.github.com> Date: Fri, 8 Nov 2024 03:44:04 +0100 Subject: [PATCH 1/2] add deps --- Cargo.lock | 35 ++++++++++++++++++++++++++++------- Cargo.toml | 6 ++++-- cli/Cargo.toml | 4 +++- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1c0f6e8..31b8894 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1100,11 +1100,11 @@ checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" [[package]] name = "git2" -version = "0.16.1" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc" +checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "libc", "libgit2-sys", "log", @@ -1353,9 +1353,9 @@ checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "libgit2-sys" -version = "0.14.2+1.5.1" +version = "0.16.2+1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4" +checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" dependencies = [ "cc", "libc", @@ -1425,9 +1425,9 @@ dependencies = [ [[package]] name = "libssh2-sys" -version = "0.2.23" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b094a36eb4b8b8c8a7b4b8ae43b2944502be3e59cd87687595cf6b0a71b3f4ca" +checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" dependencies = [ "cc", "libc", @@ -2036,6 +2036,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2721,7 +2730,9 @@ dependencies = [ "solana-cli-config", "solana-sdk", "syn 2.0.77", + "tempfile", "tokio", + "walkdir", ] [[package]] @@ -3003,6 +3014,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index eef6780..0d49c82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ clap = { features = ["derive", "env"], version = "4.4" } clap_v3 = { version = "3", package = "clap" } anyhow = "1" colored = "2.0" -git2 = "0.16" +git2 = { version = "0.18", features = ["vendored-libgit2"] } indicatif = "0.17" num_enum = "0.7" prettyplease = "0.2" @@ -28,7 +28,9 @@ solana-cli-config = "^1.18" solana-program = "^1.18" solana-sdk = "^1.18" spl-token = { features = ["no-entrypoint"], version = "^4" } -spl-associated-token-account = { features = [ "no-entrypoint" ], version = "^2.3" } +spl-associated-token-account = { features = [ "no-entrypoint" ], version = "^2.3" } thiserror = "1.0.57" tokio = "1.35" quote = "1.0" +tempfile = "3.2" +walkdir = "2.4" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 76bbe83..ab0e7fe 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -23,7 +23,7 @@ anyhow.workspace = true clap.workspace = true clap_v3.workspace = true colored.workspace = true -indicatif.workspace = true +indicatif.workspace = true git2.workspace = true prettyplease.workspace = true syn.workspace = true @@ -32,3 +32,5 @@ quote.workspace = true solana-sdk.workspace = true solana-cli-config.workspace = true solana-clap-v3-utils.workspace = true +tempfile.workspace = true +walkdir.workspace = true From 03332882bbedb87925e461665257a6df60dc2a76 Mon Sep 17 00:00:00 2001 From: Perelyn <64838956+Perelyn-sama@users.noreply.github.com> Date: Fri, 8 Nov 2024 03:44:48 +0100 Subject: [PATCH 2/2] add template url to NewArgs --- cli/src/args.rs | 7 +++ cli/src/new_project.rs | 121 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 123 insertions(+), 5 deletions(-) diff --git a/cli/src/args.rs b/cli/src/args.rs index 1768dd4..46d4e20 100644 --- a/cli/src/args.rs +++ b/cli/src/args.rs @@ -4,6 +4,13 @@ use clap::{arg, Parser}; pub struct NewArgs { #[arg(value_name = "NAME", help = "The name of the program")] pub name: Option, + + #[arg( + long = "template", + value_name = "URL", + help = "Git repository URL containing program templates (optional)" + )] + pub template_url: Option, } #[derive(Parser, Debug)] diff --git a/cli/src/new_project.rs b/cli/src/new_project.rs index 4ec794b..131f3ee 100644 --- a/cli/src/new_project.rs +++ b/cli/src/new_project.rs @@ -1,14 +1,117 @@ use std::{fs, io, path::Path}; use colored::*; -use git2::Repository; +use git2::{FetchOptions, RemoteCallbacks, Repository}; + +use anyhow::{Context, Result}; +use walkdir::WalkDir; use crate::{ utils::{prompt, to_camel_case, to_lib_case, to_type_case}, NewArgs, }; -pub fn new_project(args: NewArgs) -> anyhow::Result<()> { +pub struct TemplateHandler; + +impl TemplateHandler { + pub fn new() -> Self { + Self + } + + pub fn clone_and_process( + &self, + url: &str, + target_dir: &Path, + project_name: &str, + ) -> Result<()> { + if target_dir.exists() { + return Err(anyhow::anyhow!( + "Directory '{}' already exists", + target_dir.display() + )); + } + + fs::create_dir_all(target_dir) + .with_context(|| format!("Failed to create directory: {}", target_dir.display()))?; + + let temp_dir = tempfile::TempDir::new().context("Failed to create temporary directory")?; + + let mut callbacks = RemoteCallbacks::new(); + let mut last_progress = 0; + callbacks.transfer_progress(|progress| { + let current = progress.received_objects(); + let total = progress.total_objects(); + if current != last_progress && current == total { + println!("Template downloaded successfully!"); + } + last_progress = current; + true + }); + + let mut fetch_options = FetchOptions::new(); + fetch_options.remote_callbacks(callbacks); + + let mut builder = git2::build::RepoBuilder::new(); + builder.fetch_options(fetch_options); + + println!("Downloading template..."); + builder + .clone(url, temp_dir.path()) + .with_context(|| format!("Failed to clone template repository from {}", url))?; + + self.copy_and_process_directory(temp_dir.path(), target_dir, project_name)?; + Repository::init(target_dir)?; + + Ok(()) + } + + fn copy_and_process_directory( + &self, + source: &Path, + target: &Path, + project_name: &str, + ) -> Result<()> { + for entry in WalkDir::new(source).min_depth(1) { + let entry = entry?; + let path = entry.path(); + + if path.components().any(|c| c.as_os_str() == ".git") { + continue; + } + + let relative_path = path.strip_prefix(source)?; + let target_path = target.join(relative_path); + + if entry.file_type().is_dir() { + fs::create_dir_all(&target_path).with_context(|| { + format!("Failed to create directory: {}", target_path.display()) + })?; + } else { + if let Some(parent) = target_path.parent() { + fs::create_dir_all(parent).with_context(|| { + format!("Failed to create parent directory: {}", parent.display()) + })?; + } + + let content = fs::read_to_string(path) + .with_context(|| format!("Failed to read file: {}", path.display()))?; + + let processed_content = content + .replace("{name_lowercase}", &project_name.to_ascii_lowercase()) + .replace("{name_uppercase}", &project_name.to_ascii_uppercase()) + .replace("{name_camelcase}", &to_camel_case(&project_name)) + .replace("{name_typecase}", &to_type_case(&project_name)) + .replace("{name_libcase}", &to_lib_case(&project_name)); + + fs::write(&target_path, processed_content) + .with_context(|| format!("Failed to write file: {}", target_path.display()))?; + } + } + Ok(()) + } +} + +pub fn new_project(args: NewArgs) -> Result<()> { // Get project name let project_name = if let Some(name) = args.name { name.to_ascii_lowercase() @@ -34,9 +137,17 @@ pub fn new_project(args: NewArgs) -> anyhow::Result<()> { // - Generate docs link let base_path = Path::new(&project_name); - stub_workspace(base_path, &project_name)?; - stub_api(base_path, &project_name)?; - stub_program(base_path, &project_name)?; + + if let Some(template_url) = args.template_url { + let handler = TemplateHandler::new(); + handler.clone_and_process(&template_url, base_path, &project_name)?; + println!("✨ Project '{}' created successfully!", project_name); + } else { + stub_workspace(base_path, &project_name)?; + stub_api(base_path, &project_name)?; + stub_program(base_path, &project_name)?; + } + Ok(()) }