#![expect(
clippy::tests_outside_test_module,
reason = "Cargo integration tests live at the file's module root"
)]
#![expect(
clippy::unwrap_used,
reason = "setup failures should abort the test loudly"
)]
use std::ffi::OsString;
use std::os::unix::net::UnixListener;
use std::path::Path;
use std::process::{Command, ExitCode};
use std::time::{Duration, SystemTime};
use tempfile::tempdir;
fn run(args: &[&str]) -> (ExitCode, String, String) {
let owned: Vec<OsString> = args.iter().map(OsString::from).collect();
let mut out = Vec::new();
let mut err = Vec::new();
let code = freshl::run(owned, &mut out, &mut err);
(
code,
String::from_utf8_lossy(&out).into_owned(),
String::from_utf8_lossy(&err).into_owned(),
)
}
fn code_repr(code: ExitCode) -> String {
format!("{code:?}")
}
fn success() -> String {
code_repr(ExitCode::SUCCESS)
}
fn git(dir: &Path, args: &[&str]) {
let mut cmd = Command::new("git");
cmd.arg("-C")
.arg(dir)
.args(args)
.env("GIT_AUTHOR_NAME", "t")
.env("GIT_AUTHOR_EMAIL", "t@example.invalid")
.env("GIT_COMMITTER_NAME", "t")
.env("GIT_COMMITTER_EMAIL", "t@example.invalid")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.env("HOME", dir);
for var in [
"GIT_DIR",
"GIT_INDEX_FILE",
"GIT_WORK_TREE",
"GIT_COMMON_DIR",
"GIT_PREFIX",
"GIT_OBJECT_DIRECTORY",
"GIT_NAMESPACE",
"GIT_CEILING_DIRECTORIES",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
] {
cmd.env_remove(var);
}
let status = cmd.status().unwrap();
assert!(status.success(), "git {args:?} failed");
}
fn init_repo(dir: &Path) {
git(dir, &["init", "-q", "-b", "main"]);
}
fn strip(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\u{1b}' {
for d in chars.by_ref() {
if d == 'm' {
break;
}
}
} else {
out.push(c);
}
}
out
}
fn row_for(out: &str, name: &str) -> String {
out.lines()
.map(strip)
.find(|l| l.contains(name))
.unwrap_or_default()
}
#[test]
fn fifo_and_socket_render_their_type_chars() {
let dir = tempdir().unwrap();
let fifo = dir.path().join("pipe");
let made = Command::new("mkfifo").arg(&fifo).status().unwrap();
assert!(made.success(), "mkfifo failed");
let _sock = UnixListener::bind(dir.path().join("sock")).unwrap();
let (code, out, _) = run(&[dir.path().to_str().unwrap()]);
assert_eq!(code_repr(code), success());
assert!(
row_for(&out, "pipe").starts_with('p'),
"FIFO row should start with the `p` type char: {out:?}"
);
assert!(
row_for(&out, "sock").starts_with('s'),
"socket row should start with the `s` type char: {out:?}"
);
}
#[test]
fn gitignore_negation_and_character_class() {
let dir = tempdir().unwrap();
init_repo(dir.path());
std::fs::write(
dir.path().join(".gitignore"),
"*.log\n!keep.log\n[abc].tmp\n",
)
.unwrap();
for name in ["a.log", "keep.log", "a.tmp", "d.tmp"] {
std::fs::write(dir.path().join(name), b"x").unwrap();
}
let (code, out, _) = run(&[dir.path().to_str().unwrap()]);
assert_eq!(code_repr(code), success());
assert!(row_for(&out, "a.log").contains('·'), "a.log ignored: {out}");
assert!(
row_for(&out, "keep.log").contains('?'),
"keep.log un-ignored by negation: {out}"
);
assert!(row_for(&out, "a.tmp").contains('·'), "a.tmp ignored: {out}");
assert!(
row_for(&out, "d.tmp").contains('?'),
"d.tmp not in [abc]: {out}"
);
}
#[test]
fn gitignore_globstar_matches_dir_at_any_depth() {
let dir = tempdir().unwrap();
init_repo(dir.path());
std::fs::write(dir.path().join(".gitignore"), "**/build/\n").unwrap();
std::fs::create_dir_all(dir.path().join("sub/build")).unwrap();
std::fs::write(dir.path().join("sub/build/artifact"), b"x").unwrap();
let (code, out, _) = run(&[dir.path().join("sub").to_str().unwrap()]);
assert_eq!(code_repr(code), success());
assert!(
row_for(&out, "build").contains('·'),
"**/build/ should ignore sub/build: {out}"
);
}
#[test]
fn submodule_subtree_lists_cleanly() {
let inner = tempdir().unwrap();
init_repo(inner.path());
std::fs::write(inner.path().join("f"), b"hi").unwrap();
git(inner.path(), &["add", "f"]);
git(inner.path(), &["commit", "-q", "-m", "inner"]);
let outer = tempdir().unwrap();
init_repo(outer.path());
git(
outer.path(),
&[
"-c",
"protocol.file.allow=always",
"submodule",
"add",
inner.path().to_str().unwrap(),
"sub",
],
);
git(outer.path(), &["commit", "-q", "-m", "add submodule"]);
let (code, out, err) = run(&["-R", outer.path().to_str().unwrap()]);
assert_eq!(code_repr(code), success(), "stderr: {err}");
assert!(out.contains("sub"), "submodule listed: {out}");
assert!(out.contains(".gitmodules"), ".gitmodules listed: {out}");
assert!(
row_for(&out, "/f").contains('○') || row_for(&out, " f").contains('○'),
"submodule content should read clean: {out}"
);
}
#[test]
fn far_future_mtime_renders_without_panicking() {
let dir = tempdir().unwrap();
let file = dir.path().join("future");
std::fs::write(&file, b"x").unwrap();
let when = SystemTime::UNIX_EPOCH + Duration::from_secs(4_000_000_000);
std::fs::File::options()
.write(true)
.open(&file)
.unwrap()
.set_modified(when)
.unwrap();
let (code, out, _) = run(&[dir.path().to_str().unwrap()]);
assert_eq!(code_repr(code), success());
assert!(out.contains("2096"), "far-future year rendered: {out}");
}
#[test]
fn very_long_name_round_trips() {
let dir = tempdir().unwrap();
let long = "z".repeat(200);
std::fs::write(dir.path().join(&long), b"x").unwrap();
let (code, out, _) = run(&[dir.path().to_str().unwrap()]);
assert_eq!(code_repr(code), success());
assert!(out.contains(&long), "long name preserved");
}
#[test]
fn secondary_worktree_reports_its_own_status() {
let main = tempdir().unwrap();
init_repo(main.path());
std::fs::write(main.path().join("tracked"), b"v1\n").unwrap();
git(main.path(), &["add", "."]);
git(main.path(), &["commit", "-q", "-m", "base"]);
let wt_parent = tempdir().unwrap();
let wt = wt_parent.path().join("wt");
git(
main.path(),
&[
"worktree",
"add",
"-q",
wt.to_str().unwrap(),
"-b",
"feature",
],
);
std::fs::write(wt.join("tracked"), b"v1\nedited\n").unwrap();
std::fs::write(wt.join("fresh"), b"new\n").unwrap();
let (code, out, err) = run(&[wt.to_str().unwrap()]);
assert_eq!(code_repr(code), success(), "stderr: {err}");
assert!(
row_for(&out, "tracked").contains('●'),
"worktree's modified tracked file should read `●`: {out}"
);
assert!(
row_for(&out, "fresh").contains('?'),
"worktree's new file should read `?`: {out}"
);
}
#[test]
fn sparse_checkout_skip_worktree_is_not_a_phantom_deletion() {
let dir = tempdir().unwrap();
init_repo(dir.path());
std::fs::create_dir_all(dir.path().join("keep")).unwrap();
std::fs::create_dir_all(dir.path().join("drop")).unwrap();
std::fs::write(dir.path().join("keep/a"), b"a\n").unwrap();
std::fs::write(dir.path().join("drop/b"), b"b\n").unwrap();
git(dir.path(), &["add", "."]);
git(dir.path(), &["commit", "-q", "-m", "base"]);
git(dir.path(), &["sparse-checkout", "init", "--cone"]);
git(dir.path(), &["sparse-checkout", "set", "keep"]);
assert!(
!dir.path().join("drop").exists(),
"sparse-checkout should have removed drop/ from disk"
);
let (code, out, err) = run(&["-d", dir.path().to_str().unwrap()]);
assert_eq!(code_repr(code), success(), "stderr: {err}");
let root = strip(&out);
assert!(
root.contains('○'),
"sparse root should read clean (`○`): {out}"
);
assert!(
!root.contains('⋯'),
"a SKIP_WORKTREE entry must not flag the tree as a dirty subtree: {out}"
);
}
#[test]
fn filemode_config_controls_exec_bit_change() {
use std::os::unix::fs::PermissionsExt;
for (filemode, expect_modified) in [("false", false), ("true", true)] {
let dir = tempdir().unwrap();
init_repo(dir.path());
git(dir.path(), &["config", "core.filemode", filemode]);
let exe = dir.path().join("script.sh");
std::fs::write(&exe, b"#!/bin/sh\n").unwrap();
std::fs::set_permissions(&exe, std::fs::Permissions::from_mode(0o755)).unwrap();
git(dir.path(), &["add", "."]);
git(dir.path(), &["commit", "-q", "-m", "base"]);
std::fs::set_permissions(&exe, std::fs::Permissions::from_mode(0o644)).unwrap();
let (code, out, _) = run(&[dir.path().to_str().unwrap()]);
assert_eq!(code_repr(code), success());
let row = row_for(&out, "script.sh");
if expect_modified {
assert!(
row.contains('●'),
"core.filemode=true should flag the exec-bit change as `●`: {out}"
);
} else {
assert!(
row.contains('○') && !row.contains('●'),
"core.filemode=false should keep the exec-bit change clean (`○`): {out}"
);
}
}
}
#[test]
fn ignorecase_config_controls_case_insensitive_exclude() {
for (ignorecase, want) in [("true", '·'), ("false", '?')] {
let dir = tempdir().unwrap();
init_repo(dir.path());
git(dir.path(), &["config", "core.ignorecase", ignorecase]);
std::fs::write(dir.path().join(".gitignore"), b"*.LOG\n").unwrap();
std::fs::write(dir.path().join("a.log"), b"x").unwrap();
let (code, out, _) = run(&[dir.path().to_str().unwrap()]);
assert_eq!(code_repr(code), success());
assert!(
row_for(&out, "a.log").contains(want),
"core.ignorecase={ignorecase}: a.log should read {want:?}: {out}"
);
}
}