suture-cli 1.0.0

A patch-based version control system with semantic merge for structured files
use tokio::io::AsyncBufReadExt;

pub(crate) async fn cmd_add(
    paths: &[String],
    all: bool,
    patch: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let repo = suture_core::repository::Repository::open(std::path::Path::new("."))?;

    if all {
        let count = repo.add_all()?;
        println!("Staged {} files", count);
        return Ok(());
    }

    let file_paths: Vec<String> = if paths.is_empty() {
        return Err("no paths specified (use --all to stage everything)".into());
    } else {
        paths.to_vec()
    };

    if patch {
        cmd_add_patch(&repo, &file_paths).await
    } else {
        for path in &file_paths {
            repo.add(path)?;
            println!("Added {}", path);
        }
        Ok(())
    }
}

async fn cmd_add_patch(
    repo: &suture_core::repository::Repository,
    paths: &[String],
) -> Result<(), Box<dyn std::error::Error>> {
    let head_tree = repo
        .snapshot_head()
        .unwrap_or_else(|_| suture_core::engine::FileTree::empty());
    let mut stage_all = false;

    for path in paths {
        if stage_all {
            repo.add(path)?;
            println!("Added {}", path);
            continue;
        }

        if head_tree.contains(path) {
            let Some(&head_hash) = head_tree.get(path) else {
                continue;
            };
            let head_bytes = repo.cas().get_blob(&head_hash).unwrap_or_default();
            let head_content = String::from_utf8_lossy(&head_bytes);

            let disk_content = std::fs::read_to_string(path).unwrap_or_default();

            if head_content == disk_content {
                continue;
            }

            let head_lines: Vec<&str> = head_content.lines().collect();
            let disk_lines: Vec<&str> = disk_content.lines().collect();

            println!("\n--- diff for {} ---", path);
            print_line_diff(&head_lines, &disk_lines);
        } else {
            let content = std::fs::read_to_string(path).unwrap_or_default();
            let lines: Vec<&str> = content.lines().take(10).collect();
            println!("\n--- new file: {} (first {} lines) ---", path, lines.len());
            for line in &lines {
                println!("+ {}", line);
            }
            if content.lines().count() > 10 {
                println!("+ ... ({} more lines)", content.lines().count() - 10);
            }
        }

        loop {
            print!("Stage changes to {}? [y/n/q/a(ll)] ", path);
            use std::io::Write;
            std::io::stdout().flush()?;

            let mut input = String::new();
            tokio::io::BufReader::new(tokio::io::stdin())
                .read_line(&mut input)
                .await?;
            let answer = input.trim().to_lowercase();

            match answer.as_str() {
                "y" | "yes" => {
                    repo.add(path)?;
                    println!("Added {}", path);
                    break;
                }
                "n" | "no" => {
                    break;
                }
                "q" | "quit" => {
                    println!("Quit.");
                    return Ok(());
                }
                "a" | "all" => {
                    repo.add(path)?;
                    println!("Added {}", path);
                    stage_all = true;
                    break;
                }
                _ => {
                    eprintln!("Please answer y, n, q, or a.");
                }
            }
        }
    }

    Ok(())
}

fn print_line_diff(head_lines: &[&str], disk_lines: &[&str]) {
    let additions: Vec<&str> = disk_lines
        .iter()
        .filter(|l| !head_lines.contains(l))
        .copied()
        .collect();
    let removals: Vec<&str> = head_lines
        .iter()
        .filter(|l| !disk_lines.contains(l))
        .copied()
        .collect();

    for line in &removals {
        println!("- {}", line);
    }
    for line in &additions {
        println!("+ {}", line);
    }
}