use clap::{Arg, ArgMatches, Command};
use crate::domain::model::issue::{IssueLink, IssueRelationship};
use crate::domain::usecases::clock::Clock;
use crate::domain::usecases::issue::create_issue;
use crate::infra::driven::clock::SystemClock;
use crate::infra::driving::cli::errors::{die1, CliError};
use crate::infra::driving::cli::helpers::{read_body_from_stdin_or_editor, ISSUE_BODY_TEMPLATE};
use crate::infra::driving::cli::theme;
use crate::infra::driving::cli::{render_structured, Context};
pub(super) fn subcommand() -> Command {
let mut new_cmd = Command::new("new")
.about("Create a new issue")
.arg(
Arg::new("title")
.required(true)
.num_args(1..)
.value_name("TITLE")
.help("Issue title (one or more words)"),
)
.arg(
Arg::new("tag")
.long("tag")
.help(
"Add a tag (repeatable). Form: `name` or `key:value`. \
Validation against [tags.<key>] descriptors is deferred to `cartu check`.",
)
.value_name("TAG")
.action(clap::ArgAction::Append),
);
for rel in IssueRelationship::user_writable() {
let flag: &'static str = rel.as_str();
new_cmd = new_cmd.arg(
Arg::new(flag)
.long(flag)
.value_name("ID")
.help(help_for_new_flag(rel)),
);
}
new_cmd
}
pub(super) fn execute(sub: &ArgMatches, ctx: &Context<'_>) {
let output_fmt = ctx.output_fmt;
let title = crate::infra::driving::cli::helpers::required_many(sub, "title")
.collect::<Vec<_>>()
.join(" ");
let template = ISSUE_BODY_TEMPLATE;
let extra_tags: Vec<crate::domain::model::tag::Tag> = sub
.get_many::<String>("tag")
.unwrap_or_default()
.map(|s| {
crate::domain::model::tag::Tag::new(s).unwrap_or_else(|e| {
die1(
CliError::new(format!("invalid tag '{s}': {e}")).kind("validation"),
output_fmt,
);
})
})
.collect();
let mut initial_links = crate::domain::model::issue::IssueLinks::new();
for rel in IssueRelationship::user_writable() {
if let Some(target_id) = sub.get_one::<String>(rel.as_str()) {
let target =
crate::domain::model::record_ref::IssueRef::new(target_id).unwrap_or_else(|e| {
die1(
CliError::new(format!("invalid target ID '{target_id}': {e}"))
.kind("validation"),
output_fmt,
);
});
initial_links.push(IssueLink {
target,
relationship: rel.clone(),
});
}
}
let typed_title = crate::domain::model::title::Title::new(&title).unwrap_or_else(|e| {
die1(CliError::new(e.to_string()).kind("validation"), output_fmt);
});
let body = read_body_from_stdin_or_editor(template, output_fmt);
let typed_body = crate::domain::model::body::Body::new(&body);
let now = SystemClock.now();
let repo = ctx.issue_repository();
let id_gen = ctx.issue_id_generator();
let initial_status = ctx
.issues_statuses
.resolve(ctx.issues_statuses.initial())
.unwrap_or_else(|_| {
crate::domain::model::status::Status::unresolved(ctx.issues_statuses.initial())
});
let issue = create_issue(
&repo,
&id_gen,
typed_title,
typed_body,
now,
initial_status,
&extra_tags,
initial_links,
)
.unwrap_or_else(|e| {
die1(CliError::new(e.to_string()), output_fmt);
});
if output_fmt.is_structured() {
render_structured(
&crate::infra::driving::cli::issue_view::IssueView::from_issue(&issue),
output_fmt,
);
} else {
let slug = crate::infra::driven::fs::frontmatter::slugify(issue.title.as_str());
println!(
"{}",
theme::success(&format!(
"Created {}/{}-{}/index.md ({})",
ctx.issues_dir().display(),
issue.id.suffix(),
slug,
issue.id,
))
);
}
}
fn help_for_new_flag(rel: &IssueRelationship) -> &'static str {
match rel {
IssueRelationship::Blocks => "ID of the issue this new issue blocks",
IssueRelationship::BlockedBy => "ID of the issue this new issue is blocked by",
IssueRelationship::ParentOf => "ID of the issue this new issue is the parent of",
IssueRelationship::ChildOf => "ID of the issue this new issue is the child of",
}
}