@@ -1,0 +1,147 @@
use crate::client::Client;
use crate::config;
use crate::output;
use clap::{Args, Subcommand};
use serde::Deserialize;
#[derive(Args)]
pub struct MilestoneArgs {
#[command(subcommand)]
pub command: MilestoneCommand,
}
#[derive(Subcommand)]
pub enum MilestoneCommand {
/// List milestones
List {
/// Repository (org/repo)
repo: Option<String>,
},
/// View milestone details
View {
/// Milestone short ID or UUID
id: String,
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
},
}
#[derive(Debug, Deserialize)]
struct Milestone {
short_id: Option<String>,
title: Option<String>,
description: Option<String>,
state: Option<String>,
due_date: Option<String>,
progress: Option<MilestoneProgress>,
}
#[derive(Debug, Deserialize)]
struct MilestoneProgress {
open: Option<u32>,
closed: Option<u32>,
percent: Option<f64>,
}
pub async fn run(args: MilestoneArgs) -> Result<(), Box<dyn std::error::Error>> {
match args.command {
MilestoneCommand::List { repo } => list(repo.as_deref()).await,
MilestoneCommand::View { id, repo } => view(&id, repo.as_deref()).await,
}
}
async fn list(repo: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
let resp: serde_json::Value = client.get(&format!("/{org}/{name}/milestones")).await?;
let milestones: Vec<Milestone> = resp
.get("milestones")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_default();
output::header(&format!("Milestones ({org}/{name})"));
if milestones.is_empty() {
output::info("No milestones found.");
return Ok(());
}
let rows: Vec<Vec<String>> = milestones
.iter()
.map(|m| {
let progress = m
.progress
.as_ref()
.map(|p| format!("{}%", p.percent.unwrap_or(0.0) as u32))
.unwrap_or_default();
vec![
m.short_id.clone().unwrap_or_default(),
m.title.clone().unwrap_or_default(),
output::colorize_status(m.state.as_deref().unwrap_or("?")),
progress,
m.due_date.clone().unwrap_or_default(),
]
})
.collect();
output::print_table(&["ID", "TITLE", "STATE", "PROGRESS", "DUE"], &rows);
Ok(())
}
async fn view(id: &str, repo: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
let resp: serde_json::Value = client
.get(&format!("/{org}/{name}/milestones/{id}"))
.await?;
let m: Milestone = resp
.get("milestone")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_else(|| {
serde_json::from_value(resp).unwrap_or(Milestone {
short_id: None,
title: None,
description: None,
state: None,
due_date: None,
progress: None,
})
});
output::header(&format!(
"Milestone {}",
m.short_id.as_deref().unwrap_or(id)
));
output::detail("Title", m.title.as_deref().unwrap_or("?"));
output::detail(
"State",
&output::colorize_status(m.state.as_deref().unwrap_or("?")),
);
if let Some(ref due) = m.due_date {
output::detail("Due", due);
}
if let Some(ref p) = m.progress {
output::detail(
"Progress",
&format!(
"{}% ({} open, {} closed)",
p.percent.unwrap_or(0.0) as u32,
p.open.unwrap_or(0),
p.closed.unwrap_or(0)
),
);
}
if let Some(ref desc) = m.description {
if !desc.is_empty() {
output::header("Description");
println!("{}", desc);
}
}
Ok(())
}