Skip to content

g1t/crates/contracts/src/rules.rs

1,434 lines49,932 bytesCodeBlame

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

Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1//! 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}
348
349impl Integration {
350 pub fn as_str(self) -> &'static str {
351 match self {
352 Integration::Actions => "actions",
353 Integration::Deployments => "deployments",
354 Integration::Security => "security",
355 Integration::G1t => "g1t",
356 }
357 }
358
359 pub fn parse(text: &str) -> Option<Integration> {
360 match text {
361 "actions" => Some(Integration::Actions),
362 "deployments" => Some(Integration::Deployments),
363 "security" => Some(Integration::Security),
364 "g1t" => Some(Integration::G1t),
365 _ => None,
366 }
367 }
368}
369
370/// One check that must pass: a workflow's name (`CI`) or another status's
371/// context, and, if set, the integration that must have reported it.
372#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
373pub struct RequiredCheck {
374 pub context: String,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub integration: Option<Integration>,
377}
378
379/// `required_status_checks`: these checks must pass on a pull request's
380/// head before it merges.
381#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
382#[serde(default)]
383#[derive(Default)]
384pub struct StatusChecksRule {
385 pub checks: Vec<RequiredCheck>,
386 /// The pull request must contain the branch's latest commits, so that
387 /// what merges is what was checked.
388 pub strict: bool,
389 /// Required only when the pull request changes a file matching one of
390 /// these patterns. Empty: always.
391 pub paths: Vec<String>,
392 /// Someone who may merge can merge past checks that have not passed,
393 /// saying so as they merge.
394 pub allow_bypass_on_merge: bool,
395}
396
397
398/// `merge_queue`: merging joins the queue, which tests each pull request
399/// together with those ahead of it. The queue lands on the default branch.
400#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
401#[serde(default)]
402pub struct MergeQueueRule {
403 pub merge_method: MergeMethod,
404 /// Entries tested at once.
405 pub max_entries_to_build: u32,
406 /// Entries a batch waits for before it starts, unless the oldest has
407 /// waited `min_entries_wait_minutes`.
408 pub min_entries_to_merge: u32,
409 pub min_entries_wait_minutes: u32,
410 /// How long a batch's checks may take before it is tested again.
411 pub check_response_timeout_minutes: u32,
412}
413
414impl Default for MergeQueueRule {
415 fn default() -> Self {
416 MergeQueueRule {
417 merge_method: MergeMethod::Merge,
418 max_entries_to_build: 4,
419 min_entries_to_merge: 1,
420 min_entries_wait_minutes: 0,
421 check_response_timeout_minutes: 45,
422 }
423 }
424}
425
426/// `required_deployments`: a pull request's head must have deployed
427/// successfully to these environments: `preview` (its preview), or a
428/// project's slug for a repository with several.
429#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
430#[serde(default)]
431pub struct DeploymentsRule {
432 pub environments: Vec<String>,
433}
434
435/// How a pattern rule compares.
436#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
437#[serde(rename_all = "snake_case")]
438pub enum PatternOperator {
439 #[default]
440 StartsWith,
441 EndsWith,
442 Contains,
443 /// A regular expression, run by a linear-time engine.
444 Regex,
445}
446
447/// A rule about text: a commit message, an author's or committer's email
448/// address, a branch's or tag's name. The text must match the pattern, or
449/// with `negate`, must not.
450#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
451#[serde(default)]
452pub struct PatternRule {
453 /// What people are told the rule is, such as "Conventional commits".
454 pub name: String,
455 pub operator: PatternOperator,
456 pub pattern: String,
457 pub negate: bool,
458}
459
460/// `file_path_restriction`: pushes and pull requests may not change files
461/// matching these patterns (fnmatch, `**` across directories).
462#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
463#[serde(default)]
464pub struct FilePathRule {
465 pub restricted_file_paths: Vec<String>,
466}
467
468/// `file_extension_restriction`: files with these extensions (`.exe`,
469/// `.zip`) may not be added or changed.
470#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
471#[serde(default)]
472pub struct FileExtensionRule {
473 pub restricted_file_extensions: Vec<String>,
474}
475
476/// `max_file_size`: no file larger than this, in megabytes (1 to 100).
477#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
478#[serde(default)]
479pub struct MaxFileSizeRule {
480 pub max_file_size_mb: u32,
481}
482
483impl Default for MaxFileSizeRule {
484 fn default() -> Self {
485 MaxFileSizeRule { max_file_size_mb: 10 }
486 }
487}
488
489/// `max_file_path_length`: no path longer than this many characters.
490#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
491#[serde(default)]
492pub struct MaxFilePathLengthRule {
493 pub max_file_path_length: u32,
494}
495
496impl Default for MaxFilePathLengthRule {
497 fn default() -> Self {
498 MaxFilePathLengthRule { max_file_path_length: 255 }
499 }
500}
501
502/// `max_files_changed`: a push's commits, each, and a pull request as a
503/// whole, change at most this many files.
504#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
505#[serde(default)]
506pub struct MaxFilesChangedRule {
507 pub max_files: u32,
508}
509
510impl Default for MaxFilesChangedRule {
511 fn default() -> Self {
512 MaxFilesChangedRule { max_files: 100 }
513 }
514}
515
516/// `confidence_threshold`: an agent's change g1t rates below `minimum`
517/// needs approvals from people before it merges.
518#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
519#[serde(default)]
520pub struct ConfidenceRule {
521 pub minimum: ConfidenceLevel,
522 pub required_approvals: u32,
523}
524
525impl Default for ConfidenceRule {
526 fn default() -> Self {
527 ConfidenceRule { minimum: ConfidenceLevel::Medium, required_approvals: 1 }
528 }
529}
530
531/// `cost_cap`: once agents have spent more than this on a pull request, in
532/// US dollars, it neither merges nor is sent back to its agent until a
533/// person approves it after that.
534#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
535#[serde(default)]
536pub struct CostCapRule {
537 pub max_usd: f64,
538}
539
540impl Default for CostCapRule {
541 fn default() -> Self {
542 CostCapRule { max_usd: 10.0 }
543 }
544}
545
546/// `path_review`: a pull request that changes a file matching `paths`
547/// needs `required_approvals` from people, from `team` when one is named
548/// (its slug, or `workspace/slug`).
549#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
550#[serde(default)]
551pub struct PathReviewRule {
552 pub paths: Vec<String>,
553 pub required_approvals: u32,
554 #[serde(skip_serializing_if = "Option::is_none")]
555 pub team: Option<String>,
556}
557
558impl Default for PathReviewRule {
559 fn default() -> Self {
560 PathReviewRule { paths: Vec::new(), required_approvals: 1, team: None }
561 }
562}
563
564/// A day of the week.
565#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
566#[serde(rename_all = "snake_case")]
567pub enum Weekday {
568 Mon,
569 Tue,
570 Wed,
571 Thu,
572 Fri,
573 Sat,
574 Sun,
575}
576
577impl Weekday {
578 pub const ALL: [Weekday; 7] = [
579 Weekday::Mon,
580 Weekday::Tue,
581 Weekday::Wed,
582 Weekday::Thu,
583 Weekday::Fri,
584 Weekday::Sat,
585 Weekday::Sun,
586 ];
587
588 pub fn as_str(self) -> &'static str {
589 match self {
590 Weekday::Mon => "mon",
591 Weekday::Tue => "tue",
592 Weekday::Wed => "wed",
593 Weekday::Thu => "thu",
594 Weekday::Fri => "fri",
595 Weekday::Sat => "sat",
596 Weekday::Sun => "sun",
597 }
598 }
599}
600
601/// Hours on some days of the week when merging is allowed, `HH:MM` to
602/// `HH:MM` in the rule's time zone. An `end` before `start` runs past
603/// midnight.
604#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
605pub struct WeeklyWindow {
606 pub days: Vec<Weekday>,
607 pub start: String,
608 pub end: String,
609}
610
611/// A stretch of time, RFC 3339 UTC. With no `end`, it lasts until removed:
612/// an incident freeze.
613#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
614pub struct Period {
615 pub start: String,
616 #[serde(default)]
617 pub end: Option<String>,
618 #[serde(default)]
619 pub reason: String,
620}
621
622/// `merge_window`: when pull requests may merge into the branch. Outside
623/// every `windows` entry (when there are any), or during a `freezes` one,
624/// merging waits, unless an `exceptions` entry covers the moment.
625#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
626#[serde(default)]
627pub struct MergeWindowRule {
628 /// A fixed offset from UTC, `+02:00` or `-05:00`; `UTC` or empty is UTC.
629 /// Daylight saving time is not applied.
630 pub time_zone: String,
631 pub windows: Vec<WeeklyWindow>,
632 pub freezes: Vec<Period>,
633 pub exceptions: Vec<Period>,
634}
635
636impl Default for MergeWindowRule {
637 fn default() -> Self {
638 MergeWindowRule {
639 time_zone: "UTC".to_owned(),
640 windows: Vec::new(),
641 freezes: Vec::new(),
642 exceptions: Vec::new(),
643 }
644 }
645}
646
647/// `agent_auto_merge`: whether g1t lands an agent's ready pull request into
648/// the branch without a person pressing merge, and how sure of it g1t must
649/// be. The repository's auto-merge setting must be on as well.
650#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
651#[serde(default)]
652pub struct AgentAutoMergeRule {
653 pub allowed: bool,
654 #[serde(skip_serializing_if = "Option::is_none")]
655 pub minimum_confidence: Option<ConfidenceLevel>,
656}
657
658impl Default for AgentAutoMergeRule {
659 fn default() -> Self {
660 AgentAutoMergeRule { allowed: true, minimum_confidence: None }
661 }
662}
663
664/// One rule and its parameters, tagged by `type`.
665#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
666#[serde(tag = "type", content = "parameters", rename_all = "snake_case")]
667pub enum Rule {
668 /// Only bypass actors may create a matching branch or tag.
669 Creation(NoParameters),
670 /// Only bypass actors may push to (move) a matching branch or tag.
671 Update(NoParameters),
672 /// Only bypass actors may delete a matching branch or tag.
673 Deletion(NoParameters),
674 /// Nobody force pushes: a push must only add to its history.
675 NonFastForward(NoParameters),
676 /// No merge commits: history stays a straight line.
677 RequiredLinearHistory(NoParameters),
678 /// Every commit carries a signature g1t verifies.
679 RequiredSignatures(NoParameters),
680 PullRequest(PullRequestRule),
681 RequiredStatusChecks(StatusChecksRule),
682 MergeQueue(MergeQueueRule),
683 RequiredDeployments(DeploymentsRule),
684 CommitMessagePattern(PatternRule),
685 CommitAuthorEmailPattern(PatternRule),
686 CommitterEmailPattern(PatternRule),
687 BranchNamePattern(PatternRule),
688 TagNamePattern(PatternRule),
689 FilePathRestriction(FilePathRule),
690 FileExtensionRestriction(FileExtensionRule),
691 MaxFileSize(MaxFileSizeRule),
692 MaxFilePathLength(MaxFilePathLengthRule),
693 MaxFilesChanged(MaxFilesChangedRule),
694 /// Pushes that add a secret are refused, whatever the repository's own
695 /// push protection setting says.
696 SecretScanning(NoParameters),
697 ConfidenceThreshold(ConfidenceRule),
698 CostCap(CostCapRule),
699 PathReview(PathReviewRule),
700 MergeWindow(MergeWindowRule),
701 AgentAutoMerge(AgentAutoMergeRule),
702}
703
704impl Rule {
705 /// Its `type`, as the API names it.
706 pub fn kind(&self) -> &'static str {
707 match self {
708 Rule::Creation(_) => "creation",
709 Rule::Update(_) => "update",
710 Rule::Deletion(_) => "deletion",
711 Rule::NonFastForward(_) => "non_fast_forward",
712 Rule::RequiredLinearHistory(_) => "required_linear_history",
713 Rule::RequiredSignatures(_) => "required_signatures",
714 Rule::PullRequest(_) => "pull_request",
715 Rule::RequiredStatusChecks(_) => "required_status_checks",
716 Rule::MergeQueue(_) => "merge_queue",
717 Rule::RequiredDeployments(_) => "required_deployments",
718 Rule::CommitMessagePattern(_) => "commit_message_pattern",
719 Rule::CommitAuthorEmailPattern(_) => "commit_author_email_pattern",
720 Rule::CommitterEmailPattern(_) => "committer_email_pattern",
721 Rule::BranchNamePattern(_) => "branch_name_pattern",
722 Rule::TagNamePattern(_) => "tag_name_pattern",
723 Rule::FilePathRestriction(_) => "file_path_restriction",
724 Rule::FileExtensionRestriction(_) => "file_extension_restriction",
725 Rule::MaxFileSize(_) => "max_file_size",
726 Rule::MaxFilePathLength(_) => "max_file_path_length",
727 Rule::MaxFilesChanged(_) => "max_files_changed",
728 Rule::SecretScanning(_) => "secret_scanning",
729 Rule::ConfidenceThreshold(_) => "confidence_threshold",
730 Rule::CostCap(_) => "cost_cap",
731 Rule::PathReview(_) => "path_review",
732 Rule::MergeWindow(_) => "merge_window",
733 Rule::AgentAutoMerge(_) => "agent_auto_merge",
734 }
735 }
736
737 /// How people are shown it.
738 pub fn label(&self) -> &'static str {
739 match self {
740 Rule::Creation(_) => "Restrict creations",
741 Rule::Update(_) => "Restrict updates",
742 Rule::Deletion(_) => "Restrict deletions",
743 Rule::NonFastForward(_) => "Block force pushes",
744 Rule::RequiredLinearHistory(_) => "Require linear history",
745 Rule::RequiredSignatures(_) => "Require signed commits",
746 Rule::PullRequest(_) => "Require a pull request before merging",
747 Rule::RequiredStatusChecks(_) => "Require status checks to pass",
748 Rule::MergeQueue(_) => "Require the merge queue",
749 Rule::RequiredDeployments(_) => "Require deployments to succeed",
750 Rule::CommitMessagePattern(_) => "Commit message pattern",
751 Rule::CommitAuthorEmailPattern(_) => "Commit author email pattern",
752 Rule::CommitterEmailPattern(_) => "Committer email pattern",
753 Rule::BranchNamePattern(_) => "Branch name pattern",
754 Rule::TagNamePattern(_) => "Tag name pattern",
755 Rule::FilePathRestriction(_) => "Restrict file paths",
756 Rule::FileExtensionRestriction(_) => "Restrict file extensions",
757 Rule::MaxFileSize(_) => "Restrict file size",
758 Rule::MaxFilePathLength(_) => "Restrict file path length",
759 Rule::MaxFilesChanged(_) => "Restrict files changed",
760 Rule::SecretScanning(_) => "Block pushes that add secrets",
761 Rule::ConfidenceThreshold(_) => "Confidence threshold",
762 Rule::CostCap(_) => "Cost cap",
763 Rule::PathReview(_) => "Review for sensitive paths",
764 Rule::MergeWindow(_) => "Merge window",
765 Rule::AgentAutoMerge(_) => "Agent auto-merge",
766 }
767 }
768
769 /// Whether the rule is about pushes: what a push may do or bring.
770 /// Pull request rules hold on merge.
771 pub fn on_push(&self) -> bool {
772 matches!(
773 self,
774 Rule::Creation(_)
775 | Rule::Update(_)
776 | Rule::Deletion(_)
777 | Rule::NonFastForward(_)
778 | Rule::RequiredLinearHistory(_)
779 | Rule::RequiredSignatures(_)
780 | Rule::PullRequest(_)
781 | Rule::MergeQueue(_)
782 | Rule::CommitMessagePattern(_)
783 | Rule::CommitAuthorEmailPattern(_)
784 | Rule::CommitterEmailPattern(_)
785 | Rule::BranchNamePattern(_)
786 | Rule::TagNamePattern(_)
787 | Rule::FilePathRestriction(_)
788 | Rule::FileExtensionRestriction(_)
789 | Rule::MaxFileSize(_)
790 | Rule::MaxFilePathLength(_)
791 | Rule::MaxFilesChanged(_)
792 | Rule::SecretScanning(_)
793 )
794 }
795
796 /// Whether it says anything only tags can break (or only branches).
797 pub fn for_branches_only(&self) -> bool {
798 matches!(
799 self,
800 Rule::PullRequest(_)
801 | Rule::RequiredStatusChecks(_)
802 | Rule::MergeQueue(_)
803 | Rule::RequiredDeployments(_)
804 | Rule::BranchNamePattern(_)
805 | Rule::ConfidenceThreshold(_)
806 | Rule::CostCap(_)
807 | Rule::PathReview(_)
808 | Rule::MergeWindow(_)
809 | Rule::AgentAutoMerge(_)
810 )
811 }
812
813 pub fn for_tags_only(&self) -> bool {
814 matches!(self, Rule::TagNamePattern(_))
815 }
816}
817
818/// One rule of a ruleset, and whose changes it holds for. `parameters`
819/// may be left out, or left partly out: what is missing takes its default.
820#[derive(Clone, Debug, PartialEq, Serialize)]
821pub struct RuleEntry {
822 #[serde(flatten)]
823 pub rule: Rule,
824 pub applies_to: AppliesTo,
825}
826
827impl<'de> Deserialize<'de> for RuleEntry {
828 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
829 #[derive(Deserialize)]
830 struct Written {
831 #[serde(rename = "type")]
832 kind: String,
833 #[serde(default)]
834 parameters: serde_json::Value,
835 #[serde(default)]
836 applies_to: AppliesTo,
837 }
838 let written = Written::deserialize(deserializer)?;
839 let parameters = match written.parameters {
840 serde_json::Value::Null => serde_json::json!({}),
841 other => other,
842 };
843 let rule = serde_json::from_value(serde_json::json!({ "type": written.kind, "parameters": parameters }))
844 .map_err(serde::de::Error::custom)?;
845 Ok(RuleEntry { rule, applies_to: written.applies_to })
846 }
847}
848
849impl RuleEntry {
850 pub fn everyone(rule: Rule) -> RuleEntry {
851 RuleEntry { rule, applies_to: AppliesTo::Everyone }
852 }
853}
854
855/// What a ruleset says, as it is created, changed, exported and imported.
856#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
857#[serde(default)]
858pub struct RulesetSpec {
859 pub name: String,
860 pub enforcement: Enforcement,
861 pub target: Target,
862 pub conditions: Conditions,
863 pub bypass_actors: Vec<BypassActor>,
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 pub rules: Vec<RuleEntry>,
1193 /// How the actor may bypass it, if they may.
1194 #[serde(default)]
1195 pub bypass: Option<BypassMode>,
1196}
1197
1198/// `ref_rules`: services only. The rulesets of a repository (its own and
1199/// its workspace's) that are not disabled and hold for any of `refs` (full
1200/// refs), with whether `actor` may bypass each. Returns
1201/// `Outcome<RefRules>`.
1202#[derive(Clone, Debug, Serialize, Deserialize)]
1203pub struct RefRulesArgs {
1204 /// The repository, as the repos service read it.
1205 pub repo: crate::repos::Repo,
1206 pub actor: Option<User>,
1207 pub refs: Vec<String>,
1208}
1209
1210/// What a pull request's merge box shows of the rules for the branch it
1211/// merges into, for whoever is looking.
1212#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1213pub struct MergeRules {
1214 /// Rules not met, which refuse the merge.
1215 pub unmet: Vec<Violation>,
1216 /// Rules not met that the viewer may bypass, by asking to as they
1217 /// merge (`bypass_rules`).
1218 pub bypassable: Vec<Violation>,
1219 /// Rules of rulesets in `evaluate` that would refuse it.
1220 pub evaluate: Vec<Violation>,
1221 /// The rulesets that hold for the branch.
1222 pub rulesets: Vec<RulesetSummary>,
1223 /// Whether merging joins the merge queue.
1224 pub merge_queue: bool,
1225 /// What the active rules ask, as they stack: the approvals a merge
1226 /// needs, whether it must be up to date, and whether a merger may merge
1227 /// past required checks that have not passed.
1228 #[serde(default)]
1229 pub required_approvals: u32,
1230 #[serde(default)]
1231 pub strict: bool,
1232 #[serde(default)]
1233 pub allow_bypass_on_merge: bool,
1234}
1235
1236#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1237pub struct RefRules {
1238 pub default_branch: String,
1239 pub workspace: String,
1240 pub rulesets: Vec<Applicable>,
1241}
1242
1243/// What g1t made of a commit's signature.
1244#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1245#[serde(tag = "state", rename_all = "snake_case")]
1246pub enum Signature {
1247 #[default]
1248 Unsigned,
1249 /// Valid, made with a key the account owning the committer's verified
1250 /// address registered: that account's username.
1251 Verified { signer: String },
1252 /// Signed, but not verified: why.
1253 Unverified { reason: String },
1254}
1255
1256/// One file a commit adds, changes or deletes.
1257#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1258pub struct FileChange {
1259 pub path: String,
1260 /// Its size in bytes, when its content is new and was read.
1261 #[serde(default)]
1262 pub size: Option<u64>,
1263 #[serde(default)]
1264 pub deleted: bool,
1265}
1266
1267/// What rules about commits look at, for one commit.
1268#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1269pub struct CommitFacts {
1270 pub sha: String,
1271 /// At most 4 KiB of it.
1272 pub message: String,
1273 #[serde(default)]
1274 pub author_email: Option<String>,
1275 #[serde(default)]
1276 pub committer_email: Option<String>,
1277 pub parents: u32,
1278 #[serde(default)]
1279 pub signature: Signature,
1280 #[serde(default)]
1281 pub files: Vec<FileChange>,
1282 /// Whether `files` is every file it changes.
1283 #[serde(default)]
1284 pub files_complete: bool,
1285}
1286
1287/// `inspect_commits`: services only. The commits a branch of `source_id`
1288/// adds on top of `base_branch` of `target_id`, read as rules look at
1289/// them, at most `limit`. Returns `Outcome<InspectedCommits>`.
1290#[derive(Clone, Debug, Serialize, Deserialize)]
1291pub struct InspectCommitsArgs {
1292 pub source_id: String,
1293 pub head: String,
1294 pub target_id: String,
1295 pub base_branch: String,
1296 #[serde(default)]
1297 pub limit: Option<u32>,
1298}
1299
1300#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1301pub struct InspectedCommits {
1302 pub commits: Vec<CommitFacts>,
1303 /// Whether `commits` holds every commit the branch adds, each read in
1304 /// full. A change too large to read is not.
1305 pub complete: bool,
1306}
1307
1308/// What kind of actor a change is by, for rules that hold only for agents'
1309/// or people's changes and for the evaluation log.
1310pub fn actor_kind(actor: &User) -> &'static str {
1311 use crate::PrincipalKind;
1312 match actor.kind {
1313 PrincipalKind::System => "g1t",
1314 PrincipalKind::Agent => "agent",
1315 _ if actor.acting.is_some() => "agent",
1316 PrincipalKind::Workspace => "token",
1317 PrincipalKind::User => "person",
1318 }
1319}
1320
1321/// Whether the actor is an agent: g1t's, or another acting through an
1322/// agent token. g1t acting on its own counts as an agent.
1323pub fn is_agent(actor: &User) -> bool {
1324 matches!(actor_kind(actor), "agent" | "g1t")
1325}
1326
1327#[cfg(test)]
1328mod tests {
1329 use super::*;
1330 use serde_json::json;
1331
1332 #[test]
1333 fn a_rule_is_its_type_and_parameters() {
1334 let entry = RuleEntry {
1335 rule: Rule::PullRequest(PullRequestRule { required_approvals: 2, ..PullRequestRule::default() }),
1336 applies_to: AppliesTo::Agents,
1337 };
1338 let value = serde_json::to_value(&entry).unwrap();
1339 assert_eq!(value["type"], "pull_request");
1340 assert_eq!(value["parameters"]["required_approvals"], 2);
1341 assert_eq!(value["applies_to"], "agents");
1342 let back: RuleEntry = serde_json::from_value(value).unwrap();
1343 assert_eq!(back, entry);
1344 }
1345
1346 #[test]
1347 fn parameters_left_out_take_their_defaults() {
1348 let entry: RuleEntry = serde_json::from_value(json!({ "type": "deletion" })).unwrap();
1349 assert_eq!(entry.rule, Rule::Deletion(NoParameters {}));
1350 assert_eq!(entry.applies_to, AppliesTo::Everyone);
1351 let entry: RuleEntry = serde_json::from_value(json!({ "type": "deletion", "parameters": {} })).unwrap();
1352 assert_eq!(entry.rule.kind(), "deletion");
1353 let entry: RuleEntry =
1354 serde_json::from_value(json!({ "type": "merge_queue", "parameters": { "max_entries_to_build": 8 } })).unwrap();
1355 let Rule::MergeQueue(queue) = entry.rule else { panic!() };
1356 assert_eq!((queue.max_entries_to_build, queue.check_response_timeout_minutes), (8, 45));
1357 }
1358
1359 #[test]
1360 fn every_rule_type_reads_back_as_it_is_named() {
1361 let rules = [
1362 Rule::Creation(NoParameters {}),
1363 Rule::Update(NoParameters {}),
1364 Rule::Deletion(NoParameters {}),
1365 Rule::NonFastForward(NoParameters {}),
1366 Rule::RequiredLinearHistory(NoParameters {}),
1367 Rule::RequiredSignatures(NoParameters {}),
1368 Rule::PullRequest(PullRequestRule::default()),
1369 Rule::RequiredStatusChecks(StatusChecksRule::default()),
1370 Rule::MergeQueue(MergeQueueRule::default()),
1371 Rule::RequiredDeployments(DeploymentsRule::default()),
1372 Rule::CommitMessagePattern(PatternRule::default()),
1373 Rule::CommitAuthorEmailPattern(PatternRule::default()),
1374 Rule::CommitterEmailPattern(PatternRule::default()),
1375 Rule::BranchNamePattern(PatternRule::default()),
1376 Rule::TagNamePattern(PatternRule::default()),
1377 Rule::FilePathRestriction(FilePathRule::default()),
1378 Rule::FileExtensionRestriction(FileExtensionRule::default()),
1379 Rule::MaxFileSize(MaxFileSizeRule::default()),
1380 Rule::MaxFilePathLength(MaxFilePathLengthRule::default()),
1381 Rule::MaxFilesChanged(MaxFilesChangedRule::default()),
1382 Rule::SecretScanning(NoParameters {}),
1383 Rule::ConfidenceThreshold(ConfidenceRule::default()),
1384 Rule::CostCap(CostCapRule::default()),
1385 Rule::PathReview(PathReviewRule::default()),
1386 Rule::MergeWindow(MergeWindowRule::default()),
1387 Rule::AgentAutoMerge(AgentAutoMergeRule::default()),
1388 ];
1389 for rule in rules {
1390 let value = serde_json::to_value(RuleEntry::everyone(rule.clone())).unwrap();
1391 assert_eq!(value["type"], rule.kind());
1392 let back: RuleEntry = serde_json::from_value(value).unwrap();
1393 assert_eq!(back.rule, rule);
1394 assert!(!rule.label().is_empty());
1395 }
1396 }
1397
1398 #[test]
1399 fn a_ruleset_reads_as_the_api_shows_it() {
1400 let ruleset: RulesetSpec = serde_json::from_value(json!({
1401 "name": "Protect main",
1402 "enforcement": "evaluate",
1403 "conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH", "release/**"], "exclude": [] } },
1404 "bypass_actors": [{ "kind": "role", "value": "admin", "mode": "pull_requests" }, { "kind": "g1t" }],
1405 "rules": [{ "type": "non_fast_forward" }, { "type": "required_status_checks", "parameters": { "checks": [{ "context": "CI", "integration": "actions" }], "strict": true } }]
1406 }))
1407 .unwrap();
1408 assert_eq!(ruleset.enforcement, Enforcement::Evaluate);
1409 assert_eq!(ruleset.target, Target::Branch);
1410 assert_eq!(ruleset.bypass_actors[1], BypassActor { kind: ActorKind::G1t, value: String::new(), mode: BypassMode::Always });
1411 let Rule::RequiredStatusChecks(checks) = &ruleset.rules[1].rule else { panic!() };
1412 assert_eq!(checks.checks[0].integration, Some(Integration::Actions));
1413 assert!(checks.strict);
1414 }
1415
1416 #[test]
1417 fn refs_split_into_their_target_and_name() {
1418 assert_eq!(Target::of_ref("refs/heads/release/1.x"), Some((Target::Branch, "release/1.x")));
1419 assert_eq!(Target::of_ref("refs/tags/v1"), Some((Target::Tag, "v1")));
1420 assert_eq!(Target::of_ref("refs/notes/x"), None);
1421 assert_eq!(Target::Tag.full_ref("v2"), "refs/tags/v2");
1422 }
1423
1424 #[test]
1425 fn whose_change_it_is() {
1426 assert!(AppliesTo::Everyone.covers(true) && AppliesTo::Everyone.covers(false));
1427 assert!(AppliesTo::Agents.covers(true) && !AppliesTo::Agents.covers(false));
1428 assert!(AppliesTo::People.covers(false) && !AppliesTo::People.covers(true));
1429 let person = User { id: "usr_1".into(), username: "ada".into(), ..User::default() };
1430 assert_eq!(actor_kind(&person), "person");
1431 assert!(!is_agent(&person));
1432 assert!(is_agent(&User::system("acme")));
1433 }
1434}

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