mod support;
use predicates::prelude::*;
use std::fs;
use std::process::Command as StdCommand;
use support::{lx, lx_no_colour};
use tempfile::tempdir;
fn git_fixture() -> tempfile::TempDir {
let dir = tempdir().expect("failed to create tempdir");
let path = dir.path();
StdCommand::new("git")
.args(["init", "-b", "main"])
.current_dir(path)
.output()
.expect("git init failed");
StdCommand::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(path)
.output()
.expect("git config failed");
StdCommand::new("git")
.args(["config", "user.name", "Test"])
.current_dir(path)
.output()
.expect("git config failed");
fs::write(path.join("tracked.txt"), "hello").unwrap();
StdCommand::new("git")
.args(["add", "tracked.txt"])
.current_dir(path)
.output()
.expect("git add failed");
StdCommand::new("git")
.args(["commit", "-m", "initial"])
.current_dir(path)
.output()
.expect("git commit failed");
fs::write(path.join("untracked.txt"), "new").unwrap();
fs::write(path.join("tracked.txt"), "modified").unwrap();
dir
}
fn jj_available() -> bool {
StdCommand::new("jj")
.arg("version")
.output()
.is_ok_and(|o| o.status.success())
}
fn jj_feature_enabled() -> bool {
let output = lx_no_colour()
.args(["--vcs=jj", "/nonexistent"])
.output()
.expect("failed to run lx");
let stderr = String::from_utf8_lossy(&output.stderr);
!stderr.contains("disabled")
}
#[test]
fn vcs_invalid_value() {
lx().arg("--vcs=svn")
.assert()
.failure()
.stderr(predicate::str::contains("invalid value"));
}
#[test]
fn vcs_none_disables_status() {
let dir = git_fixture();
lx_no_colour()
.args(["--vcs=none", "--vcs-status", "-l"])
.arg(dir.path())
.assert()
.success()
.stdout(predicate::str::contains(" M ").not())
.stdout(predicate::str::contains(" N ").not());
}
#[test]
fn git_vcs_status_shows_column() {
let dir = git_fixture();
lx_no_colour()
.args(["--vcs=git", "--vcs-status", "-l"])
.arg(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("N"))
.stdout(predicate::str::contains("untracked.txt"));
}
#[test]
fn git_modified_file_shows_m() {
let dir = git_fixture();
lx_no_colour()
.args(["--vcs=git", "--vcs-status", "-l"])
.arg(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("M"))
.stdout(predicate::str::contains("tracked.txt"));
}
#[test]
fn git_vcs_status_header() {
let dir = git_fixture();
lx_no_colour()
.args(["--vcs=git", "--vcs-status", "-lh"])
.arg(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("Git"));
}
#[test]
fn git_vcs_ignore_hides_ignored_files() {
let dir = git_fixture();
let path = dir.path();
fs::write(path.join(".gitignore"), "*.log\n").unwrap();
StdCommand::new("git")
.args(["add", ".gitignore"])
.current_dir(path)
.output()
.expect("git add failed");
fs::write(path.join("debug.log"), "log output").unwrap();
lx_no_colour()
.args(["--vcs=git", "--vcs-ignore", "-1"])
.arg(path)
.assert()
.success()
.stdout(predicate::str::contains("debug.log").not())
.stdout(predicate::str::contains("tracked.txt"));
}
#[test]
fn vcs_status_without_long_is_fine() {
lx_no_colour()
.args(["--vcs-status", "."])
.assert()
.success();
}
#[test]
fn jj_vcs_status_in_this_repo() {
if !jj_feature_enabled() || !jj_available() {
eprintln!("skipping: jj feature disabled or jj not available");
return;
}
lx_no_colour()
.args(["--vcs=jj", "--vcs-status", "-l", "Cargo.toml"])
.assert()
.success()
.stdout(predicate::str::contains("Cargo.toml"));
}
#[test]
fn jj_single_column_display() {
if !jj_feature_enabled() || !jj_available() {
eprintln!("skipping: jj feature disabled or jj not available");
return;
}
lx_no_colour()
.args(["--vcs=jj", "--vcs-status", "-l", "."])
.assert()
.success()
.stdout(predicate::str::contains("MM").not())
.stdout(predicate::str::contains("NN").not());
}
#[test]
fn jj_auto_detection() {
if !jj_feature_enabled() || !jj_available() {
eprintln!("skipping: jj feature disabled or jj not available");
return;
}
lx_no_colour()
.args(["--vcs=auto", "--vcs-status", "-l", "."])
.assert()
.success()
.stdout(predicate::str::contains("MM").not());
}
#[test]
fn auto_falls_back_to_git() {
let dir = git_fixture();
lx_no_colour()
.args(["--vcs=auto", "--vcs-status", "-l"])
.arg(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("N"))
.stdout(predicate::str::contains("untracked.txt"));
}
fn jj_run(cwd: &std::path::Path, args: &[&str]) {
let output = StdCommand::new("jj")
.args(args)
.current_dir(cwd)
.output()
.expect("jj command failed to spawn");
assert!(
output.status.success(),
"jj {:?} failed: {}",
args,
String::from_utf8_lossy(&output.stderr)
);
}
fn jj_non_colocated_fixture() -> tempfile::TempDir {
let dir = tempdir().expect("failed to create tempdir");
let path = dir.path();
jj_run(path, &["git", "init", "--no-colocate"]);
fs::write(path.join("tracked.txt"), "hello").unwrap();
fs::write(path.join(".gitignore"), "*.log\n").unwrap();
fs::write(path.join("ignored.log"), "noise").unwrap();
jj_run(path, &["describe", "-m", "initial"]);
jj_run(path, &["new"]);
fs::write(path.join("untracked.txt"), "new").unwrap();
jj_run(path, &["status"]);
dir
}
fn jj_external_git_fixture() -> tempfile::TempDir {
let dir = tempdir().expect("failed to create tempdir");
let path = dir.path();
StdCommand::new("git")
.args(["init", "-b", "main"])
.current_dir(path)
.output()
.expect("git init failed");
StdCommand::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(path)
.output()
.expect("git config failed");
StdCommand::new("git")
.args(["config", "user.name", "Test"])
.current_dir(path)
.output()
.expect("git config failed");
fs::write(path.join("tracked.txt"), "hello").unwrap();
fs::write(path.join(".gitignore"), "*.log\n").unwrap();
StdCommand::new("git")
.args(["add", "tracked.txt", ".gitignore"])
.current_dir(path)
.output()
.expect("git add failed");
StdCommand::new("git")
.args(["commit", "-m", "initial"])
.current_dir(path)
.output()
.expect("git commit failed");
jj_run(path, &["git", "init", "--git-repo", "."]);
fs::write(path.join("ignored.log"), "noise").unwrap();
fs::write(path.join("untracked.txt"), "new").unwrap();
jj_run(path, &["status"]);
dir
}
#[test]
fn jj_non_colocated_auto_picks_jj() {
if !jj_feature_enabled() || !jj_available() {
eprintln!("skipping: jj feature disabled or jj not available");
return;
}
let dir = jj_non_colocated_fixture();
lx_no_colour()
.args(["--vcs=auto", "--vcs-status", "-l"])
.arg(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("tracked.txt"))
.stdout(predicate::str::contains("MM").not())
.stdout(predicate::str::contains("NN").not());
}
#[test]
fn jj_non_colocated_status_column_works() {
if !jj_feature_enabled() || !jj_available() {
eprintln!("skipping: jj feature disabled or jj not available");
return;
}
let dir = jj_non_colocated_fixture();
lx_no_colour()
.args(["--vcs=jj", "--vcs-status", "-l"])
.arg(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("untracked.txt"))
.stdout(predicate::str::contains("A"));
}
#[test]
fn jj_non_colocated_vcs_ignore_hides_ignored_files() {
if !jj_feature_enabled() || !jj_available() {
eprintln!("skipping: jj feature disabled or jj not available");
return;
}
let dir = jj_non_colocated_fixture();
lx_no_colour()
.args(["--vcs=jj", "--vcs-ignore"])
.arg(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("tracked.txt"))
.stdout(predicate::str::contains("untracked.txt"))
.stdout(predicate::str::contains("ignored.log").not());
}
#[test]
fn jj_external_git_repo_status_works() {
if !jj_feature_enabled() || !jj_available() {
eprintln!("skipping: jj feature disabled or jj not available");
return;
}
let dir = jj_external_git_fixture();
lx_no_colour()
.args(["--vcs=jj", "--vcs-status", "-l"])
.arg(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("tracked.txt"))
.stdout(predicate::str::contains("untracked.txt"));
}
#[test]
fn jj_external_git_repo_vcs_ignore_works() {
if !jj_feature_enabled() || !jj_available() {
eprintln!("skipping: jj feature disabled or jj not available");
return;
}
let dir = jj_external_git_fixture();
lx_no_colour()
.args(["--vcs=jj", "--vcs-ignore"])
.arg(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("tracked.txt"))
.stdout(predicate::str::contains("ignored.log").not());
}