1use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7use crate::client::issue::{IssueUser, Label, Milestone};
8use crate::client::InstallationClient;
9use crate::error::ApiError;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct PullRequest {
18 pub id: u64,
20
21 pub node_id: String,
23
24 pub number: u64,
26
27 pub title: String,
29
30 pub body: Option<String>,
32
33 pub state: String, pub user: IssueUser,
38
39 pub head: PullRequestBranch,
41
42 pub base: PullRequestBranch,
44
45 pub draft: bool,
47
48 pub merged: bool,
50
51 pub mergeable: Option<bool>,
53
54 pub merge_commit_sha: Option<String>,
56
57 pub assignees: Vec<IssueUser>,
59
60 pub requested_reviewers: Vec<IssueUser>,
62
63 pub labels: Vec<Label>,
65
66 pub milestone: Option<Milestone>,
68
69 pub created_at: DateTime<Utc>,
71
72 pub updated_at: DateTime<Utc>,
74
75 pub closed_at: Option<DateTime<Utc>>,
77
78 pub merged_at: Option<DateTime<Utc>>,
80
81 pub html_url: String,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct PullRequestBranch {
88 #[serde(rename = "ref")]
90 pub branch_ref: String,
91
92 pub sha: String,
94
95 pub repo: PullRequestRepo,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct PullRequestRepo {
102 pub id: u64,
104
105 pub name: String,
107
108 pub full_name: String,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct Review {
115 pub id: u64,
117
118 pub node_id: String,
120
121 pub user: IssueUser,
123
124 pub body: Option<String>,
126
127 pub state: String, pub commit_id: String,
132
133 pub submitted_at: Option<DateTime<Utc>>,
135
136 pub html_url: String,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct PullRequestComment {
143 pub id: u64,
145
146 pub node_id: String,
148
149 pub body: String,
151
152 pub user: IssueUser,
154
155 pub path: String,
157
158 pub line: Option<u64>,
160
161 pub commit_id: String,
163
164 pub created_at: DateTime<Utc>,
166
167 pub updated_at: DateTime<Utc>,
169
170 pub html_url: String,
172}
173
174#[derive(Debug, Clone, Serialize)]
176pub struct CreatePullRequestRequest {
177 pub title: String,
179
180 pub head: String,
182
183 pub base: String,
185
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub body: Option<String>,
189
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub draft: Option<bool>,
193
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub milestone: Option<u64>,
197
198 #[serde(skip_serializing_if = "Option::is_none")]
221 pub maintainer_can_modify: Option<bool>,
222}
223
224#[derive(Debug, Clone, Serialize, Default)]
226pub struct UpdatePullRequestRequest {
227 #[serde(skip_serializing_if = "Option::is_none")]
229 pub title: Option<String>,
230
231 #[serde(skip_serializing_if = "Option::is_none")]
233 pub body: Option<String>,
234
235 #[serde(skip_serializing_if = "Option::is_none")]
237 pub state: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
241 pub base: Option<String>,
242
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub milestone: Option<u64>,
246}
247
248#[derive(Debug, Clone, Serialize, Default)]
250pub struct MergePullRequestRequest {
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub commit_title: Option<String>,
254
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub commit_message: Option<String>,
258
259 #[serde(skip_serializing_if = "Option::is_none")]
261 pub sha: Option<String>,
262
263 #[serde(skip_serializing_if = "Option::is_none")]
265 pub merge_method: Option<String>, }
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct MergeResult {
271 pub merged: bool,
273
274 pub sha: String,
276
277 pub message: String,
279}
280
281#[derive(Debug, Clone, Serialize)]
283pub struct CreateReviewRequest {
284 #[serde(skip_serializing_if = "Option::is_none")]
286 pub commit_id: Option<String>,
287
288 #[serde(skip_serializing_if = "Option::is_none")]
290 pub body: Option<String>,
291
292 pub event: String, }
295
296#[derive(Debug, Clone, Serialize)]
298pub struct UpdateReviewRequest {
299 pub body: String,
301}
302
303#[derive(Debug, Clone, Serialize)]
305pub struct DismissReviewRequest {
306 pub message: String,
308}
309
310#[derive(Debug, Clone, Serialize)]
312pub struct CreatePullRequestCommentRequest {
313 pub body: String,
315}
316
317#[derive(Debug, Clone, Serialize)]
319pub struct SetPullRequestMilestoneRequest {
320 pub milestone: Option<u64>,
322}
323
324impl InstallationClient {
325 pub async fn list_pull_requests(
363 &self,
364 owner: &str,
365 repo: &str,
366 state: Option<&str>,
367 page: Option<u32>,
368 ) -> Result<crate::client::PagedResponse<PullRequest>, ApiError> {
369 let mut path = format!("/repos/{}/{}/pulls", owner, repo);
370 let mut query_params = Vec::new();
371
372 if let Some(state_value) = state {
373 query_params.push(format!("state={}", state_value));
374 }
375 if let Some(page_num) = page {
376 query_params.push(format!("page={}", page_num));
377 }
378
379 if !query_params.is_empty() {
380 path = format!("{}?{}", path, query_params.join("&"));
381 }
382
383 let response = self.get(&path).await?;
384 let status = response.status();
385
386 if !status.is_success() {
387 return Err(match status.as_u16() {
388 404 => ApiError::NotFound,
389 403 => ApiError::AuthorizationFailed,
390 401 => ApiError::AuthenticationFailed,
391 _ => {
392 let message = response
393 .text()
394 .await
395 .unwrap_or_else(|_| "Unknown error".to_string());
396 ApiError::HttpError {
397 status: status.as_u16(),
398 message,
399 }
400 }
401 });
402 }
403
404 let pagination = response
406 .headers()
407 .get("Link")
408 .and_then(|h| h.to_str().ok())
409 .map(|h| crate::client::parse_link_header(Some(h)))
410 .unwrap_or_default();
411
412 let items: Vec<PullRequest> = response.json().await.map_err(ApiError::from)?;
414
415 Ok(crate::client::PagedResponse {
416 items,
417 total_count: None, pagination,
419 })
420 }
421
422 pub async fn get_pull_request(
426 &self,
427 owner: &str,
428 repo: &str,
429 pull_number: u64,
430 ) -> Result<PullRequest, ApiError> {
431 let path = format!("/repos/{}/{}/pulls/{}", owner, repo, pull_number);
432 let response = self.get(&path).await?;
433
434 let status = response.status();
435 if !status.is_success() {
436 return Err(match status.as_u16() {
437 404 => ApiError::NotFound,
438 403 => ApiError::AuthorizationFailed,
439 401 => ApiError::AuthenticationFailed,
440 _ => {
441 let message = response
442 .text()
443 .await
444 .unwrap_or_else(|_| "Unknown error".to_string());
445 ApiError::HttpError {
446 status: status.as_u16(),
447 message,
448 }
449 }
450 });
451 }
452 response.json().await.map_err(ApiError::from)
453 }
454
455 pub async fn create_pull_request(
459 &self,
460 owner: &str,
461 repo: &str,
462 request: CreatePullRequestRequest,
463 ) -> Result<PullRequest, ApiError> {
464 let path = format!("/repos/{}/{}/pulls", owner, repo);
465 let response = self.post(&path, &request).await?;
466
467 let status = response.status();
468 if !status.is_success() {
469 return Err(match status.as_u16() {
470 422 => {
471 let message = response
472 .text()
473 .await
474 .unwrap_or_else(|_| "Validation failed".to_string());
475 ApiError::InvalidRequest { message }
476 }
477 404 => ApiError::NotFound,
478 403 => ApiError::AuthorizationFailed,
479 401 => ApiError::AuthenticationFailed,
480 _ => {
481 let message = response
482 .text()
483 .await
484 .unwrap_or_else(|_| "Unknown error".to_string());
485 ApiError::HttpError {
486 status: status.as_u16(),
487 message,
488 }
489 }
490 });
491 }
492 response.json().await.map_err(ApiError::from)
493 }
494
495 pub async fn update_pull_request(
499 &self,
500 owner: &str,
501 repo: &str,
502 pull_number: u64,
503 request: UpdatePullRequestRequest,
504 ) -> Result<PullRequest, ApiError> {
505 let path = format!("/repos/{}/{}/pulls/{}", owner, repo, pull_number);
506 let response = self.patch(&path, &request).await?;
507
508 let status = response.status();
509 if !status.is_success() {
510 return Err(match status.as_u16() {
511 422 => {
512 let message = response
513 .text()
514 .await
515 .unwrap_or_else(|_| "Validation failed".to_string());
516 ApiError::InvalidRequest { message }
517 }
518 404 => ApiError::NotFound,
519 403 => ApiError::AuthorizationFailed,
520 401 => ApiError::AuthenticationFailed,
521 _ => {
522 let message = response
523 .text()
524 .await
525 .unwrap_or_else(|_| "Unknown error".to_string());
526 ApiError::HttpError {
527 status: status.as_u16(),
528 message,
529 }
530 }
531 });
532 }
533 response.json().await.map_err(ApiError::from)
534 }
535
536 pub async fn merge_pull_request(
540 &self,
541 owner: &str,
542 repo: &str,
543 pull_number: u64,
544 request: MergePullRequestRequest,
545 ) -> Result<MergeResult, ApiError> {
546 let path = format!("/repos/{}/{}/pulls/{}/merge", owner, repo, pull_number);
547 let response = self.put(&path, &request).await?;
548
549 let status = response.status();
550 if !status.is_success() {
551 return Err(match status.as_u16() {
552 405 => {
553 let message = response
554 .text()
555 .await
556 .unwrap_or_else(|_| "Pull request not mergeable".to_string());
557 ApiError::HttpError {
558 status: 405,
559 message,
560 }
561 }
562 409 => {
563 let message = response
564 .text()
565 .await
566 .unwrap_or_else(|_| "Merge conflict".to_string());
567 ApiError::HttpError {
568 status: 409,
569 message,
570 }
571 }
572 404 => ApiError::NotFound,
573 403 => ApiError::AuthorizationFailed,
574 401 => ApiError::AuthenticationFailed,
575 _ => {
576 let message = response
577 .text()
578 .await
579 .unwrap_or_else(|_| "Unknown error".to_string());
580 ApiError::HttpError {
581 status: status.as_u16(),
582 message,
583 }
584 }
585 });
586 }
587 response.json().await.map_err(ApiError::from)
588 }
589
590 pub async fn set_pull_request_milestone(
594 &self,
595 owner: &str,
596 repo: &str,
597 pull_number: u64,
598 milestone_number: Option<u64>,
599 ) -> Result<PullRequest, ApiError> {
600 let request = UpdatePullRequestRequest {
601 milestone: milestone_number,
602 ..Default::default()
603 };
604 self.update_pull_request(owner, repo, pull_number, request)
605 .await
606 }
607
608 pub async fn list_reviews(
616 &self,
617 owner: &str,
618 repo: &str,
619 pull_number: u64,
620 ) -> Result<Vec<Review>, ApiError> {
621 let path = format!("/repos/{}/{}/pulls/{}/reviews", owner, repo, pull_number);
622 let response = self.get(&path).await?;
623
624 let status = response.status();
625 if !status.is_success() {
626 return Err(match status.as_u16() {
627 404 => ApiError::NotFound,
628 403 => ApiError::AuthorizationFailed,
629 401 => ApiError::AuthenticationFailed,
630 _ => {
631 let message = response
632 .text()
633 .await
634 .unwrap_or_else(|_| "Unknown error".to_string());
635 ApiError::HttpError {
636 status: status.as_u16(),
637 message,
638 }
639 }
640 });
641 }
642 response.json().await.map_err(ApiError::from)
643 }
644
645 pub async fn get_review(
649 &self,
650 owner: &str,
651 repo: &str,
652 pull_number: u64,
653 review_id: u64,
654 ) -> Result<Review, ApiError> {
655 let path = format!(
656 "/repos/{}/{}/pulls/{}/reviews/{}",
657 owner, repo, pull_number, review_id
658 );
659 let response = self.get(&path).await?;
660
661 let status = response.status();
662 if !status.is_success() {
663 return Err(match status.as_u16() {
664 404 => ApiError::NotFound,
665 403 => ApiError::AuthorizationFailed,
666 401 => ApiError::AuthenticationFailed,
667 _ => {
668 let message = response
669 .text()
670 .await
671 .unwrap_or_else(|_| "Unknown error".to_string());
672 ApiError::HttpError {
673 status: status.as_u16(),
674 message,
675 }
676 }
677 });
678 }
679 response.json().await.map_err(ApiError::from)
680 }
681
682 pub async fn create_review(
686 &self,
687 owner: &str,
688 repo: &str,
689 pull_number: u64,
690 request: CreateReviewRequest,
691 ) -> Result<Review, ApiError> {
692 let path = format!("/repos/{}/{}/pulls/{}/reviews", owner, repo, pull_number);
693 let response = self.post(&path, &request).await?;
694
695 let status = response.status();
696 if !status.is_success() {
697 return Err(match status.as_u16() {
698 422 => {
699 let message = response
700 .text()
701 .await
702 .unwrap_or_else(|_| "Validation failed".to_string());
703 ApiError::InvalidRequest { message }
704 }
705 404 => ApiError::NotFound,
706 403 => ApiError::AuthorizationFailed,
707 401 => ApiError::AuthenticationFailed,
708 _ => {
709 let message = response
710 .text()
711 .await
712 .unwrap_or_else(|_| "Unknown error".to_string());
713 ApiError::HttpError {
714 status: status.as_u16(),
715 message,
716 }
717 }
718 });
719 }
720 response.json().await.map_err(ApiError::from)
721 }
722
723 pub async fn update_review(
727 &self,
728 owner: &str,
729 repo: &str,
730 pull_number: u64,
731 review_id: u64,
732 request: UpdateReviewRequest,
733 ) -> Result<Review, ApiError> {
734 let path = format!(
735 "/repos/{}/{}/pulls/{}/reviews/{}",
736 owner, repo, pull_number, review_id
737 );
738 let response = self.put(&path, &request).await?;
739
740 let status = response.status();
741 if !status.is_success() {
742 return Err(match status.as_u16() {
743 422 => {
744 let message = response
745 .text()
746 .await
747 .unwrap_or_else(|_| "Validation failed".to_string());
748 ApiError::InvalidRequest { message }
749 }
750 404 => ApiError::NotFound,
751 403 => ApiError::AuthorizationFailed,
752 401 => ApiError::AuthenticationFailed,
753 _ => {
754 let message = response
755 .text()
756 .await
757 .unwrap_or_else(|_| "Unknown error".to_string());
758 ApiError::HttpError {
759 status: status.as_u16(),
760 message,
761 }
762 }
763 });
764 }
765 response.json().await.map_err(ApiError::from)
766 }
767
768 pub async fn dismiss_review(
772 &self,
773 owner: &str,
774 repo: &str,
775 pull_number: u64,
776 review_id: u64,
777 request: DismissReviewRequest,
778 ) -> Result<Review, ApiError> {
779 let path = format!(
780 "/repos/{}/{}/pulls/{}/reviews/{}/dismissals",
781 owner, repo, pull_number, review_id
782 );
783 let response = self.put(&path, &request).await?;
784
785 let status = response.status();
786 if !status.is_success() {
787 return Err(match status.as_u16() {
788 422 => {
789 let message = response
790 .text()
791 .await
792 .unwrap_or_else(|_| "Validation failed".to_string());
793 ApiError::InvalidRequest { message }
794 }
795 404 => ApiError::NotFound,
796 403 => ApiError::AuthorizationFailed,
797 401 => ApiError::AuthenticationFailed,
798 _ => {
799 let message = response
800 .text()
801 .await
802 .unwrap_or_else(|_| "Unknown error".to_string());
803 ApiError::HttpError {
804 status: status.as_u16(),
805 message,
806 }
807 }
808 });
809 }
810 response.json().await.map_err(ApiError::from)
811 }
812
813 pub async fn list_pull_request_comments(
821 &self,
822 owner: &str,
823 repo: &str,
824 pull_number: u64,
825 ) -> Result<Vec<PullRequestComment>, ApiError> {
826 let path = format!("/repos/{}/{}/pulls/{}/comments", owner, repo, pull_number);
827 let response = self.get(&path).await?;
828
829 let status = response.status();
830 if !status.is_success() {
831 return Err(match status.as_u16() {
832 404 => ApiError::NotFound,
833 403 => ApiError::AuthorizationFailed,
834 401 => ApiError::AuthenticationFailed,
835 _ => {
836 let message = response
837 .text()
838 .await
839 .unwrap_or_else(|_| "Unknown error".to_string());
840 ApiError::HttpError {
841 status: status.as_u16(),
842 message,
843 }
844 }
845 });
846 }
847 response.json().await.map_err(ApiError::from)
848 }
849
850 pub async fn create_pull_request_comment(
854 &self,
855 owner: &str,
856 repo: &str,
857 pull_number: u64,
858 request: CreatePullRequestCommentRequest,
859 ) -> Result<PullRequestComment, ApiError> {
860 let path = format!("/repos/{}/{}/pulls/{}/comments", owner, repo, pull_number);
861 let response = self.post(&path, &request).await?;
862
863 let status = response.status();
864 if !status.is_success() {
865 return Err(match status.as_u16() {
866 422 => {
867 let message = response
868 .text()
869 .await
870 .unwrap_or_else(|_| "Validation failed".to_string());
871 ApiError::InvalidRequest { message }
872 }
873 404 => ApiError::NotFound,
874 403 => ApiError::AuthorizationFailed,
875 401 => ApiError::AuthenticationFailed,
876 _ => {
877 let message = response
878 .text()
879 .await
880 .unwrap_or_else(|_| "Unknown error".to_string());
881 ApiError::HttpError {
882 status: status.as_u16(),
883 message,
884 }
885 }
886 });
887 }
888 response.json().await.map_err(ApiError::from)
889 }
890
891 pub async fn add_labels_to_pull_request(
899 &self,
900 owner: &str,
901 repo: &str,
902 pull_number: u64,
903 labels: Vec<String>,
904 ) -> Result<Vec<Label>, ApiError> {
905 let path = format!("/repos/{}/{}/issues/{}/labels", owner, repo, pull_number);
907 let response = self.post(&path, &labels).await?;
908
909 let status = response.status();
910 if !status.is_success() {
911 return Err(match status.as_u16() {
912 422 => {
913 let message = response
914 .text()
915 .await
916 .unwrap_or_else(|_| "Validation failed".to_string());
917 ApiError::InvalidRequest { message }
918 }
919 404 => ApiError::NotFound,
920 403 => ApiError::AuthorizationFailed,
921 401 => ApiError::AuthenticationFailed,
922 _ => {
923 let message = response
924 .text()
925 .await
926 .unwrap_or_else(|_| "Unknown error".to_string());
927 ApiError::HttpError {
928 status: status.as_u16(),
929 message,
930 }
931 }
932 });
933 }
934 response.json().await.map_err(ApiError::from)
935 }
936
937 pub async fn remove_label_from_pull_request(
941 &self,
942 owner: &str,
943 repo: &str,
944 pull_number: u64,
945 name: &str,
946 ) -> Result<(), ApiError> {
947 let path = format!(
949 "/repos/{}/{}/issues/{}/labels/{}",
950 owner, repo, pull_number, name
951 );
952 let response = self.delete(&path).await?;
953
954 let status = response.status();
955 if !status.is_success() {
956 return Err(match status.as_u16() {
957 404 => ApiError::NotFound,
958 403 => ApiError::AuthorizationFailed,
959 401 => ApiError::AuthenticationFailed,
960 _ => {
961 let message = response
962 .text()
963 .await
964 .unwrap_or_else(|_| "Unknown error".to_string());
965 ApiError::HttpError {
966 status: status.as_u16(),
967 message,
968 }
969 }
970 });
971 }
972 Ok(())
973 }
974}
975
976#[cfg(test)]
977#[path = "pull_request_tests.rs"]
978mod tests;