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 merge | 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 | ||
| 29 | use serde::{Deserialize, Serialize}; | |
| 30 | ||
| 31 | use crate::repos::RepoPath; | |
| 32 | pub use crate::work::ConfidenceLevel; | |
| 33 | use crate::{User, Viewer}; | |
| 34 | ||
| 35 | /// The repository's default branch, whatever it is called at the time. | |
| 36 | pub const DEFAULT_BRANCH: &str = "~DEFAULT_BRANCH"; | |
| 37 | /// Every branch or tag, or every repository. | |
| 38 | pub const ALL: &str = "~ALL"; | |
| 39 | /// Rulesets a repository, or a workspace, may have. | |
| 40 | pub const MAX_RULESETS: usize = 75; | |
| 41 | /// Rules in one ruleset. | |
| 42 | pub const MAX_RULES: usize = 50; | |
| 43 | /// Patterns in one list (branches, paths, extensions, repositories). | |
| 44 | pub const MAX_PATTERNS: usize = 100; | |
| 45 | /// The longest pattern, regular expression or name kept. | |
| 46 | pub const MAX_PATTERN_CHARS: usize = 512; | |
| 47 | /// Bypass actors in one ruleset. | |
| 48 | pub const MAX_BYPASS_ACTORS: usize = 50; | |
| 49 | /// Required approvals a pull request rule may ask for. | |
| 50 | pub const MAX_APPROVALS: u32 = 10; | |
| 51 | /// The ruleset made from a repository's branch protection, as it was | |
| 52 | /// before rulesets: its `source`. | |
| 53 | pub 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")] | |
| 58 | pub 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 | ||
| 69 | impl 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")] | |
| 82 | pub enum Target { | |
| 83 | #[default] | |
| 84 | Branch, | |
| 85 | Tag, | |
| 86 | } | |
| 87 | ||
| 88 | impl 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")] | |
| 116 | pub enum Level { | |
| 117 | #[default] | |
| 118 | Repository, | |
| 119 | Workspace, | |
| 120 | } | |
| 121 | ||
| 122 | impl 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)] | |
| 138 | pub 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")] | |
| 146 | pub 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)] | |
| 159 | pub struct RepositoryCondition { | |
| 160 | pub include: Vec<String>, | |
| 161 | pub exclude: Vec<String>, | |
| 162 | pub visibility: VisibilityCondition, | |
| 163 | pub topics: Vec<String>, | |
| 164 | } | |
| 165 | ||
| 166 | impl 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)] | |
| 180 | pub 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")] | |
| 189 | pub 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")] | |
| 210 | pub 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 | ||
| 218 | impl 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)] | |
| 229 | pub 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")] | |
| 241 | pub 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 | ||
| 251 | impl 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)] | |
| 272 | pub 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")] | |
| 277 | pub enum MergeMethod { | |
| 278 | /// The branch lands as it is, its commits included: how g1t merges. | |
| 279 | Merge, | |
| 280 | Squash, | |
| 281 | Rebase, | |
| 282 | } | |
| 283 | ||
| 284 | impl 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)] | |
| 298 | pub 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 | ||
| 321 | impl 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")] | |
| 338 | pub 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 | ||
| 349 | impl 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)] | |
| 373 | pub 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)] | |
| 384 | pub 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)] | |
| 402 | pub 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 | ||
| 414 | impl 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)] | |
| 431 | pub 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")] | |
| 438 | pub 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)] | |
| 452 | pub 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)] | |
| 464 | pub 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)] | |
| 472 | pub 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)] | |
| 479 | pub struct MaxFileSizeRule { | |
| 480 | pub max_file_size_mb: u32, | |
| 481 | } | |
| 482 | ||
| 483 | impl 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)] | |
| 492 | pub struct MaxFilePathLengthRule { | |
| 493 | pub max_file_path_length: u32, | |
| 494 | } | |
| 495 | ||
| 496 | impl 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)] | |
| 506 | pub struct MaxFilesChangedRule { | |
| 507 | pub max_files: u32, | |
| 508 | } | |
| 509 | ||
| 510 | impl 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)] | |
| 520 | pub struct ConfidenceRule { | |
| 521 | pub minimum: ConfidenceLevel, | |
| 522 | pub required_approvals: u32, | |
| 523 | } | |
| 524 | ||
| 525 | impl 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)] | |
| 536 | pub struct CostCapRule { | |
| 537 | pub max_usd: f64, | |
| 538 | } | |
| 539 | ||
| 540 | impl 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)] | |
| 551 | pub 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 | ||
| 558 | impl 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")] | |
| 567 | pub enum Weekday { | |
| 568 | Mon, | |
| 569 | Tue, | |
| 570 | Wed, | |
| 571 | Thu, | |
| 572 | Fri, | |
| 573 | Sat, | |
| 574 | Sun, | |
| 575 | } | |
| 576 | ||
| 577 | impl 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)] | |
| 605 | pub 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)] | |
| 614 | pub 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)] | |
| 627 | pub 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 | ||
| 636 | impl 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)] | |
| 652 | pub struct AgentAutoMergeRule { | |
| 653 | pub allowed: bool, | |
| 654 | #[serde(skip_serializing_if = "Option::is_none")] | |
| 655 | pub minimum_confidence: Option<ConfidenceLevel>, | |
| 656 | } | |
| 657 | ||
| 658 | impl 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")] | |
| 667 | pub 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 | ||
| 704 | impl 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)] | |
| 821 | pub struct RuleEntry { | |
| 822 | #[serde(flatten)] | |
| 823 | pub rule: Rule, | |
| 824 | pub applies_to: AppliesTo, | |
| 825 | } | |
| 826 | ||
| 827 | impl<'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 | ||
| 849 | impl 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)] | |
| 858 | pub 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)] | |
| 869 | pub 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)] | |
| 896 | pub 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 | ||
| 903 | impl 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)] | |
| 918 | pub 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)] | |
| 928 | pub 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)] | |
| 939 | pub 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)] | |
| 953 | pub 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)] | |
| 966 | pub 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)] | |
| 976 | pub 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)] | |
| 987 | pub 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)] | |
| 1000 | pub 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")] | |
| 1011 | pub 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 | ||
| 1021 | impl 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")] | |
| 1037 | pub 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 | ||
| 1047 | impl 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)] | |
| 1059 | pub 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)] | |
| 1074 | pub 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)] | |
| 1099 | pub 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)] | |
| 1112 | pub 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)] | |
| 1120 | pub 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)] | |
| 1140 | pub 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)] | |
| 1150 | pub 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)] | |
| 1166 | pub 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)] | |
| 1177 | pub 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)] | |
| 1185 | pub 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)] | |
| 1203 | pub 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)] | |
| 1213 | pub 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)] | |
| 1237 | pub 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")] | |
| 1246 | pub 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)] | |
| 1258 | pub 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)] | |
| 1269 | pub 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)] | |
| 1291 | pub 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)] | |
| 1301 | pub 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. | |
| 1310 | pub 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. | |
| 1323 | pub fn is_agent(actor: &User) -> bool { | |
| 1324 | matches!(actor_kind(actor), "agent" | "g1t") | |
| 1325 | } | |
| 1326 | ||
| 1327 | #[cfg(test)] | |
| 1328 | mod 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.