Skip to content

Commit a420065

Browse files
Merge pull request #1540 from Mark-Simulacrum/label-changes
Add/remove labels instead of setting a new set of labels
2 parents 0ccf3ee + 31494a0 commit a420065

9 files changed

+214
-256
lines changed

src/github.rs

+63-64
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use tracing as log;
33

44
use async_trait::async_trait;
55
use chrono::{DateTime, FixedOffset, Utc};
6-
use futures::stream::{FuturesUnordered, StreamExt};
76
use futures::{future::BoxFuture, FutureExt};
87
use hyper::header::HeaderValue;
98
use once_cell::sync::OnceCell;
@@ -233,18 +232,6 @@ pub struct Label {
233232
pub name: String,
234233
}
235234

236-
impl Label {
237-
async fn exists<'a>(&'a self, repo_api_prefix: &'a str, client: &'a GithubClient) -> bool {
238-
#[allow(clippy::redundant_pattern_matching)]
239-
let url = format!("{}/labels/{}", repo_api_prefix, self.name);
240-
match client.send_req(client.get(&url)).await {
241-
Ok(_) => true,
242-
// XXX: Error handling if the request failed for reasons beyond 'label didn't exist'
243-
Err(_) => false,
244-
}
245-
}
246-
}
247-
248235
#[derive(Debug, serde::Deserialize)]
249236
pub struct PullRequestDetails {
250237
// none for now
@@ -468,39 +455,68 @@ impl Issue {
468455
Ok(())
469456
}
470457

471-
pub async fn set_labels(
458+
pub async fn remove_label(&self, client: &GithubClient, label: &str) -> anyhow::Result<()> {
459+
log::info!("remove_label from {}: {:?}", self.global_id(), label);
460+
// DELETE /repos/:owner/:repo/issues/:number/labels/{name}
461+
let url = format!(
462+
"{repo_url}/issues/{number}/labels/{name}",
463+
repo_url = self.repository().url(),
464+
number = self.number,
465+
name = label,
466+
);
467+
468+
if !self.labels().iter().any(|l| l.name == label) {
469+
log::info!(
470+
"remove_label from {}: {:?} already not present, skipping",
471+
self.global_id(),
472+
label
473+
);
474+
return Ok(());
475+
}
476+
477+
client
478+
._send_req(client.delete(&url))
479+
.await
480+
.context("failed to delete label")?;
481+
482+
Ok(())
483+
}
484+
485+
pub async fn add_labels(
472486
&self,
473487
client: &GithubClient,
474488
labels: Vec<Label>,
475489
) -> anyhow::Result<()> {
476-
log::info!("set_labels {} to {:?}", self.global_id(), labels);
477-
// PUT /repos/:owner/:repo/issues/:number/labels
490+
log::info!("add_labels: {} +{:?}", self.global_id(), labels);
491+
// POST /repos/:owner/:repo/issues/:number/labels
478492
// repo_url = https://api.github.com/repos/Codertocat/Hello-World
479493
let url = format!(
480494
"{repo_url}/issues/{number}/labels",
481495
repo_url = self.repository().url(),
482496
number = self.number
483497
);
484498

485-
let mut stream = labels
499+
// Don't try to add labels already present on this issue.
500+
let labels = labels
486501
.into_iter()
487-
.map(|label| async { (label.exists(&self.repository().url(), &client).await, label) })
488-
.collect::<FuturesUnordered<_>>();
489-
let mut labels = Vec::new();
490-
while let Some((true, label)) = stream.next().await {
491-
labels.push(label);
502+
.filter(|l| !self.labels().contains(&l))
503+
.map(|l| l.name)
504+
.collect::<Vec<_>>();
505+
506+
log::info!("add_labels: {} filtered to {:?}", self.global_id(), labels);
507+
508+
if labels.is_empty() {
509+
return Ok(());
492510
}
493511

494512
#[derive(serde::Serialize)]
495513
struct LabelsReq {
496514
labels: Vec<String>,
497515
}
498516
client
499-
._send_req(client.put(&url).json(&LabelsReq {
500-
labels: labels.iter().map(|l| l.name.clone()).collect(),
501-
}))
517+
._send_req(client.post(&url).json(&LabelsReq { labels }))
502518
.await
503-
.context("failed to set labels")?;
519+
.context("failed to add labels")?;
504520

505521
Ok(())
506522
}
@@ -659,6 +675,25 @@ impl Issue {
659675
.context("failed to close issue")?;
660676
Ok(())
661677
}
678+
679+
/// Returns the diff in this event, for Open and Synchronize events for now.
680+
pub async fn diff(&self, client: &GithubClient) -> anyhow::Result<Option<String>> {
681+
let (before, after) = if let (Some(base), Some(head)) = (&self.base, &self.head) {
682+
(base.sha.clone(), head.sha.clone())
683+
} else {
684+
return Ok(None);
685+
};
686+
687+
let mut req = client.get(&format!(
688+
"{}/compare/{}...{}",
689+
self.repository().url(),
690+
before,
691+
after
692+
));
693+
req = req.header("Accept", "application/vnd.github.v3.diff");
694+
let diff = client.send_req(req).await?;
695+
Ok(Some(String::from(String::from_utf8_lossy(&diff))))
696+
}
662697
}
663698

664699
#[derive(serde::Serialize)]
@@ -762,13 +797,6 @@ pub struct IssuesEvent {
762797
pub repository: Repository,
763798
/// Some if action is IssuesAction::Labeled, for example
764799
pub label: Option<Label>,
765-
766-
// These fields are the sha fields before/after a synchronize operation,
767-
// used to compute the diff between these two commits.
768-
#[serde(default)]
769-
before: Option<String>,
770-
#[serde(default)]
771-
after: Option<String>,
772800
}
773801

774802
#[derive(Debug, serde::Deserialize)]
@@ -794,37 +822,7 @@ pub fn files_changed(diff: &str) -> Vec<&str> {
794822
files
795823
}
796824

797-
impl IssuesEvent {
798-
/// Returns the diff in this event, for Open and Synchronize events for now.
799-
pub async fn diff_between(&self, client: &GithubClient) -> anyhow::Result<Option<String>> {
800-
let (before, after) = if self.action == IssuesAction::Synchronize {
801-
(
802-
self.before
803-
.clone()
804-
.expect("synchronize has before populated"),
805-
self.after.clone().expect("synchronize has after populated"),
806-
)
807-
} else if self.action == IssuesAction::Opened {
808-
if let (Some(base), Some(head)) = (&self.issue.base, &self.issue.head) {
809-
(base.sha.clone(), head.sha.clone())
810-
} else {
811-
return Ok(None);
812-
}
813-
} else {
814-
return Ok(None);
815-
};
816-
817-
let mut req = client.get(&format!(
818-
"{}/compare/{}...{}",
819-
self.issue.repository().url(),
820-
before,
821-
after
822-
));
823-
req = req.header("Accept", "application/vnd.github.v3.diff");
824-
let diff = client.send_req(req).await?;
825-
Ok(Some(String::from(String::from_utf8_lossy(&diff))))
826-
}
827-
}
825+
impl IssuesEvent {}
828826

829827
#[derive(Debug, serde::Deserialize)]
830828
pub struct IssueSearchResult {
@@ -1282,6 +1280,7 @@ impl GithubClient {
12821280
self.client.post(url).configure(self)
12831281
}
12841282

1283+
#[allow(unused)]
12851284
fn put(&self, url: &str) -> RequestBuilder {
12861285
log::trace!("put {:?}", url);
12871286
self.client.put(url).configure(self)

src/handlers/autolabel.rs

+59-35
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,55 @@ use crate::{
33
github::{files_changed, IssuesAction, IssuesEvent, Label},
44
handlers::Context,
55
};
6+
use anyhow::Context as _;
67
use tracing as log;
78

89
pub(super) struct AutolabelInput {
9-
labels: Vec<Label>,
10+
add: Vec<Label>,
11+
remove: Vec<Label>,
1012
}
1113

1214
pub(super) async fn parse_input(
1315
ctx: &Context,
1416
event: &IssuesEvent,
1517
config: Option<&AutolabelConfig>,
1618
) -> Result<Option<AutolabelInput>, String> {
19+
// On opening a new PR or sync'ing the branch, look at the diff and try to
20+
// add any appropriate labels.
21+
//
22+
// FIXME: This will re-apply labels after a push that the user had tried to
23+
// remove. Not much can be done about that currently; the before/after on
24+
// synchronize may be straddling a rebase, which will break diff generation.
1725
if let Some(config) = config {
18-
if let Some(diff) = event
19-
.diff_between(&ctx.github)
20-
.await
21-
.map_err(|e| {
22-
log::error!("failed to fetch diff: {:?}", e);
23-
})
24-
.unwrap_or_default()
25-
{
26-
let files = files_changed(&diff);
27-
let mut autolabels = Vec::new();
28-
for (label, cfg) in config.labels.iter() {
29-
if cfg
30-
.trigger_files
31-
.iter()
32-
.any(|f| files.iter().any(|diff_file| diff_file.starts_with(f)))
33-
{
34-
autolabels.push(Label {
35-
name: label.to_owned(),
36-
});
26+
if event.action == IssuesAction::Opened || event.action == IssuesAction::Synchronize {
27+
if let Some(diff) = event
28+
.issue
29+
.diff(&ctx.github)
30+
.await
31+
.map_err(|e| {
32+
log::error!("failed to fetch diff: {:?}", e);
33+
})
34+
.unwrap_or_default()
35+
{
36+
let files = files_changed(&diff);
37+
let mut autolabels = Vec::new();
38+
for (label, cfg) in config.labels.iter() {
39+
if cfg
40+
.trigger_files
41+
.iter()
42+
.any(|f| files.iter().any(|diff_file| diff_file.starts_with(f)))
43+
{
44+
autolabels.push(Label {
45+
name: label.to_owned(),
46+
});
47+
}
48+
}
49+
if !autolabels.is_empty() {
50+
return Ok(Some(AutolabelInput {
51+
add: autolabels,
52+
remove: vec![],
53+
}));
3754
}
38-
}
39-
if !autolabels.is_empty() {
40-
return Ok(Some(AutolabelInput { labels: autolabels }));
4155
}
4256
}
4357
}
@@ -75,17 +89,21 @@ pub(super) async fn parse_input(
7589
});
7690
}
7791
if !autolabels.is_empty() {
78-
return Ok(Some(AutolabelInput { labels: autolabels }));
92+
return Ok(Some(AutolabelInput {
93+
add: autolabels,
94+
remove: vec![],
95+
}));
7996
}
8097
}
8198
}
8299
if event.action == IssuesAction::Closed {
83100
let labels = event.issue.labels();
84-
if let Some(x) = labels.iter().position(|x| x.name == "I-prioritize") {
85-
let mut labels_excluded = labels.to_vec();
86-
labels_excluded.remove(x);
101+
if labels.iter().any(|x| x.name == "I-prioritize") {
87102
return Ok(Some(AutolabelInput {
88-
labels: labels_excluded,
103+
add: vec![],
104+
remove: vec![Label {
105+
name: "I-prioritize".to_owned(),
106+
}],
89107
}));
90108
}
91109
}
@@ -98,13 +116,19 @@ pub(super) async fn handle_input(
98116
event: &IssuesEvent,
99117
input: AutolabelInput,
100118
) -> anyhow::Result<()> {
101-
let mut labels = event.issue.labels().to_owned();
102-
for label in input.labels {
103-
// Don't add the label if it's already there
104-
if !labels.contains(&label) {
105-
labels.push(label);
106-
}
119+
event.issue.add_labels(&ctx.github, input.add).await?;
120+
for label in input.remove {
121+
event
122+
.issue
123+
.remove_label(&ctx.github, &label.name)
124+
.await
125+
.with_context(|| {
126+
format!(
127+
"failed to remove {:?} from {:?}",
128+
label,
129+
event.issue.global_id()
130+
)
131+
})?;
107132
}
108-
event.issue.set_labels(&ctx.github, labels).await?;
109133
Ok(())
110134
}

src/handlers/major_change.rs

+1-3
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,7 @@ async fn handle(
217217
label_to_add: String,
218218
new_proposal: bool,
219219
) -> anyhow::Result<()> {
220-
let mut labels = issue.labels().to_owned();
221-
labels.push(Label { name: label_to_add });
222-
let github_req = issue.set_labels(&ctx.github, labels);
220+
let github_req = issue.add_labels(&ctx.github, vec![Label { name: label_to_add }]);
223221

224222
let partial_issue = issue.to_zulip_github_reference();
225223
let zulip_topic = zulip_topic_from_issue(&partial_issue);

0 commit comments

Comments
 (0)