Skip to content

Commit 3da8091

Browse files
committed
Add alias for compound labels
Configure relabel command aliases from the `triagebot.toml`. When a valid alias is parsed, it will be replaced with the labels configured. Example configuration: ``` [relabel.cmd-alias] add-labels = ["Foo", "Bar"] rem-labels = ["Baz"] ``` The command `@rustbot label cmd-alias` will translate to: ``` @rustbot label +Foo +Bar -Baz ``` The command `@rustbot label -cmd-alias` will translate to: ``` @rustbot label +Baz -Foo -Bar ``` Note: self-canceling labels will be omitted. The command `@rustbot label cmd-alias +Bar` will translate to: ``` @rustbot label -Foo -Bar ```
1 parent 31f0e2f commit 3da8091

File tree

4 files changed

+120
-42
lines changed

4 files changed

+120
-42
lines changed

parser/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ pub mod command;
22
pub mod error;
33
mod ignore_block;
44
mod mentions;
5-
mod token;
5+
pub mod token;
66

77
pub use ignore_block::replace_all_outside_ignore_blocks;
88
pub use mentions::get_mentions;

src/config.rs

Lines changed: 107 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -256,53 +256,55 @@ pub(crate) struct RelabelConfig {
256256
pub(crate) allow_unauthenticated: Vec<String>,
257257
// alias identifier -> labels
258258
#[serde(flatten)]
259-
pub(crate) configs: Option<HashMap<String, RelabelRuleConfig>>,
259+
pub(crate) aliases: HashMap<String, RelabelAliasConfig>,
260260
}
261261

262262
impl RelabelConfig {
263263
pub(crate) fn retrieve_command_from_alias(&self, input: RelabelCommand) -> RelabelCommand {
264-
match &self.configs {
265-
Some(configs) => {
266-
dbg!(&configs);
267-
// get only the first token from the command
268-
// extract the "alias" from the RelabelCommand
269-
if input.0.len() > 0 {
270-
let name = input.0.get(0).unwrap();
271-
let name = name.label().as_str();
272-
// check if this alias matches any RelabelRuleConfig key in our config
273-
// extract the labels and build a new command
274-
if configs.contains_key(name) {
275-
let (_alias, cfg) = configs.get_key_value(name).unwrap();
276-
return cfg.to_command();
277-
}
264+
let mut deltas = vec![];
265+
if !self.aliases.is_empty() {
266+
// parse all tokens: if one matches an alias, extract the labels
267+
// else, it will assumed to be a valid label
268+
for tk in input.0.into_iter() {
269+
let name = tk.label() as &str;
270+
if let Some(alias) = self.aliases.get(name) {
271+
let cmd = alias.to_command(matches!(tk, LabelDelta::Remove(_)));
272+
deltas.extend(cmd.0);
273+
} else {
274+
deltas.push(tk);
278275
}
279276
}
280-
None => {
281-
return input;
282-
}
283-
};
284-
input
277+
}
278+
RelabelCommand(deltas)
285279
}
286280
}
287281

288282
#[derive(Default, PartialEq, Eq, Debug, serde::Deserialize)]
289283
#[serde(rename_all = "kebab-case")]
290284
#[serde(deny_unknown_fields)]
291-
pub(crate) struct RelabelRuleConfig {
285+
pub(crate) struct RelabelAliasConfig {
292286
/// Labels to be added
293287
pub(crate) add_labels: Vec<String>,
294288
/// Labels to be removed
295289
pub(crate) rem_labels: Vec<String>,
296290
}
297291

298-
impl RelabelRuleConfig {
299-
/// Translate a RelabelRuleConfig into a RelabelCommand for GitHub consumption
300-
pub fn to_command(&self) -> RelabelCommand {
292+
impl RelabelAliasConfig {
293+
/// Translate a RelabelAliasConfig into a RelabelCommand for GitHub consumption
294+
pub fn to_command(&self, inverted: bool) -> RelabelCommand {
301295
let mut deltas = Vec::new();
302-
for l in self.add_labels.iter() {
296+
let mut add_labels = &self.add_labels;
297+
let mut rem_labels = &self.rem_labels;
298+
299+
// if the polarity of the alias is inverted, swap labels before parsing the command
300+
if inverted {
301+
std::mem::swap(&mut add_labels, &mut rem_labels);
302+
}
303+
304+
for l in add_labels.iter() {
303305
deltas.push(LabelDelta::Add(Label(l.into())));
304306
}
305-
for l in self.rem_labels.iter() {
307+
for l in rem_labels.iter() {
306308
deltas.push(LabelDelta::Remove(Label(l.into())));
307309
}
308310
RelabelCommand(deltas)
@@ -778,6 +780,8 @@ where
778780
#[cfg(test)]
779781
mod tests {
780782
use super::*;
783+
use parser::error::Error;
784+
use parser::token::Tokenizer;
781785

782786
#[test]
783787
fn sample() {
@@ -888,7 +892,7 @@ mod tests {
888892
Config {
889893
relabel: Some(RelabelConfig {
890894
allow_unauthenticated: vec!["C-*".into()],
891-
configs: Some(HashMap::new())
895+
aliases: HashMap::new()
892896
}),
893897
assign: Some(AssignConfig {
894898
warn_non_default_branch: WarnNonDefaultBranchConfig::Simple(false),
@@ -1087,6 +1091,82 @@ mod tests {
10871091
);
10881092
}
10891093

1094+
#[test]
1095+
fn relabel_alias_config() {
1096+
let config = r#"
1097+
[relabel.to-stable]
1098+
add-labels = ["regression-from-stable-to-stable"]
1099+
rem-labels = ["regression-from-stable-to-beta", "regression-from-stable-to-nightly"]
1100+
"#;
1101+
let config = toml::from_str::<Config>(&config).unwrap();
1102+
1103+
let mut relabel_configs = HashMap::new();
1104+
relabel_configs.insert(
1105+
"to-stable".into(),
1106+
RelabelAliasConfig {
1107+
add_labels: vec!["regression-from-stable-to-stable".to_string()],
1108+
rem_labels: vec![
1109+
"regression-from-stable-to-beta".to_string(),
1110+
"regression-from-stable-to-nightly".to_string(),
1111+
],
1112+
},
1113+
);
1114+
1115+
let expected_cfg = RelabelConfig {
1116+
allow_unauthenticated: vec![],
1117+
aliases: relabel_configs,
1118+
};
1119+
1120+
assert_eq!(config.relabel, Some(expected_cfg));
1121+
}
1122+
1123+
#[cfg(test)]
1124+
fn parse<'a>(input: &'a str) -> Result<Option<Vec<LabelDelta>>, Error<'a>> {
1125+
let mut toks = Tokenizer::new(input);
1126+
Ok(RelabelCommand::parse(&mut toks)?.map(|c| c.0))
1127+
}
1128+
1129+
#[test]
1130+
fn relabel_alias() {
1131+
// [relabel.my-alias]
1132+
// add-labels = ["Alpha"]
1133+
// rem-labels = ["Bravo", "Charlie"]
1134+
let relabel_cfg = RelabelConfig {
1135+
allow_unauthenticated: vec![],
1136+
aliases: HashMap::from([(
1137+
"my-alias".to_string(),
1138+
RelabelAliasConfig {
1139+
add_labels: vec!["Alpha".to_string()],
1140+
rem_labels: vec!["Bravo".to_string(), "Charlie".to_string()],
1141+
},
1142+
)]),
1143+
};
1144+
1145+
// @triagebot label my-alias
1146+
let deltas = parse("label my-alias").unwrap().unwrap();
1147+
let new_input = relabel_cfg.retrieve_command_from_alias(RelabelCommand(deltas));
1148+
assert_eq!(
1149+
new_input,
1150+
RelabelCommand(vec![
1151+
LabelDelta::Add(Label("Alpha".into())),
1152+
LabelDelta::Remove(Label("Bravo".into())),
1153+
LabelDelta::Remove(Label("Charlie".into())),
1154+
])
1155+
);
1156+
1157+
// @triagebot label -my-alias
1158+
let deltas = parse("label -my-alias").unwrap().unwrap();
1159+
let new_input = relabel_cfg.retrieve_command_from_alias(RelabelCommand(deltas));
1160+
assert_eq!(
1161+
new_input,
1162+
RelabelCommand(vec![
1163+
LabelDelta::Add(Label("Bravo".into())),
1164+
LabelDelta::Add(Label("Charlie".into())),
1165+
LabelDelta::Remove(Label("Alpha".into())),
1166+
])
1167+
);
1168+
}
1169+
10901170
#[test]
10911171
fn issue_links_uncanonicalized() {
10921172
let config = r#"

src/github.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,9 +1338,6 @@ impl IssuesEvent {
13381338
}
13391339
}
13401340

1341-
#[derive(Debug, serde::Deserialize)]
1342-
struct PullRequestEventFields {}
1343-
13441341
#[derive(Debug, serde::Deserialize)]
13451342
pub struct WorkflowRunJob {
13461343
pub name: String,

src/handlers/relabel.rs

Lines changed: 12 additions & 11 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
//!
@@ -26,17 +26,17 @@ pub(super) async fn handle_command(
2626
event: &Event,
2727
input: RelabelCommand,
2828
) -> anyhow::Result<()> {
29-
let mut to_rem = vec![];
30-
let mut to_add = vec![];
29+
let Some(issue) = event.issue() else {
30+
return user_error!("Can only add and remove labels on issues and pull requests");
31+
};
3132

3233
// If the input matches a valid alias, read the [relabel] config.
33-
// if any alias matches, extract the alias config (RelabelRuleConfig) and build a new RelabelCommand.
34-
// Discard anything after the alias.
34+
// if any alias matches, extract the alias config (RelabelAliasConfig) and build a new RelabelCommand.
3535
let new_input = config.retrieve_command_from_alias(input);
3636

37-
// Parse input label command, checks permissions, built GitHub commands
37+
// Check label authorization for the current user
3838
for delta in &new_input.0 {
39-
let name = delta.label().as_str();
39+
let name = delta.label() as &str;
4040
let err = match check_filter(name, config, is_member(&event.user(), &ctx.team).await) {
4141
Ok(CheckFilterResult::Allow) => None,
4242
Ok(CheckFilterResult::Deny) => {
@@ -56,7 +56,7 @@ pub(super) async fn handle_command(
5656
}
5757

5858
// Compute the labels to add and remove
59-
let (to_add, to_remove) = compute_label_deltas(&input.0);
59+
let (to_add, to_remove) = compute_label_deltas(&new_input.0);
6060

6161
// Add labels
6262
if let Err(e) = issue.add_labels(&ctx.github, to_add.clone()).await {
@@ -201,6 +201,7 @@ fn compute_label_deltas(deltas: &[LabelDelta]) -> (Vec<Label>, Vec<Label>) {
201201
#[cfg(test)]
202202
mod tests {
203203
use parser::command::relabel::{Label, LabelDelta};
204+
use std::collections::HashMap;
204205

205206
use super::{
206207
CheckFilterResult, MatchPatternResult, TeamMembership, check_filter, compute_label_deltas,
@@ -239,7 +240,7 @@ mod tests {
239240
($($member:ident { $($label:expr => $res:ident,)* })*) => {
240241
let config = RelabelConfig {
241242
allow_unauthenticated: vec!["T-*".into(), "I-*".into(), "!I-*nominated".into()],
242-
configs: None
243+
aliases: HashMap::new()
243244
};
244245
$($(assert_eq!(
245246
check_filter($label, &config, TeamMembership::$member),

0 commit comments

Comments
 (0)