use super::*;
use crate::candidate_prediction::CandidateRequest;
use crate::command_test_support::os_arguments;
use crate::config_schema::PolicyConfig;
use crate::policy_checks::shipped_checks;
use crate::policy_content::LifecycleContent;
use crate::policy_engine::StageRequest;
use crate::repository_facts::git_facts;
use crate::test_support::{REAL_GIT, fixture, git, git_context, remove, repository};
use crate::transaction_git::{GitRequest, run_git};
use crate::wrapper_controls::no_controls;
use std::path::{Path, PathBuf};
const INDEX_VARIABLE: &str = "GIT_INDEX_FILE";
fn overlay(repo: &Path, index: &Path) -> Vec<(OsString, OsString)> {
return vec![
(OsString::from("GIT_DIR"), repo.join(".git").into_os_string()),
(OsString::from("GIT_WORK_TREE"), repo.as_os_str().to_os_string()),
(OsString::from(INDEX_VARIABLE), index.as_os_str().to_os_string()),
];
}
fn scene(name: &str, staged: &[u8]) -> (PathBuf, PathBuf, PathBuf) {
let root: PathBuf = fixture(name);
let repo: PathBuf = repository(root.as_path(), "repo");
std::fs::write(repo.join("a.txt"), b"base\n").expect("a");
git(repo.as_path(), &["add", "--", "a.txt"]);
git(repo.as_path(), &["commit", "--quiet", "--message=base"]);
let index: PathBuf = root.join("private-index");
let mut read: GitRequest = GitRequest::new(repo.as_path(), &["read-tree", "HEAD"]);
read.without_prefix = true;
read.index = Some(index.clone());
let output = run_git(&git_context(), &read).expect("read-tree starts");
assert!(output.succeeded(), "{}", output.error_text());
let stage: PrivateStage<'_> = PrivateStage {
context: &git_context(),
cwd: repo.as_path(),
index: index.as_path(),
object_directory: None,
};
let entry: Correction = Correction {
path: b"a.txt".to_vec(),
mode: crate::candidate_object::CandidateMode::Regular,
before: std::rc::Rc::from(b"base\n".as_slice()),
after: std::rc::Rc::from(staged),
};
assert_eq!(stage_correction(&stage, &entry), Ok(()), "the staged text is written");
return (root, repo, index);
}
fn private_text(repo: &Path, index: &Path) -> String {
let mut show: GitRequest = GitRequest::new(repo, &["show", ":a.txt"]);
show.without_prefix = true;
show.index = Some(index.to_path_buf());
let output = run_git(&git_context(), &show).expect("show starts");
assert!(output.succeeded(), "{}", output.error_text());
return String::from_utf8_lossy(output.stdout.as_slice()).into_owned();
}
fn converge_index(repo: &Path, index: &Path, object_directory: Option<&Path>) -> CommitConvergence {
let facts = git_facts(Path::new(REAL_GIT), &[], overlay(repo, index).as_slice());
let mut checks: ShippedChecks<_> = shipped_checks(
facts,
os_arguments(&[]),
LifecycleContent::Requested(CandidateRequest::Staged(vec![OsString::from("a.txt")])),
Vec::<PathBuf>::new(),
);
let request: StageRequest = StageRequest {
trigger: Trigger::PreForward,
config: PolicyConfig::defaults(),
controls: no_controls(),
selected: Vec::new(),
};
return converge_commit_policies(CommitConvergenceInput {
request: &request,
checks: &mut checks,
stage: PrivateStage {
context: &git_context(),
cwd: repo,
index,
object_directory,
},
});
}
#[test]
fn the_final_newline_correction_lands_in_the_private_index() {
let (root, repo, index): (PathBuf, PathBuf, PathBuf) = scene("commit-convergence-newline", b"changed");
let outcome: CommitConvergence = converge_index(repo.as_path(), index.as_path(), None);
assert_eq!(outcome.convergence, Convergence::Settled { changed_passes: 1 }, "{:?}", outcome.pass.events);
assert_eq!(outcome.changed_paths, vec![b"a.txt".to_vec()]);
assert_eq!(private_text(repo.as_path(), index.as_path()), "changed\n");
remove(root.as_path());
}
#[test]
fn a_settled_private_index_is_left_unchanged() {
let (root, repo, index): (PathBuf, PathBuf, PathBuf) = scene("commit-convergence-settled", b"already\n");
let outcome: CommitConvergence = converge_index(repo.as_path(), index.as_path(), None);
assert_eq!(outcome.convergence, Convergence::Settled { changed_passes: 0 }, "{:?}", outcome.pass.events);
assert!(outcome.changed_paths.is_empty());
assert_eq!(private_text(repo.as_path(), index.as_path()), "already\n");
remove(root.as_path());
}
fn names_conflict(events: &[PolicyEvent], path_name: &str) -> bool {
for event in events {
if let PolicyEvent::EngineFailure { code: EngineFailureCode::PatchConflict, path: Some(path), .. } = event && path.text() == path_name {
return true;
}
}
return false;
}
#[test]
fn a_correction_the_index_cannot_take_is_a_patch_conflict() {
let (root, repo, index): (PathBuf, PathBuf, PathBuf) = scene("commit-convergence-conflict", b"changed");
let blocked: PathBuf = root.join("not-a-directory");
std::fs::write(&blocked, b"a file").expect("blocker");
let outcome: CommitConvergence = converge_index(repo.as_path(), index.as_path(), Some(blocked.as_path()));
assert_eq!(outcome.convergence, Convergence::Blocked);
let coded: bool = names_conflict(outcome.pass.events.as_slice(), "a.txt");
assert!(coded, "{:?}", outcome.pass.events);
remove(root.as_path());
}
#[test]
fn stored_states_compare_equal_exactly_when_the_corrected_bytes_match() {
let (root, repo, index): (PathBuf, PathBuf, PathBuf) = scene("commit-convergence-states", b"changed");
let facts = git_facts(Path::new(REAL_GIT), &[], overlay(repo.as_path(), index.as_path()).as_slice());
let mut checks: ShippedChecks<_> = shipped_checks(
facts,
os_arguments(&[]),
LifecycleContent::Requested(CandidateRequest::Staged(vec![OsString::from("a.txt")])),
Vec::<PathBuf>::new(),
);
let request: StageRequest = StageRequest {
trigger: Trigger::PreForward,
config: PolicyConfig::defaults(),
controls: no_controls(),
selected: Vec::new(),
};
checks.can_apply_patches = true;
let context = git_context();
let mut passes: CommitPasses<'_, _> = CommitPasses::new(CommitConvergenceInput {
request: &request,
checks: &mut checks,
stage: PrivateStage {
context: &context,
cwd: repo.as_path(),
index: index.as_path(),
object_directory: None,
},
});
assert_eq!(passes.run_pass(0), ConvergencePass::Proposed);
assert!(passes.apply(1), "the final-newline correction is written");
let reversal: Correction = Correction {
path: b"a.txt".to_vec(),
mode: crate::candidate_object::CandidateMode::Regular,
before: std::rc::Rc::from(b"changed\n".as_slice()),
after: std::rc::Rc::from(b"changed".as_slice()),
};
passes.checks.content.propose(reversal);
assert!(passes.apply(2), "the reversal is written");
assert!(!passes.snapshots_equal(0, 1), "the corrected state differs from the uncorrected one");
assert!(!passes.snapshots_equal(1, 2), "the reversed state differs from the corrected one");
assert!(passes.snapshots_equal(0, 2), "the reversed state repeats the uncorrected one");
assert!(passes.snapshots_equal(1, 1), "a state equals itself");
remove(root.as_path());
}
#[test]
fn a_correction_of_a_name_that_is_not_utf8_is_written_by_its_bytes() {
let (root, repo, index): (PathBuf, PathBuf, PathBuf) = scene("commit-convergence-odd-name", b"changed\n");
let stage: PrivateStage<'_> = PrivateStage {
context: &git_context(),
cwd: repo.as_path(),
index: index.as_path(),
object_directory: None,
};
let entry: Correction = Correction {
path: b"caf\xe9.txt".to_vec(),
mode: crate::candidate_object::CandidateMode::Regular,
before: std::rc::Rc::from(b"".as_slice()),
after: std::rc::Rc::from(b"odd\n".as_slice()),
};
assert_eq!(stage_correction(&stage, &entry), Ok(()));
let mut listing: GitRequest = GitRequest::new(repo.as_path(), &["ls-files", "-z"]);
listing.without_prefix = true;
listing.index = Some(index.clone());
let output = run_git(&git_context(), &listing).expect("ls-files starts");
let names: Vec<&[u8]> = output.stdout.split(|byte| { return *byte == 0; }).filter(|name| { return !name.is_empty(); }).collect();
assert_eq!(names, vec![b"a.txt".as_slice(), b"caf\xe9.txt".as_slice()]);
remove(root.as_path());
}
#[test]
fn a_patch_conflict_over_a_name_that_is_not_utf8_carries_the_exact_bytes() {
let pass: PassResult = patch_conflict_pass(&[], b"caf\xe9.txt", String::from("cause"));
let [PolicyEvent::EngineFailure { message, path: Some(path), .. }] = pass.events.as_slice() else {
panic!("expected one engine failure with a path: {:?}", pass.events);
};
assert_eq!(message, "Policy patch for caf\u{fffd}.txt does not apply to the private index: cause");
assert_eq!((path.text(), path.exact()), ("caf\u{fffd}.txt", Some(b"caf\xe9.txt".as_slice())));
}