Skip to content
477 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//! Which rulesets hold where, who may bypass them, and what holds for one
2//! branch once they are stacked.
3
4use g1t_contracts::access::{self, RepoRole};
5use g1t_contracts::repos::Repo;
6use g1t_contracts::rules::{
7 ALL, ActorKind, Applicable, BypassActor, BypassMode, DEFAULT_BRANCH, EffectiveRule, EffectiveRules, Enforcement,
8 Level, RefCondition, RepositoryCondition, Ruleset, RulesetSummary, Target, VisibilityCondition,
9};
10use g1t_contracts::{PrincipalKind, Role, User};
11
12use crate::glob;
13
14/// What a ruleset needs to know about a repository to say whether it holds.
15#[derive(Clone, Copy, Debug)]
16pub struct RepoFacts<'a> {
17 pub id: &'a str,
18 pub name: &'a str,
19 pub private: bool,
20 pub topics: &'a [String],
21 pub default_branch: &'a str,
22}
23
24impl<'a> From<&'a Repo> for RepoFacts<'a> {
25 fn from(repo: &'a Repo) -> Self {
26 RepoFacts {
27 id: &repo.id,
28 name: &repo.name,
29 private: repo.is_private,
30 topics: &repo.topics,
31 default_branch: &repo.default_branch,
32 }
33 }
34}
35
36/// A name written as a full ref, shortened: `refs/heads/main` is `main`.
37fn short(pattern: &str, target: Target) -> &str {
38 let prefix = match target {
39 Target::Branch => "refs/heads/",
40 Target::Tag => "refs/tags/",
41 };
42 pattern.strip_prefix(prefix).unwrap_or(pattern)
43}
44
45fn ref_pattern_matches(pattern: &str, target: Target, name: &str, default_branch: &str) -> bool {
46 let pattern = pattern.trim();
47 match pattern {
48 ALL => true,
49 DEFAULT_BRANCH => target == Target::Branch && name == default_branch,
50 _ => glob::matches(short(pattern, target), name),
51 }
52}
53
54/// Whether a branch or tag named `name` is one `condition` selects.
55pub fn ref_matches(condition: &RefCondition, target: Target, name: &str, default_branch: &str) -> bool {
56 condition.include.iter().any(|pattern| ref_pattern_matches(pattern, target, name, default_branch))
57 && !condition.exclude.iter().any(|pattern| ref_pattern_matches(pattern, target, name, default_branch))
58}
59
60/// Whether a workspace ruleset's repository condition selects `repo`.
61pub fn repo_matches(condition: &RepositoryCondition, repo: RepoFacts<'_>) -> bool {
62 let name = repo.name.to_lowercase();
63 let named = |pattern: &String| {
64 let pattern = pattern.trim();
65 pattern == ALL || glob::matches(&pattern.to_lowercase(), &name)
66 };
67 let visible = match condition.visibility {
68 VisibilityCondition::Any => true,
69 VisibilityCondition::Public => !repo.private,
70 VisibilityCondition::Private => repo.private,
71 };
72 let topical = condition.topics.is_empty()
73 || condition
74 .topics
75 .iter()
76 .any(|topic| repo.topics.iter().any(|has| has.eq_ignore_ascii_case(topic.trim())));
77 condition.include.iter().any(named) && !condition.exclude.iter().any(named) && visible && topical
78}
79
80/// Whether a ruleset holds in `repo` at all, whatever the branch: a
81/// repository's own, or a workspace's that selects it. Disabled ones never.
82pub fn holds_in(ruleset: &Ruleset, repo: RepoFacts<'_>) -> bool {
83 if ruleset.spec.enforcement == Enforcement::Disabled {
84 return false;
85 }
86 match ruleset.level {
87 Level::Repository => ruleset.repo_id.as_deref() == Some(repo.id),
88 Level::Workspace => {
89 let condition = ruleset.spec.conditions.repository.clone().unwrap_or_default();
90 repo_matches(&condition, repo)
91 }
92 }
93}
94
95/// Who is changing something, as bypass lists name people.
96#[derive(Clone, Debug, Default, PartialEq, Eq)]
97pub struct ActorFacts {
98 pub username: String,
99 pub kind: Who,
100 /// Their role on the repository.
101 pub role: Option<RepoRole>,
102 /// Whether they own its workspace.
103 pub owner: bool,
104 /// The teams they are in, as `workspace/slug`, lowercase.
105 pub teams: Vec<String>,
106 /// The token they act through, if any.
107 pub token_id: Option<String>,
108}
109
110/// What kind of actor.
111#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
112pub enum Who {
113 #[default]
114 Person,
115 /// An agent acting through a token, g1t's or another.
116 Agent,
117 /// g1t acting on its own: the merge queue, security updates.
118 G1t,
119 /// A workspace's own token.
120 Token,
121}
122
123impl Who {
124 pub fn as_str(self) -> &'static str {
125 match self {
126 Who::Person => "person",
127 Who::Agent => "agent",
128 Who::G1t => "g1t",
129 Who::Token => "token",
130 }
131 }
132
133 /// Rules for agents hold for agents and g1t; the rest are people's.
134 pub fn is_agent(self) -> bool {
135 matches!(self, Who::Agent | Who::G1t)
136 }
137}
138
139impl ActorFacts {
140 /// What `user` is in `repo`. Their teams are the caller's to add.
141 pub fn of(user: &User, repo: &Repo) -> ActorFacts {
142 let kind = match user.kind {
143 PrincipalKind::System => Who::G1t,
144 PrincipalKind::Agent => Who::Agent,
145 _ if user.acting.is_some() => Who::Agent,
146 PrincipalKind::Workspace => Who::Token,
147 PrincipalKind::User => Who::Person,
148 };
149 let token_id = user
150 .acting
151 .as_ref()
152 .map(|acting| acting.credential_id.clone())
153 .or_else(|| user.token.as_ref().map(|token| token.token_id.clone()))
154 .filter(|id| !id.is_empty());
155 ActorFacts {
156 username: user.username.clone(),
157 kind,
158 role: access::permission(Some(user), repo),
159 owner: user.role_in(&repo.namespace.to_lowercase()) == Some(Role::Owner),
160 teams: Vec::new(),
161 token_id,
162 }
163 }
164}
165
166/// A team named in a bypass list or a rule, as `workspace/slug`.
167pub fn team_key(name: &str, workspace: &str) -> String {
168 let name = name.trim().trim_start_matches('@').to_lowercase();
169 if name.contains('/') { name } else { format!("{}/{name}", workspace.to_lowercase()) }
170}
171
172/// Whether one bypass entry names the actor. An agent or a token is never
173/// named by a role, a team or a person: only by `g1t` or its token, so an
174/// agent acting for an admin obeys the rules its admin may bypass.
175fn names(entry: &BypassActor, who: &ActorFacts, workspace: &str) -> bool {
176 let value = entry.value.trim();
177 match entry.kind {
The work, events, webhooks, actions, repos and packages services no longer run g1t's own agents. Work drops the 35 calls that queued issues for g1t, delegated them, messaged and reviewed through the agent, planned, captured memories automatically, rated confidence and capped agent spend, and its pull request lifecycle no longer revises or reviews: a pull request only advances to a merge by the repository's auto-merge setting. Merging a pull request that is behind now catches it up itself, through the repository's branch update, and lands it when the push arrives; when it can't be caught up cleanly the merge says to merge the base in and try again. Recording a session on a pull request, memory recall and remember, overlaps, the merge queue and required checks stay as they were.178 // g1t on its own, as `System`: the merge queue, security updates,
179 // a merge its settings make. An agent acting through a token is
180 // named only by that token.
181 ActorKind::G1t => who.kind == Who::G1t,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge182 ActorKind::Token => {
183 (value.eq_ignore_ascii_case("workspace") && who.kind == Who::Token)
184 || who.token_id.as_deref().is_some_and(|id| !value.is_empty() && id == value)
185 }
186 _ if who.kind != Who::Person => false,
187 ActorKind::User => !value.is_empty() && who.username.eq_ignore_ascii_case(value.trim_start_matches('@')),
188 ActorKind::Team => !value.is_empty() && who.teams.contains(&team_key(value, workspace)),
189 ActorKind::Role => match value.to_ascii_lowercase().as_str() {
190 "owner" => who.owner,
191 role => RepoRole::parse(role).is_some_and(|wanted| who.role.is_some_and(|has| has >= wanted)),
192 },
193 }
194}
195
196/// How the actor may bypass a ruleset with these bypass actors, if at all.
197/// `always` wins over `pull_requests` when both name them.
198pub fn bypass(actors: &[BypassActor], who: &ActorFacts, workspace: &str) -> Option<BypassMode> {
199 let modes: Vec<BypassMode> = actors.iter().filter(|entry| names(entry, who, workspace)).map(|entry| entry.mode).collect();
200 if modes.contains(&BypassMode::Always) {
201 Some(BypassMode::Always)
202 } else {
203 modes.first().copied()
204 }
205}
206
207/// Whether any bypass list names a team: only then are the actor's teams
208/// worth looking up.
209pub fn names_teams(rulesets: &[Ruleset]) -> bool {
210 rulesets
211 .iter()
212 .any(|ruleset| ruleset.spec.bypass_actors.iter().any(|entry| entry.kind == ActorKind::Team))
213}
214
215/// The rulesets that hold in `repo` for any of `refs` (full refs), as a
216/// service applies them, with how the actor may bypass each.
217pub fn applicable(rulesets: &[Ruleset], repo: RepoFacts<'_>, refs: &[String], who: Option<&ActorFacts>, workspace: &str) -> Vec<Applicable> {
218 rulesets
219 .iter()
220 .filter(|ruleset| holds_in(ruleset, repo))
221 .filter(|ruleset| {
222 refs.iter().any(|git_ref| match Target::of_ref(git_ref) {
223 Some((target, name)) => {
224 target == ruleset.spec.target
225 && ref_matches(&ruleset.spec.conditions.ref_name, target, name, repo.default_branch)
226 }
227 None => false,
228 })
229 })
230 .map(|ruleset| Applicable {
231 id: ruleset.id.clone(),
232 name: ruleset.spec.name.clone(),
233 level: ruleset.level,
234 enforcement: ruleset.spec.enforcement,
235 target: ruleset.spec.target,
236 conditions: ruleset.spec.conditions.ref_name.clone(),
237 rules: ruleset.spec.rules.clone(),
238 bypass: who.and_then(|who| bypass(&ruleset.spec.bypass_actors, who, workspace)),
239 })
240 .collect()
241}
242
243/// Whether an applicable ruleset holds for a full ref.
244pub fn applies_to_ref(ruleset: &Applicable, git_ref: &str, default_branch: &str) -> bool {
245 match Target::of_ref(git_ref) {
246 Some((target, name)) => target == ruleset.target && ref_matches(&ruleset.conditions, target, name, default_branch),
247 None => false,
248 }
249}
250
251/// Every rule that holds for one branch or tag: active rulesets' first,
252/// then those being evaluated, each with where it comes from.
253pub fn effective(rulesets: &[Ruleset], repo: RepoFacts<'_>, target: Target, name: &str) -> EffectiveRules {
254 let mut holding: Vec<&Ruleset> = rulesets
255 .iter()
256 .filter(|ruleset| holds_in(ruleset, repo))
257 .filter(|ruleset| ruleset.spec.target == target)
258 .filter(|ruleset| ref_matches(&ruleset.spec.conditions.ref_name, target, name, repo.default_branch))
259 .collect();
260 // Active before evaluate; workspace before repository; then by name.
261 holding.sort_by_key(|ruleset| {
262 (
263 ruleset.spec.enforcement != Enforcement::Active,
264 ruleset.level != Level::Workspace,
265 ruleset.spec.name.to_lowercase(),
266 )
267 });
268 EffectiveRules {
269 name: name.to_owned(),
270 target,
271 default_branch: target == Target::Branch && name == repo.default_branch,
272 rules: holding
273 .iter()
274 .flat_map(|ruleset| {
275 ruleset.spec.rules.iter().map(|entry| EffectiveRule {
276 entry: entry.clone(),
277 ruleset_id: ruleset.id.clone(),
278 ruleset_name: ruleset.spec.name.clone(),
279 level: ruleset.level,
280 enforcement: ruleset.spec.enforcement,
281 })
282 })
283 .collect(),
284 rulesets: holding
285 .iter()
286 .map(|ruleset| RulesetSummary {
287 id: ruleset.id.clone(),
288 name: ruleset.spec.name.clone(),
289 level: ruleset.level,
290 enforcement: ruleset.spec.enforcement,
291 bypass_actors: ruleset.spec.bypass_actors.clone(),
292 })
293 .collect(),
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use g1t_contracts::rules::{BypassMode, Conditions, NoParameters, Rule, RuleEntry, RulesetSpec};
301
302 pub(crate) fn ruleset(id: &str, level: Level, include: &[&str], exclude: &[&str], rules: Vec<Rule>) -> Ruleset {
303 Ruleset {
304 id: id.into(),
305 level,
306 workspace: "acme".into(),
307 repo_id: (level == Level::Repository).then(|| "rep_1".to_owned()),
308 repository: None,
309 spec: RulesetSpec {
310 name: id.into(),
311 conditions: Conditions {
312 ref_name: RefCondition {
313 include: include.iter().map(|p| (*p).to_owned()).collect(),
314 exclude: exclude.iter().map(|p| (*p).to_owned()).collect(),
315 },
316 repository: None,
317 },
318 rules: rules.into_iter().map(RuleEntry::everyone).collect(),
319 ..RulesetSpec::default()
320 },
321 source: None,
322 created_by: "ada".into(),
323 created_at: String::new(),
324 updated_by: "ada".into(),
325 updated_at: String::new(),
326 }
327 }
328
329 fn repo<'a>(topics: &'a [String]) -> RepoFacts<'a> {
330 RepoFacts { id: "rep_1", name: "web", private: true, topics, default_branch: "main" }
331 }
332
333 #[test]
334 fn names_match_patterns_the_default_branch_and_all() {
335 let condition = |include: &[&str], exclude: &[&str]| RefCondition {
336 include: include.iter().map(|p| (*p).to_owned()).collect(),
337 exclude: exclude.iter().map(|p| (*p).to_owned()).collect(),
338 };
339 assert!(ref_matches(&condition(&["~DEFAULT_BRANCH"], &[]), Target::Branch, "main", "main"));
340 assert!(!ref_matches(&condition(&["~DEFAULT_BRANCH"], &[]), Target::Branch, "dev", "main"));
341 assert!(!ref_matches(&condition(&["~DEFAULT_BRANCH"], &[]), Target::Tag, "main", "main"));
342 assert!(ref_matches(&condition(&["~ALL"], &["dependabot/**"]), Target::Branch, "feature/x", "main"));
343 assert!(!ref_matches(&condition(&["~ALL"], &["g1t-queue/**"]), Target::Branch, "g1t-queue/a", "main"));
344 assert!(ref_matches(&condition(&["refs/heads/release/*"], &[]), Target::Branch, "release/2", "main"));
345 assert!(ref_matches(&condition(&["v*"], &[]), Target::Tag, "v1.0.0", "main"));
346 assert!(!ref_matches(&condition(&[], &[]), Target::Branch, "main", "main"), "an empty include selects nothing");
347 }
348
349 #[test]
350 fn workspace_rulesets_select_repositories_by_name_visibility_and_topic() {
351 let topics = vec!["payments".to_owned()];
352 let mut condition = RepositoryCondition::default();
353 assert!(repo_matches(&condition, repo(&topics)));
354 condition.include = vec!["api-*".into()];
355 assert!(!repo_matches(&condition, repo(&topics)));
356 condition.include = vec!["W*".into()];
357 assert!(repo_matches(&condition, repo(&topics)), "names ignore case");
358 condition.exclude = vec!["web".into()];
359 assert!(!repo_matches(&condition, repo(&topics)));
360 condition.exclude.clear();
361 condition.visibility = VisibilityCondition::Public;
362 assert!(!repo_matches(&condition, repo(&topics)));
363 condition.visibility = VisibilityCondition::Private;
364 condition.topics = vec!["Payments".into()];
365 assert!(repo_matches(&condition, repo(&topics)));
366 condition.topics = vec!["docs".into()];
367 assert!(!repo_matches(&condition, repo(&topics)));
368 }
369
370 #[test]
371 fn a_repository_ruleset_holds_only_in_its_repository_and_disabled_ones_nowhere() {
372 let topics = Vec::new();
373 let own = ruleset("a", Level::Repository, &["~ALL"], &[], vec![]);
374 assert!(holds_in(&own, repo(&topics)));
375 let other = Ruleset { repo_id: Some("rep_2".into()), ..own.clone() };
376 assert!(!holds_in(&other, repo(&topics)));
377 let mut off = own.clone();
378 off.spec.enforcement = Enforcement::Disabled;
379 assert!(!holds_in(&off, repo(&topics)));
380 }
381
382 fn person(role: RepoRole) -> ActorFacts {
383 ActorFacts { username: "ada".into(), kind: Who::Person, role: Some(role), ..ActorFacts::default() }
384 }
385
386 fn entry(kind: ActorKind, value: &str, mode: BypassMode) -> BypassActor {
387 BypassActor { kind, value: value.into(), mode }
388 }
389
390 #[test]
391 fn nobody_bypasses_by_default() {
392 assert_eq!(bypass(&[], &person(RepoRole::Admin), "acme"), None);
393 let g1t = ActorFacts { kind: Who::G1t, username: "g1t".into(), ..ActorFacts::default() };
394 assert_eq!(bypass(&[], &g1t, "acme"), None);
395 }
396
397 #[test]
398 fn roles_people_and_teams_bypass_as_listed() {
399 let list = [entry(ActorKind::Role, "maintain", BypassMode::PullRequests)];
400 assert_eq!(bypass(&list, &person(RepoRole::Admin), "acme"), Some(BypassMode::PullRequests));
401 assert_eq!(bypass(&list, &person(RepoRole::Write), "acme"), None);
402 let both = [
403 entry(ActorKind::Role, "write", BypassMode::PullRequests),
404 entry(ActorKind::User, "@Ada", BypassMode::Always),
405 ];
406 assert_eq!(bypass(&both, &person(RepoRole::Write), "acme"), Some(BypassMode::Always));
407 let team = [entry(ActorKind::Team, "release", BypassMode::Always)];
408 let mut ada = person(RepoRole::Read);
409 assert_eq!(bypass(&team, &ada, "acme"), None);
410 ada.teams.push("acme/release".into());
411 assert_eq!(bypass(&team, &ada, "acme"), Some(BypassMode::Always));
412 let owners = [entry(ActorKind::Role, "owner", BypassMode::Always)];
413 assert_eq!(bypass(&owners, &ada, "acme"), None);
414 ada.owner = true;
415 assert_eq!(bypass(&owners, &ada, "acme"), Some(BypassMode::Always));
416 }
417
418 #[test]
The work, events, webhooks, actions, repos and packages services no longer run g1t's own agents. Work drops the 35 calls that queued issues for g1t, delegated them, messaged and reviewed through the agent, planned, captured memories automatically, rated confidence and capped agent spend, and its pull request lifecycle no longer revises or reviews: a pull request only advances to a merge by the repository's auto-merge setting. Merging a pull request that is behind now catches it up itself, through the repository's branch update, and lands it when the push arrives; when it can't be caught up cleanly the merge says to merge the base in and try again. Recording a session on a pull request, memory recall and remember, overlaps, the merge queue and required checks stay as they were.419 fn agents_bypass_only_when_their_token_is_listed() {
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge420 let agent = ActorFacts {
421 username: "g1t".into(),
422 kind: Who::Agent,
423 role: Some(RepoRole::Admin),
424 owner: true,
425 token_id: Some("tok_1".into()),
426 ..ActorFacts::default()
427 };
428 let admins = [entry(ActorKind::Role, "admin", BypassMode::Always), entry(ActorKind::Role, "owner", BypassMode::Always)];
429 assert_eq!(bypass(&admins, &agent, "acme"), None, "an agent does not take its person's role");
The work, events, webhooks, actions, repos and packages services no longer run g1t's own agents. Work drops the 35 calls that queued issues for g1t, delegated them, messaged and reviewed through the agent, planned, captured memories automatically, rated confidence and capped agent spend, and its pull request lifecycle no longer revises or reviews: a pull request only advances to a merge by the repository's auto-merge setting. Merging a pull request that is behind now catches it up itself, through the repository's branch update, and lands it when the push arrives; when it can't be caught up cleanly the merge says to merge the base in and try again. Recording a session on a pull request, memory recall and remember, overlaps, the merge queue and required checks stay as they were.430 assert_eq!(bypass(&[entry(ActorKind::G1t, "", BypassMode::Always)], &agent, "acme"), None, "g1t names g1t itself, not agents");
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge431 assert_eq!(bypass(&[entry(ActorKind::Token, "tok_1", BypassMode::Always)], &agent, "acme"), Some(BypassMode::Always));
432 assert_eq!(bypass(&[entry(ActorKind::Token, "tok_2", BypassMode::Always)], &agent, "acme"), None);
433 let system = ActorFacts { kind: Who::G1t, ..ActorFacts::default() };
434 assert_eq!(bypass(&[entry(ActorKind::G1t, "", BypassMode::PullRequests)], &system, "acme"), Some(BypassMode::PullRequests));
435 let token = ActorFacts { kind: Who::Token, ..ActorFacts::default() };
436 assert_eq!(bypass(&[entry(ActorKind::Token, "workspace", BypassMode::Always)], &token, "acme"), Some(BypassMode::Always));
437 }
438
439 #[test]
440 fn effective_rules_stack_every_ruleset_that_holds() {
441 let topics = Vec::new();
442 let mut evaluate = ruleset("b-dry", Level::Repository, &["main"], &[], vec![Rule::RequiredLinearHistory(NoParameters {})]);
443 evaluate.spec.enforcement = Enforcement::Evaluate;
444 let rulesets = vec![
445 evaluate,
446 ruleset("a-main", Level::Repository, &["~DEFAULT_BRANCH"], &[], vec![Rule::Deletion(NoParameters {}), Rule::NonFastForward(NoParameters {})]),
447 ruleset("org", Level::Workspace, &["~ALL"], &[], vec![Rule::Deletion(NoParameters {})]),
448 ruleset("release", Level::Repository, &["release/*"], &[], vec![Rule::Creation(NoParameters {})]),
449 ];
450 let main = effective(&rulesets, repo(&topics), Target::Branch, "main");
451 assert!(main.default_branch);
452 let from: Vec<(&str, &str)> = main.rules.iter().map(|rule| (rule.ruleset_id.as_str(), rule.entry.rule.kind())).collect();
453 assert_eq!(
454 from,
455 vec![("org", "deletion"), ("a-main", "deletion"), ("a-main", "non_fast_forward"), ("b-dry", "required_linear_history")]
456 );
457 assert_eq!(main.rulesets.len(), 3);
458 let release = effective(&rulesets, repo(&topics), Target::Branch, "release/1");
459 assert_eq!(release.rules.iter().map(|rule| rule.entry.rule.kind()).collect::<Vec<_>>(), vec!["deletion", "creation"]);
460 assert!(effective(&rulesets, repo(&topics), Target::Tag, "main").rules.is_empty());
461 }
462
463 #[test]
464 fn applicable_rulesets_carry_the_actors_bypass() {
465 let topics = Vec::new();
466 let mut guarded = ruleset("a", Level::Repository, &["~DEFAULT_BRANCH"], &[], vec![Rule::Update(NoParameters {})]);
467 guarded.spec.bypass_actors = vec![entry(ActorKind::Role, "admin", BypassMode::Always)];
468 let rulesets = vec![guarded, ruleset("b", Level::Repository, &["feature/*"], &[], vec![])];
469 let found = applicable(&rulesets, repo(&topics), &["refs/heads/main".into()], Some(&person(RepoRole::Admin)), "acme");
470 assert_eq!(found.len(), 1);
471 assert_eq!(found[0].bypass, Some(BypassMode::Always));
472 assert!(applies_to_ref(&found[0], "refs/heads/main", "main"));
473 assert!(!applies_to_ref(&found[0], "refs/tags/main", "main"));
474 let none = applicable(&rulesets, repo(&topics), &["refs/heads/main".into()], Some(&person(RepoRole::Write)), "acme");
475 assert_eq!(none[0].bypass, None);
476 }
477}

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