git_adr/cli/
templates.rs

1//! Generate project templates with ADR integration.
2
3use anyhow::Result;
4use clap::{Args as ClapArgs, Subcommand};
5use colored::Colorize;
6use std::fs;
7use std::path::Path;
8
9use crate::core::Git;
10
11/// Arguments for the templates command.
12#[derive(ClapArgs, Debug)]
13pub struct Args {
14    /// Templates subcommand.
15    #[command(subcommand)]
16    pub command: TemplatesCommand,
17}
18
19/// Templates subcommands.
20#[derive(Subcommand, Debug)]
21pub enum TemplatesCommand {
22    /// Generate a pull request template with ADR section.
23    Pr(PrArgs),
24
25    /// Generate GitHub issue templates for ADR workflows.
26    Issue(IssueArgs),
27
28    /// Generate or update CODEOWNERS file.
29    Codeowners(CodeownersArgs),
30
31    /// Generate all templates at once.
32    All(AllArgs),
33}
34
35/// Arguments for PR template generation.
36#[derive(ClapArgs, Debug)]
37pub struct PrArgs {
38    /// Output file path.
39    #[arg(long, short, default_value = ".github/PULL_REQUEST_TEMPLATE.md")]
40    pub output: String,
41
42    /// Force overwrite existing file.
43    #[arg(long, short)]
44    pub force: bool,
45}
46
47/// Arguments for issue template generation.
48#[derive(ClapArgs, Debug)]
49pub struct IssueArgs {
50    /// Output directory.
51    #[arg(long, short, default_value = ".github/ISSUE_TEMPLATE")]
52    pub output: String,
53
54    /// Force overwrite existing files.
55    #[arg(long, short)]
56    pub force: bool,
57}
58
59/// Arguments for CODEOWNERS generation.
60#[derive(ClapArgs, Debug)]
61pub struct CodeownersArgs {
62    /// Output file path.
63    #[arg(long, short, default_value = ".github/CODEOWNERS")]
64    pub output: String,
65
66    /// ADR owner (GitHub username or team).
67    #[arg(long, default_value = "@architecture-team")]
68    pub owner: String,
69
70    /// Force overwrite existing file.
71    #[arg(long, short)]
72    pub force: bool,
73}
74
75/// Arguments for generating all templates.
76#[derive(ClapArgs, Debug)]
77pub struct AllArgs {
78    /// Force overwrite existing files.
79    #[arg(long, short)]
80    pub force: bool,
81}
82
83/// Run the templates command.
84///
85/// # Errors
86///
87/// Returns an error if template generation fails.
88pub fn run(args: Args) -> Result<()> {
89    let git = Git::new();
90    git.check_repository()?;
91
92    match args.command {
93        TemplatesCommand::Pr(pr_args) => run_pr(pr_args),
94        TemplatesCommand::Issue(issue_args) => run_issue(issue_args),
95        TemplatesCommand::Codeowners(codeowners_args) => run_codeowners(codeowners_args),
96        TemplatesCommand::All(all_args) => run_all(all_args),
97    }
98}
99
100/// Generate PR template.
101fn run_pr(args: PrArgs) -> Result<()> {
102    let output_path = Path::new(&args.output);
103
104    // Create parent directories if needed
105    if let Some(parent) = output_path.parent() {
106        if !parent.exists() {
107            fs::create_dir_all(parent)?;
108        }
109    }
110
111    if output_path.exists() && !args.force {
112        anyhow::bail!(
113            "PR template already exists: {}. Use --force to overwrite.",
114            output_path.display()
115        );
116    }
117
118    fs::write(output_path, PR_TEMPLATE)?;
119
120    eprintln!(
121        "{} Generated PR template: {}",
122        "✓".green(),
123        output_path.display().to_string().cyan()
124    );
125
126    Ok(())
127}
128
129/// Generate issue templates.
130fn run_issue(args: IssueArgs) -> Result<()> {
131    let output_dir = Path::new(&args.output);
132
133    if !output_dir.exists() {
134        fs::create_dir_all(output_dir)?;
135    }
136
137    let templates = [
138        ("adr-proposal.md", ADR_PROPOSAL_TEMPLATE),
139        ("adr-amendment.md", ADR_AMENDMENT_TEMPLATE),
140        ("config.yml", ISSUE_CONFIG),
141    ];
142
143    eprintln!("{} Generating issue templates...", "→".blue());
144
145    for (filename, content) in &templates {
146        let file_path = output_dir.join(filename);
147
148        if file_path.exists() && !args.force {
149            eprintln!(
150                "  {} {} already exists (use --force to overwrite)",
151                "!".yellow(),
152                filename
153            );
154            continue;
155        }
156
157        fs::write(&file_path, *content)?;
158        eprintln!("  {} Generated {}", "✓".green(), filename.cyan());
159    }
160
161    Ok(())
162}
163
164/// Generate CODEOWNERS file.
165fn run_codeowners(args: CodeownersArgs) -> Result<()> {
166    let output_path = Path::new(&args.output);
167
168    // Create parent directories if needed
169    if let Some(parent) = output_path.parent() {
170        if !parent.exists() {
171            fs::create_dir_all(parent)?;
172        }
173    }
174
175    if output_path.exists() && !args.force {
176        anyhow::bail!(
177            "CODEOWNERS already exists: {}. Use --force to overwrite.",
178            output_path.display()
179        );
180    }
181
182    let content = CODEOWNERS_TEMPLATE.replace("{owner}", &args.owner);
183    fs::write(output_path, content)?;
184
185    eprintln!(
186        "{} Generated CODEOWNERS: {}",
187        "✓".green(),
188        output_path.display().to_string().cyan()
189    );
190    eprintln!("  ADR owner set to: {}", args.owner.cyan());
191
192    Ok(())
193}
194
195/// Generate all templates.
196fn run_all(args: AllArgs) -> Result<()> {
197    eprintln!("{} Generating all ADR-related templates...", "→".blue());
198    eprintln!();
199
200    // Generate PR template
201    run_pr(PrArgs {
202        output: ".github/PULL_REQUEST_TEMPLATE.md".to_string(),
203        force: args.force,
204    })?;
205
206    // Generate issue templates
207    run_issue(IssueArgs {
208        output: ".github/ISSUE_TEMPLATE".to_string(),
209        force: args.force,
210    })?;
211
212    // Generate CODEOWNERS
213    run_codeowners(CodeownersArgs {
214        output: ".github/CODEOWNERS".to_string(),
215        owner: "@architecture-team".to_string(),
216        force: args.force,
217    })?;
218
219    eprintln!();
220    eprintln!("{} All templates generated!", "✓".green());
221
222    Ok(())
223}
224
225/// PR template content.
226const PR_TEMPLATE: &str = r#"## Description
227
228<!-- Brief description of the changes -->
229
230## Related ADRs
231
232<!-- Reference any related Architecture Decision Records -->
233<!-- Example: Implements ADR-0005: Use PostgreSQL for primary database -->
234
235- [ ] This PR implements/relates to an ADR
236- ADR Reference: <!-- ADR-XXXX -->
237
238## Type of Change
239
240- [ ] Bug fix (non-breaking change that fixes an issue)
241- [ ] New feature (non-breaking change that adds functionality)
242- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
243- [ ] Documentation update
244- [ ] Architecture change (requires ADR)
245
246## Checklist
247
248- [ ] My code follows the project's style guidelines
249- [ ] I have performed a self-review of my code
250- [ ] I have commented my code, particularly in hard-to-understand areas
251- [ ] I have made corresponding changes to the documentation
252- [ ] My changes generate no new warnings
253- [ ] I have added tests that prove my fix is effective or that my feature works
254- [ ] New and existing unit tests pass locally with my changes
255
256## Architecture Decision
257
258<!-- If this PR involves a significant architectural decision, please ensure an ADR exists -->
259
260- [ ] No architectural decision needed
261- [ ] ADR already exists: <!-- ADR-XXXX -->
262- [ ] ADR needs to be created (link to issue: <!-- #XXX -->)
263"#;
264
265/// ADR proposal issue template.
266const ADR_PROPOSAL_TEMPLATE: &str = r#"---
267name: ADR Proposal
268about: Propose a new Architecture Decision Record
269title: "[ADR] "
270labels: adr, proposal
271assignees: ''
272---
273
274## Context
275
276<!-- What is the issue that we're seeing that is motivating this decision or change? -->
277
278## Decision Drivers
279
280<!-- What factors are influencing this decision? -->
281
282-
283-
284-
285
286## Considered Options
287
288<!-- What options have you considered? -->
289
2901.
2912.
2923.
293
294## Proposed Decision
295
296<!-- What is your proposed decision? -->
297
298## Consequences
299
300### Positive
301
302-
303
304### Negative
305
306-
307
308### Neutral
309
310-
311
312## Additional Context
313
314<!-- Add any other context, diagrams, or screenshots about the decision here -->
315"#;
316
317/// ADR amendment issue template.
318const ADR_AMENDMENT_TEMPLATE: &str = r#"---
319name: ADR Amendment
320about: Request changes to an existing ADR
321title: "[ADR Amendment] "
322labels: adr, amendment
323assignees: ''
324---
325
326## ADR Reference
327
328<!-- Which ADR are you proposing to amend? -->
329ADR ID:
330
331## Reason for Amendment
332
333<!-- Why does this ADR need to be updated? -->
334
335- [ ] Circumstances have changed
336- [ ] New information available
337- [ ] Implementation revealed issues
338- [ ] Other:
339
340## Proposed Changes
341
342<!-- What changes are you proposing? -->
343
344## Impact Assessment
345
346<!-- What is the impact of this change? -->
347
348### Affected Components
349
350-
351
352### Migration Required
353
354- [ ] Yes
355- [ ] No
356
357### Timeline
358
359<!-- When should this change take effect? -->
360"#;
361
362/// Issue template config.
363const ISSUE_CONFIG: &str = r#"blank_issues_enabled: true
364contact_links:
365  - name: ADR Documentation
366    url: https://adr.github.io/
367    about: Learn more about Architecture Decision Records
368"#;
369
370/// CODEOWNERS template.
371const CODEOWNERS_TEMPLATE: &str = r#"# CODEOWNERS for Architecture Decision Records
372# Generated by git-adr
373
374# ADR-related files require architecture team review
375# Note: git-adr stores ADRs in git notes, not files
376# This covers any exported ADRs or ADR-related documentation
377
378# ADR documentation
379/docs/adr/ {owner}
380/docs/architecture/ {owner}
381
382# Exported ADRs
383/adrs/ {owner}
384
385# GitHub ADR templates
386/.github/ISSUE_TEMPLATE/adr-*.md {owner}
387
388# Architecture-related configs
389/ARCHITECTURE.md {owner}
390"#;