Skip to content

Commit 81dc476

Browse files
authored
Merge pull request #491 from Kobzol/mergeability-msg
Add conflict reporting on PRs
2 parents f9cfac5 + 2d51725 commit 81dc476

17 files changed

+316
-78
lines changed

.sqlx/query-05746d59bfab29431f55b85422e9edff63e8f6f97c748da8ca4fa8466350a829.json

Lines changed: 0 additions & 20 deletions
This file was deleted.

.sqlx/query-d16ca5631fcdd4216b1bf3a7b1672b8721d72dead28356fbbc69f84029d08132.json renamed to .sqlx/query-f697693a6cbccdf9b2557842a1ff87959d6e85012dba5e9d7159541ef317b691.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust-bors.example.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ timeout = 3600
77
# (Optional, defaults to false)
88
merge_queue_enabled = true
99

10+
# Whether merge conflicts should be reported on PRs.
11+
# (Optional, defaults to false)
12+
report_merge_conflicts = true
13+
1014
# Labels that will block approval when present on a PR
1115
# (Optional)
1216
labels_blocking_approval = ["final-comment-period", "proposed-final-comment-period"]

src/bors/comment.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::time::Duration;
55

66
use crate::bors::FailedWorkflowRun;
77
use crate::bors::command::CommandPrefix;
8-
use crate::github::GithubRepoName;
8+
use crate::github::{GithubRepoName, PullRequestNumber};
99
use crate::utils::text::pluralize;
1010
use crate::{
1111
database::{WorkflowModel, WorkflowStatus},
@@ -368,3 +368,17 @@ pub fn auto_build_push_failed_comment(error: &str) -> Comment {
368368
":eyes: Test was successful, but fast-forwarding failed: {error}"
369369
))
370370
}
371+
372+
pub fn conflict_comment(source: Option<PullRequestNumber>, was_unapproved: bool) -> Comment {
373+
Comment::new(format!(
374+
r#":umbrella: The latest upstream changes{} made this pull request unmergeable. Please [resolve the merge conflicts](https://rustc-dev-guide.rust-lang.org/git.html#rebasing-and-conflicts).{}"#,
375+
source
376+
.map(|n| format!(" (presumably #{n})"))
377+
.unwrap_or_default(),
378+
if was_unapproved {
379+
"\n\nThis pull request was unapproved."
380+
} else {
381+
""
382+
}
383+
))
384+
}

src/bors/event.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ pub struct PullRequestUnassigned {
160160
pub struct PushToBranch {
161161
pub repository: GithubRepoName,
162162
pub branch: String,
163+
pub sha: String,
163164
}
164165

165166
#[derive(Debug)]

src/bors/handlers/pr_events.rs

Lines changed: 164 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ use crate::bors::event::{
55
PullRequestReadyForReview, PullRequestReopened, PullRequestUnassigned, PushToBranch,
66
};
77

8-
use crate::bors::BorsContext;
98
use crate::bors::handlers::handle_comment;
109
use crate::bors::handlers::unapprove_pr;
1110
use crate::bors::handlers::workflow::{AutoBuildCancelReason, maybe_cancel_auto_build};
1211
use crate::bors::merge_queue::MergeQueueSender;
1312
use crate::bors::mergeability_queue::MergeabilityQueueSender;
13+
use crate::bors::{AUTO_BRANCH_NAME, BorsContext};
1414
use crate::bors::{Comment, PullRequestStatus, RepositoryState};
15-
use crate::database::{MergeableState, UpsertPullRequestParams};
15+
use crate::database::{MergeableState, PullRequestModel, UpsertPullRequestParams};
1616
use crate::github::{CommitSha, PullRequestNumber};
1717
use crate::utils::text::pluralize;
1818
use std::sync::Arc;
@@ -34,7 +34,7 @@ pub(super) async fn handle_pull_request_edited(
3434
return Ok(());
3535
};
3636

37-
mergeability_queue.enqueue_pr(&pr_model);
37+
mergeability_queue.enqueue_pr(&pr_model, None);
3838

3939
if !pr_model.is_approved() {
4040
return Ok(());
@@ -56,7 +56,7 @@ pub(super) async fn handle_push_to_pull_request(
5656
.upsert_pull_request(repo_state.repository(), pr.clone().into())
5757
.await?;
5858

59-
mergeability_queue.enqueue_pr(&pr_model);
59+
mergeability_queue.enqueue_pr(&pr_model, None);
6060

6161
let auto_build_cancel_message = maybe_cancel_auto_build(
6262
&repo_state.client,
@@ -119,15 +119,15 @@ pub(super) async fn handle_pull_request_opened(
119119
author: payload.pull_request.author.username.clone(),
120120
assignees,
121121
base_branch: payload.pull_request.base.name.clone(),
122-
mergeable_state: MergeableState::Unknown,
122+
mergeable_state: payload.pull_request.mergeable_state.clone().into(),
123123
pr_status,
124124
},
125125
)
126126
.await?;
127127

128128
process_pr_description_commands(&payload, repo_state.clone(), db, ctx, merge_queue_tx).await?;
129129

130-
mergeability_queue.enqueue_pr(&pr);
130+
mergeability_queue.enqueue_pr(&pr, None);
131131

132132
Ok(())
133133
}
@@ -169,7 +169,7 @@ pub(super) async fn handle_pull_request_reopened(
169169
.upsert_pull_request(repo_state.repository(), pr.clone().into())
170170
.await?;
171171

172-
mergeability_queue.enqueue_pr(&pr);
172+
mergeability_queue.enqueue_pr(&pr, None);
173173

174174
Ok(())
175175
}
@@ -260,14 +260,44 @@ pub(super) async fn handle_push_to_branch(
260260
payload.branch
261261
);
262262

263+
// Try to find an auto build that matches this SHA
264+
let merged_pr = find_pr_by_merged_commit(&repo_state, &db, CommitSha(payload.sha))
265+
.await
266+
.ok()
267+
.flatten()
268+
.map(|pr| pr.number);
269+
263270
for pr in affected_prs {
264-
mergeability_queue.enqueue_pr(&pr);
271+
mergeability_queue.enqueue_pr(&pr, merged_pr);
265272
}
266273
}
267274

268275
Ok(())
269276
}
270277

278+
/// Try to find a merged PR that might have produced a build with the given commit SHA.
279+
async fn find_pr_by_merged_commit(
280+
repo_state: &RepositoryState,
281+
db: &PgDbClient,
282+
sha: CommitSha,
283+
) -> anyhow::Result<Option<PullRequestModel>> {
284+
let Some(build) = db
285+
.find_build(repo_state.repository(), AUTO_BRANCH_NAME, sha)
286+
.await?
287+
else {
288+
return Ok(None);
289+
};
290+
291+
let Some(pr) = db.find_pr_by_build(&build).await? else {
292+
return Ok(None);
293+
};
294+
if pr.auto_build.as_ref().map(|b| b.id) == Some(build.id) {
295+
Ok(Some(pr))
296+
} else {
297+
Ok(None)
298+
}
299+
}
300+
271301
async fn process_pr_description_commands(
272302
payload: &PullRequestOpened,
273303
repo: Arc<RepositoryState>,
@@ -331,11 +361,12 @@ async fn notify_of_pushed_pr(
331361

332362
#[cfg(test)]
333363
mod tests {
364+
use insta::assert_snapshot;
334365
use octocrab::params::checks::{CheckRunConclusion, CheckRunStatus};
335366

336367
use crate::bors::PullRequestStatus;
337368
use crate::bors::merge_queue::AUTO_BUILD_CHECK_RUN_NAME;
338-
use crate::tests::{BorsTester, WorkflowRunData};
369+
use crate::tests::{BorsBuilder, BorsTester, GitHubState, WorkflowRunData};
339370
use crate::{
340371
database::{MergeableState, OctocrabMergeableState},
341372
tests::{User, default_branch_name, default_repo_name, run_test},
@@ -409,7 +440,7 @@ mod tests {
409440
tester.get_pr_copy(()).await.expect_unapproved();
410441
Ok(())
411442
})
412-
.await;
443+
.await;
413444
}
414445

415446
#[sqlx::test]
@@ -623,13 +654,13 @@ mod tests {
623654
run_test(pool, async |tester: &mut BorsTester| {
624655
let pr = tester
625656
.open_pr(default_repo_name(), |pr| {
626-
pr.mergeable_state = OctocrabMergeableState::Unknown
657+
pr.mergeable_state = OctocrabMergeableState::Unknown;
627658
})
628659
.await?;
629-
tester.push_to_branch(default_branch_name()).await?;
660+
tester.push_to_branch(default_branch_name(), "sha").await?;
630661
tester
631662
.modify_pr_state(pr.id(), |pr| {
632-
pr.mergeable_state = OctocrabMergeableState::Dirty
663+
pr.mergeable_state = OctocrabMergeableState::Dirty;
633664
})
634665
.await;
635666
tester
@@ -642,6 +673,124 @@ mod tests {
642673
.await;
643674
}
644675

676+
#[sqlx::test]
677+
async fn conflict_message_disabled_in_config(pool: sqlx::PgPool) {
678+
run_test(pool, async |tester: &mut BorsTester| {
679+
let pr = tester
680+
.open_pr(default_repo_name(), |pr| {
681+
pr.mergeable_state = OctocrabMergeableState::Clean;
682+
})
683+
.await?;
684+
tester
685+
.modify_pr_state(pr.id(), |pr| {
686+
pr.mergeable_state = OctocrabMergeableState::Dirty;
687+
})
688+
.await;
689+
tester.push_to_branch(default_branch_name(), "sha").await?;
690+
691+
Ok(())
692+
})
693+
.await;
694+
}
695+
696+
#[sqlx::test]
697+
async fn conflict_message_unknown_sha(pool: sqlx::PgPool) {
698+
BorsBuilder::new(pool)
699+
.github(GitHubState::default().with_default_config(
700+
r#"
701+
merge_queue_enabled = true
702+
report_merge_conflicts = true
703+
"#,
704+
)).run_test(async |tester: &mut BorsTester| {
705+
let pr = tester
706+
.open_pr(default_repo_name(), |pr| {
707+
pr.mergeable_state = OctocrabMergeableState::Clean;
708+
})
709+
.await?;
710+
tester
711+
.modify_pr_state(pr.id(), |pr| {
712+
pr.mergeable_state = OctocrabMergeableState::Dirty;
713+
})
714+
.await;
715+
tester.push_to_branch(default_branch_name(), "sha").await?;
716+
assert_snapshot!(tester.get_next_comment_text(pr).await?, @":umbrella: The latest upstream changes made this pull request unmergeable. Please [resolve the merge conflicts](https://rustc-dev-guide.rust-lang.org/git.html#rebasing-and-conflicts).");
717+
718+
Ok(())
719+
})
720+
.await;
721+
}
722+
723+
#[sqlx::test]
724+
async fn conflict_message_unknown_sha_approved(pool: sqlx::PgPool) {
725+
BorsBuilder::new(pool)
726+
.github(GitHubState::default().with_default_config(
727+
r#"
728+
merge_queue_enabled = true
729+
report_merge_conflicts = true
730+
"#,
731+
)).run_test(async |tester: &mut BorsTester| {
732+
let pr = tester
733+
.open_pr(default_repo_name(), |pr| {
734+
pr.mergeable_state = OctocrabMergeableState::Clean;
735+
})
736+
.await?;
737+
tester.approve(pr.id()).await?;
738+
tester
739+
.modify_pr_state(pr.id(), |pr| {
740+
pr.mergeable_state = OctocrabMergeableState::Dirty;
741+
})
742+
.await;
743+
tester.push_to_branch(default_branch_name(), "sha").await?;
744+
assert_snapshot!(tester.get_next_comment_text(pr.id()).await?, @r"
745+
:umbrella: The latest upstream changes made this pull request unmergeable. Please [resolve the merge conflicts](https://rustc-dev-guide.rust-lang.org/git.html#rebasing-and-conflicts).
746+
747+
This pull request was unapproved.
748+
");
749+
tester.get_pr_copy(pr.id()).await.expect_unapproved();
750+
751+
Ok(())
752+
})
753+
.await;
754+
}
755+
756+
#[sqlx::test]
757+
async fn conflict_message_known_sha(pool: sqlx::PgPool) {
758+
BorsBuilder::new(pool)
759+
.github(GitHubState::default().with_default_config(
760+
r#"
761+
merge_queue_enabled = true
762+
report_merge_conflicts = true
763+
"#,
764+
)).run_test(async |tester: &mut BorsTester| {
765+
let pr2 = tester
766+
.open_pr(default_repo_name(), |pr| {
767+
pr.mergeable_state = OctocrabMergeableState::Clean;
768+
})
769+
.await?;
770+
771+
let pr1 = tester
772+
.open_pr(default_repo_name(), |_| {})
773+
.await?;
774+
tester.approve(pr1.id()).await?;
775+
tester.start_auto_build(pr1.id()).await?;
776+
tester.workflow_full_success(tester.auto_branch().await).await?;
777+
tester.process_merge_queue().await;
778+
tester.expect_comments(pr1.id(), 1).await;
779+
let sha = tester.auto_branch().await.get_sha().to_string();
780+
781+
tester
782+
.modify_pr_state(pr2.id(), |pr| {
783+
pr.mergeable_state = OctocrabMergeableState::Dirty;
784+
})
785+
.await;
786+
tester.push_to_branch(default_branch_name(), &sha).await?;
787+
assert_snapshot!(tester.get_next_comment_text(pr2.id()).await?, @":umbrella: The latest upstream changes (presumably #3) made this pull request unmergeable. Please [resolve the merge conflicts](https://rustc-dev-guide.rust-lang.org/git.html#rebasing-and-conflicts).");
788+
789+
Ok(())
790+
})
791+
.await;
792+
}
793+
645794
#[sqlx::test]
646795
async fn enqueue_prs_on_pr_opened(pool: sqlx::PgPool) {
647796
run_test(pool, async |tester: &mut BorsTester| {
@@ -722,7 +871,7 @@ mod tests {
722871
");
723872
Ok(())
724873
})
725-
.await;
874+
.await;
726875
gh.check_cancelled_workflows(default_repo_name(), &[123]);
727876
}
728877

@@ -747,7 +896,7 @@ mod tests {
747896
");
748897
Ok(())
749898
})
750-
.await;
899+
.await;
751900
}
752901

753902
#[sqlx::test]

src/bors/handlers/refresh.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ pub async fn reload_mergeability_status(
115115
);
116116

117117
for pr in prs {
118-
mergeability_queue.enqueue_pr(&pr);
118+
mergeability_queue.enqueue_pr(&pr, None);
119119
}
120120

121121
Ok(())

src/bors/handlers/trybuild.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -632,7 +632,7 @@ try-job: Bar
632632
.db()
633633
.find_build(
634634
&default_repo_name(),
635-
TRY_BRANCH_NAME.to_string(),
635+
TRY_BRANCH_NAME,
636636
CommitSha(tester.try_branch().await.get_sha().to_string()),
637637
)
638638
.await?
@@ -791,7 +791,7 @@ try-job: Bar
791791
insta::assert_snapshot!(tester.get_next_comment_text(()).await?, @"Try build was cancelled. It was not possible to cancel some workflows.");
792792
let build = tester.db().find_build(
793793
&default_repo_name(),
794-
tester.try_branch().await.get_name().to_string(),
794+
tester.try_branch().await.get_name(),
795795
tester.try_branch().await.get_sha().to_string().into()
796796
).await?.expect("build not found");
797797
assert_eq!(build.status, BuildStatus::Cancelled);

0 commit comments

Comments
 (0)