Skip to content
231 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1//! 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.
The runner base keeps its old comment, so the pushed base image still matches its folder8
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;
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails20/// 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;
The runner base keeps its old comment, so the pushed base image still matches its folder23
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>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar38 #[serde(default)]
39 require_code_owner_review: u8,
The runner base keeps its old comment, so the pushed base image still matches its folder40 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,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar62 require_code_owner_review: row.require_code_owner_review != 0,
The runner base keeps its old comment, so the pushed base image still matches its folder63 updated_by: Some(row.updated_by),
64 updated_at: Some(row.updated_at),
65 }
66 }
67}
68
69impl Work {
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge70 /// 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`.
The runner base keeps its old comment, so the pushed base image still matches its folder73 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
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge98 /// The default branch's settings: the stored ones, with branch
99 /// protection as its rules stack.
The runner base keeps its old comment, so the pushed base image still matches its folder100 pub(crate) async fn get_settings(&self, a: ViewArgs) -> Result<Outcome<RepoSettings>> {
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge101 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?))
The runner base keeps its old comment, so the pushed base image still matches its folder106 }
107
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge108 /// 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.
The runner base keeps its old comment, so the pushed base image still matches its folder112 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 };
Merge branch 'mirroring' into artifacts-mode120 if let Outcome::Fail(failure) = crate::retired::not_archived(&repo) {
The runner base keeps its old comment, so the pushed base image still matches its folder121 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 }
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge131 let before = self.default_branch_settings(&repo).await?;
The runner base keeps its old comment, so the pushed base image still matches its folder132 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),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge136 updated_by: Some(a.actor.username.clone()),
The runner base keeps its old comment, so the pushed base image still matches its folder137 updated_at: Some(rfc3339(now_ms())),
138 ..a.settings
139 };
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge140 // 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 }
The runner base keeps its old comment, so the pushed base image still matches its folder147 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,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar152 merge_queue, required_checks, require_code_owner_review, updated_by, updated_at)
153 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
The runner base keeps its old comment, so the pushed base image still matches its folder154 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,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar164 require_code_owner_review = excluded.require_code_owner_review,
The runner base keeps its old comment, so the pushed base image still matches its folder165 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(),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar179 u32::from(settings.require_code_owner_review).into(),
The runner base keeps its old comment, so the pushed base image still matches its folder180 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?;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge192 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?))
The runner base keeps its old comment, so the pushed base image still matches its folder196 }
197}
198
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge199/// 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
The runner base keeps its old comment, so the pushed base image still matches its folder215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge220 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));
The runner base keeps its old comment, so the pushed base image still matches its folder230 }
231}

This file's history is long; its oldest lines are credited to the oldest commit read.