-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #135 from nikomatsakis/gh-json
generate json files with status of tracking issues
- Loading branch information
Showing
11 changed files
with
543 additions
and
298 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,50 +1,7 @@ | ||
use std::fmt::Display; | ||
//! Code for querying and interacting with github. | ||
//! | ||
//! We do most everything through the `gh` command-line tool. | ||
|
||
use crate::re::TRACKING_ISSUE; | ||
|
||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] | ||
pub struct IssueId { | ||
/// Something like `rust-lang/rust-project-goals` | ||
pub repository: String, | ||
|
||
/// Something like `22` | ||
pub number: u64, | ||
} | ||
|
||
impl IssueId { | ||
pub fn new(repository: &(impl Display + ?Sized), number: u64) -> Self { | ||
Self { | ||
repository: repository.to_string(), | ||
number, | ||
} | ||
} | ||
} | ||
|
||
impl std::fmt::Debug for IssueId { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
write!(f, "{self}") | ||
} | ||
} | ||
|
||
impl std::fmt::Display for IssueId { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
write!( | ||
f, | ||
"[{repository}#{number}]", | ||
repository = self.repository, | ||
number = self.number, | ||
) | ||
} | ||
} | ||
|
||
impl std::str::FromStr for IssueId { | ||
type Err = anyhow::Error; | ||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
let Some(c) = TRACKING_ISSUE.captures(s) else { | ||
anyhow::bail!("invalid issue-id") | ||
}; | ||
|
||
Ok(IssueId::new(&c[1], c[2].parse()?)) | ||
} | ||
} | ||
pub mod issue_id; | ||
pub mod issues; | ||
pub mod labels; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
use crate::re::TRACKING_ISSUE; | ||
use std::fmt::Display; | ||
|
||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] | ||
pub struct IssueId { | ||
/// Something like `rust-lang/rust-project-goals` | ||
pub repository: String, | ||
|
||
/// Something like `22` | ||
pub number: u64, | ||
} | ||
|
||
impl IssueId { | ||
pub fn new(repository: &(impl Display + ?Sized), number: u64) -> Self { | ||
Self { | ||
repository: repository.to_string(), | ||
number, | ||
} | ||
} | ||
} | ||
|
||
impl std::fmt::Debug for IssueId { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
write!(f, "{self}") | ||
} | ||
} | ||
|
||
impl std::fmt::Display for IssueId { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
write!( | ||
f, | ||
"[{repository}#{number}]", | ||
repository = self.repository, | ||
number = self.number, | ||
) | ||
} | ||
} | ||
|
||
impl std::str::FromStr for IssueId { | ||
type Err = anyhow::Error; | ||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
let Some(c) = TRACKING_ISSUE.captures(s) else { | ||
anyhow::bail!("invalid issue-id") | ||
}; | ||
|
||
Ok(IssueId::new(&c[1], c[2].parse()?)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
use std::{ | ||
collections::{BTreeMap, BTreeSet}, | ||
process::Command, | ||
}; | ||
|
||
use serde::{Deserialize, Serialize}; | ||
|
||
use crate::util::comma; | ||
|
||
use super::labels::GhLabel; | ||
|
||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] | ||
pub struct ExistingGithubIssue { | ||
pub number: u64, | ||
/// Just github username, no `@` | ||
pub assignees: BTreeSet<String>, | ||
pub comments: Vec<ExistingGithubComment>, | ||
pub body: String, | ||
pub state: ExistingIssueState, | ||
pub labels: Vec<GhLabel>, | ||
} | ||
|
||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] | ||
pub struct ExistingGithubComment { | ||
/// Just github username, no `@` | ||
pub author: String, | ||
pub body: String, | ||
pub created_at: String, | ||
pub url: String, | ||
} | ||
|
||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] | ||
struct ExistingGithubIssueJson { | ||
title: String, | ||
number: u64, | ||
assignees: Vec<ExistingGithubAssigneeJson>, | ||
comments: Vec<ExistingGithubCommentJson>, | ||
body: String, | ||
state: ExistingIssueState, | ||
labels: Vec<GhLabel>, | ||
} | ||
|
||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] | ||
struct ExistingGithubAssigneeJson { | ||
login: String, | ||
name: String, | ||
} | ||
|
||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] | ||
struct ExistingGithubCommentJson { | ||
body: String, | ||
author: ExistingGithubAuthorJson, | ||
#[serde(rename = "createdAt")] | ||
created_at: String, | ||
url: String, | ||
} | ||
|
||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] | ||
struct ExistingGithubAuthorJson { | ||
login: String, | ||
} | ||
|
||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] | ||
#[serde(rename_all = "UPPERCASE")] | ||
pub enum ExistingIssueState { | ||
Open, | ||
Closed, | ||
} | ||
|
||
pub fn list_issue_titles_in_milestone( | ||
repository: &str, | ||
timeframe: &str, | ||
) -> anyhow::Result<BTreeMap<String, ExistingGithubIssue>> { | ||
let output = Command::new("gh") | ||
.arg("-R") | ||
.arg(repository) | ||
.arg("issue") | ||
.arg("list") | ||
.arg("-m") | ||
.arg(timeframe) | ||
.arg("-s") | ||
.arg("all") | ||
.arg("--json") | ||
.arg("title,assignees,number,comments,body,state,labels") | ||
.output()?; | ||
|
||
let existing_issues: Vec<ExistingGithubIssueJson> = serde_json::from_slice(&output.stdout)?; | ||
|
||
Ok(existing_issues | ||
.into_iter() | ||
.map(|e_i| { | ||
( | ||
e_i.title, | ||
ExistingGithubIssue { | ||
number: e_i.number, | ||
assignees: e_i.assignees.into_iter().map(|a| a.login).collect(), | ||
comments: e_i | ||
.comments | ||
.into_iter() | ||
.map(|c| ExistingGithubComment { | ||
author: format!("@{}", c.author.login), | ||
body: c.body, | ||
url: c.url, | ||
created_at: c.created_at, | ||
}) | ||
.collect(), | ||
body: e_i.body, | ||
state: e_i.state, | ||
labels: e_i.labels, | ||
}, | ||
) | ||
}) | ||
.collect()) | ||
} | ||
|
||
pub fn create_issue( | ||
repository: &str, | ||
body: &str, | ||
title: &str, | ||
labels: &[String], | ||
assignees: &BTreeSet<String>, | ||
milestone: &str, | ||
) -> anyhow::Result<()> { | ||
let output = Command::new("gh") | ||
.arg("-R") | ||
.arg(&repository) | ||
.arg("issue") | ||
.arg("create") | ||
.arg("-b") | ||
.arg(&body) | ||
.arg("-t") | ||
.arg(&title) | ||
.arg("-l") | ||
.arg(labels.join(",")) | ||
.arg("-a") | ||
.arg(comma(&assignees)) | ||
.arg("-m") | ||
.arg(&milestone) | ||
.output()?; | ||
|
||
if !output.status.success() { | ||
Err(anyhow::anyhow!( | ||
"failed to create issue `{}`: {}", | ||
title, | ||
String::from_utf8_lossy(&output.stderr) | ||
)) | ||
} else { | ||
Ok(()) | ||
} | ||
} | ||
|
||
pub fn sync_assignees( | ||
repository: &str, | ||
number: u64, | ||
remove_owners: &BTreeSet<String>, | ||
add_owners: &BTreeSet<String>, | ||
) -> anyhow::Result<()> { | ||
let mut command = Command::new("gh"); | ||
command | ||
.arg("-R") | ||
.arg(&repository) | ||
.arg("issue") | ||
.arg("edit") | ||
.arg(number.to_string()); | ||
|
||
if !remove_owners.is_empty() { | ||
command.arg("--remove-assignee").arg(comma(&remove_owners)); | ||
} | ||
|
||
if !add_owners.is_empty() { | ||
command.arg("--add-assignee").arg(comma(&add_owners)); | ||
} | ||
|
||
let output = command.output()?; | ||
if !output.status.success() { | ||
Err(anyhow::anyhow!( | ||
"failed to sync issue `{}`: {}", | ||
number, | ||
String::from_utf8_lossy(&output.stderr) | ||
)) | ||
} else { | ||
Ok(()) | ||
} | ||
} | ||
|
||
const LOCK_TEXT: &str = "This issue is intended for status updates only.\n\nFor general questions or comments, please contact the owner(s) directly."; | ||
|
||
impl ExistingGithubIssue { | ||
/// We use the presence of a "lock comment" as a signal that we successfully locked the issue. | ||
/// The github CLI doesn't let you query that directly. | ||
pub fn was_locked(&self) -> bool { | ||
self.comments.iter().any(|c| c.body.trim() == LOCK_TEXT) | ||
} | ||
} | ||
|
||
pub fn lock_issue(repository: &str, number: u64) -> anyhow::Result<()> { | ||
let output = Command::new("gh") | ||
.arg("-R") | ||
.arg(repository) | ||
.arg("issue") | ||
.arg("lock") | ||
.arg(number.to_string()) | ||
.output()?; | ||
|
||
if !output.status.success() { | ||
if !output.stderr.starts_with(b"already locked") { | ||
return Err(anyhow::anyhow!( | ||
"failed to lock issue `{}`: {}", | ||
number, | ||
String::from_utf8_lossy(&output.stderr) | ||
)); | ||
} | ||
} | ||
|
||
// Leave a comment explaining what is going on. | ||
let output = Command::new("gh") | ||
.arg("-R") | ||
.arg(repository) | ||
.arg("issue") | ||
.arg("comment") | ||
.arg(number.to_string()) | ||
.arg("-b") | ||
.arg(LOCK_TEXT) | ||
.output()?; | ||
|
||
if !output.status.success() { | ||
return Err(anyhow::anyhow!( | ||
"failed to leave lock comment `{}`: {}", | ||
number, | ||
String::from_utf8_lossy(&output.stderr) | ||
)); | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
impl ExistingGithubComment { | ||
/// True if this is one of the special comments that we put on issues. | ||
pub fn is_automated_comment(&self) -> bool { | ||
self.body.trim() == LOCK_TEXT | ||
} | ||
} |
Oops, something went wrong.