Skip to content

Commit 30acfc2

Browse files
authored
Merge pull request #2216 from apiraino/alias-for-compound-labels-take2
Add alias for compound labels (take 2)
2 parents f3d0ef9 + 83c13b0 commit 30acfc2

File tree

4 files changed

+193
-13
lines changed

4 files changed

+193
-13
lines changed

parser/src/command/relabel.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ fn delta_empty() {
102102
}
103103

104104
impl RelabelCommand {
105+
/// Parse and validate command tokens
105106
pub fn parse<'a>(input: &mut Tokenizer<'a>) -> Result<Option<Self>, Error<'a>> {
106107
let mut toks = input.clone();
107108

src/config.rs

Lines changed: 176 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::changelogs::ChangelogFormat;
22
use crate::github::{GithubClient, Repository};
3+
use parser::command::relabel::{Label, LabelDelta, RelabelCommand};
34
use std::collections::{HashMap, HashSet};
45
use std::fmt;
56
use std::sync::{Arc, LazyLock, RwLock};
@@ -250,10 +251,62 @@ pub(crate) struct MentionsEntryConfig {
250251

251252
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
252253
#[serde(rename_all = "kebab-case")]
253-
#[serde(deny_unknown_fields)]
254254
pub(crate) struct RelabelConfig {
255255
#[serde(default)]
256256
pub(crate) allow_unauthenticated: Vec<String>,
257+
// alias identifier -> labels
258+
#[serde(flatten)]
259+
pub(crate) aliases: HashMap<String, RelabelAliasConfig>,
260+
}
261+
262+
impl RelabelConfig {
263+
pub(crate) fn retrieve_command_from_alias(&self, input: RelabelCommand) -> RelabelCommand {
264+
let mut deltas = vec![];
265+
// parse all tokens: if one matches an alias, extract the labels
266+
// else, it will assumed to be a label
267+
for tk in input.0.into_iter() {
268+
let name = tk.label() as &str;
269+
if let Some(alias) = self.aliases.get(name) {
270+
let cmd = alias.to_command(matches!(tk, LabelDelta::Remove(_)));
271+
deltas.extend(cmd.0);
272+
} else {
273+
deltas.push(tk);
274+
}
275+
}
276+
RelabelCommand(deltas)
277+
}
278+
}
279+
280+
#[derive(Default, PartialEq, Eq, Debug, serde::Deserialize)]
281+
#[serde(rename_all = "kebab-case")]
282+
#[serde(deny_unknown_fields)]
283+
pub(crate) struct RelabelAliasConfig {
284+
/// Labels to be added
285+
pub(crate) add_labels: Vec<String>,
286+
/// Labels to be removed
287+
pub(crate) rem_labels: Vec<String>,
288+
}
289+
290+
impl RelabelAliasConfig {
291+
/// Translate a RelabelAliasConfig into a RelabelCommand for GitHub consumption
292+
fn to_command(&self, inverted: bool) -> RelabelCommand {
293+
let mut deltas = Vec::new();
294+
let mut add_labels = &self.add_labels;
295+
let mut rem_labels = &self.rem_labels;
296+
297+
// if the polarity of the alias is inverted, swap labels before parsing the command
298+
if inverted {
299+
std::mem::swap(&mut add_labels, &mut rem_labels);
300+
}
301+
302+
for l in add_labels.iter() {
303+
deltas.push(LabelDelta::Add(Label(l.into())));
304+
}
305+
for l in rem_labels.iter() {
306+
deltas.push(LabelDelta::Remove(Label(l.into())));
307+
}
308+
RelabelCommand(deltas)
309+
}
257310
}
258311

259312
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
@@ -761,11 +814,11 @@ mod tests {
761814
762815
[mentions."src/"]
763816
cc = ["@someone"]
764-
817+
765818
[mentions."target/"]
766819
message = "This is a message."
767820
cc = ["@someone"]
768-
821+
769822
[mentions."#[rustc_attr]"]
770823
type = "content"
771824
message = "This is a message."
@@ -835,6 +888,7 @@ mod tests {
835888
Config {
836889
relabel: Some(RelabelConfig {
837890
allow_unauthenticated: vec!["C-*".into()],
891+
aliases: HashMap::new()
838892
}),
839893
assign: Some(AssignConfig {
840894
warn_non_default_branch: WarnNonDefaultBranchConfig::Simple(false),
@@ -1033,6 +1087,93 @@ mod tests {
10331087
);
10341088
}
10351089

1090+
#[test]
1091+
fn relabel_alias_config() {
1092+
let config = r#"
1093+
[relabel.to-stable]
1094+
add-labels = ["regression-from-stable-to-stable"]
1095+
rem-labels = ["regression-from-stable-to-beta", "regression-from-stable-to-nightly"]
1096+
"#;
1097+
let config = toml::from_str::<Config>(&config).unwrap();
1098+
1099+
let mut relabel_configs = HashMap::new();
1100+
relabel_configs.insert(
1101+
"to-stable".into(),
1102+
RelabelAliasConfig {
1103+
add_labels: vec!["regression-from-stable-to-stable".to_string()],
1104+
rem_labels: vec![
1105+
"regression-from-stable-to-beta".to_string(),
1106+
"regression-from-stable-to-nightly".to_string(),
1107+
],
1108+
},
1109+
);
1110+
1111+
let expected_cfg = RelabelConfig {
1112+
allow_unauthenticated: vec![],
1113+
aliases: relabel_configs,
1114+
};
1115+
1116+
assert_eq!(config.relabel, Some(expected_cfg));
1117+
}
1118+
1119+
#[test]
1120+
fn relabel_alias() {
1121+
// [relabel.my-alias]
1122+
// add-labels = ["Alpha"]
1123+
// rem-labels = ["Bravo", "Charlie"]
1124+
let relabel_cfg = RelabelConfig {
1125+
allow_unauthenticated: vec![],
1126+
aliases: HashMap::from([(
1127+
"my-alias".to_string(),
1128+
RelabelAliasConfig {
1129+
add_labels: vec!["Alpha".to_string()],
1130+
rem_labels: vec!["Bravo".to_string(), "Charlie".to_string()],
1131+
},
1132+
)]),
1133+
};
1134+
1135+
// @triagebot label my-alias
1136+
let deltas = vec![LabelDelta::Add(Label("my-alias".into()))];
1137+
let new_input = relabel_cfg.retrieve_command_from_alias(RelabelCommand(deltas));
1138+
assert_eq!(
1139+
new_input,
1140+
RelabelCommand(vec![
1141+
LabelDelta::Add(Label("Alpha".into())),
1142+
LabelDelta::Remove(Label("Bravo".into())),
1143+
LabelDelta::Remove(Label("Charlie".into())),
1144+
])
1145+
);
1146+
1147+
// @triagebot label -my-alias
1148+
let deltas = vec![LabelDelta::Remove(Label("my-alias".into()))];
1149+
let new_input = relabel_cfg.retrieve_command_from_alias(RelabelCommand(deltas));
1150+
assert_eq!(
1151+
new_input,
1152+
RelabelCommand(vec![
1153+
LabelDelta::Add(Label("Bravo".into())),
1154+
LabelDelta::Add(Label("Charlie".into())),
1155+
LabelDelta::Remove(Label("Alpha".into())),
1156+
])
1157+
);
1158+
}
1159+
1160+
#[test]
1161+
fn relabel_alias_empty_config() {
1162+
// empty alias config
1163+
let relabel_cfg = RelabelConfig {
1164+
allow_unauthenticated: vec![],
1165+
aliases: HashMap::new(),
1166+
};
1167+
1168+
// @triagebot label T-compiler
1169+
let deltas = vec![LabelDelta::Add(Label("T-compiler".into()))];
1170+
let new_input = relabel_cfg.retrieve_command_from_alias(RelabelCommand(deltas));
1171+
assert_eq!(
1172+
new_input,
1173+
RelabelCommand(vec![LabelDelta::Add(Label("T-compiler".into())),])
1174+
);
1175+
}
1176+
10361177
#[test]
10371178
fn issue_links_uncanonicalized() {
10381179
let config = r#"
@@ -1093,4 +1234,36 @@ Multi text body with ${mcp_issue} and ${mcp_title}
10931234
})
10941235
);
10951236
}
1237+
1238+
#[test]
1239+
fn relabel_new_config() {
1240+
let config = r#"
1241+
[relabel]
1242+
allow-unauthenticated = ["ABCD-*"]
1243+
1244+
[relabel.to-stable]
1245+
add-labels = ["regression-from-stable-to-stable"]
1246+
rem-labels = ["regression-from-stable-to-beta", "regression-from-stable-to-nightly"]
1247+
"#;
1248+
let config = toml::from_str::<Config>(&config).unwrap();
1249+
1250+
let mut relabel_configs = HashMap::new();
1251+
relabel_configs.insert(
1252+
"to-stable".into(),
1253+
RelabelAliasConfig {
1254+
add_labels: vec!["regression-from-stable-to-stable".to_string()],
1255+
rem_labels: vec![
1256+
"regression-from-stable-to-beta".to_string(),
1257+
"regression-from-stable-to-nightly".to_string(),
1258+
],
1259+
},
1260+
);
1261+
1262+
let expected_cfg = RelabelConfig {
1263+
allow_unauthenticated: vec!["ABCD-*".to_string()],
1264+
aliases: relabel_configs,
1265+
};
1266+
1267+
assert_eq!(config.relabel, Some(expected_cfg));
1268+
}
10961269
}

src/github.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,9 +1326,6 @@ impl IssuesEvent {
13261326
}
13271327
}
13281328

1329-
#[derive(Debug, serde::Deserialize)]
1330-
struct PullRequestEventFields {}
1331-
13321329
#[derive(Debug, serde::Deserialize)]
13331330
pub struct WorkflowRunJob {
13341331
pub name: String,

src/handlers/relabel.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
//! Purpose: Allow any user to modify issue labels on GitHub via comments.
1+
//! Purpose: Allow any user to modify labels on GitHub issues and pull requests via comments.
22
//!
3-
//! Labels are checked against the labels in the project; the bot does not support creating new
4-
//! labels.
3+
//! Labels are checked against the existing set in the git repository; the bot does not support
4+
//! creating new labels.
55
//!
66
//! Parsing is done in the `parser::command::relabel` module.
77
//!
@@ -28,13 +28,17 @@ pub(super) async fn handle_command(
2828
input: RelabelCommand,
2929
) -> anyhow::Result<()> {
3030
let Some(issue) = event.issue() else {
31-
return user_error!("Can only add and remove labels on an issue");
31+
return user_error!("Can only add and remove labels on issues and pull requests");
3232
};
3333

34+
// If the input matches a valid alias, read the [relabel] config.
35+
// if any alias matches, extract the alias config (RelabelAliasConfig) and build a new RelabelCommand.
36+
let new_input = config.retrieve_command_from_alias(input);
37+
3438
// Check label authorization for the current user
35-
for delta in &input.0 {
39+
for delta in &new_input.0 {
3640
let name = delta.label() as &str;
37-
let err = match check_filter(name, config, is_member(event.user(), &ctx.team).await) {
41+
let err = match check_filter(name, config, is_member(&event.user(), &ctx.team).await) {
3842
Ok(CheckFilterResult::Allow) => None,
3943
Ok(CheckFilterResult::Deny) => {
4044
Some(format!("Label {name} can only be set by Rust team members"))
@@ -45,14 +49,15 @@ pub(super) async fn handle_command(
4549
)),
4650
Err(err) => Some(err),
4751
};
52+
4853
if let Some(err) = err {
4954
// bail-out and inform the user why
5055
return user_error!(err);
5156
}
5257
}
5358

5459
// Compute the labels to add and remove
55-
let (to_add, to_remove) = compute_label_deltas(&input.0);
60+
let (to_add, to_remove) = compute_label_deltas(&new_input.0);
5661

5762
// Add labels
5863
issue
@@ -94,6 +99,8 @@ enum CheckFilterResult {
9499
DenyUnknown,
95100
}
96101

102+
/// Check if the team member is allowed to apply labels
103+
/// configured in `allow_unauthenticated`
97104
fn check_filter(
98105
label: &str,
99106
config: &RelabelConfig,
@@ -185,6 +192,7 @@ fn compute_label_deltas(deltas: &[LabelDelta]) -> (Vec<Label>, Vec<Label>) {
185192
#[cfg(test)]
186193
mod tests {
187194
use parser::command::relabel::{Label, LabelDelta};
195+
use std::collections::HashMap;
188196

189197
use super::{
190198
CheckFilterResult, MatchPatternResult, TeamMembership, check_filter, compute_label_deltas,
@@ -223,6 +231,7 @@ mod tests {
223231
($($member:ident { $($label:expr => $res:ident,)* })*) => {
224232
let config = RelabelConfig {
225233
allow_unauthenticated: vec!["T-*".into(), "I-*".into(), "!I-*nominated".into()],
234+
aliases: HashMap::new()
226235
};
227236
$($(assert_eq!(
228237
check_filter($label, &config, TeamMembership::$member),

0 commit comments

Comments
 (0)