use assert_cmd::Command;
use std::fs;
use tempfile::TempDir;
fn git(repo_path: &std::path::Path, args: &[&str]) {
let output = std::process::Command::new("git")
.args(args)
.current_dir(repo_path)
.output()
.unwrap();
assert!(
output.status.success(),
"git {} failed: {}",
args.join(" "),
String::from_utf8_lossy(&output.stderr)
);
}
fn init_repo(dir: &TempDir) {
let repo_path = dir.path();
git(repo_path, &["init", "--initial-branch=main"]);
git(repo_path, &["config", "user.email", "test@test.com"]);
git(repo_path, &["config", "user.name", "Test"]);
}
fn setup_repo(filename: &str, initial_content: &str) -> TempDir {
let dir = TempDir::new().unwrap();
let repo_path = dir.path();
init_repo(&dir);
let file_path = repo_path.join(filename);
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(&file_path, initial_content).unwrap();
git(repo_path, &["add", "."]);
git(repo_path, &["commit", "-m", "initial commit"]);
dir
}
fn setup_repo_multi(files: &[(&str, &str)]) -> TempDir {
let dir = TempDir::new().unwrap();
let repo_path = dir.path();
init_repo(&dir);
for (filename, content) in files {
let file_path = repo_path.join(filename);
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(&file_path, content).unwrap();
}
git(repo_path, &["add", "."]);
git(repo_path, &["commit", "-m", "initial commit"]);
dir
}
fn run_skim_diff(dir: &TempDir, extra_args: &[&str]) -> assert_cmd::assert::Assert {
let mut cmd = Command::cargo_bin("skim").unwrap();
cmd.current_dir(dir.path());
cmd.args(["git", "diff"]);
cmd.args(extra_args);
cmd.assert()
}
#[test]
fn test_diff_working_tree_typescript() {
let initial = r#"import { Request } from 'express';
export function greet(name: string): string {
return `Hello, ${name}!`;
}
export function farewell(name: string): string {
return `Goodbye, ${name}!`;
}
"#;
let modified = r#"import { Request } from 'express';
export function greet(name: string, title?: string): string {
const prefix = title ? `${title} ` : '';
return `Hello, ${prefix}${name}!`;
}
export function farewell(name: string): string {
return `Goodbye, ${name}!`;
}
"#;
let dir = setup_repo("src/greet.ts", initial);
fs::write(dir.path().join("src/greet.ts"), modified).unwrap();
let assert = run_skim_diff(&dir, &[]);
let output = assert.success().get_output().stdout.clone();
let stdout = String::from_utf8(output).unwrap();
assert!(
stdout.contains("greet.ts"),
"expected file path in output, got:\n{stdout}"
);
assert!(
stdout.contains('+') || stdout.contains('-'),
"expected +/- patch markers in AST-aware output, got:\n{stdout}"
);
assert!(
stdout.contains("greet"),
"expected changed function 'greet' in output, got:\n{stdout}"
);
assert!(
stdout.contains("title"),
"expected new parameter 'title' in diff output, got:\n{stdout}"
);
}
#[test]
fn test_diff_no_changes() {
let content = "function hello() { return 'hi'; }\n";
let dir = setup_repo("src/hello.ts", content);
let assert = run_skim_diff(&dir, &[]);
assert
.success()
.stderr(predicates::str::contains("No changes"));
}
#[test]
fn test_diff_new_file_unstaged() {
let initial = "function old() {}\n";
let dir = setup_repo("old.ts", initial);
fs::write(dir.path().join("new.ts"), "function newFn() {}\n").unwrap();
let assert = run_skim_diff(&dir, &[]);
assert
.success()
.stderr(predicates::str::contains("No changes"));
}
#[test]
fn test_diff_staged() {
let initial = "export const VERSION = '1.0';\n";
let modified = "export const VERSION = '2.0';\n";
let dir = setup_repo("version.ts", initial);
fs::write(dir.path().join("version.ts"), modified).unwrap();
git(dir.path(), &["add", "version.ts"]);
let assert = run_skim_diff(&dir, &["--cached"]);
let output = assert.success().get_output().stdout.clone();
let stdout = String::from_utf8(output).unwrap();
assert!(
stdout.contains("version.ts"),
"expected file path in staged output, got:\n{stdout}"
);
assert!(
stdout.contains("2.0") || stdout.contains("VERSION"),
"expected version change in staged diff output, got:\n{stdout}"
);
}
#[test]
fn test_diff_stat_passthrough() {
let initial = "const x = 1;\n";
let modified = "const x = 2;\n";
let dir = setup_repo("main.ts", initial);
fs::write(dir.path().join("main.ts"), modified).unwrap();
let assert = run_skim_diff(&dir, &["--stat"]);
assert
.success()
.stdout(predicates::str::contains("main.ts"));
}
#[test]
fn test_diff_name_only_passthrough() {
let initial = "const x = 1;\n";
let modified = "const x = 2;\n";
let dir = setup_repo("main.ts", initial);
fs::write(dir.path().join("main.ts"), modified).unwrap();
let assert = run_skim_diff(&dir, &["--name-only"]);
assert
.success()
.stdout(predicates::str::contains("main.ts"));
}
#[test]
fn test_diff_unsupported_language_falls_back_to_raw() {
let initial = "Hello world\n";
let modified = "Hello modified world\n";
let dir = setup_repo("readme.txt", initial);
fs::write(dir.path().join("readme.txt"), modified).unwrap();
let assert = run_skim_diff(&dir, &[]);
assert
.success()
.stdout(predicates::str::contains("readme.txt"));
}
#[test]
fn test_diff_rust_file() {
let initial = r#"fn main() {
println!("hello");
}
fn helper() -> i32 {
42
}
"#;
let modified = r#"fn main() {
println!("hello world");
eprintln!("debug");
}
fn helper() -> i32 {
42
}
"#;
let dir = setup_repo("src/main.rs", initial);
fs::write(dir.path().join("src/main.rs"), modified).unwrap();
let assert = run_skim_diff(&dir, &[]);
assert
.success()
.stdout(predicates::str::contains("main.rs"))
.stdout(predicates::str::contains("modified"));
}
#[test]
fn test_diff_multiple_files() {
let dir = setup_repo_multi(&[
("src/a.ts", "export function a() { return 1; }\n"),
("src/b.ts", "export function b() { return 2; }\n"),
]);
fs::write(
dir.path().join("src/a.ts"),
"export function a() { return 10; }\n",
)
.unwrap();
fs::write(
dir.path().join("src/b.ts"),
"export function b() { return 20; }\n",
)
.unwrap();
let assert = run_skim_diff(&dir, &[]);
assert
.success()
.stdout(predicates::str::contains("a.ts"))
.stdout(predicates::str::contains("b.ts"));
}
#[test]
fn test_diff_mode_structure() {
let initial = r#"import { Request } from 'express';
export function greet(name: string): string {
return `Hello, ${name}!`;
}
export function farewell(name: string): string {
return `Goodbye, ${name}!`;
}
"#;
let modified = r#"import { Request } from 'express';
export function greet(name: string, title?: string): string {
const prefix = title ? `${title} ` : '';
return `Hello, ${prefix}${name}!`;
}
export function farewell(name: string): string {
return `Goodbye, ${name}!`;
}
"#;
let dir = setup_repo("src/greet.ts", initial);
fs::write(dir.path().join("src/greet.ts"), modified).unwrap();
let assert = run_skim_diff(&dir, &["--mode", "structure"]);
let output = assert.success().get_output().stdout.clone();
let stdout = String::from_utf8(output).unwrap();
assert!(
stdout.contains("greet"),
"expected 'greet' in output, got:\n{stdout}"
);
assert!(
stdout.contains("farewell"),
"expected unchanged 'farewell' to appear in structure mode, got:\n{stdout}"
);
}
#[test]
fn test_diff_mode_full() {
let initial = r#"export function greet(name: string): string {
return `Hello, ${name}!`;
}
export function farewell(name: string): string {
return `Goodbye, ${name}!`;
}
"#;
let modified = r#"export function greet(name: string, title?: string): string {
const prefix = title ? `${title} ` : '';
return `Hello, ${prefix}${name}!`;
}
export function farewell(name: string): string {
return `Goodbye, ${name}!`;
}
"#;
let dir = setup_repo("src/greet.ts", initial);
fs::write(dir.path().join("src/greet.ts"), modified).unwrap();
let assert = run_skim_diff(&dir, &["--mode", "full"]);
let output = assert.success().get_output().stdout.clone();
let stdout = String::from_utf8(output).unwrap();
assert!(
stdout.contains("greet"),
"expected 'greet' in output, got:\n{stdout}"
);
assert!(
stdout.contains("farewell") && stdout.contains("Goodbye"),
"expected unchanged 'farewell' with full body in full mode, got:\n{stdout}"
);
}
#[test]
fn test_diff_json_output() {
let initial = "const x = 1;\n";
let modified = "const x = 2;\n";
let dir = setup_repo("main.ts", initial);
fs::write(dir.path().join("main.ts"), modified).unwrap();
let assert = run_skim_diff(&dir, &["--json"]);
let output = assert.success().get_output().stdout.clone();
let stdout = String::from_utf8(output).unwrap();
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("expected valid JSON output");
assert!(
parsed.get("files_changed").is_some(),
"expected 'files_changed' in JSON output"
);
assert!(
parsed.get("files").is_some(),
"expected 'files' in JSON output"
);
let files = parsed["files"].as_array().unwrap();
assert!(!files.is_empty());
assert_eq!(files[0]["path"], "main.ts");
}
#[test]
fn test_diff_show_stats() {
let initial = "const x = 1;\n";
let modified = "const x = 2;\n";
let dir = setup_repo("main.ts", initial);
fs::write(dir.path().join("main.ts"), modified).unwrap();
let assert = run_skim_diff(&dir, &["--show-stats"]);
assert.success().stderr(predicates::str::contains("tokens"));
}
#[test]
fn test_diff_name_status_passthrough() {
let initial = "const x = 1;\n";
let modified = "const x = 2;\n";
let dir = setup_repo("main.ts", initial);
fs::write(dir.path().join("main.ts"), modified).unwrap();
let assert = run_skim_diff(&dir, &["--name-status"]);
let output = assert.success().get_output().stdout.clone();
let stdout = String::from_utf8(output).unwrap();
assert!(
stdout.contains("main.ts"),
"expected 'main.ts' in --name-status output"
);
}
#[test]
fn test_diff_check_passthrough() {
let initial = "const x = 1;\n";
let modified = "const x = 2;\n";
let dir = setup_repo("main.ts", initial);
fs::write(dir.path().join("main.ts"), modified).unwrap();
let assert = run_skim_diff(&dir, &["--check"]);
assert.success();
}