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.

1,461 linesCodeBlameRaw
1//! Rulesets: what may happen to a repository's branches and tags, and what
2//! a pull request needs before it merges.
3//!
4//! A **ruleset** belongs to a repository, or to a workspace and through it
5//! to every repository it selects. It targets branches or tags by name
6//! (fnmatch patterns, `~DEFAULT_BRANCH`, `~ALL`), lists the rules that hold
7//! there, and who may bypass them. Its enforcement is `active` (rules hold),
8//! `evaluate` (nothing is refused; what would have been is recorded), or
9//! `disabled`.
10//!
11//! Several rulesets can target the same branch. They stack: every rule of
12//! every active ruleset holds, so the most restrictive wins (the largest
13//! approval count, every required check, the narrowest merge window).
14//!
15//! Each rule can hold for everyone, only for agents' changes, or only for
16//! people's ([`AppliesTo`]). Agents, g1t's own included, obey rules exactly
17//! as people do unless a ruleset lists them as a bypass actor: nobody
18//! bypasses by default.
19//!
20//! The rules engine (`crates/rules`) decides; the work service keeps the
21//! rulesets and every evaluation, and enforces them on merge; the repos
22//! service enforces them on push and on every change to a branch or tag.
23//!
24//! Rulesets travel in the shape the API shows them: `snake_case` fields,
25//! between services too, so that an exported ruleset imports unchanged on
26//! the site, through the API and through MCP. Mirrors
27//! `packages/contracts/src/rules.ts`.
28
29use serde::{Deserialize, Serialize};
30
31use crate::repos::RepoPath;
32use crate::{User, Viewer};
33
34/// The repository's default branch, whatever it is called at the time.
35pub const DEFAULT_BRANCH: &str = "~DEFAULT_BRANCH";
36/// Every branch or tag, or every repository.
37pub const ALL: &str = "~ALL";
38/// Rulesets a repository, or a workspace, may have.
39pub const MAX_RULESETS: usize = 75;
40/// Rules in one ruleset.
41pub const MAX_RULES: usize = 50;
42/// Patterns in one list (branches, paths, extensions, repositories).
43pub const MAX_PATTERNS: usize = 100;
44/// The longest pattern, regular expression or name kept.
45pub const MAX_PATTERN_CHARS: usize = 512;
46/// Bypass actors in one ruleset.
47pub const MAX_BYPASS_ACTORS: usize = 50;
48/// Required approvals a pull request rule may ask for.
49pub const MAX_APPROVALS: u32 = 10;
50/// The ruleset made from a repository's branch protection, as it was
51/// before rulesets: its `source`.
52pub const BRANCH_PROTECTION: &str = "branch_protection";
53
54/// Whether a ruleset's rules hold.
55#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57pub enum Enforcement {
58 /// Its rules hold, and what breaks them is refused.
59 #[default]
60 Active,
61 /// A dry run: nothing is refused, and every push or merge it would
62 /// have refused is recorded, for its insights.
63 Evaluate,
64 /// Kept, but not evaluated at all.
65 Disabled,
66}
67
68impl Enforcement {
69 pub fn as_str(self) -> &'static str {
70 match self {
71 Enforcement::Active => "active",
72 Enforcement::Evaluate => "evaluate",
73 Enforcement::Disabled => "disabled",
74 }
75 }
76}
77
78/// What a ruleset's name conditions match.
79#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
80#[serde(rename_all = "snake_case")]
81pub enum Target {
82 #[default]
83 Branch,
84 Tag,
85}
86
87impl Target {
88 pub fn as_str(self) -> &'static str {
89 match self {
90 Target::Branch => "branch",
91 Target::Tag => "tag",
92 }
93 }
94
95 /// The full ref of `name`: `refs/heads/<name>` or `refs/tags/<name>`.
96 pub fn full_ref(self, name: &str) -> String {
97 match self {
98 Target::Branch => format!("refs/heads/{name}"),
99 Target::Tag => format!("refs/tags/{name}"),
100 }
101 }
102
103 /// The target and short name of a full ref, if it is a branch or a tag.
104 pub fn of_ref(git_ref: &str) -> Option<(Target, &str)> {
105 if let Some(name) = git_ref.strip_prefix("refs/heads/") {
106 return Some((Target::Branch, name));
107 }
108 git_ref.strip_prefix("refs/tags/").map(|name| (Target::Tag, name))
109 }
110}
111
112/// Whose ruleset it is.
113#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
114#[serde(rename_all = "snake_case")]
115pub enum Level {
116 #[default]
117 Repository,
118 Workspace,
119}
120
121impl Level {
122 pub fn as_str(self) -> &'static str {
123 match self {
124 Level::Repository => "repository",
125 Level::Workspace => "workspace",
126 }
127 }
128}
129
130/// Which branches or tags a ruleset holds for, by name. A name matches when
131/// it matches an `include` pattern and no `exclude` pattern. Patterns are
132/// fnmatch: `*` matches within one path segment, `**` across them, `?` one
133/// character, `[abc]` one of a set. `~DEFAULT_BRANCH` is the default
134/// branch, `~ALL` everything. An empty `include` matches nothing.
135#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(default)]
137pub struct RefCondition {
138 pub include: Vec<String>,
139 pub exclude: Vec<String>,
140}
141
142/// Which visibility of repository a workspace ruleset selects.
143#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(rename_all = "snake_case")]
145pub enum VisibilityCondition {
146 #[default]
147 Any,
148 Public,
149 Private,
150}
151
152/// Which of a workspace's repositories its ruleset holds in: those whose
153/// name matches an `include` pattern (fnmatch, or `~ALL`) and no `exclude`
154/// one, of the `visibility` chosen, and, when `topics` is not empty,
155/// carrying at least one of them.
156#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(default)]
158pub struct RepositoryCondition {
159 pub include: Vec<String>,
160 pub exclude: Vec<String>,
161 pub visibility: VisibilityCondition,
162 pub topics: Vec<String>,
163}
164
165impl Default for RepositoryCondition {
166 fn default() -> Self {
167 RepositoryCondition {
168 include: vec![ALL.to_owned()],
169 exclude: Vec::new(),
170 visibility: VisibilityCondition::Any,
171 topics: Vec::new(),
172 }
173 }
174}
175
176/// Where a ruleset holds. `repository` is a workspace ruleset's only.
177#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
178#[serde(default)]
179pub struct Conditions {
180 pub ref_name: RefCondition,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub repository: Option<RepositoryCondition>,
183}
184
185/// Who a bypass actor is.
186#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
187#[serde(rename_all = "snake_case")]
188pub enum ActorKind {
189 /// Everyone with at least this repository role (`value`: `write`,
190 /// `maintain` or `admin`), or the workspace's owners (`owner`).
191 Role,
192 /// The people of a team (`value`: its slug, or `workspace/slug`), its
193 /// child teams' people included.
194 Team,
195 /// One person, by username.
196 User,
197 /// An access token, by its id; `value` `workspace` is any of the
198 /// workspace's own tokens.
199 Token,
200 /// g1t: its agent at work in a sandbox, and the platform acting on its
201 /// own (the merge queue, security updates). Never a bypass actor
202 /// unless listed.
203 G1t,
204}
205
206/// When a bypass actor may bypass.
207#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
208#[serde(rename_all = "snake_case")]
209pub enum BypassMode {
210 /// Always: pushes and merges alike.
211 #[default]
212 Always,
213 /// Only when merging a pull request; their pushes obey the rules.
214 PullRequests,
215}
216
217impl BypassMode {
218 pub fn as_str(self) -> &'static str {
219 match self {
220 BypassMode::Always => "always",
221 BypassMode::PullRequests => "pull_requests",
222 }
223 }
224}
225
226/// Someone a ruleset does not hold for.
227#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
228pub struct BypassActor {
229 pub kind: ActorKind,
230 /// Who, as [`ActorKind`] says. Empty for `g1t`.
231 #[serde(default)]
232 pub value: String,
233 #[serde(default)]
234 pub mode: BypassMode,
235}
236
237/// Whose changes a rule holds for.
238#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
239#[serde(rename_all = "snake_case")]
240pub enum AppliesTo {
241 #[default]
242 Everyone,
243 /// Only agents' changes: a push by an agent, a pull request an agent
244 /// made (g1t's or another's through a token).
245 Agents,
246 /// Only people's changes.
247 People,
248}
249
250impl AppliesTo {
251 pub fn as_str(self) -> &'static str {
252 match self {
253 AppliesTo::Everyone => "everyone",
254 AppliesTo::Agents => "agents",
255 AppliesTo::People => "people",
256 }
257 }
258
259 /// Whether it holds for a change by an agent (`agent`) or a person.
260 pub fn covers(self, agent: bool) -> bool {
261 match self {
262 AppliesTo::Everyone => true,
263 AppliesTo::Agents => agent,
264 AppliesTo::People => !agent,
265 }
266 }
267}
268
269/// A rule with no parameters.
270#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
271pub struct NoParameters {}
272
273/// How a pull request is merged.
274#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
275#[serde(rename_all = "snake_case")]
276pub enum MergeMethod {
277 /// A merge commit joins the branch to the base: every commit of the
278 /// branch lands, with one more on top whose parents are both.
279 Merge,
280 /// One new commit on the base holds everything the branch changed.
281 Squash,
282 /// Each commit of the branch is made again on top of the base, in
283 /// order, and the base moves to the last.
284 Rebase,
285 /// The base moves to the branch's head, exactly the commit that was
286 /// checked: no new commit. Only possible when the branch contains the
287 /// base's head.
288 FastForward,
289}
290
291impl MergeMethod {
292 pub const ALL: [MergeMethod; 4] = [MergeMethod::Merge, MergeMethod::Squash, MergeMethod::Rebase, MergeMethod::FastForward];
293
294 pub fn as_str(self) -> &'static str {
295 match self {
296 MergeMethod::Merge => "merge",
297 MergeMethod::Squash => "squash",
298 MergeMethod::Rebase => "rebase",
299 MergeMethod::FastForward => "fast_forward",
300 }
301 }
302
303 /// Whether every commit that lands has one parent: what required
304 /// linear history asks of the base branch.
305 pub fn is_linear(self) -> bool {
306 !matches!(self, MergeMethod::Merge)
307 }
308}
309
310/// `pull_request`: changes reach the branch only by merging a pull request,
311/// and the pull request needs what this says first.
312#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
313#[serde(default)]
314pub struct PullRequestRule {
315 /// Approving reviews needed. A reviewer who has since asked for
316 /// changes blocks it; nobody approves their own.
317 pub required_approvals: u32,
318 /// Whether an agent's approval (g1t's reviewer) counts towards
319 /// `required_approvals`. Off means only people's approvals count.
320 pub count_agent_approvals: bool,
321 /// Approvals given before the latest push no longer count.
322 pub dismiss_stale_reviews_on_push: bool,
323 /// The code owners of every file it changes must approve.
324 pub require_code_owner_review: bool,
325 /// Someone other than whoever pushed last must approve after that push.
326 pub require_last_push_approval: bool,
327 /// The ways it may be merged. Empty allows every one.
328 pub allowed_merge_methods: Vec<MergeMethod>,
329 /// Pull requests need what this rule says, but pushes straight to the
330 /// branch are still allowed. Off (the default) refuses them. Only the
331 /// ruleset made from branch protection that did not require pull
332 /// requests turns it on.
333 #[serde(skip_serializing_if = "std::ops::Not::not")]
334 pub allow_direct_pushes: bool,
335}
336
337impl Default for PullRequestRule {
338 fn default() -> Self {
339 PullRequestRule {
340 required_approvals: 0,
341 count_agent_approvals: true,
342 dismiss_stale_reviews_on_push: false,
343 require_code_owner_review: false,
344 require_last_push_approval: false,
345 allowed_merge_methods: Vec::new(),
346 allow_direct_pushes: false,
347 }
348 }
349}
350
351/// Where a required check's status must come from.
352#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
353#[serde(rename_all = "snake_case")]
354pub enum Integration {
355 /// Workflow runs (`.g1t/workflows`).
356 Actions,
357 /// Deployments: `g1t / deploy`.
358 Deployments,
359 /// The security suite: code scanning and dependency review.
360 Security,
361 /// g1t itself, such as code owners.
362 G1t,
363 /// Reported through the API, by a token or an integration: a status
364 /// or a check run.
365 Api,
366}
367
368impl Integration {
369 pub fn as_str(self) -> &'static str {
370 match self {
371 Integration::Actions => "actions",
372 Integration::Deployments => "deployments",
373 Integration::Security => "security",
374 Integration::G1t => "g1t",
375 Integration::Api => "api",
376 }
377 }
378
379 pub fn parse(text: &str) -> Option<Integration> {
380 match text {
381 "actions" => Some(Integration::Actions),
382 "deployments" => Some(Integration::Deployments),
383 "security" => Some(Integration::Security),
384 "g1t" => Some(Integration::G1t),
385 "api" => Some(Integration::Api),
386 _ => None,
387 }
388 }
389}
390
391/// One check that must pass: a workflow's name (`CI`) or another status's
392/// context, and, if set, the integration that must have reported it.
393#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
394pub struct RequiredCheck {
395 pub context: String,
396 #[serde(default, skip_serializing_if = "Option::is_none")]
397 pub integration: Option<Integration>,
398}
399
400/// `required_status_checks`: these checks must pass on a pull request's
401/// head before it merges.
402#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
403#[serde(default)]
404#[derive(Default)]
405pub struct StatusChecksRule {
406 pub checks: Vec<RequiredCheck>,
407 /// The pull request must contain the branch's latest commits, so that
408 /// what merges is what was checked.
409 pub strict: bool,
410 /// Required only when the pull request changes a file matching one of
411 /// these patterns. Empty: always.
412 pub paths: Vec<String>,
413 /// Someone who may merge can merge past checks that have not passed,
414 /// saying so as they merge.
415 pub allow_bypass_on_merge: bool,
416}
417
418
419/// `merge_queue`: merging joins the queue, which tests each pull request
420/// together with those ahead of it. The queue lands on the default branch.
421#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
422#[serde(default)]
423pub struct MergeQueueRule {
424 pub merge_method: MergeMethod,
425 /// Entries tested at once.
426 pub max_entries_to_build: u32,
427 /// Entries a batch waits for before it starts, unless the oldest has
428 /// waited `min_entries_wait_minutes`.
429 pub min_entries_to_merge: u32,
430 pub min_entries_wait_minutes: u32,
431 /// How long a batch's checks may take before it is tested again.
432 pub check_response_timeout_minutes: u32,
433}
434
435impl Default for MergeQueueRule {
436 fn default() -> Self {
437 MergeQueueRule {
438 merge_method: MergeMethod::Merge,
439 max_entries_to_build: 4,
440 min_entries_to_merge: 1,
441 min_entries_wait_minutes: 0,
442 check_response_timeout_minutes: 45,
443 }
444 }
445}
446
447/// `required_deployments`: a pull request's head must have deployed
448/// successfully to these environments: `preview` (its preview), a
449/// project's slug for a repository with several, or any environment
450/// deployments are reported to (its `deploy / <environment>` check).
451#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
452#[serde(default)]
453pub struct DeploymentsRule {
454 pub environments: Vec<String>,
455}
456
457/// How a pattern rule compares.
458#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
459#[serde(rename_all = "snake_case")]
460pub enum PatternOperator {
461 #[default]
462 StartsWith,
463 EndsWith,
464 Contains,
465 /// A regular expression, run by a linear-time engine.
466 Regex,
467}
468
469/// A rule about text: a commit message, an author's or committer's email
470/// address, a branch's or tag's name. The text must match the pattern, or
471/// with `negate`, must not.
472#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
473#[serde(default)]
474pub struct PatternRule {
475 /// What people are told the rule is, such as "Conventional commits".
476 pub name: String,
477 pub operator: PatternOperator,
478 pub pattern: String,
479 pub negate: bool,
480}
481
482/// `file_path_restriction`: pushes and pull requests may not change files
483/// matching these patterns (fnmatch, `**` across directories).
484#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
485#[serde(default)]
486pub struct FilePathRule {
487 pub restricted_file_paths: Vec<String>,
488}
489
490/// `file_extension_restriction`: files with these extensions (`.exe`,
491/// `.zip`) may not be added or changed.
492#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
493#[serde(default)]
494pub struct FileExtensionRule {
495 pub restricted_file_extensions: Vec<String>,
496}
497
498/// `max_file_size`: no file larger than this, in megabytes (1 to 100).
499#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
500#[serde(default)]
501pub struct MaxFileSizeRule {
502 pub max_file_size_mb: u32,
503}
504
505impl Default for MaxFileSizeRule {
506 fn default() -> Self {
507 MaxFileSizeRule { max_file_size_mb: 10 }
508 }
509}
510
511/// `max_file_path_length`: no path longer than this many characters.
512#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
513#[serde(default)]
514pub struct MaxFilePathLengthRule {
515 pub max_file_path_length: u32,
516}
517
518impl Default for MaxFilePathLengthRule {
519 fn default() -> Self {
520 MaxFilePathLengthRule { max_file_path_length: 255 }
521 }
522}
523
524/// `max_files_changed`: a push's commits, each, and a pull request as a
525/// whole, change at most this many files.
526#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
527#[serde(default)]
528pub struct MaxFilesChangedRule {
529 pub max_files: u32,
530}
531
532impl Default for MaxFilesChangedRule {
533 fn default() -> Self {
534 MaxFilesChangedRule { max_files: 100 }
535 }
536}
537
538/// `path_review`: a pull request that changes a file matching `paths`
539/// needs `required_approvals` from people, from `team` when one is named
540/// (its slug, or `workspace/slug`).
541#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
542#[serde(default)]
543pub struct PathReviewRule {
544 pub paths: Vec<String>,
545 pub required_approvals: u32,
546 #[serde(skip_serializing_if = "Option::is_none")]
547 pub team: Option<String>,
548}
549
550impl Default for PathReviewRule {
551 fn default() -> Self {
552 PathReviewRule { paths: Vec::new(), required_approvals: 1, team: None }
553 }
554}
555
556/// A day of the week.
557#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
558#[serde(rename_all = "snake_case")]
559pub enum Weekday {
560 Mon,
561 Tue,
562 Wed,
563 Thu,
564 Fri,
565 Sat,
566 Sun,
567}
568
569impl Weekday {
570 pub const ALL: [Weekday; 7] = [
571 Weekday::Mon,
572 Weekday::Tue,
573 Weekday::Wed,
574 Weekday::Thu,
575 Weekday::Fri,
576 Weekday::Sat,
577 Weekday::Sun,
578 ];
579
580 pub fn as_str(self) -> &'static str {
581 match self {
582 Weekday::Mon => "mon",
583 Weekday::Tue => "tue",
584 Weekday::Wed => "wed",
585 Weekday::Thu => "thu",
586 Weekday::Fri => "fri",
587 Weekday::Sat => "sat",
588 Weekday::Sun => "sun",
589 }
590 }
591}
592
593/// Hours on some days of the week when merging is allowed, `HH:MM` to
594/// `HH:MM` in the rule's time zone. An `end` before `start` runs past
595/// midnight.
596#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
597pub struct WeeklyWindow {
598 pub days: Vec<Weekday>,
599 pub start: String,
600 pub end: String,
601}
602
603/// A stretch of time, RFC 3339 UTC. With no `end`, it lasts until removed:
604/// an incident freeze.
605#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
606pub struct Period {
607 pub start: String,
608 #[serde(default)]
609 pub end: Option<String>,
610 #[serde(default)]
611 pub reason: String,
612}
613
614/// `merge_window`: when pull requests may merge into the branch. Outside
615/// every `windows` entry (when there are any), or during a `freezes` one,
616/// merging waits, unless an `exceptions` entry covers the moment.
617#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
618#[serde(default)]
619pub struct MergeWindowRule {
620 /// A fixed offset from UTC, `+02:00` or `-05:00`; `UTC` or empty is UTC.
621 /// Daylight saving time is not applied.
622 pub time_zone: String,
623 pub windows: Vec<WeeklyWindow>,
624 pub freezes: Vec<Period>,
625 pub exceptions: Vec<Period>,
626}
627
628impl Default for MergeWindowRule {
629 fn default() -> Self {
630 MergeWindowRule {
631 time_zone: "UTC".to_owned(),
632 windows: Vec::new(),
633 freezes: Vec::new(),
634 exceptions: Vec::new(),
635 }
636 }
637}
638
639/// `agent_auto_merge`: whether g1t lands an agent's ready pull request into
640/// the branch without a person pressing merge. The repository's auto-merge
641/// setting must be on as well. A `minimum_confidence` written before
642/// confidence ratings were removed is ignored.
643#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
644#[serde(default)]
645pub struct AgentAutoMergeRule {
646 pub allowed: bool,
647}
648
649impl Default for AgentAutoMergeRule {
650 fn default() -> Self {
651 AgentAutoMergeRule { allowed: true }
652 }
653}
654
655/// Rule types that are no longer offered: `confidence_threshold` and
656/// `cost_cap`, which were about g1t's own agents. A stored ruleset that has
657/// one still reads; the rule is left out, so it holds for nothing.
658pub const RETIRED_RULES: [&str; 2] = ["confidence_threshold", "cost_cap"];
659
660/// Reads a list of rules, leaving out those of a [retired](RETIRED_RULES)
661/// type.
662fn live_rules<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Vec<RuleEntry>, D::Error> {
663 let written = Vec::<serde_json::Value>::deserialize(deserializer)?;
664 written
665 .into_iter()
666 .filter(|entry| !entry.get("type").and_then(serde_json::Value::as_str).is_some_and(|kind| RETIRED_RULES.contains(&kind)))
667 .map(|entry| serde_json::from_value(entry).map_err(serde::de::Error::custom))
668 .collect()
669}
670
671/// One rule and its parameters, tagged by `type`.
672#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
673#[serde(tag = "type", content = "parameters", rename_all = "snake_case")]
674pub enum Rule {
675 /// Only bypass actors may create a matching branch or tag.
676 Creation(NoParameters),
677 /// Only bypass actors may push to (move) a matching branch or tag.
678 Update(NoParameters),
679 /// Only bypass actors may delete a matching branch or tag.
680 Deletion(NoParameters),
681 /// Nobody force pushes: a push must only add to its history.
682 NonFastForward(NoParameters),
683 /// No merge commits: history stays a straight line.
684 RequiredLinearHistory(NoParameters),
685 /// Every commit carries a signature g1t verifies.
686 RequiredSignatures(NoParameters),
687 PullRequest(PullRequestRule),
688 RequiredStatusChecks(StatusChecksRule),
689 MergeQueue(MergeQueueRule),
690 RequiredDeployments(DeploymentsRule),
691 CommitMessagePattern(PatternRule),
692 CommitAuthorEmailPattern(PatternRule),
693 CommitterEmailPattern(PatternRule),
694 BranchNamePattern(PatternRule),
695 TagNamePattern(PatternRule),
696 FilePathRestriction(FilePathRule),
697 FileExtensionRestriction(FileExtensionRule),
698 MaxFileSize(MaxFileSizeRule),
699 MaxFilePathLength(MaxFilePathLengthRule),
700 MaxFilesChanged(MaxFilesChangedRule),
701 /// Pushes that add a secret are refused, whatever the repository's own
702 /// push protection setting says.
703 SecretScanning(NoParameters),
704 PathReview(PathReviewRule),
705 MergeWindow(MergeWindowRule),
706 AgentAutoMerge(AgentAutoMergeRule),
707}
708
709impl Rule {
710 /// Its `type`, as the API names it.
711 pub fn kind(&self) -> &'static str {
712 match self {
713 Rule::Creation(_) => "creation",
714 Rule::Update(_) => "update",
715 Rule::Deletion(_) => "deletion",
716 Rule::NonFastForward(_) => "non_fast_forward",
717 Rule::RequiredLinearHistory(_) => "required_linear_history",
718 Rule::RequiredSignatures(_) => "required_signatures",
719 Rule::PullRequest(_) => "pull_request",
720 Rule::RequiredStatusChecks(_) => "required_status_checks",
721 Rule::MergeQueue(_) => "merge_queue",
722 Rule::RequiredDeployments(_) => "required_deployments",
723 Rule::CommitMessagePattern(_) => "commit_message_pattern",
724 Rule::CommitAuthorEmailPattern(_) => "commit_author_email_pattern",
725 Rule::CommitterEmailPattern(_) => "committer_email_pattern",
726 Rule::BranchNamePattern(_) => "branch_name_pattern",
727 Rule::TagNamePattern(_) => "tag_name_pattern",
728 Rule::FilePathRestriction(_) => "file_path_restriction",
729 Rule::FileExtensionRestriction(_) => "file_extension_restriction",
730 Rule::MaxFileSize(_) => "max_file_size",
731 Rule::MaxFilePathLength(_) => "max_file_path_length",
732 Rule::MaxFilesChanged(_) => "max_files_changed",
733 Rule::SecretScanning(_) => "secret_scanning",
734 Rule::PathReview(_) => "path_review",
735 Rule::MergeWindow(_) => "merge_window",
736 Rule::AgentAutoMerge(_) => "agent_auto_merge",
737 }
738 }
739
740 /// How people are shown it.
741 pub fn label(&self) -> &'static str {
742 match self {
743 Rule::Creation(_) => "Restrict creations",
744 Rule::Update(_) => "Restrict updates",
745 Rule::Deletion(_) => "Restrict deletions",
746 Rule::NonFastForward(_) => "Block force pushes",
747 Rule::RequiredLinearHistory(_) => "Require linear history",
748 Rule::RequiredSignatures(_) => "Require signed commits",
749 Rule::PullRequest(_) => "Require a pull request before merging",
750 Rule::RequiredStatusChecks(_) => "Require status checks to pass",
751 Rule::MergeQueue(_) => "Require the merge queue",
752 Rule::RequiredDeployments(_) => "Require deployments to succeed",
753 Rule::CommitMessagePattern(_) => "Commit message pattern",
754 Rule::CommitAuthorEmailPattern(_) => "Commit author email pattern",
755 Rule::CommitterEmailPattern(_) => "Committer email pattern",
756 Rule::BranchNamePattern(_) => "Branch name pattern",
757 Rule::TagNamePattern(_) => "Tag name pattern",
758 Rule::FilePathRestriction(_) => "Restrict file paths",
759 Rule::FileExtensionRestriction(_) => "Restrict file extensions",
760 Rule::MaxFileSize(_) => "Restrict file size",
761 Rule::MaxFilePathLength(_) => "Restrict file path length",
762 Rule::MaxFilesChanged(_) => "Restrict files changed",
763 Rule::SecretScanning(_) => "Block pushes that add secrets",
764 Rule::PathReview(_) => "Review for sensitive paths",
765 Rule::MergeWindow(_) => "Merge window",
766 Rule::AgentAutoMerge(_) => "Agent auto-merge",
767 }
768 }
769
770 /// Whether the rule is about pushes: what a push may do or bring.
771 /// Pull request rules hold on merge.
772 pub fn on_push(&self) -> bool {
773 matches!(
774 self,
775 Rule::Creation(_)
776 | Rule::Update(_)
777 | Rule::Deletion(_)
778 | Rule::NonFastForward(_)
779 | Rule::RequiredLinearHistory(_)
780 | Rule::RequiredSignatures(_)
781 | Rule::PullRequest(_)
782 | Rule::MergeQueue(_)
783 | Rule::CommitMessagePattern(_)
784 | Rule::CommitAuthorEmailPattern(_)
785 | Rule::CommitterEmailPattern(_)
786 | Rule::BranchNamePattern(_)
787 | Rule::TagNamePattern(_)
788 | Rule::FilePathRestriction(_)
789 | Rule::FileExtensionRestriction(_)
790 | Rule::MaxFileSize(_)
791 | Rule::MaxFilePathLength(_)
792 | Rule::MaxFilesChanged(_)
793 | Rule::SecretScanning(_)
794 )
795 }
796
797 /// Whether it says anything only tags can break (or only branches).
798 pub fn for_branches_only(&self) -> bool {
799 matches!(
800 self,
801 Rule::PullRequest(_)
802 | Rule::RequiredStatusChecks(_)
803 | Rule::MergeQueue(_)
804 | Rule::RequiredDeployments(_)
805 | Rule::BranchNamePattern(_)
806 | Rule::PathReview(_)
807 | Rule::MergeWindow(_)
808 | Rule::AgentAutoMerge(_)
809 )
810 }
811
812 pub fn for_tags_only(&self) -> bool {
813 matches!(self, Rule::TagNamePattern(_))
814 }
815}
816
817/// One rule of a ruleset, and whose changes it holds for. `parameters`
818/// may be left out, or left partly out: what is missing takes its default.
819#[derive(Clone, Debug, PartialEq, Serialize)]
820pub struct RuleEntry {
821 #[serde(flatten)]
822 pub rule: Rule,
823 pub applies_to: AppliesTo,
824}
825
826impl<'de> Deserialize<'de> for RuleEntry {
827 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
828 #[derive(Deserialize)]
829 struct Written {
830 #[serde(rename = "type")]
831 kind: String,
832 #[serde(default)]
833 parameters: serde_json::Value,
834 #[serde(default)]
835 applies_to: AppliesTo,
836 }
837 let written = Written::deserialize(deserializer)?;
838 let parameters = match written.parameters {
839 serde_json::Value::Null => serde_json::json!({}),
840 other => other,
841 };
842 let rule = serde_json::from_value(serde_json::json!({ "type": written.kind, "parameters": parameters }))
843 .map_err(serde::de::Error::custom)?;
844 Ok(RuleEntry { rule, applies_to: written.applies_to })
845 }
846}
847
848impl RuleEntry {
849 pub fn everyone(rule: Rule) -> RuleEntry {
850 RuleEntry { rule, applies_to: AppliesTo::Everyone }
851 }
852}
853
854/// What a ruleset says, as it is created, changed, exported and imported.
855#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
856#[serde(default)]
857pub struct RulesetSpec {
858 pub name: String,
859 pub enforcement: Enforcement,
860 pub target: Target,
861 pub conditions: Conditions,
862 pub bypass_actors: Vec<BypassActor>,
863 #[serde(deserialize_with = "live_rules")]
864 pub rules: Vec<RuleEntry>,
865}
866
867/// A ruleset, as it is kept.
868#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
869pub struct Ruleset {
870 pub id: String,
871 pub level: Level,
872 /// The workspace it belongs to, or its repository's.
873 pub workspace: String,
874 /// A repository ruleset's repository: its id and `owner/name`.
875 #[serde(default, skip_serializing_if = "Option::is_none")]
876 pub repo_id: Option<String>,
877 #[serde(default, skip_serializing_if = "Option::is_none")]
878 pub repository: Option<String>,
879 #[serde(flatten)]
880 pub spec: RulesetSpec,
881 /// `branch_protection` for the ruleset made from a repository's branch
882 /// protection settings when rulesets arrived.
883 #[serde(default, skip_serializing_if = "Option::is_none")]
884 pub source: Option<String>,
885 pub created_by: String,
886 /// RFC 3339.
887 pub created_at: String,
888 pub updated_by: String,
889 pub updated_at: String,
890}
891
892/// Whose rulesets: a repository's (`repo`) or a workspace's (`workspace`).
893/// Exactly one is set.
894#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
895#[serde(default)]
896pub struct Owner {
897 #[serde(skip_serializing_if = "Option::is_none")]
898 pub repo: Option<RepoPath>,
899 #[serde(skip_serializing_if = "Option::is_none")]
900 pub workspace: Option<String>,
901}
902
903impl Owner {
904 pub fn repo(path: RepoPath) -> Owner {
905 Owner { repo: Some(path), workspace: None }
906 }
907
908 pub fn workspace(slug: &str) -> Owner {
909 Owner { repo: None, workspace: Some(slug.to_lowercase()) }
910 }
911}
912
913/// `list_rulesets`: a repository's or a workspace's rulesets. With
914/// `include_parents`, a repository's list also has its workspace's
915/// rulesets that hold in it. Anyone who may see the repository (members,
916/// for a workspace). Returns `Outcome<Vec<Ruleset>>`.
917#[derive(Clone, Debug, Serialize, Deserialize)]
918pub struct ListRulesetsArgs {
919 pub viewer: Viewer,
920 #[serde(flatten)]
921 pub owner: Owner,
922 #[serde(default)]
923 pub include_parents: bool,
924}
925
926/// `get_ruleset`. Returns `Outcome<Ruleset>`.
927#[derive(Clone, Debug, Serialize, Deserialize)]
928pub struct GetRulesetArgs {
929 pub viewer: Viewer,
930 #[serde(flatten)]
931 pub owner: Owner,
932 pub id: String,
933}
934
935/// `save_ruleset`: creates one (no `id`) or replaces one. The Maintain
936/// role on a repository (`ManageProtection`); a workspace's owners for its
937/// own. Returns `Outcome<Ruleset>`.
938#[derive(Clone, Debug, Serialize, Deserialize)]
939pub struct SaveRulesetArgs {
940 pub actor: User,
941 #[serde(flatten)]
942 pub owner: Owner,
943 #[serde(default)]
944 pub id: Option<String>,
945 pub ruleset: RulesetSpec,
946 /// Set by the API, which records the change in the audit log itself.
947 #[serde(default)]
948 pub from_api: bool,
949}
950
951/// `delete_ruleset`. Returns `Outcome<bool>`.
952#[derive(Clone, Debug, Serialize, Deserialize)]
953pub struct DeleteRulesetArgs {
954 pub actor: User,
955 #[serde(flatten)]
956 pub owner: Owner,
957 pub id: String,
958 #[serde(default)]
959 pub from_api: bool,
960}
961
962/// `effective_rules`: every rule that holds for a branch (or a tag, with
963/// `target` `tag`) of a repository, with the ruleset each comes from.
964/// Returns `Outcome<EffectiveRules>`.
965#[derive(Clone, Debug, Serialize, Deserialize)]
966pub struct EffectiveRulesArgs {
967 pub viewer: Viewer,
968 pub repo: RepoPath,
969 pub name: String,
970 #[serde(default)]
971 pub target: Target,
972}
973
974/// A rule that holds for a branch, and where it comes from.
975#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
976pub struct EffectiveRule {
977 #[serde(flatten)]
978 pub entry: RuleEntry,
979 pub ruleset_id: String,
980 pub ruleset_name: String,
981 pub level: Level,
982 pub enforcement: Enforcement,
983}
984
985/// What holds for one branch or tag.
986#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
987pub struct EffectiveRules {
988 pub name: String,
989 pub target: Target,
990 /// Whether it is the repository's default branch.
991 pub default_branch: bool,
992 /// Active rules first, then those being evaluated.
993 pub rules: Vec<EffectiveRule>,
994 /// The rulesets that hold, by id: their names and who may bypass them.
995 pub rulesets: Vec<RulesetSummary>,
996}
997
998/// A ruleset in brief.
999#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1000pub struct RulesetSummary {
1001 pub id: String,
1002 pub name: String,
1003 pub level: Level,
1004 pub enforcement: Enforcement,
1005 pub bypass_actors: Vec<BypassActor>,
1006}
1007
1008/// What a change was: a push, a merge, or a change made through g1t.
1009#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1010#[serde(rename_all = "snake_case")]
1011pub enum Action {
1012 Push,
1013 Merge,
1014 CreateRef,
1015 DeleteRef,
1016 RenameRef,
1017 /// A commit made through g1t, such as a web edit.
1018 Commit,
1019}
1020
1021impl Action {
1022 pub fn as_str(self) -> &'static str {
1023 match self {
1024 Action::Push => "push",
1025 Action::Merge => "merge",
1026 Action::CreateRef => "create_ref",
1027 Action::DeleteRef => "delete_ref",
1028 Action::RenameRef => "rename_ref",
1029 Action::Commit => "commit",
1030 }
1031 }
1032}
1033
1034/// How an evaluation came out.
1035#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1036#[serde(rename_all = "snake_case")]
1037pub enum Verdict {
1038 /// Every rule was met.
1039 Pass,
1040 /// A rule was broken and the change refused (an active ruleset), or
1041 /// would have been (`evaluate`).
1042 Fail,
1043 /// A rule was broken by a bypass actor, who was let through.
1044 Bypass,
1045}
1046
1047impl Verdict {
1048 pub fn as_str(self) -> &'static str {
1049 match self {
1050 Verdict::Pass => "pass",
1051 Verdict::Fail => "fail",
1052 Verdict::Bypass => "bypass",
1053 }
1054 }
1055}
1056
1057/// One rule that a change breaks, and how to meet it.
1058#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1059pub struct Violation {
1060 /// The rule's `type`.
1061 pub rule: String,
1062 pub ruleset_id: String,
1063 pub ruleset_name: String,
1064 pub enforcement: Enforcement,
1065 /// What is wrong, in a sentence.
1066 pub message: String,
1067 /// How to satisfy it, in a sentence. May be empty.
1068 #[serde(default)]
1069 pub remedy: String,
1070}
1071
1072/// One ruleset's evaluation of one change, as it is recorded.
1073#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1074pub struct NewEvaluation {
1075 pub repo_id: String,
1076 pub workspace: String,
1077 pub ruleset_id: String,
1078 pub ruleset_name: String,
1079 pub enforcement: Enforcement,
1080 pub action: Action,
1081 /// The full ref: `refs/heads/main`.
1082 pub git_ref: String,
1083 pub actor: String,
1084 /// `person`, `agent` or `g1t`.
1085 pub actor_kind: String,
1086 pub verdict: Verdict,
1087 #[serde(default)]
1088 pub violations: Vec<Violation>,
1089 /// The pull request merged, for a merge.
1090 #[serde(default)]
1091 pub number: Option<u32>,
1092 /// The commit it would have moved the ref to.
1093 #[serde(default)]
1094 pub sha: Option<String>,
1095}
1096
1097/// A recorded evaluation.
1098#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1099pub struct Evaluation {
1100 pub id: String,
1101 #[serde(flatten)]
1102 pub evaluation: NewEvaluation,
1103 /// `owner/name`, as it was.
1104 #[serde(default)]
1105 pub repository: String,
1106 /// RFC 3339.
1107 pub created_at: String,
1108}
1109
1110/// `record_evaluations`: services only. Returns how many were kept.
1111#[derive(Clone, Debug, Serialize, Deserialize)]
1112pub struct RecordEvaluationsArgs {
1113 pub evaluations: Vec<NewEvaluation>,
1114}
1115
1116/// `rule_evaluations`: the latest evaluations of a repository's or a
1117/// workspace's rulesets, newest first, filtered. Returns
1118/// `Outcome<EvaluationPage>`.
1119#[derive(Clone, Debug, Serialize, Deserialize)]
1120pub struct EvaluationsArgs {
1121 pub viewer: Viewer,
1122 #[serde(flatten)]
1123 pub owner: Owner,
1124 #[serde(default)]
1125 pub ruleset_id: Option<String>,
1126 #[serde(default)]
1127 pub verdict: Option<Verdict>,
1128 /// Only those that broke a rule (failed, would have failed, bypassed).
1129 #[serde(default)]
1130 pub problems_only: bool,
1131 /// An evaluation's id: only older ones.
1132 #[serde(default)]
1133 pub before: Option<String>,
1134 #[serde(default)]
1135 pub limit: Option<u32>,
1136}
1137
1138/// A page of evaluations, and how they came out over the last 30 days.
1139#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1140pub struct EvaluationPage {
1141 pub evaluations: Vec<Evaluation>,
1142 /// The `before` for the next page, when there is one.
1143 #[serde(default)]
1144 pub next: Option<String>,
1145 pub insights: Insights,
1146}
1147
1148/// How a ruleset's evaluations came out.
1149#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1150pub struct Insights {
1151 pub days: u32,
1152 pub total: u32,
1153 pub passed: u32,
1154 /// Refused by an active ruleset.
1155 pub blocked: u32,
1156 /// Would have been refused by a ruleset in `evaluate`.
1157 pub would_block: u32,
1158 pub bypassed: u32,
1159 /// Per ruleset, most problems first.
1160 pub by_ruleset: Vec<RulesetInsight>,
1161 /// Per rule type, most problems first.
1162 pub by_rule: Vec<RuleInsight>,
1163}
1164
1165#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1166pub struct RulesetInsight {
1167 pub ruleset_id: String,
1168 pub ruleset_name: String,
1169 pub enforcement: Enforcement,
1170 pub total: u32,
1171 pub blocked: u32,
1172 pub would_block: u32,
1173 pub bypassed: u32,
1174}
1175
1176#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1177pub struct RuleInsight {
1178 pub rule: String,
1179 pub count: u32,
1180}
1181
1182/// A ruleset that holds for refs a service is about to change, with
1183/// whether the actor may bypass it and how. What `ref_rules` returns.
1184#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1185pub struct Applicable {
1186 pub id: String,
1187 pub name: String,
1188 pub level: Level,
1189 pub enforcement: Enforcement,
1190 pub target: Target,
1191 pub conditions: RefCondition,
1192 #[serde(deserialize_with = "live_rules")]
1193 pub rules: Vec<RuleEntry>,
1194 /// How the actor may bypass it, if they may.
1195 #[serde(default)]
1196 pub bypass: Option<BypassMode>,
1197}
1198
1199/// `ref_rules`: services only. The rulesets of a repository (its own and
1200/// its workspace's) that are not disabled and hold for any of `refs` (full
1201/// refs), with whether `actor` may bypass each. Returns
1202/// `Outcome<RefRules>`.
1203#[derive(Clone, Debug, Serialize, Deserialize)]
1204pub struct RefRulesArgs {
1205 /// The repository, as the repos service read it.
1206 pub repo: crate::repos::Repo,
1207 pub actor: Option<User>,
1208 pub refs: Vec<String>,
1209}
1210
1211/// What a pull request's merge box shows of the rules for the branch it
1212/// merges into, for whoever is looking.
1213#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1214pub struct MergeRules {
1215 /// Rules not met, which refuse the merge.
1216 pub unmet: Vec<Violation>,
1217 /// Rules not met that the viewer may bypass, by asking to as they
1218 /// merge (`bypass_rules`).
1219 pub bypassable: Vec<Violation>,
1220 /// Rules of rulesets in `evaluate` that would refuse it.
1221 pub evaluate: Vec<Violation>,
1222 /// The rulesets that hold for the branch.
1223 pub rulesets: Vec<RulesetSummary>,
1224 /// Whether merging joins the merge queue.
1225 pub merge_queue: bool,
1226 /// What the active rules ask, as they stack: the approvals a merge
1227 /// needs, whether it must be up to date, and whether a merger may merge
1228 /// past required checks that have not passed.
1229 #[serde(default)]
1230 pub required_approvals: u32,
1231 #[serde(default)]
1232 pub strict: bool,
1233 #[serde(default)]
1234 pub allow_bypass_on_merge: bool,
1235}
1236
1237#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1238pub struct RefRules {
1239 pub default_branch: String,
1240 pub workspace: String,
1241 pub rulesets: Vec<Applicable>,
1242}
1243
1244/// What g1t made of a commit's signature.
1245#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1246#[serde(tag = "state", rename_all = "snake_case")]
1247pub enum Signature {
1248 #[default]
1249 Unsigned,
1250 /// Valid, made with a key the account owning the committer's verified
1251 /// address registered: that account's username.
1252 Verified { signer: String },
1253 /// Signed, but not verified: why.
1254 Unverified { reason: String },
1255}
1256
1257/// One file a commit adds, changes or deletes.
1258#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1259pub struct FileChange {
1260 pub path: String,
1261 /// Its size in bytes, when its content is new and was read.
1262 #[serde(default)]
1263 pub size: Option<u64>,
1264 #[serde(default)]
1265 pub deleted: bool,
1266}
1267
1268/// What rules about commits look at, for one commit.
1269#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1270pub struct CommitFacts {
1271 pub sha: String,
1272 /// At most 4 KiB of it.
1273 pub message: String,
1274 #[serde(default)]
1275 pub author_email: Option<String>,
1276 #[serde(default)]
1277 pub committer_email: Option<String>,
1278 pub parents: u32,
1279 #[serde(default)]
1280 pub signature: Signature,
1281 #[serde(default)]
1282 pub files: Vec<FileChange>,
1283 /// Whether `files` is every file it changes.
1284 #[serde(default)]
1285 pub files_complete: bool,
1286}
1287
1288/// `inspect_commits`: services only. The commits a branch of `source_id`
1289/// adds on top of `base_branch` of `target_id`, read as rules look at
1290/// them, at most `limit`. Returns `Outcome<InspectedCommits>`.
1291#[derive(Clone, Debug, Serialize, Deserialize)]
1292pub struct InspectCommitsArgs {
1293 pub source_id: String,
1294 pub head: String,
1295 pub target_id: String,
1296 pub base_branch: String,
1297 #[serde(default)]
1298 pub limit: Option<u32>,
1299 /// Where the branch left `base_branch`, when the caller knows it (a
1300 /// `divergence` worked it out): the two histories are not read again.
1301 #[serde(default)]
1302 pub merge_base: Option<String>,
1303 /// Only each commit's message, addresses and parents, for default merge
1304 /// messages: no files are listed and no signature is decided, so
1305 /// `files` is empty and `signature` is left as unsigned.
1306 #[serde(default)]
1307 pub messages_only: bool,
1308}
1309
1310#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1311pub struct InspectedCommits {
1312 pub commits: Vec<CommitFacts>,
1313 /// Whether `commits` holds every commit the branch adds, each read in
1314 /// full. A change too large to read is not.
1315 pub complete: bool,
1316}
1317
1318/// What kind of actor a change is by, for rules that hold only for agents'
1319/// or people's changes and for the evaluation log.
1320pub fn actor_kind(actor: &User) -> &'static str {
1321 use crate::PrincipalKind;
1322 match actor.kind {
1323 PrincipalKind::System => "g1t",
1324 PrincipalKind::Agent => "agent",
1325 _ if actor.acting.is_some() => "agent",
1326 PrincipalKind::Workspace => "token",
1327 PrincipalKind::User => "person",
1328 }
1329}
1330
1331/// Whether the actor is an agent: g1t's, or another acting through an
1332/// agent token. g1t acting on its own counts as an agent.
1333pub fn is_agent(actor: &User) -> bool {
1334 matches!(actor_kind(actor), "agent" | "g1t")
1335}
1336
1337#[cfg(test)]
1338mod tests {
1339 use super::*;
1340 use serde_json::json;
1341
1342 #[test]
1343 fn a_rule_is_its_type_and_parameters() {
1344 let entry = RuleEntry {
1345 rule: Rule::PullRequest(PullRequestRule { required_approvals: 2, ..PullRequestRule::default() }),
1346 applies_to: AppliesTo::Agents,
1347 };
1348 let value = serde_json::to_value(&entry).unwrap();
1349 assert_eq!(value["type"], "pull_request");
1350 assert_eq!(value["parameters"]["required_approvals"], 2);
1351 assert_eq!(value["applies_to"], "agents");
1352 let back: RuleEntry = serde_json::from_value(value).unwrap();
1353 assert_eq!(back, entry);
1354 }
1355
1356 #[test]
1357 fn parameters_left_out_take_their_defaults() {
1358 let entry: RuleEntry = serde_json::from_value(json!({ "type": "deletion" })).unwrap();
1359 assert_eq!(entry.rule, Rule::Deletion(NoParameters {}));
1360 assert_eq!(entry.applies_to, AppliesTo::Everyone);
1361 let entry: RuleEntry = serde_json::from_value(json!({ "type": "deletion", "parameters": {} })).unwrap();
1362 assert_eq!(entry.rule.kind(), "deletion");
1363 let entry: RuleEntry =
1364 serde_json::from_value(json!({ "type": "merge_queue", "parameters": { "max_entries_to_build": 8 } })).unwrap();
1365 let Rule::MergeQueue(queue) = entry.rule else { panic!() };
1366 assert_eq!((queue.max_entries_to_build, queue.check_response_timeout_minutes), (8, 45));
1367 }
1368
1369 #[test]
1370 fn every_rule_type_reads_back_as_it_is_named() {
1371 let rules = [
1372 Rule::Creation(NoParameters {}),
1373 Rule::Update(NoParameters {}),
1374 Rule::Deletion(NoParameters {}),
1375 Rule::NonFastForward(NoParameters {}),
1376 Rule::RequiredLinearHistory(NoParameters {}),
1377 Rule::RequiredSignatures(NoParameters {}),
1378 Rule::PullRequest(PullRequestRule::default()),
1379 Rule::RequiredStatusChecks(StatusChecksRule::default()),
1380 Rule::MergeQueue(MergeQueueRule::default()),
1381 Rule::RequiredDeployments(DeploymentsRule::default()),
1382 Rule::CommitMessagePattern(PatternRule::default()),
1383 Rule::CommitAuthorEmailPattern(PatternRule::default()),
1384 Rule::CommitterEmailPattern(PatternRule::default()),
1385 Rule::BranchNamePattern(PatternRule::default()),
1386 Rule::TagNamePattern(PatternRule::default()),
1387 Rule::FilePathRestriction(FilePathRule::default()),
1388 Rule::FileExtensionRestriction(FileExtensionRule::default()),
1389 Rule::MaxFileSize(MaxFileSizeRule::default()),
1390 Rule::MaxFilePathLength(MaxFilePathLengthRule::default()),
1391 Rule::MaxFilesChanged(MaxFilesChangedRule::default()),
1392 Rule::SecretScanning(NoParameters {}),
1393 Rule::PathReview(PathReviewRule::default()),
1394 Rule::MergeWindow(MergeWindowRule::default()),
1395 Rule::AgentAutoMerge(AgentAutoMergeRule::default()),
1396 ];
1397 for rule in rules {
1398 let value = serde_json::to_value(RuleEntry::everyone(rule.clone())).unwrap();
1399 assert_eq!(value["type"], rule.kind());
1400 let back: RuleEntry = serde_json::from_value(value).unwrap();
1401 assert_eq!(back.rule, rule);
1402 assert!(!rule.label().is_empty());
1403 }
1404 }
1405
1406 #[test]
1407 fn a_ruleset_reads_as_the_api_shows_it() {
1408 let ruleset: RulesetSpec = serde_json::from_value(json!({
1409 "name": "Protect main",
1410 "enforcement": "evaluate",
1411 "conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH", "release/**"], "exclude": [] } },
1412 "bypass_actors": [{ "kind": "role", "value": "admin", "mode": "pull_requests" }, { "kind": "g1t" }],
1413 "rules": [{ "type": "non_fast_forward" }, { "type": "required_status_checks", "parameters": { "checks": [{ "context": "CI", "integration": "actions" }], "strict": true } }]
1414 }))
1415 .unwrap();
1416 assert_eq!(ruleset.enforcement, Enforcement::Evaluate);
1417 assert_eq!(ruleset.target, Target::Branch);
1418 assert_eq!(ruleset.bypass_actors[1], BypassActor { kind: ActorKind::G1t, value: String::new(), mode: BypassMode::Always });
1419 let Rule::RequiredStatusChecks(checks) = &ruleset.rules[1].rule else { panic!() };
1420 assert_eq!(checks.checks[0].integration, Some(Integration::Actions));
1421 assert!(checks.strict);
1422 }
1423
1424 #[test]
1425 fn retired_rule_types_still_read_and_are_left_out() {
1426 let ruleset: RulesetSpec = serde_json::from_value(json!({
1427 "name": "Agents",
1428 "rules": [
1429 { "type": "confidence_threshold", "parameters": { "minimum": "high", "required_approvals": 1 } },
1430 { "type": "cost_cap", "parameters": { "max_usd": 5.0 } },
1431 { "type": "agent_auto_merge", "parameters": { "allowed": false, "minimum_confidence": "high" } },
1432 { "type": "deletion" }
1433 ]
1434 }))
1435 .unwrap();
1436 let kinds: Vec<&str> = ruleset.rules.iter().map(|entry| entry.rule.kind()).collect();
1437 assert_eq!(kinds, ["agent_auto_merge", "deletion"]);
1438 assert_eq!(ruleset.rules[0].rule, Rule::AgentAutoMerge(AgentAutoMergeRule { allowed: false }));
1439 // A type nobody ever offered is still refused.
1440 assert!(serde_json::from_value::<RulesetSpec>(json!({ "rules": [{ "type": "made_up" }] })).is_err());
1441 }
1442
1443 #[test]
1444 fn refs_split_into_their_target_and_name() {
1445 assert_eq!(Target::of_ref("refs/heads/release/1.x"), Some((Target::Branch, "release/1.x")));
1446 assert_eq!(Target::of_ref("refs/tags/v1"), Some((Target::Tag, "v1")));
1447 assert_eq!(Target::of_ref("refs/notes/x"), None);
1448 assert_eq!(Target::Tag.full_ref("v2"), "refs/tags/v2");
1449 }
1450
1451 #[test]
1452 fn whose_change_it_is() {
1453 assert!(AppliesTo::Everyone.covers(true) && AppliesTo::Everyone.covers(false));
1454 assert!(AppliesTo::Agents.covers(true) && !AppliesTo::Agents.covers(false));
1455 assert!(AppliesTo::People.covers(false) && !AppliesTo::People.covers(true));
1456 let person = User { id: "usr_1".into(), username: "ada".into(), ..User::default() };
1457 assert_eq!(actor_kind(&person), "person");
1458 assert!(!is_agent(&person));
1459 assert!(is_agent(&User::system("acme")));
1460 }
1461}