suture-cli 1.0.0

A patch-based version control system with semantic merge for structured files
use crate::style::{ANSI_BOLD_CYAN, ANSI_GREEN, ANSI_RED, ANSI_RESET};

pub(crate) fn walk_repo_files(dir: &std::path::Path) -> Vec<String> {
    let mut files = Vec::new();
    walk_repo_files_inner(dir, dir, &mut files);
    files
}

fn walk_repo_files_inner(
    root: &std::path::Path,
    current: &std::path::Path,
    files: &mut Vec<String>,
) {
    let Ok(entries) = std::fs::read_dir(current) else {
        return;
    };
    for entry in entries.filter_map(|e| e.ok()) {
        let path = entry.path();
        let name = entry.file_name();
        if name == ".suture" {
            continue;
        }
        let rel = path
            .strip_prefix(root)
            .unwrap_or(&path)
            .to_string_lossy()
            .replace('\\', "/");
        if path.is_dir() {
            walk_repo_files_inner(root, &path, files);
        } else if path.is_file() {
            files.push(rel);
        }
    }
}

pub(crate) fn format_line_diff(path: &str, changes: &[suture_core::engine::merge::LineChange]) {
    use suture_core::engine::merge::LineChange;

    let has_changes = changes
        .iter()
        .any(|c| !matches!(c, LineChange::Unchanged(_)));
    if !has_changes {
        return;
    }

    println!("{ANSI_BOLD_CYAN}diff --git a/{path} b/{path}{ANSI_RESET}");
    println!("{ANSI_BOLD_CYAN}--- a/{path}{ANSI_RESET}");
    println!("{ANSI_BOLD_CYAN}+++ b/{path}{ANSI_RESET}");

    let mut old_line = 1usize;
    let mut new_line = 1usize;
    let mut i = 0;

    while i < changes.len() {
        match &changes[i] {
            LineChange::Unchanged(lines) => {
                old_line += lines.len();
                new_line += lines.len();
                i += 1;
            }
            LineChange::Deleted(_) | LineChange::Inserted(_) => {
                let hunk_old_start = old_line;
                let hunk_new_start = new_line;
                let mut hunk_old_count = 0usize;
                let mut hunk_new_count = 0usize;
                let mut hunk_lines: Vec<(char, String)> = Vec::new();

                while i < changes.len() {
                    match &changes[i] {
                        LineChange::Deleted(lines) => {
                            for line in lines {
                                hunk_lines.push(('-', line.clone()));
                                hunk_old_count += 1;
                                old_line += 1;
                            }
                            i += 1;
                        }
                        LineChange::Inserted(lines) => {
                            for line in lines {
                                hunk_lines.push(('+', line.clone()));
                                hunk_new_count += 1;
                                new_line += 1;
                            }
                            i += 1;
                        }
                        LineChange::Unchanged(_) => break,
                    }
                }

                println!(
                    "{ANSI_BOLD_CYAN}@@ -{hunk_old_start},{hunk_old_count} +{hunk_new_start},{hunk_new_count} @@{ANSI_RESET}"
                );
                for (prefix, line) in &hunk_lines {
                    if *prefix == '-' {
                        println!("{ANSI_RED}-{line}{ANSI_RESET}");
                    } else {
                        println!("{ANSI_GREEN}+{line}{ANSI_RESET}");
                    }
                }
            }
        }
    }
}

pub(crate) fn format_timestamp(ts: u64) -> String {
    let days = ts / 86400;
    let hours = (ts % 86400) / 3600;
    let minutes = (ts % 3600) / 60;
    let remaining_secs = ts % 60;
    format!(
        "{}d {:02}:{:02}:{:02} (unix: {})",
        days, hours, minutes, remaining_secs, ts
    )
}