1+ use async_stream:: stream as async_stream;
2+ use futures:: { Stream , StreamExt , stream} ;
13use reqwest_middleware:: ClientWithMiddleware ;
24use serde:: { Deserialize , Serialize } ;
35
@@ -61,42 +63,6 @@ pub struct AzureDevOpsCommitsResponse {
6163 pub count : i64 ,
6264}
6365
64- impl RemoteEntry for AzureDevOpsCommitsResponse {
65- fn url ( _id : i64 , api_url : & str , remote : & Remote , ref_name : Option < & str > , page : i32 ) -> String {
66- let skip = page * MAX_PAGE_SIZE as i32 ;
67- // Azure DevOps format: owner should be "organization/project"
68- // and repo is the repository name
69- let mut url = format ! (
70- "{}/{}/_apis/git/repositories/{}/commits?api-version=7.1&$top={}&$skip={}" ,
71- api_url,
72- urlencoding:: encode( & remote. owner) ,
73- urlencoding:: encode( & remote. repo) ,
74- MAX_PAGE_SIZE ,
75- skip
76- ) ;
77-
78- if let Some ( ref_name) = ref_name {
79- // Azure DevOps needs versionType to distinguish between branch/tag/commit
80- // For git-cliff, ref_name is typically a tag, but could be a branch or commit
81- // We'll default to tag since that's most common with version ranges
82- url. push_str ( & format ! (
83- "&searchCriteria.itemVersion.versionType=tag&searchCriteria.itemVersion.version={}" ,
84- urlencoding:: encode( ref_name)
85- ) ) ;
86- }
87-
88- url
89- }
90-
91- fn buffer_size ( ) -> usize {
92- 10
93- }
94-
95- fn early_exit ( & self ) -> bool {
96- self . value . is_empty ( )
97- }
98- }
99-
10066/// Author/Committer of the commit.
10167#[ derive( Default , Debug , Clone , PartialEq , Serialize , Deserialize ) ]
10268pub struct AzureDevOpsCommitAuthor {
@@ -160,31 +126,6 @@ pub struct AzureDevOpsPullRequestsResponse {
160126 pub count : i64 ,
161127}
162128
163- impl RemoteEntry for AzureDevOpsPullRequestsResponse {
164- fn url ( _id : i64 , api_url : & str , remote : & Remote , _ref_name : Option < & str > , page : i32 ) -> String {
165- let skip = page * MAX_PAGE_SIZE as i32 ;
166- // Azure DevOps format: owner should be "organization/project"
167- // and repo is the repository name
168- format ! (
169- "{}/{}/_apis/git/repositories/{}/pullrequests?api-version=7.1&searchCriteria.\
170- status=completed&$top={}&$skip={}",
171- api_url,
172- urlencoding:: encode( & remote. owner) ,
173- urlencoding:: encode( & remote. repo) ,
174- MAX_PAGE_SIZE ,
175- skip
176- )
177- }
178-
179- fn buffer_size ( ) -> usize {
180- 5
181- }
182-
183- fn early_exit ( & self ) -> bool {
184- self . value . is_empty ( )
185- }
186- }
187-
188129/// Label of the pull request.
189130#[ derive( Default , Debug , Clone , PartialEq , Serialize , Deserialize ) ]
190131pub struct AzureDevOpsPullRequestLabel {
@@ -245,29 +186,127 @@ impl RemoteClient for AzureDevOpsClient {
245186}
246187
247188impl AzureDevOpsClient {
248- /// Fetches the Azure DevOps API and returns the commits.
189+ /// Constructs the URL for Azure DevOps commits API.
190+ fn commits_url ( api_url : & str , remote : & Remote , ref_name : Option < & str > , page : i32 ) -> String {
191+ let skip = page * MAX_PAGE_SIZE as i32 ;
192+ let mut url = format ! (
193+ "{}/{}/_apis/git/repositories/{}/commits?api-version=7.1&$top={}&$skip={}" ,
194+ api_url,
195+ urlencoding:: encode( & remote. owner) ,
196+ urlencoding:: encode( & remote. repo) ,
197+ MAX_PAGE_SIZE ,
198+ skip
199+ ) ;
200+
201+ if let Some ( ref_name) = ref_name {
202+ url. push_str ( & format ! (
203+ "&searchCriteria.itemVersion.versionType=tag&searchCriteria.itemVersion.version={}" ,
204+ urlencoding:: encode( ref_name)
205+ ) ) ;
206+ }
207+
208+ url
209+ }
210+
211+ /// Constructs the URL for Azure DevOps pull requests API.
212+ fn pull_requests_url ( api_url : & str , remote : & Remote , page : i32 ) -> String {
213+ let skip = page * MAX_PAGE_SIZE as i32 ;
214+ format ! (
215+ "{}/{}/_apis/git/repositories/{}/pullrequests?api-version=7.1&searchCriteria.\
216+ status=completed&$top={}&$skip={}",
217+ api_url,
218+ urlencoding:: encode( & remote. owner) ,
219+ urlencoding:: encode( & remote. repo) ,
220+ MAX_PAGE_SIZE ,
221+ skip
222+ )
223+ }
224+
225+ /// Fetches the complete list of commits.
226+ /// This is inefficient for large repositories; consider using
227+ /// `get_commit_stream` instead.
249228 pub async fn get_commits ( & self , ref_name : Option < & str > ) -> Result < Vec < Box < dyn RemoteCommit > > > {
250- Ok ( self
251- . fetch_with_early_exit :: < AzureDevOpsCommitsResponse > ( 0 , ref_name)
252- . await ?
253- . into_iter ( )
254- . flat_map ( |v| v. value )
255- . map ( |v| Box :: new ( v) as Box < dyn RemoteCommit > )
256- . collect ( ) )
257- }
258-
259- /// Fetches the Azure DevOps API and returns the pull requests.
260- pub async fn get_pull_requests (
261- & self ,
229+ use futures:: TryStreamExt ;
230+ self . get_commit_stream ( ref_name) . try_collect ( ) . await
231+ }
232+
233+ /// Fetches the complete list of pull requests.
234+ /// This is inefficient for large repositories; consider using
235+ /// `get_pull_request_stream` instead.
236+ pub async fn get_pull_requests ( & self ) -> Result < Vec < Box < dyn RemotePullRequest > > > {
237+ use futures:: TryStreamExt ;
238+ self . get_pull_request_stream ( ) . try_collect ( ) . await
239+ }
240+
241+ fn get_commit_stream < ' a > (
242+ & ' a self ,
262243 ref_name : Option < & str > ,
263- ) -> Result < Vec < Box < dyn RemotePullRequest > > > {
264- Ok ( self
265- . fetch_with_early_exit :: < AzureDevOpsPullRequestsResponse > ( 0 , ref_name)
266- . await ?
267- . into_iter ( )
268- . flat_map ( |v| v. value )
269- . map ( |v| Box :: new ( v) as Box < dyn RemotePullRequest > )
270- . collect ( ) )
244+ ) -> impl Stream < Item = Result < Box < dyn RemoteCommit > > > + ' a {
245+ let ref_name = ref_name. map ( |s| s. to_string ( ) ) ;
246+ async_stream ! {
247+ let page_stream = stream:: iter( 0 ..)
248+ . map( |page| {
249+ let ref_name = ref_name. clone( ) ;
250+ async move {
251+ let url = Self :: commits_url( & self . api_url( ) , & self . remote( ) , ref_name. as_deref( ) , page) ;
252+ self . get_json:: <AzureDevOpsCommitsResponse >( & url) . await
253+ }
254+ } )
255+ . buffered( 10 ) ;
256+
257+ let mut page_stream = Box :: pin( page_stream) ;
258+
259+ while let Some ( page_result) = page_stream. next( ) . await {
260+ match page_result {
261+ Ok ( response) => {
262+ if response. value. is_empty( ) {
263+ break ;
264+ }
265+
266+ for commit in response. value {
267+ yield Ok ( Box :: new( commit) as Box <dyn RemoteCommit >) ;
268+ }
269+ }
270+ Err ( e) => {
271+ yield Err ( e) ;
272+ break ;
273+ }
274+ }
275+ }
276+ }
277+ }
278+
279+ fn get_pull_request_stream < ' a > (
280+ & ' a self ,
281+ ) -> impl Stream < Item = Result < Box < dyn RemotePullRequest > > > + ' a {
282+ async_stream ! {
283+ let page_stream = stream:: iter( 0 ..)
284+ . map( |page| async move {
285+ let url = Self :: pull_requests_url( & self . api_url( ) , & self . remote( ) , page) ;
286+ self . get_json:: <AzureDevOpsPullRequestsResponse >( & url) . await
287+ } )
288+ . buffered( 5 ) ;
289+
290+ let mut page_stream = Box :: pin( page_stream) ;
291+
292+ while let Some ( page_result) = page_stream. next( ) . await {
293+ match page_result {
294+ Ok ( response) => {
295+ if response. value. is_empty( ) {
296+ break ;
297+ }
298+
299+ for pr in response. value {
300+ yield Ok ( Box :: new( pr) as Box <dyn RemotePullRequest >) ;
301+ }
302+ }
303+ Err ( e) => {
304+ yield Err ( e) ;
305+ break ;
306+ }
307+ }
308+ }
309+ }
271310 }
272311}
273312
@@ -278,7 +317,7 @@ mod test {
278317
279318 use super :: * ;
280319 use crate :: config:: Remote ;
281- use crate :: remote:: { RemoteCommit , RemoteEntry , RemotePullRequest } ;
320+ use crate :: remote:: { RemoteCommit , RemotePullRequest } ;
282321
283322 #[ test]
284323 fn timestamp ( ) {
@@ -369,7 +408,7 @@ mod test {
369408 }
370409
371410 #[ test]
372- fn commits_response_url ( ) {
411+ fn commits_url ( ) {
373412 let remote = Remote {
374413 owner : String :: from ( "myorg/myproject" ) ,
375414 repo : String :: from ( "myrepo" ) ,
@@ -379,7 +418,7 @@ mod test {
379418 native_tls : None ,
380419 } ;
381420
382- let url = AzureDevOpsCommitsResponse :: url ( 0 , "https://dev.azure.com" , & remote, None , 0 ) ;
421+ let url = AzureDevOpsClient :: commits_url ( "https://dev.azure.com" , & remote, None , 0 ) ;
383422
384423 assert_eq ! (
385424 "https://dev.azure.com/myorg%2Fmyproject/_apis/git/repositories/myrepo/commits?api-version=7.1&$top=100&$skip=0" ,
@@ -388,7 +427,7 @@ mod test {
388427 }
389428
390429 #[ test]
391- fn commits_response_url_with_tag ( ) {
430+ fn commits_url_with_tag ( ) {
392431 let remote = Remote {
393432 owner : String :: from ( "myorg/myproject" ) ,
394433 repo : String :: from ( "myrepo" ) ,
@@ -399,14 +438,14 @@ mod test {
399438 } ;
400439
401440 let url =
402- AzureDevOpsCommitsResponse :: url ( 0 , "https://dev.azure.com" , & remote, Some ( "v1.0.0" ) , 0 ) ;
441+ AzureDevOpsClient :: commits_url ( "https://dev.azure.com" , & remote, Some ( "v1.0.0" ) , 0 ) ;
403442
404443 assert ! ( url. contains( "searchCriteria.itemVersion.versionType=tag" ) ) ;
405444 assert ! ( url. contains( "searchCriteria.itemVersion.version=v1.0.0" ) ) ;
406445 }
407446
408447 #[ test]
409- fn commits_response_url_pagination ( ) {
448+ fn commits_url_pagination ( ) {
410449 let remote = Remote {
411450 owner : String :: from ( "org/proj" ) ,
412451 repo : String :: from ( "repo" ) ,
@@ -416,14 +455,14 @@ mod test {
416455 native_tls : None ,
417456 } ;
418457
419- let url = AzureDevOpsCommitsResponse :: url ( 0 , "https://dev.azure.com" , & remote, None , 2 ) ;
458+ let url = AzureDevOpsClient :: commits_url ( "https://dev.azure.com" , & remote, None , 2 ) ;
420459
421460 assert ! ( url. contains( "$skip=200" ) ) ;
422461 assert ! ( url. contains( "$top=100" ) ) ;
423462 }
424463
425464 #[ test]
426- fn pull_requests_response_url ( ) {
465+ fn pull_requests_url ( ) {
427466 let remote = Remote {
428467 owner : String :: from ( "myorg/myproject" ) ,
429468 repo : String :: from ( "myrepo" ) ,
@@ -433,62 +472,14 @@ mod test {
433472 native_tls : None ,
434473 } ;
435474
436- let url =
437- AzureDevOpsPullRequestsResponse :: url ( 0 , "https://dev.azure.com" , & remote, None , 0 ) ;
475+ let url = AzureDevOpsClient :: pull_requests_url ( "https://dev.azure.com" , & remote, 0 ) ;
438476
439477 assert ! ( url. contains( "pullrequests" ) ) ;
440478 assert ! ( url. contains( "searchCriteria.status=completed" ) ) ;
441479 assert ! ( url. contains( "$top=100" ) ) ;
442480 assert ! ( url. contains( "$skip=0" ) ) ;
443481 }
444482
445- #[ test]
446- fn commits_response_early_exit ( ) {
447- let empty_response = AzureDevOpsCommitsResponse {
448- value : vec ! [ ] ,
449- count : 0 ,
450- } ;
451- assert ! ( empty_response. early_exit( ) ) ;
452-
453- let non_empty_response = AzureDevOpsCommitsResponse {
454- value : vec ! [ AzureDevOpsCommit {
455- commit_id: String :: from( "abc" ) ,
456- author: None ,
457- committer: None ,
458- } ] ,
459- count : 1 ,
460- } ;
461- assert ! ( !non_empty_response. early_exit( ) ) ;
462- }
463-
464- #[ test]
465- fn pull_requests_response_early_exit ( ) {
466- let empty_response = AzureDevOpsPullRequestsResponse {
467- value : vec ! [ ] ,
468- count : 0 ,
469- } ;
470- assert ! ( empty_response. early_exit( ) ) ;
471-
472- let non_empty_response = AzureDevOpsPullRequestsResponse {
473- value : vec ! [ AzureDevOpsPullRequest {
474- pull_request_id: 1 ,
475- title: None ,
476- status: String :: from( "completed" ) ,
477- created_by: None ,
478- last_merge_commit: None ,
479- labels: vec![ ] ,
480- } ] ,
481- count : 1 ,
482- } ;
483- assert ! ( !non_empty_response. early_exit( ) ) ;
484- }
485-
486- #[ test]
487- fn buffer_sizes ( ) {
488- assert_eq ! ( 10 , AzureDevOpsCommitsResponse :: buffer_size( ) ) ;
489- assert_eq ! ( 5 , AzureDevOpsPullRequestsResponse :: buffer_size( ) ) ;
490- }
491-
492483 #[ test]
493484 fn client_try_from_remote ( ) {
494485 let remote = Remote {
0 commit comments