tix-tracker 0.12.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
#[allow(clippy::wildcard_imports)]
use super::*;

pub(super) fn group_value(issue: &Issue, group: &str) -> String {
    let values = match group {
        "cycle" => issue.cycles.as_ref(),
        "milestone" => issue.milestones.as_ref(),
        _ => None,
    };
    values
        .and_then(|values| values.first().cloned())
        .unwrap_or_else(|| "(none)".to_string())
}

/// The group-section label for one issue.
///
/// `project` reads the **resolved** memberships the caller built from the shared
/// index, so its labels are the service's ([`crate::projects::project_bucket_label`])
/// and cannot differ from the board's — an issue in no project is the clearly
/// labelled bucket, an issue in several collapses to one bucket, and a single
/// membership is labelled with the project **id** (WP09 item D). Every other key is
/// read straight off the issue.
pub(super) fn group_key(
    issue: &Issue,
    group: &str,
    project_groups: Option<&BTreeMap<u32, String>>,
    epic_groups: Option<&BTreeMap<u32, String>>,
) -> String {
    match group {
        "project" => project_groups
            .and_then(|groups| groups.get(&issue.number).cloned())
            .unwrap_or_else(|| crate::projects::NO_PROJECT_BUCKET.to_string()),
        "epic" => epic_groups
            .and_then(|groups| groups.get(&issue.number).cloned())
            .unwrap_or_else(|| crate::search::NO_EPIC_BUCKET.to_string()),
        _ => group_value(issue, group),
    }
}

/// Issue number -> its project bucket label, from the shared membership index.
///
/// One [`crate::projects::ProjectIndex`] build for the whole listing, so the labels
/// are derived from the same buckets `tix project list` counts and the board
/// partitions (C-006, WP09 item D).
pub(super) fn project_group_labels(store: &Store, issues: &[Issue]) -> BTreeMap<u32, String> {
    let index = crate::projects::ProjectIndex::build(&store.config, issues);
    let memberships = index.resolved_memberships();
    issues
        .iter()
        .map(|issue| {
            let ids = memberships.get(&issue.number).cloned().unwrap_or_default();
            (issue.number, crate::projects::project_bucket_label(&ids))
        })
        .collect()
}

/// Issue number -> its epic bucket label, from the shared membership helper.
///
/// One [`crate::epics::memberships_by_issue`] read for the whole listing, so the
/// labels are derived from the same resolved memberships the board's epic partition
/// and `tix status --group epic` use, and labelled by the service's own
/// [`crate::search::epic_bucket_label`] (WP09 item C).
pub(super) fn epic_group_labels(store: &Store, issues: &[Issue]) -> BTreeMap<u32, String> {
    let memberships = crate::epics::memberships_by_issue(&store.config, issues);
    issues
        .iter()
        .map(|issue| {
            let ids = memberships.get(&issue.number).cloned().unwrap_or_default();
            (issue.number, crate::search::epic_bucket_label(&ids))
        })
        .collect()
}

/// Sort issues by the project partition, the board's own rank.
///
/// A single membership ranks by its project's **declaration index**, several rank
/// after every declared project (`(multiple projects)`), and none sorts **last** so
/// `(no project)` is the final section — the same order
/// [`crate::ui::board`]'s partition uses.
pub(super) fn sort_by_project(store: &Store, issues: &mut [Issue], groups: &BTreeMap<u32, String>) {
    let declaration: Vec<&str> = store.config.projects.keys().map(String::as_str).collect();
    let rank = |issue: &Issue| -> usize {
        match groups.get(&issue.number).map(String::as_str) {
            Some(crate::projects::MULTIPLE_PROJECTS_BUCKET) => usize::MAX - 1,
            Some(crate::projects::NO_PROJECT_BUCKET) | None => usize::MAX,
            Some(id) => declaration
                .iter()
                .position(|candidate| *candidate == id)
                .unwrap_or(usize::MAX),
        }
    };
    issues.sort_by(|a, b| rank(a).cmp(&rank(b)).then(a.number.cmp(&b.number)));
}

/// Sort issues by the epic partition, the board's own rank.
///
/// The epic twin of [`sort_by_project`]: a single membership ranks by its epic's
/// **declaration index**, several rank after every declared epic
/// ([`crate::search::MULTIPLE_EPICS_BUCKET`]), and none sorts **last** so
/// [`crate::search::NO_EPIC_BUCKET`] is the final section — the same order the
/// board's [`crate::ui::board`] partition uses (WP09 item C).
pub(super) fn sort_by_epic(store: &Store, issues: &mut [Issue], groups: &BTreeMap<u32, String>) {
    let declaration: Vec<&str> = store.config.epics.keys().map(String::as_str).collect();
    let rank = |issue: &Issue| -> usize {
        match groups.get(&issue.number).map(String::as_str) {
            Some(crate::search::MULTIPLE_EPICS_BUCKET) => usize::MAX - 1,
            Some(crate::search::NO_EPIC_BUCKET) | None => usize::MAX,
            Some(id) => declaration
                .iter()
                .position(|candidate| *candidate == id)
                .unwrap_or(usize::MAX),
        }
    };
    issues.sort_by(|a, b| rank(a).cmp(&rank(b)).then(a.number.cmp(&b.number)));
}