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