Skip to content

Commit c6faaef

Browse files
committed
imp(core): add list-files, list-owners, list-tags initial implementation
1 parent 9d82508 commit c6faaef

File tree

3 files changed

+283
-7
lines changed

3 files changed

+283
-7
lines changed

cli/src/lib.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ enum CodeownersSubcommand {
136136
about = "Display aggregated owner statistics and associations"
137137
)]
138138
ListOwners {
139+
/// Directory path to analyze (default: current directory)
140+
#[arg(default_value = ".")]
141+
path: Option<PathBuf>,
142+
139143
/// Output format: text|json|bincode
140144
#[arg(long, value_name = "FORMAT", default_value = "text", value_parser = parse_output_format)]
141145
format: OutputFormat,
@@ -145,6 +149,10 @@ enum CodeownersSubcommand {
145149
about = "Audit and analyze tag usage across CODEOWNERS files"
146150
)]
147151
ListTags {
152+
/// Directory path to analyze (default: current directory)
153+
#[arg(default_value = ".")]
154+
path: Option<PathBuf>,
155+
148156
/// Output format: text|json|bincode
149157
#[arg(long, value_name = "FORMAT", default_value = "text", value_parser = parse_output_format)]
150158
format: OutputFormat,
@@ -207,8 +215,12 @@ pub(crate) fn codeowners(subcommand: &CodeownersSubcommand) -> Result<()> {
207215
*unowned,
208216
format,
209217
),
210-
CodeownersSubcommand::ListOwners { format } => commands::codeowners_list_owners(format),
211-
CodeownersSubcommand::ListTags { format } => commands::codeowners_list_tags(format),
218+
CodeownersSubcommand::ListOwners { path, format } => {
219+
commands::codeowners_list_owners(path.as_deref(), format)
220+
}
221+
CodeownersSubcommand::ListTags { path, format } => {
222+
commands::codeowners_list_tags(path.as_deref(), format)
223+
}
212224
}
213225
}
214226

core/src/commands.rs

Lines changed: 257 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pub fn codeowners_parse(
4646
dbg!(cache);
4747

4848
println!("CODEOWNERS parsing completed successfully");
49+
4950
Ok(())
5051
}
5152

@@ -61,22 +62,273 @@ pub fn codeowners_list_files(
6162
info!("Unowned only: {}", unowned);
6263
info!("Output format: {}", format);
6364

64-
println!("Files listing completed");
65+
// Determine the cache file path based on repository path
66+
let repo_path = path.unwrap_or_else(|| std::path::Path::new("."));
67+
//let config = utils::app_config::AppConfig::fetch()?;
68+
// let cache_dir = config
69+
// .cache_dir
70+
// .unwrap_or_else(|| repo_path.join(".codeowners.cache"));
71+
let cache_file = repo_path.join(".codeowners.cache");
72+
73+
if !cache_file.exists() {
74+
return Err(utils::error::Error::new(&format!(
75+
"Cache file not found at {}. Please run 'codeowners parse' first.",
76+
cache_file.display()
77+
)));
78+
}
79+
80+
// Load the cache
81+
let cache = load_cache(&cache_file)?;
82+
83+
// Filter files based on criteria
84+
let filtered_files = cache
85+
.files
86+
.iter()
87+
.filter(|file| {
88+
// Check if we should include this file based on filters
89+
let passes_owner_filter = match owners {
90+
Some(owner_filter) => {
91+
let owner_patterns: Vec<&str> = owner_filter.split(',').collect();
92+
file.owners.iter().any(|owner| {
93+
owner_patterns
94+
.iter()
95+
.any(|pattern| owner.identifier.contains(pattern))
96+
})
97+
}
98+
None => true,
99+
};
100+
101+
let passes_tag_filter = match tags {
102+
Some(tag_filter) => {
103+
let tag_patterns: Vec<&str> = tag_filter.split(',').collect();
104+
file.tags
105+
.iter()
106+
.any(|tag| tag_patterns.iter().any(|pattern| tag.0.contains(pattern)))
107+
}
108+
None => true,
109+
};
110+
111+
let passes_unowned_filter = if unowned {
112+
file.owners.is_empty()
113+
} else {
114+
true
115+
};
116+
117+
passes_owner_filter && passes_tag_filter && passes_unowned_filter
118+
})
119+
.collect::<Vec<_>>();
120+
121+
// Output the filtered files in the requested format
122+
match format {
123+
OutputFormat::Text => {
124+
for file in filtered_files {
125+
let owners_str = file
126+
.owners
127+
.iter()
128+
.map(|o| o.identifier.clone())
129+
.collect::<Vec<_>>()
130+
.join(", ");
131+
132+
let tags_str = file
133+
.tags
134+
.iter()
135+
.map(|t| t.0.clone())
136+
.collect::<Vec<_>>()
137+
.join(", ");
138+
139+
println!("File: {}", file.path.display());
140+
println!(
141+
" Owners: {}",
142+
if owners_str.is_empty() {
143+
"None"
144+
} else {
145+
&owners_str
146+
}
147+
);
148+
println!(
149+
" Tags: {}",
150+
if tags_str.is_empty() {
151+
"None"
152+
} else {
153+
&tags_str
154+
}
155+
);
156+
println!();
157+
}
158+
}
159+
OutputFormat::Json => {
160+
println!("{}", serde_json::to_string_pretty(&filtered_files).unwrap());
161+
}
162+
OutputFormat::Bincode => {
163+
eprintln!("Binary output to stdout not supported for file listing");
164+
return Err(utils::error::Error::new(
165+
"Binary output to stdout not supported",
166+
));
167+
}
168+
}
169+
65170
Ok(())
66171
}
67172

68173
/// Display aggregated owner statistics and associations
69-
pub fn codeowners_list_owners(format: &OutputFormat) -> Result<()> {
174+
pub fn codeowners_list_owners(path: Option<&std::path::Path>, format: &OutputFormat) -> Result<()> {
175+
info!("Listing owners");
70176
info!("Output format: {}", format);
71177

72-
println!("Owners listing completed");
178+
// Determine the cache file path based on repository path
179+
let repo_path = path.unwrap_or_else(|| std::path::Path::new("."));
180+
let cache_file = repo_path.join(".codeowners.cache");
181+
182+
if !cache_file.exists() {
183+
return Err(utils::error::Error::new(&format!(
184+
"Cache file not found at {}. Please run 'codeowners parse' first.",
185+
cache_file.display()
186+
)));
187+
}
188+
189+
// Load the cache
190+
let cache = load_cache(&cache_file)?;
191+
192+
// Process the owners from the cache
193+
match format {
194+
OutputFormat::Text => {
195+
println!("CODEOWNERS Ownership Report");
196+
println!("==========================\n");
197+
198+
if cache.owners_map.is_empty() {
199+
println!("No owners found in the codebase.");
200+
} else {
201+
// Sort owners by number of files they own (descending)
202+
let mut owners_with_counts: Vec<_> = cache.owners_map.iter().collect();
203+
owners_with_counts.sort_by(|a, b| b.1.len().cmp(&a.1.len()));
204+
205+
for (owner, paths) in owners_with_counts {
206+
println!("Owner: {} ({})", owner.identifier, owner.owner_type);
207+
println!("Files owned: {}", paths.len());
208+
209+
// List first 5 files (to avoid overwhelming output)
210+
if !paths.is_empty() {
211+
println!("Sample files:");
212+
for path in paths.iter().take(5) {
213+
println!(" - {}", path.display());
214+
}
215+
216+
if paths.len() > 5 {
217+
println!(" ... and {} more", paths.len() - 5);
218+
}
219+
}
220+
221+
println!(); // Empty line between owners
222+
}
223+
}
224+
}
225+
OutputFormat::Json => {
226+
// Convert to a more friendly JSON structure
227+
let owners_data: Vec<_> = cache.owners_map.iter()
228+
.map(|(owner, paths)| {
229+
serde_json::json!({
230+
"identifier": owner.identifier,
231+
"type": format!("{:?}", owner.owner_type),
232+
"file_count": paths.len(),
233+
"files": paths.iter().map(|p| p.to_string_lossy().to_string()).collect::<Vec<_>>()
234+
})
235+
})
236+
.collect();
237+
238+
println!("{}", serde_json::to_string_pretty(&owners_data).unwrap());
239+
}
240+
OutputFormat::Bincode => {
241+
eprintln!("Binary output to stdout not supported for owners listing");
242+
return Err(utils::error::Error::new(
243+
"Binary output to stdout not supported",
244+
));
245+
}
246+
}
247+
248+
println!(
249+
"Owners listing completed - {} owners found",
250+
cache.owners_map.len()
251+
);
73252
Ok(())
74253
}
75254

76255
/// Audit and analyze tag usage across CODEOWNERS files
77-
pub fn codeowners_list_tags(format: &OutputFormat) -> Result<()> {
256+
pub fn codeowners_list_tags(path: Option<&std::path::Path>, format: &OutputFormat) -> Result<()> {
257+
info!("Listing tags");
78258
info!("Output format: {}", format);
79259

80-
println!("Tags listing completed");
260+
// Determine the cache file path based on repository path
261+
let repo_path = path.unwrap_or_else(|| std::path::Path::new("."));
262+
let cache_file = repo_path.join(".codeowners.cache");
263+
264+
if !cache_file.exists() {
265+
return Err(utils::error::Error::new(&format!(
266+
"Cache file not found at {}. Please run 'codeowners parse' first.",
267+
cache_file.display()
268+
)));
269+
}
270+
271+
// Load the cache
272+
let cache = load_cache(&cache_file)?;
273+
274+
// Process the tags from the cache
275+
match format {
276+
OutputFormat::Text => {
277+
println!("CODEOWNERS Tags Report");
278+
println!("======================\n");
279+
280+
if cache.tags_map.is_empty() {
281+
println!("No tags found in the codebase.");
282+
} else {
283+
// Sort tags by number of files they're associated with (descending)
284+
let mut tags_with_counts: Vec<_> = cache.tags_map.iter().collect();
285+
tags_with_counts.sort_by(|a, b| b.1.len().cmp(&a.1.len()));
286+
287+
for (tag, paths) in tags_with_counts {
288+
println!("Tag: {}", tag.0);
289+
println!("Files tagged: {}", paths.len());
290+
291+
// List first 5 files (to avoid overwhelming output)
292+
if !paths.is_empty() {
293+
println!("Sample files:");
294+
for path in paths.iter().take(5) {
295+
println!(" - {}", path.display());
296+
}
297+
298+
if paths.len() > 5 {
299+
println!(" ... and {} more", paths.len() - 5);
300+
}
301+
}
302+
303+
println!(); // Empty line between tags
304+
}
305+
}
306+
}
307+
OutputFormat::Json => {
308+
// Convert to a more friendly JSON structure
309+
let tags_data: Vec<_> = cache.tags_map.iter()
310+
.map(|(tag, paths)| {
311+
serde_json::json!({
312+
"name": tag.0,
313+
"file_count": paths.len(),
314+
"files": paths.iter().map(|p| p.to_string_lossy().to_string()).collect::<Vec<_>>()
315+
})
316+
})
317+
.collect();
318+
319+
println!("{}", serde_json::to_string_pretty(&tags_data).unwrap());
320+
}
321+
OutputFormat::Bincode => {
322+
eprintln!("Binary output to stdout not supported for tags listing");
323+
return Err(utils::error::Error::new(
324+
"Binary output to stdout not supported",
325+
));
326+
}
327+
}
328+
329+
println!(
330+
"Tags listing completed - {} tags found",
331+
cache.tags_map.len()
332+
);
81333
Ok(())
82334
}

core/src/types.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ pub enum OwnerType {
2929
Unknown,
3030
}
3131

32+
impl std::fmt::Display for OwnerType {
33+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34+
match self {
35+
OwnerType::User => write!(f, "User"),
36+
OwnerType::Team => write!(f, "Team"),
37+
OwnerType::Email => write!(f, "Email"),
38+
OwnerType::Unowned => write!(f, "Unowned"),
39+
OwnerType::Unknown => write!(f, "Unknown"),
40+
}
41+
}
42+
}
43+
3244
/// Tag representation
3345
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
3446
pub struct Tag(pub String);

0 commit comments

Comments
 (0)