1#![allow(clippy::result_large_err)]
4
5use std::cell::{OnceCell, RefCell};
6use std::collections::{BTreeMap, BTreeSet};
7use std::path::PathBuf;
8use std::sync::Arc;
9
10use callisto_manifests::Manifest;
11use callisto_model::{CommandRunner, Ecosystem, PackageId, Version};
12use callisto_vcs::GitAccess;
13
14pub mod aggregate;
15pub mod apply;
16pub mod cascade;
17pub mod changed;
18pub mod commands;
19pub mod config;
20pub mod crosscheck;
21pub mod error;
22pub mod groups;
23pub mod identity;
24pub mod infer;
25pub mod locate;
26mod manifest_cache;
27pub(crate) mod matrix;
28pub mod napi;
29pub mod plan;
30pub mod resolver;
31pub mod tags;
32pub mod toposort;
33pub mod walk;
34
35pub use aggregate::{aggregate, load_changesets, Aggregation, LoadedChangeset, NamedBy};
36pub use apply::{apply_version_plan, ApplyOptions, ApplyOutcome};
37pub use cascade::{
38 cascade_action, coverage, rewrite_spec, run_cascade, CascadeDecision, CascadeInput, CascadeOutcome, DepWriteTarget,
39 RewriteKey, RewriteOutcome, SpecRewrite,
40};
41pub use config::{load as load_config, GroupDef, GroupTable, ResolvedConfig};
42pub use error::{ConfigError, GraphError};
43pub use groups::{fixed_group_target, pre_mutation_checks, GroupCheckOutcome};
44pub use identity::{IdentityIndex, IdentityResolver};
45pub use infer::{InferenceOutcome, InferenceWindowSpec, NoInference, SeverityInference};
46pub use locate::{find_workspace_root, IgnoreWalkLocator, LocateError, ProjectLocator};
47pub use napi::{napi_drift, role_to_triple, triple_to_role, NapiTargetsIndex};
48pub use plan::{PlannedBump, VersionPlan, VersionWriteTarget};
49pub use resolver::{DependencyResolver, ManifestWalkResolver};
50pub use tags::{last_tag_for, TagIndex};
51pub use toposort::toposort_impl;
52
53pub struct Workspace<'a, R: CommandRunner, D: DependencyResolver = ManifestWalkResolver> {
54 pub root: PathBuf,
55 pub config: ResolvedConfig,
56 pub graph: D,
57 pub tags: OnceCell<TagIndex>,
70 pub git: OnceCell<GitAccess<'a>>,
81 pub runner: &'a R,
82 pub manifest_cache: RefCell<BTreeMap<PathBuf, Arc<dyn Manifest>>>,
90 pub identity: IdentityIndex,
91}
92
93impl<'a, R: CommandRunner> Workspace<'a, R, ManifestWalkResolver> {
94 pub fn load<L: ProjectLocator>(root: PathBuf, locator: &L, runner: &'a R) -> Result<Self, GraphError> {
95 let mut config = config::load(&root)?;
96 let manifest_cache: RefCell<BTreeMap<PathBuf, Arc<dyn Manifest>>> = RefCell::new(BTreeMap::new());
97 let graph = ManifestWalkResolver::build(&root, locator, runner, &config, &manifest_cache)?;
98
99 config.groups = GroupTable::resolve(&config.raw_groups, graph.identity())?;
100
101 {
102 let mut by_name: BTreeMap<String, Vec<(PackageId, BTreeSet<Ecosystem>)>> = BTreeMap::new();
103 let mut ecosystems_by_id: BTreeMap<(String, PackageId), BTreeSet<Ecosystem>> = BTreeMap::new();
104 for ((eco, name), id) in &graph.identity().prefixed {
105 ecosystems_by_id
106 .entry((name.clone(), id.clone()))
107 .or_default()
108 .insert(*eco);
109 }
110 for ((name, id), ecos) in ecosystems_by_id {
111 by_name.entry(name).or_default().push((id, ecos));
112 }
113 config.promoted_siblings = by_name.into_iter().filter(|(_, ids)| ids.len() >= 2).collect();
114 }
115
116 let identity = graph.identity().clone();
117
118 Ok(Workspace {
119 root,
120 config,
121 graph,
122 tags: OnceCell::new(),
123 git: OnceCell::new(),
124 runner,
125 manifest_cache,
126 identity,
127 })
128 }
129}
130
131impl<'a, R: CommandRunner, D: DependencyResolver> Workspace<'a, R, D> {
132 pub fn tags(&self) -> Result<&TagIndex, GraphError> {
137 if let Some(existing) = self.tags.get() {
138 return Ok(existing);
139 }
140 let built = TagIndex::build(self.git_access(), &self.graph, &self.config)?;
141 self.tags.set(built).ok();
148 Ok(self
149 .tags
150 .get()
151 .expect("tags was just set above, or already set by a prior call"))
152 }
153
154 pub fn git_access(&self) -> &GitAccess<'a> {
163 self.git.get_or_init(|| GitAccess::discover(&self.root, self.runner))
164 }
165
166 pub fn base_versions(&self) -> Result<BTreeMap<PackageId, Version>, GraphError> {
167 let cargo_workspace = if self.root.join("Cargo.toml").exists() {
168 if let Ok(resolver) = callisto_manifests::WorkspaceCargoResolver::load(&self.root.join("Cargo.toml")) {
169 resolver.inheritance().ok().map(std::sync::Arc::new)
170 } else {
171 None
172 }
173 } else {
174 None
175 };
176 let npm_workspace_kind = callisto_manifests::detect_npm_workspace_kind(&self.root).ok().flatten();
177 let ctx = callisto_manifests::OpenContext {
178 workspace_root: &self.root,
179 cargo_workspace,
180 npm_workspace_kind,
181 };
182
183 let mut versions = BTreeMap::new();
184 for pkg in self.graph.packages() {
185 let mut found_version = None;
186 for decl in &pkg.manifests {
187 if decl.role == callisto_model::ManifestRole::Canonical {
188 let handle = manifest_cache::open_cached(&self.manifest_cache, decl, &ctx)?;
189 let v = handle.current_version()?;
190 found_version = Some(v);
191 break;
192 }
193 }
194 if let Some(version) = found_version {
195 versions.insert(pkg.id.clone(), version);
196 } else {
197 return Err(GraphError::Manifest(callisto_model::ManifestError::MissingField {
198 path: pkg.manifests.first().map(|m| m.path.clone()).unwrap_or_default(),
199 field: "version",
200 }));
201 }
202 }
203 Ok(versions)
204 }
205
206 pub fn pre_json_key<'b>(&self, id: &'b PackageId) -> Result<&'b str, GraphError> {
207 Ok(id.name())
208 }
209
210 pub fn initial_versions(&self) -> Result<Vec<(String, Version)>, GraphError> {
211 let base = self.base_versions()?;
212 Ok(base.into_iter().map(|(id, v)| (id.name().to_string(), v)).collect())
213 }
214}