Skip to content

Commit 05c7505

Browse files
committed
display goal progress with a tracking bar
1 parent bca84de commit 05c7505

File tree

13 files changed

+263
-64
lines changed

13 files changed

+263
-64
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
book
22
target
33
.cache
4+
src/api

book.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ command = "cargo run -p mdbook-goals --"
2727
git-repository-url = "https://github.com/rust-lang/rust-project-goals"
2828
edit-url-template = "https://github.com/rust-lang/rust-project-goals/edit/main/{path}"
2929
site-url = "/rust-project-goals/"
30+
additional-js = ["src/update-progress-bars.js"]
3031

3132
[output.html.fold]
3233
enable = true

justfile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
api:
2+
cargo run -- json 2024h2 --json-path src/api/2024h2.json
3+
4+
serve: api
5+
mdbook serve
6+
7+
build: api
8+
mdbook build

mdbook-goals/src/gh/issue_id.rs

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,62 @@
1-
use crate::re::TRACKING_ISSUE;
1+
use crate::re::{REPOSITORY, TRACKING_ISSUE};
22
use std::fmt::Display;
33

4+
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
5+
pub struct Repository {
6+
/// Something like `rust-lang`
7+
pub org: String,
8+
9+
/// Something like `rust-project-goals`
10+
pub repo: String,
11+
}
12+
13+
impl Repository {
14+
pub fn new(org: &(impl Display + ?Sized), repo: &(impl Display + ?Sized)) -> Self {
15+
Self {
16+
org: org.to_string(),
17+
repo: repo.to_string(),
18+
}
19+
}
20+
}
21+
22+
impl std::fmt::Display for Repository {
23+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24+
let Repository { org, repo } = self;
25+
write!(f, "{org}/{repo}")
26+
}
27+
}
28+
29+
impl std::str::FromStr for Repository {
30+
type Err = anyhow::Error;
31+
32+
fn from_str(s: &str) -> Result<Self, Self::Err> {
33+
let Some(c) = REPOSITORY.captures(s) else {
34+
anyhow::bail!("invalid repository `{s}`")
35+
};
36+
37+
Ok(Repository::new(&c[1], &c[2]))
38+
}
39+
}
40+
441
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
542
pub struct IssueId {
6-
/// Something like `rust-lang/rust-project-goals`
7-
pub repository: String,
43+
pub repository: Repository,
844

945
/// Something like `22`
1046
pub number: u64,
1147
}
1248

1349
impl IssueId {
14-
pub fn new(repository: &(impl Display + ?Sized), number: u64) -> Self {
15-
Self {
16-
repository: repository.to_string(),
50+
pub fn new(repository: Repository, number: u64) -> Self {
51+
Self { repository, number }
52+
}
53+
54+
pub fn url(&self) -> String {
55+
let IssueId {
56+
repository: Repository { org, repo },
1757
number,
18-
}
58+
} = self;
59+
format!("https://github.com/{org}/{repo}/issues/{number}")
1960
}
2061
}
2162

@@ -27,12 +68,11 @@ impl std::fmt::Debug for IssueId {
2768

2869
impl std::fmt::Display for IssueId {
2970
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30-
write!(
31-
f,
32-
"[{repository}#{number}]",
33-
repository = self.repository,
34-
number = self.number,
35-
)
71+
let IssueId {
72+
repository: Repository { org, repo },
73+
number,
74+
} = self;
75+
write!(f, "[{org}/{repo}#{number}]")
3676
}
3777
}
3878

@@ -44,6 +84,6 @@ impl std::str::FromStr for IssueId {
4484
anyhow::bail!("invalid issue-id")
4585
};
4686

47-
Ok(IssueId::new(&c[1], c[2].parse()?))
87+
Ok(IssueId::new(Repository::new(&c[1], &c[2]), c[3].parse()?))
4888
}
4989
}

mdbook-goals/src/gh/issues.rs

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

99
use crate::util::comma;
1010

11-
use super::labels::GhLabel;
11+
use super::{issue_id::Repository, labels::GhLabel};
1212

1313
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
1414
pub struct ExistingGithubIssue {
@@ -78,12 +78,12 @@ impl std::fmt::Display for ExistingIssueState {
7878
}
7979

8080
pub fn list_issue_titles_in_milestone(
81-
repository: &str,
81+
repository: &Repository,
8282
timeframe: &str,
8383
) -> anyhow::Result<BTreeMap<String, ExistingGithubIssue>> {
8484
let output = Command::new("gh")
8585
.arg("-R")
86-
.arg(repository)
86+
.arg(&repository.to_string())
8787
.arg("issue")
8888
.arg("list")
8989
.arg("-m")
@@ -124,7 +124,7 @@ pub fn list_issue_titles_in_milestone(
124124
}
125125

126126
pub fn create_issue(
127-
repository: &str,
127+
repository: &Repository,
128128
body: &str,
129129
title: &str,
130130
labels: &[String],
@@ -133,7 +133,7 @@ pub fn create_issue(
133133
) -> anyhow::Result<()> {
134134
let output = Command::new("gh")
135135
.arg("-R")
136-
.arg(&repository)
136+
.arg(&repository.to_string())
137137
.arg("issue")
138138
.arg("create")
139139
.arg("-b")
@@ -160,15 +160,15 @@ pub fn create_issue(
160160
}
161161

162162
pub fn sync_assignees(
163-
repository: &str,
163+
repository: &Repository,
164164
number: u64,
165165
remove_owners: &BTreeSet<String>,
166166
add_owners: &BTreeSet<String>,
167167
) -> anyhow::Result<()> {
168168
let mut command = Command::new("gh");
169169
command
170170
.arg("-R")
171-
.arg(&repository)
171+
.arg(&repository.to_string())
172172
.arg("issue")
173173
.arg("edit")
174174
.arg(number.to_string());
@@ -203,10 +203,10 @@ impl ExistingGithubIssue {
203203
}
204204
}
205205

206-
pub fn lock_issue(repository: &str, number: u64) -> anyhow::Result<()> {
206+
pub fn lock_issue(repository: &Repository, number: u64) -> anyhow::Result<()> {
207207
let output = Command::new("gh")
208208
.arg("-R")
209-
.arg(repository)
209+
.arg(&repository.to_string())
210210
.arg("issue")
211211
.arg("lock")
212212
.arg(number.to_string())
@@ -225,7 +225,7 @@ pub fn lock_issue(repository: &str, number: u64) -> anyhow::Result<()> {
225225
// Leave a comment explaining what is going on.
226226
let output = Command::new("gh")
227227
.arg("-R")
228-
.arg(repository)
228+
.arg(&repository.to_string())
229229
.arg("issue")
230230
.arg("comment")
231231
.arg(number.to_string())

mdbook-goals/src/gh/labels.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@ use std::process::Command;
22

33
use serde::{Deserialize, Serialize};
44

5+
use super::issue_id::Repository;
6+
57
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
68
pub struct GhLabel {
79
pub name: String,
810
pub color: String,
911
}
1012

1113
impl GhLabel {
12-
pub fn list(repository: &str) -> anyhow::Result<Vec<GhLabel>> {
14+
pub fn list(repository: &Repository) -> anyhow::Result<Vec<GhLabel>> {
1315
let output = Command::new("gh")
1416
.arg("-R")
15-
.arg(repository)
17+
.arg(&repository.to_string())
1618
.arg("label")
1719
.arg("list")
1820
.arg("--json")
@@ -24,10 +26,10 @@ impl GhLabel {
2426
Ok(labels)
2527
}
2628

27-
pub fn create(&self, repository: &str) -> anyhow::Result<()> {
29+
pub fn create(&self, repository: &Repository) -> anyhow::Result<()> {
2830
let output = Command::new("gh")
2931
.arg("-R")
30-
.arg(repository)
32+
.arg(&repository.to_string())
3133
.arg("label")
3234
.arg("create")
3335
.arg(&self.name)

mdbook-goals/src/goal.rs

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::{collections::BTreeSet, path::PathBuf};
66
use anyhow::Context;
77
use regex::Regex;
88

9-
use crate::gh::issue_id::IssueId;
9+
use crate::gh::issue_id::{IssueId, Repository};
1010
use crate::re::USERNAME;
1111
use crate::team::{self, TeamName};
1212
use crate::util::{commas, markdown_files};
@@ -232,27 +232,66 @@ pub fn format_team_asks(asks_of_any_team: &[&TeamAsk]) -> anyhow::Result<String>
232232
}
233233

234234
pub fn format_goal_table(goals: &[&GoalDocument]) -> anyhow::Result<String> {
235-
let mut table = vec![vec![
236-
"Goal".to_string(),
237-
"Owner".to_string(),
238-
"Team".to_string(),
239-
]];
240-
241-
for goal in goals {
242-
let teams: BTreeSet<&TeamName> = goal
243-
.team_asks
244-
.iter()
245-
.flat_map(|ask| &ask.teams)
246-
.copied()
247-
.collect();
248-
let teams: Vec<&TeamName> = teams.into_iter().collect();
249-
table.push(vec![
250-
format!("[{}]({})", goal.metadata.title, goal.link_path.display()),
251-
goal.metadata.owners.clone(),
252-
commas(&teams),
253-
]);
254-
}
235+
// If any of the goals have tracking issues, include those in the table.
236+
let goal_has_tracking_issue = goals.iter().any(|g| g.metadata.tracking_issue.is_some());
237+
238+
let mut table;
239+
240+
if goal_has_tracking_issue {
241+
table = vec![vec![
242+
"Goal".to_string(),
243+
"Owner".to_string(),
244+
"Progress".to_string(),
245+
]];
246+
247+
for goal in goals {
248+
// Find the directory in which the goal document is located.
249+
// That is our "milestone" directory (e.g., 2024h2).
250+
let milestone: &str = goal
251+
.path
252+
.parent()
253+
.unwrap()
254+
.file_stem()
255+
.unwrap()
256+
.to_str()
257+
.unwrap();
258+
259+
let progress_bar = match &goal.metadata.tracking_issue {
260+
Some(issue_id @ IssueId { repository: Repository { org, repo }, number }) => format!(
261+
"<a href='{url}' alt='Tracking issue'><progress id='{milestone}:{org}:{repo}:{number}' value='0' max='100'></progress></a>",
262+
url = issue_id.url(),
263+
),
264+
None => format!("(no tracking issue)"),
265+
};
255266

267+
table.push(vec![
268+
format!("[{}]({})", goal.metadata.title, goal.link_path.display()),
269+
goal.metadata.owners.clone(),
270+
progress_bar,
271+
]);
272+
}
273+
} else {
274+
table = vec![vec![
275+
"Goal".to_string(),
276+
"Owner".to_string(),
277+
"Team".to_string(),
278+
]];
279+
280+
for goal in goals {
281+
let teams: BTreeSet<&TeamName> = goal
282+
.team_asks
283+
.iter()
284+
.flat_map(|ask| &ask.teams)
285+
.copied()
286+
.collect();
287+
let teams: Vec<&TeamName> = teams.into_iter().collect();
288+
table.push(vec![
289+
format!("[{}]({})", goal.metadata.title, goal.link_path.display()),
290+
goal.metadata.owners.clone(),
291+
commas(&teams),
292+
]);
293+
}
294+
}
256295
Ok(util::format_table(&table))
257296
}
258297

mdbook-goals/src/json.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,18 @@ use std::path::PathBuf;
1111
use serde::Serialize;
1212

1313
use crate::{
14-
gh::issues::{
15-
list_issue_titles_in_milestone, ExistingGithubComment, ExistingGithubIssue,
16-
ExistingIssueState,
14+
gh::{
15+
issue_id::Repository,
16+
issues::{
17+
list_issue_titles_in_milestone, ExistingGithubComment, ExistingGithubIssue,
18+
ExistingIssueState,
19+
},
1720
},
1821
re,
1922
};
2023

2124
pub(super) fn generate_json(
22-
repository: &str,
25+
repository: &Repository,
2326
milestone: &str,
2427
json_path: &Option<PathBuf>,
2528
) -> anyhow::Result<()> {

mdbook-goals/src/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use anyhow::Context;
2+
use gh::issue_id::Repository;
23
use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
34
use mdbook_preprocessor::GoalPreprocessor;
45
use regex::Regex;
@@ -27,7 +28,7 @@ struct Opt {
2728

2829
/// Repository to use if applicable
2930
#[structopt(long, default_value = "rust-lang/rust-project-goals")]
30-
repository: String,
31+
repository: Repository,
3132
}
3233

3334
#[derive(StructOpt, Debug)]

mdbook-goals/src/re.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ lazy_static! {
1818
}
1919

2020
lazy_static! {
21-
pub static ref TRACKING_ISSUE: Regex = Regex::new(r"\[([^#]*)#([0-9]+)\]").unwrap();
21+
pub static ref REPOSITORY: Regex = Regex::new(r"([^#/]*)/([^#/]*)").unwrap();
22+
}
23+
24+
lazy_static! {
25+
pub static ref TRACKING_ISSUE: Regex = Regex::new(r"\[([^#/]*)/([^#/]*)#([0-9]+)\]").unwrap();
2226
}
2327

2428
lazy_static! {

0 commit comments

Comments
 (0)