Skip to content
231 linesCodeBlameRaw
1//! A repository's settings for how g1t's agents handle its pull requests,
2//! and the branch protection settings that are now its "Default branch
3//! protection" ruleset (rulesets.rs).
4//!
5//! What a merge needs is decided by the rules of the branch it merges into
6//! (`Work::merge_gate`); `approvals_gap` asks them for what people must
7//! still do, as g1t sees it when it merges by itself.
8
9use g1t_contracts::time::rfc3339;
10use g1t_contracts::work::*;
11use g1t_contracts::{FailureCode, Outcome};
12use g1t_kit::now_ms;
13use serde::Deserialize;
14use worker::Result;
15
16use crate::Work;
17
18const MAX_REQUIRED_APPROVALS: u32 = 6;
19const MAX_REVISIONS: u32 = 5;
20/// The most times in a day people's mentions may send g1t back to one pull
21/// request. They outrank `MAX_REVISIONS` and a stall, but not this.
22pub(crate) const MAX_MENTION_REVISIONS_PER_DAY: u32 = 10;
23
24#[derive(Deserialize)]
25struct SettingsRow {
26 auto_merge: u8,
27 require_up_to_date: u8,
28 required_approvals: u32,
29 count_agent_approvals: u8,
30 allow_ignoring_checks: u8,
31 agent_review: u8,
32 max_revisions: u32,
33 #[serde(default)]
34 merge_queue: u8,
35 /// JSON array of names.
36 #[serde(default)]
37 required_checks: Option<String>,
38 #[serde(default)]
39 require_code_owner_review: u8,
40 updated_by: String,
41 updated_at: String,
42}
43
44impl From<SettingsRow> for RepoSettings {
45 fn from(row: SettingsRow) -> Self {
46 RepoSettings {
47 auto_merge: row.auto_merge != 0,
48 required_checks: row
49 .required_checks
50 .as_deref()
51 .and_then(|names| serde_json::from_str(names).ok())
52 .unwrap_or_default(),
53 require_up_to_date: row.require_up_to_date != 0,
54 required_approvals: row.required_approvals,
55 count_agent_approvals: row.count_agent_approvals != 0,
56 allow_ignoring_checks: row.allow_ignoring_checks != 0,
57 agent_review: row.agent_review != 0,
58 max_revisions: row.max_revisions,
59 merge_queue: row.merge_queue != 0,
60 // Kept in its own table (confidence.rs), read beside this row.
61 hold_low_confidence: true,
62 require_code_owner_review: row.require_code_owner_review != 0,
63 updated_by: Some(row.updated_by),
64 updated_at: Some(row.updated_at),
65 }
66 }
67}
68
69impl Work {
70 /// A repository's stored settings, by its id: how g1t's agents handle
71 /// its pull requests. Defaults if none were set. Branch protection is
72 /// its rules': see `settings_on`.
73 pub(crate) async fn settings(&self, repo_id: &str) -> Result<RepoSettings> {
74 if let Some(found) = self.prefetched_repo(repo_id) {
75 let row = found.first::<SettingsRow>(crate::prefetch::Slot::Settings)?;
76 let hold = found
77 .first::<crate::rows::NumberRow>(crate::prefetch::Slot::Hold)?
78 .is_none_or(|row| row.n != 0);
79 return Ok(RepoSettings {
80 hold_low_confidence: hold,
81 ..row.map_or_else(RepoSettings::default, RepoSettings::from)
82 });
83 }
84 let row = async {
85 self.db
86 .prepare("SELECT * FROM repo_settings WHERE repo_id = ?")
87 .bind(&[repo_id.into()])?
88 .first::<SettingsRow>(None)
89 .await
90 };
91 let (row, hold) = futures_util::future::try_join(row, self.holds_low_confidence(repo_id)).await?;
92 Ok(RepoSettings {
93 hold_low_confidence: hold,
94 ..row.map_or_else(RepoSettings::default, RepoSettings::from)
95 })
96 }
97
98 /// The default branch's settings: the stored ones, with branch
99 /// protection as its rules stack.
100 pub(crate) async fn get_settings(&self, a: ViewArgs) -> Result<Outcome<RepoSettings>> {
101 let repo = match self.repo(&a.repo, &a.viewer).await? {
102 Outcome::Ok(repo) => repo,
103 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
104 };
105 Ok(Outcome::Ok(self.timing.db(3, self.default_branch_settings(&repo)).await?))
106 }
107
108 /// Replaces a repository's settings: how g1t's agents work, kept here,
109 /// and the branch protection ones, written to its "Default branch
110 /// protection" ruleset (made when it has none and they protect
111 /// anything). Rules that only rulesets have stay as they are.
112 pub(crate) async fn update_settings(
113 &self,
114 a: UpdateSettingsArgs,
115 ) -> Result<Outcome<RepoSettings>> {
116 let repo = match self.repo(&a.repo, &Some(a.actor.clone())).await? {
117 Outcome::Ok(repo) => repo,
118 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
119 };
120 if let Outcome::Fail(failure) = crate::retired::writable(&repo) {
121 return Ok(Outcome::Fail(failure));
122 }
123 if !a.actor.verified {
124 return Ok(Outcome::fail(FailureCode::Forbidden, crate::UNVERIFIED));
125 }
126 if let Outcome::Fail(failure) =
127 crate::allowed(Some(&a.actor), &repo, g1t_contracts::access::Capability::ManageSettings)
128 {
129 return Ok(Outcome::Fail(failure));
130 }
131 let before = self.default_branch_settings(&repo).await?;
132 let settings = RepoSettings {
133 required_approvals: a.settings.required_approvals.min(MAX_REQUIRED_APPROVALS),
134 max_revisions: a.settings.max_revisions.min(MAX_REVISIONS),
135 required_checks: tidy_required(&a.settings.required_checks),
136 updated_by: Some(a.actor.username.clone()),
137 updated_at: Some(rfc3339(now_ms())),
138 ..a.settings
139 };
140 // Branch protection changes need the role that changes it.
141 if protection_changed(&before, &settings)
142 && let Outcome::Fail(failure) =
143 crate::allowed(Some(&a.actor), &repo, g1t_contracts::access::Capability::ManageProtection)
144 {
145 return Ok(Outcome::Fail(failure));
146 }
147 self.db
148 .prepare(
149 "INSERT INTO repo_settings
150 (repo_id, auto_merge, require_up_to_date, required_approvals,
151 count_agent_approvals, allow_ignoring_checks, agent_review, max_revisions,
152 merge_queue, required_checks, require_code_owner_review, updated_by, updated_at)
153 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
154 ON CONFLICT (repo_id) DO UPDATE SET
155 auto_merge = excluded.auto_merge,
156 require_up_to_date = excluded.require_up_to_date,
157 required_approvals = excluded.required_approvals,
158 count_agent_approvals = excluded.count_agent_approvals,
159 allow_ignoring_checks = excluded.allow_ignoring_checks,
160 agent_review = excluded.agent_review,
161 max_revisions = excluded.max_revisions,
162 merge_queue = excluded.merge_queue,
163 required_checks = excluded.required_checks,
164 require_code_owner_review = excluded.require_code_owner_review,
165 updated_by = excluded.updated_by,
166 updated_at = excluded.updated_at",
167 )
168 .bind(&[
169 repo.id.as_str().into(),
170 u32::from(settings.auto_merge).into(),
171 u32::from(settings.require_up_to_date).into(),
172 settings.required_approvals.into(),
173 u32::from(settings.count_agent_approvals).into(),
174 u32::from(settings.allow_ignoring_checks).into(),
175 u32::from(settings.agent_review).into(),
176 settings.max_revisions.into(),
177 u32::from(settings.merge_queue).into(),
178 serde_json::to_string(&settings.required_checks)?.into(),
179 u32::from(settings.require_code_owner_review).into(),
180 settings.updated_by.as_deref().unwrap_or_default().into(),
181 settings.updated_at.as_deref().unwrap_or_default().into(),
182 ])?
183 .run()
184 .await?;
185 self.set_hold_low_confidence(
186 &repo.id,
187 settings.hold_low_confidence,
188 settings.updated_by.as_deref().unwrap_or_default(),
189 settings.updated_at.as_deref().unwrap_or_default(),
190 )
191 .await?;
192 if protection_changed(&before, &settings) {
193 self.write_branch_protection(&repo, &settings, &a.actor).await?;
194 }
195 Ok(Outcome::Ok(self.default_branch_settings(&repo).await?))
196 }
197}
198
199/// Whether the branch protection part of the settings differs.
200fn protection_changed(before: &RepoSettings, after: &RepoSettings) -> bool {
201 let names = |settings: &RepoSettings| -> Vec<String> {
202 let mut names: Vec<String> = settings.required_checks.iter().map(|name| name.to_lowercase()).collect();
203 names.sort();
204 names
205 };
206 names(before) != names(after)
207 || before.require_up_to_date != after.require_up_to_date
208 || before.required_approvals != after.required_approvals
209 || before.count_agent_approvals != after.count_agent_approvals
210 || before.allow_ignoring_checks != after.allow_ignoring_checks
211 || before.merge_queue != after.merge_queue
212 || before.require_code_owner_review != after.require_code_owner_review
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn only_protection_changes_count_as_protection_changes() {
221 let before = RepoSettings::default();
222 let agents = RepoSettings { auto_merge: true, agent_review: false, max_revisions: 4, ..RepoSettings::default() };
223 assert!(!protection_changed(&before, &agents));
224 let checks = RepoSettings { required_checks: vec!["CI".into()], ..RepoSettings::default() };
225 assert!(protection_changed(&before, &checks));
226 let same = RepoSettings { required_checks: vec!["ci".into()], ..RepoSettings::default() };
227 assert!(!protection_changed(&checks, &same), "names compare without case");
228 let approvals = RepoSettings { required_approvals: 1, ..RepoSettings::default() };
229 assert!(protection_changed(&before, &approvals));
230 }
231}