onevcs/ops.rs
1//! The typed operations the command line renders.
2//!
3//! Every command of this crate answers a *value* first and a rendering second.
4//! `app.rs` calls one of these and turns what it hands back into stdout, stderr
5//! and an exit code; it decides nothing else. That is what keeps the two surfaces
6//! one decision — a consumer embedding the crate and a user reading the command
7//! are told the same thing by the same code, rather than by two readers of one
8//! store that drift a rewording at a time.
9//!
10//! What lives here is the operations that had no home of their own: the ones whose
11//! body used to be inside a command handler, or which composed two private modules
12//! and a rendering. An operation that already belongs to a module — a publication,
13//! a session, a pool, a release — stays there and is re-exported from `lib.rs`
14//! beside these.
15
16use std::path::{Path, PathBuf};
17use std::time::Duration;
18
19use serde::Serialize;
20use url::Url;
21
22use crate::error::{Error, Result};
23use crate::import::Imported;
24use crate::integrate::Outcome as Integration;
25use crate::providers::Providers;
26use crate::publish::{PublishOutcome, Subject};
27use crate::registry::Registry;
28use crate::rules::{Approvals, MergePolicy, RuleMatch};
29pub use crate::store::Coverage as MergePathCoverage;
30use crate::store::{self, Resolution};
31use crate::stream::Stream;
32use crate::sweep::Report as SweepReport;
33use crate::workspace::Ref;
34use crate::{git, policy, provenance, status, stream};
35
36/// The publication policy a repository resolves to, and where each half of it came
37/// from.
38///
39/// The two fields travel with their provenance because every rendering of a policy
40/// in this crate states it: a policy read without knowing whether a rule or the
41/// built-in default decided it is one nobody can act on — the edit that changes it
42/// is in a different file in each case.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ResolvedPolicy {
45 /// How this repository publishes.
46 pub publication: MergePolicy,
47 /// What decided `publication`: a numbered rule of the rules file, or the default.
48 pub publication_from: String,
49 /// Whether a change of this repository needs an approval.
50 pub approvals: Approvals,
51 /// What decided `approvals`, in the same words `publication_from` uses.
52 pub approvals_from: String,
53 /// Whether the draft lifecycle is off, so a change request opens ready.
54 pub drafts_disabled: bool,
55 /// What decided `drafts_disabled`: a numbered rule, the default, or the shipped
56 /// default.
57 pub drafts_disabled_from: String,
58 /// Whether an early lift of a draft prints its warning line.
59 pub warn_on_early_lift: bool,
60 /// What decided `warn_on_early_lift`, in the same words.
61 pub warn_on_early_lift_from: String,
62}
63
64impl ResolvedPolicy {
65 fn of(resolved: &policy::Resolved) -> Self {
66 Self {
67 publication: resolved.policy.publication,
68 publication_from: resolved.publication_from.clone(),
69 approvals: resolved.policy.approvals,
70 approvals_from: resolved.approvals_from.clone(),
71 drafts_disabled: resolved.drafts.disabled,
72 drafts_disabled_from: resolved.drafts.disabled_from.clone(),
73 warn_on_early_lift: resolved.drafts.warn_on_early_lift,
74 warn_on_early_lift_from: resolved.drafts.warn_on_early_lift_from.clone(),
75 }
76 }
77}
78
79/// What `onevcs register` recorded, and what the identity it recorded publishes
80/// under.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct Registration {
83 /// The identity key the checkout's origin normalized to.
84 pub identity: String,
85 /// The alias the checkout is addressed by.
86 pub alias: String,
87 /// The publication checkout the identity resolves to.
88 pub checkout: PathBuf,
89 /// The command this repository states as its complete bar.
90 pub gate: String,
91 /// The policy the identity publishes under, as its rules resolve it.
92 pub policy: ResolvedPolicy,
93 /// What runs a gate on the identity's merge path — and
94 /// [`MergePathCoverage::None`] for one where nothing does, which is the state a
95 /// caller warns about.
96 pub coverage: MergePathCoverage,
97}
98
99/// Register a checkout, resolving its origin to a repository identity.
100///
101/// The library form of `onevcs register`. `origin` is the URL to resolve the
102/// identity from where the checkout's own remote is not the one to use.
103pub fn register_checkout(path: &Path, origin: Option<&Url>) -> Result<Registration> {
104 let origin = origin.map(Url::to_string);
105 let resolution = store::register(path, origin.as_deref())?;
106 // What was registered is the identity and its checkout; how it publishes is the
107 // rules file's answer, resolved here so the coverage below is about the merge
108 // path the identity will actually take.
109 let registry = store::load()?;
110 let (file, source) = policy::load(®istry)?;
111 let resolved = policy::resolve_for(&file, &source, &resolution);
112 let coverage = store::merge_path_coverage(
113 &resolution,
114 &resolution.publication,
115 resolved.policy.publication,
116 );
117 Ok(Registration {
118 identity: resolution.key,
119 alias: resolution.alias,
120 checkout: resolution.publication,
121 gate: resolution.identity.gate,
122 policy: ResolvedPolicy::of(&resolved),
123 coverage,
124 })
125}
126
127/// One registered repository identity, as `onevcs repos` lists it.
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct RegisteredRepository {
130 /// The identity key: the normalized origin every checkout of it shares.
131 pub identity: String,
132 /// The command this repository states as its complete bar.
133 pub gate: String,
134 /// Its registered checkouts, in alias order.
135 pub checkouts: Vec<RegisteredCheckout>,
136 /// What the identity's host requires on its base. `None` is a question that was
137 /// not asked — the audit was not requested, or the identity has no registered
138 /// checkout to ask through — because a listing that did not ask must not read as
139 /// one that asked and was told nothing.
140 pub required_checks: Option<RequiredChecksAnswer>,
141}
142
143/// One registered checkout of an identity.
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct RegisteredCheckout {
146 /// The alias it is addressed by.
147 pub alias: String,
148 /// Where it is.
149 pub path: PathBuf,
150 /// The policy it publishes under and what covers its merge path, asked only
151 /// when the audit was.
152 pub audit: Option<CheckoutAudit>,
153}
154
155/// What the gate audit found about one checkout.
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct CheckoutAudit {
158 /// The policy this checkout publishes under.
159 pub policy: ResolvedPolicy,
160 /// What runs a gate on its merge path, decided from that policy.
161 pub coverage: MergePathCoverage,
162}
163
164/// What an identity's host says it requires before a merge.
165///
166/// Three answers, and none collapses into another for the reason a release probe's
167/// do not: a consumer that reads "nothing required" stops waiting on a check, and
168/// one that reads "unreadable" knows it has not been told. Whether an *answered*
169/// list is complete is [`crate::RequiredChecks`]'s own question — a host protects a
170/// branch from more than one source, and a credential may be refused one of them.
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub enum RequiredChecksAnswer {
173 /// The identity is not hosted anywhere this build answers for, so no host
174 /// answers for it at all.
175 NotHosted,
176 /// The host was asked and refused, or the base could not be read.
177 Unreadable {
178 /// What refused, in its own words.
179 reason: String,
180 },
181 /// The host answered about this base.
182 Answered {
183 /// The base the checks were read for.
184 base: String,
185 /// What it said, and which of its sources went unconsulted.
186 checks: crate::host::RequiredChecks,
187 },
188}
189
190/// Whether a listing carries the gate audit, which is what `--audit-gates` asks for.
191///
192/// Named rather than a flag, because the two answers are different documents: one is
193/// the registry as it stands, and the other reaches a host and reads this host's
194/// rules — a distinction a caller should have to spell at the call site.
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub enum GateAudit {
197 /// The registry as it stands: no rules read, no host asked.
198 Skipped,
199 /// Each identity's required checks, and each checkout's policy and what covers
200 /// its merge path.
201 Asked,
202}
203
204/// Every registered repository, in the order `onevcs repos` lists them.
205///
206/// With [`GateAudit::Asked`], each identity carries what its host requires and each
207/// checkout the policy it publishes under and what covers its merge path. With
208/// [`GateAudit::Skipped`] nothing reads the rules file and no host is asked — the
209/// answer is the registry as it stands.
210pub fn repositories(
211 providers: &Providers<'_>,
212 audit: GateAudit,
213) -> Result<Vec<RegisteredRepository>> {
214 let registry = store::load()?;
215 let mut listed = listing(®istry);
216 if audit == GateAudit::Skipped {
217 return Ok(listed);
218 }
219 // The audit is about each identity's merge path, and the merge path is the
220 // resolved policy's: which verifier covers it follows from how it publishes, so
221 // the rules are read once here and resolved per checkout below.
222 let (file, source) = policy::load(®istry)?;
223 // Zipped rather than looked up again: the listing is this document's identities
224 // in this document's order, so the pairing is exact and there is no second read
225 // of the registry to answer from.
226 for (repository, (_, identity)) in listed.iter_mut().zip(registry.identities.iter()) {
227 // The base is a fact about the origin, so the first registered checkout of it
228 // answers; an identity with none is left unasked rather than answered.
229 repository.required_checks = repository
230 .checkouts
231 .first()
232 .map(|first| required_checks(&repository.identity, &first.path, providers));
233 for checkout in &mut repository.checkouts {
234 let resolution = Resolution {
235 key: repository.identity.clone(),
236 identity: identity.clone(),
237 alias: checkout.alias.clone(),
238 publication: checkout.path.clone(),
239 };
240 let resolved = policy::resolve_for(&file, &source, &resolution);
241 let coverage = store::merge_path_coverage(
242 &resolution,
243 &checkout.path,
244 resolved.policy.publication,
245 );
246 checkout.audit = Some(CheckoutAudit {
247 policy: ResolvedPolicy::of(&resolved),
248 coverage,
249 });
250 }
251 }
252 Ok(listed)
253}
254
255/// Every repository identity this host has registered, in the order `onevcs repos`
256/// lists them.
257///
258/// The library form of the unindented lines `onevcs repos` prints: one normalized
259/// origin per registered identity — `host/owner/name` for a hosted one, the origin
260/// path for a local one — which is the key every other repository-taking operation
261/// here accepts.
262///
263/// It exists because the enumeration had no library form at all. The migration-aware
264/// loader is private, so a consumer that wanted the registered identities — to
265/// maintain each of them, to sweep across them, to ask what each one releases — had
266/// to spawn the binary and parse the prose `repos` prints, which couples it to a
267/// display line nobody promised to keep.
268///
269/// The order is the registry's own, which is the order the command prints, so the
270/// two readings of one document cannot come apart. A registry an older build wrote
271/// is migrated as this reads it, exactly as the command migrates it, and a document
272/// this build cannot read is an `Err` naming what could not be read rather than an
273/// empty list — "this host has registered nothing" and "this host's registry could
274/// not be read" are opposite facts, and a caller acts on only one of them.
275///
276/// It takes no [`Providers`] for the reason [`crate::session_holders`] does not: the
277/// registry is this host's own document, and there is nothing here for an
278/// implementation of either interface to answer.
279pub fn registered_identities() -> Result<Vec<String>> {
280 Ok(listing(&store::load()?)
281 .into_iter()
282 .map(|repository| repository.identity)
283 .collect())
284}
285
286/// The registry as a listing: the one enumeration both reads above answer from, so
287/// the identities a caller asks for and the rows the command prints cannot come to
288/// be two different sets in two different orders.
289fn listing(registry: &Registry) -> Vec<RegisteredRepository> {
290 registry
291 .identities
292 .iter()
293 .map(|(key, identity)| RegisteredRepository {
294 identity: key.clone(),
295 gate: identity.gate.clone(),
296 checkouts: registry
297 .checkouts
298 .iter()
299 .filter(|(_, checkout)| checkout.identity == *key)
300 .map(|(alias, checkout)| RegisteredCheckout {
301 alias: alias.clone(),
302 path: checkout.path.clone(),
303 audit: None,
304 })
305 .collect(),
306 required_checks: None,
307 })
308 .collect()
309}
310
311/// What one identity's host requires on the base its first checkout tracks.
312fn required_checks(key: &str, checkout: &Path, providers: &Providers<'_>) -> RequiredChecksAnswer {
313 let Some(slug) = crate::gh::slug(key) else {
314 return RequiredChecksAnswer::NotHosted;
315 };
316 let asked = git::default_branch(checkout, "origin").and_then(|base| {
317 providers
318 .hosting
319 .for_repo(&slug)?
320 .required_checks_on(&base)
321 .map(|checks| (base, checks))
322 });
323 match asked {
324 Ok((base, checks)) => RequiredChecksAnswer::Answered { base, checks },
325 Err(error) => RequiredChecksAnswer::Unreadable {
326 reason: error.to_string(),
327 },
328 }
329}
330
331/// What one repository argument resolves to.
332#[derive(Debug, Clone, PartialEq, Eq)]
333pub struct ResolvedRepository {
334 /// The identity key.
335 pub identity: String,
336 /// The alias of the checkout the argument selected.
337 pub alias: String,
338 /// The origin the identity was derived from.
339 pub origin: String,
340 /// The command this repository states as its complete bar.
341 pub gate: String,
342 /// The publication checkout: never worked in, only ever fast-forwarded.
343 pub publication_checkout: PathBuf,
344 /// The policy it publishes under, which is what every verb that routes on
345 /// "local or through the host" reads.
346 pub policy: ResolvedPolicy,
347}
348
349/// Resolve a repository argument — an identity key, a registered alias, an origin
350/// URL, or a path — to the identity it selects.
351///
352/// The library form of `onevcs resolve`. It asks the repository side through the
353/// seam, so a supplied [`crate::Vcs`] answers it as readily as this build's git.
354pub fn resolve_repository(providers: &Providers<'_>, repo: &str) -> Result<ResolvedRepository> {
355 let registry = store::load()?;
356 let resolution = store::resolve(®istry, repo)?;
357 // Through the trait, which is the seam a second implementation replaces.
358 let identity = providers.vcs.resolve_identity(repo)?;
359 debug_assert_eq!(identity, resolution.identity);
360 let (file, source) = policy::load(®istry)?;
361 let resolved = policy::resolve_for(&file, &source, &resolution);
362 Ok(ResolvedRepository {
363 identity: resolution.key,
364 alias: resolution.alias,
365 origin: resolution.identity.origin,
366 gate: resolution.identity.gate,
367 publication_checkout: resolution.publication,
368 policy: ResolvedPolicy::of(&resolved),
369 })
370}
371
372/// What to publish, for the branch-keyed verb that takes completed work.
373#[derive(Debug, Clone, PartialEq, Eq)]
374pub struct BranchPublishRequest {
375 /// The repository the branch belongs to: an identity key, a registered alias,
376 /// an origin URL, or a path, exactly as `--repo` takes one.
377 pub repo: PathBuf,
378 /// The branch to publish.
379 pub branch: String,
380 /// The subject to land it under, or none to compose one from its commits.
381 pub title: Option<Subject>,
382 /// The body of the change request, where one is opened.
383 pub body: Option<String>,
384 /// A policy to narrow to; it may only ask for more review than the rules do.
385 pub policy: Option<MergePolicy>,
386}
387
388/// Verify and publish a completed branch no session holds.
389///
390/// The library form of `onevcs publish-branch`. It refuses a branch carrying an
391/// unattested incomplete-step marker, naming [`recover`] — the two verbs are one
392/// path, and provenance is the whole of what separates them.
393pub fn publish_branch(
394 providers: &Providers<'_>,
395 request: &BranchPublishRequest,
396) -> Result<PublishOutcome> {
397 let registry = store::load()?;
398 let token = format!("publish-branch-{}", policy::branch_slug(&request.branch));
399 let mut stream = Stream::open(&token)?;
400 crate::publish_branch::run(
401 ®istry,
402 &request.repo,
403 &request.branch,
404 request.title.clone(),
405 request.body.clone(),
406 request.policy,
407 providers.hosting,
408 &mut stream,
409 )
410}
411
412/// What to recover, for the branch-keyed verb that takes work a step left behind.
413#[derive(Debug, Clone, PartialEq, Eq)]
414pub struct RecoverRequest {
415 /// The repository the branch belongs to, as `--repo` takes one.
416 pub repo: PathBuf,
417 /// The branch to recover.
418 pub branch: String,
419 /// The subject to land it under, or none to compose one from its commits.
420 pub title: Option<Subject>,
421 /// The body of the change request, where one is opened.
422 pub body: Option<String>,
423}
424
425/// Verify and publish a preserved branch that a step left behind, attesting it.
426///
427/// The library form of `onevcs recover`. It requires an unattested incomplete-step
428/// marker and writes the attestation that clears it, so a branch with none is
429/// refused naming [`publish_branch`].
430pub fn recover(providers: &Providers<'_>, request: &RecoverRequest) -> Result<PublishOutcome> {
431 let registry = store::load()?;
432 let token = format!("recover-{}", policy::branch_slug(&request.branch));
433 let mut stream = Stream::open(&token)?;
434 crate::recover::run(
435 ®istry,
436 &request.repo,
437 &request.branch,
438 request.title.clone(),
439 request.body.clone(),
440 providers.hosting,
441 &mut stream,
442 )
443}
444
445/// Which branch to make reachable from an identity's registered checkouts.
446#[derive(Debug, Clone, PartialEq, Eq)]
447pub struct ImportRequest {
448 /// The repository to import into, as `--repo` takes one.
449 pub repo: PathBuf,
450 /// The branch to import.
451 pub branch: String,
452 /// Where to take it from, or none to search the identity's own locations.
453 pub from: Option<String>,
454 /// The name to write it under, or none to keep its own.
455 pub under: Option<String>,
456}
457
458/// Make one branch reachable from an identity's registered checkouts.
459///
460/// The library form of `onevcs import`. It writes refs and nothing else: a name the
461/// destination has checked out is refused rather than written, and a
462/// non-fast-forward is refused naming the commits that would go.
463pub fn import_branch(request: &ImportRequest) -> Result<Imported> {
464 let registry = store::load()?;
465 crate::import::run(
466 ®istry,
467 &request.repo,
468 &request.branch,
469 request.from.as_deref(),
470 request.under.as_deref(),
471 )
472}
473
474/// What the train does with the base once it has advanced it.
475#[derive(Debug, Clone, Copy, PartialEq, Eq)]
476pub enum BasePush {
477 /// Push it, which is what the repository's own `pre-push` hook rules on —
478 /// `onevcs integrate --push`.
479 Push,
480 /// Leave it in the checkout, unpushed and unverified.
481 Keep,
482}
483
484/// Which branches to merge into their base, in order.
485#[derive(Debug, Clone, PartialEq, Eq)]
486pub struct IntegrateRequest {
487 /// The branches to take, in the order they are to be taken.
488 pub branches: Vec<String>,
489 /// What becomes of the base the train advanced.
490 pub push: BasePush,
491}
492
493/// Merge finished branches into their base, in order.
494///
495/// The library form of `onevcs integrate`, and it answers for **the repository the
496/// current directory is in**, exactly as the command does: the train reads its
497/// candidates out of one publication checkout, and it is refused for an identity
498/// whose rules do not publish `local-direct`.
499pub fn integrate(request: &IntegrateRequest) -> Result<Integration> {
500 let registry = store::load()?;
501 let resolution = store::resolve_here(®istry)?;
502 let token = format!("integrate-{}", policy::branch_slug(&resolution.alias));
503 let mut stream = Stream::open(&token)?;
504 crate::integrate::run(
505 &resolution,
506 &request.branches,
507 request.push == BasePush::Push,
508 &mut stream,
509 )
510}
511
512/// What one fast-forward of a publication checkout did.
513#[derive(Debug, Clone, PartialEq, Eq)]
514pub struct Synced {
515 /// The identity whose checkout was synced.
516 pub identity: String,
517 /// The branch it is on, which is the only branch a sync ever moves.
518 pub branch: String,
519 /// The checkout itself.
520 pub checkout: PathBuf,
521 /// The commit it was on before.
522 pub before: String,
523 /// The commit it is on now.
524 pub after: String,
525}
526
527impl Synced {
528 /// Whether the fast-forward moved the checkout, or found it already level.
529 pub fn moved(&self) -> bool {
530 self.before != self.after
531 }
532}
533
534/// Fast-forward a publication checkout to its origin.
535///
536/// The library form of `onevcs sync`, and like the train it answers for the
537/// repository the current directory is in. `branch` is the branch to sync, and
538/// `None` is the origin's default one; a checkout that does not have that branch
539/// checked out is refused, because a sync only ever fast-forwards the branch a
540/// checkout is already on.
541pub fn sync(branch: Option<&str>) -> Result<Synced> {
542 let registry = store::load()?;
543 let resolution = store::resolve_here(®istry)?;
544 let checkout = &resolution.publication;
545 // The name goes on to spell a ref, so an unusable one is refused here rather
546 // than by whichever git command met it first.
547 let branch = match branch {
548 Some(branch) => Ref::try_from(branch.to_owned()).map_err(|reason| Error::Invalid {
549 reason: format!("{reason}: it is not a valid branch name"),
550 })?,
551 None => Ref::from_git(git::default_branch(checkout, "origin")?),
552 };
553 if git::current_branch(checkout)? != *branch {
554 return Err(Error::Invalid {
555 reason: format!(
556 "{} does not have {branch:?} checked out; sync only ever fast-forwards the \
557 branch a checkout is already on",
558 checkout.display()
559 ),
560 });
561 }
562 let before = git::head_sha(checkout)?;
563 git::fetch(checkout, "origin")?;
564 git::merge_ff_only(checkout, &format!("origin/{branch}"))?;
565 let after = git::head_sha(checkout)?;
566 Ok(Synced {
567 identity: resolution.key,
568 branch: branch.to_string(),
569 checkout: checkout.clone(),
570 before,
571 after,
572 })
573}
574
575/// Whether a sweep performs what it decided, which is what `--dry-run` asks.
576///
577/// Named rather than a flag, because the difference between the two is whether
578/// directories are removed, and a call site that spells it `true` says nothing about
579/// which way round that is.
580#[derive(Debug, Clone, Copy, PartialEq, Eq)]
581pub enum Sweeping {
582 /// Decide everything, remove nothing.
583 Rehearse,
584 /// Decide, and reclaim what was decided.
585 Reclaim,
586}
587
588/// Reclaim the publication workspaces this host has finished with.
589///
590/// The library form of `onevcs sweep`. `min_age` is how long evidence outlives the
591/// failure that produced it. Every outcome it reports is a decision — a directory
592/// somebody else owns is an expected outcome of a shared state root rather than a
593/// failure — so an `Err` here means the sweep could not run at all.
594pub fn sweep(sweeping: Sweeping, min_age: Duration) -> Result<SweepReport> {
595 crate::sweep::run(sweeping == Sweeping::Rehearse, min_age)
596}
597
598/// Read one stored artifact.
599///
600/// The library form of `onevcs artifact cat`. The id is checked before it names a
601/// file, so an id that is not one is refused rather than reaching the state root.
602pub fn read_artifact(id: &crate::event::ArtifactId) -> Result<String> {
603 stream::read_artifact(&id.0)
604}
605
606/// Everything this host knows about one piece of work, as `onevcs status` answers it.
607///
608/// Its two renderings are its surface: [`render`](Self::render) is the report a
609/// person reads, and its serialization is the versioned document `--json` prints,
610/// byte for byte. The sections behind them are deliberately **not** public — they
611/// are a document versioned by `status`'s own report version and held to checked-in
612/// goldens, so a consumer reads the JSON it already reads rather than a dozen Rust
613/// types this crate would then owe semver on.
614#[derive(Debug, Clone, Serialize)]
615#[serde(transparent)]
616pub struct StatusReport(status::Report);
617
618impl StatusReport {
619 /// The report as a human reads it, which is what `onevcs status` prints.
620 pub fn render(&self) -> String {
621 self.0.render()
622 }
623}
624
625/// Report everything this host knows about one piece of work.
626///
627/// The library form of `onevcs status`. `reference` is the four-spelling reference
628/// `onevcs status` takes: a change request's URL, a session token, a branch name, or
629/// a commit.
630///
631/// The host is reached through the seam like every other operation that touches one,
632/// and a host that could not be reached leaves a section of the report unavailable
633/// rather than failing — which is the whole reason this answers at all where `gh pr
634/// checks` would not. Whether one piece of work *landed* is
635/// [`crate::landing_status`]: that is the decision on its own, over every repository
636/// and with no host asked.
637pub fn work_status(providers: &Providers<'_>, reference: &str) -> Result<StatusReport> {
638 let registry = store::load()?;
639 Ok(StatusReport(status::run(
640 ®istry,
641 reference,
642 providers.hosting,
643 )?))
644}
645
646/// The rule a repository matched, and what it matched on.
647#[derive(Debug, Clone, PartialEq, Eq)]
648pub struct MatchedRule {
649 /// Its one-based position in the rules file, which is the order that decided it.
650 pub index: usize,
651 /// What it matches on.
652 pub criteria: RuleMatch,
653}
654
655/// How one repository resolves against this host's rules.
656#[derive(Debug, Clone, PartialEq, Eq)]
657pub struct RulesCheck {
658 /// The identity key the argument selected.
659 pub identity: String,
660 /// The publication checkout a `path:` rule is matched against.
661 pub checkout: PathBuf,
662 /// Where the rules came from: the file that was read, or the built-in default.
663 pub rules: String,
664 /// The rule that decided it, where one did; none means the default applies.
665 pub matched: Option<MatchedRule>,
666 /// What it resolved to.
667 pub policy: ResolvedPolicy,
668 /// The git trailer key provenance is written and read under.
669 pub trailer_prefix: String,
670 /// What set that prefix. It is not part of the matched policy: one vocabulary
671 /// reads and writes every repository's provenance, so it is resolved once, from
672 /// the file or the default.
673 pub trailer_prefix_source: TrailerPrefixSource,
674}
675
676/// What decided the provenance trailer prefix.
677#[derive(Debug, Clone, Copy, PartialEq, Eq)]
678pub enum TrailerPrefixSource {
679 /// This host's rules file names it.
680 RulesFile,
681 /// Nothing names one, so the built-in default applies.
682 BuiltIn,
683}
684
685/// Explain how one repository resolves against this host's rules.
686///
687/// The library form of `onevcs rules check`, and the question asked *before* a
688/// publication rather than deduced from one afterwards.
689pub fn rules_check(repo: &str) -> Result<RulesCheck> {
690 let registry = store::load()?;
691 let resolution = store::resolve(®istry, repo)?;
692 let (file, source) = policy::load(®istry)?;
693 let normalized = store::normalize(&resolution.identity.origin);
694 let resolved = policy::resolve(&file, &source, &normalized, &resolution.publication);
695 Ok(RulesCheck {
696 identity: resolution.key,
697 checkout: resolution.publication,
698 rules: resolved.source.clone(),
699 matched: resolved.matched.as_ref().map(|matched| MatchedRule {
700 index: matched.index,
701 criteria: matched.criteria.clone(),
702 }),
703 policy: ResolvedPolicy::of(&resolved),
704 trailer_prefix: provenance::from_rules(&file).prefix().to_string(),
705 trailer_prefix_source: match file.trailer_prefix.is_some() {
706 true => TrailerPrefixSource::RulesFile,
707 false => TrailerPrefixSource::BuiltIn,
708 },
709 })
710}