use std::path::{Path, PathBuf};
use std::time::Duration;
use serde::Serialize;
use url::Url;
use crate::error::{Error, Result};
use crate::import::Imported;
use crate::integrate::Outcome as Integration;
use crate::providers::Providers;
use crate::publish::{PublishOutcome, Subject};
use crate::registry::Registry;
use crate::rules::{Approvals, MergePolicy, RuleMatch};
pub use crate::store::Coverage as MergePathCoverage;
use crate::store::{self, Resolution};
use crate::stream::Stream;
use crate::sweep::Report as SweepReport;
use crate::workspace::Ref;
use crate::{git, policy, provenance, status, stream};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedPolicy {
pub publication: MergePolicy,
pub publication_from: String,
pub approvals: Approvals,
pub approvals_from: String,
}
impl ResolvedPolicy {
fn of(resolved: &policy::Resolved) -> Self {
Self {
publication: resolved.policy.publication,
publication_from: resolved.publication_from.clone(),
approvals: resolved.policy.approvals,
approvals_from: resolved.approvals_from.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Registration {
pub identity: String,
pub alias: String,
pub checkout: PathBuf,
pub gate: String,
pub policy: ResolvedPolicy,
pub coverage: MergePathCoverage,
}
pub fn register_checkout(path: &Path, origin: Option<&Url>) -> Result<Registration> {
let origin = origin.map(Url::to_string);
let resolution = store::register(path, origin.as_deref())?;
let registry = store::load()?;
let (file, source) = policy::load(®istry)?;
let resolved = policy::resolve_for(&file, &source, &resolution);
let coverage = store::merge_path_coverage(
&resolution,
&resolution.publication,
resolved.policy.publication,
);
Ok(Registration {
identity: resolution.key,
alias: resolution.alias,
checkout: resolution.publication,
gate: resolution.identity.gate,
policy: ResolvedPolicy::of(&resolved),
coverage,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegisteredRepository {
pub identity: String,
pub gate: String,
pub checkouts: Vec<RegisteredCheckout>,
pub required_checks: Option<RequiredChecksAnswer>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegisteredCheckout {
pub alias: String,
pub path: PathBuf,
pub audit: Option<CheckoutAudit>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckoutAudit {
pub policy: ResolvedPolicy,
pub coverage: MergePathCoverage,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RequiredChecksAnswer {
NotHosted,
Unreadable {
reason: String,
},
Answered {
base: String,
checks: crate::host::RequiredChecks,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GateAudit {
Skipped,
Asked,
}
pub fn repositories(
providers: &Providers<'_>,
audit: GateAudit,
) -> Result<Vec<RegisteredRepository>> {
let registry = store::load()?;
let mut listed = listing(®istry);
if audit == GateAudit::Skipped {
return Ok(listed);
}
let (file, source) = policy::load(®istry)?;
for (repository, (_, identity)) in listed.iter_mut().zip(registry.identities.iter()) {
repository.required_checks = repository
.checkouts
.first()
.map(|first| required_checks(&repository.identity, &first.path, providers));
for checkout in &mut repository.checkouts {
let resolution = Resolution {
key: repository.identity.clone(),
identity: identity.clone(),
alias: checkout.alias.clone(),
publication: checkout.path.clone(),
};
let resolved = policy::resolve_for(&file, &source, &resolution);
let coverage = store::merge_path_coverage(
&resolution,
&checkout.path,
resolved.policy.publication,
);
checkout.audit = Some(CheckoutAudit {
policy: ResolvedPolicy::of(&resolved),
coverage,
});
}
}
Ok(listed)
}
pub fn registered_identities() -> Result<Vec<String>> {
Ok(listing(&store::load()?)
.into_iter()
.map(|repository| repository.identity)
.collect())
}
fn listing(registry: &Registry) -> Vec<RegisteredRepository> {
registry
.identities
.iter()
.map(|(key, identity)| RegisteredRepository {
identity: key.clone(),
gate: identity.gate.clone(),
checkouts: registry
.checkouts
.iter()
.filter(|(_, checkout)| checkout.identity == *key)
.map(|(alias, checkout)| RegisteredCheckout {
alias: alias.clone(),
path: checkout.path.clone(),
audit: None,
})
.collect(),
required_checks: None,
})
.collect()
}
fn required_checks(key: &str, checkout: &Path, providers: &Providers<'_>) -> RequiredChecksAnswer {
let Some(slug) = crate::gh::slug(key) else {
return RequiredChecksAnswer::NotHosted;
};
let asked = git::default_branch(checkout, "origin").and_then(|base| {
providers
.hosting
.for_repo(&slug)?
.required_checks_on(&base)
.map(|checks| (base, checks))
});
match asked {
Ok((base, checks)) => RequiredChecksAnswer::Answered { base, checks },
Err(error) => RequiredChecksAnswer::Unreadable {
reason: error.to_string(),
},
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedRepository {
pub identity: String,
pub alias: String,
pub origin: String,
pub gate: String,
pub publication_checkout: PathBuf,
pub policy: ResolvedPolicy,
}
pub fn resolve_repository(providers: &Providers<'_>, repo: &str) -> Result<ResolvedRepository> {
let registry = store::load()?;
let resolution = store::resolve(®istry, repo)?;
let identity = providers.vcs.resolve_identity(repo)?;
debug_assert_eq!(identity, resolution.identity);
let (file, source) = policy::load(®istry)?;
let resolved = policy::resolve_for(&file, &source, &resolution);
Ok(ResolvedRepository {
identity: resolution.key,
alias: resolution.alias,
origin: resolution.identity.origin,
gate: resolution.identity.gate,
publication_checkout: resolution.publication,
policy: ResolvedPolicy::of(&resolved),
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BranchPublishRequest {
pub repo: PathBuf,
pub branch: String,
pub title: Option<Subject>,
pub body: Option<String>,
pub policy: Option<MergePolicy>,
}
pub fn publish_branch(
providers: &Providers<'_>,
request: &BranchPublishRequest,
) -> Result<PublishOutcome> {
let registry = store::load()?;
let token = format!("publish-branch-{}", policy::branch_slug(&request.branch));
let mut stream = Stream::open(&token)?;
crate::publish_branch::run(
®istry,
&request.repo,
&request.branch,
request.title.clone(),
request.body.clone(),
request.policy,
providers.hosting,
&mut stream,
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecoverRequest {
pub repo: PathBuf,
pub branch: String,
pub title: Option<Subject>,
pub body: Option<String>,
}
pub fn recover(providers: &Providers<'_>, request: &RecoverRequest) -> Result<PublishOutcome> {
let registry = store::load()?;
let token = format!("recover-{}", policy::branch_slug(&request.branch));
let mut stream = Stream::open(&token)?;
crate::recover::run(
®istry,
&request.repo,
&request.branch,
request.title.clone(),
request.body.clone(),
providers.hosting,
&mut stream,
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImportRequest {
pub repo: PathBuf,
pub branch: String,
pub from: Option<String>,
pub under: Option<String>,
}
pub fn import_branch(request: &ImportRequest) -> Result<Imported> {
let registry = store::load()?;
crate::import::run(
®istry,
&request.repo,
&request.branch,
request.from.as_deref(),
request.under.as_deref(),
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BasePush {
Push,
Keep,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IntegrateRequest {
pub branches: Vec<String>,
pub push: BasePush,
}
pub fn integrate(request: &IntegrateRequest) -> Result<Integration> {
let registry = store::load()?;
let resolution = store::resolve_here(®istry)?;
let token = format!("integrate-{}", policy::branch_slug(&resolution.alias));
let mut stream = Stream::open(&token)?;
crate::integrate::run(
&resolution,
&request.branches,
request.push == BasePush::Push,
&mut stream,
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Synced {
pub identity: String,
pub branch: String,
pub checkout: PathBuf,
pub before: String,
pub after: String,
}
impl Synced {
pub fn moved(&self) -> bool {
self.before != self.after
}
}
pub fn sync(branch: Option<&str>) -> Result<Synced> {
let registry = store::load()?;
let resolution = store::resolve_here(®istry)?;
let checkout = &resolution.publication;
let branch = match branch {
Some(branch) => Ref::try_from(branch.to_owned()).map_err(|reason| Error::Invalid {
reason: format!("{reason}: it is not a valid branch name"),
})?,
None => Ref::from_git(git::default_branch(checkout, "origin")?),
};
if git::current_branch(checkout)? != *branch {
return Err(Error::Invalid {
reason: format!(
"{} does not have {branch:?} checked out; sync only ever fast-forwards the \
branch a checkout is already on",
checkout.display()
),
});
}
let before = git::head_sha(checkout)?;
git::fetch(checkout, "origin")?;
git::merge_ff_only(checkout, &format!("origin/{branch}"))?;
let after = git::head_sha(checkout)?;
Ok(Synced {
identity: resolution.key,
branch: branch.to_string(),
checkout: checkout.clone(),
before,
after,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Sweeping {
Rehearse,
Reclaim,
}
pub fn sweep(sweeping: Sweeping, min_age: Duration) -> Result<SweepReport> {
crate::sweep::run(sweeping == Sweeping::Rehearse, min_age)
}
pub fn read_artifact(id: &crate::event::ArtifactId) -> Result<String> {
stream::read_artifact(&id.0)
}
#[derive(Debug, Clone, Serialize)]
#[serde(transparent)]
pub struct StatusReport(status::Report);
impl StatusReport {
pub fn render(&self) -> String {
self.0.render()
}
}
pub fn work_status(providers: &Providers<'_>, reference: &str) -> Result<StatusReport> {
let registry = store::load()?;
Ok(StatusReport(status::run(
®istry,
reference,
providers.hosting,
)?))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MatchedRule {
pub index: usize,
pub criteria: RuleMatch,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RulesCheck {
pub identity: String,
pub checkout: PathBuf,
pub rules: String,
pub matched: Option<MatchedRule>,
pub policy: ResolvedPolicy,
pub trailer_prefix: String,
pub trailer_prefix_source: TrailerPrefixSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrailerPrefixSource {
RulesFile,
BuiltIn,
}
pub fn rules_check(repo: &str) -> Result<RulesCheck> {
let registry = store::load()?;
let resolution = store::resolve(®istry, repo)?;
let (file, source) = policy::load(®istry)?;
let normalized = store::normalize(&resolution.identity.origin);
let resolved = policy::resolve(&file, &source, &normalized, &resolution.publication);
Ok(RulesCheck {
identity: resolution.key,
checkout: resolution.publication,
rules: resolved.source.clone(),
matched: resolved.matched.as_ref().map(|matched| MatchedRule {
index: matched.index,
criteria: matched.criteria.clone(),
}),
policy: ResolvedPolicy::of(&resolved),
trailer_prefix: provenance::from_rules(&file).prefix().to_string(),
trailer_prefix_source: match file.trailer_prefix.is_some() {
true => TrailerPrefixSource::RulesFile,
false => TrailerPrefixSource::BuiltIn,
},
})
}