magg 0.4.4

Opinionated tools for Rust devs
Documentation
use crate::errors::*;
use crate::model::*;
use crate::utils;
use std::fmt::Write;

pub fn get_project_report(project_owner: &str, project_name: &str) -> Result<String> {
    let projects = get_projects(false, project_owner)?;
    let Some(project) = projects
        .iter()
        .find(|project| project.title == project_name)
    else {
        return Err(MaggError::new(format!(
            "Project '{project_name}' not found for owner '{project_owner}'",
        )));
    };
    if project.closed {
        return Err(MaggError::new(format!(
            "Project '{project_name}' is closed",
        )));
    }
    let project_items = get_project_items(false, project_owner, project.number)?;
    let mut report_items = vec![];
    for project_item in project_items.iter().filter(|i| i.status == "done") {
        let groups = project_item
            .labels
            .iter()
            .filter(|l| l.is_group())
            .collect::<Vec<&GHLabel>>();
        if groups.is_empty() {
            return Err(MaggError::new(format!(
                "Item is not assigned to any group: {}",
                project_item.url
            )));
        }
        if groups.len() > 1 {
            return Err(MaggError::new(format!(
                "Item is assigned to multiple groups: {}",
                project_item.url
            )));
        }
        let mut report_item = project_item.clone();
        report_item.group = Some(groups[0].clone());
        report_items.push(report_item);
    }
    Ok(get_report(report_items))
}

/// Retrieves projects for specified owner.
fn get_projects(verbose: bool, project_owner: &str) -> Result<Vec<GHProject>> {
    let owner = format!("--owner={project_owner}");
    let args = &[
        "project",
        "list",
        owner.as_str(),
        "--limit=9999",
        "--format=json",
        r#"--template='{{range .projects}}{{printf "%v ||| %s ||| %v\n" .number .title .closed}}{{end}}'"#,
    ];
    let stdout = utils::execute_command(verbose, "gh", args, ".")?;
    parse_projects(stdout)
}

/// Parses project details.
fn parse_projects(input: String) -> Result<Vec<GHProject>> {
    let mut projects = vec![];
    let rows = utils::parse_columns(&input, 3)?;
    for columns in rows {
        projects.push(GHProject {
            number: columns[0].to_string().parse().unwrap(),
            title: columns[1].to_string(),
            closed: columns[2].to_string().parse().unwrap(),
        });
    }
    Ok(projects)
}

/// Retrieves project items for specified owner.
fn get_project_items(
    verbose: bool,
    project_owner: &str,
    project_number: usize,
) -> Result<Vec<GHProjectItem>> {
    let project_number = format!("{project_number}");
    let owner = format!("--owner={project_owner}");
    let args = &[
        "project",
        "item-list",
        project_number.as_str(),
        owner.as_str(),
        "--limit=9999",
        "--format=json",
        r#"--template='{{range .items}}{{printf "%v ||| %s ||| %s ||| %s ||| %s ||| %v\n" .content.number .content.title .content.url .repository .status .labels}}{{end}}'"#,
    ];
    let stdout = utils::execute_command(verbose, "gh", args, ".")?;
    parse_project_items(stdout)
}

/// Parses project item details.
fn parse_project_items(input: String) -> Result<Vec<GHProjectItem>> {
    let mut project_items = vec![];
    let rows = utils::parse_columns(&input, 6)?;
    for columns in rows {
        project_items.push(GHProjectItem {
            number: columns[0].to_string().parse().unwrap(),
            title: columns[1].to_string(),
            url: columns[2].to_string(),
            repository: columns[3].to_string(),
            status: columns[4].to_lowercase(),
            labels: parse_labels(&columns[5]),
            group: None,
        });
    }
    Ok(project_items)
}

fn parse_labels(mut input: &str) -> Vec<GHLabel> {
    input = input.trim();
    if input.is_empty() || input == "<nil>" {
        vec![]
    } else {
        if input.starts_with("[") {
            input = input.strip_prefix("[").unwrap();
        }
        if input.ends_with("]") {
            input = input.strip_suffix("]").unwrap();
        }
        input
            .split(" ")
            .map(GHLabel::from)
            .collect::<Vec<GHLabel>>()
    }
}

fn get_report(mut report_items: Vec<GHProjectItem>) -> String {
    report_items.sort_by_key(|i| i.repository.clone());
    println!("\nTotal count = {}", report_items.len());
    let mut output = String::new();
    for group in &GHLabel::groups() {
        _ = writeln!(&mut output, "{group}");
        let mut items_in_group = vec![];
        for report_item in report_items.iter().rev() {
            if let Some(item_group) = &report_item.group
                && item_group == group
            {
                items_in_group.push(report_item);
            }
        }
        items_in_group.sort_by(|a, b| {
            let repository_index_a = repository_index(&a.repository);
            let repository_index_b = repository_index(&b.repository);
            repository_index_a
                .cmp(&repository_index_b)
                .then(a.number.cmp(&b.number))
        });
        for item in items_in_group {
            _ = writeln!(&mut output, " - {}", item.url)
        }
    }
    output
}

fn repository_index(repository: &str) -> usize {
    const KEYS: [&str; 7] = [
        "wasmd",
        "wasmvm",
        "cosmwasm",
        "cw-storage-plus",
        "cw-plus",
        "advisories",
        "maintainers",
    ];
    for (i, key) in KEYS.iter().enumerate() {
        if repository.contains(key) {
            return i;
        }
    }
    panic!("no sorting key for repository: '{repository}'");
}