use std::path::Path;
use crate::bounded_git::{run_git, GitDeadline, GitOutcome};
use crate::execution::state::{classify_base_completion_within, BaseCompletionEvidence};
use crate::web::remote_control_api::dto::{OwnerExecutionContract, TerminalMode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CertificationStage {
Repository,
Remote,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Verdict {
Completed { evidence: String },
NotCompleted { detail: String },
Broken { detail: String },
Unsupported { detail: String },
DeadlineExpired { stage: CertificationStage },
}
pub async fn verify(
change_id: &str,
repo_root: &Path,
contract: &OwnerExecutionContract,
deadline: GitDeadline,
) -> Verdict {
match contract.terminal_mode {
TerminalMode::Merged => {
verify_base(change_id, repo_root, &contract.base_branch, deadline).await
}
TerminalMode::BasePublished => {
let Some(remote) = contract.remote.as_deref() else {
return Verdict::Unsupported {
detail: "the owner declared base publication but named no remote".to_string(),
};
};
match verify_base(change_id, repo_root, &contract.base_branch, deadline).await {
Verdict::Completed { evidence } => {
match remote_matches_local(repo_root, remote, &contract.base_branch, deadline)
.await
{
Ok(Some(oid)) => Verdict::Completed {
evidence: format!(
"{evidence}; {remote}/{} published at {oid}",
contract.base_branch
),
},
Ok(None) => Verdict::NotCompleted {
detail: format!(
"'{}' is integrated locally but {remote}/{} does not yet match the \
local base tip",
change_id, contract.base_branch
),
},
Err(RemoteError::Broken(detail)) => Verdict::Broken { detail },
Err(RemoteError::DeadlineExpired) => expired(
deadline,
CertificationStage::Remote,
&format!("git ls-remote {remote}"),
),
}
}
other => other,
}
}
TerminalMode::BranchPushed => {
let (Some(remote), Some(branch)) = (
contract.remote.as_deref(),
contract.pushed_branch.as_deref(),
) else {
return Verdict::Unsupported {
detail: "the owner declared branch publication but named no remote or branch"
.to_string(),
};
};
match verify_base(change_id, repo_root, branch, deadline).await {
Verdict::Completed { evidence } => {
match remote_matches_local(repo_root, remote, branch, deadline).await {
Ok(Some(oid)) => Verdict::Completed {
evidence: format!(
"{evidence}; {remote}/{branch} published at {oid} (branch \
publication, not base integration)"
),
},
Ok(None) => Verdict::NotCompleted {
detail: format!(
"'{branch}' carries the archived proposal but {remote}/{branch} \
does not yet match its local tip"
),
},
Err(RemoteError::Broken(detail)) => Verdict::Broken { detail },
Err(RemoteError::DeadlineExpired) => expired(
deadline,
CertificationStage::Remote,
&format!("git ls-remote {remote}"),
),
}
}
other => other,
}
}
}
}
fn expired(deadline: GitDeadline, stage: CertificationStage, what: &str) -> Verdict {
if deadline.is_operation_deadline() {
Verdict::DeadlineExpired { stage }
} else {
Verdict::NotCompleted {
detail: format!(
"{what} did not finish within its subprocess budget; the child was terminated and \
the check will be retried"
),
}
}
}
async fn verify_base(
change_id: &str,
repo_root: &Path,
branch: &str,
deadline: GitDeadline,
) -> Verdict {
let Some(evidence) =
classify_base_completion_within(change_id, repo_root, branch, deadline).await
else {
return expired(
deadline,
CertificationStage::Repository,
"local base-completion classification",
);
};
match evidence {
BaseCompletionEvidence::Completed => Verdict::Completed {
evidence: format!(
"'{branch}' holds the archived '{change_id}' entry and no active change directory"
),
},
BaseCompletionEvidence::NotCompleted => Verdict::NotCompleted {
detail: format!("'{branch}' holds no archive entry for '{change_id}'"),
},
BaseCompletionEvidence::Contradictory { detail } => Verdict::Broken { detail },
BaseCompletionEvidence::EvidenceError { detail, .. } => Verdict::Broken { detail },
}
}
enum RemoteError {
Broken(String),
DeadlineExpired,
}
async fn remote_matches_local(
repo_root: &Path,
remote: &str,
branch: &str,
deadline: GitDeadline,
) -> Result<Option<String>, RemoteError> {
let local = rev_parse(repo_root, branch, deadline)
.await?
.ok_or_else(|| {
RemoteError::Broken(format!("local branch '{branch}' could not be resolved"))
})?;
let output = match run_git(
repo_root,
&["ls-remote", "--exit-code", "--heads", remote, branch],
deadline,
)
.await
{
Ok(GitOutcome::Finished(output)) => output,
Ok(GitOutcome::DeadlineExpired) => return Err(RemoteError::DeadlineExpired),
Err(error) => {
return Err(RemoteError::Broken(format!(
"failed to run git ls-remote for '{remote}': {error}"
)))
}
};
if !output.status.success() {
if output.status.code() == Some(2) {
return Ok(None);
}
return Err(RemoteError::Broken(format!(
"git ls-remote {remote} {branch} failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
)));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let remote_oid = stdout
.lines()
.find_map(|line| parse_ls_remote_line(line, branch));
Ok(match remote_oid {
Some(remote_oid) if remote_oid == local => Some(local),
_ => None,
})
}
pub fn parse_ls_remote_line(line: &str, branch: &str) -> Option<String> {
let (oid, reference) = line.split_once('\t')?;
if reference.trim() != format!("refs/heads/{branch}") {
return None;
}
let oid = oid.trim();
if oid.is_empty() {
return None;
}
Some(oid.to_string())
}
async fn rev_parse(
repo_root: &Path,
revision: &str,
deadline: GitDeadline,
) -> Result<Option<String>, RemoteError> {
let output = match run_git(
repo_root,
&["rev-parse", "--verify", &format!("{revision}^{{commit}}")],
deadline,
)
.await
{
Ok(GitOutcome::Finished(output)) => output,
Ok(GitOutcome::DeadlineExpired) => return Err(RemoteError::DeadlineExpired),
Err(error) => {
return Err(RemoteError::Broken(format!(
"failed to run git rev-parse for '{revision}': {error}"
)))
}
};
if !output.status.success() {
return Ok(None);
}
let oid = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(if oid.is_empty() { None } else { Some(oid) })
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use tokio::time::Instant;
#[test]
fn an_ls_remote_line_matches_only_the_exact_branch_ref() {
let line = "1111111111111111111111111111111111111111\trefs/heads/alpha";
assert_eq!(
parse_ls_remote_line(line, "alpha").as_deref(),
Some("1111111111111111111111111111111111111111")
);
assert_eq!(parse_ls_remote_line(line, "pha"), None);
assert_eq!(
parse_ls_remote_line(
"2222222222222222222222222222222222222222\trefs/heads/team/alpha",
"alpha"
),
None
);
assert_eq!(
parse_ls_remote_line(
"3333333333333333333333333333333333333333\trefs/tags/alpha",
"alpha"
),
None
);
assert_eq!(parse_ls_remote_line("garbage", "alpha"), None);
}
#[test]
fn a_contract_without_its_publication_identity_is_unsupported_not_completed() {
let contract = OwnerExecutionContract {
base_branch: "main".to_string(),
terminal_mode: TerminalMode::BasePublished,
remote: None,
pushed_branch: None,
};
let verdict = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
verify(
"alpha",
Path::new("/nonexistent"),
&contract,
GitDeadline::Operation(Instant::now() + Duration::from_secs(30)),
)
.await
});
assert!(
matches!(verdict, Verdict::Unsupported { .. }),
"{verdict:?}"
);
}
#[test]
fn branch_publication_without_a_branch_is_unsupported() {
let contract = OwnerExecutionContract {
base_branch: "main".to_string(),
terminal_mode: TerminalMode::BranchPushed,
remote: Some("origin".to_string()),
pushed_branch: None,
};
let verdict = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
verify(
"alpha",
Path::new("/nonexistent"),
&contract,
GitDeadline::Operation(Instant::now() + Duration::from_secs(30)),
)
.await
});
assert!(
matches!(verdict, Verdict::Unsupported { .. }),
"{verdict:?}"
);
}
#[test]
fn only_an_operation_deadline_turns_a_git_expiry_into_the_operations_answer() {
assert_eq!(
expired(
GitDeadline::Operation(Instant::now() + Duration::from_secs(30)),
CertificationStage::Remote,
"git ls-remote origin"
),
Verdict::DeadlineExpired {
stage: CertificationStage::Remote
}
);
}
#[test]
fn an_operation_expiry_names_the_half_of_the_proof_it_interrupted() {
assert_eq!(
expired(
GitDeadline::Operation(Instant::now() + Duration::from_secs(30)),
CertificationStage::Repository,
"local base-completion classification"
),
Verdict::DeadlineExpired {
stage: CertificationStage::Repository
}
);
}
#[test]
fn a_per_child_expiry_is_a_retryable_absence_of_evidence_not_a_timeout() {
let verdict = expired(
GitDeadline::PerChild(Duration::from_secs(30)),
CertificationStage::Remote,
"git ls-remote origin",
);
let Verdict::NotCompleted { detail } = verdict else {
panic!("a per-child expiry must keep the caller observing: {verdict:?}");
};
assert!(detail.contains("terminated"), "{detail}");
assert!(detail.contains("retried"), "{detail}");
}
#[test]
fn an_unbounded_git_deadline_never_claims_the_operation_expired() {
assert!(!matches!(
expired(
GitDeadline::Unbounded,
CertificationStage::Repository,
"git rev-parse"
),
Verdict::DeadlineExpired { .. }
));
}
}