git_adr/cli/
new.rs

1//! Create a new ADR.
2
3use anyhow::Result;
4use chrono::Utc;
5use clap::Args as ClapArgs;
6use colored::Colorize;
7
8use crate::core::{Adr, AdrStatus, ConfigManager, FlexibleDate, Git, NotesManager, TemplateEngine};
9
10/// Arguments for the new command.
11#[derive(ClapArgs, Debug)]
12pub struct Args {
13    /// ADR title.
14    pub title: String,
15
16    /// Initial status.
17    #[arg(long, short, default_value = "proposed")]
18    pub status: String,
19
20    /// Tags (can be specified multiple times).
21    #[arg(long, short = 'g')]
22    pub tag: Vec<String>,
23
24    /// Deciders (can be specified multiple times).
25    #[arg(long, short)]
26    pub deciders: Vec<String>,
27
28    /// Link to commit SHA.
29    #[arg(long, short)]
30    pub link: Option<String>,
31
32    /// Template format to use.
33    #[arg(long)]
34    pub template: Option<String>,
35
36    /// Read content from file.
37    #[arg(long, short)]
38    pub file: Option<String>,
39
40    /// Don't open editor.
41    #[arg(long)]
42    pub no_edit: bool,
43
44    /// Preview without saving.
45    #[arg(long)]
46    pub preview: bool,
47}
48
49/// Run the new command.
50///
51/// # Errors
52///
53/// Returns an error if ADR creation fails.
54pub fn run(args: Args) -> Result<()> {
55    let git = Git::new();
56    git.check_repository()?;
57
58    let config = ConfigManager::new(git.clone()).load()?;
59
60    if !config.initialized {
61        anyhow::bail!("git-adr not initialized. Run 'git adr init' first.");
62    }
63
64    let notes = NotesManager::new(git.clone(), config.clone());
65
66    // Generate ADR ID
67    let next_num = notes.next_number()?;
68    let adr_id = notes.format_id(next_num);
69
70    eprintln!("{} Creating new ADR: {}", "→".blue(), adr_id);
71
72    // Parse status
73    let status: AdrStatus = args.status.parse().map_err(|e| anyhow::anyhow!("{}", e))?;
74
75    // Determine template format
76    let format = args.template.as_deref().unwrap_or(&config.format);
77
78    // Get commit to attach to
79    let commit = match &args.link {
80        Some(sha) => sha.clone(),
81        None => git.head()?,
82    };
83
84    // Create ADR struct
85    let mut adr = Adr::new(adr_id.clone(), args.title.clone());
86    adr.commit = commit;
87    adr.frontmatter.status = status;
88    adr.frontmatter.tags.clone_from(&args.tag);
89    adr.frontmatter.deciders.clone_from(&args.deciders);
90    adr.frontmatter.date = Some(FlexibleDate(Utc::now()));
91    adr.frontmatter.format = Some(format.to_string());
92
93    // Render template for body
94    let template_engine = TemplateEngine::new();
95    let mut context = std::collections::HashMap::new();
96    context.insert("title".to_string(), adr.frontmatter.title.clone());
97    context.insert("status".to_string(), adr.frontmatter.status.to_string());
98    let body = template_engine.render(format, &context)?;
99    adr.body = body;
100
101    // Read content from file if provided
102    if let Some(file_path) = &args.file {
103        let file_content = std::fs::read_to_string(file_path)?;
104        // If file has frontmatter, parse it; otherwise use as body
105        if file_content.trim().starts_with("---") {
106            let parsed = Adr::from_markdown(adr_id.clone(), adr.commit.clone(), &file_content)?;
107            adr.frontmatter = parsed.frontmatter;
108            adr.body = parsed.body;
109        } else {
110            adr.body = file_content;
111        }
112    }
113
114    // Preview mode
115    if args.preview {
116        eprintln!("{} Preview mode - not saving", "!".yellow());
117        println!("{}", adr.to_markdown()?);
118        return Ok(());
119    }
120
121    // Save ADR
122    notes.create(&adr)?;
123
124    eprintln!("{} Created ADR: {}", "✓".green(), adr_id);
125    eprintln!("  Title: {}", args.title);
126    eprintln!("  Status: {}", adr.frontmatter.status);
127    if !args.tag.is_empty() {
128        eprintln!("  Tags: {}", args.tag.join(", "));
129    }
130
131    Ok(())
132}