1use anyhow::{Context, Result};
5use colored::*;
6use serde::{Deserialize, Serialize};
7use std::collections::{HashMap, HashSet};
8
9use crate::core::Parser;
10
11#[derive(Debug, Serialize, Deserialize)]
12pub struct DiffResult {
13 pub missing: Vec<String>,
14 pub extra: Vec<String>,
15 pub different: Vec<DiffItem>,
16}
17
18#[derive(Debug, Serialize, Deserialize)]
19pub struct DiffItem {
20 pub key: String,
21 pub example_value: String,
22 pub env_value: String,
23}
24
25pub fn run(
26 env: String,
27 example: String,
28 show_values: bool,
29 format: String,
30 reverse: bool,
31 verbose: bool,
32) -> Result<()> {
33 if verbose {
34 println!("{}", "Running diff in verbose mode".dimmed());
35 }
36
37 println!(
38 "\n{}",
39 "┌─ Comparing .env ↔ .env.example ─────────────────────┐".cyan()
40 );
41 println!(
42 "{}\n",
43 "└──────────────────────────────────────────────────────┘".cyan()
44 );
45
46 let parser = Parser::default();
47
48 let env_file = parser
49 .parse_file(&env)
50 .with_context(|| format!("Failed to parse {}", env))?;
51
52 let example_file = parser
53 .parse_file(&example)
54 .with_context(|| format!("Failed to parse {}", example))?;
55
56 let (left, right, left_name, right_name) = if reverse {
57 (&example_file.vars, &env_file.vars, &example, &env)
58 } else {
59 (&env_file.vars, &example_file.vars, &env, &example)
60 };
61
62 let diff = compute_diff(left, right);
63
64 match format.as_str() {
65 "json" => output_json(&diff)?,
66 "patch" => output_patch(&diff, left, right)?,
67 _ => output_pretty(&diff, left, right, left_name, right_name, show_values)?,
68 }
69
70 Ok(())
71}
72
73fn compute_diff(left: &HashMap<String, String>, right: &HashMap<String, String>) -> DiffResult {
74 let left_keys: HashSet<_> = left.keys().cloned().collect();
75 let right_keys: HashSet<_> = right.keys().cloned().collect();
76
77 let missing: Vec<String> = right_keys.difference(&left_keys).cloned().collect();
78
79 let extra: Vec<String> = left_keys.difference(&right_keys).cloned().collect();
80
81 let mut different = Vec::new();
82 for key in left_keys.intersection(&right_keys) {
83 let left_val = left.get(key).unwrap();
84 let right_val = right.get(key).unwrap();
85
86 if left_val != right_val {
87 different.push(DiffItem {
88 key: key.clone(),
89 example_value: right_val.clone(),
90 env_value: left_val.clone(),
91 });
92 }
93 }
94
95 DiffResult {
96 missing,
97 extra,
98 different,
99 }
100}
101
102fn output_pretty(
103 diff: &DiffResult,
104 left: &HashMap<String, String>,
105 right: &HashMap<String, String>,
106 left_name: &str,
107 right_name: &str,
108 show_values: bool,
109) -> Result<()> {
110 let has_changes =
111 !diff.missing.is_empty() || !diff.extra.is_empty() || !diff.different.is_empty();
112
113 if !has_changes {
114 println!("{} Files are identical", "✓".green());
115 return Ok(());
116 }
117
118 if !diff.missing.is_empty() {
119 println!(
120 "{}",
121 format!("Missing from {} (present in {}):", left_name, right_name).bold()
122 );
123 for key in &diff.missing {
124 if show_values {
125 if let Some(val) = right.get(key) {
126 println!(" {} {} = {}", "+".green(), key.bold(), val.dimmed());
127 }
128 } else {
129 println!(" {} {}", "+".green(), key.bold());
130 }
131 }
132 println!();
133 }
134
135 if !diff.extra.is_empty() {
136 println!(
137 "{}",
138 format!("Extra in {} (not in {}):", left_name, right_name).bold()
139 );
140 for key in &diff.extra {
141 if show_values {
142 if let Some(val) = left.get(key) {
143 println!(" {} {} = {}", "-".red(), key.bold(), val.dimmed());
144 }
145 } else {
146 println!(" {} {}", "-".red(), key.bold());
147 }
148 }
149 println!();
150 }
151
152 if !diff.different.is_empty() {
153 println!("{}", "Different values:".bold());
154 for item in &diff.different {
155 println!(" {} {}", "~".yellow(), item.key.bold());
156 if show_values {
157 println!(" {}: {}", right_name, item.example_value.dimmed());
158 println!(" {}: {}", left_name, item.env_value.dimmed());
159 }
160 }
161 println!();
162 }
163
164 println!("{}", "Summary:".bold());
165 println!(" {} missing (add to {})", diff.missing.len(), left_name);
166 println!(
167 " {} extra (consider removing or adding to {})",
168 diff.extra.len(),
169 right_name
170 );
171 println!(" {} different values", diff.different.len());
172
173 Ok(())
174}
175
176fn output_json(diff: &DiffResult) -> Result<()> {
177 let json = serde_json::to_string_pretty(diff)?;
178 println!("{}", json);
179 Ok(())
180}
181
182fn output_patch(
183 diff: &DiffResult,
184 left: &HashMap<String, String>,
185 right: &HashMap<String, String>,
186) -> Result<()> {
187 println!("# Add these to .env:");
188 for key in &diff.missing {
189 if let Some(val) = right.get(key) {
190 println!("+ {}={}", key, val);
191 }
192 }
193
194 println!("\n# Remove these from .env:");
195 for key in &diff.extra {
196 if let Some(val) = left.get(key) {
197 println!("- {}={}", key, val);
198 }
199 }
200
201 println!("\n# Update these in .env:");
202 for item in &diff.different {
203 println!("- {}={}", item.key, item.env_value);
204 println!("+ {}={}", item.key, item.example_value);
205 }
206
207 Ok(())
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn test_compute_diff() {
216 let mut left = HashMap::new();
217 left.insert("KEY1".to_string(), "value1".to_string());
218 left.insert("KEY2".to_string(), "value2".to_string());
219 left.insert("EXTRA".to_string(), "extra".to_string());
220
221 let mut right = HashMap::new();
222 right.insert("KEY1".to_string(), "value1".to_string());
223 right.insert("KEY2".to_string(), "different".to_string());
224 right.insert("MISSING".to_string(), "missing".to_string());
225
226 let diff = compute_diff(&left, &right);
227
228 assert_eq!(diff.missing, vec!["MISSING"]);
229 assert_eq!(diff.extra, vec!["EXTRA"]);
230 assert_eq!(diff.different.len(), 1);
231 assert_eq!(diff.different[0].key, "KEY2");
232 }
233}