use super::support::{
Fixture, Observed, TRANSACTION_ID, fixture, git, observe, porcelain, registered_transaction,
remove, repository, run_direct, run_wrapped, silent_success, stderr_of, stopped_with, wrapped,
};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
fn head(fixture: &Fixture, repo: &Path) -> String {
let output: std::process::Output = git(fixture, repo, &["rev-parse", "HEAD"]);
return String::from_utf8_lossy(&output.stdout).into_owned();
}
fn refs(fixture: &Fixture, repo: &Path) -> String {
let output: std::process::Output = git(fixture, repo, &["for-each-ref", "--format=%(refname)"]);
return String::from_utf8_lossy(&output.stdout).into_owned();
}
fn repository_with_linked_worktree(fixture: &Fixture) -> (PathBuf, PathBuf) {
let repo: PathBuf = repository(fixture, OsStr::new("repo"));
let linked: PathBuf = fixture.root.join("linked");
git(
fixture,
repo.as_path(),
&["worktree", "add", "--quiet", "-b", "topic", "../linked"],
);
return (repo, linked);
}
#[test]
fn a_dry_run_commit_is_forwarded_and_commits_nothing() {
let fixture: Fixture = fixture("frontier-commit");
let repo: PathBuf = repository(&fixture, OsStr::new("repo"));
std::fs::write(repo.join("file.txt"), b"content\n").expect("file");
git(&fixture, repo.as_path(), &["add", "--", "file.txt"]);
let head_before: String = head(&fixture, repo.as_path());
let native: Observed = run_direct(
&fixture,
repo.as_path(),
&["commit", "-o", "--dry-run", "-m", "dry", "--", "file.txt"],
);
assert_eq!(native.code, Some(0));
assert_eq!(
run_wrapped(
&fixture,
repo.as_path(),
&["commit", "--dry-run", "-m", "dry", "--", "file.txt"]
),
native
);
assert_eq!(head(&fixture, repo.as_path()), head_before);
for format in ["--short", "--porcelain", "--long", "-z"] {
let through_git: Observed = run_direct(
&fixture,
repo.as_path(),
&["commit", "-o", format, "--", "file.txt"],
);
assert_eq!(through_git.code, Some(0), "{format}");
assert_eq!(
run_wrapped(
&fixture,
repo.as_path(),
&["commit", format, "--", "file.txt"]
),
through_git,
"{format}"
);
assert_eq!(head(&fixture, repo.as_path()), head_before, "{format}");
}
assert_eq!(porcelain(&fixture, repo.as_path()), "A file.txt\n");
remove(&fixture);
}
const FILE_WARNING: &str = "{\"schemaVersion\":1,\"sequence\":0,\"type\":\"finding\",\"trigger\":\"pre-forward\",\"policyId\":\"final-newline\",\"severity\":\"warn\",\"code\":\"final-newline/noncanonical-final-newline\",\"message\":\"Non-empty text file must end with exactly one LF byte.\",\"path\":\"file.txt\",\"fix\":\"none\"}\n";
const CONTEXT_ERROR: &str = "{\"schemaVersion\":1,\"sequence\":0,\"type\":\"finding\",\"trigger\":\"pre-forward\",\"policyId\":\"mono/forbidden-root-context\",\"severity\":\"error\",\"code\":\"mono/forbidden-root-context/root-context-forbidden\",\"message\":\"Root CONTEXT.md is forbidden; read source code directly.\",\"path\":\"CONTEXT.md\",\"fix\":\"none\"}\n";
#[test]
fn add_runs_the_content_policies_over_what_it_stages() {
let fixture: Fixture = fixture("frontier-add");
let repo: PathBuf = repository(&fixture, OsStr::new("repo"));
std::fs::write(repo.join("file.txt"), b"no final newline").expect("file");
std::fs::write(repo.join("second.txt"), b"second\n").expect("second file");
std::fs::write(repo.join("CONTEXT.md"), b"context\n").expect("context file");
assert_eq!(
run_wrapped(&fixture, repo.as_path(), &["add", "--", "file.txt"]),
Observed {
code: Some(0),
stdout: Vec::<u8>::new(),
stderr: FILE_WARNING.as_bytes().to_vec(),
}
);
assert_eq!(
porcelain(&fixture, repo.as_path()),
"A file.txt\n?? CONTEXT.md\n?? second.txt\n"
);
let configuration: PathBuf = repo.join("cli-git.config.jsonc");
std::fs::write(
&configuration,
"{ \"policies\": { \"mono/forbidden-root-context\": \"error\" } }\n",
)
.expect("configuration");
assert_eq!(
run_wrapped(&fixture, repo.as_path(), &["add", "--", "CONTEXT.md"]),
Observed {
code: Some(1),
stdout: Vec::<u8>::new(),
stderr: CONTEXT_ERROR.as_bytes().to_vec(),
}
);
assert_eq!(
porcelain(&fixture, repo.as_path()),
"A file.txt\n?? CONTEXT.md\n?? cli-git.config.jsonc\n?? second.txt\n"
);
std::fs::write(
&configuration,
"{ \"policies\": { \"markdown/autofix\": \"error\" } }\n",
)
.expect("configuration");
assert_eq!(
run_wrapped(&fixture, repo.as_path(), &["add", "--", "second.txt"]),
silent_success()
);
assert_eq!(
porcelain(&fixture, repo.as_path()),
"A file.txt\nA second.txt\n?? CONTEXT.md\n?? cli-git.config.jsonc\n"
);
std::fs::write(repo.join("third.txt"), b"third\n").expect("third file");
std::fs::write(
&configuration,
"{ \"policies\": { \"mono/dependent-version-bump\": \"error\" } }\n",
)
.expect("configuration");
assert_eq!(
run_wrapped(&fixture, repo.as_path(), &["add", "--", "third.txt"]),
silent_success()
);
assert_eq!(
porcelain(&fixture, repo.as_path()),
"A file.txt\nA second.txt\nA third.txt\n?? CONTEXT.md\n?? cli-git.config.jsonc\n"
);
remove(&fixture);
}
#[test]
fn a_real_push_is_scanned_and_forwarded() {
let fixture: Fixture = fixture("frontier-push");
let repo: PathBuf = repository(&fixture, OsStr::new("repo"));
git(
&fixture,
fixture.root.as_path(),
&["init", "--quiet", "--bare", "remote.git"],
);
let remote: PathBuf = fixture.root.join("remote.git");
git(
&fixture,
repo.as_path(),
&["remote", "add", "origin", "../remote.git"],
);
let pushed: Observed = run_wrapped(&fixture, repo.as_path(), &["push", "origin", "main"]);
assert_eq!(pushed.code, Some(0), "{pushed:?}");
assert!(
!refs(&fixture, remote.as_path()).is_empty(),
"the clean push must publish main"
);
let native: Observed = run_direct(
&fixture,
repo.as_path(),
&["push", "--atomic", "--dry-run", "origin", "main"],
);
assert_eq!(native.code, Some(0));
assert_eq!(
run_wrapped(
&fixture,
repo.as_path(),
&["push", "--dry-run", "origin", "main"]
),
native
);
assert_eq!(
refs(&fixture, remote.as_path()),
"refs/heads/main
"
);
assert_eq!(
run_wrapped(
&fixture,
repo.as_path(),
&[
"push",
"--quiet",
"--no-enforce-final-newline",
"origin",
"main"
]
),
silent_success()
);
assert_eq!(refs(&fixture, remote.as_path()), "refs/heads/main\n");
remove(&fixture);
}
#[test]
fn leftover_transaction_state_refuses_guarded_commands() {
let fixture: Fixture = fixture("frontier-state");
let (repo, linked): (PathBuf, PathBuf) = repository_with_linked_worktree(&fixture);
let guarded: [&str; 3] = ["reset", "--soft", "HEAD"];
assert_eq!(
run_wrapped(&fixture, repo.as_path(), &guarded),
silent_success()
);
let registry: PathBuf = registered_transaction(repo.as_path(), true);
assert_eq!(
run_wrapped(&fixture, repo.as_path(), &guarded),
silent_success()
);
assert_eq!(
run_wrapped(&fixture, linked.as_path(), &guarded),
silent_success()
);
assert_eq!(
run_wrapped(
&fixture,
repo.as_path(),
&["rev-parse", "--is-inside-work-tree"]
)
.stdout,
b"true\n"
);
assert!(registry.join(TRANSACTION_ID).exists());
std::fs::remove_dir_all(®istry).expect("remove registry");
registered_transaction(repo.as_path(), false);
assert_eq!(
run_wrapped(&fixture, repo.as_path(), &guarded),
silent_success()
);
assert!(!registry.join(TRANSACTION_ID).exists());
let broken: PathBuf = registry.join(TRANSACTION_ID);
std::fs::create_dir(&broken).expect("published directory");
std::fs::write(broken.join("owner.json"), b"{}").expect("malformed owner");
std::fs::write(broken.join("preparing.json"), b"kept").expect("evidence");
let stopped: Observed = run_wrapped(&fixture, repo.as_path(), &guarded);
assert_eq!(stopped.code, Some(2));
assert_eq!(
stderr_of(&stopped),
format!(
"{{\"schemaVersion\":1,\"sequence\":0,\"type\":\"engine-failure\",\"code\":\"content-unavailable\",\"message\":\"Transaction owner record is malformed: {}\"}}\n",
broken.display()
)
);
assert_eq!(
std::fs::read(broken.join("owner.json")).expect("kept"),
b"{}"
);
assert_eq!(
std::fs::read(broken.join("preparing.json")).expect("kept"),
b"kept"
);
assert_eq!(
run_wrapped(&fixture, repo.as_path(), &["log", "--oneline"]).code,
Some(2)
);
std::fs::remove_dir_all(®istry).expect("remove registry");
let legacy: PathBuf = repo.join(".git/cli-git-transaction");
std::fs::create_dir(&legacy).expect("legacy transaction directory");
assert_eq!(
run_wrapped(&fixture, repo.as_path(), &guarded),
stopped_with(
format!(
"cli-git: {} holds a commit journal in a format this executable does not recover. \
Nothing was changed. Run any git command once with the previous cli-git executable to \
recover it, then run this command again.\n",
legacy.display()
)
.as_str()
)
);
assert!(legacy.exists());
std::fs::remove_dir(&legacy).expect("remove legacy directory");
assert_eq!(
observe(
wrapped(&fixture)
.current_dir(&repo)
.env("CLI_GIT_WORKTREE_COPY_LEASE", "inherited")
.args(guarded),
b""
),
silent_success()
);
assert_eq!(
observe(
wrapped(&fixture)
.current_dir(&repo)
.env("CLI_GIT_LANDING_LEASE", "inherited")
.args(guarded),
b""
),
silent_success()
);
assert_eq!(
stderr_of(&run_wrapped(&fixture, repo.as_path(), &guarded)),
""
);
remove(&fixture);
}
#[test]
fn a_real_push_of_a_nonconforming_commit_is_refused_and_publishes_nothing() {
let fixture: Fixture = fixture("frontier-push-refused");
let repo: PathBuf = repository(&fixture, OsStr::new("repo"));
git(
&fixture,
fixture.root.as_path(),
&["init", "--quiet", "--bare", "remote.git"],
);
let remote: PathBuf = fixture.root.join("remote.git");
git(
&fixture,
repo.as_path(),
&["remote", "add", "origin", "../remote.git"],
);
std::fs::write(
repo.join("cli-git.config.jsonc"),
"{ \"policies\": { \"final-newline\": \"error\" } }\n",
)
.expect("configuration");
git(&fixture, repo.as_path(), &["add", "cli-git.config.jsonc"]);
git(
&fixture,
repo.as_path(),
&["commit", "--quiet", "-m", "configure"],
);
git(
&fixture,
repo.as_path(),
&["push", "--quiet", "origin", "main"],
);
std::fs::write(repo.join("bad.txt"), "no final newline").expect("nonconforming file");
git(&fixture, repo.as_path(), &["add", "bad.txt"]);
git(
&fixture,
repo.as_path(),
&["commit", "--quiet", "-m", "nonconforming"],
);
let before: String = refs(&fixture, remote.as_path());
let pushed: Observed = run_wrapped(&fixture, repo.as_path(), &["push", "origin", "main"]);
assert_eq!(pushed.code, Some(1), "{pushed:?}");
assert!(
String::from_utf8_lossy(&pushed.stderr)
.contains("final-newline/noncanonical-final-newline"),
"{pushed:?}"
);
assert_eq!(refs(&fixture, remote.as_path()), before);
remove(&fixture);
}