use super::*;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub(super) enum GitFault {
History,
Parent,
PublishBeforeTag,
PublishTagCommit,
PublishTagMissing,
PublishTagObject,
RemoteAhead,
RemoteLock,
Status,
TagAdmissionRefused,
TagAdmissionMissing,
TagAdmissionObject,
TagCommit,
TagObject,
}
#[derive(Default, Deserialize, Serialize)]
pub(super) struct GitState {
pub source: BTreeMap<String, String>,
pub release: Option<BTreeMap<String, String>>,
pub staged: bool,
pub tag: bool,
pub descendant: Option<BTreeMap<String, String>>,
pub fail: Option<GitFault>,
}
impl GitState {
fn head(&self) -> &str {
if self.descendant.is_some() {
"fix"
} else if self.release.is_some() {
"release"
} else {
"source"
}
}
fn revision_files(&self, revision: &str) -> Result<&BTreeMap<String, String>> {
match revision {
"source" => Ok(&self.source),
"release" => self
.release
.as_ref()
.ok_or_else(|| "missing fixture release".into()),
"fix" => self
.descendant
.as_ref()
.ok_or_else(|| "missing fixture descendant".into()),
_ => Err("unexpected fixture revision".into()),
}
}
pub(super) fn check_tag(&mut self, args: &[&str]) -> Result<String> {
require(
args == [TAG_CHECKER, "release", "0.13.0"],
"unexpected fixture tag selection",
)?;
match self.fail.as_ref() {
Some(GitFault::TagCommit | GitFault::TagAdmissionRefused) => {
Err("fixture shared tag checker refused".into())
}
Some(GitFault::TagAdmissionObject) => {
self.fail = Some(GitFault::TagObject);
Ok(String::new())
}
Some(GitFault::TagAdmissionMissing) => {
self.tag = false;
Ok(String::new())
}
_ => Ok(String::new()),
}
}
pub(super) fn after_publish(&mut self) {
match self.fail.as_ref() {
Some(GitFault::PublishTagMissing) => self.tag = false,
Some(GitFault::PublishTagCommit) => self.fail = Some(GitFault::TagCommit),
Some(GitFault::PublishTagObject) => self.fail = Some(GitFault::TagObject),
_ => {}
}
}
#[expect(
clippy::too_many_lines,
reason = "one explicit Git observation protocol for release fixtures"
)]
pub(super) fn capture(
&mut self,
root: &Path,
args: &[&str],
calls: &[Vec<String>],
) -> Result<String> {
let changed = |reference: &BTreeMap<String, String>| -> Result<String> {
Ok(reference
.iter()
.filter_map(|(path, text)| match fs::read_to_string(root.join(path)) {
Ok(current) if current == *text => None,
_ => Some(path.as_str()),
})
.collect::<Vec<_>>()
.join("\n"))
};
match args {
["--no-optional-locks", "status", ..]
if self.fail.as_ref() == Some(&GitFault::Status) =>
{
Err("fixture Git status observation failure".into())
}
["--no-optional-locks", "status", ..] => changed(self.revision_files(self.head())?),
["rev-parse", "HEAD"] => {
if self.fail.as_ref() == Some(&GitFault::PublishBeforeTag)
&& calls
.iter()
.any(|call| call.get(1).is_some_and(|arg| arg == TAG_CHECKER))
{
self.tag = false;
}
Ok(self.head().to_owned())
}
["rev-parse", "HEAD^"] => Ok("source".to_owned()),
["show", object] => {
let (revision, path) = object.split_once(':').ok_or("invalid fixture object")?;
match revision {
"" if self.staged => Ok(fs::read_to_string(root.join(path))?),
revision => self
.revision_files(revision)?
.get(path)
.cloned()
.ok_or_else(|| "missing fixture path".into()),
}
}
["for-each-ref", ..] => Ok(if self.tag
&& self.fail.as_ref() == Some(&GitFault::TagObject)
{
"changed-tag-object refs/tags/v0.13.0"
} else if self.tag {
"tag-object refs/tags/v0.13.0"
} else {
""
}
.to_owned()),
["symbolic-ref", ..] => Ok("main".to_owned()),
["merge-base", ..] if self.fail.as_ref() == Some(&GitFault::RemoteAhead) => {
Err("fixture remote is ahead".into())
}
["fetch", ..] if self.fail.as_ref() == Some(&GitFault::RemoteLock) => {
if calls
.iter()
.filter(|call| call.get(1).is_some_and(|arg| arg == "fetch"))
.count()
== 2
{
fs::write(
root.join("Cargo.lock"),
LOCK.replace(
"name = \"other\"\nversion = \"0.12.3\"",
"name = \"other\"\nversion = \"0.12.4\"",
),
)?;
}
Ok(String::new())
}
["merge-base", "--is-ancestor", _, _]
if self.fail.as_ref() == Some(&GitFault::History) =>
{
Err("fixture unrelated history".into())
}
["fetch" | "merge-base" | "ls-files", ..]
| ["diff", "--cached", "--summary", "source", "--", _] => Ok(String::new()),
["diff", "--cached", "--name-only", "source", "--"] => {
if self.staged && self.release.is_none() {
changed(&self.source)
} else {
Ok(String::new())
}
}
["diff", "--name-only", "source", "release", "--"] => {
let release = self.revision_files("release")?;
Ok(self
.source
.iter()
.filter_map(|(path, original)| {
(release.get(path) != Some(original)).then_some(path.as_str())
})
.collect::<Vec<_>>()
.join("\n"))
}
["diff", "--name-only", "source", ..] => changed(&self.source),
["diff", "--name-only", "fix", ..] => changed(self.revision_files("fix")?),
["diff", "--cached", "--name-only" | "--summary", "fix", ..] => {
if self.staged {
changed(self.revision_files("fix")?)
} else {
Ok(String::new())
}
}
["diff", "--name-only"] => {
if self.staged {
Ok(String::new())
} else {
changed(&self.source)
}
}
["diff", "--cached", "--name-only"] => {
if self.staged {
changed(&self.source)
} else {
Ok(String::new())
}
}
["log", "-1", "--format=%P", "release"] => {
Ok(if self.fail.as_ref() == Some(&GitFault::Parent) {
"wrong-source"
} else {
"source"
}
.to_owned())
}
["log", "-1", "--format=%s", "fix"] => Ok("Fix callback".to_owned()),
["log", "-1", "--format=%s", "release"] => Ok(if self.release.is_some() {
"Release 0.13.0"
} else {
"Fixture source"
}
.to_owned()),
_ => Err(format!("unexpected fixture git command: {args:?}").into()),
}
}
}