use std::collections::HashMap;
use super::Error as TidyError;
use crate::domain::action::identity::{CommitSha, Version};
use crate::domain::action::spec::Spec as ActionSpec;
use crate::domain::event::Event as SyncEvent;
use crate::domain::lock::Lock;
use crate::domain::manifest::Manifest;
use crate::domain::resolution::{ActionResolver, Error as ResolutionError, VersionRegistry};
pub(super) fn update_lock<R: VersionRegistry>(
lock: &mut Lock,
manifest: &mut Manifest,
resolver: &ActionResolver<'_, R>,
workflow_shas: &HashMap<ActionSpec, CommitSha>,
) -> Result<Vec<SyncEvent>, TidyError> {
let mut events: Vec<SyncEvent> = Vec::new();
let mut unresolved = Vec::new();
let mut recoverable_count: usize = 0;
let all_specs: Vec<ActionSpec> = manifest
.specs()
.cloned()
.chain(manifest.all_overrides().iter().flat_map(|(id, overrides)| {
overrides
.iter()
.map(move |exc| ActionSpec::new(id.clone(), exc.version.clone()))
}))
.collect();
let needs_resolving = all_specs.iter().any(|spec| !lock.has(spec));
if !needs_resolving {
return Ok(events);
}
for spec in &all_specs {
match populate_lock_entry(lock, resolver, spec, workflow_shas) {
Ok(Some(event)) => events.push(event),
Ok(None) => {}
Err(e) => {
if e.is_skippable() {
events.push(SyncEvent::ResolutionSkipped {
spec: spec.clone(),
reason: e.to_string(),
});
recoverable_count = recoverable_count.saturating_add(1);
} else {
unresolved.push(format!("{spec}: {e}"));
}
}
}
}
if recoverable_count > 0 {
events.push(SyncEvent::RecoverableWarning {
count: recoverable_count,
});
}
if !unresolved.is_empty() {
return Err(TidyError::ResolutionFailed {
count: unresolved.len(),
specs: unresolved.join("\n "),
});
}
Ok(events)
}
fn populate_lock_entry<R: VersionRegistry>(
lock: &mut Lock,
resolver: &ActionResolver<'_, R>,
spec: &ActionSpec,
workflow_shas: &HashMap<ActionSpec, CommitSha>,
) -> Result<Option<SyncEvent>, ResolutionError> {
if lock.has(spec) {
return Ok(None);
}
let sha_first = workflow_shas
.get(spec)
.and_then(|sha| resolver.resolve_from_sha(&spec.id, sha).ok());
let Some(action) = sha_first else {
let action = resolver.resolve(spec)?;
lock.set(spec, action.reference, action.commit);
return Ok(None);
};
if spec.specifier.matches_version(&action.reference) {
lock.set(spec, action.reference, action.commit);
return Ok(None);
}
let rejected = Version::from(action.reference.label(&action.commit.sha));
let within_range = resolver.resolve(spec)?;
let event = SyncEvent::PinOutOfRange {
spec: spec.clone(),
rejected,
resolved: Version::from(within_range.reference.label(&within_range.commit.sha)),
};
lock.set(spec, within_range.reference, within_range.commit);
Ok(Some(event))
}
#[cfg(test)]
#[expect(
clippy::unwrap_used,
clippy::expect_used,
reason = "tests use unwrap, indexing, and other patterns freely"
)]
#[path = "lock_sync_tests.rs"]
mod tests;