1use std::collections::{BTreeMap, BTreeSet};
6
7use crate::config::{Config, Host};
8use crate::erlang_mk::{DepPin, dep_to_tag};
9use crate::jigsaw::plan::{
10 DraftInputs, JigsawPlan, PlanError, ReleaseFacts, TagSourceKind, draft_plan,
11};
12use crate::jigsaw::releases::SeriesReleases;
13use crate::jigsaw::tags::{select_otp_lines, select_series_tags};
14use crate::model::names::{
15 ApplicationName, CommitSha, DependencyName, ExtractorVersion, ProjectName, TagName,
16};
17use crate::model::non_empty::NonEmpty;
18use crate::model::release_version::{ErlangMajor, HostSeries, HostTag};
19use crate::store::{ReadOnly, SnapshotStore};
20
21#[derive(Debug, Clone, Default, PartialEq, Eq)]
22pub struct ListedTags {
23 pub host: Vec<TagName>,
24 pub otp: Vec<TagName>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct SeriesRequest {
29 pub series: HostSeries,
30 pub up_to: Option<TagName>,
31 pub majors: NonEmpty<ErlangMajor>,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ResolvedRelease {
36 pub commit: CommitSha,
37 pub pins: Vec<DepPin>,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct DraftedPlan {
42 pub plan: JigsawPlan,
43 pub skipped_tags: Vec<TagName>,
44}
45
46fn release_facts(
48 cfg: &Config,
49 host: &Host,
50 tag: HostTag,
51 resolved: ResolvedRelease,
52 untracked: &mut BTreeSet<DependencyName>,
53) -> Result<ReleaseFacts, PlanError> {
54 let mut pins: BTreeMap<ProjectName, TagName> = BTreeMap::new();
55 for pin in &resolved.pins {
56 let project = ProjectName::new(pin.name.as_str())
57 .ok()
58 .filter(|name| *name != host.project && *name != host.otp_project)
59 .and_then(|name| cfg.project(&name).ok());
60 let Some(project) = project.filter(|project| !project.is_self()) else {
61 untracked.insert(pin.name.clone());
62 continue;
63 };
64 let raw = dep_to_tag(pin, &project.tag_prefix);
65 let dep_tag = TagName::new(raw.clone()).map_err(|e| PlanError::ReleaseUnreadable {
66 tag: tag.tag().clone(),
67 detail: format!("pin {} resolves to {raw:?}: {e}", pin.name),
68 })?;
69 pins.insert(project.name.clone(), dep_tag);
70 }
71 Ok(ReleaseFacts {
72 tag,
73 commit: resolved.commit,
74 pins,
75 })
76}
77
78pub fn plan_series(
80 cfg: &Config,
81 host: &Host,
82 request: &SeriesRequest,
83 listed: &ListedTags,
84 tag_source: TagSourceKind,
85 extractor_version: ExtractorVersion,
86 mut resolve: impl FnMut(&HostTag) -> Result<ResolvedRelease, PlanError>,
87) -> Result<DraftedPlan, PlanError> {
88 let host_project = cfg.project(&host.project)?;
89 let otp_project = cfg.project(&host.otp_project)?;
90 let selected = select_series_tags(
91 host_project,
92 &host.series_tag_pattern,
93 request.series,
94 request.up_to.as_ref(),
95 &listed.host,
96 )?;
97 let otp_lines = select_otp_lines(otp_project, &request.majors, &listed.otp)?;
98 let mut untracked_dependencies = BTreeSet::new();
99 let facts = selected
100 .releases
101 .iter()
102 .map(|tag| {
103 let resolved = resolve(tag)?;
104 release_facts(
105 cfg,
106 host,
107 tag.clone(),
108 resolved,
109 &mut untracked_dependencies,
110 )
111 })
112 .collect::<Result<Vec<_>, PlanError>>()?;
113 let plan = draft_plan(
114 host,
115 DraftInputs {
116 releases: SeriesReleases::new(facts)?,
117 otp_lines,
118 untracked_dependencies,
119 excluded_otp_apps: otp_project
120 .exclude_apps
121 .iter()
122 .cloned()
123 .collect::<BTreeSet<ApplicationName>>(),
124 tag_source,
125 extractor_version,
126 },
127 )?;
128 Ok(DraftedPlan {
129 plan,
130 skipped_tags: selected.skipped_tags,
131 })
132}
133
134pub fn plan_series_from_store(
136 cfg: &Config,
137 host: &Host,
138 request: &SeriesRequest,
139 store: &SnapshotStore<ReadOnly>,
140 extractor_version: ExtractorVersion,
141) -> Result<DraftedPlan, PlanError> {
142 let listed = ListedTags {
143 host: store.list_tags(&host.project)?,
144 otp: store.list_tags(&host.otp_project)?,
145 };
146 plan_series(
147 cfg,
148 host,
149 request,
150 &listed,
151 TagSourceKind::Store,
152 extractor_version,
153 |tag| {
154 let header = store.read_header(&host.project, tag.tag()).map_err(|e| {
155 PlanError::ReleaseUnreadable {
156 tag: tag.tag().clone(),
157 detail: e.to_string(),
158 }
159 })?;
160 Ok(ResolvedRelease {
161 commit: header.commit,
162 pins: header.dep_pins,
163 })
164 },
165 )
166}