suture-cli 1.0.0

A patch-based version control system with semantic merge for structured files
pub(crate) fn resolve_ref<'a>(
    repo: &suture_core::repository::Repository,
    ref_str: &str,
    all_patches: &'a [suture_core::patch::types::Patch],
) -> Result<&'a suture_core::patch::types::Patch, Box<dyn std::error::Error>> {
    if ref_str == "HEAD" || ref_str.starts_with("HEAD~") {
        let (_branch_name, head_id) = repo.head().map_err(|e| e.to_string())?;
        let mut target_id = head_id;
        if let Some(n_str) = ref_str.strip_prefix("HEAD~") {
            let n: usize = n_str
                .parse()
                .map_err(|_| format!("invalid HEAD~N: {}", n_str))?;
            for _ in 0..n {
                let patch = all_patches
                    .iter()
                    .find(|p| p.id == target_id)
                    .ok_or_else(|| String::from("HEAD ancestor not found in patches"))?;
                target_id = *patch
                    .parent_ids
                    .first()
                    .ok_or_else(|| String::from("HEAD has no parent"))?;
            }
        }
        return all_patches
            .iter()
            .find(|p| p.id == target_id)
            .ok_or_else(|| "HEAD not found in patches".into());
    }

    {
        let branches = repo.list_branches();
        for (name, target_id) in &branches {
            if name == ref_str {
                return all_patches
                    .iter()
                    .find(|p| p.id == *target_id)
                    .ok_or_else(|| "branch tip not found in patches".into());
            }
        }
    }
    if let Ok(Some(target_id)) = repo.resolve_tag(ref_str) {
        return all_patches
            .iter()
            .find(|p| p.id == target_id)
            .ok_or_else(|| "tag target not found in patches".into());
    }
    let matches: Vec<&suture_core::patch::types::Patch> = all_patches
        .iter()
        .filter(|p| p.id.to_hex().starts_with(ref_str))
        .collect();
    match matches.len() {
        1 => Ok(matches[0]),
        0 => {
            let mut candidates: Vec<String> = Vec::new();
            for (name, _) in &repo.list_branches() {
                candidates.push(name.clone());
            }
            if let Ok(tags) = repo.list_tags() {
                for (name, _) in &tags {
                    candidates.push(name.clone());
                }
            }
            if let Some(suggestion) = crate::fuzzy::suggest(ref_str, &candidates) {
                Err(format!(
                    "unknown ref: '{}' (did you mean '{}'?)",
                    ref_str, suggestion
                )
                .into())
            } else {
                Err(format!("unknown ref: {}", ref_str).into())
            }
        }
        n => Err(format!("ambiguous ref '{}' matches {} commits", ref_str, n).into()),
    }
}

pub(crate) fn parse_time_filter(s: &str) -> Result<u64, String> {
    if let Some(rest) = s.strip_suffix(" ago") {
        let parts: Vec<&str> = rest.split_whitespace().collect();
        if parts.len() == 2
            && let Ok(n) = parts[0].parse::<u64>()
        {
            let now = std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs();
            let seconds = match parts[1] {
                "second" | "seconds" => n,
                "minute" | "minutes" => n * 60,
                "hour" | "hours" => n * 3600,
                "day" | "days" => n * 86400,
                "week" | "weeks" => n * 86400 * 7,
                "month" | "months" => n * 86400 * 30,
                "year" | "years" => n * 86400 * 365,
                _ => return Err(format!("unknown time unit: {}", parts[1])),
            };
            return Ok(now.saturating_sub(seconds));
        }
    }

    let date_str = s.trim();
    let parts: Vec<&str> = date_str.split('-').collect();
    if parts.len() == 3
        && let (Ok(year), Ok(month), Ok(day)) = (
            parts[0].parse::<u64>(),
            parts[1].parse::<u64>(),
            parts[2].parse::<u64>(),
        )
        && (1970..=2100).contains(&year)
        && (1..=12).contains(&month)
        && (1..=31).contains(&day)
    {
        let mut ts: u64 = 0;
        let mut y = 1970;
        while y < year {
            let days_in_year =
                if y.is_multiple_of(4) && (!y.is_multiple_of(100) || y.is_multiple_of(400)) {
                    366
                } else {
                    365
                };
            ts += days_in_year * 86400;
            y += 1;
        }
        for m in 1..month {
            ts += days_in_month(year, m) * 86400;
        }
        ts += (day - 1) * 86400;
        return Ok(ts);
    }

    Err(format!("invalid time filter: {}", s))
}

fn days_in_month(_year: u64, month: u64) -> u64 {
    match month {
        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
        4 | 6 | 9 | 11 => 30,
        2 => {
            let year = _year;
            if year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400)) {
                29
            } else {
                28
            }
        }
        _ => 30,
    }
}