wsb 0.85.227722

Workspace - A tool suite for file operations, version management, and development workflow automation
Documentation
use std::path::Path;
use std::process::Command;
use tempfile::TempDir;

fn git(path: &Path, args: &[&str]) {
    let output = Command::new("git")
        .args(args)
        .current_dir(path)
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "git {} in {} failed: {}",
        args.join(" "),
        path.display(),
        String::from_utf8_lossy(&output.stderr)
    );
}

fn git_config(path: &Path) {
    git(path, &["config", "user.email", "test@test.com"]);
    git(path, &["config", "user.name", "Test"]);
}

fn create_test_repo() -> TempDir {
    let tmp = TempDir::new().unwrap();
    git(tmp.path(), &["init", "-b", "main"]);
    git_config(tmp.path());
    std::fs::write(tmp.path().join("file1.rs"), "fn original() {}\n").unwrap();
    std::fs::write(tmp.path().join("file2.rs"), "fn two() {}\n").unwrap();
    git(tmp.path(), &["add", "."]);
    git(tmp.path(), &["commit", "-m", "Initial commit"]);
    tmp
}

fn wsb_git(repo_path: &Path, args: &[&str]) -> std::process::Output {
    let mut cmd_args = vec!["git"];
    cmd_args.extend_from_slice(args);
    Command::new(env!("CARGO_BIN_EXE_wsb"))
        .args(&cmd_args)
        .current_dir(repo_path)
        .output()
        .unwrap()
}

// ─── Tests ──────────────────────────────────────────────────────────────

#[test]
fn test_diff_no_changes_produces_no_files() {
    let repo = create_test_repo();
    let outdir = repo.path().join("diffs");

    let output = wsb_git(repo.path(), &["diff", "-o", outdir.to_str().unwrap()]);
    let stdout = String::from_utf8_lossy(&output.stdout);

    assert!(output.status.success(), "Should succeed with no changes");
    assert!(
        stdout.contains("No changes"),
        "Should report no changes. Got: {}",
        stdout
    );
    // Output dir should not exist or be empty
    assert!(
        !outdir.exists() || std::fs::read_dir(&outdir).unwrap().count() == 0,
        "No diff files should be created"
    );
}

#[test]
fn test_diff_unstaged_changes_produce_diff_files() {
    let repo = create_test_repo();
    let outdir = repo.path().join("diffs");

    // Modify a file without staging
    std::fs::write(repo.path().join("file1.rs"), "fn modified() {}\n").unwrap();

    let output = wsb_git(repo.path(), &["diff", "-o", outdir.to_str().unwrap()]);
    let stdout = String::from_utf8_lossy(&output.stdout);

    assert!(output.status.success(), "Should succeed");
    assert!(
        stdout.contains("1 diff file(s)"),
        "Should report 1 diff file. Got: {}",
        stdout
    );

    let diff_file = outdir.join("file1.rs.diff");
    assert!(diff_file.exists(), "file1.rs.diff should exist");

    let content = std::fs::read_to_string(&diff_file).unwrap();
    assert!(
        content.contains("original") && content.contains("modified"),
        "Diff should show old and new content. Got: {}",
        content
    );
}

#[test]
fn test_diff_staged_flag_includes_staged_changes() {
    let repo = create_test_repo();
    let outdir = repo.path().join("diffs");

    // Stage a change
    std::fs::write(repo.path().join("file2.rs"), "fn staged_change() {}\n").unwrap();
    git(repo.path(), &["add", "file2.rs"]);

    // Without --staged: should find nothing (change is staged, not unstaged)
    let output = wsb_git(repo.path(), &["diff", "-o", outdir.to_str().unwrap()]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        stdout.contains("No changes"),
        "Without --staged, should find no unstaged changes. Got: {}",
        stdout
    );

    // With --staged: should find the staged change
    let outdir2 = repo.path().join("diffs2");
    let output = wsb_git(
        repo.path(),
        &["diff", "--staged", "-o", outdir2.to_str().unwrap()],
    );
    let stdout = String::from_utf8_lossy(&output.stdout);

    assert!(output.status.success());
    assert!(
        stdout.contains("1 diff file(s)"),
        "With --staged, should find staged change. Got: {}",
        stdout
    );

    let diff_file = outdir2.join("file2.rs.diff");
    assert!(diff_file.exists(), "file2.rs.diff should exist");

    let content = std::fs::read_to_string(&diff_file).unwrap();
    assert!(
        content.contains("staged_change"),
        "Diff should contain staged change. Got: {}",
        content
    );
}

#[test]
fn test_diff_staged_appends_to_existing_diff() {
    let repo = create_test_repo();
    let outdir = repo.path().join("diffs");

    // Create both unstaged AND staged changes to the same file
    std::fs::write(repo.path().join("file1.rs"), "fn unstaged_ver() {}\n").unwrap();
    git(repo.path(), &["add", "file1.rs"]);
    // Now modify again (creating an unstaged change on top of the staged one)
    std::fs::write(repo.path().join("file1.rs"), "fn unstaged_ver2() {}\n").unwrap();

    let output = wsb_git(
        repo.path(),
        &["diff", "--staged", "-o", outdir.to_str().unwrap()],
    );
    let stdout = String::from_utf8_lossy(&output.stdout);

    assert!(output.status.success());
    assert!(
        stdout.contains("file1.rs"),
        "Should mention file1.rs. Got: {}",
        stdout
    );

    let diff_file = outdir.join("file1.rs.diff");
    assert!(diff_file.exists());

    let content = std::fs::read_to_string(&diff_file).unwrap();
    // Should contain both the unstaged diff (staged→working) and the staged diff (committed→staged)
    assert!(
        content.contains("unstaged_ver2") && content.contains("unstaged_ver"),
        "Diff should contain both unstaged and staged hunks. Got: {}",
        content
    );
}

#[test]
fn test_diff_with_base_ref() {
    let repo = create_test_repo();
    let outdir = repo.path().join("diffs");

    // Make a second commit
    std::fs::write(repo.path().join("file1.rs"), "fn second_commit() {}\n").unwrap();
    std::fs::write(repo.path().join("new_file.rs"), "fn brand_new() {}\n").unwrap();
    git(repo.path(), &["add", "."]);
    git(repo.path(), &["commit", "-m", "Second commit"]);

    // Diff against HEAD~1
    let output = wsb_git(
        repo.path(),
        &["diff", "--base", "HEAD~1", "-o", outdir.to_str().unwrap()],
    );
    let stdout = String::from_utf8_lossy(&output.stdout);

    assert!(output.status.success());
    assert!(
        stdout.contains("2 diff file(s)"),
        "Should find 2 changed files. Got: {}",
        stdout
    );

    assert!(
        outdir.join("file1.rs.diff").exists(),
        "file1.rs.diff should exist"
    );
    assert!(
        outdir.join("new_file.rs.diff").exists(),
        "new_file.rs.diff should exist"
    );

    let content = std::fs::read_to_string(outdir.join("new_file.rs.diff")).unwrap();
    assert!(
        content.contains("brand_new"),
        "new_file.rs diff should show new content. Got: {}",
        content
    );
}

#[test]
fn test_diff_preserves_directory_structure() {
    let repo = create_test_repo();
    let outdir = repo.path().join("diffs");

    // Create a file in a subdirectory
    std::fs::create_dir_all(repo.path().join("src/deep/nested")).unwrap();
    std::fs::write(
        repo.path().join("src/deep/nested/module.rs"),
        "fn nested() {}\n",
    )
    .unwrap();
    git(repo.path(), &["add", "."]);
    git(repo.path(), &["commit", "-m", "Add nested file"]);

    // Modify the nested file
    std::fs::write(
        repo.path().join("src/deep/nested/module.rs"),
        "fn nested_modified() {}\n",
    )
    .unwrap();

    let output = wsb_git(repo.path(), &["diff", "-o", outdir.to_str().unwrap()]);

    assert!(output.status.success());

    let diff_path = outdir.join("src/deep/nested/module.rs.diff");
    assert!(
        diff_path.exists(),
        "Diff file should preserve directory structure: {}",
        diff_path.display()
    );
}

#[test]
fn test_diff_with_invalid_base_ref_fails() {
    let repo = create_test_repo();
    let outdir = repo.path().join("diffs");

    let output = wsb_git(
        repo.path(),
        &[
            "diff",
            "--base",
            "nonexistent-ref-abc123",
            "-o",
            outdir.to_str().unwrap(),
        ],
    );

    assert!(
        !output.status.success(),
        "Should fail with invalid base ref"
    );
}