features_cli/
codeowners.rs1use anyhow::Result;
2use std::fs;
3use std::io::{BufRead, BufReader, Write};
4use std::path::Path;
5
6use crate::models::Feature;
7
8const SECTION_START: &str = "# ==== GENERATED BY FEATURES-CLI ====";
9const SECTION_END: &str = "# ==== END SECTION GENERATED BY FEATURES-CLI ====";
10
11pub fn generate_codeowners(
31 features: &[Feature],
32 base_path: &Path,
33 project_dir: Option<&Path>,
34 output_dir: &Path,
35 codeowners_path_override: Option<&Path>,
36 owner_prefix: &str,
37) -> Result<()> {
38 let codeowners_path = if let Some(custom_path) = codeowners_path_override {
39 custom_path.to_path_buf()
40 } else {
41 output_dir.join("CODEOWNERS")
42 };
43
44 let (before_section, after_section) = if codeowners_path.exists() {
46 read_existing_codeowners(&codeowners_path)?
47 } else {
48 (String::new(), String::new())
49 };
50
51 let mut entries = Vec::new();
53 collect_entries(features, base_path, project_dir, &mut entries, owner_prefix);
54
55 write_codeowners_file(&codeowners_path, &before_section, &entries, &after_section)?;
57
58 eprintln!(
59 "✅ CODEOWNERS file generated at: {}",
60 codeowners_path.display()
61 );
62 Ok(())
63}
64
65fn read_existing_codeowners(codeowners_path: &Path) -> Result<(String, String)> {
67 let file = fs::File::open(codeowners_path)?;
68 let reader = BufReader::new(file);
69 let mut lines = Vec::new();
70
71 for line in reader.lines() {
72 lines.push(line?);
73 }
74
75 let start_idx = lines.iter().position(|l| l.trim() == SECTION_START);
77 let end_idx = lines.iter().position(|l| l.trim() == SECTION_END);
78
79 match (start_idx, end_idx) {
80 (Some(start), Some(end)) if start < end => {
81 let before = lines[..start].join("\n");
83 let after = if end + 1 < lines.len() {
84 lines[end + 1..].join("\n")
85 } else {
86 String::new()
87 };
88 Ok((before, after))
89 }
90 _ => {
91 Ok((lines.join("\n"), String::new()))
93 }
94 }
95}
96
97fn collect_entries(
99 features: &[Feature],
100 base_path: &Path,
101 project_dir: Option<&Path>,
102 entries: &mut Vec<String>,
103 owner_prefix: &str,
104) {
105 collect_entries_with_parent(
106 features,
107 base_path,
108 project_dir,
109 entries,
110 None,
111 owner_prefix,
112 );
113}
114
115fn collect_entries_with_parent(
117 features: &[Feature],
118 base_path: &Path,
119 project_dir: Option<&Path>,
120 entries: &mut Vec<String>,
121 parent_owner: Option<&str>,
122 owner_prefix: &str,
123) {
124 for feature in features {
125 let should_skip = parent_owner.is_some_and(|parent| feature.owner == parent);
127
128 if !should_skip {
129 let feature_path = std::path::PathBuf::from(&feature.path);
131 let full_path = base_path.join(&feature_path);
132
133 let codeowners_path = if let Some(proj_dir) = project_dir {
135 if let Ok(relative) = full_path.strip_prefix(proj_dir) {
137 relative.to_path_buf()
138 } else {
139 full_path
140 }
141 } else {
142 full_path
143 };
144
145 let path_str = codeowners_path.to_str().unwrap_or("").replace('\\', "/");
147
148 let path_str = if path_str.starts_with('/') {
149 path_str
150 } else {
151 format!("/{}", path_str)
152 };
153
154 if feature.owner.is_empty() {
156 let unknown_owner = format_owner("Unknown", owner_prefix);
157 entries.push(format!("{} {}", path_str, unknown_owner));
158 } else {
159 let owner = format_owner(&feature.owner, owner_prefix);
160 entries.push(format!("{} {}", path_str, owner));
161 }
162 }
163
164 collect_entries_with_parent(
166 &feature.features,
167 base_path,
168 project_dir,
169 entries,
170 Some(&feature.owner),
171 owner_prefix,
172 );
173 }
174}
175
176fn format_owner(owner: &str, prefix: &str) -> String {
178 if prefix.is_empty() || owner.starts_with(prefix) {
179 owner.to_string()
180 } else {
181 format!("{}{}", prefix, owner)
182 }
183}
184
185fn write_codeowners_file(
187 codeowners_path: &Path,
188 before_section: &str,
189 entries: &[String],
190 after_section: &str,
191) -> Result<()> {
192 let mut file = fs::File::create(codeowners_path)?;
193
194 if !before_section.is_empty() {
196 writeln!(file, "{}", before_section)?;
197 if !before_section.ends_with('\n') {
198 writeln!(file)?;
199 }
200 }
201
202 writeln!(file, "{}", SECTION_START)?;
204 for entry in entries {
205 writeln!(file, "{}", entry)?;
206 }
207 writeln!(file, "{}", SECTION_END)?;
208
209 if !after_section.is_empty() {
211 if !after_section.starts_with('\n') {
212 writeln!(file)?;
213 }
214 write!(file, "{}", after_section)?;
215 if !after_section.ends_with('\n') {
216 writeln!(file)?;
217 }
218 }
219
220 Ok(())
221}