use std::path::Path;
use serde_json::json;
use crate::branch::{self, Verb};
use crate::error::{Error, Result};
use crate::event::EventKind;
use crate::host::Hosting;
use crate::publish::{PublishOutcome, Subject};
use crate::registry::{Registry, RepoType, Workflow};
use crate::store;
use crate::stream::Stream;
use crate::workspace::object;
use crate::{provenance, rules};
pub fn run(
registry: &Registry,
repo: &Path,
branch: &str,
title: Option<Subject>,
hosting: &dyn Hosting,
stream: &mut Stream,
) -> Result<PublishOutcome> {
let landing = branch::prepare(registry, Verb::Recover, repo, branch, None)?;
let unattested = landing.unattested()?;
if unattested.is_empty() {
return Err(Error::Invalid {
reason: nothing_to_recover(&landing)?,
});
}
if let Some(reason) = attests_nothing(&landing) {
return Err(Error::Invalid { reason });
}
landing.check_subject(title.as_ref())?;
landing.sync_change_base(stream)?;
let attested = provenance::attest(
&landing.worktree,
&landing.compared_change_base,
&landing.trailers,
)?;
stream.emit(
EventKind::RecoveryAttested,
object(json!({
"branch": branch,
"markers": unattested,
"attestation": attested,
})),
);
landing.publish(title, hosting, stream)
}
fn nothing_to_recover(landing: &branch::Landing) -> Result<String> {
let branch = &landing.branch;
if let Some(prefix) = landing.unrecognized()?.first() {
return Ok(landing.unreadable_prefix(prefix));
}
if landing.ahead()?.is_empty() {
return Ok(format!(
"branch {branch:?} has nothing ahead of {}; there is no preserved work to recover. \
`onevcs recoverable` lists the branches that do carry unpublished work",
landing.change_base
));
}
Ok(format!(
"branch {branch:?} carries no unattested incomplete provenance: it has commits ahead of \
{}, and all of them are complete. `recover` publishes interrupted work; publish a \
completed branch with `{}`",
landing.change_base,
complete_branch_verb(landing),
))
}
fn complete_branch_verb(landing: &branch::Landing) -> String {
let identity = &landing.resolution.identity;
if identity.repo_type == RepoType::Team || identity.workflow == Workflow::Remote {
landing.command_for(Verb::PublishBranch)
} else {
crate::guidance::command(["onevcs", "integrate", &landing.branch])
}
}
fn attests_nothing(landing: &branch::Landing) -> Option<String> {
let merge_path_gate = matches!(
landing.resolved.policy.gate,
rules::Gate::Kind {
kind: rules::GateKind::PrePush
}
);
if landing.resolution.identity.gate != store::NOOP_GATE
|| !merge_path_gate
|| store::pre_push_hook(&landing.source).is_some()
{
return None;
}
Some(format!(
"identity {:?} names no complete bar and its merge path runs no gate, so a recovery \
attestation would attest nothing. Give it one in the rules file at {}: a rule matching \
this identity with `gate: {{command: [...]}}` names the bar itself, and \
`gate: {{kind: pre-push}}` keeps the merge path as the gate once {} carries an \
executable pre-push hook. Confirm it with `{}`, then re-run `{}`",
landing.resolution.key,
landing.rules_file.display(),
landing.source.display(),
landing.rules_check(),
landing.command(),
))
}