git_adr/cli/
list.rs

1//! List all ADRs.
2
3use anyhow::Result;
4use chrono::{DateTime, NaiveDate, Utc};
5use clap::Args as ClapArgs;
6use colored::Colorize;
7
8use crate::core::{Adr, AdrStatus, ConfigManager, Git, NotesManager};
9
10/// Arguments for the list command.
11#[derive(ClapArgs, Debug)]
12pub struct Args {
13    /// Filter by status.
14    #[arg(long, short)]
15    pub status: Option<String>,
16
17    /// Filter by tag.
18    #[arg(long, short = 'g')]
19    pub tag: Option<String>,
20
21    /// Filter by date (since).
22    #[arg(long)]
23    pub since: Option<String>,
24
25    /// Filter by date (until).
26    #[arg(long)]
27    pub until: Option<String>,
28
29    /// Output format (table, json, csv, oneline).
30    #[arg(long, short, default_value = "table")]
31    pub format: String,
32
33    /// Reverse sort order.
34    #[arg(long, short)]
35    pub reverse: bool,
36}
37
38/// Run the list command.
39///
40/// # Errors
41///
42/// Returns an error if listing fails.
43pub fn run(args: Args) -> Result<()> {
44    // Initialize git and load config
45    let git = Git::new();
46    git.check_repository()?;
47
48    let config = ConfigManager::new(git.clone()).load()?;
49    let notes = NotesManager::new(git, config);
50
51    // Get all ADRs
52    let mut adrs = notes.list()?;
53
54    // Apply filters
55    if let Some(status_filter) = &args.status {
56        let target_status: AdrStatus = status_filter
57            .parse()
58            .map_err(|e| anyhow::anyhow!("{}", e))?;
59        adrs.retain(|adr| *adr.status() == target_status);
60    }
61
62    if let Some(tag_filter) = &args.tag {
63        adrs.retain(|adr| adr.has_tag(tag_filter));
64    }
65
66    if let Some(since) = &args.since {
67        let since_date = parse_date(since)?;
68        adrs.retain(|adr| {
69            adr.frontmatter
70                .date
71                .as_ref()
72                .is_none_or(|d| d.datetime() >= since_date)
73        });
74    }
75
76    if let Some(until) = &args.until {
77        let until_date = parse_date(until)?;
78        adrs.retain(|adr| {
79            adr.frontmatter
80                .date
81                .as_ref()
82                .is_none_or(|d| d.datetime() <= until_date)
83        });
84    }
85
86    // Apply sort order
87    if args.reverse {
88        adrs.reverse();
89    }
90
91    // Check if empty
92    if adrs.is_empty() {
93        eprintln!(
94            "{} No ADRs found. Create one with: git adr new \"Title\"",
95            "→".yellow()
96        );
97        return Ok(());
98    }
99
100    // Format output
101    match args.format.as_str() {
102        "json" => print_json(&adrs)?,
103        "csv" => print_csv(&adrs),
104        "oneline" => print_oneline(&adrs),
105        _ => print_table(&adrs),
106    }
107
108    Ok(())
109}
110
111/// Parse a date string into a DateTime.
112fn parse_date(s: &str) -> Result<DateTime<Utc>> {
113    // Try ISO 8601 format first
114    if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
115        return Ok(dt.with_timezone(&Utc));
116    }
117
118    // Try YYYY-MM-DD format
119    if let Ok(date) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
120        return Ok(date.and_hms_opt(0, 0, 0).unwrap().and_utc());
121    }
122
123    Err(anyhow::anyhow!(
124        "Invalid date format: {}. Use YYYY-MM-DD or RFC3339.",
125        s
126    ))
127}
128
129/// Print ADRs as a table.
130fn print_table(adrs: &[Adr]) {
131    // Calculate column widths
132    let id_width = adrs.iter().map(|a| a.id.len()).max().unwrap_or(10).max(4);
133    let status_width = adrs
134        .iter()
135        .map(|a| a.status().to_string().len())
136        .max()
137        .unwrap_or(10)
138        .max(6);
139    let title_width = 50;
140
141    // Print header
142    println!(
143        "{:id_width$}  {:status_width$}  {}",
144        "ID".bold(),
145        "STATUS".bold(),
146        "TITLE".bold()
147    );
148    println!(
149        "{:-<id_width$}  {:-<status_width$}  {:-<title_width$}",
150        "", "", ""
151    );
152
153    // Print rows
154    for adr in adrs {
155        let status_str = adr.status().to_string();
156        let status_colored = match adr.status() {
157            AdrStatus::Proposed => status_str.yellow(),
158            AdrStatus::Accepted => status_str.green(),
159            AdrStatus::Deprecated => status_str.dimmed(),
160            AdrStatus::Superseded => status_str.magenta(),
161            AdrStatus::Rejected => status_str.red(),
162        };
163
164        let title = if adr.title().len() > title_width {
165            format!("{}...", &adr.title()[..title_width - 3])
166        } else {
167            adr.title().to_string()
168        };
169
170        println!(
171            "{:id_width$}  {:status_width$}  {}",
172            adr.id.cyan(),
173            status_colored,
174            title
175        );
176    }
177
178    println!();
179    println!("{} ADR(s) found", adrs.len().to_string().bold());
180}
181
182/// Print ADRs as JSON.
183fn print_json(adrs: &[Adr]) -> Result<()> {
184    let output: Vec<serde_json::Value> = adrs
185        .iter()
186        .map(|adr| {
187            serde_json::json!({
188                "id": adr.id,
189                "title": adr.title(),
190                "status": adr.status().to_string(),
191                "date": adr.frontmatter.date.as_ref().map(|d| d.datetime().to_rfc3339()),
192                "tags": adr.frontmatter.tags,
193                "commit": adr.commit,
194            })
195        })
196        .collect();
197
198    println!("{}", serde_json::to_string_pretty(&output)?);
199    Ok(())
200}
201
202/// Print ADRs as CSV.
203fn print_csv(adrs: &[Adr]) {
204    println!("id,status,title,date,tags,commit");
205    for adr in adrs {
206        let date = adr
207            .frontmatter
208            .date
209            .as_ref()
210            .map(|d| d.datetime().format("%Y-%m-%d").to_string())
211            .unwrap_or_default();
212        let tags = adr.frontmatter.tags.join(";");
213        // Escape title for CSV (double quotes)
214        let title = adr.title().replace('"', "\"\"");
215        println!(
216            "\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\"",
217            adr.id,
218            adr.status(),
219            title,
220            date,
221            tags,
222            adr.commit
223        );
224    }
225}
226
227/// Print ADRs in one-line format.
228fn print_oneline(adrs: &[Adr]) {
229    for adr in adrs {
230        let status = match adr.status() {
231            AdrStatus::Proposed => "[P]".yellow(),
232            AdrStatus::Accepted => "[A]".green(),
233            AdrStatus::Deprecated => "[D]".dimmed(),
234            AdrStatus::Superseded => "[S]".magenta(),
235            AdrStatus::Rejected => "[R]".red(),
236        };
237        println!("{} {} {}", adr.id.cyan(), status, adr.title());
238    }
239}