ephemeris_forge_forgejo/plan/
repository.rs1use ephemeris_forge::{FieldDiff, Limitation, NameSet, Report};
2
3use crate::api::model;
4use crate::api::request::{self, EditRepoOption};
5use crate::operation::repository::Operation;
6use crate::{Live, Repository, config};
7
8macro_rules! compare {
9 ($diff:ident, $edit:ident, $desired:ident, $live:ident, $($field:ident),+ $(,)?) => {$(
10 if let Some(wanted) = $diff.want(
11 stringify!($field),
12 $desired.$field.as_ref(),
13 $live.$field.as_ref(),
14 ) {
15 $edit.$field = Some(wanted);
16 }
17 )+};
18}
19
20macro_rules! write_unread {
21 ($diff:ident, $edit:ident, $desired:ident, $report:ident, $($field:ident),+ $(,)?) => {$(
22 if let Some(wanted) = $desired.$field {
23 $report.limitations.push(Limitation::WrittenOnEveryApply {
24 field: stringify!($field),
25 });
26 $diff.check(stringify!($field), false, &None::<()>, &wanted);
27 $edit.$field = Some(wanted);
28 }
29 )+};
30}
31
32pub(super) fn plan(desired: &Repository, live: &Live, report: &mut Report) -> Vec<Operation> {
33 let (edit, topics, diff) = compare_repository(desired, live, report);
34 diff.record("repository", report);
35 if topics.is_some() || !edit.is_empty() {
36 vec![Operation {
37 topics,
38 edit: Box::new(edit),
39 }]
40 } else {
41 Vec::new()
42 }
43}
44
45fn compare_repository(
46 desired: &Repository,
47 live: &Live,
48 report: &mut Report,
49) -> (EditRepoOption, Option<Vec<String>>, FieldDiff) {
50 let actual = &live.repository;
51 let mut diff = FieldDiff::default();
52 let mut edit = EditRepoOption::default();
53 compare!(
54 diff,
55 edit,
56 desired,
57 actual,
58 allow_fast_forward_only_merge,
59 allow_merge_commits,
60 allow_rebase,
61 allow_rebase_explicit,
62 allow_rebase_update,
63 allow_squash_merge,
64 archived,
65 default_allow_maintainer_edit,
66 default_branch,
67 default_delete_branch_after_merge,
68 description,
69 globally_editable_wiki,
70 has_actions,
71 has_issues,
72 has_packages,
73 has_projects,
74 has_pull_requests,
75 has_releases,
76 has_wiki,
77 ignore_whitespace_conflicts,
78 private,
79 template,
80 website,
81 );
82 if let Some(style) = desired.default_merge_style
83 && diff.check(
84 "default_merge_style",
85 actual.default_merge_style.as_deref() == Some(style.as_str()),
86 &actual.default_merge_style,
87 &style.as_str(),
88 )
89 {
90 edit.default_merge_style = Some(style.as_str().to_owned());
91 }
92 if let Some(style) = desired.default_update_style
93 && diff.check(
94 "default_update_style",
95 actual.default_update_style.as_deref() == Some(style.as_str()),
96 &actual.default_update_style,
97 &style.as_str(),
98 )
99 {
100 edit.default_update_style = Some(style.as_str().to_owned());
101 }
102 write_unread!(
103 diff,
104 edit,
105 desired,
106 report,
107 allow_manual_merge,
108 autodetect_manual_merge
109 );
110 compare_wiki(desired, actual, &mut diff, &mut edit, report);
111 compare_issues(desired, actual, &mut diff, &mut edit, report);
112 compare_mirror(desired, actual, &mut diff, &mut edit, report);
113 let current = NameSet::new(live.topics.clone());
114 let topics = desired
115 .topics
116 .as_ref()
117 .map(|topics| NameSet::new(topics.iter().map(|topic| topic.as_str().to_owned())))
118 .and_then(|wanted| diff.want_set("topics", &wanted, Some(¤t)))
119 .map(NameSet::into_vec);
120 (edit, topics, diff)
121}
122
123fn compare_wiki(
124 desired: &Repository,
125 actual: &model::Repository,
126 diff: &mut FieldDiff,
127 edit: &mut EditRepoOption,
128 report: &mut Report,
129) {
130 if let Some(branch) = &desired.wiki_branch {
131 if actual.wiki_branch.is_none() {
132 report.limitations.push(Limitation::NotReported {
133 field: "wiki_branch",
134 });
135 } else if diff.check(
136 "wiki_branch",
137 actual.wiki_branch.as_ref() == Some(branch),
138 &actual.wiki_branch,
139 branch,
140 ) {
141 edit.wiki_branch = Some(branch.clone());
142 }
143 }
144 let Some(wiki) = &desired.external_wiki else {
145 return;
146 };
147 if desired.has_wiki == Some(false) {
148 report.limitations.push(Limitation::Ignored {
149 field: "external_wiki",
150 switch: "has_wiki",
151 });
152 return;
153 }
154 let live = actual
155 .external_wiki
156 .as_ref()
157 .and_then(|live| live.external_wiki_url.as_deref());
158 if diff.check(
159 "external_wiki",
160 live == Some(wiki.external_wiki_url.as_str()),
161 &live,
162 &wiki.external_wiki_url,
163 ) {
164 edit.external_wiki = Some(wiki.clone());
165 }
166}
167
168fn compare_issues(
169 desired: &Repository,
170 actual: &model::Repository,
171 diff: &mut FieldDiff,
172 edit: &mut EditRepoOption,
173 report: &mut Report,
174) {
175 if desired.has_issues == Some(false) {
176 if desired.external_tracker.is_some() {
177 report.limitations.push(Limitation::Ignored {
178 field: "external_tracker",
179 switch: "has_issues",
180 });
181 }
182 return;
183 }
184 if let Some(tracker) = &desired.external_tracker {
185 let live = actual.external_tracker.clone().unwrap_or_default();
186 if diff.check(
187 "external_tracker",
188 external_matches(tracker, &live),
189 &live,
190 tracker,
191 ) {
192 edit.external_tracker = Some(request::ExternalTracker::new(tracker, &live));
193 }
194 } else if desired.internal_tracker.declares_any() {
195 let live = actual.internal_tracker.unwrap_or_default();
196 let same =
197 actual.internal_tracker.is_some() && internal_matches(desired.internal_tracker, live);
198 if diff.check(
199 "internal_tracker",
200 same,
201 &actual.internal_tracker,
202 &desired.internal_tracker,
203 ) {
204 edit.internal_tracker = Some(request::InternalTracker::new(
205 desired.internal_tracker,
206 live,
207 ));
208 }
209 }
210}
211
212fn internal_matches(desired: config::InternalTracker, live: model::InternalTracker) -> bool {
213 declared_matches(desired.enable_time_tracker, live.enable_time_tracker)
214 && declared_matches(
215 desired.allow_only_contributors_to_track_time,
216 live.allow_only_contributors_to_track_time,
217 )
218 && declared_matches(
219 desired.enable_issue_dependencies,
220 live.enable_issue_dependencies,
221 )
222}
223
224fn external_matches(desired: &config::ExternalTracker, live: &model::ExternalTracker) -> bool {
225 live.external_tracker_url.as_deref() == Some(desired.external_tracker_url.as_str())
226 && live.external_tracker_format.as_deref() == Some(desired.external_tracker_format.as_str())
227 && declared_matches(
228 desired
229 .external_tracker_style
230 .map(config::TrackerStyle::as_str),
231 live.external_tracker_style.as_deref(),
232 )
233 && declared_matches(
234 desired.external_tracker_regexp_pattern.as_deref(),
235 live.external_tracker_regexp_pattern.as_deref(),
236 )
237}
238
239fn declared_matches<T: PartialEq>(desired: Option<T>, live: Option<T>) -> bool {
240 desired.is_none_or(|wanted| live.is_some_and(|live| live == wanted))
241}
242
243fn compare_mirror(
244 desired: &Repository,
245 actual: &model::Repository,
246 diff: &mut FieldDiff,
247 edit: &mut EditRepoOption,
248 report: &mut Report,
249) {
250 if desired.mirror_interval.is_none() && desired.enable_prune.is_none() {
251 return;
252 }
253 if actual.mirror != Some(true) {
254 report.limitations.push(Limitation::MirrorOnly);
255 return;
256 }
257 if let Some(interval) = &desired.mirror_interval
258 && diff.check(
259 "mirror_interval",
260 actual.mirror_interval.as_ref() == Some(interval),
261 &actual.mirror_interval,
262 interval,
263 )
264 {
265 edit.mirror_interval = Some(interval.clone());
266 }
267 write_unread!(diff, edit, desired, report, enable_prune);
268}