@@ -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 ! ( "\n Owners:" ) ;
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 ! ( "\n Tags:" ) ;
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 ! ( "\n Matching 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