Skip to content

Commit e4882cc

Browse files
committed
start code to reconcile github state
1 parent d5a4587 commit e4882cc

File tree

4 files changed

+226
-24
lines changed

4 files changed

+226
-24
lines changed

mdbook-goals/src/goal.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,3 +369,14 @@ fn extract_identifiers(s: &str) -> Vec<&str> {
369369
let regex = Regex::new("[-.A-Za-z]+").unwrap();
370370
regex.find_iter(s).map(|m| m.as_str()).collect()
371371
}
372+
373+
impl Metadata {
374+
/// Extracts the `@abc` usernames found in the owner listing.
375+
pub fn owner_usernames(&self) -> Vec<&str> {
376+
self.owners
377+
.split(char::is_whitespace)
378+
.filter_map(|owner| USERNAME.captures(owner))
379+
.map(|captures| captures.get(0).unwrap().as_str())
380+
.collect()
381+
}
382+
}

mdbook-goals/src/main.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ mod util;
1919
struct Opt {
2020
#[structopt(subcommand)]
2121
cmd: Option<Command>,
22+
23+
/// Repository to use if applicable
24+
#[structopt(long, default_value = "rust-lang/rust-project-goals")]
25+
repository: String,
2226
}
2327

2428
#[derive(StructOpt, Debug)]
@@ -33,6 +37,14 @@ enum Command {
3337
/// Print the RFC text to stdout
3438
RFC { path: PathBuf },
3539

40+
/// Use `gh` CLI tool to create issues on the rust-lang/rust-project-goals repository
41+
Issues {
42+
path: PathBuf,
43+
44+
#[structopt(short = "-n", long)]
45+
dry_run: bool,
46+
},
47+
3648
/// Checks that the goal documents are well-formed, intended for use within CI
3749
Check {},
3850
}
@@ -57,6 +69,10 @@ fn main() -> anyhow::Result<()> {
5769
rfc::generate_rfc(&path)?;
5870
}
5971

72+
Some(Command::Issues { path, dry_run }) => {
73+
rfc::generate_issues(&opt.repository, path, *dry_run)?;
74+
}
75+
6076
None => {
6177
handle_preprocessing(&GoalPreprocessor)?;
6278
}

mdbook-goals/src/rfc.rs

Lines changed: 194 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,45 @@
11
use std::{
22
collections::BTreeSet,
3+
fmt::Display,
34
path::{Path, PathBuf},
45
process::Command,
56
};
67

78
use anyhow::Context;
89
use regex::Regex;
10+
use serde::{Deserialize, Serialize};
911

10-
use crate::{goal, team::TeamName};
12+
use crate::{
13+
goal::{self, GoalDocument},
14+
team::TeamName,
15+
};
16+
17+
fn validate_path(path: &Path) -> anyhow::Result<String> {
18+
if !path.is_dir() {
19+
return Err(anyhow::anyhow!(
20+
"RFC path should be a directory like src/2024h2"
21+
));
22+
};
23+
24+
if path.is_absolute() {
25+
return Err(anyhow::anyhow!("RFC path should be relative"));
26+
}
27+
28+
let timeframe = path
29+
.components()
30+
.last()
31+
.unwrap()
32+
.as_os_str()
33+
.to_str()
34+
.ok_or_else(|| anyhow::anyhow!("invalid path `{}`", path.display()))?;
35+
36+
Ok(timeframe.to_string())
37+
}
1138

1239
pub fn generate_comment(path: &Path) -> anyhow::Result<()> {
40+
let _ = validate_path(path)?;
1341
let goal_documents = goal::goals_in_dir(path)?;
14-
let teams_with_asks: BTreeSet<&TeamName> = goal_documents
15-
.iter()
16-
.flat_map(|g| &g.team_asks)
17-
.flat_map(|ask| &ask.teams)
18-
.copied()
19-
.collect();
42+
let teams_with_asks = teams_with_asks(&goal_documents);
2043

2144
for team_name in teams_with_asks {
2245
let team_data = team_name.data();
@@ -38,23 +61,7 @@ pub fn generate_comment(path: &Path) -> anyhow::Result<()> {
3861
}
3962

4063
pub fn generate_rfc(path: &Path) -> anyhow::Result<()> {
41-
if !path.is_dir() {
42-
return Err(anyhow::anyhow!(
43-
"RFC path should be a directory like src/2024h2"
44-
));
45-
};
46-
47-
if path.is_absolute() {
48-
return Err(anyhow::anyhow!("RFC path should be relative"));
49-
}
50-
51-
let timeframe = path
52-
.components()
53-
.last()
54-
.unwrap()
55-
.as_os_str()
56-
.to_str()
57-
.ok_or_else(|| anyhow::anyhow!("invalid path `{}`", path.display()))?;
64+
let timeframe = &validate_path(path)?;
5865

5966
// run mdbook build
6067
Command::new("mdbook").arg("build").status()?;
@@ -88,3 +95,166 @@ pub fn generate_rfc(path: &Path) -> anyhow::Result<()> {
8895

8996
Ok(())
9097
}
98+
99+
pub fn generate_issues(repository: &str, path: &Path, dry_run: bool) -> anyhow::Result<()> {
100+
let _ = validate_path(path)?;
101+
102+
let goal_documents = goal::goals_in_dir(path)?;
103+
let teams_with_asks = teams_with_asks(&goal_documents);
104+
// let issues: Vec<_> = goal_documents
105+
// .iter()
106+
// .map(|goal_document| {
107+
// let title = format!("Goal: {}", goal_document.title);
108+
// let owners = goal_document.metadata.owner_usernames();
109+
// let body = goal_document.description.clone();
110+
// let teams = goal_document
111+
// .team_asks
112+
// .iter()
113+
// .flat_map(|ask| &ask.teams)
114+
// .copied()
115+
// .collect::<BTreeSet<&TeamName>>();
116+
117+
// GithubIssue {
118+
// title,
119+
// owners,
120+
// body,
121+
// teams,
122+
// }
123+
// })
124+
// .collect();
125+
126+
let mut actions = initialize_labels(repository, &teams_with_asks)?;
127+
128+
eprintln!("Actions to be executed:");
129+
for action in &actions {
130+
eprintln!("* {action}");
131+
}
132+
133+
if !dry_run {
134+
for action in actions {
135+
action.execute(repository)?;
136+
}
137+
}
138+
139+
Ok(())
140+
}
141+
142+
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
143+
pub struct GithubIssue {
144+
pub title: String,
145+
pub owners: Vec<String>,
146+
pub body: String,
147+
pub teams: BTreeSet<&'static TeamName>,
148+
}
149+
150+
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
151+
enum GithubAction {
152+
CreateLabel { name: String, color: String },
153+
CreateIssue { issue: GithubIssue },
154+
}
155+
156+
#[derive(Debug, Serialize, Deserialize)]
157+
struct GhLabel {
158+
name: String,
159+
color: String,
160+
}
161+
162+
fn list_labels(repository: &str) -> anyhow::Result<Vec<GhLabel>> {
163+
let output = Command::new("gh")
164+
.arg("-R")
165+
.arg(repository)
166+
.arg("label")
167+
.arg("list")
168+
.arg("--json")
169+
.arg("name,color")
170+
.output()?;
171+
172+
let labels: Vec<GhLabel> = serde_json::from_slice(&output.stdout)?;
173+
174+
Ok(labels)
175+
}
176+
177+
/// Initializes the required `T-<team>` labels on the repository.
178+
/// Warns if the labels are found with wrong color.
179+
pub fn initialize_labels(
180+
repository: &str,
181+
teams_with_asks: &BTreeSet<&TeamName>,
182+
) -> anyhow::Result<BTreeSet<GithubAction>> {
183+
const TEAM_LABEL_COLOR: &str = "bfd4f2";
184+
185+
let existing_labels = list_labels(repository)?;
186+
187+
Ok(teams_with_asks
188+
.iter()
189+
.flat_map(|team| {
190+
let label_name = team.gh_label();
191+
192+
if let Some(existing_label) = existing_labels
193+
.iter()
194+
.find(|label| label.name == label_name)
195+
{
196+
if existing_label.color == TEAM_LABEL_COLOR {
197+
return None;
198+
}
199+
}
200+
201+
Some(GithubAction::CreateLabel {
202+
name: label_name,
203+
color: TEAM_LABEL_COLOR.to_string(),
204+
})
205+
})
206+
.collect())
207+
}
208+
209+
fn teams_with_asks(goal_documents: &[GoalDocument]) -> BTreeSet<&'static TeamName> {
210+
goal_documents
211+
.iter()
212+
.flat_map(|g| &g.team_asks)
213+
.flat_map(|ask| &ask.teams)
214+
.copied()
215+
.collect()
216+
}
217+
218+
impl Display for GithubAction {
219+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220+
match self {
221+
GithubAction::CreateLabel { name, color } => {
222+
write!(f, "create label `{}` with color `{}`", name, color)
223+
}
224+
GithubAction::CreateIssue { issue } => {
225+
write!(f, "create issue `{}`", issue.title)
226+
}
227+
}
228+
}
229+
}
230+
231+
impl GithubAction {
232+
pub fn execute(self, repository: &str) -> anyhow::Result<()> {
233+
match self {
234+
GithubAction::CreateLabel { name, color } => {
235+
let output = Command::new("gh")
236+
.arg("-R")
237+
.arg(repository)
238+
.arg("label")
239+
.arg("create")
240+
.arg(&name)
241+
.arg("--color")
242+
.arg(&color)
243+
.arg("--force")
244+
.output()?;
245+
246+
if !output.status.success() {
247+
Err(anyhow::anyhow!(
248+
"failed to create label `{}`: {}",
249+
name,
250+
String::from_utf8_lossy(&output.stderr)
251+
))
252+
} else {
253+
Ok(())
254+
}
255+
}
256+
257+
GithubAction::CreateIssue { issue } => todo!(),
258+
}
259+
}
260+
}

mdbook-goals/src/team.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ impl TeamName {
7777
// FIXME: do better :)
7878
format!("https://www.rust-lang.org/governance/teams")
7979
}
80+
81+
/// Label to use on github
82+
pub fn gh_label(&self) -> String {
83+
format!("T-{}", self.0)
84+
}
8085
}
8186

8287
fn fetch<T>(path: &str) -> anyhow::Result<T>

0 commit comments

Comments
 (0)