From d0b8edd9609fafbdec84851d146d9f2138abb38d Mon Sep 17 00:00:00 2001 From: Dmytro Horskyi Date: Mon, 11 Mar 2024 23:26:11 +0200 Subject: [PATCH 01/12] added comment handler for /repos/{OWNER}/{REPO}/pulls/comments/{COMMENT_ID} --- src/api/checks.rs | 1 - src/api/pulls.rs | 31 ++++++-- src/api/pulls/comment.rs | 74 ++++++++++++++++++ tests/pull_request_review_comment_test.rs | 77 +++++++++++++++++++ .../pull_request_review_comment.json | 56 ++++++++++++++ 5 files changed, 231 insertions(+), 8 deletions(-) create mode 100644 tests/pull_request_review_comment_test.rs create mode 100644 tests/resources/pull_request_review_comment.json diff --git a/src/api/checks.rs b/src/api/checks.rs index a722be65..79d95d2b 100644 --- a/src/api/checks.rs +++ b/src/api/checks.rs @@ -1,5 +1,4 @@ use chrono::{DateTime, Utc}; -use hyper::body::Body; use crate::models::checks::{AutoTriggerCheck, CheckSuite, CheckSuitePreferences}; use crate::models::{AppId, CheckRunId, CheckSuiteId}; diff --git a/src/api/pulls.rs b/src/api/pulls.rs index 12718eb6..da7750ee 100644 --- a/src/api/pulls.rs +++ b/src/api/pulls.rs @@ -1,17 +1,11 @@ //! The pull request API. -mod comment; -mod create; -mod list; -mod merge; -mod update; - use http::request::Builder; use http::{Method, Uri}; - use snafu::ResultExt; use crate::error::HttpSnafu; +use crate::models::CommentId; use crate::{Octocrab, Page}; pub use self::{ @@ -19,6 +13,12 @@ pub use self::{ update::UpdatePullRequestBuilder, }; +mod comment; +mod create; +mod list; +mod merge; +mod update; + /// A client to GitHub's pull request API. /// /// Created with [`Octocrab::pulls`]. @@ -369,6 +369,23 @@ impl<'octo> PullRequestHandler<'octo> { comment::ListCommentsBuilder::new(self, pr) } + //creates a new `CommentBuilder` for GET/PATCH/DELETE requests + // to the `/repos/{owner}/{repo}/pulls/{pr}/comments/{comment_id}` endpoint + /// ```no_run + /// use octocrab::models::CommentId; + /// async fn run() -> octocrab::Result { + /// let octocrab = octocrab::Octocrab::default(); + /// let _ = octocrab.pulls("owner", "repo").comment(CommentId(21)).delete(); + /// let _ = octocrab.pulls("owner", "repo").comment(CommentId(42)).update("new comment"); + /// let comment = octocrab.pulls("owner", "repo").comment(CommentId(42)).get(); + /// + /// Ok(comment) + /// } + /// ``` + pub fn comment(&self, comment_id: CommentId) -> comment::CommentBuilder { + comment::CommentBuilder::new(self, comment_id) + } + /// Creates a new `MergePullRequestsBuilder` that can be configured used to /// merge a pull request. /// ```no_run diff --git a/src/api/pulls/comment.rs b/src/api/pulls/comment.rs index df4506e6..4122ced0 100644 --- a/src/api/pulls/comment.rs +++ b/src/api/pulls/comment.rs @@ -1,3 +1,7 @@ +use serde_json::json; + +use crate::models::pulls::Comment; + use super::*; /// A builder pattern struct for listing comments. @@ -84,6 +88,76 @@ impl<'octo, 'b> ListCommentsBuilder<'octo, 'b> { } } +/// A builder pattern struct for working with specific comment. +/// +/// created by [`PullRequestHandler::comment`] +/// +/// [`PullRequestHandler::comment`]: ./struct.PullRequestHandler.html#method.comment +#[derive(serde::Serialize)] +pub struct CommentBuilder<'octo, 'b> { + #[serde(skip)] + handler: &'b PullRequestHandler<'octo>, + comment_id: CommentId, +} + +impl<'octo, 'b> CommentBuilder<'octo, 'b> { + pub(crate) fn new(handler: &'b PullRequestHandler<'octo>, comment_id: CommentId) -> Self { + Self { + handler, + comment_id, + } + } + + ///https://docs.github.com/en/rest/pulls/comments?apiVersion=2022-11-28#get-a-review-comment-for-a-pull-request + pub async fn get(self) -> crate::Result { + self.handler + .crab + .get( + format!( + "/repos/{owner}/{repo}/pulls/comments/{comment_id}", + owner = self.handler.owner, + repo = self.handler.repo, + comment_id = self.comment_id + ), + None::<&Comment>, + ) + .await + } + + ///https://docs.github.com/en/rest/pulls/comments?apiVersion=2022-11-28#update-a-review-comment-for-a-pull-request + pub async fn update(self, comment: &str) -> crate::Result { + self.handler + .crab + .patch( + format!( + "/repos/{owner}/{repo}/pulls/comments/{comment_id}", + owner = self.handler.owner, + repo = self.handler.repo, + comment_id = self.comment_id + ), + Some(&json!({ "body": comment })), + ) + .await + } + + ///https://docs.github.com/en/rest/pulls/comments?apiVersion=2022-11-28#delete-a-review-comment-for-a-pull-request + pub async fn delete(self) -> crate::Result<()> { + self.handler + .crab + ._delete( + format!( + "/repos/{owner}/{repo}/pulls/comments/{comment_id}", + owner = self.handler.owner, + repo = self.handler.repo, + comment_id = self.comment_id + ), + None::<&()>, + ) + .await?; + Ok(()) + } +} + #[cfg(test)] mod tests { #[tokio::test] diff --git a/tests/pull_request_review_comment_test.rs b/tests/pull_request_review_comment_test.rs new file mode 100644 index 00000000..c062b8f1 --- /dev/null +++ b/tests/pull_request_review_comment_test.rs @@ -0,0 +1,77 @@ +use wiremock::matchers::{method, path}; +use wiremock::{Mock, MockServer, ResponseTemplate}; + +use octocrab::models::pulls::Comment; +use octocrab::models::CommentId; +use octocrab::Octocrab; + +use crate::mock_error::setup_error_handler; + +/// Unit test for calls to the `/repos/{owner}/{repo}/pulls/comments/{comment_id}` endpoint +mod mock_error; + +const OWNER: &str = "XAMPPRocky"; +const REPO: &str = "octocrab"; +const COMMENT_ID: u64 = 42; + +fn setup_octocrab(uri: &str) -> Octocrab { + Octocrab::builder().base_uri(uri).unwrap().build().unwrap() +} + +#[tokio::test] +async fn should_work_with_review_comment() { + let review_comment_response: Comment = + serde_json::from_str(include_str!("resources/pull_request_review_comment.json")).unwrap(); + let template = ResponseTemplate::new(200).set_body_json(&review_comment_response); + let mock_server = MockServer::start().await; + Mock::given(method("GET")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/comments/{COMMENT_ID}" + ))) + .respond_with(template.clone()) + .mount(&mock_server) + .await; + Mock::given(method("PATCH")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/comments/{COMMENT_ID}" + ))) + .respond_with(template.clone()) + .mount(&mock_server) + .await; + Mock::given(method("DELETE")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/comments/{COMMENT_ID}" + ))) + .respond_with(ResponseTemplate::new(204)) + .mount(&mock_server) + .await; + setup_error_handler( + &mock_server, + &format!("request on /repos/{OWNER}/{REPO}/pulls/comments/{COMMENT_ID} was not received"), + ) + .await; + let client = setup_octocrab(&mock_server.uri()); + + let result = client + .pulls(OWNER, REPO) + .comment(CommentId(COMMENT_ID)) + .get() + .await; + assert_eq!(result.unwrap(), review_comment_response); + let result = client + .pulls(OWNER, REPO) + .comment(CommentId(COMMENT_ID)) + .update("test") + .await; + assert_eq!(result.unwrap(), review_comment_response); + let result = client + .pulls(OWNER, REPO) + .comment(CommentId(COMMENT_ID)) + .delete() + .await; + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); +} diff --git a/tests/resources/pull_request_review_comment.json b/tests/resources/pull_request_review_comment.json new file mode 100644 index 00000000..45c12619 --- /dev/null +++ b/tests/resources/pull_request_review_comment.json @@ -0,0 +1,56 @@ +{ + "url": "https://api.github.com/repos/octocat/Hello-World/pulls/comments/1", + "pull_request_review_id": 42, + "id": 10, + "node_id": "MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDEw", + "diff_hunk": "@@ -16,33 +16,40 @@ public class Connection : IConnection...", + "path": "file1.txt", + "position": 1, + "original_position": 4, + "commit_id": "6dcb09b5b57875f334f61aebed695e2e4193db5e", + "original_commit_id": "9c48853fa3dc5c1c3d6f1f1cd1f2743e72652840", + "in_reply_to_id": 8, + "user": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "body": "Great stuff!", + "created_at": "2011-04-14T16:00:49Z", + "updated_at": "2011-04-14T16:00:49Z", + "html_url": "https://github.com/octocat/Hello-World/pull/1#discussion-diff-1", + "pull_request_url": "https://api.github.com/repos/octocat/Hello-World/pulls/1", + "author_association": "NONE", + "_links": { + "self": { + "href": "https://api.github.com/repos/octocat/Hello-World/pulls/comments/1" + }, + "html": { + "href": "https://github.com/octocat/Hello-World/pull/1#discussion-diff-1" + }, + "pull_request": { + "href": "https://api.github.com/repos/octocat/Hello-World/pulls/1" + } + }, + "start_line": 1, + "original_start_line": 1, + "start_side": "RIGHT", + "line": 2, + "original_line": 2, + "side": "RIGHT" +} From 94d607d3a4c89fcc5dd255cab178ea9fc9907b3d Mon Sep 17 00:00:00 2001 From: Dmytro Horskyi Date: Mon, 11 Mar 2024 23:45:42 +0200 Subject: [PATCH 02/12] fixed doctest --- src/api/pulls.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/api/pulls.rs b/src/api/pulls.rs index da7750ee..68e773a7 100644 --- a/src/api/pulls.rs +++ b/src/api/pulls.rs @@ -373,13 +373,14 @@ impl<'octo> PullRequestHandler<'octo> { // to the `/repos/{owner}/{repo}/pulls/{pr}/comments/{comment_id}` endpoint /// ```no_run /// use octocrab::models::CommentId; - /// async fn run() -> octocrab::Result { + /// use octocrab::models::pulls::Comment; + /// async fn run() -> octocrab::Result { /// let octocrab = octocrab::Octocrab::default(); /// let _ = octocrab.pulls("owner", "repo").comment(CommentId(21)).delete(); /// let _ = octocrab.pulls("owner", "repo").comment(CommentId(42)).update("new comment"); - /// let comment = octocrab.pulls("owner", "repo").comment(CommentId(42)).get(); + /// let comment = octocrab.pulls("owner", "repo").comment(CommentId(42)).get().await; /// - /// Ok(comment) + /// comment /// } /// ``` pub fn comment(&self, comment_id: CommentId) -> comment::CommentBuilder { From 56172d9f0abc34ea5cf3fc6ab2124f895e6852bb Mon Sep 17 00:00:00 2001 From: Dmytro Horskyi Date: Tue, 12 Mar 2024 18:32:54 +0200 Subject: [PATCH 03/12] added 'list commits on a pull request' --- src/api/pulls.rs | 19 +++++- src/api/pulls/specific_pr.rs | 58 ++++++++++++++++ tests/pull_request_commits_test.rs | 71 ++++++++++++++++++++ tests/resources/pull_request_commits.json | 80 +++++++++++++++++++++++ 4 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 src/api/pulls/specific_pr.rs create mode 100644 tests/pull_request_commits_test.rs create mode 100644 tests/resources/pull_request_commits.json diff --git a/src/api/pulls.rs b/src/api/pulls.rs index 68e773a7..f58a9916 100644 --- a/src/api/pulls.rs +++ b/src/api/pulls.rs @@ -6,6 +6,7 @@ use snafu::ResultExt; use crate::error::HttpSnafu; use crate::models::CommentId; +use crate::pulls::specific_pr::SpecificPullRequestBuilder; use crate::{Octocrab, Page}; pub use self::{ @@ -17,6 +18,7 @@ mod comment; mod create; mod list; mod merge; +mod specific_pr; mod update; /// A client to GitHub's pull request API. @@ -369,8 +371,8 @@ impl<'octo> PullRequestHandler<'octo> { comment::ListCommentsBuilder::new(self, pr) } - //creates a new `CommentBuilder` for GET/PATCH/DELETE requests - // to the `/repos/{owner}/{repo}/pulls/{pr}/comments/{comment_id}` endpoint + ///creates a new `CommentBuilder` for GET/PATCH/DELETE requests + /// to the `/repos/{owner}/{repo}/pulls/{pr}/comments/{comment_id}` endpoint /// ```no_run /// use octocrab::models::CommentId; /// use octocrab::models::pulls::Comment; @@ -387,6 +389,19 @@ impl<'octo> PullRequestHandler<'octo> { comment::CommentBuilder::new(self, comment_id) } + /// creates a builder for the `/repos/{owner}/{repo}/pulls/{pull_number}/......` endpoint + /// working with particular pull request, e.g. + /// * /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/events + /// * /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id} + /// * /repos/{owner}/{repo}/pulls/{pull_number}/commits + /// * /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/comments + /// * /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/dismissals + /// * /repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies + /// + pub fn pull_number(&self, pull_nr: u64) -> SpecificPullRequestBuilder { + SpecificPullRequestBuilder::new(self, pull_nr) + } + /// Creates a new `MergePullRequestsBuilder` that can be configured used to /// merge a pull request. /// ```no_run diff --git a/src/api/pulls/specific_pr.rs b/src/api/pulls/specific_pr.rs new file mode 100644 index 00000000..3bf8df30 --- /dev/null +++ b/src/api/pulls/specific_pr.rs @@ -0,0 +1,58 @@ +use crate::models::repos::RepoCommit; +use crate::pulls::PullRequestHandler; +use crate::Page; + +/// A builder pattern struct for working with a specific pull request data, +/// e.g. reviews, commits, comments, etc. +/// +/// created by [`PullRequestHandler::pull_number`] +/// +/// [`PullRequestHandler::pull_number`]: ./struct.PullRequestHandler.html#method.pull_number +#[derive(serde::Serialize)] +pub struct SpecificPullRequestBuilder<'octo, 'b> { + #[serde(skip)] + handler: &'b PullRequestHandler<'octo>, + #[serde(skip)] + pr_number: u64, + #[serde(skip_serializing_if = "Option::is_none")] + per_page: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + page: Option, +} + +impl<'octo, 'b> SpecificPullRequestBuilder<'octo, 'b> { + pub(crate) fn new(handler: &'b PullRequestHandler<'octo>, pr_number: u64) -> Self { + Self { + handler, + pr_number, + per_page: None, + page: None, + } + } + + /// Results per page (max: 100, default: 30). + pub fn per_page(mut self, per_page: impl Into) -> Self { + self.per_page = Some(per_page.into()); + self + } + + /// Page number of the results to fetch. (default: 1) + pub fn page(mut self, page: impl Into) -> Self { + self.page = Some(page.into()); + self + } + + ///Lists a maximum of 250 commits for a pull request. + /// To receive a complete commit list for pull requests with more than 250 commits, + /// use the [List commits](https://docs.github.com/rest/commits/commits#list-commits) endpoint. + pub async fn commits(&self) -> crate::Result> { + let route = format!( + "/repos/{owner}/{repo}/pulls/{pr_number}/commits", + owner = self.handler.owner, + repo = self.handler.repo, + pr_number = self.pr_number + ); + self.handler.crab.get(route, Some(&self)).await + } +} diff --git a/tests/pull_request_commits_test.rs b/tests/pull_request_commits_test.rs new file mode 100644 index 00000000..7257ac88 --- /dev/null +++ b/tests/pull_request_commits_test.rs @@ -0,0 +1,71 @@ +use wiremock::{ + matchers::{method, path}, + Mock, MockServer, ResponseTemplate, +}; + +use mock_error::setup_error_handler; +use octocrab::models::repos::RepoCommit; +use octocrab::Octocrab; + +/// Unit test for calls to the `/repos/OWNER/REPO/contributors` endpoint +mod mock_error; + +const OWNER: &str = "XAMPPRocky"; +const REPO: &str = "octocrab"; +const PULL_NUMBER: u64 = 42; + +async fn setup_api(template: ResponseTemplate) -> MockServer { + let mock_server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/commits" + ))) + .respond_with(template) + .mount(&mock_server) + .await; + setup_error_handler( + &mock_server, + "GET on /repos/OWNER/REPO/pulls/{PULL_NUMBER}/commits not called", + ) + .await; + mock_server +} + +fn setup_octocrab(uri: &str) -> Octocrab { + Octocrab::builder().base_uri(uri).unwrap().build().unwrap() +} + +#[tokio::test] +async fn should_return_pull_request_commits() { + let pull_request_commits_response: Vec = + serde_json::from_str(include_str!("resources/pull_request_commits.json")).unwrap(); + let template = ResponseTemplate::new(200).set_body_json(&pull_request_commits_response); + let mock_server = setup_api(template).await; + let client = setup_octocrab(&mock_server.uri()); + + let result = client + .pulls(OWNER, REPO) + .pull_number(PULL_NUMBER) + .page(0u32) + .commits() + .await; + + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); + + let commits = result.unwrap(); + + assert!(!commits.items.is_empty()); + assert!(!commits.items.first().unwrap().author.is_none()); + assert!(!commits.items.first().unwrap().committer.is_none()); + + let RepoCommit { author, .. } = commits.items.first().unwrap(); + + { + assert_eq!(author.clone().unwrap().login, "octocat"); + } +} diff --git a/tests/resources/pull_request_commits.json b/tests/resources/pull_request_commits.json new file mode 100644 index 00000000..1c39d6e1 --- /dev/null +++ b/tests/resources/pull_request_commits.json @@ -0,0 +1,80 @@ +[ + { + "url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", + "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", + "node_id": "MDY6Q29tbWl0NmRjYjA5YjViNTc4NzVmMzM0ZjYxYWViZWQ2OTVlMmU0MTkzZGI1ZQ==", + "html_url": "https://github.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e", + "comments_url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/comments", + "commit": { + "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", + "author": { + "name": "Monalisa Octocat", + "email": "support@github.com", + "date": "2011-04-14T16:00:49Z" + }, + "committer": { + "name": "Monalisa Octocat", + "email": "support@github.com", + "date": "2011-04-14T16:00:49Z" + }, + "message": "Fix all the bugs", + "tree": { + "url": "https://api.github.com/repos/octocat/Hello-World/tree/6dcb09b5b57875f334f61aebed695e2e4193db5e", + "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e" + }, + "comment_count": 0, + "verification": { + "verified": false, + "reason": "unsigned", + "signature": null, + "payload": null + } + }, + "author": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "committer": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "parents": [ + { + "url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", + "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e" + } + ] + } +] From ac954f84b61d6d7452bce59104e39f9af07b70e3 Mon Sep 17 00:00:00 2001 From: Dmytro Horskyi Date: Fri, 15 Mar 2024 16:04:08 +0200 Subject: [PATCH 04/12] added particular review operations (/repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}) --- src/api/pulls/specific_pr.rs | 8 +- src/api/pulls/specific_pr/pr_reviews.rs | 30 +++++++ .../specific_pr/pr_reviews/specific_review.rs | 65 +++++++++++++++ src/models/pulls.rs | 3 + tests/pull_request_commits_test.rs | 4 +- tests/pull_request_review_operations_test.rs | 79 +++++++++++++++++++ tests/resources/get_pull_request_review.json | 39 +++++++++ 7 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 src/api/pulls/specific_pr/pr_reviews.rs create mode 100644 src/api/pulls/specific_pr/pr_reviews/specific_review.rs create mode 100644 tests/pull_request_review_operations_test.rs create mode 100644 tests/resources/get_pull_request_review.json diff --git a/src/api/pulls/specific_pr.rs b/src/api/pulls/specific_pr.rs index 3bf8df30..17ff93ca 100644 --- a/src/api/pulls/specific_pr.rs +++ b/src/api/pulls/specific_pr.rs @@ -1,7 +1,10 @@ use crate::models::repos::RepoCommit; +use crate::pulls::specific_pr::pr_reviews::ReviewsBuilder; use crate::pulls::PullRequestHandler; use crate::Page; +mod pr_reviews; + /// A builder pattern struct for working with a specific pull request data, /// e.g. reviews, commits, comments, etc. /// @@ -12,7 +15,6 @@ use crate::Page; pub struct SpecificPullRequestBuilder<'octo, 'b> { #[serde(skip)] handler: &'b PullRequestHandler<'octo>, - #[serde(skip)] pr_number: u64, #[serde(skip_serializing_if = "Option::is_none")] per_page: Option, @@ -55,4 +57,8 @@ impl<'octo, 'b> SpecificPullRequestBuilder<'octo, 'b> { ); self.handler.crab.get(route, Some(&self)).await } + + pub fn reviews(&self) -> ReviewsBuilder<'octo, '_> { + ReviewsBuilder::new(self.handler, self.pr_number) + } } diff --git a/src/api/pulls/specific_pr/pr_reviews.rs b/src/api/pulls/specific_pr/pr_reviews.rs new file mode 100644 index 00000000..89d18d37 --- /dev/null +++ b/src/api/pulls/specific_pr/pr_reviews.rs @@ -0,0 +1,30 @@ +use crate::pulls::specific_pr::pr_reviews::specific_review::SpecificReviewBuilder; +use crate::pulls::PullRequestHandler; + +mod specific_review; + +#[derive(serde::Serialize)] +pub struct ReviewsBuilder<'octo, 'b> { + #[serde(skip)] + handler: &'b PullRequestHandler<'octo>, + #[serde(skip_serializing_if = "Option::is_none")] + per_page: Option, + #[serde(skip_serializing_if = "Option::is_none")] + page: Option, + pr_number: u64, +} + +impl<'octo, 'b> ReviewsBuilder<'octo, 'b> { + pub(crate) fn new(handler: &'b PullRequestHandler<'octo>, pr_number: u64) -> Self { + Self { + handler, + per_page: None, + page: None, + pr_number, + } + } + + pub fn review(&self, review_id: u64) -> SpecificReviewBuilder<'octo, '_> { + SpecificReviewBuilder::new(self.handler, self.pr_number, review_id) + } +} diff --git a/src/api/pulls/specific_pr/pr_reviews/specific_review.rs b/src/api/pulls/specific_pr/pr_reviews/specific_review.rs new file mode 100644 index 00000000..f3b6e45c --- /dev/null +++ b/src/api/pulls/specific_pr/pr_reviews/specific_review.rs @@ -0,0 +1,65 @@ +use crate::pulls::PullRequestHandler; + +#[derive(serde::Serialize)] +pub struct SpecificReviewBuilder<'octo, 'b> { + #[serde(skip)] + handler: &'b PullRequestHandler<'octo>, + pr_number: u64, + review_id: u64, +} + +impl<'octo, 'b> SpecificReviewBuilder<'octo, 'b> { + pub(crate) fn new( + handler: &'b PullRequestHandler<'octo>, + pr_number: u64, + review_id: u64, + ) -> Self { + Self { + handler, + pr_number, + review_id, + } + } + + ///Retrieves a pull request review by its ID. + ///see https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#get-a-review-for-a-pull-request + pub async fn get(&self) -> crate::Result { + let route = format!( + "/repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}", + owner = self.handler.owner, + repo = self.handler.repo, + pull_number = self.pr_number, + review_id = self.review_id + ); + self.handler.crab.get(route, Some(&self)).await + } + + ///Updates the contents of a specified review summary comment. + ///see https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#update-a-review-for-a-pull-request + pub async fn update( + &self, + body: impl Into, + ) -> crate::Result { + let route = format!( + "/repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}", + owner = self.handler.owner, + repo = self.handler.repo, + pull_number = self.pr_number, + review_id = self.review_id + ); + self.handler.crab.patch(route, Some(&body.into())).await + } + + ///Deletes a pull request review that has not been submitted. Submitted reviews cannot be deleted. + ///see https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#delete-a-pending-review-for-a-pull-request + pub async fn delete_pending(&self) -> crate::Result { + let route = format!( + "/repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}", + owner = self.handler.owner, + repo = self.handler.repo, + pull_number = self.pr_number, + review_id = self.review_id + ); + self.handler.crab.delete(route, None::<&()>).await + } +} diff --git a/src/models/pulls.rs b/src/models/pulls.rs index 71600cc1..7553c047 100644 --- a/src/models/pulls.rs +++ b/src/models/pulls.rs @@ -229,6 +229,9 @@ pub struct Review { #[serde(rename = "_links")] #[serde(skip_serializing_if = "Option::is_none")] pub links: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub author_association: Option, + } #[derive(Debug, Copy, Clone, PartialEq, Serialize)] diff --git a/tests/pull_request_commits_test.rs b/tests/pull_request_commits_test.rs index 7257ac88..8e096928 100644 --- a/tests/pull_request_commits_test.rs +++ b/tests/pull_request_commits_test.rs @@ -60,8 +60,8 @@ async fn should_return_pull_request_commits() { let commits = result.unwrap(); assert!(!commits.items.is_empty()); - assert!(!commits.items.first().unwrap().author.is_none()); - assert!(!commits.items.first().unwrap().committer.is_none()); + assert!(commits.items.first().unwrap().author.is_some()); + assert!(commits.items.first().unwrap().committer.is_some()); let RepoCommit { author, .. } = commits.items.first().unwrap(); diff --git a/tests/pull_request_review_operations_test.rs b/tests/pull_request_review_operations_test.rs new file mode 100644 index 00000000..557561a7 --- /dev/null +++ b/tests/pull_request_review_operations_test.rs @@ -0,0 +1,79 @@ +use wiremock::{Mock, MockServer, ResponseTemplate}; +use wiremock::matchers::{method, path}; + +use octocrab::models::pulls::Review; +use octocrab::Octocrab; + +use crate::mock_error::setup_error_handler; + +/// Unit test for calls to the `/repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}` endpoint +mod mock_error; + +const OWNER: &str = "XAMPPRocky"; +const REPO: &str = "octocrab"; +const PULL_NUMBER: u64 = 42; +const REVIEW_ID: u64 = 42; + +fn setup_octocrab(uri: &str) -> Octocrab { + Octocrab::builder().base_uri(uri).unwrap().build().unwrap() +} + +#[tokio::test] +async fn should_work_with_specific_review() { + let review_ops_response: Review = + serde_json::from_str(include_str!("resources/get_pull_request_review.json")).unwrap(); + let template = ResponseTemplate::new(200).set_body_json(&review_ops_response); + let mock_server = MockServer::start().await; + Mock::given(method("GET")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/reviews/{REVIEW_ID}" + ))) + .respond_with(template.clone()) + .mount(&mock_server) + .await; + Mock::given(method("PATCH")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/reviews/{REVIEW_ID}" + ))) + .respond_with(template.clone()) + .mount(&mock_server) + .await; + Mock::given(method("DELETE")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/reviews/{REVIEW_ID}" + ))) + .respond_with(template.clone()) + .mount(&mock_server) + .await; + setup_error_handler( + &mock_server, + &format!("request on /repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/reviews/{REVIEW_ID} was not received"), + ) + .await; + let client = setup_octocrab(&mock_server.uri()); + + let result = client + .pulls(OWNER, REPO) + .pull_number(PULL_NUMBER) + .reviews() + .review(REVIEW_ID) + .get() + .await; + assert_eq!(result.unwrap(), review_ops_response); + let result = client + .pulls(OWNER, REPO) + .pull_number(PULL_NUMBER) + .reviews() + .review(REVIEW_ID) + .update("test") + .await; + assert_eq!(result.unwrap(), review_ops_response); + let result = client + .pulls(OWNER, REPO) + .pull_number(PULL_NUMBER) + .reviews() + .review(REVIEW_ID) + .delete_pending() + .await; + assert_eq!(result.unwrap(), review_ops_response); +} diff --git a/tests/resources/get_pull_request_review.json b/tests/resources/get_pull_request_review.json new file mode 100644 index 00000000..bb3f74eb --- /dev/null +++ b/tests/resources/get_pull_request_review.json @@ -0,0 +1,39 @@ +{ + "id": 80, + "node_id": "MDE3OlB1bGxSZXF1ZXN0UmV2aWV3ODA=", + "user": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "body": "Here is the body for the review.", + "state": "APPROVED", + "html_url": "https://github.com/octocat/Hello-World/pull/12#pullrequestreview-80", + "pull_request_url": "https://api.github.com/repos/octocat/Hello-World/pulls/12", + "_links": { + "html": { + "href": "https://github.com/octocat/Hello-World/pull/12#pullrequestreview-80" + }, + "pull_request": { + "href": "https://api.github.com/repos/octocat/Hello-World/pulls/12" + } + }, + "submitted_at": "2019-11-17T17:43:43Z", + "commit_id": "ecdd80bb57125d7ba9641ffaa4d7d2c19d3f3091", + "author_association": "COLLABORATOR" +} From 38c6886a573885d645e2b473025d15dd12475e8c Mon Sep 17 00:00:00 2001 From: Dmytro Horskyi Date: Fri, 15 Mar 2024 16:07:55 +0200 Subject: [PATCH 05/12] fixed cargo fmt --- src/models/pulls.rs | 1 - tests/pull_request_review_operations_test.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/models/pulls.rs b/src/models/pulls.rs index 7553c047..56e4a18d 100644 --- a/src/models/pulls.rs +++ b/src/models/pulls.rs @@ -231,7 +231,6 @@ pub struct Review { pub links: Option, #[serde(skip_serializing_if = "Option::is_none")] pub author_association: Option, - } #[derive(Debug, Copy, Clone, PartialEq, Serialize)] diff --git a/tests/pull_request_review_operations_test.rs b/tests/pull_request_review_operations_test.rs index 557561a7..25675cbc 100644 --- a/tests/pull_request_review_operations_test.rs +++ b/tests/pull_request_review_operations_test.rs @@ -1,5 +1,5 @@ -use wiremock::{Mock, MockServer, ResponseTemplate}; use wiremock::matchers::{method, path}; +use wiremock::{Mock, MockServer, ResponseTemplate}; use octocrab::models::pulls::Review; use octocrab::Octocrab; From d6df86bb8bcf7e90ac1b31b121ba2e4715ea1a8d Mon Sep 17 00:00:00 2001 From: Dmytro Horskyi Date: Sun, 17 Mar 2024 00:34:15 +0200 Subject: [PATCH 06/12] added "Submit a review for a pull request" /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/events --- .../specific_pr/pr_reviews/specific_review.rs | 26 ++++++++++++++++++- src/models/pulls.rs | 9 +++++++ tests/pull_request_review_operations_test.rs | 17 +++++++++++- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/api/pulls/specific_pr/pr_reviews/specific_review.rs b/src/api/pulls/specific_pr/pr_reviews/specific_review.rs index f3b6e45c..8ed4f97f 100644 --- a/src/api/pulls/specific_pr/pr_reviews/specific_review.rs +++ b/src/api/pulls/specific_pr/pr_reviews/specific_review.rs @@ -1,3 +1,4 @@ +use crate::models::pulls::{Review, ReviewAction}; use crate::pulls::PullRequestHandler; #[derive(serde::Serialize)] @@ -52,7 +53,7 @@ impl<'octo, 'b> SpecificReviewBuilder<'octo, 'b> { ///Deletes a pull request review that has not been submitted. Submitted reviews cannot be deleted. ///see https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#delete-a-pending-review-for-a-pull-request - pub async fn delete_pending(&self) -> crate::Result { + pub async fn delete_pending(&self) -> crate::Result { let route = format!( "/repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}", owner = self.handler.owner, @@ -62,4 +63,27 @@ impl<'octo, 'b> SpecificReviewBuilder<'octo, 'b> { ); self.handler.crab.delete(route, None::<&()>).await } + + ///Submits a pending review for a pull request. + ///see https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#submit-a-review-for-a-pull-request + pub async fn submit( + &self, + action: ReviewAction, + body: impl Into, + ) -> crate::Result { + let route = format!( + "/repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/events", + owner = self.handler.owner, + repo = self.handler.repo, + pull_number = self.pr_number, + review_id = self.review_id + ); + self.handler + .crab + .post( + route, + Some(&serde_json::json!({ "body": body.into(), "event": action })), + ) + .await + } } diff --git a/src/models/pulls.rs b/src/models/pulls.rs index 56e4a18d..256a9529 100644 --- a/src/models/pulls.rs +++ b/src/models/pulls.rs @@ -245,6 +245,15 @@ pub enum ReviewState { Dismissed, } +#[derive(Debug, Copy, Clone, PartialEq, Serialize)] +#[serde(rename_all(serialize = "SCREAMING_SNAKE_CASE"))] +#[non_exhaustive] +pub enum ReviewAction { + Approve, + RequestChanges, + Comment, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[non_exhaustive] pub struct Comment { diff --git a/tests/pull_request_review_operations_test.rs b/tests/pull_request_review_operations_test.rs index 25675cbc..6bf145f0 100644 --- a/tests/pull_request_review_operations_test.rs +++ b/tests/pull_request_review_operations_test.rs @@ -1,7 +1,7 @@ use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; -use octocrab::models::pulls::Review; +use octocrab::models::pulls::{Review, ReviewAction}; use octocrab::Octocrab; use crate::mock_error::setup_error_handler; @@ -45,6 +45,13 @@ async fn should_work_with_specific_review() { .respond_with(template.clone()) .mount(&mock_server) .await; + Mock::given(method("POST")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/reviews/{REVIEW_ID}/events" + ))) + .respond_with(template.clone()) + .mount(&mock_server) + .await; setup_error_handler( &mock_server, &format!("request on /repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/reviews/{REVIEW_ID} was not received"), @@ -76,4 +83,12 @@ async fn should_work_with_specific_review() { .delete_pending() .await; assert_eq!(result.unwrap(), review_ops_response); + let result = client + .pulls(OWNER, REPO) + .pull_number(PULL_NUMBER) + .reviews() + .review(REVIEW_ID) + .submit(ReviewAction::Comment, "test") + .await; + assert_eq!(result.unwrap(), review_ops_response); } From 1a59acc3020419e5a847eb5e5f70797090335368 Mon Sep 17 00:00:00 2001 From: Dmytro Horskyi Date: Sun, 17 Mar 2024 00:41:18 +0200 Subject: [PATCH 07/12] added "Dismiss a review for a pull request" https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#dismiss-a-review-for-a-pull-request --- .../specific_pr/pr_reviews/specific_review.rs | 19 +++++++++++++++++++ tests/pull_request_review_operations_test.rs | 15 +++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/api/pulls/specific_pr/pr_reviews/specific_review.rs b/src/api/pulls/specific_pr/pr_reviews/specific_review.rs index 8ed4f97f..81a696ad 100644 --- a/src/api/pulls/specific_pr/pr_reviews/specific_review.rs +++ b/src/api/pulls/specific_pr/pr_reviews/specific_review.rs @@ -86,4 +86,23 @@ impl<'octo, 'b> SpecificReviewBuilder<'octo, 'b> { ) .await } + + ///Dismisses a specified review on a pull request. + ///see https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#dismiss-a-review-for-a-pull-request + pub async fn dismiss(&self, message: impl Into) -> crate::Result { + let route = format!( + "/repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/dismissals", + owner = self.handler.owner, + repo = self.handler.repo, + pull_number = self.pr_number, + review_id = self.review_id + ); + self.handler + .crab + .put( + route, + Some(&serde_json::json!({ "message": message.into(), "event": "DISMISS" })), + ) + .await + } } diff --git a/tests/pull_request_review_operations_test.rs b/tests/pull_request_review_operations_test.rs index 6bf145f0..c8a20cba 100644 --- a/tests/pull_request_review_operations_test.rs +++ b/tests/pull_request_review_operations_test.rs @@ -52,6 +52,13 @@ async fn should_work_with_specific_review() { .respond_with(template.clone()) .mount(&mock_server) .await; + Mock::given(method("PUT")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/reviews/{REVIEW_ID}/dismissals" + ))) + .respond_with(template.clone()) + .mount(&mock_server) + .await; setup_error_handler( &mock_server, &format!("request on /repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/reviews/{REVIEW_ID} was not received"), @@ -91,4 +98,12 @@ async fn should_work_with_specific_review() { .submit(ReviewAction::Comment, "test") .await; assert_eq!(result.unwrap(), review_ops_response); + let result = client + .pulls(OWNER, REPO) + .pull_number(PULL_NUMBER) + .reviews() + .review(REVIEW_ID) + .dismiss("test") + .await; + assert_eq!(result.unwrap(), review_ops_response); } From 151027a9e8e778b1fb70880bcd642bb6dc4aef11 Mon Sep 17 00:00:00 2001 From: Dmytro Horskyi Date: Sun, 17 Mar 2024 12:47:12 +0200 Subject: [PATCH 08/12] added "List comments for a pull request review" https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#list-comments-for-a-pull-request-review --- .../specific_pr/pr_reviews/specific_review.rs | 9 ++++ .../specific_review/list_comments.rs | 53 +++++++++++++++++++ src/models/commits.rs | 1 + src/models/pulls.rs | 48 +++++++++++++++++ tests/pull_request_review_operations_test.rs | 27 +++++++++- .../get_pull_request_review_comments.json | 52 ++++++++++++++++++ 6 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 src/api/pulls/specific_pr/pr_reviews/specific_review/list_comments.rs create mode 100644 tests/resources/get_pull_request_review_comments.json diff --git a/src/api/pulls/specific_pr/pr_reviews/specific_review.rs b/src/api/pulls/specific_pr/pr_reviews/specific_review.rs index 81a696ad..c91a380d 100644 --- a/src/api/pulls/specific_pr/pr_reviews/specific_review.rs +++ b/src/api/pulls/specific_pr/pr_reviews/specific_review.rs @@ -1,6 +1,9 @@ use crate::models::pulls::{Review, ReviewAction}; +use crate::pulls::specific_pr::pr_reviews::specific_review::list_comments::ListReviewCommentsBuilder; use crate::pulls::PullRequestHandler; +mod list_comments; + #[derive(serde::Serialize)] pub struct SpecificReviewBuilder<'octo, 'b> { #[serde(skip)] @@ -105,4 +108,10 @@ impl<'octo, 'b> SpecificReviewBuilder<'octo, 'b> { ) .await } + + ///Lists comments for a specific pull request review. + ///see https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#list-comments-for-a-pull-request-review + pub fn list_comments(&self) -> ListReviewCommentsBuilder<'octo, '_> { + ListReviewCommentsBuilder::new(self.handler, self.pr_number, self.review_id) + } } diff --git a/src/api/pulls/specific_pr/pr_reviews/specific_review/list_comments.rs b/src/api/pulls/specific_pr/pr_reviews/specific_review/list_comments.rs new file mode 100644 index 00000000..11e60be8 --- /dev/null +++ b/src/api/pulls/specific_pr/pr_reviews/specific_review/list_comments.rs @@ -0,0 +1,53 @@ +use super::*; + +#[derive(serde::Serialize)] +pub struct ListReviewCommentsBuilder<'octo, 'b> { + #[serde(skip)] + handler: &'b PullRequestHandler<'octo>, + pr_number: u64, + review_id: u64, + #[serde(skip_serializing_if = "Option::is_none")] + per_page: Option, + #[serde(skip_serializing_if = "Option::is_none")] + page: Option, +} + +impl<'octo, 'b> ListReviewCommentsBuilder<'octo, 'b> { + pub(crate) fn new( + handler: &'b PullRequestHandler<'octo>, + pr_number: u64, + review_id: u64, + ) -> Self { + Self { + handler, + pr_number, + review_id, + per_page: None, + page: None, + } + } + + /// Results per page (max 100). + pub fn per_page(mut self, per_page: impl Into) -> Self { + self.per_page = Some(per_page.into()); + self + } + + /// Page number of the results to fetch. + pub fn page(mut self, page: impl Into) -> Self { + self.page = Some(page.into()); + self + } + + /// Sends the actual request. + pub async fn send(self) -> crate::Result> { + let route = format!( + "/repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/comments", + owner = self.handler.owner, + repo = self.handler.repo, + pull_number = self.pr_number, + review_id = self.review_id + ); + self.handler.crab.get(route, Some(&self)).await + } +} diff --git a/src/models/commits.rs b/src/models/commits.rs index b22d10fb..42fe74b2 100644 --- a/src/models/commits.rs +++ b/src/models/commits.rs @@ -5,6 +5,7 @@ use super::{reactions::ReactionContent, *}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[non_exhaustive] pub struct Comment { + // TODO check actuality comparing with github json schema and pulls::ReviewComment pub html_url: Url, pub url: Url, pub id: CommentId, diff --git a/src/models/pulls.rs b/src/models/pulls.rs index 256a9529..4f5ace9d 100644 --- a/src/models/pulls.rs +++ b/src/models/pulls.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::commits::CommentReactions; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[non_exhaustive] @@ -285,6 +286,53 @@ pub struct Comment { pub side: Option, } +///Legacy Review Comment +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[non_exhaustive] +pub struct ReviewComment { + pub url: Url, + pub pull_request_review_id: Option, + pub id: CommentId, + pub node_id: String, + pub diff_hunk: String, + pub path: String, + pub position: Option, + pub original_position: Option, + pub commit_id: String, + pub original_commit_id: String, + #[serde(default)] + pub in_reply_to_id: Option, + pub user: Option, + pub body: String, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub html_url: String, + pub pull_request_url: String, + pub author_association: AuthorAssociation, + #[serde(rename = "_links")] + pub links: Links, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_text: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_html: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub reactions: Option, + pub side: Option, + pub start_side: Option, + pub line: Option, + pub original_line: Option, + pub start_line: Option, + pub original_start_line: Option, +} + +#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all(serialize = "SCREAMING_SNAKE_CASE"))] +#[non_exhaustive] +pub enum Side { + Left, + Right, +} + /// A Thread in a pull request review #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[non_exhaustive] diff --git a/tests/pull_request_review_operations_test.rs b/tests/pull_request_review_operations_test.rs index c8a20cba..0cfa32a3 100644 --- a/tests/pull_request_review_operations_test.rs +++ b/tests/pull_request_review_operations_test.rs @@ -1,7 +1,7 @@ use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; -use octocrab::models::pulls::{Review, ReviewAction}; +use octocrab::models::pulls::{Review, ReviewAction, ReviewComment}; use octocrab::Octocrab; use crate::mock_error::setup_error_handler; @@ -22,7 +22,12 @@ fn setup_octocrab(uri: &str) -> Octocrab { async fn should_work_with_specific_review() { let review_ops_response: Review = serde_json::from_str(include_str!("resources/get_pull_request_review.json")).unwrap(); + let review_comments_response: Vec = serde_json::from_str(include_str!( + "resources/get_pull_request_review_comments.json" + )) + .unwrap(); let template = ResponseTemplate::new(200).set_body_json(&review_ops_response); + let comments_template = ResponseTemplate::new(200).set_body_json(&review_comments_response); let mock_server = MockServer::start().await; Mock::given(method("GET")) .and(path(format!( @@ -59,6 +64,14 @@ async fn should_work_with_specific_review() { .respond_with(template.clone()) .mount(&mock_server) .await; + Mock::given(method("GET")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/reviews/{REVIEW_ID}/comments" + ))) + .respond_with(comments_template.clone()) + .mount(&mock_server) + .await; + setup_error_handler( &mock_server, &format!("request on /repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/reviews/{REVIEW_ID} was not received"), @@ -106,4 +119,16 @@ async fn should_work_with_specific_review() { .dismiss("test") .await; assert_eq!(result.unwrap(), review_ops_response); + + let result = client + .pulls(OWNER, REPO) + .pull_number(PULL_NUMBER) + .reviews() + .review(REVIEW_ID) + .list_comments() + .per_page(15) + .send() + .await; + let result_items = result.unwrap(); + assert_eq!(result_items.items, review_comments_response); } diff --git a/tests/resources/get_pull_request_review_comments.json b/tests/resources/get_pull_request_review_comments.json new file mode 100644 index 00000000..2c266910 --- /dev/null +++ b/tests/resources/get_pull_request_review_comments.json @@ -0,0 +1,52 @@ +[ + { + "url": "https://api.github.com/repos/octocat/Hello-World/pulls/comments/1", + "pull_request_review_id": 42, + "id": 10, + "node_id": "MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDEw", + "diff_hunk": "@@ -16,33 +16,40 @@ public class Connection : IConnection...", + "path": "file1.txt", + "position": 1, + "original_position": 4, + "commit_id": "6dcb09b5b57875f334f61aebed695e2e4193db5e", + "original_commit_id": "9c48853fa3dc5c1c3d6f1f1cd1f2743e72652840", + "in_reply_to_id": 8, + "user": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "body": "Great stuff!", + "created_at": "2011-04-14T16:00:49Z", + "updated_at": "2011-04-14T16:00:49Z", + "html_url": "https://github.com/octocat/Hello-World/pull/1#discussion-diff-1", + "pull_request_url": "https://api.github.com/repos/octocat/Hello-World/pulls/1", + "author_association": "NONE", + "_links": { + "self": { + "href": "https://api.github.com/repos/octocat/Hello-World/pulls/comments/1" + }, + "html": { + "href": "https://github.com/octocat/Hello-World/pull/1#discussion-diff-1" + }, + "pull_request": { + "href": "https://api.github.com/repos/octocat/Hello-World/pulls/1" + } + } + } +] From ffca0b322291ad35d2c4a6f073f5b53bb1918753 Mon Sep 17 00:00:00 2001 From: Dmytro Horskyi Date: Sun, 17 Mar 2024 16:39:20 +0200 Subject: [PATCH 09/12] added "Create a reply for a review comment" https://docs.github.com/en/rest/pulls/comments?apiVersion=2022-11-28#create-a-reply-for-a-review-comment --- src/api/pulls/specific_pr.rs | 8 +++- src/api/pulls/specific_pr/pr_comment.rs | 41 ++++++++++++++++++++ src/models/pulls.rs | 37 +++++++++++++++++- tests/pull_request_review_operations_test.rs | 20 ++++++++++ 4 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 src/api/pulls/specific_pr/pr_comment.rs diff --git a/src/api/pulls/specific_pr.rs b/src/api/pulls/specific_pr.rs index 17ff93ca..196bdc56 100644 --- a/src/api/pulls/specific_pr.rs +++ b/src/api/pulls/specific_pr.rs @@ -1,10 +1,12 @@ use crate::models::repos::RepoCommit; +use crate::models::CommentId; +use crate::pulls::specific_pr::pr_comment::SpecificPullRequestCommentBuilder; use crate::pulls::specific_pr::pr_reviews::ReviewsBuilder; use crate::pulls::PullRequestHandler; use crate::Page; +mod pr_comment; mod pr_reviews; - /// A builder pattern struct for working with a specific pull request data, /// e.g. reviews, commits, comments, etc. /// @@ -61,4 +63,8 @@ impl<'octo, 'b> SpecificPullRequestBuilder<'octo, 'b> { pub fn reviews(&self) -> ReviewsBuilder<'octo, '_> { ReviewsBuilder::new(self.handler, self.pr_number) } + + pub fn comment(&self, comment_id: CommentId) -> SpecificPullRequestCommentBuilder { + SpecificPullRequestCommentBuilder::new(self.handler, self.pr_number, comment_id) + } } diff --git a/src/api/pulls/specific_pr/pr_comment.rs b/src/api/pulls/specific_pr/pr_comment.rs new file mode 100644 index 00000000..db687438 --- /dev/null +++ b/src/api/pulls/specific_pr/pr_comment.rs @@ -0,0 +1,41 @@ +use serde_json::json; + +use crate::models::pulls::ReviewComment; +use crate::models::CommentId; +use crate::pulls::PullRequestHandler; + +#[derive(serde::Serialize)] +pub struct SpecificPullRequestCommentBuilder<'octo, 'b> { + #[serde(skip)] + handler: &'b PullRequestHandler<'octo>, + pr_number: u64, + comment_id: CommentId, +} + +impl<'octo, 'b> SpecificPullRequestCommentBuilder<'octo, 'b> { + pub(crate) fn new( + handler: &'b PullRequestHandler<'octo>, + pr_number: u64, + comment_id: CommentId, + ) -> Self { + Self { + handler, + comment_id, + pr_number, + } + } + + pub async fn reply(&self, comment: impl Into) -> crate::Result { + let route = format!( + "/repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies", + owner = self.handler.owner, + repo = self.handler.repo, + pull_number = self.pr_number, + comment_id = self.comment_id + ); + self.handler + .crab + .post(route, Some(&json!({ "body": comment.into() }))) + .await + } +} diff --git a/src/models/pulls.rs b/src/models/pulls.rs index 4f5ace9d..7a8e7ccf 100644 --- a/src/models/pulls.rs +++ b/src/models/pulls.rs @@ -325,7 +325,7 @@ pub struct ReviewComment { pub original_start_line: Option, } -#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, PartialEq, Serialize)] #[serde(rename_all(serialize = "SCREAMING_SNAKE_CASE"))] #[non_exhaustive] pub enum Side { @@ -379,6 +379,41 @@ impl<'de> Deserialize<'de> for ReviewState { } } +//same, see above +impl<'de> Deserialize<'de> for Side { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = Side; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + Ok(match value.to_uppercase().as_str() { + "LEFT" => Side::Left, + "RIGHT" => Side::Right, + unknown => { + return Err(E::custom(format!( + "unknown variant `{unknown}`, expected one of `left`, `right`" + ))) + } + }) + } + } + + deserializer.deserialize_str(Visitor) + } +} + #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] #[non_exhaustive] diff --git a/tests/pull_request_review_operations_test.rs b/tests/pull_request_review_operations_test.rs index 0cfa32a3..06ac116f 100644 --- a/tests/pull_request_review_operations_test.rs +++ b/tests/pull_request_review_operations_test.rs @@ -13,6 +13,7 @@ const OWNER: &str = "XAMPPRocky"; const REPO: &str = "octocrab"; const PULL_NUMBER: u64 = 42; const REVIEW_ID: u64 = 42; +const COMMENT_ID: u64 = 42; fn setup_octocrab(uri: &str) -> Octocrab { Octocrab::builder().base_uri(uri).unwrap().build().unwrap() @@ -26,8 +27,11 @@ async fn should_work_with_specific_review() { "resources/get_pull_request_review_comments.json" )) .unwrap(); + let pr_comment_response: ReviewComment = + serde_json::from_str(include_str!("resources/pull_request_review_comment.json")).unwrap(); let template = ResponseTemplate::new(200).set_body_json(&review_ops_response); let comments_template = ResponseTemplate::new(200).set_body_json(&review_comments_response); + let pr_comment_template = ResponseTemplate::new(200).set_body_json(&pr_comment_response); let mock_server = MockServer::start().await; Mock::given(method("GET")) .and(path(format!( @@ -72,6 +76,14 @@ async fn should_work_with_specific_review() { .mount(&mock_server) .await; + Mock::given(method("POST")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/comments/{COMMENT_ID}/replies" + ))) + .respond_with(pr_comment_template.clone()) + .mount(&mock_server) + .await; + setup_error_handler( &mock_server, &format!("request on /repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/reviews/{REVIEW_ID} was not received"), @@ -131,4 +143,12 @@ async fn should_work_with_specific_review() { .await; let result_items = result.unwrap(); assert_eq!(result_items.items, review_comments_response); + + let result = client + .pulls(OWNER, REPO) + .pull_number(PULL_NUMBER) + .comment(COMMENT_ID.into()) + .reply("test") + .await; + assert_eq!(result.unwrap(), pr_comment_response); } From 6f975abf117fcb2dae1f40e4b13a5892e4d5889a Mon Sep 17 00:00:00 2001 From: Dmytro Horskyi Date: Sun, 17 Mar 2024 18:26:45 +0200 Subject: [PATCH 10/12] added doc comments --- src/api/pulls/specific_pr.rs | 30 +++++++ src/api/pulls/specific_pr/pr_reviews.rs | 15 ++++ .../specific_pr/pr_reviews/specific_review.rs | 87 +++++++++++++++++++ 3 files changed, 132 insertions(+) diff --git a/src/api/pulls/specific_pr.rs b/src/api/pulls/specific_pr.rs index 196bdc56..5b90193d 100644 --- a/src/api/pulls/specific_pr.rs +++ b/src/api/pulls/specific_pr.rs @@ -60,10 +60,40 @@ impl<'octo, 'b> SpecificPullRequestBuilder<'octo, 'b> { self.handler.crab.get(route, Some(&self)).await } + /// Creates a new `ReviewsBuilder` + /// ```no_run + /// # use octocrab::models::CommentId; + /// async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let _ = octocrab.pulls("owner", "repo") + /// .pull_number(42) + /// .reviews() + /// .review(42) + /// .get() + /// .await; + /// Ok(()) + /// ``` pub fn reviews(&self) -> ReviewsBuilder<'octo, '_> { ReviewsBuilder::new(self.handler, self.pr_number) } + /// Creates a new `SpecificPullRequestCommentBuilder` + /// ```no_run + /// # use octocrab::models::CommentId; + /// async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let _ = octocrab.pulls("owner", "repo") + /// .pull_number(42) + /// .comment(CommentId(42)) + /// .reply("new comment") + /// .await; + /// Ok(()) + /// } + /// ``` pub fn comment(&self, comment_id: CommentId) -> SpecificPullRequestCommentBuilder { SpecificPullRequestCommentBuilder::new(self.handler, self.pr_number, comment_id) } diff --git a/src/api/pulls/specific_pr/pr_reviews.rs b/src/api/pulls/specific_pr/pr_reviews.rs index 89d18d37..f23494c3 100644 --- a/src/api/pulls/specific_pr/pr_reviews.rs +++ b/src/api/pulls/specific_pr/pr_reviews.rs @@ -24,6 +24,21 @@ impl<'octo, 'b> ReviewsBuilder<'octo, 'b> { } } + /// Creates a new `SpecificReviewBuilder` + /// ```no_run + /// # async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let _ = octocrab.pulls("owner", "repo") + /// .pull_number(42) + /// .reviews() + /// .review(42) + /// .get() // + update, delete_pending, submit, dismiss, list_comments + /// .await?; + /// # Ok(()) + /// # } + /// ``` pub fn review(&self, review_id: u64) -> SpecificReviewBuilder<'octo, '_> { SpecificReviewBuilder::new(self.handler, self.pr_number, review_id) } diff --git a/src/api/pulls/specific_pr/pr_reviews/specific_review.rs b/src/api/pulls/specific_pr/pr_reviews/specific_review.rs index c91a380d..6e7a68f5 100644 --- a/src/api/pulls/specific_pr/pr_reviews/specific_review.rs +++ b/src/api/pulls/specific_pr/pr_reviews/specific_review.rs @@ -27,6 +27,20 @@ impl<'octo, 'b> SpecificReviewBuilder<'octo, 'b> { ///Retrieves a pull request review by its ID. ///see https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#get-a-review-for-a-pull-request + /// ```no_run + /// # async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let _ = octocrab.pulls("owner", "repo") + /// .pull_number(42) + /// .reviews() + /// .review(42) + /// .get() + /// .await?; + /// # Ok(()) + /// # } + /// ``` pub async fn get(&self) -> crate::Result { let route = format!( "/repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}", @@ -40,6 +54,20 @@ impl<'octo, 'b> SpecificReviewBuilder<'octo, 'b> { ///Updates the contents of a specified review summary comment. ///see https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#update-a-review-for-a-pull-request + /// ```no_run + /// # async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let _ = octocrab.pulls("owner", "repo") + /// .pull_number(42) + /// .reviews() + /// .review(42) + /// .update("this is a new body") + /// .await?; + /// # Ok(()) + /// # } + /// ``` pub async fn update( &self, body: impl Into, @@ -56,6 +84,20 @@ impl<'octo, 'b> SpecificReviewBuilder<'octo, 'b> { ///Deletes a pull request review that has not been submitted. Submitted reviews cannot be deleted. ///see https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#delete-a-pending-review-for-a-pull-request + /// ```no_run + /// # async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let _ = octocrab.pulls("owner", "repo") + /// .pull_number(42) + /// .reviews() + /// .review(42) + /// .delete_pending() + /// .await?; + /// # Ok(()) + /// # } + /// ``` pub async fn delete_pending(&self) -> crate::Result { let route = format!( "/repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}", @@ -69,6 +111,21 @@ impl<'octo, 'b> SpecificReviewBuilder<'octo, 'b> { ///Submits a pending review for a pull request. ///see https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#submit-a-review-for-a-pull-request + ///```no_run + /// # use octocrab::models::pulls::ReviewAction; + /// async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let _ = octocrab.pulls("owner", "repo") + /// .pull_number(42) + /// .reviews() + /// .review(42) + /// .submit(ReviewAction::RequestChanges, "comment body") + /// .await?; + /// # Ok(()) + /// # } + /// ``` pub async fn submit( &self, action: ReviewAction, @@ -92,6 +149,20 @@ impl<'octo, 'b> SpecificReviewBuilder<'octo, 'b> { ///Dismisses a specified review on a pull request. ///see https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#dismiss-a-review-for-a-pull-request + /// ```no_run + /// # async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let _ = octocrab.pulls("owner", "repo") + /// .pull_number(42) + /// .reviews() + /// .review(42) + /// .dismiss("message") + /// .await?; + /// # Ok(()) + /// # } + /// ``` pub async fn dismiss(&self, message: impl Into) -> crate::Result { let route = format!( "/repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/dismissals", @@ -111,6 +182,22 @@ impl<'octo, 'b> SpecificReviewBuilder<'octo, 'b> { ///Lists comments for a specific pull request review. ///see https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#list-comments-for-a-pull-request-review + /// ```no_run + /// # async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let _ = octocrab.pulls("owner", "repo") + /// .pull_number(42) + /// .reviews() + /// .review(42) + /// .list_comments() + /// .per_page(10) + /// .page(3.into()) + /// .await?; + /// # Ok(()) + /// # } + /// ``` pub fn list_comments(&self) -> ListReviewCommentsBuilder<'octo, '_> { ListReviewCommentsBuilder::new(self.handler, self.pr_number, self.review_id) } From 181fbc1d071ad20b47a15e435a0dd150507a4255 Mon Sep 17 00:00:00 2001 From: Dmytro Horskyi Date: Sun, 17 Mar 2024 18:36:40 +0200 Subject: [PATCH 11/12] fixed doctest --- src/api/pulls/specific_pr.rs | 1 + src/api/pulls/specific_pr/pr_reviews/specific_review.rs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/api/pulls/specific_pr.rs b/src/api/pulls/specific_pr.rs index 5b90193d..5e3ab033 100644 --- a/src/api/pulls/specific_pr.rs +++ b/src/api/pulls/specific_pr.rs @@ -74,6 +74,7 @@ impl<'octo, 'b> SpecificPullRequestBuilder<'octo, 'b> { /// .get() /// .await; /// Ok(()) + /// } /// ``` pub fn reviews(&self) -> ReviewsBuilder<'octo, '_> { ReviewsBuilder::new(self.handler, self.pr_number) diff --git a/src/api/pulls/specific_pr/pr_reviews/specific_review.rs b/src/api/pulls/specific_pr/pr_reviews/specific_review.rs index 6e7a68f5..5f9f8960 100644 --- a/src/api/pulls/specific_pr/pr_reviews/specific_review.rs +++ b/src/api/pulls/specific_pr/pr_reviews/specific_review.rs @@ -193,7 +193,8 @@ impl<'octo, 'b> SpecificReviewBuilder<'octo, 'b> { /// .review(42) /// .list_comments() /// .per_page(10) - /// .page(3.into()) + /// .page(3u32) + /// .send() /// .await?; /// # Ok(()) /// # } From 377342b0838f048b2b56b3274075bc25884577fe Mon Sep 17 00:00:00 2001 From: Dmytro Horskyi Date: Fri, 5 Apr 2024 13:07:16 +0300 Subject: [PATCH 12/12] refactored specific PR usage; tests pending --- src/api/pulls.rs | 56 +++++++++++++++++++++++++ src/api/pulls/specific_pr.rs | 2 +- src/api/pulls/specific_pr/pr_reviews.rs | 2 +- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/api/pulls.rs b/src/api/pulls.rs index f58a9916..f709e817 100644 --- a/src/api/pulls.rs +++ b/src/api/pulls.rs @@ -2,10 +2,13 @@ use http::request::Builder; use http::{Method, Uri}; +use serde_json::json; use snafu::ResultExt; use crate::error::HttpSnafu; +use crate::models::pulls::ReviewComment; use crate::models::CommentId; +use crate::pulls::specific_pr::pr_reviews::specific_review::SpecificReviewBuilder; use crate::pulls::specific_pr::SpecificPullRequestBuilder; use crate::{Octocrab, Page}; @@ -398,10 +401,63 @@ impl<'octo> PullRequestHandler<'octo> { /// * /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/dismissals /// * /repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies /// + #[deprecated( + since = "0.34.4", + note = "specific PR builder transitioned to pr_review_actions, reply_to_comment, reply_to_comment" + )] + //FIXME: remove? pub fn pull_number(&self, pull_nr: u64) -> SpecificPullRequestBuilder { SpecificPullRequestBuilder::new(self, pull_nr) } + // /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/events + // /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id} + // repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/comments + // repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/dismissals + pub fn pr_review_actions( + &self, + pull_nr: u64, + review_id: u64, + ) -> SpecificReviewBuilder<'octo, '_> { + SpecificReviewBuilder::new(self, pull_nr, review_id) + } + + /// /repos/{owner}/{repo}/pulls/{pull_number}/commits + // pub fn pr_commits(&self, pull_nr: u64) -> SpecificPullRequestCommentBuilder<'octo, '_> { + // SpecificPullRequestCommentBuilder::new(self, pull_nr, 0) + // } + + // /repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies + /// Creates a reply to a specific comment of a pull request specified in the first argument + /// ```no_run + /// # use octocrab::models::CommentId; + /// async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let page = octocrab.pulls("owner", "repo").reply_to_comment(142, CommentId(24), "This is my reply") + /// .await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn reply_to_comment( + &self, + pull_nr: u64, + comment_id: CommentId, + comment: impl Into, + ) -> crate::Result { + let route = format!( + "/repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies", + owner = self.owner, + repo = self.repo, + pull_number = pull_nr, + comment_id = comment_id + ); + self.crab + .post(route, Some(&json!({ "body": comment.into() }))) + .await + } + /// Creates a new `MergePullRequestsBuilder` that can be configured used to /// merge a pull request. /// ```no_run diff --git a/src/api/pulls/specific_pr.rs b/src/api/pulls/specific_pr.rs index 5e3ab033..b8d7af02 100644 --- a/src/api/pulls/specific_pr.rs +++ b/src/api/pulls/specific_pr.rs @@ -6,7 +6,7 @@ use crate::pulls::PullRequestHandler; use crate::Page; mod pr_comment; -mod pr_reviews; +pub(crate) mod pr_reviews; /// A builder pattern struct for working with a specific pull request data, /// e.g. reviews, commits, comments, etc. /// diff --git a/src/api/pulls/specific_pr/pr_reviews.rs b/src/api/pulls/specific_pr/pr_reviews.rs index f23494c3..e23b67c6 100644 --- a/src/api/pulls/specific_pr/pr_reviews.rs +++ b/src/api/pulls/specific_pr/pr_reviews.rs @@ -1,7 +1,7 @@ use crate::pulls::specific_pr::pr_reviews::specific_review::SpecificReviewBuilder; use crate::pulls::PullRequestHandler; -mod specific_review; +pub mod specific_review; #[derive(serde::Serialize)] pub struct ReviewsBuilder<'octo, 'b> {