sem-cli 0.10.1

Semantic version control CLI. Shows what entities changed (functions, classes, methods) instead of lines.
use std::path::Path;
use std::process::{Command, Output};

struct TempRepo {
    repo: tempfile::TempDir,
    home: tempfile::TempDir,
}

impl TempRepo {
    fn new() -> Self {
        let repo = tempfile::tempdir().expect("create temp repo");
        let home = tempfile::tempdir().expect("create temp home");

        run_git(repo.path(), &["init", "-q"]);
        run_git(repo.path(), &["config", "user.name", "Test"]);
        run_git(repo.path(), &["config", "user.email", "test@example.com"]);

        Self { repo, home }
    }

    fn path(&self) -> &Path {
        self.repo.path()
    }

    fn run_sem(&self, args: &[&str]) -> Output {
        Command::new(env!("CARGO_BIN_EXE_sem"))
            .args(args)
            .current_dir(self.repo.path())
            .env("HOME", self.home.path())
            .output()
            .expect("run sem")
    }
}

fn run_git(repo: &Path, args: &[&str]) -> Output {
    let output = Command::new("git")
        .args(args)
        .current_dir(repo)
        .output()
        .expect("run git");
    assert!(
        output.status.success(),
        "git {:?} failed: {}",
        args,
        String::from_utf8_lossy(&output.stderr)
    );
    output
}

fn commit_file(repo: &TempRepo, path: &str, content: &str) {
    std::fs::write(repo.path().join(path), content).expect("write file");
    run_git(repo.path(), &["add", path]);
    run_git(repo.path(), &["commit", "-qm", "init"]);
}

#[test]
fn markdown_verbose_diff_uses_fence_longer_than_source_backticks() {
    let repo = TempRepo::new();
    commit_file(
        &repo,
        "app.py",
        "def foo():\n    s = \"plain\"\n    return s\n",
    );
    std::fs::write(
        repo.path().join("app.py"),
        "def foo():\n    s = \"X``` Y ``` Z\"\n    return s\n",
    )
    .expect("write modified file");

    let output = repo.run_sem(&["diff", "--format", "markdown", "-v"]);
    assert!(
        output.status.success(),
        "sem failed\nstdout: {}\nstderr: {}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    let stdout = String::from_utf8(output.stdout).expect("stdout should be utf-8");
    let lines: Vec<&str> = stdout.lines().collect();
    let opening = lines
        .iter()
        .position(|line| *line == "````diff")
        .expect("diff block should open with a 4-backtick fence");
    let closing = lines[opening + 1..]
        .iter()
        .position(|line| *line == "````")
        .map(|offset| opening + 1 + offset)
        .expect("diff block should close with a matching 4-backtick fence");

    assert!(lines[opening + 1..closing]
        .iter()
        .any(|line| line.contains("```")));
    assert!(!lines.iter().any(|line| *line == "```diff"), "{stdout}");
}

#[test]
fn terminal_verbose_diff_escapes_source_control_bytes_with_color_never() {
    let repo = TempRepo::new();
    commit_file(&repo, "app.py", "def foo():\n    return 1\n");
    std::fs::write(
        repo.path().join("app.py"),
        "def foo():\n    s = \"\u{1b}[31mRED\u{1b}[0m\"\n    return s\n",
    )
    .expect("write modified file");

    let output = repo.run_sem(&["diff", "-v", "--color", "never"]);
    assert!(
        output.status.success(),
        "sem failed\nstdout: {}\nstderr: {}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );

    assert!(
        !output.stdout.contains(&0x1b),
        "stdout contains raw ESC bytes: {}",
        String::from_utf8_lossy(&output.stdout)
    );
    let stdout = String::from_utf8(output.stdout).expect("stdout should be utf-8");
    assert!(stdout.contains("\\u{1b}[31mRED\\u{1b}[0m"), "{stdout}");
}

#[test]
#[cfg(not(windows))]
fn terminal_unsupported_file_warning_escapes_file_path_control_bytes() {
    let repo = TempRepo::new();
    let file_name = "bad\u{1b}[31m.txt";
    commit_file(&repo, file_name, "alpha\nbeta\n");
    std::fs::write(repo.path().join(file_name), "alpha\nchanged\n").expect("write modified file");

    let output = repo.run_sem(&["diff", "-v", "--color", "never"]);
    assert!(
        output.status.success(),
        "sem failed\nstdout: {}\nstderr: {}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );

    assert!(
        !output.stdout.contains(&0x1b),
        "stdout contains raw ESC bytes: {}",
        String::from_utf8_lossy(&output.stdout)
    );
    let stdout = String::from_utf8(output.stdout).expect("stdout should be utf-8");
    assert!(stdout.contains("bad\\u{1b}[31m.txt"), "{stdout}");
    assert!(
        stdout.contains("used line-based chunking (unsupported file extension)"),
        "{stdout}"
    );
}