@@ -10,15 +10,15 @@ use uuid::Uuid;
1010
1111use super :: pr_readiness:: {
1212 apply_dynamic_review_state, build_pr_readiness_snapshot, build_repo_blocker_rollups,
13- get_pr_readiness_snapshot, latest_review_head_by_source , load_review_inventory ,
14- PrReadinessSnapshot ,
13+ get_pr_readiness_snapshot, latest_pr_review_session , latest_review_head_by_source ,
14+ load_review_inventory , pr_diff_source , PrReadinessSnapshot ,
1515} ;
1616use super :: state:: {
1717 build_progress_callback, count_diff_files, count_reviewed_files, current_timestamp,
1818 emit_wide_event, AppState , FileMetricEvent , HotspotDetail , ReviewEventBuilder , ReviewListItem ,
1919 ReviewSession , ReviewStatus , MAX_DIFF_SIZE ,
2020} ;
21- use crate :: core:: comment:: { CommentSynthesizer , MergeReadiness } ;
21+ use crate :: core:: comment:: { CommentStatus , CommentSynthesizer , MergeReadiness } ;
2222use crate :: core:: convention_learner:: ConventionStore ;
2323use tracing:: { info, warn} ;
2424
@@ -1837,6 +1837,79 @@ pub struct PrReadinessParams {
18371837 pub pr_number : u32 ,
18381838}
18391839
1840+ #[ derive( Deserialize ) ]
1841+ pub struct PrCommentSearchParams {
1842+ pub repo : String ,
1843+ pub pr_number : u32 ,
1844+ pub status : Option < String > ,
1845+ }
1846+
1847+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
1848+ enum CommentSearchFilter {
1849+ All ,
1850+ Unresolved ,
1851+ Resolved ,
1852+ Dismissed ,
1853+ }
1854+
1855+ impl CommentSearchFilter {
1856+ fn from_api_value ( value : Option < & str > ) -> Option < Self > {
1857+ match value. map ( |value| value. trim ( ) . to_ascii_lowercase ( ) ) {
1858+ None => Some ( Self :: All ) ,
1859+ Some ( value) if value. is_empty ( ) || value == "all" => Some ( Self :: All ) ,
1860+ Some ( value) if value == "open" || value == "unresolved" => Some ( Self :: Unresolved ) ,
1861+ Some ( value) if value == "resolved" => Some ( Self :: Resolved ) ,
1862+ Some ( value) if value == "dismissed" => Some ( Self :: Dismissed ) ,
1863+ _ => None ,
1864+ }
1865+ }
1866+
1867+ fn as_api_str ( self ) -> & ' static str {
1868+ match self {
1869+ Self :: All => "all" ,
1870+ Self :: Unresolved => "unresolved" ,
1871+ Self :: Resolved => "resolved" ,
1872+ Self :: Dismissed => "dismissed" ,
1873+ }
1874+ }
1875+
1876+ fn matches ( self , status : CommentStatus ) -> bool {
1877+ match self {
1878+ Self :: All => true ,
1879+ Self :: Unresolved => status == CommentStatus :: Open ,
1880+ Self :: Resolved => status == CommentStatus :: Resolved ,
1881+ Self :: Dismissed => status == CommentStatus :: Dismissed ,
1882+ }
1883+ }
1884+ }
1885+
1886+ #[ derive( Serialize ) ]
1887+ pub struct PrCommentSearchResponse {
1888+ pub repo : String ,
1889+ pub pr_number : u32 ,
1890+ pub diff_source : String ,
1891+ pub status : String ,
1892+ #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
1893+ pub latest_review_id : Option < String > ,
1894+ #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
1895+ pub latest_review_status : Option < ReviewStatus > ,
1896+ #[ serde( default ) ]
1897+ pub total_comments : usize ,
1898+ #[ serde( default ) ]
1899+ pub comments : Vec < crate :: core:: Comment > ,
1900+ }
1901+
1902+ fn filter_comments_by_search_filter (
1903+ comments : & [ crate :: core:: Comment ] ,
1904+ filter : CommentSearchFilter ,
1905+ ) -> Vec < crate :: core:: Comment > {
1906+ comments
1907+ . iter ( )
1908+ . filter ( |comment| filter. matches ( comment. status ) )
1909+ . cloned ( )
1910+ . collect ( )
1911+ }
1912+
18401913#[ derive( Serialize ) ]
18411914pub struct GhPullRequest {
18421915 pub number : u32 ,
@@ -2081,6 +2154,49 @@ pub async fn get_gh_pr_readiness(
20812154 ) )
20822155}
20832156
2157+ pub async fn get_gh_pr_comments (
2158+ State ( state) : State < Arc < AppState > > ,
2159+ Query ( params) : Query < PrCommentSearchParams > ,
2160+ ) -> Result < Json < PrCommentSearchResponse > , ( StatusCode , String ) > {
2161+ if !is_valid_repo_name ( & params. repo ) {
2162+ return Err ( (
2163+ StatusCode :: BAD_REQUEST ,
2164+ "Invalid repo format. Expected 'owner/repo'." . to_string ( ) ,
2165+ ) ) ;
2166+ }
2167+
2168+ if params. pr_number == 0 || params. pr_number > 999_999_999 {
2169+ return Err ( ( StatusCode :: BAD_REQUEST , "Invalid PR number." . to_string ( ) ) ) ;
2170+ }
2171+
2172+ let filter =
2173+ CommentSearchFilter :: from_api_value ( params. status . as_deref ( ) ) . ok_or_else ( || {
2174+ (
2175+ StatusCode :: BAD_REQUEST ,
2176+ "Invalid status. Must be all, unresolved, open, resolved, or dismissed."
2177+ . to_string ( ) ,
2178+ )
2179+ } ) ?;
2180+
2181+ let inventory = load_review_inventory ( & state) . await ;
2182+ let latest_review = latest_pr_review_session ( & inventory, & params. repo , params. pr_number ) ;
2183+ let comments = latest_review
2184+ . as_ref ( )
2185+ . map ( |review| filter_comments_by_search_filter ( & review. comments , filter) )
2186+ . unwrap_or_default ( ) ;
2187+
2188+ Ok ( Json ( PrCommentSearchResponse {
2189+ repo : params. repo . clone ( ) ,
2190+ pr_number : params. pr_number ,
2191+ diff_source : pr_diff_source ( & params. repo , params. pr_number ) ,
2192+ status : filter. as_api_str ( ) . to_string ( ) ,
2193+ latest_review_id : latest_review. as_ref ( ) . map ( |review| review. id . clone ( ) ) ,
2194+ latest_review_status : latest_review. as_ref ( ) . map ( |review| review. status . clone ( ) ) ,
2195+ total_comments : comments. len ( ) ,
2196+ comments,
2197+ } ) )
2198+ }
2199+
20842200// === GitHub PR Review ===
20852201
20862202#[ derive( Deserialize ) ]
@@ -3211,4 +3327,86 @@ mod tests {
32113327 assert_eq ! ( req. pr_number, 42 ) ;
32123328 assert ! ( req. post_results) ;
32133329 }
3330+
3331+ fn make_search_comment ( id : & str , status : CommentStatus ) -> crate :: core:: Comment {
3332+ crate :: core:: Comment {
3333+ id : id. to_string ( ) ,
3334+ file_path : std:: path:: PathBuf :: from ( "src/lib.rs" ) ,
3335+ line_number : 10 ,
3336+ content : format ! ( "comment {id}" ) ,
3337+ rule_id : None ,
3338+ severity : crate :: core:: comment:: Severity :: Warning ,
3339+ category : crate :: core:: comment:: Category :: Bug ,
3340+ suggestion : None ,
3341+ confidence : 0.8 ,
3342+ code_suggestion : None ,
3343+ tags : Vec :: new ( ) ,
3344+ fix_effort : crate :: core:: comment:: FixEffort :: Low ,
3345+ feedback : None ,
3346+ status,
3347+ }
3348+ }
3349+
3350+ #[ test]
3351+ fn test_comment_search_filter_parses_aliases ( ) {
3352+ assert_eq ! (
3353+ CommentSearchFilter :: from_api_value( None ) ,
3354+ Some ( CommentSearchFilter :: All )
3355+ ) ;
3356+ assert_eq ! (
3357+ CommentSearchFilter :: from_api_value( Some ( "open" ) ) ,
3358+ Some ( CommentSearchFilter :: Unresolved )
3359+ ) ;
3360+ assert_eq ! (
3361+ CommentSearchFilter :: from_api_value( Some ( "unresolved" ) ) ,
3362+ Some ( CommentSearchFilter :: Unresolved )
3363+ ) ;
3364+ assert_eq ! (
3365+ CommentSearchFilter :: from_api_value( Some ( "resolved" ) ) ,
3366+ Some ( CommentSearchFilter :: Resolved )
3367+ ) ;
3368+ assert_eq ! (
3369+ CommentSearchFilter :: from_api_value( Some ( "dismissed" ) ) ,
3370+ Some ( CommentSearchFilter :: Dismissed )
3371+ ) ;
3372+ assert_eq ! ( CommentSearchFilter :: from_api_value( Some ( "wat" ) ) , None ) ;
3373+ }
3374+
3375+ #[ test]
3376+ fn test_filter_comments_by_search_filter_matches_status ( ) {
3377+ let comments = vec ! [
3378+ make_search_comment( "open" , CommentStatus :: Open ) ,
3379+ make_search_comment( "resolved" , CommentStatus :: Resolved ) ,
3380+ make_search_comment( "dismissed" , CommentStatus :: Dismissed ) ,
3381+ ] ;
3382+
3383+ assert_eq ! (
3384+ filter_comments_by_search_filter( & comments, CommentSearchFilter :: All )
3385+ . into_iter( )
3386+ . map( |comment| comment. id)
3387+ . collect:: <Vec <_>>( ) ,
3388+ vec![ "open" , "resolved" , "dismissed" ]
3389+ ) ;
3390+ assert_eq ! (
3391+ filter_comments_by_search_filter( & comments, CommentSearchFilter :: Unresolved )
3392+ . into_iter( )
3393+ . map( |comment| comment. id)
3394+ . collect:: <Vec <_>>( ) ,
3395+ vec![ "open" ]
3396+ ) ;
3397+ assert_eq ! (
3398+ filter_comments_by_search_filter( & comments, CommentSearchFilter :: Resolved )
3399+ . into_iter( )
3400+ . map( |comment| comment. id)
3401+ . collect:: <Vec <_>>( ) ,
3402+ vec![ "resolved" ]
3403+ ) ;
3404+ assert_eq ! (
3405+ filter_comments_by_search_filter( & comments, CommentSearchFilter :: Dismissed )
3406+ . into_iter( )
3407+ . map( |comment| comment. id)
3408+ . collect:: <Vec <_>>( ) ,
3409+ vec![ "dismissed" ]
3410+ ) ;
3411+ }
32143412}
0 commit comments