1pub mod apply;
2pub mod diff;
3pub mod filter;
4pub mod mutation;
5pub mod plan;
6pub mod provider;
7pub mod rewrite;
8pub mod target;
9pub mod types;
10mod upgrades;
11mod validate;
12
13use std::collections::BTreeMap;
14use std::collections::HashSet;
15use std::path::Path;
16
17use crate::config::{Config, EffectiveConfig, LocalConfig, Settings};
18use crate::diagnostic::{Diagnostic, DiagnosticCollector, LossinessMode};
19use crate::error::MarsError;
20use crate::fs::FileLock;
21use crate::hash;
22use crate::lock::{CANONICAL_TARGET_ROOT, ItemId, ItemKind};
23use crate::lock::{LockFile, LockIndex};
24use crate::resolve::{ResolveOptions, ResolvedGraph};
25use crate::source::GlobalCache;
26use crate::sync::apply::ApplyResult;
27pub use crate::sync::apply::SyncOptions;
28use crate::sync::target::{TargetItem, TargetState};
29use crate::types::managed_cmd;
30use crate::types::{ContentHash, DestPath, MarsContext, SourceId, SourceName, SourceOrigin};
31use crate::validate::ValidationWarning;
32
33pub use crate::sync::mutation::{ConfigMutation, DependencyUpsertChange, apply_config_mutation};
35
36#[derive(Debug)]
38pub struct SyncReport {
39 pub applied: ApplyResult,
40 pub pruned: Vec<apply::ActionOutcome>,
41 pub diagnostics: Vec<Diagnostic>,
42 pub dependency_changes: Vec<DependencyUpsertChange>,
43 pub upgrades_available: usize,
44 pub target_outcomes: Vec<crate::target_sync::TargetSyncOutcome>,
46 pub dry_run: bool,
48 pub native_emitted: Vec<(String, String)>,
52 pub native_removed: Vec<(String, String)>,
55}
56
57impl SyncReport {
58 pub fn has_conflicts(&self) -> bool {
60 self.applied
61 .outcomes
62 .iter()
63 .any(|o| matches!(o.action, apply::ActionTaken::Conflicted))
64 }
65}
66
67#[derive(Debug, Clone)]
69pub struct SyncRequest {
70 pub resolution: ResolutionMode,
72 pub mutation: Option<ConfigMutation>,
74 pub options: SyncOptions,
76 pub lossiness_mode: LossinessMode,
79}
80
81#[derive(Debug, Clone)]
83pub enum ResolutionMode {
84 Normal,
86 Maximize {
89 targets: HashSet<SourceName>,
90 bump: bool,
91 },
92}
93
94pub(crate) struct LoadedConfig {
101 pub config: Config,
102 pub local: LocalConfig,
103 pub effective: EffectiveConfig,
104 pub old_lock: LockFile,
105 pub dependency_changes: Vec<DependencyUpsertChange>,
106 #[allow(dead_code)]
108 pub sync_lock: FileLock,
109}
110
111pub(crate) struct ResolvedState {
113 pub loaded: LoadedConfig,
114 pub graph: ResolvedGraph,
115 pub upgrades_available: usize,
116}
117
118pub(crate) struct TargetedState {
120 pub resolved: ResolvedState,
121 pub target: TargetState,
122 pub warnings: Vec<ValidationWarning>,
123}
124
125pub(crate) struct PlannedState {
127 pub targeted: TargetedState,
128 pub plan: plan::SyncPlan,
129}
130
131pub(crate) struct AppliedState {
133 pub planned: PlannedState,
134 pub applied: ApplyResult,
135}
136
137pub(crate) struct SyncedState {
139 pub applied: AppliedState,
140 pub target_outcomes: Vec<crate::target_sync::TargetSyncOutcome>,
141 pub config_entries: BTreeMap<String, BTreeMap<String, crate::lock::ConfigEntryRecord>>,
142 pub compiled_native_outputs: Vec<crate::lock::CompiledNativeOutput>,
143 pub removed_native_outputs: Vec<crate::compiler::RemovedNativeOutput>,
144}
145
146pub fn execute(ctx: &MarsContext, request: &SyncRequest) -> Result<SyncReport, MarsError> {
150 validate_request(request)?;
151 let mut diag = DiagnosticCollector::with_lossiness_mode(request.lossiness_mode);
152 let ir = crate::reader::read(ctx, request, &mut diag)?;
153 crate::compiler::compile(ctx, ir, request, &mut diag)
154}
155
156pub(crate) fn load_config(
163 ctx: &MarsContext,
164 request: &SyncRequest,
165 diag: &mut DiagnosticCollector,
166) -> Result<LoadedConfig, MarsError> {
167 let project_root = &ctx.project_root;
168 let mars_dir = project_root.join(".mars");
169
170 std::fs::create_dir_all(mars_dir.join("cache"))?;
171
172 let lock_path = mars_dir.join("sync.lock");
174 let _sync_lock = crate::fs::FileLock::acquire(&lock_path)?;
175
176 let mut config = match crate::config::load(project_root) {
178 Ok(config) => config,
179 Err(err) if mutation::is_config_not_found(&err) && request.mutation.is_some() => Config {
180 settings: Settings::default(),
181 ..Config::default()
182 },
183 Err(err) => return Err(err),
184 };
185
186 let dependency_changes = if let Some(m) = &request.mutation {
188 mutation::apply_mutation(&mut config, m)?
189 } else {
190 Vec::new()
191 };
192
193 let mut local = crate::config::load_local(project_root)?;
195 if let Some(m) = &request.mutation {
196 mutation::apply_local_mutation(&mut local, m);
197 }
198
199 let (effective, config_diagnostics) =
201 crate::config::merge_with_root(config.clone(), local.clone(), project_root)?;
202 diag.extend(config_diagnostics);
203
204 let (old_lock, lock_diagnostics) = crate::lock::load_with_diagnostics(project_root)?;
206 diag.extend(lock_diagnostics);
207
208 Ok(LoadedConfig {
209 config,
210 local,
211 effective,
212 old_lock,
213 dependency_changes,
214 sync_lock: _sync_lock,
215 })
216}
217
218pub(crate) fn resolve_graph(
220 ctx: &MarsContext,
221 mut loaded: LoadedConfig,
222 request: &SyncRequest,
223 diag: &mut DiagnosticCollector,
224) -> Result<ResolvedState, MarsError> {
225 validate_targets(&request.resolution, &loaded.effective)?;
226
227 let cache = GlobalCache::new()?;
228 let source_provider = provider::RealSourceProvider::new(&cache, &ctx.project_root);
229 let resolve_options = to_resolve_options(&request.resolution, request.options.frozen)
230 .with_staging_root(ctx.project_root.join(".mars/staging"));
231 let graph = crate::resolve::resolve(
232 &loaded.effective,
233 &source_provider,
234 Some(&loaded.old_lock),
235 &resolve_options,
236 diag,
237 )?;
238 let upgrades_available = if request.options.frozen || !request.options.check_upgrades {
239 0
240 } else {
241 upgrades::count_compatible_upgrades(&graph, &source_provider, diag)
242 };
243
244 let bump_entries = planned_bump_entries(&loaded.config, &graph, &request.resolution);
245 if !bump_entries.is_empty() {
246 let bump_changes = mutation::apply_mutation(
247 &mut loaded.config,
248 &ConfigMutation::BatchUpsert(bump_entries),
249 )?;
250 loaded.dependency_changes.extend(bump_changes);
251 }
252
253 let _ = crate::models::merged_model_aliases(
255 &graph,
256 &loaded.effective,
257 &loaded.config,
258 &loaded.local,
259 diag,
260 );
261
262 Ok(ResolvedState {
263 loaded,
264 graph,
265 upgrades_available,
266 })
267}
268
269pub(crate) fn build_target(
275 ctx: &MarsContext,
276 resolved: ResolvedState,
277 local_items: Vec<crate::local_source::LocalDiscoveredItem>,
278 request: &SyncRequest,
279 diag: &mut DiagnosticCollector,
280) -> Result<TargetedState, MarsError> {
281 let mars_dir = ctx.project_root.join(".mars");
283 let managed_root = &mars_dir;
284
285 let (mut target_state, renames, collision_renames) =
287 target::build_with_collisions_and_diag(&resolved.graph, &resolved.loaded.effective, diag)?;
288
289 let local_source_name: SourceName = SourceOrigin::LocalPackage.to_string().into();
290 let local_source_id = SourceId::Path {
291 canonical: dunce::canonicalize(&ctx.project_root)
292 .unwrap_or_else(|_| ctx.project_root.clone()),
293 subpath: None,
294 };
295 let old_lock_index = LockIndex::new(&resolved.loaded.old_lock);
296
297 for item in local_items {
298 let staging_root = ctx.project_root.join(".mars/staging");
299 let item_key = format!("{}:{}", item.discovered.id.kind, item.discovered.id.name);
300 let staged_path = crate::staging::stage_local_item(
301 &item.disk_path(),
302 item.discovered.id.kind,
303 crate::dialect::Dialect::resolve_local(None, &item.root),
304 &resolved.loaded.effective.skills,
305 &staging_root,
306 &item_key,
307 (item.discovered.id.kind == ItemKind::Skill).then(|| item.discovered.id.name.as_str()),
308 diag,
309 )?;
310 let source_path = staged_path;
311 let is_flat_skill = item.discovered.id.kind == ItemKind::Skill
312 && item.discovered.source_path == Path::new(".");
313 let source_hash = if is_flat_skill {
314 ContentHash::from(hash::compute_skill_hash_filtered(
315 &source_path,
316 crate::fs::FLAT_SKILL_EXCLUDED_TOP_LEVEL,
317 )?)
318 } else {
319 ContentHash::from(hash::compute_hash(&source_path, item.discovered.id.kind)?)
320 };
321 if item.discovered.id.kind == ItemKind::Agent
322 && let Err(message) =
323 crate::target::validate_agent_filename(item.discovered.id.name.as_str())
324 {
325 diag.error_with_category(
326 "invalid-agent-filename",
327 format!("{message}; skipping local agent"),
328 crate::diagnostic::DiagnosticCategory::Validation,
329 );
330 continue;
331 }
332 let dest_path =
333 default_dest_path(item.discovered.id.kind, item.discovered.id.name.as_str());
334
335 if let Some(existing) = target_state.items.shift_remove(&dest_path)
336 && existing.source_hash != source_hash
337 {
338 diag.warn(
339 "local-shadow",
340 format!(
341 "local {} `{}` shadows dependency `{}` {} `{}`",
342 item.discovered.id.kind,
343 item.discovered.id.name,
344 existing.source_name,
345 existing.id.kind,
346 existing.id.name
347 ),
348 );
349 }
350
351 let disk_path = dest_path.resolve(managed_root);
352 if !old_lock_index.contains_output(CANONICAL_TARGET_ROOT, &dest_path)
353 && disk_path.symlink_metadata().is_ok()
354 {
355 diag.warn(
356 "unmanaged-collision",
357 format!(
358 "local {} `{}` collides with unmanaged path `{}` — leaving existing content untouched",
359 item.discovered.id.kind, item.discovered.id.name, dest_path
360 ),
361 );
362 continue;
363 }
364
365 target_state.items.insert(
366 dest_path.clone(),
367 TargetItem {
368 id: ItemId {
369 kind: item.discovered.id.kind,
370 name: item.discovered.id.name.clone(),
371 },
372 source_name: local_source_name.clone(),
373 origin: SourceOrigin::LocalPackage,
374 source_id: local_source_id.clone(),
375 source_path,
376 dest_path,
377 source_hash,
378 is_flat_skill,
379 rewritten_content: None,
380 },
381 );
382 }
383
384 let unmanaged_collisions = target::check_unmanaged_collisions(
386 managed_root,
387 &resolved.loaded.old_lock,
388 &target_state,
389 request.options.force,
390 );
391 for collision in &unmanaged_collisions {
392 diag.warn(
393 "unmanaged-collision",
394 format!(
395 "source `{}` collides with unmanaged path `{}` — leaving existing content untouched",
396 collision.source_name, collision.path
397 ),
398 );
399 target_state.items.shift_remove(&collision.path);
400 }
401
402 let rename_index = rewrite::RenameIndex::new(&renames, &collision_renames, &target_state);
404 if !rename_index.is_empty() {
405 let dep_precedence: Vec<SourceName> = resolved
406 .loaded
407 .effective
408 .dependencies
409 .keys()
410 .cloned()
411 .collect();
412 let rewrite_warnings = rewrite::apply_renames(
413 &mut target_state,
414 &rename_index,
415 &resolved.graph,
416 &dep_precedence,
417 )?;
418 for w in &rewrite_warnings {
419 diag.warn("rewrite-warning", w.to_string());
420 }
421 }
422
423 validate::warn_config_dangles_after_rename(
424 &renames,
425 &collision_renames,
426 &target_state,
427 &resolved.loaded,
428 diag,
429 );
430
431 validate::validate_skill_frontmatter_in_target(&target_state, diag);
432
433 let warnings = validate::validate_skill_refs(&target_state);
435
436 Ok(TargetedState {
437 resolved,
438 target: target_state,
439 warnings,
440 })
441}
442
443pub(crate) fn create_plan(
445 ctx: &MarsContext,
446 targeted: TargetedState,
447 request: &SyncRequest,
448 diag: &mut DiagnosticCollector,
449) -> Result<PlannedState, MarsError> {
450 let mars_dir = ctx.project_root.join(".mars");
452 let managed_root = &mars_dir;
453 let cache_bases_dir = mars_dir.join("cache").join("bases");
454
455 let sync_diff = diff::compute(
457 managed_root,
458 &targeted.resolved.loaded.old_lock,
459 &targeted.target,
460 request.options.force,
461 )?;
462
463 if !request.options.force {
464 for entry in &sync_diff.items {
465 if let diff::DiffEntry::LocalModified { target, .. } = entry {
466 diag.warn(
467 "disk-lock-divergent",
468 format!(
469 "{} diverged from mars.lock checksum; preserving local content (run `{cmd1}` or `{cmd2}` to reset)",
470 target.dest_path,
471 cmd1 = managed_cmd("mars sync --force"),
472 cmd2 = managed_cmd("mars repair"),
473 ),
474 );
475 }
476 }
477 }
478
479 let sync_plan = plan::create(&sync_diff, &request.options, &cache_bases_dir, diag);
481
482 Ok(PlannedState {
483 targeted,
484 plan: sync_plan,
485 })
486}
487
488pub(crate) fn check_frozen_gate(planned: &PlannedState) -> Result<(), MarsError> {
490 let has_changes = planned.plan.actions.iter().any(|a| {
491 !matches!(
492 a,
493 plan::PlannedAction::Skip { .. } | plan::PlannedAction::KeepLocal { .. }
494 )
495 });
496 if has_changes {
497 return Err(MarsError::FrozenViolation {
498 message: "lock file would change but --frozen is set".into(),
499 });
500 }
501 Ok(())
502}
503
504pub(crate) fn apply_plan(
506 ctx: &MarsContext,
507 planned: PlannedState,
508 request: &SyncRequest,
509) -> Result<AppliedState, MarsError> {
510 let project_root = &ctx.project_root;
511 let mars_dir = project_root.join(".mars");
512 let cache_bases_dir = mars_dir.join("cache").join("bases");
513
514 let has_bump_version_changes =
515 has_version_changes(&planned.targeted.resolved.loaded.dependency_changes)
516 && matches!(
517 request.resolution,
518 ResolutionMode::Maximize { bump: true, .. }
519 );
520 let has_mutation = request.mutation.is_some() || has_bump_version_changes;
521
522 if has_mutation && !request.options.dry_run {
524 match &request.mutation {
525 Some(ConfigMutation::SetOverride { .. } | ConfigMutation::ClearOverride { .. }) => {
526 crate::config::save_local(project_root, &planned.targeted.resolved.loaded.local)?;
527 }
528 Some(
529 ConfigMutation::UpsertDependency { .. }
530 | ConfigMutation::BatchUpsert(..)
531 | ConfigMutation::RemoveDependency { .. }
532 | ConfigMutation::SetRename { .. },
533 ) => {
534 crate::config::save(project_root, &planned.targeted.resolved.loaded.config)?;
535 }
536 None => {
537 if has_bump_version_changes {
538 crate::config::save(project_root, &planned.targeted.resolved.loaded.config)?;
539 }
540 }
541 }
542 }
543
544 let applied = apply::execute(&mars_dir, &planned.plan, &request.options, &cache_bases_dir)?;
548
549 Ok(AppliedState { planned, applied })
550}
551
552pub(crate) fn sync_targets(
558 ctx: &MarsContext,
559 applied: AppliedState,
560 request: &SyncRequest,
561 agent_surface_policy: crate::compiler::AgentSurfacePolicy,
562 diag: &mut DiagnosticCollector,
563) -> SyncedState {
564 if request.options.dry_run {
565 return SyncedState {
566 applied,
567 target_outcomes: Vec::new(),
568 config_entries: BTreeMap::new(),
569 compiled_native_outputs: Vec::new(),
570 removed_native_outputs: Vec::new(),
571 };
572 }
573
574 let mars_dir = ctx.project_root.join(".mars");
575 let targets = applied
576 .planned
577 .targeted
578 .resolved
579 .loaded
580 .effective
581 .settings
582 .managed_targets();
583 let old_lock = &applied.planned.targeted.resolved.loaded.old_lock;
584
585 let filtered_outcomes;
586 let orphan_preserve_paths;
587 let (target_outcomes_source, orphan_preserve) = match &agent_surface_policy {
588 crate::compiler::AgentSurfacePolicy::SuppressAll => {
589 filtered_outcomes = crate::compiler::suppress_agent_outcomes(&applied.applied.outcomes);
590 (&filtered_outcomes, None)
591 }
592 crate::compiler::AgentSurfacePolicy::EmitSelective(spec) => {
593 orphan_preserve_paths =
594 crate::compiler::selective_native_orphan_preserve_paths(old_lock, spec);
595 filtered_outcomes = crate::compiler::omit_agent_outcomes(&applied.applied.outcomes);
596 (&filtered_outcomes, Some(&orphan_preserve_paths))
597 }
598 crate::compiler::AgentSurfacePolicy::EmitAll => (&applied.applied.outcomes, None),
599 };
600
601 let target_sync_ctx = crate::target_sync::TargetSyncContext {
602 old_lock,
603 force: request.options.force,
604 collision_hint: crate::surface_ownership::CollisionAdoptHint::SyncForce,
605 orphan_preserve_paths: orphan_preserve,
606 };
607 let target_outcomes = crate::target_sync::sync_managed_targets(
608 &ctx.project_root,
609 &mars_dir,
610 &targets,
611 target_outcomes_source,
612 &target_sync_ctx,
613 diag,
614 );
615
616 SyncedState {
617 applied,
618 target_outcomes,
619 config_entries: BTreeMap::new(),
620 compiled_native_outputs: Vec::new(),
621 removed_native_outputs: Vec::new(),
622 }
623}
624
625pub(crate) fn finalize(
629 ctx: &MarsContext,
630 state: SyncedState,
631 request: &SyncRequest,
632 diag: &mut DiagnosticCollector,
633) -> Result<SyncReport, MarsError> {
634 let project_root = &ctx.project_root;
635 let old_lock = &state.applied.planned.targeted.resolved.loaded.old_lock;
636 let graph = &state.applied.planned.targeted.resolved.graph;
637 let native_removed: Vec<(String, String)> = state.removed_native_outputs.clone();
640 let native_emitted: Vec<(String, String)> = state
641 .compiled_native_outputs
642 .iter()
643 .filter(|out| crate::lock::native_output_is_new_or_changed(old_lock, out))
644 .map(|out| (out.target_root.clone(), out.dest_path.clone()))
645 .collect();
646
647 if !request.options.dry_run {
649 let dep_models = crate::models::declaration_ordered_dep_models(
650 graph,
651 &state.applied.planned.targeted.resolved.loaded.effective,
652 );
653 let mut dep_model_aliases = crate::models::dependency_alias_snapshot(&dep_models);
654 dep_model_aliases.sort_keys();
655
656 let mut new_lock = crate::lock::build(
657 graph,
658 &state.applied.applied,
659 old_lock,
660 state.config_entries,
661 )?;
662 new_lock.dependency_model_aliases = dep_model_aliases;
663 crate::lock::apply_target_sync_outputs(&mut new_lock, &state.target_outcomes);
664 crate::lock::apply_removed_native_outputs(&mut new_lock, &state.removed_native_outputs);
665 crate::lock::apply_compiled_native_outputs(&mut new_lock, &state.compiled_native_outputs);
666 if let Some(warning) =
667 crate::compiler::persist_lock_then_native_agent_manifest(project_root, &new_lock)?
668 {
669 diag.warn("native-agent-manifest-write", warning);
670 }
671
672 let mars_path = ctx.project_root.join(".mars");
676 let ttl = state
677 .applied
678 .planned
679 .targeted
680 .resolved
681 .loaded
682 .effective
683 .settings
684 .models_cache_ttl_hours;
685 let refresh = crate::models::resolve_models_refresh_control(
686 request.options.refresh_models,
687 request.options.no_refresh_models,
688 )?;
689 match crate::models::ensure_fresh(&mars_path, ttl, refresh.catalog_mode) {
690 Ok((_, crate::models::RefreshOutcome::StaleFallback { reason })) => {
691 diag.warn(
692 "models-cache-refresh",
693 format!("using stale models cache: {reason}"),
694 );
695 }
696 Ok((_, crate::models::RefreshOutcome::Offline)) => {}
697 Ok(_) => {}
698 Err(err) => {
699 diag.warn(
700 "models-cache-refresh",
701 format!("failed to refresh models cache: {err}"),
702 );
703 }
704 }
705 }
706
707 for w in &state.applied.planned.targeted.warnings {
708 match w {
709 ValidationWarning::MissingSkill {
710 agent,
711 skill_name,
712 suggestion,
713 } => {
714 let msg = match suggestion {
715 Some(s) => format!(
716 "agent `{}` references missing skill `{}` (did you mean `{}`?)",
717 agent.name, skill_name, s
718 ),
719 None => {
720 format!(
721 "agent `{}` references missing skill `{}`",
722 agent.name, skill_name
723 )
724 }
725 };
726 diag.warn("missing-skill", msg);
727 }
728 }
729 }
730 let dependency_changes = state
731 .applied
732 .planned
733 .targeted
734 .resolved
735 .loaded
736 .dependency_changes;
737 let upgrades_available = state.applied.planned.targeted.resolved.upgrades_available;
738
739 let diagnostics = diag.drain();
740
741 Ok(SyncReport {
742 applied: state.applied.applied,
743 pruned: Vec::new(),
744 diagnostics,
745 dependency_changes,
746 upgrades_available,
747 target_outcomes: state.target_outcomes,
748 dry_run: request.options.dry_run,
749 native_emitted,
750 native_removed,
751 })
752}
753
754fn default_dest_path(kind: ItemKind, name: &str) -> DestPath {
755 match kind {
756 ItemKind::Agent => DestPath::from(format!("agents/{name}.md")),
757 ItemKind::Skill => DestPath::from(format!("skills/{name}")),
758 ItemKind::Hook => DestPath::from(format!("hooks/{name}")),
759 ItemKind::McpServer => DestPath::from(format!("mcp/{name}")),
760 ItemKind::BootstrapDoc => DestPath::from(format!("bootstrap/{name}/BOOTSTRAP.md")),
761 }
762}
763
764fn validate_request(request: &SyncRequest) -> Result<(), MarsError> {
765 if request.options.frozen && matches!(request.resolution, ResolutionMode::Maximize { .. }) {
766 return Err(MarsError::InvalidRequest {
767 message:
768 "cannot use --frozen with upgrade (frozen locks versions; upgrade maximizes them)"
769 .to_string(),
770 });
771 }
772
773 if request.options.frozen && request.mutation.is_some() {
774 return Err(MarsError::InvalidRequest {
775 message:
776 "cannot modify config in --frozen mode (config change would require lock update)"
777 .to_string(),
778 });
779 }
780
781 Ok(())
782}
783
784fn validate_targets(
785 resolution: &ResolutionMode,
786 effective: &EffectiveConfig,
787) -> Result<(), MarsError> {
788 if let ResolutionMode::Maximize { targets, .. } = resolution {
789 for name in targets {
790 if !effective.dependencies.contains_key(name) {
791 return Err(MarsError::Source {
792 source_name: name.to_string(),
793 message: format!("dependency `{name}` not found in mars.toml"),
794 });
795 }
796 }
797 }
798
799 Ok(())
800}
801
802fn to_resolve_options(mode: &ResolutionMode, frozen: bool) -> ResolveOptions {
803 if frozen {
804 return ResolveOptions::frozen();
805 }
806
807 match mode {
808 ResolutionMode::Normal => ResolveOptions::sync(),
809 ResolutionMode::Maximize { targets, bump } => {
810 ResolveOptions::upgrade(targets.clone(), *bump)
811 }
812 }
813}
814
815fn planned_bump_entries(
816 config: &Config,
817 graph: &ResolvedGraph,
818 mode: &ResolutionMode,
819) -> Vec<(SourceName, crate::config::DependencyEntry)> {
820 let ResolutionMode::Maximize {
821 targets,
822 bump: true,
823 } = mode
824 else {
825 return Vec::new();
826 };
827
828 config
829 .dependencies
830 .iter()
831 .filter_map(|(name, entry)| {
832 if !targets.is_empty() && !targets.contains(name) {
833 return None;
834 }
835 entry.url.as_ref()?;
837 let node = graph.nodes.get(name)?;
838 let resolved_version = node.resolved_ref.version.as_ref()?;
839 let resolved_tag = node.resolved_ref.version_tag.as_ref()?;
840 if !constraint_needs_bump(entry.version.as_deref(), resolved_version) {
841 return None;
842 }
843 if entry.version.as_deref() == Some(resolved_tag.as_str()) {
844 return None;
845 }
846 let mut bumped = entry.clone();
847 bumped.version = Some(resolved_tag.clone());
848 Some((name.clone(), bumped))
849 })
850 .collect()
851}
852
853fn constraint_needs_bump(current: Option<&str>, resolved: &semver::Version) -> bool {
854 match crate::resolve::parse_version_constraint(current) {
855 crate::resolve::VersionConstraint::Semver(req) => !req.matches(resolved),
856 crate::resolve::VersionConstraint::Latest
857 | crate::resolve::VersionConstraint::RefPin(_) => false,
858 }
859}
860
861fn has_version_changes(changes: &[DependencyUpsertChange]) -> bool {
862 changes
863 .iter()
864 .any(|change| change.old_version != change.new_version)
865}
866
867#[cfg(test)]
868mod tests;