@@ -1844,62 +1844,63 @@ async fn gitcode_review_target_parts(
18441844 ) ;
18451845 let initial_detail =
18461846 send_bounded_json ( gitcode_request ( client. clone ( ) , & base, ctx. token . as_deref ( ) ) ) . await ?;
1847- let token = ctx. token . clone ( ) ;
18481847 let files_url = format ! ( "{}/files" , base) ;
1849- let files = fetch_bounded_paginated_array (
1850- |page| {
1851- let page = page. to_string ( ) ;
1852- gitcode_request ( client. clone ( ) , & files_url, token. as_deref ( ) )
1853- . query ( & [ ( "per_page" , "100" ) , ( "page" , & page) ] )
1854- } ,
1855- github_next_page,
1856- MAX_REVIEW_TARGET_LIST_ITEMS ,
1857- )
1848+ let files_response = send_bounded_gitcode_files_response ( gitcode_request (
1849+ client. clone ( ) ,
1850+ & files_url,
1851+ ctx. token . as_deref ( ) ,
1852+ ) )
18581853 . await ?;
1854+ let files = files_response
1855+ . value
1856+ . as_array ( )
1857+ . ok_or_else ( || {
1858+ ReviewPlatformError :: Parse (
1859+ "GitCode pull request files response was not an array" . to_string ( ) ,
1860+ )
1861+ } ) ?
1862+ . iter ( )
1863+ . take ( MAX_REVIEW_TARGET_LIST_ITEMS )
1864+ . map ( gitcode_file_from_value)
1865+ . collect :: < Vec < _ > > ( ) ;
18591866 let confirmed_detail =
18601867 send_bounded_json ( gitcode_request ( client, & base, ctx. token . as_deref ( ) ) ) . await ?;
18611868 let initial_pull_request = gitcode_pull_request_from_value ( & initial_detail) ;
1862- let confirmed_pull_request = gitcode_pull_request_from_value ( & confirmed_detail) ;
1869+ let mut confirmed_pull_request = gitcode_pull_request_from_value ( & confirmed_detail) ;
18631870 ensure_pull_request_revisions_stable ( & initial_pull_request, & confirmed_pull_request) ?;
1864- Ok ( (
1865- confirmed_pull_request,
1866- array_items ( & files)
1867- . iter ( )
1868- . map ( gitcode_file_from_value)
1869- . collect ( ) ,
1870- ) )
1871+ apply_gitcode_review_target_file_stats ( & mut confirmed_pull_request, & files) ;
1872+ Ok ( ( confirmed_pull_request, files) )
18711873}
18721874
18731875async fn gitcode_review_file_parts (
18741876 ctx : & ProviderContext ,
18751877 pull_request_id : & str ,
18761878 file_path : & str ,
1877- file_page_hint : Option < u32 > ,
1879+ _file_page_hint : Option < u32 > ,
18781880) -> Result < ( ReviewPlatformPullRequest , Vec < ReviewPlatformFile > ) , ReviewPlatformError > {
18791881 let client = http_client ( ) ?;
18801882 let base = format ! (
18811883 "{}/repos/{}/{}/pulls/{}" ,
18821884 ctx. api_base_url, ctx. remote. owner, ctx. remote. repository_name, pull_request_id
18831885 ) ;
1884- let token = ctx. token . clone ( ) ;
18851886 let files_url = format ! ( "{}/files" , base) ;
1886- let file = fetch_bounded_paginated_file (
1887- |page| {
1888- let page = page . to_string ( ) ;
1889- gitcode_request ( client . clone ( ) , & files_url , token. as_deref ( ) )
1890- . query ( & [ ( "per_page" , "100" ) , ( "page" , & page ) ] )
1891- } ,
1892- github_next_page ,
1893- file_page_hint . unwrap_or ( 1 ) ,
1894- if file_page_hint . is_some ( ) {
1895- 100
1896- } else {
1897- MAX_REVIEW_TARGET_LIST_ITEMS
1898- } ,
1899- file_path ,
1900- gitcode_file_from_value ,
1901- )
1902- . await ? ;
1887+ let files = send_bounded_gitcode_files_response ( gitcode_request (
1888+ client . clone ( ) ,
1889+ & files_url ,
1890+ ctx . token . as_deref ( ) ,
1891+ ) )
1892+ . await ?
1893+ . value ;
1894+ let file = files
1895+ . as_array ( )
1896+ . ok_or_else ( || {
1897+ ReviewPlatformError :: Parse (
1898+ "GitCode pull request files response was not an array" . to_string ( ) ,
1899+ )
1900+ } ) ?
1901+ . iter ( )
1902+ . map ( gitcode_file_from_value )
1903+ . find ( |file| file . path == file_path || file . old_path . as_deref ( ) == Some ( file_path ) ) ;
19031904 let detail = send_bounded_json ( gitcode_request ( client, & base, ctx. token . as_deref ( ) ) ) . await ?;
19041905 Ok ( (
19051906 gitcode_pull_request_from_value ( & detail) ,
@@ -2851,7 +2852,7 @@ async fn gitcode_pull_request_detail_page(
28512852
28522853 match section {
28532854 ReviewPlatformDetailSection :: Overview => {
2854- if let Ok ( response) = send_json_response ( gitcode_request (
2855+ if let Ok ( response) = send_bounded_gitcode_files_response ( gitcode_request (
28552856 client. clone ( ) ,
28562857 & format ! ( "{}/files" , base) ,
28572858 ctx. token . as_deref ( ) ,
@@ -2866,22 +2867,21 @@ async fn gitcode_pull_request_detail_page(
28662867 ci = slice_page ( ci, pagination) ;
28672868 }
28682869 ReviewPlatformDetailSection :: Files => {
2869- if let Ok ( response) = fetch_array_page (
2870- gitcode_request (
2871- client. clone ( ) ,
2872- & format ! ( "{}/files" , base) ,
2873- ctx. token . as_deref ( ) ,
2874- ) ,
2875- pagination,
2876- )
2870+ if let Ok ( response) = send_bounded_gitcode_files_response ( gitcode_request (
2871+ client. clone ( ) ,
2872+ & format ! ( "{}/files" , base) ,
2873+ ctx. token . as_deref ( ) ,
2874+ ) )
28772875 . await
28782876 {
2879- apply_gitcode_pull_request_change_stats ( & mut pull_request, & response) ;
2880- section_pagination = pagination_from_response ( & response, pagination) ;
2881- files = array_items ( & response. value )
2882- . iter ( )
2883- . map ( gitcode_file_from_value)
2884- . collect ( ) ;
2877+ if let Some ( values) = response. value . as_array ( ) {
2878+ apply_gitcode_pull_request_change_stats ( & mut pull_request, & response) ;
2879+ section_pagination = gitcode_files_pagination ( pagination, values. len ( ) ) ;
2880+ files = slice_page (
2881+ values. iter ( ) . map ( gitcode_file_from_value) . collect ( ) ,
2882+ pagination,
2883+ ) ;
2884+ }
28852885 }
28862886 }
28872887 ReviewPlatformDetailSection :: Commits => {
@@ -3003,7 +3003,7 @@ impl ReviewProvider for GitcodeProvider {
30033003 let detail =
30043004 send_json ( gitcode_request ( client. clone ( ) , & base, ctx. token . as_deref ( ) ) ) . await ?;
30053005 let files_url = format ! ( "{}/files" , base) ;
3006- let files_response = send_json_response ( gitcode_request (
3006+ let files_response = send_bounded_gitcode_files_response ( gitcode_request (
30073007 client. clone ( ) ,
30083008 & files_url,
30093009 ctx. token . as_deref ( ) ,
@@ -3242,6 +3242,15 @@ fn review_http_error(error: ReviewHttpError) -> ReviewPlatformError {
32423242 }
32433243}
32443244
3245+ fn gitcode_files_http_error ( error : ReviewHttpError ) -> ReviewPlatformError {
3246+ match error {
3247+ ReviewHttpError :: ResponseTooLarge { limit_bytes } => ReviewPlatformError :: Api ( format ! (
3248+ "GitCode pull request files response exceeded the {limit_bytes}-byte limit"
3249+ ) ) ,
3250+ error => review_http_error ( error) ,
3251+ }
3252+ }
3253+
32453254async fn send_json ( request : ReviewHttpRequest ) -> Result < Value , ReviewPlatformError > {
32463255 send_review_json ( request) . await . map_err ( review_http_error)
32473256}
@@ -3269,6 +3278,14 @@ async fn send_bounded_json_response(
32693278 . map_err ( review_http_error)
32703279}
32713280
3281+ async fn send_bounded_gitcode_files_response (
3282+ request : ReviewHttpRequest ,
3283+ ) -> Result < JsonResponse , ReviewPlatformError > {
3284+ send_review_json_response_bounded ( request, MAX_REVIEW_TARGET_RESPONSE_BYTES )
3285+ . await
3286+ . map_err ( gitcode_files_http_error)
3287+ }
3288+
32723289async fn send_bounded_text (
32733290 request : ReviewHttpRequest ,
32743291 max_bytes : usize ,
@@ -3432,6 +3449,17 @@ fn pagination_from_total(
34323449 }
34333450}
34343451
3452+ fn gitcode_files_pagination (
3453+ pagination : PullRequestPagination ,
3454+ available_files : usize ,
3455+ ) -> ReviewPlatformPagination {
3456+ let mut result = pagination_from_total ( pagination, available_files) ;
3457+ if available_files >= GITCODE_PULL_REQUEST_FILES_RESPONSE_LIMIT {
3458+ result. total = None ;
3459+ }
3460+ result
3461+ }
3462+
34353463fn slice_page < T > ( items : Vec < T > , pagination : PullRequestPagination ) -> Vec < T > {
34363464 let start = pagination
34373465 . page
@@ -3536,7 +3564,8 @@ async fn enrich_gitcode_pull_request_change_stats(
35363564 let token = ctx. token . clone ( ) ;
35373565 async move {
35383566 if let Ok ( response) =
3539- send_json_response ( gitcode_request ( client, & url, token. as_deref ( ) ) ) . await
3567+ send_bounded_gitcode_files_response ( gitcode_request ( client, & url, token. as_deref ( ) ) )
3568+ . await
35403569 {
35413570 apply_gitcode_pull_request_change_stats ( & mut pull_request, & response) ;
35423571 }
@@ -6646,17 +6675,37 @@ fn gitcode_file_from_value(value: &Value) -> ReviewPlatformFile {
66466675 value_string ( value, "filename" ) ,
66476676 value_string ( value, "new_path" ) ,
66486677 ] ) ,
6649- old_path : value
6650- . get ( "previous_filename" )
6651- . and_then ( Value :: as_str)
6652- . map ( str:: to_string) ,
6653- status : file_status ( & value_string ( value, "status" ) ) ,
6678+ old_path : optional_string ( value, "old_path" )
6679+ . or_else ( || optional_string ( value, "previous_filename" ) ) ,
6680+ status : gitcode_file_status ( value) ,
66546681 additions : value_i64 ( value, "additions" ) as i32 ,
66556682 deletions : value_i64 ( value, "deletions" ) as i32 ,
6656- patch : optional_string ( value, "patch" ) . or_else ( || optional_string ( value, "diff" ) ) ,
6683+ patch : gitcode_patch_from_value ( value) ,
6684+ }
6685+ }
6686+
6687+ fn gitcode_file_status ( value : & Value ) -> ReviewFileStatus {
6688+ if value_bool ( value, "new_file" ) {
6689+ ReviewFileStatus :: Added
6690+ } else if value_bool ( value, "deleted_file" ) {
6691+ ReviewFileStatus :: Deleted
6692+ } else if value_bool ( value, "renamed_file" ) {
6693+ ReviewFileStatus :: Renamed
6694+ } else {
6695+ file_status ( & value_string ( value, "status" ) )
66576696 }
66586697}
66596698
6699+ fn gitcode_patch_from_value ( value : & Value ) -> Option < String > {
6700+ optional_string ( value, "patch" )
6701+ . or_else ( || {
6702+ value
6703+ . get ( "patch" )
6704+ . and_then ( |patch| optional_string ( patch, "diff" ) )
6705+ } )
6706+ . or_else ( || optional_string ( value, "diff" ) )
6707+ }
6708+
66606709fn gitlab_files ( value : & Value ) -> Vec < ReviewPlatformFile > {
66616710 value
66626711 . get ( "changes" )
@@ -7119,6 +7168,15 @@ fn apply_files_stats(pull_request: &mut ReviewPlatformPullRequest, files: &[Revi
71197168 pull_request. deletions = deletions;
71207169}
71217170
7171+ fn apply_gitcode_review_target_file_stats (
7172+ pull_request : & mut ReviewPlatformPullRequest ,
7173+ files : & [ ReviewPlatformFile ] ,
7174+ ) {
7175+ apply_files_stats ( pull_request, files) ;
7176+ pull_request. changed_files = i32:: try_from ( files. len ( ) ) . unwrap_or ( i32:: MAX ) ;
7177+ pull_request. changed_file_count_known = files. len ( ) < MAX_REVIEW_TARGET_LIST_ITEMS ;
7178+ }
7179+
71227180async fn fetch_bounded_paginated_array < F > (
71237181 mut build_request : F ,
71247182 next_page : fn ( & ReviewHttpHeaders , u32 ) -> Option < u32 > ,
@@ -8245,6 +8303,83 @@ mod tests {
82458303 assert_eq ! ( pull_request. deletions, 22 ) ;
82468304 }
82478305
8306+ #[ test]
8307+ fn gitcode_file_maps_nested_patch_and_change_flags ( ) {
8308+ let file = gitcode_file_from_value ( & json ! ( {
8309+ "filename" : "src/new.rs" ,
8310+ "old_path" : "src/old.rs" ,
8311+ "status" : null,
8312+ "new_file" : false ,
8313+ "renamed_file" : true ,
8314+ "deleted_file" : false ,
8315+ "additions" : 1 ,
8316+ "deletions" : 1 ,
8317+ "patch" : { "diff" : "@@ -1 +1 @@\n -old\n +new" }
8318+ } ) ) ;
8319+
8320+ assert_eq ! ( file. path, "src/new.rs" ) ;
8321+ assert_eq ! ( file. old_path. as_deref( ) , Some ( "src/old.rs" ) ) ;
8322+ assert_eq ! ( file. status, ReviewFileStatus :: Renamed ) ;
8323+ assert_eq ! ( file. patch. as_deref( ) , Some ( "@@ -1 +1 @@\n -old\n +new" ) ) ;
8324+ assert ! ( file_has_complete_patch( & file) ) ;
8325+ }
8326+
8327+ #[ test]
8328+ fn gitcode_capped_files_pagination_does_not_claim_exact_total ( ) {
8329+ let pagination = gitcode_files_pagination (
8330+ PullRequestPagination {
8331+ page : 60 ,
8332+ per_page : 50 ,
8333+ } ,
8334+ GITCODE_PULL_REQUEST_FILES_RESPONSE_LIMIT ,
8335+ ) ;
8336+
8337+ assert_eq ! ( pagination. total, None ) ;
8338+ assert ! ( !pagination. has_next) ;
8339+ }
8340+
8341+ #[ test]
8342+ fn gitcode_review_target_stats_use_the_same_thousand_file_budget ( ) {
8343+ let mut pull_request = gitcode_pull_request_from_value ( & json ! ( {
8344+ "number" : 5 ,
8345+ "title" : "large change" ,
8346+ "state" : "open" ,
8347+ "added_lines" : 9_999 ,
8348+ "removed_lines" : 8_888 ,
8349+ "changes_count" : "2500"
8350+ } ) ) ;
8351+ let files = vec ! [
8352+ ReviewPlatformFile {
8353+ path: "src/file.rs" . to_string( ) ,
8354+ old_path: None ,
8355+ status: ReviewFileStatus :: Modified ,
8356+ additions: 1 ,
8357+ deletions: 2 ,
8358+ patch: Some ( "@@ -1 +1 @@\n -old\n +new" . to_string( ) ) ,
8359+ } ;
8360+ MAX_REVIEW_TARGET_LIST_ITEMS
8361+ ] ;
8362+
8363+ apply_gitcode_review_target_file_stats ( & mut pull_request, & files) ;
8364+
8365+ assert_eq ! ( pull_request. changed_files, 1_000 ) ;
8366+ assert ! ( !pull_request. changed_file_count_known) ;
8367+ assert_eq ! ( pull_request. additions, 1_000 ) ;
8368+ assert_eq ! ( pull_request. deletions, 2_000 ) ;
8369+ }
8370+
8371+ #[ test]
8372+ fn gitcode_files_response_too_large_reports_explicit_reason ( ) {
8373+ let error = gitcode_files_http_error ( ReviewHttpError :: ResponseTooLarge {
8374+ limit_bytes : MAX_REVIEW_TARGET_RESPONSE_BYTES ,
8375+ } ) ;
8376+
8377+ assert_eq ! (
8378+ error. to_string( ) ,
8379+ "Provider API failed: GitCode pull request files response exceeded the 4194304-byte limit"
8380+ ) ;
8381+ }
8382+
82488383 #[ test]
82498384 fn gitcode_file_response_overrides_incomplete_list_stats ( ) {
82508385 let mut pull_request = gitcode_pull_request_from_value ( & json ! ( {
0 commit comments