Skip to content

Commit 5370418

Browse files
authored
Merge pull request #2163 from Urgau/gh-changes-since
Update review body with link to the changes since the review
2 parents bfc7964 + c9dd46e commit 5370418

File tree

10 files changed

+232
-63
lines changed

10 files changed

+232
-63
lines changed

src/config.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub(crate) struct Config {
4949
pub(crate) behind_upstream: Option<BehindUpstreamConfig>,
5050
pub(crate) backport: Option<BackportConfig>,
5151
pub(crate) range_diff: Option<RangeDiffConfig>,
52+
pub(crate) review_changes_since: Option<ReviewChangesSinceConfig>,
5253
}
5354

5455
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
@@ -563,6 +564,12 @@ pub(crate) struct BackportRuleConfig {
563564
#[serde(deny_unknown_fields)]
564565
pub(crate) struct RangeDiffConfig {}
565566

567+
/// Configuration for the changes since link on review body
568+
#[derive(Default, PartialEq, Eq, Debug, serde::Deserialize)]
569+
#[serde(rename_all = "kebab-case")]
570+
#[serde(deny_unknown_fields)]
571+
pub(crate) struct ReviewChangesSinceConfig {}
572+
566573
fn get_cached_config(repo: &str) -> Option<Result<Arc<Config>, ConfigurationError>> {
567574
let cache = CONFIG_CACHE.read().unwrap();
568575
cache.get(repo).and_then(|(config, fetch_time)| {
@@ -704,6 +711,8 @@ mod tests {
704711
add-labels = ["stable-nominated"]
705712
706713
[range-diff]
714+
715+
[review-changes-since]
707716
"#;
708717
let config = toml::from_str::<Config>(&config).unwrap();
709718
let mut ping_teams = HashMap::new();
@@ -795,6 +804,7 @@ mod tests {
795804
}),
796805
backport: Some(backport_team_config),
797806
range_diff: Some(RangeDiffConfig {}),
807+
review_changes_since: Some(ReviewChangesSinceConfig {}),
798808
}
799809
);
800810
}
@@ -884,6 +894,7 @@ mod tests {
884894
}),
885895
backport: None,
886896
range_diff: None,
897+
review_changes_since: None,
887898
}
888899
);
889900
}

src/gh_changes_since.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use std::sync::Arc;
2+
3+
use anyhow::Context as _;
4+
use axum::{
5+
extract::{Path, State},
6+
response::{IntoResponse, Redirect, Response},
7+
};
8+
use hyper::StatusCode;
9+
10+
use crate::{
11+
github,
12+
handlers::Context,
13+
utils::{AppError, is_repo_autorized},
14+
};
15+
16+
/// Redirects to either `/gh-range-diff` (when the base changed) or to GitHub's compare
17+
/// page (when the base is the same).
18+
///
19+
/// Takes an PR number and an `oldbase..oldhead` representing the range we are starting from.
20+
pub async fn gh_changes_since(
21+
Path((owner, repo, pr_num, oldbasehead)): Path<(String, String, u64, String)>,
22+
State(ctx): State<Arc<Context>>,
23+
) -> axum::response::Result<Response, AppError> {
24+
let Some((oldbase, oldhead)) = oldbasehead.split_once("..") else {
25+
return Ok((
26+
StatusCode::BAD_REQUEST,
27+
format!("`{oldbasehead}` is not in the form `base..head`"),
28+
)
29+
.into_response());
30+
};
31+
32+
if !is_repo_autorized(&ctx, &owner, &repo).await? {
33+
return Ok((
34+
StatusCode::UNAUTHORIZED,
35+
format!("repository `{owner}/{repo}` is not part of the Rust Project team repos"),
36+
)
37+
.into_response());
38+
}
39+
40+
let issue_repo = github::IssueRepository {
41+
organization: owner.to_string(),
42+
repository: repo.to_string(),
43+
};
44+
45+
let pr = ctx.github.pull_request(&issue_repo, pr_num).await?;
46+
47+
let newbase = &pr.base.as_ref().context("no base")?.sha;
48+
let newhead = &pr.head.as_ref().context("no head")?.sha;
49+
50+
// Has the base changed?
51+
if oldbase == newbase {
52+
// No, redirect to GitHub native compare page
53+
return Ok(Redirect::to(&format!(
54+
"https://github.com/{owner}/{repo}/compare/{oldhead}..{newhead}"
55+
))
56+
.into_response());
57+
}
58+
59+
// Yes, use our Github range-diff instead
60+
Ok(Redirect::to(&format!(
61+
"/gh-range-diff/{owner}/{repo}/{oldbase}..{oldhead}/{newbase}..{newhead}"
62+
))
63+
.into_response())
64+
}

src/gh_range_diff.rs

Lines changed: 7 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use pulldown_cmark_escape::FmtWriter;
2121
use regex::Regex;
2222

2323
use crate::github::GithubCompare;
24+
use crate::utils::is_repo_autorized;
2425
use crate::{github, handlers::Context, utils::AppError};
2526

2627
static MARKER_RE: LazyLock<Regex> =
@@ -42,27 +43,11 @@ pub async fn gh_range_diff(
4243
));
4344
};
4445

45-
let repos = ctx
46-
.team
47-
.repos()
48-
.await
49-
.context("unable to retrieve team repos")?;
50-
51-
// Verify that the request org is part of the Rust project
52-
let Some(repos) = repos.repos.get(&owner) else {
53-
return Ok((
54-
StatusCode::BAD_REQUEST,
55-
HeaderMap::new(),
56-
format!("organization `{owner}` is not part of the Rust Project team repos"),
57-
));
58-
};
59-
60-
// Verify that the request repo is part of the Rust project
61-
if !repos.iter().any(|r| r.name == repo) {
46+
if !is_repo_autorized(&ctx, &owner, &repo).await? {
6247
return Ok((
63-
StatusCode::BAD_REQUEST,
48+
StatusCode::UNAUTHORIZED,
6449
HeaderMap::new(),
65-
format!("repository `{owner}` is not part of the Rust Project team repos"),
50+
format!("repository `{owner}/{repo}` is not part of the Rust Project team repos"),
6651
));
6752
}
6853

@@ -166,27 +151,11 @@ pub async fn gh_ranges_diff(
166151
));
167152
};
168153

169-
let repos = ctx
170-
.team
171-
.repos()
172-
.await
173-
.context("unable to retrieve team repos")?;
174-
175-
// Verify that the request org is part of the Rust project
176-
let Some(repos) = repos.repos.get(&owner) else {
177-
return Ok((
178-
StatusCode::BAD_REQUEST,
179-
HeaderMap::new(),
180-
format!("organization `{owner}` is not part of the Rust Project team repos"),
181-
));
182-
};
183-
184-
// Verify that the request repo is part of the Rust project
185-
if !repos.iter().any(|r| r.name == repo) {
154+
if !is_repo_autorized(&ctx, &owner, &repo).await? {
186155
return Ok((
187-
StatusCode::BAD_REQUEST,
156+
StatusCode::UNAUTHORIZED,
188157
HeaderMap::new(),
189-
format!("repository `{owner}` is not part of the Rust Project team repos"),
158+
format!("repository `{owner}/{repo}` is not part of the Rust Project team repos"),
190159
));
191160
}
192161

src/gha_logs.rs

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::github::{self, WorkflowRunJob};
22
use crate::handlers::Context;
33
use crate::interactions::REPORT_TO;
4-
use crate::utils::AppError;
4+
use crate::utils::{AppError, is_repo_autorized};
55
use anyhow::Context as _;
66
use axum::extract::{Path, State};
77
use axum::http::HeaderValue;
@@ -71,25 +71,11 @@ pub async fn gha_logs(
7171
Path((owner, repo, log_id)): Path<(String, String, u128)>,
7272
State(ctx): State<Arc<Context>>,
7373
) -> axum::response::Result<impl IntoResponse, AppError> {
74-
let repos = ctx
75-
.team
76-
.repos()
77-
.await
78-
.context("unable to retrieve team repos")?;
79-
80-
let Some(repos) = repos.repos.get(&owner) else {
81-
return Ok((
82-
StatusCode::BAD_REQUEST,
83-
HeaderMap::new(),
84-
format!("organization `{owner}` is not part of the Rust Project team repos"),
85-
));
86-
};
87-
88-
if !repos.iter().any(|r| r.name == repo) {
74+
if !is_repo_autorized(&ctx, &owner, &repo).await? {
8975
return Ok((
90-
StatusCode::BAD_REQUEST,
76+
StatusCode::UNAUTHORIZED,
9177
HeaderMap::new(),
92-
format!("repository `{owner}` is not part of the Rust Project team repos"),
78+
format!("repository `{owner}/{repo}` is not part of the Rust Project team repos"),
9379
));
9480
}
9581

src/github.rs

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,16 @@ impl GithubClient {
270270
.await
271271
.context("failed to retrive the compare")
272272
}
273+
274+
pub async fn pull_request(&self, repo: &IssueRepository, pr_num: u64) -> anyhow::Result<Issue> {
275+
let url = format!("{}/pulls/{pr_num}", repo.url(&self));
276+
let mut pr: Issue = self
277+
.json(self.get(&url))
278+
.await
279+
.with_context(|| format!("{repo} failed to get pr {pr_num}"))?;
280+
pr.pull_request = Some(PullRequestDetails::new());
281+
Ok(pr)
282+
}
273283
}
274284

275285
#[derive(Debug, serde::Serialize)]
@@ -709,6 +719,33 @@ impl Issue {
709719
Ok(comment)
710720
}
711721

722+
pub async fn edit_review(
723+
&self,
724+
client: &GithubClient,
725+
id: u64,
726+
new_body: &str,
727+
) -> anyhow::Result<()> {
728+
let comment_url = format!(
729+
"{}/pulls/{}/reviews/{}",
730+
self.repository().url(client),
731+
self.number,
732+
id
733+
);
734+
#[derive(serde::Serialize)]
735+
struct NewComment<'a> {
736+
body: &'a str,
737+
}
738+
client
739+
.send_req(
740+
client
741+
.put(&comment_url)
742+
.json(&NewComment { body: new_body }),
743+
)
744+
.await
745+
.context("failed to edit review comment")?;
746+
Ok(())
747+
}
748+
712749
pub async fn post_comment(&self, client: &GithubClient, body: &str) -> anyhow::Result<Comment> {
713750
#[derive(serde::Serialize)]
714751
struct PostComment<'a> {
@@ -1982,13 +2019,15 @@ impl Repository {
19822019
}
19832020

19842021
pub async fn get_pr(&self, client: &GithubClient, pr_num: u64) -> anyhow::Result<Issue> {
1985-
let url = format!("{}/pulls/{pr_num}", self.url(client));
1986-
let mut pr: Issue = client
1987-
.json(client.get(&url))
2022+
client
2023+
.pull_request(
2024+
&IssueRepository {
2025+
organization: self.owner().to_string(),
2026+
repository: self.name().to_string(),
2027+
},
2028+
pr_num,
2029+
)
19882030
.await
1989-
.with_context(|| format!("{} failed to get pr {pr_num}", self.full_name))?;
1990-
pr.pull_request = Some(PullRequestDetails::new());
1991-
Ok(pr)
19922031
}
19932032

19942033
/// Fetches information about merge conflicts on open PRs.

src/handlers.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ pub mod pull_requests_assignment_update;
5353
mod relabel;
5454
mod relnotes;
5555
mod rendered_link;
56+
mod review_changes_since;
5657
mod review_requested;
5758
mod review_submitted;
5859
pub mod rustc_commits;
@@ -160,6 +161,20 @@ pub async fn handle(ctx: &Context, host: &str, event: &Event) -> Vec<HandlerErro
160161
}
161162
}
162163

164+
if let Some(config) = config
165+
.as_ref()
166+
.ok()
167+
.and_then(|c| c.review_changes_since.as_ref())
168+
{
169+
if let Err(e) = review_changes_since::handle(ctx, host, event, config).await {
170+
log::error!(
171+
"failed to process event {:?} with review_changes_since handler: {:?}",
172+
event,
173+
e
174+
)
175+
}
176+
}
177+
163178
if let Some(ghr_config) = config
164179
.as_ref()
165180
.ok()
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use anyhow::Context as _;
2+
3+
use crate::{
4+
config::ReviewChangesSinceConfig,
5+
github::{Comment, Event, Issue, IssueCommentAction, IssueCommentEvent},
6+
handlers::Context,
7+
};
8+
9+
/// Checks if this event is a PR review creation and adds in the body a link our `gh-changes-since`
10+
/// endpoint to view changes since this review.
11+
pub(crate) async fn handle(
12+
ctx: &Context,
13+
host: &str,
14+
event: &Event,
15+
_config: &ReviewChangesSinceConfig,
16+
) -> anyhow::Result<()> {
17+
if let Event::IssueComment(
18+
event @ IssueCommentEvent {
19+
action: IssueCommentAction::Created,
20+
issue: Issue {
21+
pull_request: Some(_),
22+
..
23+
},
24+
comment:
25+
Comment {
26+
pr_review_state: Some(_),
27+
..
28+
},
29+
..
30+
},
31+
) = event
32+
{
33+
// Add link our gh-changes-since endpoint to view changes since this review
34+
35+
let issue_repo = event.issue.repository();
36+
let pr_num = event.issue.number;
37+
38+
let base = &event.issue.base.as_ref().context("no base")?.sha;
39+
let head = &event.issue.head.as_ref().context("no head")?.sha;
40+
41+
let link = format!("https://{host}/gh-changes-since/{issue_repo}/{pr_num}/{base}..{head}");
42+
let new_body = format!(
43+
"{}\n\n*[View changes since this review]({link})*",
44+
event.comment.body
45+
);
46+
47+
event
48+
.issue
49+
.edit_review(&ctx.github, event.comment.id, &new_body)
50+
.await
51+
.context("failed to update the review body")?;
52+
}
53+
54+
Ok(())
55+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub mod bors;
66
mod changelogs;
77
mod config;
88
pub mod db;
9+
pub mod gh_changes_since;
910
pub mod gh_range_diff;
1011
pub mod gha_logs;
1112
pub mod github;

src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ async fn run_server(addr: SocketAddr) -> anyhow::Result<()> {
193193
"/gh-range-diff/{owner}/{repo}/{oldbasehead}/{newbasehead}",
194194
get(triagebot::gh_range_diff::gh_ranges_diff),
195195
)
196+
.route(
197+
"/gh-changes-since/{owner}/{repo}/{pr}/{oldbasehead}",
198+
get(triagebot::gh_changes_since::gh_changes_since),
199+
)
196200
.nest("/agenda", agenda)
197201
.route("/bors-commit-list", get(triagebot::bors::bors_commit_list))
198202
.route(

0 commit comments

Comments
 (0)