use crate::bulk::{IssuePatch, PatchValue};
use crate::model::{Issue, Priority};
use crate::storage::Store;
pub fn is_field_path(patch: &IssuePatch) -> bool {
patch.relationship.is_none() && patch.archive.is_none()
}
pub fn is_pair_path(patch: &IssuePatch) -> bool {
patch.relationship.is_some() && !patch.has_any_field() && patch.archive.is_none()
}
pub fn merge_issue(issue: &Issue, patch: &IssuePatch) -> Issue {
let mut merged = issue.clone();
match &patch.priority {
PatchValue::Unchanged => {}
PatchValue::Set(v) => merged.priority = v.clone(),
PatchValue::Clear => merged.priority = Priority::default(),
}
match &patch.assignee {
PatchValue::Unchanged => {}
PatchValue::Set(v) => merged.assignee = Some(v.clone()),
PatchValue::Clear => merged.assignee = None,
}
merge_text(&mut merged.estimate, &patch.estimate);
merge_text(&mut merged.start_date, &patch.start_date);
merge_text(&mut merged.due_date, &patch.due_date);
match &patch.labels {
PatchValue::Unchanged => {}
PatchValue::Set(v) => merged.labels = v.clone(),
PatchValue::Clear => merged.labels = Vec::new(),
}
merge_list(&mut merged.cycles, &patch.cycles);
merge_list(&mut merged.milestones, &patch.milestones);
merged
}
fn merge_text(slot: &mut Option<String>, patch: &PatchValue<String>) {
match patch {
PatchValue::Unchanged => {}
PatchValue::Set(v) => *slot = Some(v.clone()),
PatchValue::Clear => *slot = None,
}
}
fn merge_list(slot: &mut Option<Vec<String>>, patch: &PatchValue<Vec<String>>) {
match patch {
PatchValue::Unchanged => {}
PatchValue::Set(v) => *slot = Some(v.clone()),
PatchValue::Clear => *slot = None,
}
}
pub fn moves_segment(issue: &Issue, patch: &IssuePatch) -> bool {
let PatchValue::Set(new) = &patch.assignee else {
return false;
};
let target = crate::assignee::parse_assignee(new);
let Some(target) = target else {
return false;
};
let from = crate::assignee::derive_segment(issue.assignee.as_deref());
let to = crate::assignee::derive_segment(Some(&target));
from != to
}
pub fn destination_path(store: &Store, issue: &Issue, patch: &IssuePatch) -> Option<String> {
if !moves_segment(issue, patch) {
return None;
}
let PatchValue::Set(new) = &patch.assignee else {
return None;
};
let target = crate::assignee::parse_assignee(new)?;
Some(
store
.issues_dir
.join(crate::assignee::derive_segment(Some(&target)))
.display()
.to_string(),
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldOutcome {
pub field: &'static str,
pub from: String,
pub to: String,
}
impl FieldOutcome {
pub fn changed(&self) -> bool {
self.from != self.to
}
}
pub fn field_outcomes(issue: &Issue, patch: &IssuePatch) -> Vec<FieldOutcome> {
let merged = merge_issue(issue, patch);
let out = |field: &'static str, from: String, to: String| FieldOutcome { field, from, to };
vec![
out(
"status",
issue.status.display().to_string(),
match &patch.status {
PatchValue::Set(target) => target.clone(),
_ => issue.status.display().to_string(),
},
),
out(
"priority",
render_priority(&issue.priority),
render_priority(&merged.priority),
),
out(
"assignee",
issue.assignee.clone().unwrap_or_default(),
merged.assignee.clone().unwrap_or_default(),
),
out("labels", issue.labels.join(", "), merged.labels.join(", ")),
out(
"estimate",
issue.estimate.clone().unwrap_or_default(),
merged.estimate.clone().unwrap_or_default(),
),
out(
"start-date",
issue.start_date.clone().unwrap_or_default(),
merged.start_date.clone().unwrap_or_default(),
),
out(
"due-date",
issue.due_date.clone().unwrap_or_default(),
merged.due_date.clone().unwrap_or_default(),
),
out(
"cycles",
issue.cycles.clone().unwrap_or_default().join(", "),
merged.cycles.clone().unwrap_or_default().join(", "),
),
out(
"milestones",
issue.milestones.clone().unwrap_or_default().join(", "),
merged.milestones.clone().unwrap_or_default().join(", "),
),
]
}
fn render_priority(p: &Priority) -> String {
if p.is_undefined() {
String::new()
} else {
p.to_string()
}
}