Skip to content

Commit 2e6cdcf

Browse files
authored
add champions to metadata and add new CSV command (#369)
* parse metadata for champions The intent is that we will use this to create CSV * add a CSV command to generate a champion table
1 parent 67595bf commit 2e6cdcf

File tree

5 files changed

+215
-1
lines changed

5 files changed

+215
-1
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
use std::collections::{BTreeMap, BTreeSet};
2+
use std::path::PathBuf;
3+
4+
use rust_project_goals::{
5+
gh::issue_id::Repository,
6+
goal,
7+
spanned::{Result, Spanned},
8+
team::TeamName,
9+
};
10+
11+
use crate::CSVReports;
12+
13+
pub fn csv(repository: &Repository, cmd: &CSVReports) -> Result<()> {
14+
match cmd {
15+
CSVReports::Champions { milestone } => champions(repository, milestone)?,
16+
}
17+
Ok(())
18+
}
19+
20+
struct ChampionRow {
21+
title: String,
22+
url: String,
23+
pocs: String,
24+
champions: BTreeMap<&'static TeamName, Spanned<String>>,
25+
teams_with_asks: BTreeSet<&'static TeamName>,
26+
}
27+
28+
fn champions(repository: &Repository, milestone: &str) -> Result<()> {
29+
let mut milestone_path = PathBuf::from("src");
30+
milestone_path.push(milestone);
31+
32+
let goal_documents = goal::goals_in_dir(&milestone_path)?;
33+
34+
let all_teams: BTreeSet<&TeamName> = goal_documents
35+
.iter()
36+
.flat_map(|d| d.teams_with_asks())
37+
.collect();
38+
39+
let rows: Vec<ChampionRow> = goal_documents
40+
.iter()
41+
.map(|doc| ChampionRow {
42+
title: doc.metadata.title.clone(),
43+
url: format!(
44+
"https://github.com/{org}/{repo}/blob/main/{path}",
45+
org = repository.org,
46+
repo = repository.repo,
47+
path = doc.path.display()
48+
),
49+
pocs: doc.metadata.pocs.clone(),
50+
champions: doc.metadata.champions.clone(),
51+
teams_with_asks: doc.teams_with_asks(),
52+
})
53+
.collect();
54+
55+
// Write header row
56+
write_csv_row(|cell| {
57+
cell.write_cell("Title");
58+
cell.write_cell("POC(s)");
59+
for team in &all_teams {
60+
cell.write_cell(&format!("{team}"));
61+
}
62+
cell.write_cell("URL");
63+
});
64+
65+
// Write data rows
66+
for row in &rows {
67+
write_csv_row(|cell| {
68+
cell.write_cell(&row.title);
69+
cell.write_cell(&row.pocs);
70+
71+
for team in &all_teams {
72+
if row.teams_with_asks.contains(team) {
73+
// Team has an ask - check if there's a champion
74+
if let Some(champion) = row.champions.get(team) {
75+
cell.write_cell(champion);
76+
} else {
77+
cell.write_cell("!");
78+
}
79+
} else {
80+
// Team has no ask for this goal
81+
cell.write_cell("-");
82+
}
83+
}
84+
85+
cell.write_cell(&row.url);
86+
});
87+
}
88+
89+
Ok(())
90+
}
91+
92+
trait WriteCell {
93+
fn write_cell(&mut self, s: &str);
94+
}
95+
96+
impl WriteCell for String {
97+
fn write_cell(&mut self, s: &str) {
98+
let mut s = s.replace(r#"""#, r#"\""#);
99+
s = s.replace("\n", "\\n");
100+
101+
if !self.is_empty() {
102+
self.push(',');
103+
}
104+
self.push('"');
105+
self.push_str(&s);
106+
self.push('"');
107+
}
108+
}
109+
110+
fn write_csv_row(op: impl FnOnce(&mut dyn WriteCell)) {
111+
let mut s = String::new();
112+
op(&mut s);
113+
println!("{s}");
114+
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::path::PathBuf;
88
use walkdir::WalkDir;
99

1010
mod cfp;
11+
mod csv_reports;
1112
mod generate_json;
1213
mod rfc;
1314
mod team_repo;
@@ -109,6 +110,21 @@ enum Command {
109110
/// If not given, no end date.
110111
end_date: Option<chrono::NaiveDate>,
111112
},
113+
114+
/// Generate various CSV reports
115+
CSV {
116+
#[command(subcommand)]
117+
cmd: CSVReports,
118+
},
119+
}
120+
121+
#[derive(clap::Subcommand, Debug)]
122+
#[allow(dead_code)]
123+
enum CSVReports {
124+
Champions {
125+
/// Milestone for which we generate tracking issue data (e.g., `2024h2`).
126+
milestone: String,
127+
},
112128
}
113129

114130
fn main() -> Result<()> {
@@ -160,6 +176,7 @@ fn main() -> Result<()> {
160176
} => {
161177
generate_json::generate_json(&opt.repository, &milestone, json_path)?;
162178
}
179+
163180
Command::Updates {
164181
milestone,
165182
vscode,
@@ -174,6 +191,8 @@ fn main() -> Result<()> {
174191
end_date,
175192
*vscode,
176193
)?,
194+
195+
Command::CSV { cmd } => csv_reports::csv(&opt.repository, cmd)?,
177196
}
178197

179198
Ok(())

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

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::collections::BTreeMap;
12
use std::path::Path;
23
use std::sync::Arc;
34
use std::{collections::BTreeSet, path::PathBuf};
@@ -8,7 +9,7 @@ use spanned::{Error, Result, Spanned};
89
use crate::config::{Configuration, TeamAskDetails};
910
use crate::gh::issue_id::{IssueId, Repository};
1011
use crate::markwaydown::{self, Section, Table};
11-
use crate::re::{self, TASK_OWNERS_STR, TEAMS_WITH_ASKS_STR};
12+
use crate::re::{self, CHAMPION_METADATA, TASK_OWNERS_STR, TEAMS_WITH_ASKS_STR};
1213
use crate::team::{self, TeamName};
1314
use crate::util::{self, commas, markdown_files};
1415

@@ -48,6 +49,9 @@ pub struct Metadata {
4849
pub status: Spanned<Status>,
4950
pub tracking_issue: Option<IssueId>,
5051
pub table: Spanned<Table>,
52+
53+
/// For each table entry like `[T-lang] champion`, we create an entry in this map
54+
pub champions: BTreeMap<&'static TeamName, Spanned<String>>,
5155
}
5256

5357
pub const TRACKING_ISSUE_ROW: &str = "Tracking issue";
@@ -440,6 +444,39 @@ fn extract_metadata(sections: &[Section]) -> Result<Option<Metadata>> {
440444
verify_row(&first_table.rows, "Teams", TEAMS_WITH_ASKS_STR)?;
441445
verify_row(&first_table.rows, "Task owners", TASK_OWNERS_STR)?;
442446

447+
let mut champions = BTreeMap::default();
448+
for row in &first_table.rows {
449+
let row_name = &row[0];
450+
let row_value = &row[1];
451+
452+
if !row_name.to_lowercase().contains("champion") {
453+
continue;
454+
}
455+
456+
if let Some(m) = CHAMPION_METADATA.captures(row_name) {
457+
let team_name = m.name("team").unwrap().as_str().to_string();
458+
459+
let Some(team) = team::get_team_name(&team_name)? else {
460+
spanned::bail!(row_name, "team `{team_name}` is not recognized")
461+
};
462+
463+
if champions.contains_key(team) {
464+
spanned::bail!(
465+
row_name,
466+
"multiple rows naming champions for team `{team_name}`"
467+
)
468+
} else {
469+
champions.insert(team, row_value.clone());
470+
}
471+
} else {
472+
spanned::bail!(
473+
row_name,
474+
"metadata row `{}` talks about champions but is not of the form `[team-name] champion`",
475+
&**row_name
476+
)
477+
}
478+
}
479+
443480
Ok(Some(Metadata {
444481
title: title.to_string(),
445482
short_title: if let Some(row) = short_title_row {
@@ -451,6 +488,7 @@ fn extract_metadata(sections: &[Section]) -> Result<Option<Metadata>> {
451488
status,
452489
tracking_issue: issue,
453490
table: first_table.clone(),
491+
champions,
454492
}))
455493
}
456494

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,10 @@ lazy_static! {
110110

111111
/// If a comment begins with this text, it will be considered a summary.
112112
pub const TLDR: &str = "TL;DR:";
113+
114+
lazy_static! {
115+
/// Metadata table rows like `[lang] champion` indicate the champion for the lang team
116+
pub static ref CHAMPION_METADATA: Regex =
117+
Regex::new(r"^\s*(?P<team>\[.*\]) champion)\s*$")
118+
.unwrap();
119+
}

src/admin/commands.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,39 @@ Note that after running this command, you'll still need to manually:
2929
2. Send an email to the `[email protected]` mailing list
3030

3131
For more details, see the [Call for proposals](./cfp.md) documentation.
32+
33+
### `cargo rpg csv`
34+
35+
Generates CSV reports for analysis and tracking purposes. Currently supports generating champion tracking reports.
36+
37+
```bash
38+
# Generate champions report for a milestone
39+
cargo rpg csv champions <milestone>
40+
```
41+
42+
Example:
43+
```bash
44+
cargo rpg csv champions 2025h2
45+
```
46+
47+
#### `champions` subcommand
48+
49+
The `champions` subcommand generates a CSV report showing the champion assignments for each goal in a milestone. The output includes:
50+
51+
- **Title**: The goal title
52+
- **POC(s)**: Point of contact for the goal
53+
- **Team columns**: One column per team that has asks across all goals in the milestone
54+
- Shows champion name if assigned
55+
- Shows `!` if team has an ask but no champion assigned
56+
- Shows `-` if team has no ask for this goal
57+
- **URL**: Link to the goal document on GitHub
58+
59+
This report is useful for:
60+
- Tracking champion coverage across teams
61+
- Identifying goals that need champion assignments
62+
- Understanding team involvement across the milestone
63+
64+
The CSV output can be redirected to a file or piped to other tools for further analysis:
65+
```bash
66+
cargo rpg csv champions 2025h2 > champions.csv
67+
```

0 commit comments

Comments
 (0)