Skip to content

Commit 642de81

Browse files
committed
wip(inspect): initial implementation for inspect function
1 parent f50e0a3 commit 642de81

File tree

2 files changed

+196
-0
lines changed

2 files changed

+196
-0
lines changed

src/cli/mod.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,27 @@ enum CodeownersSubcommand {
169169
#[arg(long, value_name = "FORMAT", default_value = "text", value_parser = parse_output_format)]
170170
format: OutputFormat,
171171

172+
/// Custom cache file location
173+
#[arg(long, value_name = "FILE", default_value = ".codeowners.cache")]
174+
cache_file: Option<PathBuf>,
175+
},
176+
#[clap(
177+
name = "inspect",
178+
about = "Inspect ownership and tags for a specific file"
179+
)]
180+
Inspect {
181+
/// File path to inspect
182+
#[arg(value_name = "FILE")]
183+
file_path: PathBuf,
184+
185+
/// Directory path to analyze (default: current directory)
186+
#[arg(short, long, default_value = ".")]
187+
repo: Option<PathBuf>,
188+
189+
/// Output format: text|json|bincode
190+
#[arg(long, value_name = "FORMAT", default_value = "text", value_parser = parse_output_format)]
191+
format: OutputFormat,
192+
172193
/// Custom cache file location
173194
#[arg(long, value_name = "FILE", default_value = ".codeowners.cache")]
174195
cache_file: Option<PathBuf>,
@@ -245,6 +266,14 @@ pub(crate) fn codeowners(subcommand: &CodeownersSubcommand) -> Result<()> {
245266
format,
246267
cache_file,
247268
} => commands::codeowners_list_tags(path.as_deref(), format, cache_file.as_deref()),
269+
CodeownersSubcommand::Inspect {
270+
file_path,
271+
repo,
272+
format,
273+
cache_file,
274+
} => {
275+
commands::codeowners_inspect(file_path, repo.as_deref(), format, cache_file.as_deref())
276+
}
248277
}
249278
}
250279

src/core/commands.rs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,3 +498,170 @@ pub fn codeowners_list_tags(
498498
);
499499
Ok(())
500500
}
501+
502+
/// Inspect ownership and tags for a specific file
503+
pub fn codeowners_inspect(
504+
file_path: &std::path::Path, repo: Option<&std::path::Path>, format: &OutputFormat,
505+
cache_file: Option<&std::path::Path>,
506+
) -> Result<()> {
507+
// Repository path
508+
let repo = repo.unwrap_or_else(|| std::path::Path::new("."));
509+
510+
// Load the cache
511+
let cache = sync_cache(repo, cache_file)?;
512+
513+
// Normalize the file path to be relative to the repo
514+
let normalized_file_path = if file_path.is_absolute() {
515+
file_path
516+
.strip_prefix(repo)
517+
.map_err(|_| {
518+
Error::new(&format!(
519+
"File {} is not within repository {}",
520+
file_path.display(),
521+
repo.display()
522+
))
523+
})?
524+
.to_path_buf()
525+
} else {
526+
file_path.to_path_buf()
527+
};
528+
529+
// Find the file in the cache
530+
let file_entry = cache
531+
.files
532+
.iter()
533+
.find(|file| file.path == normalized_file_path)
534+
.ok_or_else(|| {
535+
Error::new(&format!(
536+
"File {} not found in cache",
537+
normalized_file_path.display()
538+
))
539+
})?;
540+
541+
// Find the CODEOWNERS entries that match this file
542+
let matching_entries: Vec<&CodeownersEntry> = cache
543+
.entries
544+
.iter()
545+
.filter(|entry| {
546+
// Simple pattern matching - in a real implementation you'd want proper glob matching
547+
let pattern = &entry.pattern;
548+
let file_str = normalized_file_path.to_string_lossy();
549+
550+
if pattern.ends_with("*") {
551+
let prefix = &pattern[..pattern.len() - 1];
552+
file_str.starts_with(prefix)
553+
} else if pattern.starts_with("*") {
554+
let suffix = &pattern[1..];
555+
file_str.ends_with(suffix)
556+
} else if pattern.contains('*') {
557+
// Basic wildcard matching - could be improved
558+
let parts: Vec<&str> = pattern.split('*').collect();
559+
if parts.len() == 2 {
560+
file_str.starts_with(parts[0]) && file_str.ends_with(parts[1])
561+
} else {
562+
file_str == *pattern || file_str.starts_with(&format!("{}/", pattern))
563+
}
564+
} else {
565+
file_str == *pattern || file_str.starts_with(&format!("{}/", pattern))
566+
}
567+
})
568+
.collect();
569+
570+
// Create inspection result
571+
let inspection_result = serde_json::json!({
572+
"file_path": normalized_file_path.to_string_lossy(),
573+
"owners": file_entry.owners,
574+
"tags": file_entry.tags.iter().map(|t| &t.0).collect::<Vec<_>>(),
575+
"matching_rules": matching_entries.iter().map(|entry| {
576+
serde_json::json!({
577+
"source_file": entry.source_file.to_string_lossy(),
578+
"line_number": entry.line_number,
579+
"pattern": entry.pattern,
580+
"owners": entry.owners,
581+
"tags": entry.tags.iter().map(|t| &t.0).collect::<Vec<_>>()
582+
})
583+
}).collect::<Vec<_>>()
584+
});
585+
586+
// Output the inspection result in the requested format
587+
match format {
588+
OutputFormat::Text => {
589+
println!(
590+
"==============================================================================="
591+
);
592+
println!(" File: {}", normalized_file_path.display());
593+
println!(
594+
"==============================================================================="
595+
);
596+
println!("\nOwners:");
597+
if file_entry.owners.is_empty() {
598+
println!(" (no owners)");
599+
} else {
600+
for owner in &file_entry.owners {
601+
println!(" - {}", owner.identifier);
602+
}
603+
}
604+
605+
println!("\nTags:");
606+
if file_entry.tags.is_empty() {
607+
println!(" (no tags)");
608+
} else {
609+
for tag in &file_entry.tags {
610+
println!(" - {}", tag.0);
611+
}
612+
}
613+
614+
println!("\nMatching CODEOWNERS Rules:");
615+
if matching_entries.is_empty() {
616+
println!(" (no explicit rules)");
617+
} else {
618+
for entry in matching_entries {
619+
println!(
620+
"\n From {}:{}",
621+
entry.source_file.display(),
622+
entry.line_number
623+
);
624+
println!(" Pattern: {}", entry.pattern);
625+
let owners_str = entry
626+
.owners
627+
.iter()
628+
.map(|o| o.identifier.as_str())
629+
.collect::<Vec<_>>()
630+
.join(", ");
631+
println!(" Owners: {}", owners_str);
632+
if !entry.tags.is_empty() {
633+
println!(
634+
" Tags: {}",
635+
entry
636+
.tags
637+
.iter()
638+
.map(|t| t.0.as_str())
639+
.collect::<Vec<_>>()
640+
.join(", ")
641+
);
642+
}
643+
}
644+
}
645+
println!();
646+
}
647+
OutputFormat::Json => {
648+
println!(
649+
"{}",
650+
serde_json::to_string_pretty(&inspection_result)
651+
.map_err(|e| Error::new(&format!("JSON serialization error: {}", e)))?
652+
);
653+
}
654+
OutputFormat::Bincode => {
655+
let encoded =
656+
bincode::serde::encode_to_vec(&inspection_result, bincode::config::standard())
657+
.map_err(|e| Error::new(&format!("Serialization error: {}", e)))?;
658+
659+
// Write raw binary bytes to stdout
660+
io::stdout()
661+
.write_all(&encoded)
662+
.map_err(|e| Error::new(&format!("IO error: {}", e)))?;
663+
}
664+
}
665+
666+
Ok(())
667+
}

0 commit comments

Comments
 (0)