Skip to content

Commit c26603c

Browse files
authored
Merge pull request #428 from nikomatsakis/main
support 2026 "all year goals"
2 parents 9fa8bef + 6d2c4f2 commit c26603c

File tree

12 files changed

+658
-202
lines changed

12 files changed

+658
-202
lines changed

crates/mdbook-goals/src/mdbook_preprocessor.rs

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ use regex::Regex;
1111
use rust_project_goals::config::{Configuration, GoalsConfig};
1212
use rust_project_goals::format_champions::format_champions;
1313
use rust_project_goals::format_team_ask::format_team_asks;
14+
use rust_project_goals::format_team_support::format_team_support;
1415
use rust_project_goals::markdown_processor::{MarkdownProcessor, MarkdownProcessorState};
1516
use rust_project_goals::util;
1617
use rust_project_goals_cli::Order;
1718

1819
use rust_project_goals::spanned::Spanned;
1920
use rust_project_goals::{
20-
goal::{self, GoalDocument, TeamAsk},
21+
goal::{self, GoalDocument, TeamAsk, TeamInvolvement},
2122
re,
2223
team::TeamName,
2324
};
@@ -303,14 +304,41 @@ impl<'c> GoalPreprocessorWithContext<'c> {
303304
};
304305

305306
let goals = self.goal_documents(path)?;
306-
let asks_of_any_team: Vec<&TeamAsk> = goals
307-
.iter()
308-
.filter(|g| g.metadata.status.is_not_not_accepted())
309-
.flat_map(|g| &g.team_asks)
310-
.collect();
311-
let format_team_asks =
312-
format_team_asks(&asks_of_any_team).map_err(|e| anyhow::anyhow!("{e}"))?;
313-
chapter.content.replace_range(range, &format_team_asks);
307+
308+
// Separate goals by format
309+
let mut old_format_asks: Vec<&TeamAsk> = vec![];
310+
let mut new_format_goals: Vec<&GoalDocument> = vec![];
311+
312+
for goal in goals.iter().filter(|g| g.metadata.status.is_not_not_accepted()) {
313+
match &goal.team_involvement {
314+
TeamInvolvement::Asks(asks) => {
315+
old_format_asks.extend(asks.iter());
316+
}
317+
TeamInvolvement::Support(_) => {
318+
new_format_goals.push(goal);
319+
}
320+
}
321+
}
322+
323+
// Format both old and new format goals
324+
let mut formatted = String::new();
325+
326+
if !old_format_asks.is_empty() {
327+
formatted.push_str(
328+
&format_team_asks(&old_format_asks).map_err(|e| anyhow::anyhow!("{e}"))?,
329+
);
330+
}
331+
332+
if !new_format_goals.is_empty() {
333+
if !formatted.is_empty() {
334+
formatted.push_str("\n\n");
335+
}
336+
formatted.push_str(
337+
&format_team_support(&new_format_goals).map_err(|e| anyhow::anyhow!("{e}"))?,
338+
);
339+
}
340+
341+
chapter.content.replace_range(range, &formatted);
314342

315343
Ok(())
316344
}

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

Lines changed: 104 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use std::path::{Path, PathBuf};
99
pub mod text_processing {
1010
use regex::Regex;
1111

12+
use crate::cfp::normalize_timeframe;
13+
1214
/// Process template content by replacing placeholders and removing notes
1315
pub fn process_template_content(
1416
content: &str,
@@ -71,7 +73,12 @@ pub mod text_processing {
7173
let mut new_content = content.to_string();
7274

7375
// Create the new section content with capitalized H
74-
let capitalized_timeframe = format!("{}H{}", &timeframe[0..4], &timeframe[5..]);
76+
let capitalized_timeframe = if timeframe.len() > 4 {
77+
format!("{}H{}", &timeframe[0..4], &timeframe[5..])
78+
} else {
79+
timeframe.to_string()
80+
};
81+
7582
let new_section_content = format!(
7683
"# ⏳ {} goal process\n\n\
7784
- [Overview](./{}/README.md)\n\
@@ -134,9 +141,9 @@ pub mod text_processing {
134141
let new_section = format!("\n{}", new_section_content);
135142

136143
// Find a good place to insert the new section
137-
// Look for the last timeframe section or insert at the beginning
138-
// Match both lowercase and uppercase H
139-
let re = Regex::new(r"# ⏳ \d{4}[hH][12] goal process").unwrap();
144+
// Look for the last timeframe section or insert after # Summary
145+
// Match both year-only (2027) and half-year (2026H1) formats
146+
let re = Regex::new(r"# ⏳ \d{4}([hH][12])? goal process").unwrap();
140147

141148
if let Some(last_match) = re.find_iter(&content).last() {
142149
// Find the end of this section (next section or end of file)
@@ -148,8 +155,20 @@ pub mod text_processing {
148155
new_content.push_str(&new_section);
149156
}
150157
} else {
151-
// No existing timeframe sections, insert at the beginning
152-
new_content = new_section + &content;
158+
// No existing ⏳ timeframe sections, find the first dated section (⏳ or ⚙️) and insert before it
159+
let dated_section_re = Regex::new(r"\n# (⏳|⚙️) \d{4}").unwrap();
160+
if let Some(first_dated) = dated_section_re.find(&content) {
161+
new_content.insert_str(first_dated.start(), &new_section);
162+
} else {
163+
// No dated sections at all, insert after "# Summary\n\n"
164+
if let Some(summary_pos) = content.find("# Summary") {
165+
let insert_pos = summary_pos + "# Summary\n\n".len();
166+
new_content.insert_str(insert_pos, &new_section);
167+
} else {
168+
// No # Summary found, prepend
169+
new_content = new_section + &content;
170+
}
171+
}
153172
}
154173

155174
new_content
@@ -163,28 +182,38 @@ pub mod text_processing {
163182
) -> String {
164183
let mut new_content = content.to_string();
165184

166-
// Extract year and half from timeframe
167-
let _year = &timeframe[0..4];
168-
let half = &timeframe[4..].to_lowercase();
169-
170-
// Determine the months based on the half
171-
let (start_month, end_month) = if half == "h1" {
172-
("January", "June")
185+
// Create the new section to add with capitalized H
186+
let capitalized_timeframe = normalize_timeframe(timeframe);
187+
188+
// Check if this is a year-only timeframe (no H1/H2)
189+
let new_section = if timeframe.len() == 4 {
190+
// Year-only format - no date range
191+
format!(
192+
"\n## Next goal period ({})\n\n\
193+
The next goal period will be {}. \
194+
We are currently in the process of assembling goals. \
195+
[Click here](./{}/goals.md) to see the current list. \
196+
If you'd like to propose a goal, [instructions can be found here](./how_to/propose_a_goal.md).\n",
197+
capitalized_timeframe, capitalized_timeframe, lowercase_timeframe
198+
)
173199
} else {
174-
("July", "December")
200+
// Half-year format - include date range
201+
let half = &timeframe[4..].to_lowercase();
202+
let (start_month, end_month) = if half == "h1" {
203+
("January", "June")
204+
} else {
205+
("July", "December")
206+
};
207+
format!(
208+
"\n## Next goal period ({})\n\n\
209+
The next goal period will be {}, running from the start of {} to the end of {}. \
210+
We are currently in the process of assembling goals. \
211+
[Click here](./{}/goals.md) to see the current list. \
212+
If you'd like to propose a goal, [instructions can be found here](./how_to/propose_a_goal.md).\n",
213+
capitalized_timeframe, capitalized_timeframe, start_month, end_month, lowercase_timeframe
214+
)
175215
};
176216

177-
// Create the new section to add with capitalized H
178-
let capitalized_timeframe = format!("{}H{}", &timeframe[0..4], &timeframe[5..]);
179-
let new_section = format!(
180-
"\n## Next goal period ({})\n\n\
181-
The next goal period will be {}, running from the start of {} to the end of {}. \
182-
We are currently in the process of assembling goals. \
183-
[Click here](./{}/goals.md) to see the current list. \
184-
If you'd like to propose a goal, [instructions can be found here](./how_to/propose_a_goal.md).\n",
185-
capitalized_timeframe, capitalized_timeframe, start_month, end_month, lowercase_timeframe
186-
);
187-
188217
// First check for an existing entry for this specific timeframe
189218
let this_period_pattern = Regex::new(&format!(
190219
r"## Next goal period(?:\s*\({}\))?\s*\n",
@@ -340,15 +369,23 @@ pub fn create_cfp(timeframe: &str, force: bool, dry_run: bool) -> Result<()> {
340369
Ok(())
341370
}
342371

343-
/// Validates that the timeframe is in the correct format (e.g., "2025h1" or "2025H1")
372+
/// Validates that the timeframe is in the correct format (e.g., "2025h1" or "2025H1" or '2026')
344373
fn validate_timeframe(timeframe: &str) -> Result<()> {
345-
let re = Regex::new(r"^\d{4}[hH][12]$").unwrap();
374+
let re = Regex::new(r"^\d{4}([hH][12])?$").unwrap();
346375
if !re.is_match(timeframe) {
347376
return Err(Error::str("Invalid timeframe format. Expected format: YYYYhN or YYYYHN (e.g., 2025h1, 2025H1, 2025h2, or 2025H2"));
348377
}
349378
Ok(())
350379
}
351380

381+
fn normalize_timeframe(timeframe: &str) -> String {
382+
if timeframe.len() == 4 {
383+
timeframe.to_string()
384+
} else {
385+
format!("{}H{}", &timeframe[0..4], &timeframe[5..])
386+
}
387+
}
388+
352389
/// Copies a template file to the destination and replaces placeholders
353390
fn copy_and_process_template(
354391
template_path: &str,
@@ -444,7 +481,7 @@ fn update_main_readme(timeframe: &str, lowercase_timeframe: &str, dry_run: bool)
444481
}
445482

446483
// Determine what kind of update was made for better logging
447-
let capitalized_timeframe = format!("{}H{}", &timeframe[0..4], &timeframe[5..]);
484+
let capitalized_timeframe = normalize_timeframe(timeframe);
448485
let specific_timeframe_pattern = format!(
449486
r"## Next goal period(?:\s*\({}\))",
450487
regex::escape(&capitalized_timeframe)
@@ -522,15 +559,39 @@ mod tests {
522559
}
523560

524561
#[test]
525-
fn test_process_summary_content_no_existing_section() {
526-
// Test adding a new section when no timeframe sections exist
527-
let content = "# Summary\n\n[Introduction](./README.md)\n";
562+
fn test_process_summary_content_new_section() {
563+
// Test adding a new section before an existing dated section
564+
let content = "# Summary\n\n[👋 Introduction](./README.md)\n\n# ⚙️ 2025H2 goal process\n\n- [Overview](./2025h2/README.md)\n";
528565
let result = process_summary_content(content, "2026h1", "2026h1");
529566

530567
assert!(result.contains("# ⏳ 2026H1 goal process"));
531568
assert!(result.contains("- [Overview](./2026h1/README.md)"));
532569
assert!(result.contains("- [Proposed goals](./2026h1/goals.md)"));
533570
assert!(result.contains("- [Goals not accepted](./2026h1/not_accepted.md)"));
571+
// Verify the section is inserted after Introduction, before the existing dated section
572+
let intro_pos = result.find("[👋 Introduction]").unwrap();
573+
let goal_process_pos = result.find("# ⏳ 2026H1 goal process").unwrap();
574+
let existing_section_pos = result.find("# ⚙️ 2025H2").unwrap();
575+
assert!(intro_pos < goal_process_pos, "Goal process section should come after Introduction");
576+
assert!(goal_process_pos < existing_section_pos, "New section should come before existing dated section");
577+
}
578+
579+
#[test]
580+
fn test_process_summary_content_year_only_timeframe() {
581+
// Test adding a new section with year-only timeframe (no H1/H2)
582+
let content = "# Summary\n\n[👋 Introduction](./README.md)\n\n# ⚙️ 2025H2 goal process\n\n- [Overview](./2025h2/README.md)\n";
583+
let result = process_summary_content(content, "2027", "2027");
584+
585+
assert!(result.contains("# ⏳ 2027 goal process"));
586+
assert!(result.contains("- [Overview](./2027/README.md)"));
587+
assert!(result.contains("- [Proposed goals](./2027/goals.md)"));
588+
assert!(result.contains("- [Goals not accepted](./2027/not_accepted.md)"));
589+
// Verify the section is inserted after Introduction, before the existing dated section
590+
let intro_pos = result.find("[👋 Introduction]").unwrap();
591+
let goal_process_pos = result.find("# ⏳ 2027 goal process").unwrap();
592+
let existing_section_pos = result.find("# ⚙️ 2025H2").unwrap();
593+
assert!(intro_pos < goal_process_pos, "Goal process section should come after Introduction");
594+
assert!(goal_process_pos < existing_section_pos, "New section should come before existing dated section");
534595
}
535596

536597
#[test]
@@ -630,4 +691,16 @@ mod tests {
630691
assert!(result.contains("## Next goal period (2026H1)"));
631692
assert!(result.contains("running from the start of January to the end of June"));
632693
}
694+
695+
#[test]
696+
fn test_process_readme_content_year_only() {
697+
// Test that year-only timeframes don't include date ranges
698+
let content = "# Project goals\n\n## Current goal period (2026)\n\nThe 2026 goal period.";
699+
let result = process_readme_content(content, "2027", "2027");
700+
701+
assert!(result.contains("## Next goal period (2027)"));
702+
assert!(result.contains("The next goal period will be 2027."));
703+
assert!(!result.contains("running from"));
704+
assert!(result.contains("[Click here](./2027/goals.md)"));
705+
}
633706
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -583,9 +583,7 @@ fn task_items(goal_plan: &GoalPlan) -> Result<Vec<String>> {
583583
fn teams_with_asks(goal_documents: &[GoalDocument]) -> BTreeSet<&'static TeamName> {
584584
goal_documents
585585
.iter()
586-
.flat_map(|g| &g.team_asks)
587-
.flat_map(|ask| &ask.teams)
588-
.copied()
586+
.flat_map(|g| g.team_involvement.teams())
589587
.collect()
590588
}
591589

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

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,7 @@ pub fn render_updates(
8484
.iter()
8585
.filter_map(|doc| {
8686
doc.metadata.tracking_issue.as_ref().map(|issue| {
87-
let teams: std::collections::BTreeSet<&rust_project_goals::team::TeamName> = doc
88-
.team_asks
89-
.iter()
90-
.flat_map(|ask| &ask.teams)
91-
.copied()
92-
.collect();
87+
let teams = doc.team_involvement.teams();
9388

9489
let team_champions: Vec<String> = teams
9590
.into_iter()

0 commit comments

Comments
 (0)