ref:b0542e8426cac3e9827616971d4a67ef1e5ebf11

feat(cli): add milestone and assignee management commands

Adds milestones, create-milestone, assign, and unassign subcommands to the issue command group, covering issue #15 API endpoints.
SHA: b0542e8426cac3e9827616971d4a67ef1e5ebf11
Author: Cole Christensen <cole.christensen@macmillan.com>
Date: 2026-03-15 22:21
Parents: 64a668e
1 files changed +95 -0
Type
src/commands/issue.rs +95 −0
@@ -95,6 +95,49 @@
#[arg(long)]
repo: Option<String>,
},
/// List milestones
Milestones {
/// Repository (org/repo)
repo: Option<String>,
},
/// Create a milestone
#[command(name = "create-milestone")]
CreateMilestone {
/// Milestone title
#[arg(long)]
title: String,
/// Description
#[arg(long)]
description: Option<String>,
/// Due date (YYYY-MM-DD)
#[arg(long)]
due_date: Option<String>,
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
},
/// Assign a user to an issue
Assign {
/// Issue number
number: u32,
/// User ID to assign
#[arg(long)]
user_id: String,
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
},
/// Remove an assignee from an issue
Unassign {
/// Issue number
number: u32,
/// User ID to remove
#[arg(long)]
user_id: String,
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
},
}
#[derive(Debug, Deserialize)]
@@ -140,5 +183,9 @@
} => edit_issue(repo.as_deref(), number, title.as_deref(), body.as_deref()).await,
IssueCommand::Comment { number, body, repo } => add_comment(repo.as_deref(), number, &body).await,
IssueCommand::Comments { number, repo } => list_comments(repo.as_deref(), number).await,
IssueCommand::Milestones { repo } => list_milestones(repo.as_deref()).await,
IssueCommand::CreateMilestone { title, description, due_date, repo } => create_milestone(repo.as_deref(), &title, description.as_deref(), due_date.as_deref()).await,
IssueCommand::Assign { number, user_id, repo } => assign(repo.as_deref(), number, &user_id).await,
IssueCommand::Unassign { number, user_id, repo } => unassign(repo.as_deref(), number, &user_id).await,
}
}
@@ -383,5 +430,53 @@
println!("\n {} — {}", author, time);
println!(" {body}");
}
Ok(())
}
async fn list_milestones(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<serde_json::Value> = resp.get("milestones").and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default();
output::header(&format!("Milestones ({org}/{name})"));
let rows: Vec<Vec<String>> = milestones.iter().map(|m| {
vec![
m.get("title").and_then(|v| v.as_str()).unwrap_or("?").to_string(),
m.get("state").and_then(|v| v.as_str()).map(|s| output::colorize_status(s)).unwrap_or_default(),
format!("{}/{}", m.get("closed_issues").and_then(|v| v.as_u64()).unwrap_or(0), m.get("open_issues").and_then(|v| v.as_u64()).unwrap_or(0) + m.get("closed_issues").and_then(|v| v.as_u64()).unwrap_or(0)),
format!("{}%", m.get("progress_percent").and_then(|v| v.as_f64()).unwrap_or(0.0) as u32),
m.get("due_date").and_then(|v| v.as_str()).unwrap_or("none").to_string(),
]
}).collect();
output::print_table(&["TITLE", "STATE", "PROGRESS", "%", "DUE"], &rows);
Ok(())
}
async fn create_milestone(repo: Option<&str>, title: &str, description: Option<&str>, due_date: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
let mut payload = serde_json::json!({"title": title});
if let Some(d) = description { payload["description"] = serde_json::json!(d); }
if let Some(dd) = due_date { payload["due_date"] = serde_json::json!(dd); }
let _resp: serde_json::Value = client.post(&format!("/{org}/{name}/milestones"), &payload).await?;
output::success(&format!("Created milestone '{title}'"));
Ok(())
}
async fn assign(repo: Option<&str>, number: u32, user_id: &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
.post(&format!("/{org}/{name}/issues/{number}/assignees"), &serde_json::json!({"user_id": user_id}))
.await?;
output::success(&format!("Assigned user to issue #{number}"));
Ok(())
}
async fn unassign(repo: Option<&str>, number: u32, user_id: &str) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
client.delete_empty(&format!("/{org}/{name}/issues/{number}/assignees/{user_id}")).await?;
output::success(&format!("Unassigned user from issue #{number}"));
Ok(())
}