Skip to content

Commit 76c987e

Browse files
committed
ability to synchronize issues
1 parent 701b2e3 commit 76c987e

File tree

2 files changed

+125
-18
lines changed

2 files changed

+125
-18
lines changed

mdbook-goals/src/rfc.rs

Lines changed: 122 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::{
2-
collections::BTreeSet,
2+
collections::{BTreeMap, BTreeSet},
33
fmt::Display,
44
path::{Path, PathBuf},
55
process::Command,
@@ -152,15 +152,26 @@ pub fn generate_issues(
152152
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
153153
pub struct GithubIssue {
154154
pub title: String,
155-
pub assignees: Vec<String>,
155+
pub assignees: BTreeSet<String>,
156156
pub body: String,
157157
pub labels: Vec<String>,
158158
}
159159

160160
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
161161
enum GithubAction {
162-
CreateLabel { label: GhLabel },
163-
CreateIssue { issue: GithubIssue },
162+
CreateLabel {
163+
label: GhLabel,
164+
},
165+
CreateIssue {
166+
issue: GithubIssue,
167+
},
168+
169+
// We intentionally do not sync the issue *text*, because it may have been edited.
170+
SyncIssue {
171+
number: u64,
172+
remove_owners: BTreeSet<String>,
173+
add_owners: BTreeSet<String>,
174+
},
164175
}
165176

166177
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
@@ -171,7 +182,21 @@ struct GhLabel {
171182

172183
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
173184
struct ExistingGithubIssue {
185+
number: u64,
186+
assignees: BTreeSet<String>,
187+
}
188+
189+
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
190+
struct ExistingGithubIssueJson {
174191
title: String,
192+
number: u64,
193+
assignees: Vec<ExistingGithubAssigneeJson>,
194+
}
195+
196+
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
197+
struct ExistingGithubAssigneeJson {
198+
login: String,
199+
name: String,
175200
}
176201

177202
fn list_labels(repository: &str) -> anyhow::Result<Vec<GhLabel>> {
@@ -192,7 +217,7 @@ fn list_labels(repository: &str) -> anyhow::Result<Vec<GhLabel>> {
192217
fn list_issue_titles_in_milestone(
193218
repository: &str,
194219
timeframe: &str,
195-
) -> anyhow::Result<BTreeSet<String>> {
220+
) -> anyhow::Result<BTreeMap<String, ExistingGithubIssue>> {
196221
let output = Command::new("gh")
197222
.arg("-R")
198223
.arg(repository)
@@ -201,12 +226,27 @@ fn list_issue_titles_in_milestone(
201226
.arg("-m")
202227
.arg(timeframe)
203228
.arg("--json")
204-
.arg("title")
229+
.arg("title,assignees,number")
205230
.output()?;
206231

207-
let existing_issues: Vec<ExistingGithubIssue> = serde_json::from_slice(&output.stdout)?;
232+
let existing_issues: Vec<ExistingGithubIssueJson> = serde_json::from_slice(&output.stdout)?;
208233

209-
Ok(existing_issues.into_iter().map(|e_i| e_i.title).collect())
234+
Ok(existing_issues
235+
.into_iter()
236+
.map(|e_i| {
237+
(
238+
e_i.title,
239+
ExistingGithubIssue {
240+
number: e_i.number,
241+
assignees: e_i
242+
.assignees
243+
.iter()
244+
.map(|a| format!("@{}", a.login))
245+
.collect(),
246+
},
247+
)
248+
})
249+
.collect())
210250
}
211251
/// Initializes the required `T-<team>` labels on the repository.
212252
/// Warns if the labels are found with wrong color.
@@ -256,26 +296,50 @@ fn initialize_issues(
256296
goal_documents: &[GoalDocument],
257297
) -> anyhow::Result<BTreeSet<GithubAction>> {
258298
// the set of issues we want to exist
259-
let mut desired_issues: BTreeSet<GithubIssue> = goal_documents
299+
let desired_issues: BTreeSet<GithubIssue> = goal_documents
260300
.iter()
261301
.map(|goal_document| issue(timeframe, goal_document))
262302
.collect::<anyhow::Result<_>>()?;
263303

264-
// remove any existings that already exist
304+
// Compare desired issues against existing issues
265305
let existing_issues = list_issue_titles_in_milestone(repository, timeframe)?;
266-
desired_issues.retain(|i| !existing_issues.contains(&i.title));
306+
let mut actions = BTreeSet::new();
307+
for desired_issue in desired_issues {
308+
match existing_issues.get(&desired_issue.title) {
309+
Some(existing_issue) => {
310+
if existing_issue.assignees != desired_issue.assignees {
311+
actions.insert(GithubAction::SyncIssue {
312+
number: existing_issue.number,
313+
remove_owners: existing_issue
314+
.assignees
315+
.difference(&desired_issue.assignees)
316+
.cloned()
317+
.collect(),
318+
add_owners: desired_issue
319+
.assignees
320+
.difference(&existing_issue.assignees)
321+
.cloned()
322+
.collect(),
323+
});
324+
}
325+
}
267326

268-
Ok(desired_issues
269-
.into_iter()
270-
.map(|issue| GithubAction::CreateIssue { issue })
271-
.collect())
327+
None => {
328+
actions.insert(GithubAction::CreateIssue {
329+
issue: desired_issue,
330+
});
331+
}
332+
}
333+
}
334+
335+
Ok(actions)
272336
}
273337

274338
fn issue(timeframe: &str, document: &GoalDocument) -> anyhow::Result<GithubIssue> {
275-
let mut assignees = vec![];
339+
let mut assignees = BTreeSet::default();
276340
for username in document.metadata.owner_usernames() {
277341
if get_person_data(username)?.is_some() {
278-
assignees.push(username[1..].to_string());
342+
assignees.insert(username[1..].to_string());
279343
}
280344
}
281345

@@ -388,6 +452,9 @@ impl Display for GithubAction {
388452
GithubAction::CreateIssue { issue } => {
389453
write!(f, "create issue \"{}\"", issue.title)
390454
}
455+
GithubAction::SyncIssue { number, .. } => {
456+
write!(f, "sync issue #{}", number)
457+
}
391458
}
392459
}
393460
}
@@ -441,7 +508,7 @@ impl GithubAction {
441508
.arg("-l")
442509
.arg(labels.join(","))
443510
.arg("-a")
444-
.arg(assignees.join(","))
511+
.arg(comma(&assignees))
445512
.arg("-m")
446513
.arg(&timeframe)
447514
.output()?;
@@ -456,6 +523,43 @@ impl GithubAction {
456523
Ok(())
457524
}
458525
}
526+
GithubAction::SyncIssue {
527+
number,
528+
remove_owners,
529+
add_owners,
530+
} => {
531+
let mut command = Command::new("gh");
532+
command
533+
.arg("-R")
534+
.arg(&repository)
535+
.arg("issue")
536+
.arg("edit")
537+
.arg(number.to_string());
538+
539+
if !remove_owners.is_empty() {
540+
command.arg("--remove-assignee").arg(comma(&remove_owners));
541+
}
542+
543+
if !add_owners.is_empty() {
544+
command.arg("--add-assignee").arg(comma(&add_owners));
545+
}
546+
547+
let output = command.output()?;
548+
if !output.status.success() {
549+
Err(anyhow::anyhow!(
550+
"failed to sync issue `{}`: {}",
551+
number,
552+
String::from_utf8_lossy(&output.stderr)
553+
))
554+
} else {
555+
Ok(())
556+
}
557+
}
459558
}
460559
}
461560
}
561+
562+
/// Returns a comma-separated list of the strings in `s` (no spaces).
563+
fn comma(s: &BTreeSet<String>) -> String {
564+
s.iter().map(|s| &s[..]).collect::<Vec<_>>().join(",")
565+
}

mdbook-goals/src/team_repo.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,5 +107,8 @@ fn team_file(owners: &BTreeSet<&str>) -> anyhow::Result<String> {
107107
}
108108
writeln!(out, "]")?;
109109
writeln!(out, "included-teams = []")?;
110+
writeln!(out, "")?;
111+
writeln!(out, "[[github]]")?;
112+
writeln!(out, "orgs = [\"rust-lang\"]")?;
110113
Ok(out)
111114
}

0 commit comments

Comments
 (0)