Skip to content

Commit 459a03a

Browse files
authored
Merge pull request #2364 from Urgau/gh-changes-since-review-comment
Handle view changes link for body-less reviews with review comments
2 parents 0adf321 + 52bfca0 commit 459a03a

2 files changed

Lines changed: 149 additions & 14 deletions

File tree

src/github/issue.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,28 @@ impl Issue {
237237
Ok(())
238238
}
239239

240+
pub async fn edit_review_comment(
241+
&self,
242+
client: &GithubClient,
243+
id: u64,
244+
new_body: &str,
245+
) -> anyhow::Result<Comment> {
246+
let comment_url = format!("{}/pulls/comments/{}", self.repository().url(client), id);
247+
#[derive(serde::Serialize)]
248+
struct EditComment<'a> {
249+
body: &'a str,
250+
}
251+
let comment = client
252+
.json(
253+
client
254+
.patch(&comment_url)
255+
.json(&EditComment { body: new_body }),
256+
)
257+
.await
258+
.context("failed to edit review comment")?;
259+
Ok(comment)
260+
}
261+
240262
pub async fn remove_labels(
241263
&self,
242264
client: &GithubClient,
@@ -596,6 +618,23 @@ impl Issue {
596618
.await?;
597619
Ok(())
598620
}
621+
622+
pub async fn get_review(
623+
&self,
624+
client: &GithubClient,
625+
review_id: u64,
626+
) -> anyhow::Result<Comment> {
627+
let review_url = format!(
628+
"{}/pulls/{}/reviews/{review_id}",
629+
self.repository().url(client),
630+
self.number,
631+
);
632+
let review = client
633+
.json(client.get(&review_url))
634+
.await
635+
.context("unable to fetch review")?;
636+
Ok(review)
637+
}
599638
}
600639

601640
// Comments
@@ -604,6 +643,10 @@ impl Issue {
604643
pub struct Comment {
605644
pub id: u64,
606645
pub node_id: String,
646+
#[serde(default)]
647+
pub in_reply_to_id: Option<u64>,
648+
#[serde(default)]
649+
pub pull_request_review_id: Option<u64>,
607650
#[serde(deserialize_with = "opt_string")]
608651
pub body: String,
609652
pub html_url: String,

src/handlers/review_changes_since.rs

Lines changed: 106 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
1+
use std::sync::{Arc, LazyLock};
2+
13
use anyhow::Context as _;
24

35
use crate::{
6+
cache,
47
config::ReviewChangesSinceConfig,
58
github::{Comment, Event, Issue, IssueCommentAction, IssueCommentEvent},
69
handlers::Context,
710
};
811

12+
static REVIEW_BODY_CACHE: LazyLock<
13+
tokio::sync::Mutex<cache::LeastRecentlyUsedCache<String, ReviewBodyState>>,
14+
> = LazyLock::new(|| tokio::sync::Mutex::new(cache::LeastRecentlyUsedCache::new(1000)));
15+
16+
#[derive(Copy, Clone, Debug)]
17+
enum ReviewBodyState {
18+
Present,
19+
Absent,
20+
}
21+
22+
impl cache::EstimatedSize for ReviewBodyState {
23+
fn estimated_size(&self) -> usize {
24+
std::mem::size_of::<Self>()
25+
}
26+
}
27+
928
/// Checks if this event is a PR review creation and adds in the body (if there is one)
1029
/// a link our `gh-changes-since` endpoint to view changes since this review.
1130
pub(crate) async fn handle(
@@ -14,6 +33,7 @@ pub(crate) async fn handle(
1433
event: &Event,
1534
_config: &ReviewChangesSinceConfig,
1635
) -> anyhow::Result<()> {
36+
// Match on each review and top-level review comment
1737
if let Event::IssueComment(
1838
event @ IssueCommentEvent {
1939
action: IssueCommentAction::Created,
@@ -23,33 +43,105 @@ pub(crate) async fn handle(
2343
},
2444
comment:
2545
Comment {
26-
pr_review_state: Some(_),
46+
in_reply_to_id: None,
2747
..
2848
},
2949
..
3050
},
3151
) = event
32-
&& !event.comment.body.is_empty()
3352
{
34-
// Add link our gh-changes-since endpoint to view changes since this review
35-
3653
let issue_repo = event.issue.repository();
3754
let pr_num = event.issue.number;
3855

3956
let base = &event.issue.base.as_ref().context("no base")?.sha;
4057
let head = &event.issue.head.as_ref().context("no head")?.sha;
4158

4259
let link = format!("https://{host}/gh-changes-since/{issue_repo}/{pr_num}/{base}..{head}");
43-
let new_body = format!(
44-
"{}\n\n*[View changes since this review]({link})*",
45-
event.comment.body
46-
);
47-
48-
event
49-
.issue
50-
.edit_review(&ctx.github, event.comment.id, &new_body)
51-
.await
52-
.context("failed to update the review body")?;
60+
61+
if event.comment.pull_request_review_id.is_none() && event.comment.pr_review_state.is_some()
62+
{
63+
// this is a review (not a review comment)
64+
65+
{
66+
// first let's store it's review body state in the cache to avoid future api calls
67+
// when the review comments webhook arrives (a few milliseconds after)
68+
let cache_key = format!(
69+
"{}/{}/{}",
70+
&event.repository.full_name, event.issue.number, event.comment.id
71+
);
72+
REVIEW_BODY_CACHE.lock().await.put(
73+
cache_key,
74+
Arc::new(if event.comment.body.is_empty() {
75+
ReviewBodyState::Absent
76+
} else {
77+
ReviewBodyState::Present
78+
}),
79+
);
80+
}
81+
82+
if !event.comment.body.is_empty() {
83+
// the review body is not empty, we can add to it the link to
84+
// our gh-changes-since endpoint
85+
let new_body = format!(
86+
"{}\n\n*[View changes since this review]({link})*",
87+
event.comment.body,
88+
);
89+
90+
event
91+
.issue
92+
.edit_review(&ctx.github, event.comment.id, &new_body)
93+
.await
94+
.context("failed to update the review body")?;
95+
}
96+
} else if !event.comment.body.is_empty()
97+
&& let Some(review_id) = event.comment.pull_request_review_id
98+
{
99+
// this is a review comment (not a review), we need to check if the parent
100+
// review already has a body (and as such a link)
101+
102+
// fetch the parent review body state, first look into the cache
103+
let review_body_state = {
104+
let cache_key = format!(
105+
"{}/{}/{}",
106+
&event.repository.full_name, event.issue.number, review_id
107+
);
108+
match { REVIEW_BODY_CACHE.lock().await.get(&cache_key) } {
109+
Some(state) => *state,
110+
None => {
111+
let review = event
112+
.issue
113+
.get_review(&ctx.github, review_id)
114+
.await
115+
.context("unable to fetch the parent review")?;
116+
let state = if review.body.is_empty() {
117+
ReviewBodyState::Absent
118+
} else {
119+
ReviewBodyState::Present
120+
};
121+
REVIEW_BODY_CACHE
122+
.lock()
123+
.await
124+
.put(cache_key, Arc::new(state));
125+
state
126+
}
127+
}
128+
};
129+
130+
if let ReviewBodyState::Absent = review_body_state {
131+
// parent review is empty, let's add the link to the review comment instead
132+
133+
let new_body = format!(
134+
"*[View changes since the review]({link})*\n\n{}",
135+
event.comment.body
136+
);
137+
138+
event
139+
.issue
140+
.edit_review_comment(&ctx.github, event.comment.id, &new_body)
141+
.await
142+
.context("failed to update the review comment body")?;
143+
}
144+
}
53145
}
54146

55147
Ok(())

0 commit comments

Comments
 (0)