Skip to main content

git_atomic/cli/
output.rs

1use crate::config::ResolvedConfig;
2use crate::config::layered::ConfigWarning;
3use crate::core::effect::Effect;
4use crate::git::atomize::AtomicResult;
5use crate::git::branch::BranchState;
6use owo_colors::OwoColorize;
7use std::io::{self, Write};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum OutputMode {
11    Human,
12    Json,
13    Quiet,
14}
15
16pub struct Printer {
17    pub mode: OutputMode,
18    pub verbosity: u8,
19}
20
21impl Printer {
22    pub fn new(json: bool, quiet: bool, verbosity: u8) -> Self {
23        let mode = if json {
24            OutputMode::Json
25        } else if quiet {
26            OutputMode::Quiet
27        } else {
28            OutputMode::Human
29        };
30        Self { mode, verbosity }
31    }
32
33    pub fn print_commit_results(&self, results: &[AtomicResult], dry_run: bool) {
34        match self.mode {
35            OutputMode::Quiet => {}
36            OutputMode::Json => {
37                let output = serde_json::json!({
38                    "dry_run": dry_run,
39                    "results": results.iter().map(|r| {
40                        serde_json::json!({
41                            "component": r.component,
42                            "branch": r.branch,
43                            "commit": r.commit_id.to_string(),
44                            "files": r.files.iter().map(|p| p.display().to_string()).collect::<Vec<_>>(),
45                            "created": r.created,
46                        })
47                    }).collect::<Vec<_>>(),
48                });
49                println!("{}", serde_json::to_string_pretty(&output).unwrap());
50            }
51            OutputMode::Human => {
52                let mut out = io::stdout().lock();
53                for r in results {
54                    let prefix = if dry_run { "would " } else { "" };
55                    let action = if r.created { "create" } else { "update" };
56                    let short_id = r.commit_id.to_string();
57                    let short_id = short_id.get(..8).unwrap_or(&short_id);
58
59                    let _ = writeln!(
60                        out,
61                        "{} [{}] {} → {} ({}{}, {} file{})",
62                        "✓".green(),
63                        r.component.cyan(),
64                        short_id.dimmed(),
65                        r.branch.bold(),
66                        prefix,
67                        action,
68                        r.files.len(),
69                        if r.files.len() == 1 { "" } else { "s" }
70                    );
71
72                    if self.verbosity > 0 {
73                        for f in &r.files {
74                            let _ = writeln!(out, "    {}", f.display().dimmed());
75                        }
76                    }
77                }
78            }
79        }
80    }
81
82    pub fn print_status(
83        &self,
84        components: &[(String, Vec<std::path::PathBuf>, BranchState, String)],
85    ) {
86        match self.mode {
87            OutputMode::Quiet => {}
88            OutputMode::Json => {
89                let output: Vec<_> = components
90                    .iter()
91                    .map(|(name, files, state, branch)| {
92                        serde_json::json!({
93                            "component": name,
94                            "branch": branch,
95                            "state": format!("{state:?}"),
96                            "file_count": files.len(),
97                        })
98                    })
99                    .collect();
100                println!("{}", serde_json::to_string_pretty(&output).unwrap());
101            }
102            OutputMode::Human => {
103                let mut out = io::stdout().lock();
104                for (name, files, state, branch) in components {
105                    let state_str = match state {
106                        BranchState::Missing => "missing".yellow().to_string(),
107                        BranchState::Current => "current".green().to_string(),
108                        BranchState::FastForward { .. } => "ahead".cyan().to_string(),
109                        BranchState::Diverged { .. } => "diverged".red().to_string(),
110                    };
111                    let _ = writeln!(
112                        out,
113                        "  {} {} ({}, {} file{})",
114                        name.bold(),
115                        branch.dimmed(),
116                        state_str,
117                        files.len(),
118                        if files.len() == 1 { "" } else { "s" }
119                    );
120                }
121            }
122        }
123    }
124
125    pub fn print_validate_ok(&self) {
126        match self.mode {
127            OutputMode::Quiet => {}
128            OutputMode::Json => println!(r#"{{"valid": true}}"#),
129            OutputMode::Human => println!("{} configuration is valid", "✓".green()),
130        }
131    }
132
133    pub fn print_init(&self, path: &std::path::Path) {
134        match self.mode {
135            OutputMode::Quiet => {}
136            OutputMode::Json => {
137                println!(
138                    "{}",
139                    serde_json::json!({"created": path.display().to_string()})
140                );
141            }
142            OutputMode::Human => {
143                println!("{} created {}", "✓".green(), path.display());
144            }
145        }
146    }
147
148    pub fn print_error(&self, err: &crate::core::Error) {
149        match self.mode {
150            OutputMode::Quiet => {}
151            OutputMode::Json => {
152                println!("{}", serde_json::json!({"error": err.to_string()}));
153            }
154            OutputMode::Human => {
155                eprintln!("{} {}", "✗".red(), err);
156            }
157        }
158    }
159
160    pub fn print_validate_error(&self, err: &crate::core::Error) {
161        match self.mode {
162            OutputMode::Quiet => {}
163            OutputMode::Json => {
164                println!(
165                    "{}",
166                    serde_json::json!({"valid": false, "error": err.to_string()})
167                );
168            }
169            OutputMode::Human => {
170                eprintln!("{} {}", "✗".red(), err);
171            }
172        }
173    }
174
175    pub fn print_config_provenance(&self, resolved: &ResolvedConfig) {
176        match self.mode {
177            OutputMode::Quiet => {}
178            OutputMode::Json => {
179                let config = serde_json::json!({
180                    "config": {
181                        "base_branch": {
182                            "value": resolved.base_branch.value,
183                            "source": resolved.base_branch.source.label(),
184                        },
185                        "branch_template": {
186                            "value": resolved.branch_template.value,
187                            "source": resolved.branch_template.source.label(),
188                        },
189                        "unmatched_files": {
190                            "value": resolved.unmatched_files.value.to_string(),
191                            "source": resolved.unmatched_files.source.label(),
192                        },
193                        "default_commit_type": {
194                            "value": resolved.default_commit_type.value,
195                            "source": resolved.default_commit_type.source.label(),
196                        },
197                    }
198                });
199                println!("{}", serde_json::to_string_pretty(&config).unwrap());
200            }
201            OutputMode::Human => {
202                let mut out = io::stdout().lock();
203                let _ = writeln!(out, "{}", "Settings:".bold());
204                let _ = writeln!(
205                    out,
206                    "  {:<20} = {:<30} ({})",
207                    "base_branch",
208                    resolved.base_branch.value,
209                    resolved.base_branch.source.label().dimmed()
210                );
211                let _ = writeln!(
212                    out,
213                    "  {:<20} = {:<30} ({})",
214                    "branch_template",
215                    resolved.branch_template.value,
216                    resolved.branch_template.source.label().dimmed()
217                );
218                let _ = writeln!(
219                    out,
220                    "  {:<20} = {:<30} ({})",
221                    "unmatched_files",
222                    resolved.unmatched_files.value.to_string(),
223                    resolved.unmatched_files.source.label().dimmed()
224                );
225                if let Some(ref ct) = resolved.default_commit_type.value {
226                    let _ = writeln!(
227                        out,
228                        "  {:<20} = {:<30} ({})",
229                        "default_commit_type",
230                        ct,
231                        resolved.default_commit_type.source.label().dimmed()
232                    );
233                }
234                let _ = writeln!(out);
235            }
236        }
237    }
238
239    pub fn print_config_warning(&self, warning: &ConfigWarning) {
240        match self.mode {
241            OutputMode::Quiet => {}
242            OutputMode::Json => {
243                println!("{}", serde_json::json!({"warning": warning.message}));
244            }
245            OutputMode::Human => {
246                eprintln!("{} {}", "⚠".yellow(), warning.message);
247            }
248        }
249    }
250
251    pub fn print_effect_preview(&self, effect: &Effect) {
252        match self.mode {
253            OutputMode::Quiet => {}
254            OutputMode::Json => {
255                let desc = match effect {
256                    Effect::RefTransaction { edits, .. } => {
257                        serde_json::json!({
258                            "effect": "ref_transaction",
259                            "dry_run": true,
260                            "refs": edits.iter().map(|e| {
261                                serde_json::json!({
262                                    "ref": e.ref_name,
263                                    "component": e.component,
264                                    "action": if e.created { "create" } else { "update" },
265                                })
266                            }).collect::<Vec<_>>(),
267                        })
268                    }
269                    Effect::Push { remote, branches } => {
270                        serde_json::json!({
271                            "effect": "push",
272                            "dry_run": true,
273                            "remote": remote,
274                            "branches": branches,
275                        })
276                    }
277                    Effect::WriteFile {
278                        path,
279                        content,
280                        structured,
281                    } => {
282                        let content_value = structured
283                            .clone()
284                            .unwrap_or_else(|| serde_json::Value::String(content.clone()));
285                        serde_json::json!({
286                            "effect": "write_file",
287                            "dry_run": true,
288                            "path": path.display().to_string(),
289                            "content": content_value,
290                        })
291                    }
292                };
293                println!("{}", serde_json::to_string_pretty(&desc).unwrap());
294            }
295            OutputMode::Human => {
296                let mut out = io::stdout().lock();
297                match effect {
298                    Effect::RefTransaction { edits, .. } => {
299                        for e in edits {
300                            let action = if e.created { "create" } else { "update" };
301                            let branch = e
302                                .ref_name
303                                .strip_prefix("refs/heads/")
304                                .unwrap_or(&e.ref_name);
305                            let _ = writeln!(
306                                out,
307                                "  {} would {} branch {}",
308                                "▸".dimmed(),
309                                action,
310                                branch.bold()
311                            );
312                        }
313                    }
314                    Effect::Push { remote, branches } => {
315                        let _ = writeln!(
316                            out,
317                            "  {} would push {} branch{} to {}",
318                            "▸".dimmed(),
319                            branches.len(),
320                            if branches.len() == 1 { "" } else { "es" },
321                            remote.bold()
322                        );
323                    }
324                    Effect::WriteFile { path, content, .. } => {
325                        let _ = writeln!(
326                            out,
327                            "  {} would create {}",
328                            "▸".dimmed(),
329                            path.display().bold()
330                        );
331                        if self.verbosity > 0 || content.len() < 4096 {
332                            let _ = writeln!(out);
333                            for line in content.lines() {
334                                let _ = writeln!(out, "    {}", line.dimmed());
335                            }
336                        }
337                    }
338                }
339            }
340        }
341    }
342}