1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
use clap::Args;
use crate::config::find_bmo_dir;
use crate::db::{Repository, open_db};
use crate::errors::ErrorCode;
use crate::model::IssueFilter;
use crate::output::{ExecutionPlan, OutputMode, Phase, make_printer};
use crate::planner::dag::Dag;
use crate::planner::plan::generate_plan;
#[derive(Args)]
pub struct PlanArgs {
/// Filter by assignee (requires --phase)
#[arg(short, long)]
pub assignee: Option<String>,
/// Show only the issues in phase N. In --json mode returns a flat array, not the full plan
/// envelope; in human mode prints a header plus a formatted list.
#[arg(long)]
pub phase: Option<u32>,
/// Ignore link relations when building the plan; ordering is driven solely by file conflicts.
#[arg(long)]
pub no_links: bool,
}
pub fn run(args: &PlanArgs, json: bool) -> anyhow::Result<()> {
let bmo_dir = find_bmo_dir()?;
let repo = open_db(&bmo_dir.join("issues.db"))?;
let printer = make_printer(if json {
OutputMode::Json
} else {
OutputMode::Human
});
// Validate: --assignee requires --phase
if args.assignee.is_some() && args.phase.is_none() {
printer.print_error("--assignee requires --phase", ErrorCode::Validation);
std::process::exit(ErrorCode::Validation.exit_code());
}
let all_issues = repo.list_issues(IssueFilter::default())?;
let all_relations = if args.no_links {
vec![]
} else {
repo.list_all_relations()?
};
let dag = Dag::build(&all_issues, &all_relations);
// NOTE: The --assignee flag is declared in PlanArgs but generate_plan() operates on a
// pre-built Dag and has no assignee parameter. As of this implementation, assignee
// filtering is NOT yet wired into generate_plan(). When --phase and --assignee are both
// given, we apply assignee filtering post-generation by retaining only issues whose
// assignee matches. Validation of the phase number (out-of-range check) always uses the
// unfiltered plan's total_phases so that --phase N with an assignee that empties a phase
// returns an empty array rather than a validation error.
let internal_plan = generate_plan(&dag)?;
// Convert planner::plan::Phase to output::Phase
let output_plan = ExecutionPlan {
total_issues: internal_plan.total_issues,
total_phases: internal_plan.total_phases,
max_parallelism: internal_plan.max_parallelism,
phases: internal_plan
.phases
.into_iter()
.map(|p| Phase {
number: p.number,
issues: p.issues,
})
.collect(),
};
if let Some(phase_n) = args.phase {
// Validate phase number against the unfiltered plan's total_phases.
// We use total_phases from the full plan so that an assignee filter that empties a
// phase does not convert an in-range phase number into a validation error.
let total = output_plan.total_phases as u32;
if phase_n == 0 || phase_n > total {
let msg = format!(
"phase {} does not exist (plan has {} phase{})",
phase_n,
total,
if total == 1 { "" } else { "s" }
);
printer.print_error(&msg, ErrorCode::Validation);
std::process::exit(ErrorCode::Validation.exit_code());
}
// Find the matching phase and extract its issues.
let mut issues: Vec<_> = output_plan
.phases
.into_iter()
.find(|p| p.number == phase_n as usize)
.map(|p| p.issues)
.unwrap_or_default();
// Apply optional assignee filter post-generation (see note above).
if let Some(ref assignee) = args.assignee {
issues.retain(|i| i.assignee.as_deref() == Some(assignee.as_str()));
}
// Human output: compact list matching the documented format.
if json {
let msg = format!("Phase {}: {} issue(s).", phase_n, issues.len());
let out = serde_json::json!({
"ok": true,
"data": issues,
"message": msg,
});
println!("{}", serde_json::to_string_pretty(&out)?);
} else {
println!(
"Phase {} ({} issue{}):",
phase_n,
issues.len(),
if issues.len() == 1 { "" } else { "s" }
);
for issue in &issues {
println!(
" {} {} {} {} {}",
issue.display_id(),
issue.status,
issue.priority,
issue.kind,
issue.title,
);
}
}
return Ok(());
}
printer.print_plan(&output_plan);
Ok(())
}