use std::fs;
use std::path::Path;
use std::process::Output;
fn mkit_bin() -> &'static str {
env!("CARGO_BIN_EXE_mkit")
}
fn run_in(cwd: &Path, xdg: &Path, args: &[&str]) -> Output {
std::process::Command::new(mkit_bin())
.args(args)
.current_dir(cwd)
.env("XDG_CONFIG_HOME", xdg)
.output()
.expect("spawn mkit")
}
fn out_str(o: &Output) -> String {
String::from_utf8_lossy(&o.stdout).trim().to_string()
}
fn repo_with_four() -> (tempfile::TempDir, tempfile::TempDir) {
let td = tempfile::tempdir().unwrap();
let xdg = tempfile::tempdir().unwrap();
let (root, x) = (td.path(), xdg.path());
assert!(run_in(root, x, &["init"]).status.success());
assert!(run_in(root, x, &["keygen"]).status.success());
for (f, m) in [
("a.txt", "c1"),
("b.txt", "c2"),
("c.txt", "c3"),
("d.txt", "c4"),
] {
fs::write(root.join(f), b"x\n").unwrap();
assert!(run_in(root, x, &["add", f]).status.success());
assert!(run_in(root, x, &["commit", "-m", m]).status.success());
}
(td, xdg)
}
fn subjects(root: &Path, x: &Path, args: &[&str]) -> Vec<String> {
let mut full = vec!["log", "--oneline"];
full.extend_from_slice(args);
out_str(&run_in(root, x, &full))
.lines()
.map(|l| {
l.split_once(' ')
.map_or(String::new(), |(_, t)| t.to_string())
})
.collect()
}
#[test]
fn log_default_shows_all_newest_first() {
let (td, xdg) = repo_with_four();
assert_eq!(
subjects(td.path(), xdg.path(), &[]),
["c4", "c3", "c2", "c1"]
);
}
#[test]
fn log_single_rev_starts_there() {
let (td, xdg) = repo_with_four();
assert_eq!(
subjects(td.path(), xdg.path(), &["HEAD~1"]),
["c3", "c2", "c1"]
);
}
#[test]
fn log_range_excludes_left_side() {
let (td, xdg) = repo_with_four();
assert_eq!(
subjects(td.path(), xdg.path(), &["HEAD~3..HEAD"]),
["c4", "c3", "c2"]
);
assert_eq!(subjects(td.path(), xdg.path(), &["HEAD~2.."]), ["c4", "c3"]);
assert!(
subjects(td.path(), xdg.path(), &["..HEAD"])
.iter()
.all(String::is_empty)
);
}
#[test]
fn log_reverse_range_is_empty() {
let (td, xdg) = repo_with_four();
let out = run_in(td.path(), xdg.path(), &["log", "--oneline", "HEAD..HEAD~2"]);
assert!(out.status.success());
assert!(
out_str(&out).is_empty(),
"reverse range must be empty: {out:?}"
);
}
#[test]
fn log_range_with_limit() {
let (td, xdg) = repo_with_four();
assert_eq!(
subjects(td.path(), xdg.path(), &["-n", "2", "HEAD~3..HEAD"]),
["c4", "c3"]
);
}
fn branched_repo() -> (tempfile::TempDir, tempfile::TempDir) {
let td = tempfile::tempdir().unwrap();
let xdg = tempfile::tempdir().unwrap();
let (root, x) = (td.path(), xdg.path());
assert!(run_in(root, x, &["init"]).status.success());
assert!(run_in(root, x, &["keygen"]).status.success());
fs::write(root.join("a.txt"), b"base\n").unwrap();
assert!(run_in(root, x, &["add", "a.txt"]).status.success());
assert!(run_in(root, x, &["commit", "-m", "c1"]).status.success());
assert!(run_in(root, x, &["branch", "feat"]).status.success());
fs::write(root.join("m.txt"), b"m\n").unwrap();
assert!(run_in(root, x, &["add", "m.txt"]).status.success());
assert!(run_in(root, x, &["commit", "-m", "c2"]).status.success());
assert!(run_in(root, x, &["checkout", "feat"]).status.success());
fs::write(root.join("f.txt"), b"f\n").unwrap();
assert!(run_in(root, x, &["add", "f.txt"]).status.success());
assert!(run_in(root, x, &["commit", "-m", "c3"]).status.success());
(td, xdg)
}
#[test]
fn log_symmetric_range_shows_both_sides() {
let (td, xdg) = branched_repo();
let (root, x) = (td.path(), xdg.path());
let mut sym = subjects(root, x, &["main...HEAD"]);
sym.sort();
assert_eq!(sym, ["c2", "c3"], "symmetric range");
assert_eq!(
subjects(root, x, &["main..HEAD"]),
["c3"],
"asymmetric range"
);
assert!(
subjects(root, x, &["...HEAD"]).iter().all(String::is_empty),
"HEAD...HEAD is empty"
);
}
#[test]
fn diff_symmetric_range_is_merge_base_vs_b() {
let (td, xdg) = branched_repo();
let (root, x) = (td.path(), xdg.path());
let out = run_in(root, x, &["diff", "main...HEAD"]);
assert!(out.status.success(), "diff failed: {out:?}");
let s = out_str(&out);
assert!(
s.contains("diff --git a/f.txt b/f.txt"),
"f.txt added: {s:?}"
);
assert!(s.contains("new file mode"), "new file header: {s:?}");
assert!(!s.contains("m.txt"), "m.txt must not appear: {s:?}");
}
#[test]
fn diff_symmetric_range_peels_annotated_tag() {
let (td, xdg) = branched_repo();
let (root, x) = (td.path(), xdg.path());
assert!(
run_in(root, x, &["tag", "-a", "v1", "main", "-m", "tag main"])
.status
.success()
);
let out = run_in(root, x, &["diff", "v1...HEAD"]);
assert!(
out.status.success(),
"tagged symmetric diff failed: {out:?}"
);
let s = out_str(&out);
assert!(
s.contains("diff --git a/f.txt b/f.txt"),
"f.txt added: {s:?}"
);
}
#[test]
fn log_peels_annotated_tag_on_include_and_exclude() {
let td = tempfile::tempdir().unwrap();
let xdg = tempfile::tempdir().unwrap();
let (root, x) = (td.path(), xdg.path());
assert!(run_in(root, x, &["init"]).status.success());
assert!(run_in(root, x, &["keygen"]).status.success());
fs::write(root.join("a.txt"), b"1\n").unwrap();
assert!(run_in(root, x, &["add", "a.txt"]).status.success());
assert!(run_in(root, x, &["commit", "-m", "c1"]).status.success());
assert!(
run_in(root, x, &["tag", "-a", "v1", "-m", "tag c1"])
.status
.success()
);
for (f, m) in [("b.txt", "c2"), ("c.txt", "c3")] {
fs::write(root.join(f), b"x\n").unwrap();
assert!(run_in(root, x, &["add", f]).status.success());
assert!(run_in(root, x, &["commit", "-m", m]).status.success());
}
assert_eq!(subjects(root, x, &["v1"]), ["c1"]);
assert_eq!(subjects(root, x, &["v1..HEAD"]), ["c3", "c2"]);
}
#[test]
fn log_bad_revision_errors() {
let (td, xdg) = repo_with_four();
let out = run_in(td.path(), xdg.path(), &["log", "no-such-ref"]);
assert!(!out.status.success(), "bad rev must error: {out:?}");
}