Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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