jira_cli/api/client/
hierarchy.rs1use super::metadata::EPIC_LINK_SCHEMA;
4use super::{ApiError, JiraClient};
5use crate::api::types::Issue;
6use std::sync::atomic::Ordering;
7
8impl JiraClient {
9 pub fn enable_epic_lookup(&self) {
15 self.epic_lookup.store(true, Ordering::Relaxed);
16 }
17
18 pub(super) async fn issue_fields(&self, base: &[&str]) -> Result<Vec<String>, ApiError> {
21 let mut fields: Vec<String> = base.iter().map(|f| (*f).to_owned()).collect();
22 fields.extend(self.epic_link_fields().await?.iter().cloned());
23 Ok(fields)
24 }
25
26 async fn epic_link_fields(&self) -> Result<&[String], ApiError> {
33 if self.api_version >= 3 || !self.epic_lookup.load(Ordering::Relaxed) {
34 return Ok(&[]);
35 }
36 let fields = self
37 .epic_link_fields
38 .get_or_try_init(|| async {
39 Ok::<_, ApiError>(
40 self.list_fields()
41 .await?
42 .into_iter()
43 .filter(|f| {
44 f.schema
45 .as_ref()
46 .is_some_and(|s| s.custom.as_deref() == Some(EPIC_LINK_SCHEMA))
47 })
48 .map(|f| f.id)
49 .collect(),
50 )
51 })
52 .await?;
53 Ok(fields)
54 }
55
56 pub(super) async fn fill_epics(&self, issues: &mut [Issue]) -> Result<(), ApiError> {
58 if !self.epic_lookup.load(Ordering::Relaxed) {
59 return Ok(());
60 }
61 if self.api_version >= 3 {
62 for issue in issues {
63 issue.epic = cloud_epic(issue);
64 }
65 return Ok(());
66 }
67 let fields = self.epic_link_fields().await?;
68 for issue in issues {
69 issue.epic = data_center_epic(issue, fields)?;
70 }
71 Ok(())
72 }
73}
74
75fn data_center_epic(issue: &Issue, fields: &[String]) -> Result<Option<String>, ApiError> {
79 let mut epic: Option<&str> = None;
80 for field in fields {
81 let key = match issue.fields.extra.get(field) {
82 None | Some(serde_json::Value::Null) => continue,
83 Some(serde_json::Value::String(key)) => key.as_str(),
84 Some(other) => {
85 return Err(ApiError::Other(format!(
86 "Unexpected Epic Link value on {} in {field}: {other}",
87 issue.key
88 )));
89 }
90 };
91 match epic {
92 Some(seen) if seen != key => {
93 return Err(ApiError::Other(format!(
94 "{} has conflicting Epic Link values in {}: {seen} and {key}",
95 issue.key,
96 fields.join(", ")
97 )));
98 }
99 _ => epic = Some(key),
100 }
101 }
102 Ok(epic.map(str::to_owned))
103}
104
105fn cloud_epic(issue: &Issue) -> Option<String> {
108 let parent = issue.fields.parent.as_ref()?;
109 let issue_type = parent.fields.as_ref()?.issuetype.as_ref()?;
110 let is_epic = match issue_type.hierarchy_level {
111 Some(level) => level == 1,
112 None => issue_type.name.eq_ignore_ascii_case("Epic"),
113 };
114 is_epic.then(|| parent.key.clone())
115}