#[allow(clippy::wildcard_imports)]
use super::*;
pub(super) fn edit_relationship(store: &Store, args: &Value) -> ToolOutcome {
let id = req_string(args, "id")?;
let kind_arg = opt_string(args, "kind")?;
let kind = match &kind_arg {
Some(raw) => match crate::commands::link::parse_kind(raw) {
Ok(kind) => Some(kind),
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
},
None => None,
};
let to_arg = opt_selector(args, "to")?;
let to = match &to_arg {
Some(raw) => match resolve_number(store, raw) {
Ok(number) => Some(number),
Err(()) => {
return Ok(with_number_resolution(store, raw, || {
error_result(format!("invalid input: no issue matches selector `{raw}`"))
}));
}
},
None => None,
};
if to.is_none() && kind.is_none() {
return Ok(error_result(format!(
"invalid input: nothing to change: pass `to` and/or `kind` to edit_relationship {id}"
)));
}
match store.edit_relation(&id, to, kind) {
Ok(outcome) => {
let text = crate::commands::link::edit_message(&outcome);
Ok(success_result(
text,
json!({ "relationship": relationship_value(&outcome.relationship), "changed": outcome.changed }),
))
}
Err(e) => Ok(error_result(crate::error::render_error(&e))),
}
}
pub(super) fn epic_hierarchy(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let reference = req_string(args, "epic")?;
let (config, _path) = match registry_config(store) {
Ok(loaded) => loaded,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let store = store_with_config(store, config);
let issues = match store.list_all() {
Ok(issues) => issues,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let index = crate::epics::EpicIndex::build(&store.config, &issues);
let hierarchy = match index.hierarchy(&reference) {
Ok(hierarchy) => hierarchy,
Err(error) => return Ok(epic_refusal(error)),
};
let epic = match index.resolve(&reference) {
Ok(epic) => epic.clone(),
Err(error) => return Ok(epic_refusal(error)),
};
let mut value = epic_hierarchy_value(&hierarchy);
value["member_count"] = json!(epic.member_count);
value["members"] = json!(
epic.members
.iter()
.map(epic_member_value)
.collect::<Vec<Value>>()
);
Ok(success_result(
cmd_epic::navigation_lines(&hierarchy, &epic).join("\n"),
value,
))
}
pub(super) fn get_parent(store: &Store, args: &Value) -> ToolOutcome {
let number = match required_number(store, args) {
Ok(number) => number,
Err(()) => {
return Ok(error_result(
"invalid input: an `issue` selector is required".into(),
));
}
};
let snapshot = match hierarchy_snapshot(store) {
Ok(snapshot) => snapshot,
Err(()) => return Ok(error_result("could not read the issue set".into())),
};
if !snapshot.index.contains(number) {
return Ok(missing_issue(number));
}
let facts = crate::commands::parent::facts_for(
&snapshot.index,
&snapshot.issues,
number,
&snapshot.done_column,
);
let text = crate::commands::parent::parent_text(&facts);
let resolved_ref = facts
.parent_ref
.filter(|reference| snapshot.index.contains_ref(reference));
let parent_number = resolved_ref.and_then(|reference| reference.as_number());
let recorded_parent = facts.parent_ref.map(|reference| reference.to_string());
let unresolved_parent = recorded_parent.clone();
let unresolved = facts.unresolved_reason.map(|reason| {
json!({
"parent": unresolved_parent,
"parent_number": facts.parent,
"reason": reason,
})
});
Ok(success_result(
text.clone(),
json!({
"issue": number,
"parent": resolved_ref.map(|reference| reference.to_string()),
"parent_number": parent_number,
"parent_display": facts.parent_label,
"recorded_parent": recorded_parent,
"recorded_parent_number": facts.parent,
"unresolved": unresolved,
"text": text,
}),
))
}
pub(super) fn issue_blocked_state(store: &Store, args: &Value) -> ToolOutcome {
let raw = selector(args, "issue")?;
let number = match resolve_number(store, &raw) {
Ok(number) => number,
Err(()) => {
return Ok(with_number_resolution(store, &raw, || {
error_result(format!("invalid input: no issue matches selector `{raw}`"))
}));
}
};
let result = with_graph(store, |graph| {
let state = graph.blocked_state(number);
let note = crate::commands::blocked_note(&state);
let blockers: Vec<Value> = state
.blockers
.iter()
.map(|n| json!({ "number": n.number, "title": n.title }))
.collect();
let text = note.clone().unwrap_or_else(|| "not blocked".to_string());
Ok((
text,
json!({
"issue": number,
"is_blocked": state.is_blocked,
"blockers": blockers,
"reason": state.reason,
"done_column": graph.done_column(),
}),
))
});
let (text, structured) = match result {
Ok(ok) => ok,
Err(result) => return Ok(result),
};
Ok(success_result(text, structured))
}
pub(super) fn issue_chain(store: &Store, args: &Value) -> ToolOutcome {
let raw = selector(args, "issue")?;
let root = match resolve_number(store, &raw) {
Ok(number) => number,
Err(()) => {
return Ok(with_number_resolution(store, &raw, || {
error_result(format!("invalid input: no issue matches selector `{raw}`"))
}));
}
};
let result = with_graph(store, |graph| {
let chain = graph.dependency_chain(root);
let paths: Vec<Value> = chain
.paths
.iter()
.map(|path| {
Value::Array(
path.iter()
.map(|node| json!({ "number": node.number, "title": node.title }))
.collect(),
)
})
.collect();
let nodes: Vec<Value> = chain
.nodes()
.iter()
.map(|node| json!({ "number": node.number, "title": node.title }))
.collect();
let text = crate::commands::link::chain_output(&chain);
Ok((
text,
json!({
"root": root,
"paths": paths,
"nodes": nodes,
"truncated": chain.truncated,
}),
))
});
let (text, structured) = match result {
Ok(ok) => ok,
Err(result) => return Ok(result),
};
Ok(success_result(text, structured))
}
pub(super) fn link_issue(config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let (_, resolution) = crate::workflow::active_resolution(config);
let a = selector(args, "a")?;
let b = selector(args, "b")?;
let kind = match require_relation_kind(args, "kind") {
Ok(kind) => kind,
Err(result) => return Ok(result),
};
match store.link(&a, &b, kind) {
Ok(outcome) => {
let owner = outcome
.written
.as_ref()
.map(crate::id::canonical_ref)
.unwrap_or(outcome.relationship.from);
let issue = match store.get(crate::storage::IssueSelector::Ref(owner)) {
Ok(issue) => issue,
Err(e) => {
return Ok(error_result(format!(
"linked, but issue {owner} could not be read back: {e}"
)));
}
};
let text = crate::commands::link::link_message(&outcome);
Ok(success_result(
text,
json!({
"issue": issue_value(&issue, &resolution),
"relationship": relationship_value(&outcome.relationship),
"changed": outcome.changed,
}),
))
}
Err(e) => Ok(error_result(crate::error::render_error(&e))),
}
}
pub(super) fn list_children(store: &Store, args: &Value) -> ToolOutcome {
let number = match required_number(store, args) {
Ok(number) => number,
Err(()) => {
return Ok(error_result(
"invalid input: an `issue` selector is required".into(),
));
}
};
let snapshot = match hierarchy_snapshot(store) {
Ok(snapshot) => snapshot,
Err(()) => return Ok(error_result("could not read the issue set".into())),
};
if !snapshot.index.contains(number) {
return Ok(missing_issue(number));
}
let text = match crate::commands::parent::children_text(&snapshot.index, number) {
Ok(text) => text,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let membership = snapshot.index.children_of(number);
let children: Vec<Value> = membership
.children
.iter()
.map(|child| {
json!({
"id": child.id.to_string(),
"number": child.number,
"display": child.display,
})
})
.collect();
Ok(success_result(
text.clone(),
json!({
"issue": number,
"direct_children_only": true,
"count": children.len(),
"children": children,
"text": text,
}),
))
}
pub(super) fn list_relationships(store: &Store, args: &Value) -> ToolOutcome {
let issue = opt_selector(args, "issue")?;
let subject = match &issue {
Some(raw) => match resolve_number(store, raw) {
Ok(number) => Some(number),
Err(()) => {
return Ok(with_number_resolution(store, raw, || {
error_result(format!("invalid input: no issue matches selector `{raw}`"))
}));
}
},
None => None,
};
let kind = match args.get("kind") {
None | Some(Value::Null) => None,
Some(Value::String(raw)) => match crate::commands::link::parse_kind(raw) {
Ok(kind) => Some(kind),
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
},
Some(_) => {
return Ok(error_result(
"invalid input: `kind` must be a string naming one of blocks, blocked_by, duplicates, related"
.to_string(),
));
}
};
let wanted = kind.map(crate::commands::link::canonical_filter);
let result = with_graph(store, |graph| {
let edges: Vec<&crate::relationships::Relationship> = match subject {
Some(number) => graph.relationships_for(number),
None => graph.all_relationships().iter().collect(),
};
let shown: Vec<&crate::relationships::Relationship> = edges
.into_iter()
.filter(|rel| wanted.is_none_or(|w| rel.kind == w))
.collect();
let values: Vec<Value> = shown.iter().map(|rel| relationship_value(rel)).collect();
let text = crate::commands::link::listing_text(&shown, subject, wanted);
Ok((text, values))
});
let (text, values) = match result {
Ok(ok) => ok,
Err(result) => return Ok(result),
};
Ok(success_result(text, json!({ "relationships": values })))
}
pub(super) fn remove_relationship(store: &Store, args: &Value) -> ToolOutcome {
let id = req_string(args, "id")?;
match store.remove_relation(&id) {
Ok(outcome) => {
let text = crate::commands::link::remove_message(&outcome);
Ok(success_result(
text,
json!({ "relationship": relationship_value(&outcome.relationship), "changed": outcome.changed }),
))
}
Err(e) => Ok(error_result(crate::error::render_error(&e))),
}
}
pub(super) fn dispatch(
config: &Config,
store: &Store,
name: &str,
arguments: &Value,
) -> Option<ToolOutcome> {
match name {
"epic_hierarchy" => Some(epic_hierarchy(config, store, arguments)),
"link_issue" => Some(link_issue(config, store, arguments)),
"list_relationships" => Some(list_relationships(store, arguments)),
"edit_relationship" => Some(edit_relationship(store, arguments)),
"remove_relationship" => Some(remove_relationship(store, arguments)),
"issue_chain" => Some(issue_chain(store, arguments)),
"issue_blocked_state" => Some(issue_blocked_state(store, arguments)),
"list_children" => Some(list_children(store, arguments)),
"get_parent" => Some(get_parent(store, arguments)),
_ => None,
}
}