Skip to content

g1t/crates/contracts/src/rules.rs

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