1use 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#[derive(ClapArgs, Debug)]
12pub struct Args {
13 #[arg(long, short)]
15 pub status: Option<String>,
16
17 #[arg(long, short = 'g')]
19 pub tag: Option<String>,
20
21 #[arg(long)]
23 pub since: Option<String>,
24
25 #[arg(long)]
27 pub until: Option<String>,
28
29 #[arg(long, short, default_value = "table")]
31 pub format: String,
32
33 #[arg(long, short)]
35 pub reverse: bool,
36}
37
38pub fn run(args: Args) -> Result<()> {
44 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 let mut adrs = notes.list()?;
53
54 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 if args.reverse {
88 adrs.reverse();
89 }
90
91 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 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
111fn parse_date(s: &str) -> Result<DateTime<Utc>> {
113 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
115 return Ok(dt.with_timezone(&Utc));
116 }
117
118 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
129fn print_table(adrs: &[Adr]) {
131 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 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 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
182fn 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
202fn 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 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
227fn 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}