Skip to content

Commit 249a062

Browse files
authored
Handle ability to change milestone (#249)
* Handle ability to change milestone * Comment on issue when changing milestone * Extract list_issues with a filter fn * Make lock_issue reuse create_comment code * Milestone dueOn could be null
1 parent 00a97e1 commit 249a062

File tree

4 files changed

+138
-29
lines changed

4 files changed

+138
-29
lines changed

crates/rust-project-goals-cli/src/rfc.rs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ use rust_project_goals::{
1313
gh::{
1414
issue_id::{IssueId, Repository},
1515
issues::{
16-
create_issue, list_issues_in_milestone, lock_issue, sync_assignees, FLAGSHIP_LABEL,
16+
change_milestone, create_comment, create_issue, list_tracking_issues, lock_issue,
17+
sync_assignees, FLAGSHIP_LABEL, LOCK_TEXT,
1718
},
1819
labels::GhLabel,
1920
},
@@ -177,6 +178,16 @@ enum GithubAction<'doc> {
177178
issue: GithubIssue<'doc>,
178179
},
179180

181+
ChangeMilestone {
182+
number: u64,
183+
milestone: String,
184+
},
185+
186+
Comment {
187+
number: u64,
188+
body: String,
189+
},
190+
180191
// We intentionally do not sync the issue *text*, because it may have been edited.
181192
SyncAssignees {
182193
number: u64,
@@ -248,7 +259,8 @@ fn initialize_issues<'doc>(
248259
.collect::<anyhow::Result<_>>()?;
249260

250261
// Compare desired issues against existing issues
251-
let existing_issues = list_issues_in_milestone(repository, timeframe)?;
262+
let existing_issues = list_tracking_issues(repository)?;
263+
252264
let mut actions = BTreeSet::new();
253265
for desired_issue in desired_issues {
254266
match existing_issues.iter().find(|issue| {
@@ -272,10 +284,29 @@ fn initialize_issues<'doc>(
272284
});
273285
}
274286

287+
if existing_issue.milestone.as_ref().map(|m| m.title.as_str()) != Some(timeframe) {
288+
actions.insert(GithubAction::ChangeMilestone {
289+
number: existing_issue.number,
290+
milestone: timeframe.to_string(),
291+
});
292+
actions.insert(GithubAction::Comment {
293+
number: existing_issue.number,
294+
body: format!(
295+
"This is a continuing project goal, and the updates below \
296+
this comment will be for the new period {}",
297+
timeframe
298+
),
299+
});
300+
}
301+
275302
if !existing_issue.was_locked() {
276303
actions.insert(GithubAction::LockIssue {
277304
number: existing_issue.number,
278305
});
306+
actions.insert(GithubAction::Comment {
307+
number: existing_issue.number,
308+
body: LOCK_TEXT.to_string(),
309+
});
279310
}
280311

281312
let issue_id = IssueId::new(repository.clone(), existing_issue.number);
@@ -419,6 +450,12 @@ impl Display for GithubAction<'_> {
419450
GithubAction::CreateIssue { issue } => {
420451
write!(f, "create issue \"{}\"", issue.title)
421452
}
453+
GithubAction::ChangeMilestone { number, milestone } => {
454+
write!(f, "update issue #{} milestone to \"{}\"", number, milestone)
455+
}
456+
GithubAction::Comment { number, body } => {
457+
write!(f, "post comment on issue #{}: \"{}\"", number, body)
458+
}
422459
GithubAction::SyncAssignees {
423460
number,
424461
remove_owners,
@@ -478,6 +515,17 @@ impl GithubAction<'_> {
478515

479516
Ok(())
480517
}
518+
519+
GithubAction::ChangeMilestone { number, milestone } => {
520+
change_milestone(repository, number, &milestone)?;
521+
Ok(())
522+
}
523+
524+
GithubAction::Comment { number, body } => {
525+
create_comment(repository, number, &body)?;
526+
Ok(())
527+
}
528+
481529
GithubAction::SyncAssignees {
482530
number,
483531
remove_owners,

crates/rust-project-goals/src/gh.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
pub mod issue_id;
66
pub mod issues;
77
pub mod labels;
8+
pub mod milestone;

crates/rust-project-goals/src/gh/issues.rs

Lines changed: 77 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
77

88
use crate::{re, util::comma};
99

10-
use super::{issue_id::Repository, labels::GhLabel};
10+
use super::{issue_id::Repository, labels::GhLabel, milestone::GhMilestone};
1111

1212
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
1313
pub struct ExistingGithubIssue {
@@ -19,6 +19,7 @@ pub struct ExistingGithubIssue {
1919
pub body: String,
2020
pub state: GithubIssueState,
2121
pub labels: Vec<GhLabel>,
22+
pub milestone: Option<GhMilestone>,
2223
}
2324

2425
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
@@ -39,6 +40,7 @@ struct ExistingGithubIssueJson {
3940
body: String,
4041
state: GithubIssueState,
4142
labels: Vec<GhLabel>,
43+
milestone: Option<GhMilestone>,
4244
}
4345

4446
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
@@ -122,17 +124,34 @@ pub fn list_issues_in_milestone(
122124
repository: &Repository,
123125
timeframe: &str,
124126
) -> anyhow::Result<Vec<ExistingGithubIssue>> {
125-
let output = Command::new("gh")
126-
.arg("-R")
127+
list_issues(repository, &[("-m", timeframe)])
128+
}
129+
130+
pub fn list_tracking_issues(repository: &Repository) -> anyhow::Result<Vec<ExistingGithubIssue>> {
131+
list_issues(repository, &[("-l", "C-tracking-issue")])
132+
}
133+
134+
pub fn list_issues(
135+
repository: &Repository,
136+
filter: &[(&str, &str)],
137+
) -> anyhow::Result<Vec<ExistingGithubIssue>> {
138+
let mut cmd = Command::new("gh");
139+
140+
cmd.arg("-R")
127141
.arg(&repository.to_string())
128142
.arg("issue")
129143
.arg("list")
130-
.arg("-m")
131-
.arg(timeframe)
132144
.arg("-s")
133-
.arg("all")
145+
.arg("all");
146+
147+
for (opt, val) in filter {
148+
cmd.arg(opt);
149+
cmd.arg(val);
150+
}
151+
152+
let output = cmd
134153
.arg("--json")
135-
.arg("title,assignees,number,comments,body,state,labels")
154+
.arg("title,assignees,number,comments,body,state,labels,milestone")
136155
.output()
137156
.with_context(|| format!("running github cli tool `gh`"))?;
138157

@@ -180,6 +199,55 @@ pub fn create_issue(
180199
}
181200
}
182201

202+
pub fn change_milestone(
203+
repository: &Repository,
204+
number: u64,
205+
milestone: &str,
206+
) -> anyhow::Result<()> {
207+
let mut command = Command::new("gh");
208+
command
209+
.arg("-R")
210+
.arg(&repository.to_string())
211+
.arg("issue")
212+
.arg("edit")
213+
.arg(number.to_string())
214+
.arg("-m")
215+
.arg(milestone);
216+
217+
let output = command.output()?;
218+
if !output.status.success() {
219+
Err(anyhow::anyhow!(
220+
"failed to change milestone `{}`: {}",
221+
number,
222+
String::from_utf8_lossy(&output.stderr)
223+
))
224+
} else {
225+
Ok(())
226+
}
227+
}
228+
229+
pub fn create_comment(repository: &Repository, number: u64, body: &str) -> anyhow::Result<()> {
230+
let output = Command::new("gh")
231+
.arg("-R")
232+
.arg(&repository.to_string())
233+
.arg("issue")
234+
.arg("comment")
235+
.arg(number.to_string())
236+
.arg("-b")
237+
.arg(body)
238+
.output()?;
239+
240+
if !output.status.success() {
241+
Err(anyhow::anyhow!(
242+
"failed to leave comment on issue `{}`: {}",
243+
number,
244+
String::from_utf8_lossy(&output.stderr)
245+
))
246+
} else {
247+
Ok(())
248+
}
249+
}
250+
183251
pub fn sync_assignees(
184252
repository: &Repository,
185253
number: u64,
@@ -216,7 +284,7 @@ pub fn sync_assignees(
216284

217285
pub const FLAGSHIP_LABEL: &str = "Flagship Goal";
218286

219-
const LOCK_TEXT: &str = "This issue is intended for status updates only.\n\nFor general questions or comments, please contact the owner(s) directly.";
287+
pub const LOCK_TEXT: &str = "This issue is intended for status updates only.\n\nFor general questions or comments, please contact the owner(s) directly.";
220288

221289
impl ExistingGithubIssue {
222290
/// We use the presence of a "lock comment" as a signal that we successfully locked the issue.
@@ -255,25 +323,6 @@ pub fn lock_issue(repository: &Repository, number: u64) -> anyhow::Result<()> {
255323
}
256324
}
257325

258-
// Leave a comment explaining what is going on.
259-
let output = Command::new("gh")
260-
.arg("-R")
261-
.arg(&repository.to_string())
262-
.arg("issue")
263-
.arg("comment")
264-
.arg(number.to_string())
265-
.arg("-b")
266-
.arg(LOCK_TEXT)
267-
.output()?;
268-
269-
if !output.status.success() {
270-
return Err(anyhow::anyhow!(
271-
"failed to leave lock comment `{}`: {}",
272-
number,
273-
String::from_utf8_lossy(&output.stderr)
274-
));
275-
}
276-
277326
Ok(())
278327
}
279328

@@ -308,6 +357,7 @@ impl From<ExistingGithubIssueJson> for ExistingGithubIssue {
308357
body: e_i.body,
309358
state: e_i.state,
310359
labels: e_i.labels,
360+
milestone: e_i.milestone,
311361
}
312362
}
313363
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
4+
pub struct GhMilestone {
5+
pub number: u64,
6+
pub title: String,
7+
pub description: String,
8+
#[serde(rename = "dueOn")]
9+
pub due_on: Option<String>,
10+
}

0 commit comments

Comments
 (0)