Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion parser/src/command/relabel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub enum LabelDelta {
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Label(String);
pub struct Label(pub String);

#[derive(PartialEq, Eq, Debug)]
pub enum ParseError {
Expand Down
2 changes: 1 addition & 1 deletion src/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ impl User {
}
}

#[derive(PartialEq, Eq, Debug, Clone, serde::Deserialize)]
#[derive(PartialEq, Eq, Debug, Clone, Ord, PartialOrd, serde::Deserialize)]
pub struct Label {
pub name: String,
}
Expand Down
135 changes: 103 additions & 32 deletions src/handlers/relabel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
//! If the command was successful, there will be no feedback beyond the label change to reduce
//! notification noise.

use std::collections::BTreeSet;

use crate::github::Label;
use crate::team_data::TeamClient;
use crate::{
config::RelabelConfig,
Expand All @@ -24,8 +27,11 @@ pub(super) async fn handle_command(
event: &Event,
input: RelabelCommand,
) -> anyhow::Result<()> {
let mut results = vec![];
let mut to_add = vec![];
let Some(issue) = event.issue() else {
anyhow::bail!("event is not an issue");
};
Comment on lines +30 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Urgau why did you introduce this block?

I don't know for other people but I need to be able to edit labels on both pull requests and issues.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PRs are also issues, so this should work fine. The code before did an unwrap on event.issue() anyway.

Copy link
Contributor

@apiraino apiraino Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah wait maybe I am misunderstanding: Issue here is meant as both a "ticket" and a pull request so Event::Issue covers both.

But I still don't understand why adding this check: what other event are you preventing to happen?

The error message at least is confusing, I think.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did you introduce this block?

Because the code below was asserting the presence of it. It's better return a message than panic and block all the other handlers.

We should indeed probably change the message to be explicitly about PRs since it's now a user message.

Copy link
Contributor

@apiraino apiraino Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should indeed probably change the message to be explicitly about PRs since it's now a user message.

I'm working on #2172 I will change that as well


// Check label authorization for the current user
for delta in &input.0 {
let name = delta.label().as_str();
let err = match check_filter(name, config, is_member(&event.user(), &ctx.team).await) {
Expand All @@ -42,54 +48,37 @@ pub(super) async fn handle_command(
Err(err) => Some(err),
};
if let Some(msg) = err {
let cmnt = ErrorComment::new(&event.issue().unwrap(), msg);
let cmnt = ErrorComment::new(issue, msg);
cmnt.post(&ctx.github).await?;
return Ok(());
}
match delta {
LabelDelta::Add(label) => {
to_add.push(github::Label {
name: label.to_string(),
});
}
LabelDelta::Remove(label) => {
results.push((
label,
event.issue().unwrap().remove_label(&ctx.github, &label),
));
}
}
}

if let Err(e) = event
.issue()
.unwrap()
.add_labels(&ctx.github, to_add.clone())
.await
{
// Compute the labels to add and remove
let (to_add, to_remove) = compute_label_deltas(&input.0);

// Add labels
if let Err(e) = issue.add_labels(&ctx.github, to_add.clone()).await {
tracing::error!(
"failed to add {:?} from issue {}: {:?}",
to_add,
event.issue().unwrap().global_id(),
issue.global_id(),
e
);
if let Some(err @ UnknownLabels { .. }) = e.downcast_ref() {
event
.issue()
.unwrap()
.post_comment(&ctx.github, &err.to_string())
.await?;
issue.post_comment(&ctx.github, &err.to_string()).await?;
}

return Err(e);
}

for (label, res) in results {
if let Err(e) = res.await {
// Remove labels
for label in to_remove {
if let Err(e) = issue.remove_label(&ctx.github, &label.name).await {
tracing::error!(
"failed to remove {:?} from issue {}: {:?}",
label,
event.issue().unwrap().global_id(),
issue.global_id(),
e
);
return Err(e);
Expand Down Expand Up @@ -182,10 +171,41 @@ fn match_pattern(pattern: &str, label: &str) -> anyhow::Result<MatchPatternResul
})
}

fn compute_label_deltas(deltas: &[LabelDelta]) -> (Vec<Label>, Vec<Label>) {
let mut add = BTreeSet::new();
let mut remove = BTreeSet::new();

for delta in deltas {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note to myself: this implementation means that doing +foo -foo -foo will actually remove foo from the issue. This is probably useful for something like alias -foo, where alias does +foo -foo.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, and it also means that +foo +foo -foo does nothing (+foo +foo -> +foo and +foo -foo cancels each other out).

match delta {
LabelDelta::Add(label) => {
let label = Label {
name: label.to_string(),
};
if !remove.remove(&label) {
add.insert(label);
}
}
LabelDelta::Remove(label) => {
let label = Label {
name: label.to_string(),
};
if !add.remove(&label) {
remove.insert(label);
}
}
}
}

(add.into_iter().collect(), remove.into_iter().collect())
}

#[cfg(test)]
mod tests {
use parser::command::relabel::{Label, LabelDelta};

use super::{
CheckFilterResult, MatchPatternResult, TeamMembership, check_filter, match_pattern,
CheckFilterResult, MatchPatternResult, TeamMembership, check_filter, compute_label_deltas,
match_pattern,
};
use crate::config::RelabelConfig;

Expand Down Expand Up @@ -252,4 +272,55 @@ mod tests {
}
Ok(())
}

#[test]
fn test_compute_label_deltas() {
use crate::github::Label as GitHubLabel;

let mut deltas = vec![
LabelDelta::Add(Label("I-nominated".to_string())),
LabelDelta::Add(Label("I-nominated".to_string())),
LabelDelta::Add(Label("I-lang-nominated".to_string())),
LabelDelta::Add(Label("I-libs-nominated".to_string())),
LabelDelta::Remove(Label("I-lang-nominated".to_string())),
];

assert_eq!(
compute_label_deltas(&deltas),
(
vec![
GitHubLabel {
name: "I-libs-nominated".to_string()
},
GitHubLabel {
name: "I-nominated".to_string()
},
],
vec![],
),
);

deltas.push(LabelDelta::Remove(Label("needs-triage".to_string())));
deltas.push(LabelDelta::Add(Label("I-lang-nominated".to_string())));

assert_eq!(
compute_label_deltas(&deltas),
(
vec![
GitHubLabel {
name: "I-lang-nominated".to_string()
},
GitHubLabel {
name: "I-libs-nominated".to_string()
},
GitHubLabel {
name: "I-nominated".to_string()
},
],
vec![GitHubLabel {
name: "needs-triage".to_string()
}],
),
);
}
}
Loading