onevcs/lib.rs
1//! Version control and its remote host behind one host-neutral vocabulary.
2//!
3//! The review unit is a [`ChangeRequest`] — GitHub maps it to a pull request,
4//! and a later host maps it to whatever it calls the same thing. [`Vcs`] owns the
5//! repository side (identities, sessions, preserved work) and [`RemoteHost`] owns
6//! the host side (opening a change, reading its checks, merging it). A
7//! [`rules`] file decides, per repository, how a change is published and whether
8//! it needs an approval. Everything a process does along the way is emitted as an
9//! [`Envelope`].
10//!
11//! # The shape of one change
12//!
13//! ```text
14//! session open → a --shared clone and one worktree, occupancy-leased: a warm
15//! pool slot where the host keeps one, else cut for this run
16//! → work happens in the worktree
17//! → publish → fetch and merge the current base (bounded resolve-and-requeue)
18//! → the repository's own merge path verifies it: its pre-push
19//! hook, or the host's required checks on the change request
20//! → local-direct squash, or a change request the host lands
21//! → session close → the branch is copied out and stays; a slot is returned
22//! warm, a per-run worktree goes
23//! ```
24//!
25//! Everything durable lives under one state root (`ONEVCS_HOME`, otherwise
26//! `~/.onevcs`): the registry document, the advisory locks and merge-queue state,
27//! the per-session workspaces, the event streams, and their artifacts.
28//!
29//! # Two surfaces over one decision
30//!
31//! [`run`] is the command line: it answers a process, with an exit code and a line
32//! of prose. A caller embedding this crate wants the decision itself, so the same
33//! operations answer values — [`publish`] hands back a [`Publication`],
34//! [`close_session`] the session it released, [`session`] what the repository side
35//! recorded, [`preserve`] what putting a branch on its origin did, [`recoverable`] the
36//! branches that still hold unpublished work, and [`EventStream`] the envelopes one
37//! session wrote. The command line is a rendering of those rather than a second path
38//! through them.
39//!
40//! **Every** command is: [`registered_identities`] and [`repositories`] are what
41//! `onevcs repos` prints, [`register_checkout`], [`resolve_repository`],
42//! [`publish_branch`], [`recover`], [`work_status`], [`import_branch`],
43//! [`integrate`], [`sync`], [`sweep`], [`rules_check`], [`read_artifact`],
44//! [`review_comments`], [`reply_to_comment`], [`change_standings`],
45//! [`review_standings`], [`retarget_change`], [`reopen_change`] and [`EventLines`] are
46//! the rest, and a command with no such operation fails this crate's own contract
47//! suite.
48
49#![warn(missing_docs)]
50
51use std::path::Path;
52
53mod app;
54mod base_merge;
55pub mod boundary;
56mod branch;
57pub mod branches;
58mod change;
59pub mod cli;
60mod closing;
61pub mod declaration;
62mod error;
63mod event;
64mod export;
65mod gate_run;
66mod gh;
67mod git;
68mod guidance;
69mod home;
70mod host;
71mod ids;
72mod import;
73mod integrate;
74mod label;
75mod landed;
76mod lock;
77mod merge_path;
78mod native_objects;
79mod native_refs;
80mod ops;
81mod policy;
82mod pool;
83mod preserve;
84mod probe;
85mod processes;
86pub mod provenance;
87mod providers;
88mod publish;
89mod publish_branch;
90mod publishing;
91mod queue;
92mod recover;
93mod recovery_cache;
94mod recovery_sessions;
95pub mod registry;
96mod release;
97pub mod releases;
98mod remainder;
99mod retire;
100mod review;
101pub mod rules;
102mod rules_apply;
103mod session;
104mod stack_base;
105mod standings;
106mod status;
107mod store;
108mod stream;
109mod sweep;
110#[cfg(feature = "testing")]
111#[doc(hidden)]
112pub mod testing;
113mod vcs;
114mod verdict;
115mod verified;
116mod vocabulary;
117mod workspace;
118pub mod workspaces;
119
120pub use boundary::{
121 boundary_schema, check_public_output, check_public_output_with_evidence, inspect_repository,
122 repository_boundary, BoundaryInput, BoundaryVerdict, RepositoryBoundary, TermMode, TermRule,
123 TermScope, Visibility,
124};
125pub use change::{ChangeDescription, SessionChange};
126pub use declaration::{
127 Declaration, DeclaredTarget, InstructionTemplate, RegistryId, RetiredArtifact,
128};
129pub use error::{Error, Result};
130pub use event::{
131 ArtifactId, ArtifactRef, Dimensions, Envelope, EventFilter, EventKind, EventMatcher, Labels,
132 MatchFields, Phase, PhaseOf, Source, VcsEvents, DIMENSIONS, RESERVED_LABELS, SOURCE_WORD,
133};
134pub use export::{
135 export, ExportRequest, Exported, EXPORT_AUTHOR_EMAIL, EXPORT_AUTHOR_NAME, EXPORT_SUBJECT,
136};
137pub use host::{
138 ChangeChecks, ChangeId, ChangeRequest, ChangeSpec, Check, CheckSource, CheckState, Description,
139 GitHub, Hosting, MergeOutcome, Mergeability, ProtectionSource, RemoteHost, RequiredChecks, Sha,
140};
141pub use import::{Imported, Source as ImportSource, Wrote};
142pub use integrate::{
143 BranchOutcome, Ending as IntegrationEnding, Outcome as Integration, Status as IntegrationStatus,
144};
145pub use landed::{Landed, LandingEvidence};
146pub use ops::{
147 import_branch, integrate, publish_branch, read_artifact, recover, register_checkout,
148 registered_identities, repositories, resolve_repository, rules_check, sweep, sync, work_status,
149 BasePush, BranchPublishRequest, CheckoutAudit, GateAudit, ImportRequest, IntegrateRequest,
150 MatchedRule, MergePathCoverage, RecoverRequest, RegisteredCheckout, RegisteredRepository,
151 Registration, RequiredChecksAnswer, ResolvedPolicy, ResolvedRepository, RulesCheck,
152 StatusReport, Sweeping, Synced, TrailerPrefixSource,
153};
154pub use pool::{
155 pool_maintain, pool_prune, pool_status, workspace_capacity, IdentityMaintenance,
156 IdentityOutcome, MaintainReport, MaintenanceOutcome, PoolStatus, PruneReport, SlotMaintenance,
157 SlotOutcome, SlotState, SlotStatus, WorkspaceCapacity,
158};
159pub use preserve::{Preservation, PreserveRequest, Preserved};
160pub use providers::Providers;
161pub use publish::{
162 ArmedMerge, DraftReason, FailureKind, Publication, PublicationCancellation, PublishOutcome,
163 PublishRequest, Retention, Subject, HOST_PREREQUISITE_MARKER,
164};
165pub use registry::Identity;
166pub use releases::{
167 Acknowledgement, Adoption, Baseline, BaselineRecord, DeclarationPolicy, DeclarationSource,
168 Discovery, Probe, ReleaseAnswer, ReleaseDefault, ReleaseMethod, ReleaseRule, ReleaseSource,
169 ReleaseStatus, ReleaseStyle, ReleaseTarget, ReleasesFile, RepositoryReleases,
170 SupersededRelease, TargetName, TargetRelease, TargetSource,
171};
172pub use retire::{
173 BranchHolder, BranchHolderKind, BranchRef, Derivation, FailedHolder, KeepReason, RetireMode,
174 RetireOutcome, RetirePass, RetireRequest, Retired, Retirement, RetirementClass,
175 RetirementPassReport, RetirementProof, RetirementQuery, SupersededBy, Supersession,
176 UnknownCause,
177};
178pub use review::{
179 reply_to_comment, review_comments, ChangeRef, CommentId, CommentKind, PostedReply, ReadCost,
180 ReadMarker, ReplyMarker, ReplyRequest, ReviewComment, ReviewRead,
181};
182pub use rules::MergePolicy;
183pub use rules_apply::{rules_apply, RulesApplied, RulesApplyRequest};
184pub use session::{
185 Detail, HeldBy, Holding, Lifecycle, LineChange, Liveness, NetNegative, OnOrigin, OpenConflict,
186 PreservedBranch, Provenance, Recoverable, Scope, Selection, Session, SessionHolder,
187 SessionRecord, SessionRequest, SessionToken,
188};
189pub use stack_base::{StackBase, StackBaseOutcome, StackBaseRequest, StackParent};
190pub use standings::{
191 change_standings, reopen_change, retarget_change, review_standings, ChangeStanding,
192 ChangeStandings, ChangeStandingsRequest, ChangeState, ChecksVerdict, ReopenRequest, Reopened,
193 RetargetOutcome, RetargetRequest, Retargeted, ReviewStandings, ReviewStandingsRequest,
194 ReviewTarget,
195};
196pub use stream::{EventLine, EventLines, EventStream};
197pub use sweep::Report as SweepReport;
198pub use vcs::{Git, Vcs};
199pub use workspaces::{first_matching, Bound, Span};
200
201/// A parsed absolute URL, re-exported so a caller needs no direct dependency on
202/// the parser this crate validates change-request URLs with.
203pub use url::Url;
204
205/// Run one parsed command line, returning the process exit code.
206///
207/// The binary is a thin shell over this, so a journey that drives `onevcs` and a
208/// caller that embeds it take the same path and cannot disagree about an exit code.
209pub fn run(cli: &cli::Cli) -> u8 {
210 run_with(cli, Providers::real())
211}
212
213/// Run one parsed command line against supplied implementations of the two
214/// interfaces, returning the process exit code.
215///
216/// [`run`] is this with [`Providers::real`], so nothing about the command's own
217/// behaviour changes with the implementations behind it: one code path, reached
218/// through [`Vcs`] and [`Hosting`] rather than through the types that satisfy them
219/// by default.
220pub fn run_with(cli: &cli::Cli, providers: Providers<'_>) -> u8 {
221 app::run(&cli.command, &providers)
222}
223
224/// Verify a session's work and publish it, returning what the publication did.
225///
226/// The library form of `onevcs publish`, and the reason it exists: a run answers
227/// with an exit code and prose, and a caller that has to branch on *what happened*
228/// can only parse the prose. [`Publication`] is that answer as a value — the policy
229/// it was taken under, whether it merged, opened a change request, queued one, or
230/// had nothing to publish, and the failure and what became of the branch when it
231/// did not land.
232///
233/// It runs through the seam, so a session a supplied [`Vcs`] opened publishes
234/// against a supplied [`Hosting`] with no git, no host, and no process.
235pub fn publish(
236 providers: &Providers<'_>,
237 token: &SessionToken,
238 request: &PublishRequest,
239) -> Result<Publication> {
240 providers.vcs.publish(token, request, providers.hosting)
241}
242
243/// [`publish`], stopping once `cancellation` says so.
244///
245/// What a caller that may have to abandon a publication — a manager cancelling or
246/// retrying the work it belongs to — runs instead of [`publish`], which is this with a
247/// cancellation nobody can trigger. Every phase that waits asks it at least once a
248/// second, whatever `ONEVCS_CHECKS_POLL_SECONDS` says: the watch of the host's checks,
249/// a draft's settle and the one after an early lift, the watch of a merge the host was
250/// armed to perform, and the wait for the identity's merge queue. A cancelled
251/// publication answers `Ok` with [`PublishOutcome::Failed`] of kind
252/// [`FailureKind::Cancelled`].
253///
254/// **Nothing is undone.** The branch stays on its remote, a change request is neither
255/// closed nor deleted, and the session is left open, so a later publication of it
256/// continues the same branch. A git command already running — the publishing push and
257/// its `pre-push` hook included — is never interrupted: it runs to completion, and the
258/// cancellation is observed once it returns.
259pub fn publish_with_cancellation(
260 providers: &Providers<'_>,
261 token: &SessionToken,
262 request: &PublishRequest,
263 cancellation: &dyn PublicationCancellation,
264) -> Result<Publication> {
265 providers
266 .vcs
267 .publish_with_cancellation(token, request, providers.hosting, cancellation)
268}
269
270/// Release a session's worktree and its occupancy lease, keeping its branch.
271///
272/// The library form of `onevcs session close`.
273pub fn close_session(providers: &Providers<'_>, token: &SessionToken) -> Result<Session> {
274 providers.vcs.close_session(token)
275}
276
277/// The session's own change request — the one [`publish`] would open or adopt for
278/// it, from its branch into the base its publication resolves — as the host holds
279/// it, or `None` when the host holds none.
280///
281/// The library form of `onevcs change show`. It never invents one: a session that
282/// has not published has no change request, and [`publish`] is what opens one.
283pub fn session_change(
284 providers: &Providers<'_>,
285 token: &SessionToken,
286) -> Result<Option<SessionChange>> {
287 change::session_change(providers, token)
288}
289
290/// Replace the description of the session's own change request — its body, and its
291/// title when one is given — and report the change as it stands after the write.
292///
293/// The library form of `onevcs change describe`. It refuses a session with no change
294/// request rather than opening one. A title is a [`Subject`] and is held to the
295/// repository's own `commit-msg` hook exactly as a publication's is, because it is
296/// the squash subject a `change-auto` merge lands under. The body is written
297/// verbatim and recorded as an artifact the `change-described` event names.
298pub fn describe_change(
299 providers: &Providers<'_>,
300 token: &SessionToken,
301 description: &ChangeDescription,
302) -> Result<SessionChange> {
303 change::describe_change(providers, token, description)
304}
305
306/// Mark the session's own change request ready for review, and report it as it
307/// stands.
308///
309/// The library form of `onevcs change ready`: the lift a publication carrying no
310/// [`DraftReason`] performs, asked for on its own. A change request that is not a
311/// draft is asked for nothing and reported as it stands.
312pub fn ready_change(providers: &Providers<'_>, token: &SessionToken) -> Result<SessionChange> {
313 change::ready_change(providers, token)
314}
315
316/// Every session recorded for one repository, live or not, in token order.
317///
318/// The library form of `onevcs session holders`, and the question a caller asks
319/// *before* it has a token: which sessions hold this repository's workspaces, which
320/// of them still have an owner, and which are the remains of a run that stopped.
321/// [`SessionHolder::token`] is what the rest of this surface takes, so a holder is
322/// a session to act on rather than a line to read.
323///
324/// It takes no [`Providers`] because there is nothing here for an implementation to
325/// answer: the holders are the records under this host's state root, which is where
326/// `Git` writes them and where the command reads them. A `Vcs` that keeps its
327/// sessions elsewhere therefore does not appear in this list — the same limit the
328/// command has, since the two are one path.
329///
330/// A record whose owner process has exited *and* whose run root no live process is
331/// working inside is forgotten as this answers rather than reported: nobody is left
332/// to be asked about it, and a list of those is what buries the one holder a caller
333/// had to act on. A session somebody is still working in is reported however long
334/// ago the command that opened it exited.
335pub fn session_holders(repo: &str) -> Result<Vec<SessionHolder>> {
336 workspace::holders(repo)
337}
338
339/// Put an unpublished branch on its identity's origin, under its own name, without
340/// publishing it.
341///
342/// The library form of `onevcs preserve`, and the half of a soft shutdown that makes
343/// the commits a host was about to lose survive it. **Nothing about it is a
344/// publication**: no change request is opened, no merge path is run, no base branch is
345/// touched, nothing is force-pushed, and no provenance marker or attestation is
346/// written — so a branch carrying an unattested incomplete-step marker is preserved
347/// exactly as it stands and still needs [`recoverable`]'s command afterwards.
348///
349/// The branch is looked for everywhere the landing verbs look, run clones included,
350/// so work a live dispatch committed a moment ago is found. A branch the origin
351/// already carries at that commit is [`Preservation::AlreadyOnOrigin`] and nothing is
352/// pushed; a location with no origin is [`Preservation::NoRemote`] and nothing is
353/// attempted. A refusal — non-fast-forward, credentials, a local-path origin that
354/// declines the ref — is an `Err` naming the identity, the branch, and git's own
355/// per-ref summary, so a caller preserving many branches reports that one and carries
356/// on.
357///
358/// It takes no [`Providers`] deliberately. Adding a method to the public [`Vcs`] trait
359/// would break every outside implementor of it, and this reaches git and nothing else
360/// — no host, no change request — so it sits beside [`workspace_capacity`] and
361/// [`pool_maintain`], which are free functions for the same reason.
362pub fn preserve(request: &PreserveRequest) -> Result<Preserved> {
363 preserve::run(&store::load()?, request)
364}
365
366/// Assemble, advance, or confirm a synthetic stack base, and push what moved.
367///
368/// The library form of `onevcs stack-base`. A stack base is a branch cut from
369/// `root` carrying a merge of each of `parents`, in order — what a change depending
370/// on several changes still in review is drafted against. A branch nothing carries is
371/// cut from the origin's `root` and merged with each parent's head
372/// ([`StackBaseOutcome::Created`]); one that exists — on the origin or held on this
373/// host — has the root's new head merged first and then each parent head it does not
374/// carry ([`StackBaseOutcome::Advanced`]), or is left as it is when it carries them
375/// all ([`StackBaseOutcome::Unchanged`]). Every merge is a merge commit carrying a
376/// `Stack-Base` trailer, and every push is forward-only: never a rebase, never a
377/// force, so a change drafted against it can always merge it forward.
378///
379/// A merge that conflicts is left in progress in a session opened on the branch —
380/// [`StackBaseOutcome::Conflicted`], exactly the [`OpenConflict`] `session open`
381/// leaves — and once that session commits it, the same request merges what is left
382/// and pushes.
383///
384/// No change request is opened and the repository's merge path is not run — a stack
385/// base merges branches each verified on its own change request — but the push is
386/// held to the public boundary like every other write to a remote. The host is asked
387/// through `providers.hosting` for that and nothing else; the repository side is
388/// git's own, as [`preserve`]'s is, and the session it may leave is a session of this
389/// host's state root.
390///
391/// Every call that answers emits one `stack-base-assembled` event on that session's
392/// stream; a call that fails emits none.
393pub fn assemble_stack_base(
394 providers: &Providers<'_>,
395 request: &StackBaseRequest,
396) -> Result<StackBase> {
397 stack_base::assemble(providers, request)
398}
399
400/// Every preserved-but-unpublished branch in scope, and what would land each.
401///
402/// The library form of `onevcs recoverable`, which had none: the enumeration was a
403/// private function over types this surface already exports, so the one consumer that
404/// wanted it had to spawn the binary and parse what it printed.
405///
406/// It answers exactly what the command answers without `--all`: a branch whose work
407/// reached the base is not one of these, because the row is read to be pasted and
408/// pasting one for finished work re-opens a change request for what the base already
409/// carries. [`Vcs::preserved`] is the wider question — every preserved branch,
410/// whatever became of its work — and it is on the seam because a supplied
411/// implementation answers it.
412///
413/// It takes no [`Providers`] for the reason [`session_holders`] does not: the branches
414/// are the ones under this host's own state root and in the checkouts its registry
415/// records, which is where `Git` keeps them and where the command reads them.
416pub fn recoverable(scope: &Scope) -> Result<Vec<Recoverable>> {
417 vcs::collect(scope, vcs::Reporting::UnpublishedOnly)
418}
419
420/// What the repository side recorded about a session.
421///
422/// The library form of the record every command that takes a token reads: which
423/// repository it belongs to, whether it is still open, and whether its branch
424/// carries an incomplete-step marker.
425pub fn session(providers: &Providers<'_>, token: &SessionToken) -> Result<SessionRecord> {
426 providers.vcs.session(token)
427}
428
429/// What one branch is, for the question of whether it may be deleted: `retirable`,
430/// `superseded-with-changes`, or `keep` with the one reason it is kept.
431///
432/// The library form of the classification `onevcs retire`, `onevcs reclaim` and
433/// `onevcs retire-finished` act on and `onevcs recoverable` reports per row. It
434/// deletes nothing and writes nothing: it reads every place this host keeps a copy of
435/// the branch, asks the origin where its copy is, and asks the host about a change
436/// request only where this host's own records do not decide it.
437///
438/// With [`RetirementQuery::repo`] `None` the branch is resolved across every
439/// registered identity, and refused, naming the candidates, where more than one holds
440/// it. A branch nothing holds any more is answered with the retirement recorded for
441/// it, and refused where none is.
442pub fn classify_retirement(
443 providers: &Providers<'_>,
444 query: &RetirementQuery,
445) -> Result<Retirement> {
446 retire::classify(providers.hosting, query)
447}
448
449/// Delete one branch everywhere this host holds it, where its class permits.
450///
451/// The library form of `onevcs retire` ([`RetireMode::Lossless`]), `onevcs
452/// reclaim` ([`RetireMode::Reclaim`]) and `onevcs reclaim --discard`
453/// ([`RetireMode::Discard`]). A class the mode does not permit deletes
454/// nothing and answers [`RetireOutcome::Kept`]; a branch nothing holds and whose
455/// retirement is recorded answers [`RetireOutcome::AlreadyRetired`]; a copy that could
456/// not be deleted answers [`RetireOutcome::Incomplete`], naming it, and a re-run
457/// finishes the job. A pool slot is returned and never removed.
458pub fn retire(providers: &Providers<'_>, request: &RetireRequest) -> Result<Retired> {
459 retire::retire_named(providers.hosting, request)
460}
461
462/// Retire every branch in scope that provably holds no work beyond its base.
463///
464/// The automatic pass: `onevcs retire-finished`, and what an engine calls from its
465/// idle maintenance. A change request opened from a branch whose landing is not
466/// recorded is asked about once first, through the same reconciliation a `status`
467/// read that meets a late merge makes. It acts on [`RetirementClass::Retirable`]
468/// alone — never on a branch superseded with changes — and the report lists every
469/// branch examined with its classification and what became of it.
470pub fn retire_finished(
471 providers: &Providers<'_>,
472 pass: &RetirePass,
473) -> Result<RetirementPassReport> {
474 retire::pass(Some(providers.hosting), pass, retire::Trigger::Pass)
475}
476
477/// Record that a branch was superseded by a retry that landed.
478///
479/// The library form of `onevcs supersede`. Idempotent: the same identity, branch,
480/// superseding branch and landing are recorded once. The record changes nothing
481/// about the branch itself; it is what makes one that still differs from its base
482/// `superseded-with-changes`, which `onevcs reclaim` acts on and nothing automatic
483/// does.
484///
485/// It takes no [`Providers`] for the reason [`preserve`] does not: it writes this
486/// host's own record, and there is nothing here for an implementation of either
487/// interface to answer.
488pub fn record_supersession(supersession: &Supersession) -> Result<()> {
489 retire::supersede(supersession)
490}
491
492/// What one repository releases, and what it adopts.
493///
494/// The library form of `onevcs release targets`. `repo` is the identity key, a
495/// registered alias, an origin URL, or a path, exactly as every other command takes
496/// one.
497///
498/// # Three layers, in one stated order
499///
500/// The answer is resolved from the repository's own declaration and this host's
501/// document together, and which of them decides is fixed rather than a consequence
502/// of read order:
503///
504/// 1. the **producer's** `release-targets.toml`, read from its publication
505/// checkout, contributes its targets in its own publication order;
506/// 2. a target this host's `releases.yml` names and the producer does not is
507/// **added** — a consumer standing in where nobody declared one;
508/// 3. a target both name is the **host's**, in the producer's position — an
509/// override, which is how a host runs a probe differently from the way the
510/// repository publishes it.
511///
512/// A rule saying [`DeclarationPolicy::Ignore`] drops layer 1 entirely, which is how
513/// a host says it does not consume what a repository declares. A host with no rules
514/// and repositories declaring nothing answers exactly what it always did.
515///
516/// [`RepositoryReleases::sources`] says which layer answered for each target, and
517/// [`RepositoryReleases::declaration`] says what the producer's half contributed —
518/// including the state that is neither a declaration nor its absence, a declaration
519/// this build could **not read**, which is never "this repository publishes
520/// nothing".
521///
522/// It takes no [`Providers`] for the reason [`session_holders`] does not: what a
523/// repository releases is this host's own configuration, the repository's own
524/// declaration, and this host's own record, and there is nothing here for an
525/// implementation of either interface to answer.
526pub fn release_targets(repo: &str) -> Result<RepositoryReleases> {
527 release::targets(&store::load()?, repo)
528}
529
530/// Every target one repository has, and what each of them has released right now.
531///
532/// The library form of `onevcs release discover`, and the answer a consumer that
533/// waits on a release actually needs: [`release_targets`] says what there is to wait
534/// for and this says what each of those has done, over the same three layers and in
535/// one resolution rather than one per target.
536///
537/// Each target is answered exactly as [`release_latest`] answers it, so the two
538/// cannot disagree — and [`ReleaseAnswer::NotAnswered`] stays distinct from
539/// [`ReleaseAnswer::NoRelease`] here as everywhere: a consumer holds on the first
540/// and acts on the second.
541///
542/// A repository with no targets answers with none rather than refusing, because
543/// "there is nothing to wait for" is a thing a consumer acts on — but what it is
544/// worth depends on [`RepositoryReleases::declaration`], which travels with it.
545pub fn release_discovery(repo: &str) -> Result<Discovery> {
546 release::discover(&store::load()?, repo)
547}
548
549/// What version of one target is released right now.
550///
551/// The library form of `onevcs release latest`. An **automated** target is answered
552/// by running its probe; a **human-step** target executes nothing at all and is
553/// answered from the newest acknowledgement across its landings, or
554/// [`ReleaseAnswer::NoRelease`] where nobody has recorded one.
555///
556/// [`ReleaseAnswer::NotAnswered`] is never [`ReleaseAnswer::NoRelease`]: a consumer
557/// holds on the first and acts on the second.
558pub fn release_latest(repo: &str, target: Option<&TargetName>) -> Result<ReleaseAnswer> {
559 release::latest(&store::load()?, repo, target)
560}
561
562/// Whether the release that carries one landed change has happened yet.
563///
564/// The library form of `onevcs release status`. `reference` is the four-spelling
565/// reference `onevcs status` takes: a change request's URL, a session token, a
566/// branch name, or a commit.
567pub fn release_status(reference: &str, target: Option<&TargetName>) -> Result<ReleaseStatus> {
568 release::status(
569 &store::load()?,
570 reference,
571 target,
572 Providers::real().hosting,
573 )
574}
575
576/// [`release_status`] against supplied implementations, which is what the command
577/// line runs.
578///
579/// The signature above is the contract's and takes no [`Providers`], so it answers
580/// through the real host. The command line was handed implementations and asks the
581/// same question of those — the one question a release read puts to a host, which is
582/// whether a change request a `checks-unsettled` close stopped watching has merged
583/// since.
584pub(crate) fn release_status_with(
585 providers: &Providers<'_>,
586 reference: &str,
587 target: Option<&TargetName>,
588) -> Result<ReleaseStatus> {
589 release::status(&store::load()?, reference, target, providers.hosting)
590}
591
592/// Whether one piece of work reached its base, and what says so.
593///
594/// The landing decision on its own, which is what [`release_status`] answers *before*
595/// it selects a release target — and the reason this exists beside it. A repository
596/// that declares no release target has no target to select, so `release_status`
597/// refuses it; a refusal is undecided rather than "not landed", and a caller that
598/// only wanted to know whether the work is on the base was left with no answer at
599/// all. This takes no target, reads no release configuration, and answers every
600/// repository the same way.
601///
602/// `reference` is the four-spelling reference [`release_status`] takes: a change
603/// request's URL, a session token, a branch name, or a commit. `repo` is the
604/// repository the question is narrowed to — an identity key, a registered alias, an
605/// origin URL, or a path, exactly as every other repository-taking read here takes one
606/// — and `None` resolves the repository from the reference alone. Naming one is how a
607/// branch name two identities both hold is asked about; a reference belonging to
608/// another identity is refused rather than answered under the name of the one asked
609/// about.
610///
611/// The tiers are decided in one private place — `landed::decide` — and nowhere else,
612/// so this, `onevcs status` and `onevcs recoverable` cannot come to disagree about one
613/// branch. Which tier decided travels inside the [`Landed`] value, so a caller acting
614/// on a `yes` can say what it acted on.
615///
616/// It takes no [`Providers`] for the reason [`release_targets`] does not: a landing is
617/// decided from the history of the repositories under this host's own state root, and
618/// there is nothing here for an implementation of either interface to answer.
619pub fn landing_status(reference: &str, repo: Option<&str>) -> Result<Landed> {
620 Ok(status::landing_of_within(&store::load()?, reference, repo, None)?.landed)
621}
622
623/// Record the release somebody says carries a landing.
624///
625/// The library form of `onevcs release acknowledge`. It accepts an automated target
626/// whose landing has no established probed baseline, and refuses one whose baseline
627/// is established. Human-step targets behave the same way regardless of baselines.
628/// It also refuses a reference that has not landed, a version
629/// that is not a semantic version, and a target the repository does not declare.
630///
631/// Recording the same version for the same landing again succeeds and changes
632/// nothing, re-reporting the existing record with its original timestamp and actor.
633/// Recording a *different* version is refused unless `supersede` is set, which
634/// writes the new version and keeps the previous one in the record's own history.
635pub fn acknowledge_release(
636 reference: &str,
637 target: &TargetName,
638 version: &str,
639 supersede: bool,
640) -> Result<Acknowledgement> {
641 release::acknowledge(&store::load()?, reference, target, version, supersede)
642}
643
644/// What one repository declares that it publishes.
645///
646/// The library form of `onevcs release declaration`, and the producer half of the
647/// release contract: [`release_targets`] answers what *this host* waits on for a
648/// repository, and this answers what the repository itself says it publishes. The
649/// two are different documents on purpose — see [`declaration`] — and deciding
650/// between them is nobody's job here.
651///
652/// `path` is either a repository's root or the `release-targets.toml` in it. A
653/// repository carrying no declaration is refused rather than answered with an empty
654/// one: "this repository publishes nothing" and "nobody has said what this
655/// repository publishes" are different answers, and a consumer that waits on a
656/// release acts differently on each.
657///
658/// It takes no [`Providers`] for the reason [`release_targets`] does not: a
659/// declaration is a file in a checkout, and there is nothing here for an
660/// implementation of either interface to answer.
661pub fn read_release_declaration(path: &Path) -> Result<Declaration> {
662 declaration::read(path)
663}
664
665/// Validate one release declaration's text, and answer what it declares.
666///
667/// The half of [`read_release_declaration`] that touches no filesystem: a caller
668/// that fetched a declaration from a host, or is about to write one, holds it to
669/// exactly the checks a repository's own file gets. `origin` is what the refusals
670/// name the document by — a path, a URL, or whatever the caller knows it as.
671pub fn validate_release_declaration(document: &str, origin: &str) -> Result<Declaration> {
672 declaration::parse(document, origin)
673}
674
675/// Render a declaration back as the TOML document it declares.
676///
677/// **A producer's comments are not preserved, because they were never read.** A
678/// declaration is mostly prose — the reasoning about what is a target and what is
679/// not — and this answers with the declaration alone, so writing the result over a
680/// producer's own file deletes that reasoning. It is for *producing* a declaration;
681/// editing one is a job for a person.
682///
683/// What it does promise is that the result reads as the same declaration, and it
684/// keeps that promise by holding what it was handed to a document's own checks
685/// first: a [`Declaration`] a caller *built* rather than read is refused here if it
686/// is one no repository could mean, rather than written out as a document nothing
687/// can read back.
688pub fn render_release_declaration(declared: &Declaration) -> Result<String> {
689 declaration::render(declared)
690}
691
692/// Which rung of the adoption chain one repository resolves to.
693///
694/// The repository rung where a rule sets one and the global rung otherwise. It
695/// never answers the node rung and never defaults to [`Adoption::Fast`] itself:
696/// those two rungs belong to the consumer, and a crate that answered all four would
697/// make the chain unreadable from either side.
698pub fn adoption_for(repo: &str) -> Result<Adoption> {
699 release::adoption(&store::load()?, repo)
700}