fundaia 0.25.2

Command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics
//! A unified diff, coloured the way git colours one.
//!
//! Deliberately git's appearance and not this tool's: whoever is reading a
//! patch in a terminal has read ten thousand of them in `git show`, and an
//! interface that invents its own green is one they have to learn. The line
//! prefixes stay in the output for the same reason — a pasted diff should still
//! apply.

use owo_colors::OwoColorize;

use crate::api::models::CommitFile;
use crate::render::{sanitise, theme};

/// What became of a file, in Spanish and in one word.
pub fn status_word(status: &str) -> &str {
    match status {
        "added" => "nuevo",
        "removed" => "borrado",
        "modified" => "modificado",
        "renamed" => "renombrado",
        "copied" => "copiado",
        "unchanged" => "sin cambios",
        other => other,
    }
}

/// The bar of pluses and minuses `git diff --stat` draws, at a fixed width.
///
/// Five characters, always, so a column of them lines up and the eye reads the
/// proportion rather than the number. A file with any additions at all keeps at
/// least one `+`: rounding a real change down to nothing is the one error this
/// must not make.
pub fn proportion(additions: u32, deletions: u32) -> String {
    let width = 5u32;
    let total = additions + deletions;
    if total == 0 {
        return "─".repeat(width as usize);
    }

    // Rounded rather than truncated, so this and the web's five squares answer
    // the same for the same file: one added and one removed is three and two,
    // not two and three.
    let pluses = (((additions * width) + total / 2) / total)
        .max(u32::from(additions > 0))
        .min(width);
    let minuses = width - pluses;

    format!(
        "{}{}",
        "+".repeat(pluses as usize).style(theme::success()),
        "-".repeat(minuses as usize).style(theme::danger())
    )
}

/// One file's patch, printed under its own heading.
pub fn print_file(file: &CommitFile) {
    println!();
    println!(
        "{} {}  {}",
        "▸".style(theme::muted()),
        heading(file).style(theme::strong()),
        format!(
            "{} {} · +{} −{}",
            status_word(&file.status),
            proportion(file.additions, file.deletions),
            file.additions,
            file.deletions
        )
        .style(theme::muted())
    );

    match &file.patch {
        // A patch is absent for a binary and for a file GitHub declines to send
        // one for, and the counters are what tell them apart: a binary reports
        // zero of both. Saying which is the difference between "there is
        // nothing to show" and "we did not fetch it".
        None if file.additions == 0 && file.deletions == 0 => {
            println!("  {}", "fichero binario".style(theme::muted()))
        }
        None => println!(
            "  {}",
            "GitHub no envía el diff de un fichero tan grande".style(theme::muted())
        ),
        Some(patch) => print_patch(patch),
    }
}

fn heading(file: &CommitFile) -> String {
    match &file.previous_path {
        Some(previous) => format!("{previous} → {}", file.path),
        None => file.path.clone(),
    }
}

fn print_patch(patch: &str) {
    // A patch is source somebody committed, and source may carry an escape
    // sequence on purpose — a shell script that colours its own output — which
    // is exactly the line that must be shown rather than obeyed.
    for line in sanitise::plain(patch).lines() {
        // The order matters: `+++` and `---` are file headers rather than
        // content, and GitHub sends them inside the patch of a rename.
        if line.starts_with("@@") {
            println!("  {}", line.style(theme::accent()));
        } else if line.starts_with("+++") || line.starts_with("---") {
            println!("  {}", line.style(theme::muted()));
        } else if line.starts_with('+') {
            println!("  {}", line.style(theme::success()));
        } else if line.starts_with('-') {
            println!("  {}", line.style(theme::danger()));
        } else if line.starts_with('\\') {
            println!("  {}", line.style(theme::muted()));
        } else {
            println!("  {line}");
        }
    }
}