feat: add template bootstrapping for project create
Some checks failed
continuous-integration/drone/push Build encountered an error
Some checks failed
continuous-integration/drone/push Build encountered an error
Adds --template/-t flag to `gitnow project create` that copies files from a template directory into new projects. Templates are discovered from ~/.gitnow/templates/ (configurable via settings.project.templates_directory). Includes a default template with a SPEC.md scaffold.
This commit is contained in:
@@ -42,6 +42,10 @@ pub struct ProjectCreateCommand {
|
|||||||
#[arg()]
|
#[arg()]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
|
||||||
|
/// Bootstrap from a template in the templates directory
|
||||||
|
#[arg(long = "template", short = 't')]
|
||||||
|
template: Option<String>,
|
||||||
|
|
||||||
/// Skip cache when fetching repositories
|
/// Skip cache when fetching repositories
|
||||||
#[arg(long = "no-cache", default_value = "false")]
|
#[arg(long = "no-cache", default_value = "false")]
|
||||||
no_cache: bool,
|
no_cache: bool,
|
||||||
@@ -73,6 +77,68 @@ pub struct ProjectDeleteCommand {
|
|||||||
force: bool,
|
force: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_templates_dir(app: &'static App) -> PathBuf {
|
||||||
|
if let Some(ref project_settings) = app.config.settings.project {
|
||||||
|
if let Some(ref dir) = project_settings.templates_directory {
|
||||||
|
let path = PathBuf::from(dir);
|
||||||
|
if let Ok(stripped) = path.strip_prefix("~") {
|
||||||
|
let home = dirs::home_dir().unwrap_or_default();
|
||||||
|
return home.join(stripped);
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let home = dirs::home_dir().unwrap_or_default();
|
||||||
|
home.join(".gitnow").join("templates")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct TemplateEntry {
|
||||||
|
name: String,
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Searchable for TemplateEntry {
|
||||||
|
fn display_label(&self) -> String {
|
||||||
|
self.name.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_templates(templates_dir: &PathBuf) -> anyhow::Result<Vec<TemplateEntry>> {
|
||||||
|
if !templates_dir.exists() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut templates = Vec::new();
|
||||||
|
for entry in std::fs::read_dir(templates_dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
if entry.file_type()?.is_dir() {
|
||||||
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
templates.push(TemplateEntry {
|
||||||
|
name,
|
||||||
|
path: entry.path(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templates.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
Ok(templates)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_dir_recursive(src: &std::path::Path, dst: &std::path::Path) -> anyhow::Result<()> {
|
||||||
|
std::fs::create_dir_all(dst)?;
|
||||||
|
for entry in std::fs::read_dir(src)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let dest_path = dst.join(entry.file_name());
|
||||||
|
if entry.file_type()?.is_dir() {
|
||||||
|
copy_dir_recursive(&entry.path(), &dest_path)?;
|
||||||
|
} else {
|
||||||
|
std::fs::copy(entry.path(), &dest_path)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn get_projects_dir(app: &'static App) -> PathBuf {
|
fn get_projects_dir(app: &'static App) -> PathBuf {
|
||||||
if let Some(ref project_settings) = app.config.settings.project {
|
if let Some(ref project_settings) = app.config.settings.project {
|
||||||
if let Some(ref dir) = project_settings.directory {
|
if let Some(ref dir) = project_settings.directory {
|
||||||
@@ -311,6 +377,40 @@ impl ProjectCreateCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 6: Apply template if requested
|
||||||
|
let templates_dir = get_templates_dir(app);
|
||||||
|
let template = match self.template.take() {
|
||||||
|
Some(name) => {
|
||||||
|
let templates = list_templates(&templates_dir)?;
|
||||||
|
Some(
|
||||||
|
templates
|
||||||
|
.into_iter()
|
||||||
|
.find(|t| t.name == name)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"template '{}' not found in {}",
|
||||||
|
name,
|
||||||
|
templates_dir.display()
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let templates = list_templates(&templates_dir)?;
|
||||||
|
if !templates.is_empty() {
|
||||||
|
eprintln!("Select a project template (Esc to skip):");
|
||||||
|
app.interactive().interactive_search_items(&templates)?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(template) = template {
|
||||||
|
eprintln!(" applying template '{}'...", template.name);
|
||||||
|
copy_dir_recursive(&template.path, &project_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"project '{}' created at {} with {} repositories",
|
"project '{}' created at {} with {} repositories",
|
||||||
dir_name,
|
dir_name,
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ pub struct ProjectSettings {
|
|||||||
/// Directory where projects are stored.
|
/// Directory where projects are stored.
|
||||||
/// Default: "~/.gitnow/projects"
|
/// Default: "~/.gitnow/projects"
|
||||||
pub directory: Option<String>,
|
pub directory: Option<String>,
|
||||||
|
|
||||||
|
/// Directory containing project templates.
|
||||||
|
/// Each subdirectory is a template whose files are copied into new projects.
|
||||||
|
/// Default: "~/.gitnow/templates"
|
||||||
|
pub templates_directory: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||||
|
|||||||
17
templates/default/SPEC.md
Normal file
17
templates/default/SPEC.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Project Spec
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
<!-- Describe the purpose and goals of this project -->
|
||||||
|
|
||||||
|
## Repositories
|
||||||
|
|
||||||
|
<!-- List the repositories included and their roles -->
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
<!-- Describe how the repositories relate to each other -->
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
<!-- Steps to get up and running after cloning -->
|
||||||
Reference in New Issue
Block a user