Skip to content

Commit 188c27d

Browse files
feat: unapprove on push
1 parent f77e09a commit 188c27d

File tree

6 files changed

+162
-77
lines changed

6 files changed

+162
-77
lines changed

src/bors/event.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use octocrab::models::RunId;
66
pub enum BorsRepositoryEvent {
77
/// A comment was posted on a pull request.
88
Comment(PullRequestComment),
9+
/// When a new commit is pushed to the pull request branch.
10+
PullRequestPushed(PullRequestPushed),
911
/// When the pull request is edited by its author
1012
PullRequestEdited(PullRequestEdited),
1113
/// A workflow run on Github Actions or a check run from external CI system has been started.
@@ -21,10 +23,11 @@ impl BorsRepositoryEvent {
2123
pub fn repository(&self) -> &GithubRepoName {
2224
match self {
2325
BorsRepositoryEvent::Comment(comment) => &comment.repository,
26+
BorsRepositoryEvent::PullRequestPushed(payload) => &payload.repository,
27+
BorsRepositoryEvent::PullRequestEdited(payload) => &payload.repository,
2428
BorsRepositoryEvent::WorkflowStarted(workflow) => &workflow.repository,
2529
BorsRepositoryEvent::WorkflowCompleted(workflow) => &workflow.repository,
2630
BorsRepositoryEvent::CheckSuiteCompleted(payload) => &payload.repository,
27-
BorsRepositoryEvent::PullRequestEdited(payload) => &payload.repository,
2831
}
2932
}
3033
}
@@ -54,6 +57,12 @@ pub struct PullRequestComment {
5457
pub text: String,
5558
}
5659

60+
#[derive(Debug)]
61+
pub struct PullRequestPushed {
62+
pub repository: GithubRepoName,
63+
pub pull_request: PullRequest,
64+
}
65+
5766
#[derive(Debug)]
5867
pub struct PullRequestEdited {
5968
pub repository: GithubRepoName,

src/bors/handlers/mod.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::bors::handlers::help::command_help;
99
use crate::bors::handlers::ping::command_ping;
1010
use crate::bors::handlers::refresh::refresh_repository;
1111
use crate::bors::handlers::review::{
12-
command_approve, command_unapprove, handle_pull_request_edited,
12+
command_approve, command_unapprove, handle_pull_request_edited, handle_push_to_pull_request,
1313
};
1414
use crate::bors::handlers::trybuild::{command_try_build, command_try_cancel, TRY_BRANCH_NAME};
1515
use crate::bors::handlers::workflow::{
@@ -122,6 +122,14 @@ pub async fn handle_bors_repository_event(
122122
.instrument(span.clone())
123123
.await?;
124124
}
125+
BorsRepositoryEvent::PullRequestPushed(payload) => {
126+
let span =
127+
tracing::info_span!("Pull request pushed", repo = payload.repository.to_string());
128+
129+
handle_push_to_pull_request(repo, db, payload)
130+
.instrument(span.clone())
131+
.await?;
132+
}
125133
}
126134
Ok(())
127135
}

src/bors/handlers/review.rs

Lines changed: 105 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ use std::sync::Arc;
22

33
use crate::bors::command::Approver;
44
use crate::bors::event::PullRequestEdited;
5+
use crate::bors::event::PullRequestPushed;
56
use crate::bors::handlers::labels::handle_label_trigger;
67
use crate::bors::Comment;
78
use crate::bors::RepositoryState;
9+
use crate::github::CommitSha;
810
use crate::github::GithubUser;
911
use crate::github::LabelTrigger;
1012
use crate::github::PullRequest;
@@ -67,7 +69,7 @@ pub(super) async fn handle_pull_request_edited(
6769
let pr_model = db
6870
.get_or_create_pull_request(repo_state.repository(), payload.pull_request.number)
6971
.await?;
70-
if pr_model.approved_by.is_none() {
72+
if !pr_model.is_approved() {
7173
return Ok(());
7274
}
7375

@@ -77,6 +79,26 @@ pub(super) async fn handle_pull_request_edited(
7779
notify_of_edited_pr(&repo_state, pr_number, &payload.pull_request.base.name).await
7880
}
7981

82+
pub(super) async fn handle_push_to_pull_request(
83+
repo_state: Arc<RepositoryState>,
84+
db: Arc<PgDbClient>,
85+
payload: PullRequestPushed,
86+
) -> anyhow::Result<()> {
87+
let pr = &payload.pull_request;
88+
let pr_model = db
89+
.get_or_create_pull_request(repo_state.repository(), pr.number)
90+
.await?;
91+
92+
if !pr_model.is_approved() {
93+
return Ok(());
94+
}
95+
96+
let pr_number = pr_model.number;
97+
db.unapprove(repo_state.repository(), pr_number).await?;
98+
handle_label_trigger(&repo_state, pr_number, LabelTrigger::Unapproved).await?;
99+
notify_of_pushed_pr(&repo_state, pr_number, pr.head.sha.clone()).await
100+
}
101+
80102
fn sufficient_approve_permission(repo: Arc<RepositoryState>, author: &GithubUser) -> bool {
81103
repo.permissions
82104
.load()
@@ -176,27 +198,37 @@ PR will need to be re-approved."#,
176198
.await
177199
}
178200

201+
async fn notify_of_pushed_pr(
202+
repo: &RepositoryState,
203+
pr_number: PullRequestNumber,
204+
head_sha: CommitSha,
205+
) -> anyhow::Result<()> {
206+
repo.client
207+
.post_comment(
208+
pr_number,
209+
Comment::new(format!(
210+
r#":warning: Branch's head was changed to `{}`, the
211+
PR will need to be re-approved."#,
212+
head_sha
213+
)),
214+
)
215+
.await
216+
}
217+
179218
#[cfg(test)]
180219
mod tests {
181220
use crate::{
182221
github::PullRequestNumber,
183222
tests::mocks::{
184-
default_pr_number, default_repo_name, BorsBuilder, BorsTester, Permissions,
223+
default_pr_number, default_repo_name, run_test, BorsBuilder, BorsTester, Permissions,
185224
PullRequestChangeEvent, User, World,
186225
},
187226
};
188227

189228
#[sqlx::test]
190229
async fn default_approve(pool: sqlx::PgPool) {
191-
let world = World::default();
192-
world.default_repo().lock().set_config(
193-
r#"
194-
[labels]
195-
approve = ["+approved"]
196-
"#,
197-
);
198230
BorsBuilder::new(pool)
199-
.world(world)
231+
.world(create_world_with_approve_config())
200232
.run_test(|mut tester| async {
201233
tester.post_comment("@bors r+").await?;
202234
assert_eq!(
@@ -221,15 +253,8 @@ approve = ["+approved"]
221253

222254
#[sqlx::test]
223255
async fn approve_on_behalf(pool: sqlx::PgPool) {
224-
let world = World::default();
225-
world.default_repo().lock().set_config(
226-
r#"
227-
[labels]
228-
approve = ["+approved"]
229-
"#,
230-
);
231256
BorsBuilder::new(pool)
232-
.world(world)
257+
.world(create_world_with_approve_config())
233258
.run_test(|mut tester| async {
234259
let approve_user = "user1";
235260
tester
@@ -269,15 +294,8 @@ approve = ["+approved"]
269294

270295
#[sqlx::test]
271296
async fn unapprove(pool: sqlx::PgPool) {
272-
let world = World::default();
273-
world.default_repo().lock().set_config(
274-
r#"
275-
[labels]
276-
approve = ["+approved"]
277-
"#,
278-
);
279297
BorsBuilder::new(pool)
280-
.world(world)
298+
.world(create_world_with_approve_config())
281299
.run_test(|mut tester| async {
282300
tester.post_comment("@bors r+").await?;
283301
assert_eq!(
@@ -307,15 +325,8 @@ approve = ["+approved"]
307325

308326
#[sqlx::test]
309327
async fn unapprove_on_base_edited(pool: sqlx::PgPool) {
310-
let world = World::default();
311-
world.default_repo().lock().set_config(
312-
r#"
313-
[labels]
314-
approve = ["+approved"]
315-
"#,
316-
);
317328
BorsBuilder::new(pool)
318-
.world(world)
329+
.world(create_world_with_approve_config())
319330
.run_test(|mut tester| async {
320331
tester.post_comment("@bors r+").await?;
321332
assert_eq!(
@@ -348,15 +359,8 @@ PR will need to be re-approved."#,
348359

349360
#[sqlx::test]
350361
async fn edit_pr_do_nothing_when_base_not_edited(pool: sqlx::PgPool) {
351-
let world = World::default();
352-
world.default_repo().lock().set_config(
353-
r#"
354-
[labels]
355-
approve = ["+approved"]
356-
"#,
357-
);
358362
BorsBuilder::new(pool)
359-
.world(world)
363+
.world(create_world_with_approve_config())
360364
.run_test(|mut tester| async {
361365
tester.post_comment("@bors r+").await?;
362366
assert_eq!(
@@ -389,31 +393,74 @@ approve = ["+approved"]
389393

390394
#[sqlx::test]
391395
async fn edit_pr_do_nothing_when_not_approved(pool: sqlx::PgPool) {
392-
let world = World::default();
393-
world.default_repo().lock().set_config(
394-
r#"
395-
[labels]
396-
approve = ["+approved"]
397-
"#,
398-
);
396+
run_test(pool, |mut tester| async {
397+
tester
398+
.edit_pull_request(
399+
default_pr_number(),
400+
PullRequestChangeEvent {
401+
from_base_sha: Some("main-sha".to_string()),
402+
},
403+
)
404+
.await?;
405+
406+
// No comment should be posted
407+
Ok(tester)
408+
})
409+
.await;
410+
}
411+
412+
#[sqlx::test]
413+
async fn unapprove_on_push(pool: sqlx::PgPool) {
399414
BorsBuilder::new(pool)
400-
.world(world)
415+
.world(create_world_with_approve_config())
401416
.run_test(|mut tester| async {
402-
tester
403-
.edit_pull_request(
417+
tester.post_comment("@bors r+").await?;
418+
assert_eq!(
419+
tester.get_comment().await?,
420+
format!(
421+
"Commit pr-{}-sha has been approved by `{}`",
404422
default_pr_number(),
405-
PullRequestChangeEvent {
406-
from_base_sha: Some("main-sha".to_string()),
407-
},
408-
)
409-
.await?;
423+
User::default_user().name
424+
),
425+
);
426+
tester.push_to_pull_request(default_pr_number()).await?;
410427

411-
// No comment should be posted
428+
assert_eq!(
429+
tester.get_comment().await?,
430+
format!(
431+
r#":warning: Branch's head was changed to `pr-{}-sha`, the
432+
PR will need to be re-approved."#,
433+
default_pr_number()
434+
)
435+
);
436+
check_pr_unapproved(&tester, default_pr_number().into()).await;
412437
Ok(tester)
413438
})
414439
.await;
415440
}
416441

442+
#[sqlx::test]
443+
async fn push_to_pr_do_nothing_when_not_approved(pool: sqlx::PgPool) {
444+
run_test(pool, |mut tester| async {
445+
tester.push_to_pull_request(default_pr_number()).await?;
446+
447+
// No comment should be posted
448+
Ok(tester)
449+
})
450+
.await;
451+
}
452+
453+
fn create_world_with_approve_config() -> World {
454+
let world = World::default();
455+
world.default_repo().lock().set_config(
456+
r#"
457+
[labels]
458+
approve = ["+approved"]
459+
"#,
460+
);
461+
world
462+
}
463+
417464
async fn check_pr_approved_by(
418465
tester: &BorsTester,
419466
pr_number: PullRequestNumber,

src/database/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,12 @@ pub struct PullRequestModel {
121121
pub created_at: DateTime<Utc>,
122122
}
123123

124+
impl PullRequestModel {
125+
pub fn is_approved(&self) -> bool {
126+
self.approved_by.is_some()
127+
}
128+
}
129+
124130
/// Describes whether a workflow is a Github Actions workflow or if it's a job from some external
125131
/// CI.
126132
#[derive(Debug, PartialEq, sqlx::Type)]

src/github/webhook.rs

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use sha2::Sha256;
1919

2020
use crate::bors::event::{
2121
BorsEvent, BorsGlobalEvent, BorsRepositoryEvent, CheckSuiteCompleted, PullRequestComment,
22-
PullRequestEdited, WorkflowCompleted, WorkflowStarted,
22+
PullRequestEdited, PullRequestPushed, WorkflowCompleted, WorkflowStarted,
2323
};
2424
use crate::database::{WorkflowStatus, WorkflowType};
2525
use crate::github::server::ServerStateRef;
@@ -204,24 +204,31 @@ fn parse_issue_comment_event(body: &[u8]) -> anyhow::Result<Option<BorsEvent>> {
204204
fn parse_pull_request_events(body: &[u8]) -> anyhow::Result<Option<BorsEvent>> {
205205
let payload: WebhookPullRequestEvent = serde_json::from_slice(body)?;
206206
let repository_name = parse_repository_name(&payload.repository)?;
207-
if payload.action == PullRequestEventAction::Edited {
208-
let Some(changes) = payload.changes else {
209-
return Err(anyhow::anyhow!(
210-
"Edited pull request event should have `changes` field"
211-
));
212-
};
213-
Ok(Some(BorsEvent::Repository(
214-
BorsRepositoryEvent::PullRequestEdited(PullRequestEdited {
215-
repository: repository_name,
207+
match payload.action {
208+
PullRequestEventAction::Edited => {
209+
let Some(changes) = payload.changes else {
210+
return Err(anyhow::anyhow!(
211+
"Edited pull request event should have `changes` field"
212+
));
213+
};
214+
Ok(Some(BorsEvent::Repository(
215+
BorsRepositoryEvent::PullRequestEdited(PullRequestEdited {
216+
repository: repository_name,
217+
pull_request: payload.pull_request.into(),
218+
from_base_sha: changes
219+
.base
220+
.and_then(|base| base.sha)
221+
.map(|sha| CommitSha(sha.from)),
222+
}),
223+
)))
224+
}
225+
PullRequestEventAction::Synchronize => Ok(Some(BorsEvent::Repository(
226+
BorsRepositoryEvent::PullRequestPushed(PullRequestPushed {
216227
pull_request: payload.pull_request.into(),
217-
from_base_sha: changes
218-
.base
219-
.and_then(|base| base.sha)
220-
.map(|sha| CommitSha(sha.from)),
228+
repository: repository_name,
221229
}),
222-
)))
223-
} else {
224-
Ok(None)
230+
))),
231+
_ => Ok(None),
225232
}
226233
}
227234

src/tests/mocks/bors.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,14 @@ impl BorsTester {
280280
.await
281281
}
282282

283+
pub async fn push_to_pull_request(&mut self, pr_number: u64) -> anyhow::Result<()> {
284+
self.send_webhook(
285+
"pull_request",
286+
GitHubPullRequestEventPayload::new(pr_number, "synchronize".to_string(), None),
287+
)
288+
.await
289+
}
290+
283291
async fn webhook_comment(&mut self, comment: Comment) -> anyhow::Result<()> {
284292
self.send_webhook(
285293
"issue_comment",

0 commit comments

Comments
 (0)