use crate::test_support::{REAL_GIT, age, fixture, git, git_text, remove, spawn_owned};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
pub(crate) enum Edit<'a> {
Put { path: &'a str, mode: &'a str, content: &'a [u8] },
Gitlink { path: &'a str, id: &'a str },
Delete(&'a str),
}
pub(crate) struct Scene {
pub root: PathBuf,
pub repo: PathBuf,
pub old: String,
pub new: String,
}
pub(crate) fn git_with(dir: &Path, env: &[(&str, OsString)], args: &[&str], input: Option<&[u8]>) -> Output {
let mut command: Command = Command::new(REAL_GIT);
command
.current_dir(dir)
.env_clear()
.env("PATH", "/usr/bin:/bin")
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/nonexistent-global-config")
.env("GIT_AUTHOR_NAME", "Fixture")
.env("GIT_AUTHOR_EMAIL", "fixture@example.invalid")
.env("GIT_COMMITTER_NAME", "Fixture")
.env("GIT_COMMITTER_EMAIL", "fixture@example.invalid")
.args(args)
.stdin(if input.is_some() { Stdio::piped() } else { Stdio::null() })
.stdout(Stdio::piped())
.stderr(Stdio::piped());
for (name, value) in env {
command.env(name, value);
}
let mut child = spawn_owned(&mut command).expect("start Git");
if let Some(bytes) = input {
use std::io::Write as _;
child.stdin.take().expect("piped").write_all(bytes).expect("write input");
}
return child.wait_with_output().expect("Git output");
}
fn git_in_index(dir: &Path, index: &Path, args: &[&str], input: Option<&[u8]>) -> String {
let output: Output = git_with(dir, &[("GIT_INDEX_FILE", index.as_os_str().to_os_string())], args, input);
assert!(output.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&output.stderr));
return String::from_utf8(output.stdout).expect("UTF-8").trim_end().to_string();
}
#[derive(Default)]
pub(crate) struct Plan<'a> {
pub files: &'a [(&'a str, &'a [u8], bool)],
pub links: &'a [(&'a str, &'a str)],
pub gitlinks: &'a [(&'a str, &'a str)],
pub edits: &'a [Edit<'a>],
pub fresh: bool,
pub sha256: bool,
}
pub(crate) fn scene(name: &str, plan: &Plan<'_>) -> Scene {
let root: PathBuf = fixture(name);
let repo: PathBuf = root.join("repo");
std::fs::create_dir(&repo).expect("repository directory");
let format: &str = if plan.sha256 { "--object-format=sha256" } else { "--object-format=sha1" };
git(repo.as_path(), &["init", "--quiet", "--initial-branch=main", format]);
for (path, content, executable) in plan.files {
let file: PathBuf = repo.join(path);
std::fs::create_dir_all(file.parent().expect("parent")).expect("directories");
std::fs::write(&file, content).expect("write");
if *executable {
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o755)).expect("chmod");
}
if !plan.fresh {
age(&file);
}
}
for (path, target) in plan.links {
std::os::unix::fs::symlink(target, repo.join(path)).expect("symlink");
if !plan.fresh {
age(repo.join(path).as_path());
}
}
git(repo.as_path(), &["add", "--all"]);
for (path, id) in plan.gitlinks {
git(repo.as_path(), &["update-index", "--add", "--cacheinfo", format!("160000,{id},{path}").as_str()]);
}
git(repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=first"]);
let old: String = git_text(repo.as_path(), &["rev-parse", "HEAD"]);
let temporary: PathBuf = repo.join(".git").join("scene-index");
git_in_index(repo.as_path(), temporary.as_path(), &["read-tree", "HEAD"], None);
for edit in plan.edits {
match edit {
Edit::Put { path, mode, content } => {
let blob: String = git_in_index(repo.as_path(), temporary.as_path(), &["hash-object", "-w", "--stdin"], Some(content));
git_in_index(repo.as_path(), temporary.as_path(), &["update-index", "--add", "--cacheinfo", format!("{mode},{blob},{path}").as_str()], None);
}
Edit::Gitlink { path, id } => {
git_in_index(repo.as_path(), temporary.as_path(), &["update-index", "--add", "--cacheinfo", format!("160000,{id},{path}").as_str()], None);
}
Edit::Delete(path) => {
git_in_index(repo.as_path(), temporary.as_path(), &["update-index", "--force-remove", path], None);
}
}
}
let tree: String = git_in_index(repo.as_path(), temporary.as_path(), &["write-tree"], None);
let new: String = git_text(repo.as_path(), &["commit-tree", tree.as_str(), "-p", old.as_str(), "-m", "second"]);
git(repo.as_path(), &["update-ref", "HEAD", new.as_str()]);
std::fs::remove_file(&temporary).expect("scene index removed");
return Scene { root, repo, old, new };
}
pub(crate) fn copy_of(scene: &Scene) -> PathBuf {
let copy: PathBuf = scene.root.join("copy");
let output: Output = Command::new("cp").args(["--archive", "--no-target-directory"]).arg(&scene.repo).arg(©).output().expect("cp");
assert!(output.status.success(), "cp failed: {}", String::from_utf8_lossy(&output.stderr));
return copy;
}
pub(crate) fn git_reset(repo: &Path, commit: &str, names: &[Vec<u8>]) {
let mut input: Vec<u8> = Vec::new();
for name in names {
input.extend_from_slice(b":(top,literal)");
input.extend_from_slice(name.as_slice());
input.push(0);
}
let output: Output = git_with(repo, &[], &["reset", "--no-refresh", commit, "--pathspec-from-file=-", "--pathspec-file-nul"], Some(input.as_slice()));
assert!(output.status.success(), "git reset: {}", String::from_utf8_lossy(&output.stderr));
}
pub(crate) fn git_reset_status(repo: &Path, commit: &str, names: &[Vec<u8>]) -> bool {
let mut input: Vec<u8> = Vec::new();
for name in names {
input.extend_from_slice(b":(top,literal)");
input.extend_from_slice(name.as_slice());
input.push(0);
}
return git_with(repo, &[], &["reset", "--no-refresh", commit, "--pathspec-from-file=-", "--pathspec-file-nul"], Some(input.as_slice())).status.success();
}
pub(crate) fn index_bytes(repo: &Path) -> Vec<u8> {
return std::fs::read(repo.join(".git").join("index")).expect("index");
}
pub(crate) fn listing(repo: &Path) -> String {
return String::from_utf8_lossy(&git_with(repo, &[], &["ls-files", "--stage", "--debug"], None).stdout).into_owned();
}
pub(crate) fn named(paths: &[&str]) -> Vec<Vec<u8>> {
return paths.iter().map(|path| { return path.as_bytes().to_vec(); }).collect();
}
pub(crate) fn finish(scene: Scene) {
remove(scene.root.as_path());
}
static NEXT_BATCH: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
pub(crate) fn landing_reset(context: &crate::transaction_git::GitContext, repo: &Path, new: &str, paths: crate::service_land_follow::IndexPaths<'_>) -> bool {
return landing_reset_copy(context, repo, new, paths, false);
}
pub(crate) fn landing_reset_copy(context: &crate::transaction_git::GitContext, repo: &Path, new: &str, paths: crate::service_land_follow::IndexPaths<'_>, old_copy: bool) -> bool {
let live: PathBuf = repo.join(".git").join("index");
let seq: u64 = NEXT_BATCH.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let Some(held) = crate::service_index_lock::try_acquire(live.as_path(), seq) else {
return false;
};
let copy = crate::service_land_follow::PrivateCopy::make(&held, seq).expect("the private copy");
let built: PathBuf = copy.path().to_path_buf();
if old_copy && built.exists() {
age(built.as_path());
}
let done: bool = crate::index_reset_file::reset_index_file(context, repo, new, paths, &held, ©);
if done {
held.release_with(built.as_path()).expect("installed");
} else {
let _removed = std::fs::remove_file(&built);
drop(held);
}
return done;
}
pub(crate) fn commit_with_raw_entry(scene: &Scene, name: &str, mode: &str, content: &[u8]) -> String {
let blob: String = String::from_utf8(git_with(scene.repo.as_path(), &[], &["hash-object", "-w", "--stdin"], Some(content)).stdout).expect("UTF-8").trim().to_string();
let mut tree: Vec<u8> = format!("{mode} {name}\0").into_bytes();
for pair in blob.as_bytes().as_chunks::<2>().0 {
tree.push(u8::from_str_radix(std::str::from_utf8(pair).expect("hex"), 16).expect("hex digit pair"));
}
let made: Output = git_with(scene.repo.as_path(), &[], &["hash-object", "-t", "tree", "-w", "--literally", "--stdin"], Some(tree.as_slice()));
assert!(made.status.success(), "{}", String::from_utf8_lossy(&made.stderr));
let tree_name: String = String::from_utf8(made.stdout).expect("UTF-8").trim().to_string();
return git_text(scene.repo.as_path(), &["commit-tree", tree_name.as_str(), "-p", scene.new.as_str(), "-m", "raw entry"]);
}