use super::support::{
Fixture, Observed, bounded_for, fixture, git, observe, porcelain, remove, repository, run_direct,
stderr_of, wrapper_first_path,
};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
const COMMIT_BOUND_SECONDS: &str = "120";
fn remove_settled(fixture: &Fixture) {
for _ in 0..50 {
if std::fs::remove_dir_all(&fixture.root).is_ok() || !fixture.root.exists() {
return;
}
std::thread::sleep(Duration::from_millis(100));
}
remove(fixture);
}
fn wrapped_commit(fixture: &Fixture, directory: &Path, arguments: &[&str]) -> Command {
let mut command: Command = bounded_for(
fixture,
fixture.root.join("bin/git").as_path(),
wrapper_first_path(fixture).as_os_str(),
COMMIT_BOUND_SECONDS,
);
command.current_dir(directory).args(arguments);
return command;
}
fn run_commit(fixture: &Fixture, directory: &Path, arguments: &[&str]) -> Observed {
return observe(&mut wrapped_commit(fixture, directory, arguments), b"");
}
fn git_text(fixture: &Fixture, repo: &Path, arguments: &[&str]) -> String {
let output = git(fixture, repo, arguments);
return String::from_utf8_lossy(output.stdout.as_slice()).into_owned();
}
fn edited_repository(fixture: &Fixture, name: &str) -> PathBuf {
let repo: PathBuf = repository(fixture, OsStr::new(name));
std::fs::write(repo.join("a.txt"), b"one\n").expect("a");
git(fixture, repo.as_path(), &["add", "--", "a.txt"]);
git(fixture, repo.as_path(), &["commit", "--quiet", "--message=base"]);
std::fs::write(repo.join("a.txt"), b"one\ntwo\n").expect("edit a");
keep_maintenance_in_the_foreground(fixture, repo.as_path());
return repo;
}
fn keep_maintenance_in_the_foreground(fixture: &Fixture, repo: &Path) {
git(fixture, repo, &["config", "maintenance.autoDetach", "false"]);
}
fn pack_entries(pack_directory: &Path) -> Vec<String> {
let mut names: Vec<String> = std::fs::read_dir(pack_directory)
.map(|entries| {
return entries
.flatten()
.map(|entry| { return entry.file_name().to_string_lossy().into_owned(); })
.collect();
})
.unwrap_or_default();
names.sort();
return names;
}
fn assert_nothing_left(fixture: &Fixture, repo: &Path) {
let git_dir: PathBuf = repo.join(".git");
let registry: Vec<String> = std::fs::read_dir(git_dir.join("cli-git-transactions"))
.map(|entries| {
return entries
.flatten()
.map(|entry| { return entry.file_name().to_string_lossy().into_owned(); })
.collect();
})
.unwrap_or_default();
assert_eq!(registry, Vec::<String>::new(), "no transaction or lock stays in the registry");
assert!(!git_dir.join("index.lock").exists(), "the real index lock is released");
let shadows: Vec<String> = std::fs::read_dir(git_dir.join("cli-git/shadow"))
.map(|entries| {
return entries
.flatten()
.map(|entry| { return entry.file_name().to_string_lossy().into_owned(); })
.collect();
})
.unwrap_or_default();
assert_eq!(shadows, Vec::<String>::new(), "no shadow repository stays");
let pack_directory: PathBuf = git_dir.join("objects/pack");
let keeps: String = git_text(fixture, repo, &["count-objects", "-v"]);
assert!(keeps.contains("garbage: 0"), "{keeps}pack directory: {:?}", pack_entries(pack_directory.as_path()));
let kept: Vec<String> = std::fs::read_dir(pack_directory)
.map(|entries| {
return entries
.flatten()
.map(|entry| { return entry.file_name().to_string_lossy().into_owned(); })
.filter(|name| { return name.ends_with(".keep"); })
.collect();
})
.unwrap_or_default();
assert_eq!(kept, Vec::<String>::new(), "no pack keeps the commit's objects alive");
}
const BULK_FILE_COUNT: usize = 32;
const BULK_FILE_BYTES: usize = 256 * 1024;
fn incompressible(seed: u64) -> Vec<u8> {
let mut state: u64 = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1;
let mut bytes: Vec<u8> = Vec::with_capacity(BULK_FILE_BYTES);
while bytes.len() < BULK_FILE_BYTES {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
bytes.extend_from_slice(&state.to_le_bytes());
}
return bytes;
}
#[test]
fn a_landing_returns_after_its_automatic_maintenance_finished() {
let fixture: Fixture = fixture("commit-foreground-maintenance");
let repo: PathBuf = edited_repository(&fixture, "repo");
git(&fixture, repo.as_path(), &["config", "gc.autoPackLimit", "1"]);
git(&fixture, repo.as_path(), &["config", "pack.compression", "9"]);
std::fs::create_dir(repo.join("filler")).expect("filler directory");
for number in 0..BULK_FILE_COUNT {
std::fs::write(repo.join(format!("filler/{number}.bin")), incompressible(number as u64)).expect("filler file");
}
git(&fixture, repo.as_path(), &["add", "--", "filler"]);
git(&fixture, repo.as_path(), &["commit", "--quiet", "--message=filler"]);
let first: Observed = run_commit(&fixture, repo.as_path(), &["commit", "--message=second", "a.txt"]);
assert_eq!(first.code, Some(0), "{}", stderr_of(&first));
std::fs::write(repo.join("a.txt"), b"one\ntwo\nthree\n").expect("edit a again");
let second: Observed = run_commit(&fixture, repo.as_path(), &["commit", "--message=third", "a.txt"]);
assert_eq!(second.code, Some(0), "{}", stderr_of(&second));
let counts: String = git_text(&fixture, repo.as_path(), &["count-objects", "-v"]);
assert!(counts.contains("count: 0\n"), "the loose objects were packed: {counts}");
assert!(counts.contains("garbage: 0\n"), "{counts}");
assert!(!repo.join(".git/objects/maintenance.lock").exists(), "no maintenance still holds its lock");
assert_nothing_left(&fixture, repo.as_path());
remove_settled(&fixture);
}
#[test]
fn an_explicit_path_commit_lands_once_and_leaves_nothing() {
let fixture: Fixture = fixture("commit-explicit");
let repo: PathBuf = edited_repository(&fixture, "repo");
let landed: Observed = run_commit(&fixture, repo.as_path(), &["commit", "--message=second", "a.txt"]);
assert_eq!(landed.code, Some(0), "{}", stderr_of(&landed));
assert_eq!(git_text(&fixture, repo.as_path(), &["rev-list", "--count", "HEAD"]).trim(), "3");
assert_eq!(git_text(&fixture, repo.as_path(), &["show", "HEAD:a.txt"]), "one\ntwo\n");
assert_eq!(git_text(&fixture, repo.as_path(), &["log", "-1", "--format=%s"]).trim(), "second");
assert_eq!(porcelain(&fixture, repo.as_path()), "");
assert_nothing_left(&fixture, repo.as_path());
remove_settled(&fixture);
}
#[test]
fn a_final_newline_correction_lands_in_the_commit_and_the_worktree() {
let fixture: Fixture = fixture("commit-newline");
let repo: PathBuf = edited_repository(&fixture, "repo");
std::fs::write(repo.join("a.txt"), b"one\ntwo").expect("edit without the final newline");
let landed: Observed = run_commit(&fixture, repo.as_path(), &["commit", "--message=second", "a.txt"]);
assert_eq!(landed.code, Some(0), "{}", stderr_of(&landed));
assert_eq!(git_text(&fixture, repo.as_path(), &["show", "HEAD:a.txt"]), "one\ntwo\n");
assert_eq!(std::fs::read(repo.join("a.txt")).expect("worktree copy"), b"one\ntwo\n");
assert!(
stderr_of(&landed).contains("\"type\":\"fix-summary\",\"trigger\":\"pre-forward\",\"passes\":1,\"changedPaths\":[\"a.txt\"]"),
"{}",
stderr_of(&landed)
);
assert_eq!(porcelain(&fixture, repo.as_path()), "");
assert_nothing_left(&fixture, repo.as_path());
remove_settled(&fixture);
}
#[test]
fn a_dry_run_commits_nothing() {
let fixture: Fixture = fixture("commit-dry-run");
let repo: PathBuf = edited_repository(&fixture, "repo");
let head: String = git_text(&fixture, repo.as_path(), &["rev-parse", "HEAD"]);
let dry: Observed = run_commit(&fixture, repo.as_path(), &["commit", "--dry-run", "--message=x", "a.txt"]);
assert_eq!(dry.code, Some(0), "{}", stderr_of(&dry));
assert_eq!(git_text(&fixture, repo.as_path(), &["rev-parse", "HEAD"]), head);
assert!(!repo.join(".git/cli-git").exists(), "a dry run creates no shadow area");
remove_settled(&fixture);
}
#[test]
fn a_commit_of_all_tracked_changes_lands_them() {
let fixture: Fixture = fixture("commit-all");
let repo: PathBuf = edited_repository(&fixture, "repo");
std::fs::write(repo.join("b.txt"), b"b\n").expect("b");
git(&fixture, repo.as_path(), &["add", "--", "b.txt"]);
git(&fixture, repo.as_path(), &["commit", "--quiet", "--message=more"]);
std::fs::remove_file(repo.join("b.txt")).expect("remove b");
let landed: Observed = run_commit(&fixture, repo.as_path(), &["commit", "-a", "--no-only", "--message=all"]);
assert_eq!(landed.code, Some(0), "{}", stderr_of(&landed));
assert_eq!(git_text(&fixture, repo.as_path(), &["show", "HEAD:a.txt"]), "one\ntwo\n");
assert_eq!(
git_text(&fixture, repo.as_path(), &["ls-tree", "--name-only", "HEAD"]).trim(),
"a.txt"
);
assert_eq!(porcelain(&fixture, repo.as_path()), "");
assert_nothing_left(&fixture, repo.as_path());
remove_settled(&fixture);
}
#[test]
fn a_blocking_finding_stops_the_commit_with_status_one() {
let fixture: Fixture = fixture("commit-blocked");
let repo: PathBuf = edited_repository(&fixture, "repo");
git(&fixture, repo.as_path(), &["add", "--", "a.txt"]);
let head: String = git_text(&fixture, repo.as_path(), &["rev-parse", "HEAD"]);
let blocked: Observed = run_commit(&fixture, repo.as_path(), &["commit", "--message=x"]);
assert_eq!(blocked.code, Some(1), "{}", stderr_of(&blocked));
assert!(stderr_of(&blocked).contains("commit-only/pathspec-required"), "{}", stderr_of(&blocked));
assert_eq!(git_text(&fixture, repo.as_path(), &["rev-parse", "HEAD"]), head);
assert_eq!(porcelain(&fixture, repo.as_path()), "M a.txt\n");
remove_settled(&fixture);
}
#[test]
fn a_correction_that_restores_the_base_is_a_no_change_normalization() {
let fixture: Fixture = fixture("commit-no-change");
let repo: PathBuf = edited_repository(&fixture, "repo");
std::fs::write(repo.join("a.txt"), b"one").expect("remove the final newline");
let head: String = git_text(&fixture, repo.as_path(), &["rev-parse", "HEAD"]);
let result: Observed = run_commit(&fixture, repo.as_path(), &["commit", "--message=x", "a.txt"]);
assert_eq!(result.code, Some(1), "{}", stderr_of(&result));
assert!(
stderr_of(&result).contains("\"code\":\"commit-normalization/no-change\""),
"{}",
stderr_of(&result)
);
assert_eq!(git_text(&fixture, repo.as_path(), &["rev-parse", "HEAD"]), head);
assert_eq!(std::fs::read(repo.join("a.txt")).expect("worktree copy"), b"one\n");
assert_eq!(porcelain(&fixture, repo.as_path()), "");
assert_nothing_left(&fixture, repo.as_path());
remove_settled(&fixture);
}
#[test]
fn an_amend_replaces_the_head_and_an_empty_commit_needs_its_flag() {
let fixture: Fixture = fixture("commit-amend");
let repo: PathBuf = edited_repository(&fixture, "repo");
let amended: Observed = run_commit(
&fixture,
repo.as_path(),
&["commit", "--amend", "--message=amended", "a.txt"],
);
assert_eq!(amended.code, Some(0), "{}", stderr_of(&amended));
assert_eq!(git_text(&fixture, repo.as_path(), &["rev-list", "--count", "HEAD"]).trim(), "2");
assert_eq!(git_text(&fixture, repo.as_path(), &["log", "-1", "--format=%s"]).trim(), "amended");
let empty: Observed = run_commit(
&fixture,
repo.as_path(),
&["commit", "--allow-empty", "--no-only", "--message=empty"],
);
assert_eq!(empty.code, Some(0), "{}", stderr_of(&empty));
assert_eq!(git_text(&fixture, repo.as_path(), &["rev-list", "--count", "HEAD"]).trim(), "3");
assert_nothing_left(&fixture, repo.as_path());
remove_settled(&fixture);
}
#[test]
fn an_unknown_option_is_refused_by_git_and_nothing_lands() {
let fixture: Fixture = fixture("commit-unknown-option");
let repo: PathBuf = edited_repository(&fixture, "repo");
let head: String = git_text(&fixture, repo.as_path(), &["rev-parse", "HEAD"]);
let refused: Observed = run_commit(&fixture, repo.as_path(), &["commit", "--no-such-option", "a.txt"]);
let direct: Observed = run_direct(&fixture, repo.as_path(), &["commit", "--no-such-option", "a.txt"]);
assert_eq!(refused.code, direct.code, "{}", stderr_of(&refused));
assert_eq!(git_text(&fixture, repo.as_path(), &["rev-parse", "HEAD"]), head);
remove_settled(&fixture);
}
#[test]
fn a_failing_pre_commit_hook_stops_the_commit_with_its_status() {
let fixture: Fixture = fixture("commit-hook-fails");
let repo: PathBuf = edited_repository(&fixture, "repo");
super::support::executable(
repo.join(".git/hooks/pre-commit").as_path(),
b"#!/bin/sh\necho rejected by the hook >&2\nexit 1\n",
);
let head: String = git_text(&fixture, repo.as_path(), &["rev-parse", "HEAD"]);
let rejected: Observed = run_commit(&fixture, repo.as_path(), &["commit", "--message=x", "a.txt"]);
assert_eq!(rejected.code, Some(1), "{}", stderr_of(&rejected));
assert!(stderr_of(&rejected).contains("rejected by the hook"), "{}", stderr_of(&rejected));
assert_eq!(git_text(&fixture, repo.as_path(), &["rev-parse", "HEAD"]), head);
assert_nothing_left(&fixture, repo.as_path());
remove_settled(&fixture);
}
#[test]
fn a_pre_commit_hook_restaging_a_file_is_kept() {
let fixture: Fixture = fixture("commit-hook-restages");
let repo: PathBuf = edited_repository(&fixture, "repo");
super::support::executable(
repo.join(".git/hooks/pre-commit").as_path(),
b"#!/bin/sh\nprintf 'hooked\\n' >> a.txt\ngit add -- a.txt\n",
);
let landed: Observed = run_commit(&fixture, repo.as_path(), &["commit", "--message=second", "a.txt"]);
assert_eq!(landed.code, Some(0), "{}", stderr_of(&landed));
assert_eq!(
git_text(&fixture, repo.as_path(), &["show", "HEAD:a.txt"]),
"one\ntwo\nhooked\n"
);
assert_nothing_left(&fixture, repo.as_path());
remove_settled(&fixture);
}
fn await_marker(path: &Path) {
let started: Instant = Instant::now();
while !path.exists() {
assert!(started.elapsed() < Duration::from_secs(90), "the marker {} never appeared", path.display());
std::thread::sleep(Duration::from_millis(20));
}
}
fn replayed_commit_stderr(name: &str, configuration: Option<&str>) -> String {
let fixture: Fixture = fixture(name);
let repo: PathBuf = edited_repository(&fixture, "repo");
if let Some(text) = configuration {
std::fs::write(repo.join("cli-git.config.jsonc"), text).expect("repository configuration");
}
std::fs::write(repo.join("b.txt"), b"b\n").expect("b");
git(&fixture, repo.as_path(), &["add", "--", "b.txt"]);
let markers: PathBuf = fixture.root.join("markers");
std::fs::create_dir(&markers).expect("marker directory");
let slow: Child = wrapped_commit(&fixture, repo.as_path(), &["commit", "--message=slow", "a.txt"])
.env("CLI_GIT_TEST_ONLY_PHASE_SIGNAL", format!("preparation-done:pause:{}", markers.display()))
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.expect("the slow commit starts");
await_marker(markers.join("preparation-done.reached").as_path());
let fast: Observed = run_commit(&fixture, repo.as_path(), &["commit", "--message=fast", "b.txt"]);
assert_eq!(fast.code, Some(0), "{}", stderr_of(&fast));
std::fs::write(markers.join("preparation-done.release"), b"").expect("release the slow commit");
let finished = slow.wait_with_output().expect("the slow commit ends");
let stderr: String = String::from_utf8_lossy(finished.stderr.as_slice()).into_owned();
assert_eq!(finished.status.code(), Some(0), "{stderr}");
assert_eq!(git_text(&fixture, repo.as_path(), &["rev-list", "--count", "HEAD"]).trim(), "4");
assert_eq!(git_text(&fixture, repo.as_path(), &["show", "HEAD:a.txt"]), "one\ntwo\n");
assert_eq!(git_text(&fixture, repo.as_path(), &["show", "HEAD:b.txt"]), "b\n");
remove_settled(&fixture);
return stderr;
}
#[test]
fn a_commit_that_loses_the_landing_race_replays_and_lands() {
let stderr: String = replayed_commit_stderr("commit-replay", None);
assert!(stderr.contains("\"type\":\"landing-race-lost\",\"attempt\":1"), "{stderr}");
assert!(stderr.contains("\"type\":\"commit-replayed\""), "{stderr}");
assert!(stderr.contains("\"type\":\"landing-reserved\",\"lostRaces\":1"), "{stderr}");
}
#[test]
fn fewer_lost_races_than_configured_take_no_reservation() {
let stderr: String = replayed_commit_stderr("commit-no-reserve", Some("{ \"landing\": { \"reserveAfterLostRaces\": 2 } }\n"));
assert!(stderr.contains("\"type\":\"commit-replayed\""), "{stderr}");
assert!(!stderr.contains("\"type\":\"landing-reserved\""), "{stderr}");
}
#[test]
fn a_commit_that_overlaps_the_winner_ends_in_a_replay_conflict() {
let fixture: Fixture = fixture("commit-conflict");
let repo: PathBuf = edited_repository(&fixture, "repo");
let markers: PathBuf = fixture.root.join("markers");
std::fs::create_dir(&markers).expect("marker directory");
let slow: Child = wrapped_commit(&fixture, repo.as_path(), &["commit", "--message=slow", "a.txt"])
.env("CLI_GIT_TEST_ONLY_PHASE_SIGNAL", format!("preparation-done:pause:{}", markers.display()))
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.expect("the slow commit starts");
await_marker(markers.join("preparation-done.reached").as_path());
std::fs::write(repo.join("a.txt"), b"other\n").expect("winner edit");
git(&fixture, repo.as_path(), &["add", "--", "a.txt"]);
git(&fixture, repo.as_path(), &["commit", "--quiet", "--message=winner"]);
let winning: String = git_text(&fixture, repo.as_path(), &["rev-parse", "HEAD"]);
std::fs::write(markers.join("preparation-done.release"), b"").expect("release the slow commit");
let finished = slow.wait_with_output().expect("the slow commit ends");
let stderr: String = String::from_utf8_lossy(finished.stderr.as_slice()).into_owned();
assert_eq!(finished.status.code(), Some(1), "{stderr}");
assert!(stderr.contains("\"code\":\"concurrent-commit/replay-conflict\""), "{stderr}");
assert_eq!(git_text(&fixture, repo.as_path(), &["rev-parse", "HEAD"]).trim(), winning.trim());
remove_settled(&fixture);
}
#[test]
fn a_termination_signal_during_a_hook_ends_the_commit_and_leaves_nothing() {
let fixture: Fixture = fixture("commit-signal");
let repo: PathBuf = edited_repository(&fixture, "repo");
let started: PathBuf = fixture.root.join("hook-started");
super::support::executable(
repo.join(".git/hooks/pre-commit").as_path(),
format!("#!/bin/sh\ntouch '{}'\nsleep 60\n", started.display()).as_bytes(),
);
let head: String = git_text(&fixture, repo.as_path(), &["rev-parse", "HEAD"]);
let child: Child = wrapped_commit(&fixture, repo.as_path(), &["commit", "--message=x", "a.txt"])
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.expect("the commit starts");
await_marker(started.as_path());
let signalled = Command::new("kill")
.args(["-s", "TERM"])
.arg(child.id().to_string())
.status()
.expect("kill starts");
assert!(signalled.success());
let finished = child.wait_with_output().expect("the commit ends");
assert_eq!(finished.status.code(), Some(143), "{}", String::from_utf8_lossy(finished.stderr.as_slice()));
assert_eq!(git_text(&fixture, repo.as_path(), &["rev-parse", "HEAD"]), head);
assert_nothing_left(&fixture, repo.as_path());
remove_settled(&fixture);
}
#[test]
fn a_patch_selection_lands_the_chosen_hunk() {
let fixture: Fixture = fixture("commit-patch");
let repo: PathBuf = edited_repository(&fixture, "repo");
let landed: Observed = observe(
&mut wrapped_commit(&fixture, repo.as_path(), &["commit", "--patch", "--message=chosen"]),
b"y\n",
);
assert_eq!(landed.code, Some(0), "{}", stderr_of(&landed));
assert_eq!(git_text(&fixture, repo.as_path(), &["show", "HEAD:a.txt"]), "one\ntwo\n");
assert_eq!(git_text(&fixture, repo.as_path(), &["log", "-1", "--format=%s"]).trim(), "chosen");
assert_nothing_left(&fixture, repo.as_path());
remove_settled(&fixture);
}
#[test]
fn an_interactive_selection_lands_the_chosen_file() {
let fixture: Fixture = fixture("commit-interactive");
let repo: PathBuf = edited_repository(&fixture, "repo");
let landed: Observed = observe(
&mut wrapped_commit(&fixture, repo.as_path(), &["commit", "--interactive", "--message=chosen"]),
b"u\n1\n\nq\n",
);
assert_eq!(landed.code, Some(0), "{}{}", stderr_of(&landed), String::from_utf8_lossy(landed.stdout.as_slice()));
assert_eq!(git_text(&fixture, repo.as_path(), &["show", "HEAD:a.txt"]), "one\ntwo\n");
assert_nothing_left(&fixture, repo.as_path());
remove_settled(&fixture);
}
#[test]
fn an_allowed_empty_commit_is_not_a_no_change_normalization() {
let fixture: Fixture = fixture("commit-allow-empty-normalized");
let repo: PathBuf = edited_repository(&fixture, "repo");
std::fs::write(repo.join("a.txt"), b"one").expect("remove the final newline");
let before: String = git_text(&fixture, repo.as_path(), &["rev-parse", "HEAD^{tree}"]);
let landed: Observed = run_commit(
&fixture,
repo.as_path(),
&["commit", "--allow-empty", "--message=empty on purpose", "a.txt"],
);
assert_eq!(landed.code, Some(0), "{}", stderr_of(&landed));
assert_eq!(git_text(&fixture, repo.as_path(), &["rev-list", "--count", "HEAD"]).trim(), "3");
assert_eq!(git_text(&fixture, repo.as_path(), &["rev-parse", "HEAD^{tree}"]), before);
assert_nothing_left(&fixture, repo.as_path());
remove_settled(&fixture);
}
#[test]
fn include_commits_the_named_path_together_with_the_staged_changes() {
let fixture: Fixture = fixture("commit-include");
let repo: PathBuf = edited_repository(&fixture, "repo");
std::fs::write(repo.join("b.txt"), b"b\n").expect("b");
git(&fixture, repo.as_path(), &["add", "--", "b.txt"]);
let landed: Observed = run_commit(&fixture, repo.as_path(), &["commit", "--include", "--message=both", "a.txt"]);
assert_eq!(landed.code, Some(0), "{}", stderr_of(&landed));
assert_eq!(git_text(&fixture, repo.as_path(), &["show", "HEAD:a.txt"]), "one\ntwo\n");
assert_eq!(git_text(&fixture, repo.as_path(), &["show", "HEAD:b.txt"]), "b\n");
assert_nothing_left(&fixture, repo.as_path());
remove_settled(&fixture);
}
#[test]
fn a_pathspec_file_selects_the_committed_paths() {
let fixture: Fixture = fixture("commit-pathspec-file");
let repo: PathBuf = edited_repository(&fixture, "repo");
std::fs::write(repo.join("b.txt"), b"b\n").expect("b");
std::fs::write(repo.join("list.txt"), b"a.txt\n").expect("list");
let landed: Observed = run_commit(&fixture, repo.as_path(), &["commit", "--message=listed", "--pathspec-from-file=list.txt"]);
assert_eq!(landed.code, Some(0), "{}", stderr_of(&landed));
assert_eq!(git_text(&fixture, repo.as_path(), &["show", "HEAD:a.txt"]), "one\ntwo\n");
assert!(!git_text(&fixture, repo.as_path(), &["ls-tree", "--name-only", "HEAD"]).contains("b.txt"));
std::fs::write(repo.join("a.txt"), b"one\ntwo\nthree\n").expect("edit a again");
let from_input: Observed = observe(
&mut wrapped_commit(&fixture, repo.as_path(), &["commit", "--message=piped", "--pathspec-from-file=-"]),
b"a.txt\n",
);
assert_eq!(from_input.code, Some(0), "{}", stderr_of(&from_input));
assert_eq!(git_text(&fixture, repo.as_path(), &["show", "HEAD:a.txt"]), "one\ntwo\nthree\n");
assert_eq!(git_text(&fixture, repo.as_path(), &["log", "-1", "--format=%s"]).trim(), "piped");
assert_nothing_left(&fixture, repo.as_path());
remove_settled(&fixture);
}