use super::*;
use crate::test_support::{REAL_GIT, fixture, git, git_context, git_text, remove, repository};
use std::path::PathBuf;
use std::process::{Command, Output};
const UNSET_MODIFICATION_TIME: &str = "mtime: 0:0";
fn committed_repository(root: &Path) -> (PathBuf, CaptureIndexes, String) {
let repo: PathBuf = repository(root, "repo");
std::fs::write(repo.join("a.txt"), "a\n").expect("a");
std::fs::write(repo.join("b.txt"), "b\n").expect("b");
git(repo.as_path(), &["add", "a.txt", "b.txt"]);
git(repo.as_path(), &["commit", "--quiet", "--message=files"]);
let base: String = git_text(repo.as_path(), &["rev-parse", "HEAD"]);
let indexes: CaptureIndexes = CaptureIndexes {
captured_index: repo.join(".git").join("captured.index"),
commit_index: repo.join(".git").join("commit.index"),
object_directory: repo.join(".git").join("objects"),
};
return (repo, indexes, base);
}
fn debug_listing(repo: &Path, index: &Path, path: &str) -> String {
let output = Command::new(REAL_GIT)
.current_dir(repo)
.env("GIT_INDEX_FILE", index)
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/nonexistent-global-config")
.args(["ls-files", "--debug", "--", path])
.output()
.expect("ls-files");
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
return String::from_utf8(output.stdout).expect("UTF-8");
}
fn initialize_explicit(repo: &Path, indexes: &CaptureIndexes, base: &str, paths: &[OsString]) {
let choice: IndexSelection = IndexSelection {
mode: TransactionMode::ExplicitPath,
pathspecs: paths,
pathspec_file: None,
pathspec_file_nul: false,
stage_into_index: false,
stage_tracked_changes: false,
base_revision: base,
};
initialize_commit_index(&git_context(), repo, repo.join(".git").join("index").as_path(), indexes, &choice)
.expect("initialize");
}
#[test]
fn an_unchanged_entry_keeps_its_cached_stat_data() {
let root: PathBuf = fixture("commit-index-stat-kept");
let (repo, indexes, base) = committed_repository(root.as_path());
std::fs::write(repo.join("b.txt"), "changed\n").expect("change b");
initialize_explicit(repo.as_path(), &indexes, base.as_str(), &[OsString::from("b.txt")]);
let listing: String = debug_listing(repo.as_path(), indexes.commit_index.as_path(), "a.txt");
assert!(listing.contains("mtime:"), "the listing names the entry: {listing}");
assert!(!listing.contains(UNSET_MODIFICATION_TIME), "a.txt must keep its stat data: {listing}");
remove(root.as_path());
}
#[test]
fn a_plain_read_tree_leaves_no_stat_data() {
let root: PathBuf = fixture("commit-index-stat-positive-control");
let (repo, indexes, base) = committed_repository(root.as_path());
let plain: Output = Command::new(REAL_GIT)
.current_dir(repo.as_path())
.env("GIT_INDEX_FILE", indexes.commit_index.as_path())
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/nonexistent-global-config")
.args(["read-tree", base.as_str()])
.output()
.expect("read-tree");
assert!(plain.status.success());
let listing: String = debug_listing(repo.as_path(), indexes.commit_index.as_path(), "a.txt");
assert!(listing.contains(UNSET_MODIFICATION_TIME), "{listing}");
remove(root.as_path());
}
#[test]
fn a_named_path_is_staged_from_the_worktree_not_from_the_cached_entry() {
let root: PathBuf = fixture("commit-index-stat-named");
let (repo, indexes, base) = committed_repository(root.as_path());
std::fs::write(repo.join("a.txt"), "changed\n").expect("change a");
initialize_explicit(repo.as_path(), &indexes, base.as_str(), &[OsString::from("a.txt")]);
let tree: String = write_private_tree(
&git_context(),
repo.as_path(),
indexes.commit_index.as_path(),
indexes.object_directory.as_path(),
)
.expect("tree");
let content: String = git_text(repo.as_path(), &["show", format!("{tree}:a.txt").as_str()]);
assert_eq!(content, "changed");
remove(root.as_path());
}
fn private_content(repo: &Path, indexes: &CaptureIndexes, path: &str) -> String {
let tree: String = write_private_tree(
&git_context(),
repo,
indexes.commit_index.as_path(),
indexes.object_directory.as_path(),
)
.expect("tree");
return git_text(repo, &["show", format!("{tree}:{path}").as_str()]);
}
fn repository_with_directory(root: &Path) -> (PathBuf, CaptureIndexes, String) {
let (repo, indexes, _base) = committed_repository(root);
std::fs::create_dir(repo.join("d")).expect("directory");
std::fs::write(repo.join("d").join("x.txt"), "x\n").expect("x");
git(repo.as_path(), &["add", "d/x.txt"]);
git(repo.as_path(), &["commit", "--quiet", "--message=directory"]);
let base: String = git_text(repo.as_path(), &["rev-parse", "HEAD"]);
return (repo, indexes, base);
}
#[test]
fn a_named_assume_unchanged_file_is_committed() {
let root: PathBuf = fixture("commit-index-assume-named");
let (repo, indexes, base) = committed_repository(root.as_path());
git(repo.as_path(), &["update-index", "--assume-unchanged", "--", "a.txt"]);
std::fs::write(repo.join("a.txt"), "changed a\n").expect("change a");
std::fs::write(repo.join("b.txt"), "changed b\n").expect("change b");
initialize_explicit(repo.as_path(), &indexes, base.as_str(), &[OsString::from("a.txt"), OsString::from("b.txt")]);
assert_eq!(private_content(repo.as_path(), &indexes, "a.txt"), "changed a");
assert_eq!(private_content(repo.as_path(), &indexes, "b.txt"), "changed b");
remove(root.as_path());
}
#[test]
fn an_unnamed_assume_unchanged_file_stays_as_the_base_has_it() {
let root: PathBuf = fixture("commit-index-assume-unnamed");
let (repo, indexes, base) = committed_repository(root.as_path());
git(repo.as_path(), &["update-index", "--assume-unchanged", "--", "a.txt"]);
std::fs::write(repo.join("a.txt"), "changed a\n").expect("change a");
std::fs::write(repo.join("b.txt"), "changed b\n").expect("change b");
initialize_explicit(repo.as_path(), &indexes, base.as_str(), &[OsString::from("b.txt")]);
assert_eq!(private_content(repo.as_path(), &indexes, "a.txt"), "a");
assert_eq!(private_content(repo.as_path(), &indexes, "b.txt"), "changed b");
remove(root.as_path());
}
#[test]
fn a_named_directory_takes_its_assume_unchanged_files() {
let root: PathBuf = fixture("commit-index-assume-directory");
let (repo, indexes, base) = repository_with_directory(root.as_path());
git(repo.as_path(), &["update-index", "--assume-unchanged", "--", "d/x.txt"]);
std::fs::write(repo.join("d").join("x.txt"), "changed x\n").expect("change x");
initialize_explicit(repo.as_path(), &indexes, base.as_str(), &[OsString::from("d")]);
assert_eq!(private_content(repo.as_path(), &indexes, "d/x.txt"), "changed x");
remove(root.as_path());
}
#[test]
fn a_named_skip_worktree_file_does_not_fail_the_commit() {
let root: PathBuf = fixture("commit-index-skip-named");
let (repo, indexes, base) = committed_repository(root.as_path());
git(repo.as_path(), &["update-index", "--skip-worktree", "--", "a.txt"]);
std::fs::write(repo.join("a.txt"), "changed a\n").expect("change a");
std::fs::write(repo.join("b.txt"), "changed b\n").expect("change b");
initialize_explicit(repo.as_path(), &indexes, base.as_str(), &[OsString::from("a.txt"), OsString::from("b.txt")]);
assert_eq!(private_content(repo.as_path(), &indexes, "a.txt"), "changed a");
assert_eq!(private_content(repo.as_path(), &indexes, "b.txt"), "changed b");
remove(root.as_path());
}
#[test]
fn an_intent_to_add_tracked_empty_file_is_not_dropped_from_the_commit() {
let root: PathBuf = fixture("commit-index-intent-empty");
let (repo, indexes, _base) = committed_repository(root.as_path());
std::fs::write(repo.join("empty"), "").expect("empty file");
git(repo.as_path(), &["add", "empty"]);
git(repo.as_path(), &["commit", "--quiet", "--message=empty"]);
let base: String = git_text(repo.as_path(), &["rev-parse", "HEAD"]);
git(repo.as_path(), &["rm", "--cached", "--quiet", "empty"]);
git(repo.as_path(), &["add", "--intent-to-add", "empty"]);
std::fs::write(repo.join("a.txt"), "changed a\n").expect("change a");
initialize_explicit(repo.as_path(), &indexes, base.as_str(), &[OsString::from("a.txt")]);
let tree: String = write_private_tree(
&git_context(),
repo.as_path(),
indexes.commit_index.as_path(),
indexes.object_directory.as_path(),
)
.expect("tree");
let names: String = git_text(repo.as_path(), &["ls-tree", "--name-only", tree.as_str()]);
assert!(names.lines().any(|name| { return name == "empty"; }), "the tree must still hold the empty file: {names}");
assert_eq!(private_content(repo.as_path(), &indexes, "a.txt"), "changed a");
remove(root.as_path());
}
#[test]
fn only_a_plain_index_seeds_the_private_index() {
let root: PathBuf = fixture("commit-index-seeds");
let (repo, indexes, _base) = committed_repository(root.as_path());
let real: PathBuf = repo.join(".git").join("index");
assert!(seeds_private_index(real.as_path()));
git(repo.as_path(), &["update-index", "--assume-unchanged", "--", "a.txt"]);
assert!(!seeds_private_index(real.as_path()));
assert!(!seeds_private_index(indexes.commit_index.as_path()));
remove(root.as_path());
}