Skip to main content

ephemeris_forge_github/plan/
repository.rs

1use ephemeris_forge::{FieldDiff, Limitation, NameSet, Report};
2
3use crate::api::model;
4use crate::api::request::RepositoryPatch;
5use crate::operation::repository::Operation;
6use crate::{Live, Repository};
7
8macro_rules! compare {
9    ($diff:ident, $patch: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,
14        ) {
15            $patch.$field = Some(wanted);
16        }
17    )+};
18}
19
20/// The repository writes split around the other planners' writes.
21pub(super) struct Planned {
22    /// Runs before every other write, so an archived repository accepts them.
23    pub(super) before: Option<Operation>,
24    /// Runs after every other write.
25    pub(super) after: Vec<Operation>,
26}
27
28pub(super) fn plan(desired: &Repository, state: &Live, report: &mut Report) -> Planned {
29    let live = &state.repository;
30    let mut diff = FieldDiff::default();
31    let mut patch = RepositoryPatch::default();
32    compare!(
33        diff,
34        patch,
35        desired,
36        live,
37        description,
38        homepage,
39        has_issues,
40        has_projects,
41        has_wiki,
42        has_pull_requests,
43        has_discussions,
44        pull_request_creation_policy,
45        is_template,
46        allow_squash_merge,
47        allow_merge_commit,
48        allow_rebase_merge,
49        allow_auto_merge,
50        delete_branch_on_merge,
51        allow_update_branch,
52        web_commit_signoff_required,
53    );
54    if let Some(visibility) = desired.visibility
55        && diff.check(
56            "visibility",
57            live.visibility == Some(visibility),
58            &live.visibility,
59            &Some(visibility),
60        )
61    {
62        patch.visibility = Some(visibility);
63    }
64    if let Some(branch) = &desired.default_branch
65        && diff.check(
66            "default_branch",
67            live.default_branch.as_deref() == Some(branch),
68            &live.default_branch,
69            &Some(branch),
70        )
71    {
72        patch.default_branch = Some(branch.clone());
73    }
74    if let Some(forking) = desired.allow_forking {
75        if live.allow_forking.is_none() {
76            report.limitations.push(Limitation::NotReported {
77                field: "allow_forking",
78            });
79        } else if diff.check(
80            "allow_forking",
81            live.allow_forking == Some(forking),
82            &live.allow_forking,
83            &Some(forking),
84        ) {
85            patch.allow_forking = Some(forking);
86        }
87    }
88    squash_commit(desired, live, &mut diff, &mut patch);
89    merge_commit(desired, live, &mut diff, &mut patch);
90    let immutable = immutable_releases(desired, state, &mut diff, report);
91    let archived = diff.want("archived", desired.archived.as_ref(), &live.archived);
92    let current = NameSet::new(live.topics.clone());
93    let topics = desired
94        .topics
95        .clone()
96        .map(NameSet::new)
97        .and_then(|wanted| diff.want_set("topics", &wanted, Some(&current)))
98        .map(NameSet::into_vec);
99    diff.record("repository", report);
100    let mut after = Vec::new();
101    after.extend(topics.map(Operation::ReplaceTopics));
102    if !patch.is_empty() {
103        after.push(Operation::PatchRepository(patch));
104    }
105    after.extend(immutable.map(Operation::SetImmutableReleases));
106    let before = if archived == Some(false) {
107        archived.map(Operation::SetArchived)
108    } else {
109        after.extend(archived.map(Operation::SetArchived));
110        None
111    };
112    Planned { before, after }
113}
114
115fn immutable_releases(
116    desired: &Repository,
117    state: &Live,
118    diff: &mut FieldDiff,
119    report: &mut Report,
120) -> Option<bool> {
121    let wanted = desired.immutable_releases?;
122    let Some(live) = state.immutable_releases else {
123        report.limitations.push(Limitation::Unavailable {
124            field: "immutable_releases",
125        });
126        return None;
127    };
128    if live.enforced_by_owner && live.enabled != wanted {
129        report.limitations.push(Limitation::EnforcedByOwner {
130            field: "immutable_releases",
131        });
132        return None;
133    }
134    diff.check(
135        "immutable_releases",
136        live.enabled == wanted,
137        &live.enabled,
138        &wanted,
139    )
140    .then_some(wanted)
141}
142
143fn squash_commit(
144    desired: &Repository,
145    live: &model::Repository,
146    diff: &mut FieldDiff,
147    patch: &mut RepositoryPatch,
148) {
149    let title = desired.squash_merge_commit_title.is_some_and(|wanted| {
150        diff.check(
151            "squash_merge_commit_title",
152            live.squash_merge_commit_title == wanted,
153            &live.squash_merge_commit_title,
154            &wanted,
155        )
156    });
157    let message = desired.squash_merge_commit_message.is_some_and(|wanted| {
158        diff.check(
159            "squash_merge_commit_message",
160            live.squash_merge_commit_message == wanted,
161            &live.squash_merge_commit_message,
162            &wanted,
163        )
164    });
165    if title || message {
166        patch.squash_merge_commit_title = Some(
167            desired
168                .squash_merge_commit_title
169                .unwrap_or(live.squash_merge_commit_title),
170        );
171        patch.squash_merge_commit_message = Some(
172            desired
173                .squash_merge_commit_message
174                .unwrap_or(live.squash_merge_commit_message),
175        );
176    }
177}
178
179fn merge_commit(
180    desired: &Repository,
181    live: &model::Repository,
182    diff: &mut FieldDiff,
183    patch: &mut RepositoryPatch,
184) {
185    let title = desired.merge_commit_title.is_some_and(|wanted| {
186        diff.check(
187            "merge_commit_title",
188            live.merge_commit_title == wanted,
189            &live.merge_commit_title,
190            &wanted,
191        )
192    });
193    let message = desired.merge_commit_message.is_some_and(|wanted| {
194        diff.check(
195            "merge_commit_message",
196            live.merge_commit_message == wanted,
197            &live.merge_commit_message,
198            &wanted,
199        )
200    });
201    if title || message {
202        patch.merge_commit_title = Some(
203            desired
204                .merge_commit_title
205                .unwrap_or(live.merge_commit_title),
206        );
207        patch.merge_commit_message = Some(
208            desired
209                .merge_commit_message
210                .unwrap_or(live.merge_commit_message),
211        );
212    }
213}