Skip to main content

ephemeris_forge_forgejo/plan/
repository.rs

1use 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(&current)))
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}