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