Skip to content

Commit e852bfa

Browse files
committed
fix: implement changes from #1272
1 parent c2ce946 commit e852bfa

File tree

3 files changed

+136
-154
lines changed

3 files changed

+136
-154
lines changed

git-cliff-core/src/changelog.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ impl<'a> Changelog<'a> {
483483
.block_on(async {
484484
let (commits, pull_requests) = tokio::try_join!(
485485
azure_devops_client.get_commits(ref_name),
486-
azure_devops_client.get_pull_requests(ref_name)
486+
azure_devops_client.get_pull_requests()
487487
)?;
488488
log::debug!("Number of Azure DevOps commits: {}", commits.len());
489489
log::debug!(

git-cliff-core/src/remote/azure_devops.rs

Lines changed: 130 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use async_stream::stream as async_stream;
2+
use futures::{Stream, StreamExt, stream};
13
use reqwest_middleware::ClientWithMiddleware;
24
use 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)]
10268
pub 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)]
190131
pub struct AzureDevOpsPullRequestLabel {
@@ -245,29 +186,127 @@ impl RemoteClient for AzureDevOpsClient {
245186
}
246187

247188
impl 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 {

git-cliff-core/src/remote/mod.rs

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -175,20 +175,11 @@ pub trait RemoteClient {
175175
log::trace!("Response: {:?}", text);
176176
text
177177
} else {
178-
log::trace!("Response: {:?}", response_text);
179-
}
180-
181-
match serde_json::from_str::<T>(&response_text) {
182-
Ok(result) => Ok(result),
183-
Err(e) => {
184-
log::error!(
185-
"Failed to parse JSON response. Error: {}. First 500 chars of response: {}",
186-
e,
187-
response_text.chars().take(500).collect::<String>()
188-
);
189-
Err(e.into())
190-
}
191-
}
178+
let text = response.text().await?;
179+
log::error!("Request error: {}", text);
180+
text
181+
};
182+
Ok(serde_json::from_str::<T>(&response_text)?)
192183
}
193184
}
194185

0 commit comments

Comments
 (0)