1use bon::bon;
15use futures::TryStreamExt;
16use futures::stream::{Stream, StreamExt};
17use serde::Deserialize;
18use url::Url;
19
20use super::diff::{self, HFFileDiff};
21use super::{HFRepository, RepoType};
22use crate::client::encode_ref;
23use crate::error::HFResult;
24use crate::{constants, retry};
25
26#[derive(Debug, Clone, Deserialize)]
31pub struct CommitAuthor {
32 pub user: Option<String>,
34 pub name: Option<String>,
36 pub email: Option<String>,
38}
39
40#[derive(Debug, Clone, Deserialize)]
44pub struct GitCommitInfo {
45 pub id: String,
47 pub authors: Vec<CommitAuthor>,
49 pub date: Option<String>,
51 pub title: String,
53 pub message: String,
55 #[serde(default, rename = "formattedTitle")]
58 pub formatted_title: Option<String>,
59 #[serde(default, rename = "formattedMessage")]
62 pub formatted_message: Option<String>,
63 #[serde(default)]
65 pub parents: Vec<String>,
66}
67
68#[derive(Debug, Clone, Deserialize)]
70#[serde(rename_all = "camelCase")]
71pub struct GitRefInfo {
72 pub name: String,
74 #[serde(rename = "ref")]
76 pub git_ref: String,
77 pub target_commit: String,
79}
80
81#[derive(Debug, Clone, Deserialize)]
85#[serde(rename_all = "camelCase")]
86pub struct GitRefs {
87 pub branches: Vec<GitRefInfo>,
89 pub tags: Vec<GitRefInfo>,
91 #[serde(default)]
93 pub converts: Vec<GitRefInfo>,
94 #[serde(default, rename = "pullRequests")]
96 pub pull_requests: Vec<GitRefInfo>,
97}
98
99#[derive(Debug, Clone, Deserialize)]
104#[serde(rename_all = "camelCase")]
105pub struct DiffEntry {
106 pub path: Option<String>,
108 pub old_path: Option<String>,
110 pub status: Option<String>,
112}
113
114#[bon]
115impl<T: RepoType> HFRepository<T> {
116 #[builder(finish_fn = send, derive(Debug, Clone))]
125 pub fn list_commits(
126 &self,
127 #[builder(into)]
129 revision: Option<String>,
130 limit: Option<usize>,
132 ) -> HFResult<impl Stream<Item = HFResult<GitCommitInfo>> + '_> {
133 let revision = revision.as_deref().unwrap_or(constants::DEFAULT_REVISION);
134 let url_str = format!(
135 "{}/commits/{}",
136 self.hf_client.api_url(self.repo_type.plural(), &self.repo_path()),
137 encode_ref(revision)
138 );
139 let url = Url::parse(&url_str)?;
140 Ok(self.hf_client.paginate(url, vec![], limit))
141 }
142
143 #[builder(finish_fn = send, derive(Debug, Clone))]
151 pub async fn list_refs(
152 &self,
153 #[builder(default)]
155 include_pull_requests: bool,
156 ) -> HFResult<GitRefs> {
157 let url = format!("{}/refs", self.hf_client.api_url(self.repo_type.plural(), &self.repo_path()));
158 let mut query: Vec<(&str, String)> = Vec::new();
159 if include_pull_requests {
160 query.push(("include_prs", "1".into()));
161 }
162
163 let headers = self.hf_client.auth_headers();
164 let response = retry::retry(self.hf_client.retry_config(), || {
165 self.hf_client
166 .http_client()
167 .get(&url)
168 .headers(headers.clone())
169 .query(&query)
170 .send()
171 })
172 .await?;
173
174 let repo_path = self.repo_path();
175 let response = self
176 .hf_client
177 .check_response(response, Some(&repo_path), crate::error::NotFoundContext::Repo)
178 .await?;
179 Ok(response.json().await?)
180 }
181
182 #[builder(finish_fn = send, derive(Debug, Clone))]
198 pub async fn get_commit_diff(
199 &self,
200 compare: &str,
206 ) -> HFResult<String> {
207 let url = format!(
208 "{}/compare/{}",
209 self.hf_client.api_url(self.repo_type.plural(), &self.repo_path()),
210 encode_ref(compare)
211 );
212
213 let headers = self.hf_client.auth_headers();
214 let response = retry::retry(self.hf_client.retry_config(), || {
215 self.hf_client.http_client().get(&url).headers(headers.clone()).send()
216 })
217 .await?;
218
219 let repo_path = self.repo_path();
220 let response = self
221 .hf_client
222 .check_response(response, Some(&repo_path), crate::error::NotFoundContext::Repo)
223 .await?;
224 Ok(response.text().await?)
225 }
226
227 #[builder(finish_fn = send, derive(Debug, Clone))]
242 pub async fn get_raw_diff(
243 &self,
244 compare: &str,
250 ) -> HFResult<String> {
251 let url = format!(
252 "{}/compare/{}",
253 self.hf_client.api_url(self.repo_type.plural(), &self.repo_path()),
254 encode_ref(compare)
255 );
256
257 let headers = self.hf_client.auth_headers();
258 let response = retry::retry(self.hf_client.retry_config(), || {
259 self.hf_client
260 .http_client()
261 .get(&url)
262 .headers(headers.clone())
263 .query(&[("raw", "true")])
264 .send()
265 })
266 .await?;
267
268 let repo_path = self.repo_path();
269 let response = self
270 .hf_client
271 .check_response(response, Some(&repo_path), crate::error::NotFoundContext::Repo)
272 .await?;
273 Ok(response.text().await?)
274 }
275
276 #[builder(finish_fn = send, derive(Debug, Clone))]
290 pub async fn get_raw_diff_stream(
291 &self,
292 #[builder(into)]
298 compare: String,
299 ) -> HFResult<impl Stream<Item = HFResult<HFFileDiff>> + '_> {
300 let url = format!(
301 "{}/compare/{}",
302 self.hf_client.api_url(self.repo_type.plural(), &self.repo_path()),
303 encode_ref(&compare)
304 );
305
306 let headers = self.hf_client.auth_headers();
307 let response = retry::retry(self.hf_client.retry_config(), || {
308 self.hf_client
309 .http_client()
310 .get(&url)
311 .headers(headers.clone())
312 .query(&[("raw", "true")])
313 .send()
314 })
315 .await?;
316
317 let repo_path = self.repo_path();
318 let response = self
319 .hf_client
320 .check_response(response, Some(&repo_path), crate::error::NotFoundContext::Repo)
321 .await?;
322 let byte_stream = response.bytes_stream().map(|r| r.map_err(std::io::Error::other));
323 Ok(diff::stream_raw_diff(byte_stream).map_err(Into::into))
324 }
325
326 #[builder(finish_fn = send, derive(Debug, Clone))]
335 pub async fn create_branch(
336 &self,
337 branch: &str,
339 #[builder(into)]
341 revision: Option<String>,
342 ) -> HFResult<()> {
343 let url = format!(
344 "{}/branch/{}",
345 self.hf_client.api_url(self.repo_type.plural(), &self.repo_path()),
346 encode_ref(branch)
347 );
348
349 let mut body = serde_json::Map::new();
350 if let Some(rev) = revision {
351 body.insert("startingPoint".into(), serde_json::Value::String(rev));
352 }
353
354 let headers = self.hf_client.auth_headers();
355 let response = retry::retry(self.hf_client.retry_config(), || {
356 self.hf_client
357 .http_client()
358 .post(&url)
359 .headers(headers.clone())
360 .json(&body)
361 .send()
362 })
363 .await?;
364
365 let repo_path = self.repo_path();
366 self.hf_client
367 .check_response(response, Some(&repo_path), crate::error::NotFoundContext::Repo)
368 .await?;
369 Ok(())
370 }
371
372 #[builder(finish_fn = send, derive(Debug, Clone))]
380 pub async fn delete_branch(
381 &self,
382 branch: &str,
384 ) -> HFResult<()> {
385 let url = format!(
386 "{}/branch/{}",
387 self.hf_client.api_url(self.repo_type.plural(), &self.repo_path()),
388 encode_ref(branch)
389 );
390
391 let headers = self.hf_client.auth_headers();
392 let response = retry::retry(self.hf_client.retry_config(), || {
393 self.hf_client.http_client().delete(&url).headers(headers.clone()).send()
394 })
395 .await?;
396
397 let repo_path = self.repo_path();
398 self.hf_client
399 .check_response(response, Some(&repo_path), crate::error::NotFoundContext::Repo)
400 .await?;
401 Ok(())
402 }
403
404 #[builder(finish_fn = send, derive(Debug, Clone))]
414 pub async fn create_tag(
415 &self,
416 tag: &str,
418 revision: Option<&str>,
420 #[builder(into)]
422 message: Option<String>,
423 ) -> HFResult<()> {
424 let revision = revision.unwrap_or(constants::DEFAULT_REVISION);
425 let url = format!(
426 "{}/tag/{}",
427 self.hf_client.api_url(self.repo_type.plural(), &self.repo_path()),
428 encode_ref(revision)
429 );
430
431 let mut body = serde_json::json!({ "tag": tag });
432 if let Some(m) = message {
433 body["message"] = serde_json::Value::String(m);
434 }
435
436 let headers = self.hf_client.auth_headers();
437 let response = retry::retry(self.hf_client.retry_config(), || {
438 self.hf_client
439 .http_client()
440 .post(&url)
441 .headers(headers.clone())
442 .json(&body)
443 .send()
444 })
445 .await?;
446
447 let repo_path = self.repo_path();
448 self.hf_client
449 .check_response(response, Some(&repo_path), crate::error::NotFoundContext::Repo)
450 .await?;
451 Ok(())
452 }
453
454 #[builder(finish_fn = send, derive(Debug, Clone))]
462 pub async fn delete_tag(
463 &self,
464 tag: &str,
466 ) -> HFResult<()> {
467 let url =
468 format!("{}/tag/{}", self.hf_client.api_url(self.repo_type.plural(), &self.repo_path()), encode_ref(tag));
469
470 let headers = self.hf_client.auth_headers();
471 let response = retry::retry(self.hf_client.retry_config(), || {
472 self.hf_client.http_client().delete(&url).headers(headers.clone()).send()
473 })
474 .await?;
475
476 let repo_path = self.repo_path();
477 self.hf_client
478 .check_response(response, Some(&repo_path), crate::error::NotFoundContext::Repo)
479 .await?;
480 Ok(())
481 }
482}
483
484#[cfg(feature = "blocking")]
485#[bon]
486impl<T: RepoType> crate::blocking::HFRepositorySync<T> {
487 #[builder(finish_fn = send, derive(Debug, Clone))]
490 pub fn list_commits(
491 &self,
492 #[builder(into)] revision: Option<String>,
493 limit: Option<usize>,
494 ) -> HFResult<Vec<GitCommitInfo>> {
495 self.runtime.block_on(async move {
496 let stream = self.inner.list_commits().maybe_revision(revision).maybe_limit(limit).send()?;
497 futures::pin_mut!(stream);
498 let mut items = Vec::new();
499 while let Some(item) = stream.next().await {
500 items.push(item?);
501 }
502 Ok(items)
503 })
504 }
505
506 #[builder(finish_fn = send, derive(Debug, Clone))]
509 pub fn list_refs(&self, #[builder(default)] include_pull_requests: bool) -> HFResult<GitRefs> {
510 self.runtime
511 .block_on(self.inner.list_refs().include_pull_requests(include_pull_requests).send())
512 }
513
514 #[builder(finish_fn = send, derive(Debug, Clone))]
517 pub fn get_commit_diff(&self, compare: &str) -> HFResult<String> {
518 self.runtime.block_on(self.inner.get_commit_diff().compare(compare).send())
519 }
520
521 #[builder(finish_fn = send, derive(Debug, Clone))]
524 pub fn get_raw_diff(&self, compare: &str) -> HFResult<String> {
525 self.runtime.block_on(self.inner.get_raw_diff().compare(compare).send())
526 }
527
528 #[builder(finish_fn = send, derive(Debug, Clone))]
531 pub fn get_raw_diff_stream(&self, #[builder(into)] compare: String) -> HFResult<Vec<HFFileDiff>> {
532 self.runtime.block_on(async move {
533 let stream = self.inner.get_raw_diff_stream().compare(compare).send().await?;
534 futures::pin_mut!(stream);
535 let mut items = Vec::new();
536 while let Some(item) = stream.next().await {
537 items.push(item?);
538 }
539 Ok(items)
540 })
541 }
542
543 #[builder(finish_fn = send, derive(Debug, Clone))]
546 pub fn create_branch(&self, branch: &str, #[builder(into)] revision: Option<String>) -> HFResult<()> {
547 self.runtime
548 .block_on(self.inner.create_branch().branch(branch).maybe_revision(revision).send())
549 }
550
551 #[builder(finish_fn = send, derive(Debug, Clone))]
554 pub fn delete_branch(&self, branch: &str) -> HFResult<()> {
555 self.runtime.block_on(self.inner.delete_branch().branch(branch).send())
556 }
557
558 #[builder(finish_fn = send, derive(Debug, Clone))]
561 pub fn create_tag(
562 &self,
563 tag: &str,
564 revision: Option<&str>,
565 #[builder(into)] message: Option<String>,
566 ) -> HFResult<()> {
567 self.runtime.block_on(
568 self.inner
569 .create_tag()
570 .tag(tag)
571 .maybe_revision(revision)
572 .maybe_message(message)
573 .send(),
574 )
575 }
576
577 #[builder(finish_fn = send, derive(Debug, Clone))]
580 pub fn delete_tag(&self, tag: &str) -> HFResult<()> {
581 self.runtime.block_on(self.inner.delete_tag().tag(tag).send())
582 }
583}
584
585#[cfg(test)]
586mod tests {
587 use super::GitCommitInfo;
588
589 #[test]
590 fn test_git_commit_info_with_formatted_fields() {
591 let json = r#"{
592 "id":"abc123",
593 "authors":[{"user":"u","name":"User","email":"u@x"}],
594 "date":"2025-01-01T00:00:00Z",
595 "title":"feat: add thing",
596 "message":"feat: add thing\n\nLong body.",
597 "formattedTitle":"<p>feat: add thing</p>",
598 "formattedMessage":"<p>feat: add thing</p><p>Long body.</p>",
599 "parents":["p1"]
600 }"#;
601 let info: GitCommitInfo = serde_json::from_str(json).unwrap();
602 assert_eq!(info.formatted_title.as_deref(), Some("<p>feat: add thing</p>"));
603 assert_eq!(info.formatted_message.as_deref(), Some("<p>feat: add thing</p><p>Long body.</p>"));
604 assert_eq!(info.parents, vec!["p1"]);
605 }
606
607 #[test]
608 fn test_git_commit_info_without_formatted_fields() {
609 let json = r#"{"id":"abc","authors":[],"date":null,"title":"t","message":"m"}"#;
610 let info: GitCommitInfo = serde_json::from_str(json).unwrap();
611 assert!(info.formatted_title.is_none());
612 assert!(info.formatted_message.is_none());
613 }
614}