Skip to content

Commit 527bf21

Browse files
authored
Merge pull request #1942 from xizheyin/pr-behind-commits
Suggest update the branch when behind too many commits
2 parents a1632b4 + 42c47d5 commit 527bf21

File tree

5 files changed

+153
-2
lines changed

5 files changed

+153
-2
lines changed

src/config.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub(crate) struct Config {
4949
#[serde(alias = "canonicalize-issue-links")]
5050
pub(crate) issue_links: Option<IssueLinksConfig>,
5151
pub(crate) no_mentions: Option<NoMentionsConfig>,
52+
pub(crate) behind_upstream: Option<BehindUpstreamConfig>,
5253
}
5354

5455
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
@@ -427,6 +428,16 @@ pub(crate) struct IssueLinksConfig {}
427428
#[serde(deny_unknown_fields)]
428429
pub(crate) struct NoMentionsConfig {}
429430

431+
/// Configuration for PR behind commits checks
432+
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
433+
#[serde(rename_all = "kebab-case")]
434+
#[serde(deny_unknown_fields)]
435+
pub(crate) struct BehindUpstreamConfig {
436+
/// The threshold of days for parent commit age to trigger a warning.
437+
/// Default is 7 days if not specified.
438+
pub(crate) days_threshold: Option<usize>,
439+
}
440+
430441
fn get_cached_config(repo: &str) -> Option<Result<Arc<Config>, ConfigurationError>> {
431442
let cache = CONFIG_CACHE.read().unwrap();
432443
cache.get(repo).and_then(|(config, fetch_time)| {
@@ -554,6 +565,9 @@ mod tests {
554565
trigger-files = ["posts/"]
555566
556567
[no-mentions]
568+
569+
[behind-upstream]
570+
days-threshold = 14
557571
"#;
558572
let config = toml::from_str::<Config>(&config).unwrap();
559573
let mut ping_teams = HashMap::new();
@@ -618,6 +632,9 @@ mod tests {
618632
}),
619633
issue_links: Some(IssueLinksConfig {}),
620634
no_mentions: Some(NoMentionsConfig {}),
635+
behind_upstream: Some(BehindUpstreamConfig {
636+
days_threshold: Some(14),
637+
}),
621638
}
622639
);
623640
}
@@ -637,6 +654,9 @@ mod tests {
637654
branch = "stable"
638655
639656
[canonicalize-issue-links]
657+
658+
[behind-upstream]
659+
days-threshold = 7
640660
"#;
641661
let config = toml::from_str::<Config>(&config).unwrap();
642662
assert_eq!(
@@ -685,6 +705,9 @@ mod tests {
685705
rendered_link: None,
686706
issue_links: Some(IssueLinksConfig {}),
687707
no_mentions: None,
708+
behind_upstream: Some(BehindUpstreamConfig {
709+
days_threshold: Some(7),
710+
}),
688711
}
689712
);
690713
}

src/github.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1459,7 +1459,7 @@ impl Repository {
14591459

14601460
/// Returns a list of commits between the SHA ranges of start (exclusive)
14611461
/// and end (inclusive).
1462-
pub async fn commits_in_range(
1462+
pub async fn github_commits_in_range(
14631463
&self,
14641464
client: &GithubClient,
14651465
start: &str,
@@ -1500,6 +1500,18 @@ impl Repository {
15001500
}
15011501
}
15021502

1503+
pub async fn github_commit(
1504+
&self,
1505+
client: &GithubClient,
1506+
sha: &str,
1507+
) -> anyhow::Result<GithubCommit> {
1508+
let url = format!("{}/commits/{}", self.url(client), sha);
1509+
client
1510+
.json(client.get(&url))
1511+
.await
1512+
.with_context(|| format!("{} failed to get github commit {sha}", self.full_name))
1513+
}
1514+
15031515
/// Retrieves a git commit for the given SHA.
15041516
pub async fn git_commit(&self, client: &GithubClient, sha: &str) -> anyhow::Result<GitCommit> {
15051517
let url = format!("{}/git/commits/{sha}", self.url(client));

src/handlers/check_commits.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use crate::{
1313
#[cfg(test)]
1414
use crate::github::GithubCommit;
1515

16+
mod behind_upstream;
1617
mod issue_links;
1718
mod modified_submodule;
1819
mod no_mentions;
@@ -93,6 +94,19 @@ pub(super) async fn handle(ctx: &Context, event: &Event, config: &Config) -> any
9394
}
9495
}
9596

97+
// Check if PR is behind upstream branch by a significant number of days
98+
if let Some(behind_upstream) = &config.behind_upstream {
99+
let age_threshold = behind_upstream
100+
.days_threshold
101+
.unwrap_or(behind_upstream::DEFAULT_DAYS_THRESHOLD);
102+
103+
if let Some(warning) =
104+
behind_upstream::behind_upstream(age_threshold, event, &ctx.github, &commits).await
105+
{
106+
warnings.push(warning);
107+
}
108+
}
109+
96110
handle_warnings_and_labels(ctx, event, warnings, labels).await
97111
}
98112

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use crate::github::{GithubClient, GithubCommit, IssuesEvent, Repository};
2+
use tracing as log;
3+
4+
/// Default threshold for parent commit age in days to trigger a warning
5+
pub const DEFAULT_DAYS_THRESHOLD: usize = 7;
6+
7+
/// Check if the PR is based on an old parent commit
8+
pub async fn behind_upstream(
9+
age_threshold: usize,
10+
event: &IssuesEvent,
11+
client: &GithubClient,
12+
commits: &Vec<GithubCommit>,
13+
) -> Option<String> {
14+
log::debug!("Checking if PR #{} is behind upstream", event.issue.number);
15+
16+
let Some(head_commit) = commits.first() else {
17+
return None;
18+
};
19+
20+
// First try the parent commit age check as it's more accurate
21+
match is_parent_commit_too_old(head_commit, &event.repository, client, age_threshold).await {
22+
Ok(Some(days_old)) => {
23+
log::info!(
24+
"PR #{} has a parent commit that is {} days old",
25+
event.issue.number,
26+
days_old
27+
);
28+
29+
return Some(format!(
30+
r"This PR is based on an upstream commit that is {} days old.
31+
32+
*It's recommended to update your branch according to the [rustc_dev_guide](https://rustc-dev-guide.rust-lang.org/contributing.html#keeping-your-branch-up-to-date).*",
33+
days_old
34+
));
35+
}
36+
Ok(None) => {
37+
// Parent commit is not too old, log and do nothing
38+
log::debug!("PR #{} parent commit is not too old", event.issue.number);
39+
}
40+
Err(e) => {
41+
// Error checking parent commit age, log and do nothing
42+
log::error!(
43+
"Error checking parent commit age for PR #{}: {}",
44+
event.issue.number,
45+
e
46+
);
47+
}
48+
}
49+
50+
None
51+
}
52+
53+
/// Checks if the PR's parent commit is too old.
54+
///
55+
/// This determines if a PR needs updating by examining the first parent of the PR's head commit,
56+
/// which typically represents the base branch commit that the PR is based on.
57+
///
58+
/// If this parent commit is older than the specified threshold, it suggests the PR
59+
/// should be updated/rebased to a more recent version of the base branch.
60+
///
61+
/// Returns:
62+
/// - Ok(Some(days_old)) - If parent commit is older than the threshold
63+
/// - Ok(None)
64+
/// - If there is no parent commit
65+
/// - If parent is within threshold
66+
/// - Err(...) - If an error occurred during processing
67+
pub async fn is_parent_commit_too_old(
68+
commit: &GithubCommit,
69+
repo: &Repository,
70+
client: &GithubClient,
71+
max_days_old: usize,
72+
) -> anyhow::Result<Option<usize>> {
73+
// Get the first parent (it should be from the base branch)
74+
let Some(parent_sha) = commit.parents.get(0).map(|c| c.sha.clone()) else {
75+
return Ok(None);
76+
};
77+
78+
let days_old = commit_days_old(&parent_sha, repo, client).await?;
79+
80+
if days_old > max_days_old {
81+
Ok(Some(days_old))
82+
} else {
83+
Ok(None)
84+
}
85+
}
86+
87+
/// Returns the number of days old the commit is
88+
pub async fn commit_days_old(
89+
sha: &str,
90+
repo: &Repository,
91+
client: &GithubClient,
92+
) -> anyhow::Result<usize> {
93+
// Get the commit details to check its date
94+
let commit: GithubCommit = repo.github_commit(client, &sha).await?;
95+
96+
// compute the number of days old the commit is
97+
let commit_date = commit.commit.author.date;
98+
let now = chrono::Utc::now().with_timezone(&commit_date.timezone());
99+
let days_old = (now - commit_date).num_days() as usize;
100+
101+
Ok(days_old)
102+
}

src/handlers/milestone_prs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ async fn milestone_cargo(
134134
let cargo_repo = gh.repository("rust-lang/cargo").await?;
135135
log::info!("loading cargo changes {cargo_start_hash}...{cargo_end_hash}");
136136
let commits = cargo_repo
137-
.commits_in_range(gh, cargo_start_hash, cargo_end_hash)
137+
.github_commits_in_range(gh, cargo_start_hash, cargo_end_hash)
138138
.await?;
139139

140140
// For each commit, look for a message from bors that indicates which

0 commit comments

Comments
 (0)