|  | 
| 1 | 1 | use crate::changelogs::ChangelogFormat; | 
| 2 | 2 | use crate::github::{GithubClient, Repository}; | 
|  | 3 | +use parser::command::relabel::{Label, LabelDelta, RelabelCommand}; | 
| 3 | 4 | use std::collections::{HashMap, HashSet}; | 
| 4 | 5 | use std::fmt; | 
| 5 | 6 | use std::sync::{Arc, LazyLock, RwLock}; | 
| @@ -250,10 +251,64 @@ pub(crate) struct MentionsEntryConfig { | 
| 250 | 251 | 
 | 
| 251 | 252 | #[derive(PartialEq, Eq, Debug, serde::Deserialize)] | 
| 252 | 253 | #[serde(rename_all = "kebab-case")] | 
| 253 |  | -#[serde(deny_unknown_fields)] | 
| 254 | 254 | pub(crate) struct RelabelConfig { | 
| 255 | 255 |     #[serde(default)] | 
| 256 | 256 |     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 | +        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); | 
|  | 275 | +                } | 
|  | 276 | +            } | 
|  | 277 | +        } | 
|  | 278 | +        RelabelCommand(deltas) | 
|  | 279 | +    } | 
|  | 280 | +} | 
|  | 281 | + | 
|  | 282 | +#[derive(Default, PartialEq, Eq, Debug, serde::Deserialize)] | 
|  | 283 | +#[serde(rename_all = "kebab-case")] | 
|  | 284 | +#[serde(deny_unknown_fields)] | 
|  | 285 | +pub(crate) struct RelabelAliasConfig { | 
|  | 286 | +    /// Labels to be added | 
|  | 287 | +    pub(crate) add_labels: Vec<String>, | 
|  | 288 | +    /// Labels to be removed | 
|  | 289 | +    pub(crate) rem_labels: Vec<String>, | 
|  | 290 | +} | 
|  | 291 | + | 
|  | 292 | +impl RelabelAliasConfig { | 
|  | 293 | +    /// Translate a RelabelAliasConfig into a RelabelCommand for GitHub consumption | 
|  | 294 | +    pub fn to_command(&self, inverted: bool) -> RelabelCommand { | 
|  | 295 | +        let mut deltas = Vec::new(); | 
|  | 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() { | 
|  | 305 | +            deltas.push(LabelDelta::Add(Label(l.into()))); | 
|  | 306 | +        } | 
|  | 307 | +        for l in rem_labels.iter() { | 
|  | 308 | +            deltas.push(LabelDelta::Remove(Label(l.into()))); | 
|  | 309 | +        } | 
|  | 310 | +        RelabelCommand(deltas) | 
|  | 311 | +    } | 
| 257 | 312 | } | 
| 258 | 313 | 
 | 
| 259 | 314 | #[derive(PartialEq, Eq, Debug, serde::Deserialize)] | 
| @@ -725,6 +780,8 @@ where | 
| 725 | 780 | #[cfg(test)] | 
| 726 | 781 | mod tests { | 
| 727 | 782 |     use super::*; | 
|  | 783 | +    use parser::error::Error; | 
|  | 784 | +    use parser::token::Tokenizer; | 
| 728 | 785 | 
 | 
| 729 | 786 |     #[test] | 
| 730 | 787 |     fn sample() { | 
| @@ -761,11 +818,11 @@ mod tests { | 
| 761 | 818 | 
 | 
| 762 | 819 |             [mentions."src/"] | 
| 763 | 820 |             cc = ["@someone"] | 
| 764 |  | -             | 
|  | 821 | +
 | 
| 765 | 822 |             [mentions."target/"] | 
| 766 | 823 |             message = "This is a message." | 
| 767 | 824 |             cc = ["@someone"] | 
| 768 |  | -             | 
|  | 825 | +
 | 
| 769 | 826 |             [mentions."#[rustc_attr]"] | 
| 770 | 827 |             type = "content" | 
| 771 | 828 |             message = "This is a message." | 
| @@ -835,6 +892,7 @@ mod tests { | 
| 835 | 892 |             Config { | 
| 836 | 893 |                 relabel: Some(RelabelConfig { | 
| 837 | 894 |                     allow_unauthenticated: vec!["C-*".into()], | 
|  | 895 | +                    aliases: HashMap::new() | 
| 838 | 896 |                 }), | 
| 839 | 897 |                 assign: Some(AssignConfig { | 
| 840 | 898 |                     warn_non_default_branch: WarnNonDefaultBranchConfig::Simple(false), | 
| @@ -1033,6 +1091,82 @@ mod tests { | 
| 1033 | 1091 |         ); | 
| 1034 | 1092 |     } | 
| 1035 | 1093 | 
 | 
|  | 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 | + | 
| 1036 | 1170 |     #[test] | 
| 1037 | 1171 |     fn issue_links_uncanonicalized() { | 
| 1038 | 1172 |         let config = r#" | 
| @@ -1093,4 +1227,36 @@ Multi text body with ${mcp_issue} and ${mcp_title} | 
| 1093 | 1227 |             }) | 
| 1094 | 1228 |         ); | 
| 1095 | 1229 |     } | 
|  | 1230 | + | 
|  | 1231 | +    #[test] | 
|  | 1232 | +    fn relabel_new_config() { | 
|  | 1233 | +        let config = r#" | 
|  | 1234 | +            [relabel] | 
|  | 1235 | +            allow-unauthenticated = ["ABCD-*"] | 
|  | 1236 | +
 | 
|  | 1237 | +            [relabel.to-stable] | 
|  | 1238 | +            add-labels = ["regression-from-stable-to-stable"] | 
|  | 1239 | +            rem-labels = ["regression-from-stable-to-beta", "regression-from-stable-to-nightly"] | 
|  | 1240 | +        "#; | 
|  | 1241 | +        let config = toml::from_str::<Config>(&config).unwrap(); | 
|  | 1242 | + | 
|  | 1243 | +        let mut relabel_configs = HashMap::new(); | 
|  | 1244 | +        relabel_configs.insert( | 
|  | 1245 | +            "to-stable".into(), | 
|  | 1246 | +            RelabelAliasConfig { | 
|  | 1247 | +                add_labels: vec!["regression-from-stable-to-stable".to_string()], | 
|  | 1248 | +                rem_labels: vec![ | 
|  | 1249 | +                    "regression-from-stable-to-beta".to_string(), | 
|  | 1250 | +                    "regression-from-stable-to-nightly".to_string(), | 
|  | 1251 | +                ], | 
|  | 1252 | +            }, | 
|  | 1253 | +        ); | 
|  | 1254 | + | 
|  | 1255 | +        let expected_cfg = RelabelConfig { | 
|  | 1256 | +            allow_unauthenticated: vec!["ABCD-*".to_string()], | 
|  | 1257 | +            aliases: relabel_configs, | 
|  | 1258 | +        }; | 
|  | 1259 | + | 
|  | 1260 | +        assert_eq!(config.relabel, Some(expected_cfg)); | 
|  | 1261 | +    } | 
| 1096 | 1262 | } | 
0 commit comments