Skip to content

Commit 7597fb3

Browse files
committed
Add PR auto-assignment.
1 parent b542f2a commit 7597fb3

File tree

9 files changed

+1028
-43
lines changed

9 files changed

+1028
-43
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ cynic = { version = "0.14" }
4040
itertools = "0.10.2"
4141
tower = { version = "0.4.13", features = ["util", "limit", "buffer", "load-shed"] }
4242
github-graphql = { path = "github-graphql" }
43+
rand = "0.8.5"
44+
ignore = "0.4.18"
4345

4446
[dependencies.serde]
4547
version = "1"

src/actions.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,9 @@ impl<'a> Action for Step<'a> {
109109
for repo in repos {
110110
let repository = Repository {
111111
full_name: format!("{}/{}", repo.0, repo.1),
112-
// This is unused for query.
112+
// These are unused for query.
113113
default_branch: "master".to_string(),
114+
fork: false,
114115
};
115116

116117
for QueryMap { name, kind, query } in queries {

src/config.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,20 @@ pub(crate) struct PingTeamConfig {
7575

7676
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
7777
pub(crate) struct AssignConfig {
78+
/// If `true`, then posts a warning comment if the PR is opened against a
79+
/// different branch than the default (usually master or main).
7880
#[serde(default)]
79-
_empty: (),
81+
pub(crate) non_default_branch: bool,
82+
/// A URL to include in the welcome message.
83+
pub(crate) contributing_url: Option<String>,
84+
/// Ad-hoc groups that can be referred to in `owners`.
85+
#[serde(default)]
86+
pub(crate) groups: HashMap<String, Vec<String>>,
87+
/// Users to assign when a new PR is opened.
88+
/// The key is a gitignore-style path, and the value is a list of
89+
/// usernames, team names, or ad-hoc groups.
90+
#[serde(default)]
91+
pub(crate) owners: HashMap<String, Vec<String>>,
8092
}
8193

8294
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
@@ -354,7 +366,12 @@ mod tests {
354366
relabel: Some(RelabelConfig {
355367
allow_unauthenticated: vec!["C-*".into()],
356368
}),
357-
assign: Some(AssignConfig { _empty: () }),
369+
assign: Some(AssignConfig {
370+
non_default_branch: false,
371+
contributing_url: None,
372+
groups: HashMap::new(),
373+
owners: HashMap::new(),
374+
}),
358375
note: Some(NoteConfig { _empty: () }),
359376
ping: Some(PingConfig { teams: ping_teams }),
360377
nominate: Some(NominateConfig {

src/github.rs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,9 @@ impl Issue {
611611
}
612612

613613
pub fn contain_assignee(&self, user: &str) -> bool {
614-
self.assignees.iter().any(|a| a.login == user)
614+
self.assignees
615+
.iter()
616+
.any(|a| a.login.to_lowercase() == user.to_lowercase())
615617
}
616618

617619
pub async fn remove_assignees(
@@ -637,7 +639,7 @@ impl Issue {
637639
.assignees
638640
.iter()
639641
.map(|u| u.login.as_str())
640-
.filter(|&u| u != user)
642+
.filter(|&u| u.to_lowercase() != user.to_lowercase())
641643
.collect::<Vec<_>>(),
642644
};
643645

@@ -677,7 +679,10 @@ impl Issue {
677679
.map_err(AssignmentError::Http)?;
678680
// Invalid assignees are silently ignored. We can just check if the user is now
679681
// contained in the assignees list.
680-
let success = result.assignees.iter().any(|u| u.login.as_str() == user);
682+
let success = result
683+
.assignees
684+
.iter()
685+
.any(|u| u.login.as_str().to_lowercase() == user.to_lowercase());
681686

682687
if success {
683688
Ok(())
@@ -942,10 +947,17 @@ pub struct IssueSearchResult {
942947
pub items: Vec<Issue>,
943948
}
944949

950+
#[derive(Debug, serde::Deserialize)]
951+
struct CommitSearchResult {
952+
total_count: u32,
953+
}
954+
945955
#[derive(Clone, Debug, serde::Deserialize)]
946956
pub struct Repository {
947957
pub full_name: String,
948958
pub default_branch: String,
959+
#[serde(default)]
960+
pub fork: bool,
949961
}
950962

951963
#[derive(Copy, Clone)]
@@ -1505,6 +1517,39 @@ impl GithubClient {
15051517
}
15061518
}
15071519
}
1520+
1521+
/// Returns whether or not the given GitHub login has made any commits to
1522+
/// the given repo.
1523+
pub async fn is_new_contributor(&self, repo: &Repository, author: &str) -> bool {
1524+
if repo.fork {
1525+
// Forks always return 0 results.
1526+
return false;
1527+
}
1528+
let url = format!(
1529+
"{}/search/commits?q=repo:{}+author:{}",
1530+
Repository::GITHUB_API_URL,
1531+
repo.full_name,
1532+
author,
1533+
);
1534+
let req = self.get(&url);
1535+
match self.json::<CommitSearchResult>(req).await {
1536+
Ok(res) => res.total_count == 0,
1537+
Err(e) => {
1538+
// 422 is returned for unknown user
1539+
if e.downcast_ref::<reqwest::Error>().map_or(false, |e| {
1540+
e.status() == Some(StatusCode::UNPROCESSABLE_ENTITY)
1541+
}) {
1542+
true
1543+
} else {
1544+
log::warn!(
1545+
"failed to search for user commits in {} for author {author}: {e}",
1546+
repo.full_name
1547+
);
1548+
false
1549+
}
1550+
}
1551+
}
1552+
}
15081553
}
15091554

15101555
#[derive(Debug, serde::Deserialize)]

src/handlers.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ mod shortcut;
4646
pub async fn handle(ctx: &Context, event: &Event) -> Vec<HandlerError> {
4747
let config = config::get(&ctx.github, event.repo()).await;
4848
if let Err(e) = &config {
49-
log::warn!("failed to load configuration: {e}");
49+
log::warn!("configuration error {}: {e}", event.repo().full_name);
5050
}
5151
let mut errors = Vec::new();
5252

@@ -155,6 +155,7 @@ macro_rules! issue_handlers {
155155
// This is for events that happen only on issues (e.g. label changes).
156156
// Each module in the list must contain the functions `parse_input` and `handle_input`.
157157
issue_handlers! {
158+
assign,
158159
autolabel,
159160
major_change,
160161
mentions,

0 commit comments

Comments
 (0)