use super::support::{Fixture, REAL_GIT, TRANSACTION_ID, direct, git, repository};
use git_policy_cli::capture_invocation::{InvocationCapture, capture_invocation_base, capture_invocation_layout};
use git_policy_cli::landing_attempt::{
LandingError, LandingOutcome, LandingPayload, LandingRequest, LandingWorkspace, land_transaction,
};
use git_policy_cli::owner_lock::OwnerLock;
use git_policy_cli::recovery_scan::acquire_registry_lock;
use git_policy_cli::shadow_git::shadow_repository_path;
use git_policy_cli::transaction_git::GitContext;
use git_policy_cli::transaction_journal::{Base, PREPARING_FILENAME, PreparingRecord, TransactionMode};
use git_policy_cli::transaction_journal_encode::encode_preparing;
use git_policy_cli::transaction_owner::{current_transaction_owner, encode_transaction_owner};
use git_policy_cli::transaction_registry::{LANDING_LOCK_NAME, TRANSACTION_ROOT_NAME, ensure_transaction_root, publish_transaction_directory};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
pub const WORLD_VARIABLE: &str = "CLI_GIT_LANDING_WORLD";
#[derive(Clone, Debug)]
pub struct World {
pub root: PathBuf,
pub repo: PathBuf,
pub git_dir: PathBuf,
pub registry: PathBuf,
pub directory: PathBuf,
pub shadow: PathBuf,
pub stage: PathBuf,
pub base: String,
pub prepared: String,
pub prepared_tree: String,
pub base_tree: String,
pub root_commit: String,
}
pub struct Opened {
pub capture: InvocationCapture,
pub workspace: LandingWorkspace,
}
pub fn git_with(fixture: &Fixture, directory: &Path, environment: &[(&str, &Path)], arguments: &[&str]) -> String {
let mut command = direct(fixture);
command.current_dir(directory).args(arguments);
for (name, value) in environment {
command.env(name, value);
}
let output = command.output().expect("Git starts");
assert!(output.status.success(), "git {arguments:?} failed: {}", String::from_utf8_lossy(&output.stderr));
return String::from_utf8_lossy(&output.stdout).trim_end().to_string();
}
pub fn git_text(fixture: &Fixture, directory: &Path, arguments: &[&str]) -> String {
return String::from_utf8_lossy(&git(fixture, directory, arguments).stdout).trim_end().to_string();
}
pub fn text(path: &Path) -> String {
return path.to_str().expect("UTF-8 fixture path").to_string();
}
pub fn context() -> GitContext {
let mut overlay: Vec<(OsString, OsString)> = Vec::new();
for (name, value) in [
("GIT_CONFIG_NOSYSTEM", "1"),
("GIT_CONFIG_GLOBAL", "/nonexistent-global-config"),
("GIT_AUTHOR_NAME", "Fixture"),
("GIT_AUTHOR_EMAIL", "fixture@example.invalid"),
("GIT_COMMITTER_NAME", "Fixture"),
("GIT_COMMITTER_EMAIL", "fixture@example.invalid"),
] {
overlay.push((OsString::from(name), OsString::from(value)));
}
return GitContext { real_git: PathBuf::from(REAL_GIT), overlay, global_prefix: Vec::new() };
}
impl World {
pub fn build(fixture: &Fixture) -> World {
let repo: PathBuf = repository(fixture, std::ffi::OsStr::new("repo"));
std::fs::write(repo.join("a.txt"), b"one\n").expect("a.txt");
git(fixture, repo.as_path(), &["add", "--", "a.txt"]);
git(fixture, repo.as_path(), &["commit", "--quiet", "--message=base"]);
let git_dir: PathBuf = repo.join(".git");
let base: String = git_text(fixture, repo.as_path(), &["rev-parse", "HEAD"]);
let base_tree: String = git_text(fixture, repo.as_path(), &["rev-parse", "HEAD^{tree}"]);
let shadow: PathBuf = shadow_repository_path(git_dir.as_path(), TRANSACTION_ID);
std::fs::create_dir_all(shadow.parent().expect("shadow parent")).expect("shadow parent");
git(fixture, fixture.root.as_path(), &["init", "--quiet", "--bare", text(shadow.as_path()).as_str()]);
std::fs::create_dir_all(shadow.join("objects/info")).expect("shadow info");
std::fs::write(shadow.join("objects/info/alternates"), format!("{}\n", git_dir.join("objects").display()))
.expect("alternates");
let stage: PathBuf = fixture.root.join("stage");
std::fs::create_dir(&stage).expect("stage");
std::fs::copy(git_dir.join("index"), stage.join("captured.index")).expect("captured index");
std::fs::copy(git_dir.join("index"), stage.join("commit.index")).expect("commit index");
std::fs::write(stage.join("a-two.txt"), b"two\n").expect("blob source");
let shadow_dir: String = format!("--git-dir={}", shadow.display());
let blob: String = git_text(
fixture,
repo.as_path(),
&[shadow_dir.as_str(), "hash-object", "-w", text(stage.join("a-two.txt").as_path()).as_str()],
);
let commit_index: PathBuf = stage.join("commit.index");
let cacheinfo: String = format!("100644,{blob},a.txt");
git_with(
fixture,
repo.as_path(),
&[("GIT_INDEX_FILE", commit_index.as_path())],
&[shadow_dir.as_str(), "update-index", "--cacheinfo", cacheinfo.as_str()],
);
let prepared_tree: String = git_with(
fixture,
repo.as_path(),
&[("GIT_INDEX_FILE", commit_index.as_path())],
&[shadow_dir.as_str(), "write-tree"],
);
let prepared: String = git_text(
fixture,
repo.as_path(),
&[shadow_dir.as_str(), "commit-tree", prepared_tree.as_str(), "-p", base.as_str(), "-m", "prepared"],
);
let root_commit: String = git_text(
fixture,
repo.as_path(),
&[shadow_dir.as_str(), "commit-tree", prepared_tree.as_str(), "-m", "prepared root"],
);
std::fs::write(repo.join("a.txt"), b"two\n").expect("worktree a.txt");
let world: World = World::attach_with(fixture.root.as_path(), base, prepared, prepared_tree, base_tree, root_commit);
std::fs::write(
world.stage.join("oids"),
format!(
"{}\n{}\n{}\n{}\n{}\n",
world.base, world.prepared, world.prepared_tree, world.base_tree, world.root_commit
),
)
.expect("recorded object IDs");
return world;
}
fn attach_with(root: &Path, base: String, prepared: String, prepared_tree: String, base_tree: String, root_commit: String) -> World {
let repo: PathBuf = root.join("repo");
let git_dir: PathBuf = repo.join(".git");
let registry: PathBuf = git_dir.join(TRANSACTION_ROOT_NAME);
let world: World = World {
root: root.to_path_buf(),
directory: registry.join(TRANSACTION_ID),
shadow: shadow_repository_path(git_dir.as_path(), TRANSACTION_ID),
stage: root.join("stage"),
repo,
git_dir,
registry,
base,
prepared,
prepared_tree,
base_tree,
root_commit,
};
return world;
}
pub fn attach(root: &Path) -> World {
let recorded: String = std::fs::read_to_string(root.join("stage/oids")).expect("recorded object IDs");
let ids: Vec<&str> = recorded.lines().collect();
return World::attach_with(
root,
ids[0].to_string(),
ids[1].to_string(),
ids[2].to_string(),
ids[3].to_string(),
ids[4].to_string(),
);
}
pub fn open(&self) -> Opened {
let git: GitContext = context();
ensure_transaction_root(self.registry.as_path()).expect("registry");
let owner = current_transaction_owner(TRANSACTION_ID, "2026-10-11T00:00:00.000Z").expect("owner");
publish_transaction_directory(
self.registry.as_path(),
TRANSACTION_ID,
encode_transaction_owner(&owner).as_bytes(),
)
.expect("published");
std::fs::copy(self.stage.join("captured.index"), self.directory.join("captured.index")).expect("captured index");
std::fs::copy(self.stage.join("commit.index"), self.directory.join("commit.index")).expect("commit index");
let layout = capture_invocation_layout(&git, self.repo.as_path(), false).expect("layout");
let capture: InvocationCapture = capture_invocation_base(&git, self.repo.as_path(), layout).expect("base");
let record: PreparingRecord = PreparingRecord {
transaction_id: TRANSACTION_ID.to_string(),
mode: TransactionMode::Index,
base: capture.base.clone(),
symbolic_head: capture.layout.symbolic_head.clone(),
target_ref: capture.layout.target_ref.clone(),
conclusion: capture.layout.conclusion,
repository_root: text(capture.layout.repository_root.as_path()),
git_dir: text(capture.layout.git_dir.as_path()),
common_dir: text(capture.layout.common_dir.as_path()),
real_index_path: text(capture.layout.real_index_path.as_path()),
object_directory: text(capture.layout.object_directory.as_path()),
ref_format: capture.layout.ref_format,
empty_tree_oid: capture.layout.empty_tree_oid.clone(),
shadow_path: text(self.shadow.as_path()),
selected_pathspecs: Vec::new(),
invoked_at: capture.layout.invoked_at.clone(),
};
std::fs::write(self.directory.join(PREPARING_FILENAME), encode_preparing(&record)).expect("preparing");
let workspace: LandingWorkspace = LandingWorkspace {
transaction_id: TRANSACTION_ID.to_string(),
directory: self.directory.clone(),
shadow: self.shadow.clone(),
captured_index: self.directory.join("captured.index"),
commit_index: self.directory.join("commit.index"),
preserved: false,
};
return Opened { capture, workspace };
}
pub fn landing_lock(&self) -> OwnerLock {
return acquire_registry_lock(self.registry.as_path(), LANDING_LOCK_NAME).expect("landing lock");
}
pub fn commit_payload(&self) -> LandingPayload {
return LandingPayload::Commit {
new_oid: self.prepared.clone(),
landed_tree_oid: self.prepared_tree.clone(),
expected_old: Base::Commit(self.base.clone()),
landed_index: self.directory.join("commit.index"),
exact_private_index: true,
guarded_paths: Vec::new(),
};
}
pub fn index(&self) -> PathBuf {
return self.git_dir.join("index");
}
pub fn index_lock(&self) -> PathBuf {
return self.git_dir.join("index.lock");
}
pub fn pid_file(&self) -> PathBuf {
return self.git_dir.join("index~pid.lock");
}
pub fn keeps(&self) -> Vec<String> {
let pack: PathBuf = self.git_dir.join("objects/pack");
let mut names: Vec<String> = Vec::new();
if let Ok(entries) = std::fs::read_dir(pack) {
for entry in entries.flatten() {
let name: String = entry.file_name().to_string_lossy().into_owned();
if name.ends_with(".keep") {
names.push(name);
}
}
}
return names;
}
}
pub fn land(world: &World, opened: &mut Opened, payload: &LandingPayload) -> Result<LandingOutcome, LandingError> {
let lock: OwnerLock = world.landing_lock();
let request: LandingRequest<'_> = LandingRequest {
capture: &opened.capture,
mode: TransactionMode::Index,
payload,
committed_paths: &[],
added_paths: &[],
selected_worktree_paths: &[],
index_lock_timeout_ms: 300,
attempt: 1,
};
let result: Result<LandingOutcome, LandingError> =
land_transaction(&context(), world.repo.as_path(), &lock, &mut opened.workspace, &request);
drop(lock);
return result;
}