#[allow(clippy::wildcard_imports)]
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum EpicReferenceKind {
Epic,
Project,
Issue,
Milestone,
Cycle,
}
impl EpicReferenceKind {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
EpicReferenceKind::Epic => "epic",
EpicReferenceKind::Project => "project",
EpicReferenceKind::Issue => "issue",
EpicReferenceKind::Milestone => "milestone",
EpicReferenceKind::Cycle => "cycle",
}
}
#[must_use]
pub fn diagnostic_kind(self) -> &'static str {
match self {
EpicReferenceKind::Epic => "missing_epic_reference",
EpicReferenceKind::Project => "missing_project_reference",
EpicReferenceKind::Issue => "missing_issue_reference",
EpicReferenceKind::Milestone => "missing_milestone_reference",
EpicReferenceKind::Cycle => "missing_cycle_reference",
}
}
}
impl fmt::Display for EpicReferenceKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ReferenceHolder {
Epic(EpicId),
Issue(IssueKey),
}
impl fmt::Display for ReferenceHolder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ReferenceHolder::Epic(id) => write!(f, "epic `{id}`"),
ReferenceHolder::Issue(key) => write!(f, "issue {key}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EpicReferenceResolution {
pub holder: ReferenceHolder,
pub target_kind: EpicReferenceKind,
pub target: String,
pub reason: String,
}
#[must_use]
pub fn missing_epic_reason(holder: &ReferenceHolder, target: &str) -> String {
format!(
"{holder} records epic reference `{target}`, which is not in the registry \
(the reference is kept so it can be repaired)"
)
}
#[must_use]
pub fn malformed_epic_reason(issue: u32, raw: &str) -> String {
format!(
"issue #{issue} records epic reference `{raw}`, which is not a well-formed epic id \
(ids are recorded, never names; the entry is kept so it can be repaired)"
)
}
#[must_use]
pub fn missing_project_reason(epic: &EpicId, target: &str) -> String {
format!(
"epic `{epic}` references project `{target}`, which is not in the project registry \
(the reference is kept so it can be repaired)"
)
}
#[must_use]
pub fn missing_milestone_reason(epic: &EpicId, target: &str) -> String {
format!(
"epic `{epic}` references milestone `{target}`, which is not in the milestone registry \
(the reference is kept so it can be repaired)"
)
}
#[must_use]
pub fn missing_cycle_reason(epic: &EpicId, target: &str) -> String {
format!(
"epic `{epic}` references cycle `{target}`, which is not in the cycle registry \
(the reference is kept so it can be repaired)"
)
}
pub(super) fn reference_order(
a: &EpicReferenceResolution,
b: &EpicReferenceResolution,
) -> std::cmp::Ordering {
a.holder
.cmp(&b.holder)
.then_with(|| a.target_kind.cmp(&b.target_kind))
.then_with(|| a.target.cmp(&b.target))
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ReferenceTargets {
pub projects: HashSet<String>,
pub milestones: HashSet<String>,
pub cycles: HashSet<String>,
}
impl ReferenceTargets {
pub fn from_planner(planner: &Planner) -> Result<Self> {
let mut cycles: HashSet<String> = planner.cycle_names().into_iter().collect();
if planner.cycle(DEFAULT_CYCLE_NAME).is_some() {
cycles.insert(DEFAULT_CYCLE_NAME.to_string());
}
Ok(Self {
projects: planner.config().projects.keys().cloned().collect(),
milestones: planner.milestone_names()?.into_iter().collect(),
cycles,
})
}
#[must_use]
pub fn project_known(&self, id: &str) -> bool {
self.projects.contains(id)
}
#[must_use]
pub fn milestone_known(&self, name: &str) -> bool {
self.milestones.contains(name)
}
#[must_use]
pub fn cycle_known(&self, name: &str) -> bool {
self.cycles.contains(name)
}
}
#[must_use]
pub fn resolve_references<N: EpicNode>(
registry: &IndexMap<String, EpicConfiguration>,
issues: &[N],
targets: &ReferenceTargets,
) -> Vec<EpicReferenceResolution> {
let index = EpicIndex::build_from_registry(registry, issues);
let mut out: Vec<EpicReferenceResolution> = index.missing().to_vec();
for epic in index.epics() {
out.extend(index.missing_for(&epic.id, targets));
}
out.sort_by(reference_order);
out
}
pub(super) fn find_issue<N: EpicNode>(issues: &[N], selector: u32) -> Result<&N, EpicError> {
issues
.iter()
.find(|issue| issue.number() == selector)
.ok_or(EpicError::UnknownIssue { selector })
}
pub(super) fn reference_resolved<N: EpicNode>(
issues: &[N],
selector: u32,
id: &EpicId,
) -> Result<EpicChange, EpicError> {
let issue = find_issue(issues, selector)?;
let current = issue.epic_ids();
let key = IssueKey::new(selector);
if current.iter().any(|stored| stored == id.as_str()) {
return Ok(EpicChange {
issue: key,
epic_ids: current.to_vec(),
event: None,
});
}
let mut next = current.to_vec();
next.push(id.to_string());
Ok(EpicChange {
issue: key,
epic_ids: next,
event: Some(EpicEvent {
kind: EpicEventKind::EpicReferenced,
epic_id: id.to_string(),
detail: reference_detail(id.as_str(), true),
}),
})
}
pub fn reference_epic<N: EpicNode>(
config: &Config,
issues: &[N],
issue_selector: u32,
epic_reference: &str,
) -> Result<EpicChange, EpicError> {
let id = resolve_epic_id(&config.epics, epic_reference)?;
reference_resolved(issues, issue_selector, &id)
}
pub fn dereference_epic<N: EpicNode>(
issues: &[N],
issue_selector: u32,
epic_id: &str,
) -> Result<EpicChange, EpicError> {
let issue = find_issue(issues, issue_selector)?;
let current = issue.epic_ids();
let key = IssueKey::new(issue_selector);
if !current.iter().any(|stored| stored == epic_id) {
return Ok(EpicChange {
issue: key,
epic_ids: current.to_vec(),
event: None,
});
}
let next: Vec<String> = current
.iter()
.filter(|stored| stored.as_str() != epic_id)
.cloned()
.collect();
Ok(EpicChange {
issue: key,
epic_ids: next,
event: Some(EpicEvent {
kind: EpicEventKind::EpicDereferenced,
epic_id: epic_id.to_string(),
detail: reference_detail(epic_id, false),
}),
})
}
#[must_use]
pub fn bulk_reference<N: EpicNode>(
config: &Config,
issues: &[N],
epic_reference: &str,
issue_selectors: &[u32],
) -> Vec<BulkEpicOutcome> {
let resolved = resolve_epic_id(&config.epics, epic_reference);
issue_selectors
.iter()
.map(|&selector| {
let outcome = match &resolved {
Ok(id) => reference_resolved(issues, selector, id),
Err(err) => Err(err.clone()),
};
BulkEpicOutcome { selector, outcome }
})
.collect()
}