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 | //! What people are told when rules refuse a change: `remote:` lines for |
| 2 | //! git, and one sentence for the API and the merge box. | |
| 3 | ||
| 4 | use g1t_contracts::rules::Violation; | |
| 5 | ||
| 6 | use crate::outcome::{Judged, blocking}; | |
| 7 | ||
| 8 | /// The `ng` reason git prints beside a refused ref: short, one line. | |
| 9 | pub fn ng_reason(judged: &[Judged]) -> String { | |
| 10 | match blocking(judged).first() { | |
| 11 | Some(violation) => format!("declined by ruleset \"{}\" ({})", violation.ruleset_name, violation.rule), | |
| 12 | None => "declined by rules".to_owned(), | |
| 13 | } | |
| 14 | } | |
| 15 | ||
| 16 | /// The lines git prints as `remote:` for a push the rules refuse: each | |
| 17 | /// broken rule with its ruleset, and how to meet it. `rules_url` is where | |
| 18 | /// the branch's rules are shown. | |
| 19 | pub fn remote_lines(git_ref: &str, judged: &[Judged], rules_url: &str) -> Vec<String> { | |
| 20 | let violations = blocking(judged); | |
| 21 | let mut lines = vec![ | |
| 22 | String::new(), | |
| 23 | format!("error: rules for {git_ref} declined this push:"), | |
| 24 | ]; | |
| 25 | let mut last_remedy = String::new(); | |
| 26 | for violation in &violations { | |
| 27 | lines.push(format!("- {} [ruleset \"{}\", {}]", violation.message, violation.ruleset_name, violation.rule)); | |
| 28 | if !violation.remedy.is_empty() && violation.remedy != last_remedy { | |
| 29 | lines.push(format!(" {}", violation.remedy)); | |
| 30 | last_remedy = violation.remedy.clone(); | |
| 31 | } | |
| 32 | } | |
| 33 | lines.push(format!("See the rules that hold for it: {rules_url}")); | |
| 34 | lines.push(String::new()); | |
| 35 | lines | |
| 36 | } | |
| 37 | ||
| 38 | /// One sentence for a refused merge: the first problem, and how many more. | |
| 39 | pub fn summary(violations: &[&Violation]) -> Option<String> { | |
| 40 | let first = violations.first()?; | |
| 41 | let more = violations.len() - 1; | |
| 42 | let tail = match more { | |
| 43 | 0 => String::new(), | |
| 44 | 1 => " One more rule is not met.".to_owned(), | |
| 45 | more => format!(" {more} more rules are not met."), | |
| 46 | }; | |
| 47 | Some(format!("{}{tail}", first.message)) | |
| 48 | } | |
| 49 | ||
| 50 | #[cfg(test)] | |
| 51 | mod tests { | |
| 52 | use super::*; | |
| 53 | use g1t_contracts::rules::{Enforcement, Level}; | |
| 54 | ||
| 55 | fn judged(violations: &[(&str, &str, &str)]) -> Judged { | |
| 56 | Judged { | |
| 57 | id: "rs_1".into(), | |
| 58 | name: "Protect main".into(), | |
| 59 | level: Level::Repository, | |
| 60 | enforcement: Enforcement::Active, | |
| 61 | bypass: None, | |
| 62 | git_ref: "refs/heads/main".into(), | |
| 63 | violations: violations | |
| 64 | .iter() | |
| 65 | .map(|(rule, message, remedy)| Violation { | |
| 66 | rule: (*rule).into(), | |
| 67 | ruleset_id: "rs_1".into(), | |
| 68 | ruleset_name: "Protect main".into(), | |
| 69 | enforcement: Enforcement::Active, | |
| 70 | message: (*message).into(), | |
| 71 | remedy: (*remedy).into(), | |
| 72 | }) | |
| 73 | .collect(), | |
| 74 | merging: false, | |
| 75 | } | |
| 76 | } | |
| 77 | ||
| 78 | #[test] | |
| 79 | fn git_is_told_which_ruleset_and_rule_and_what_to_do() { | |
| 80 | let refused = [judged(&[ | |
| 81 | ("pull_request", "Changes to main must be made through a pull request.", "Push a branch and open a pull request."), | |
| 82 | ("non_fast_forward", "Force pushes to main are blocked.", "Pull, then push without --force."), | |
| 83 | ])]; | |
| 84 | assert_eq!(ng_reason(&refused), "declined by ruleset \"Protect main\" (pull_request)"); | |
| 85 | let lines = remote_lines("refs/heads/main", &refused, "https://g1t.sh/acme/web/settings/rules?branch=main"); | |
| 86 | assert_eq!( | |
| 87 | lines, | |
| 88 | vec![ | |
| 89 | "", | |
| 90 | "error: rules for refs/heads/main declined this push:", | |
| 91 | "- Changes to main must be made through a pull request. [ruleset \"Protect main\", pull_request]", | |
| 92 | " Push a branch and open a pull request.", | |
| 93 | "- Force pushes to main are blocked. [ruleset \"Protect main\", non_fast_forward]", | |
| 94 | " Pull, then push without --force.", | |
| 95 | "See the rules that hold for it: https://g1t.sh/acme/web/settings/rules?branch=main", | |
| 96 | "", | |
| 97 | ] | |
| 98 | ); | |
| 99 | } | |
| 100 | ||
| 101 | #[test] | |
| 102 | fn a_merge_refusal_is_one_sentence() { | |
| 103 | let refused = judged(&[("pull_request", "It needs 2 approving reviews; it has 0.", ""), ("merge_window", "Merging is frozen.", "")]); | |
| 104 | assert_eq!(summary(&blocking(&[refused]).to_vec()).as_deref(), Some("It needs 2 approving reviews; it has 0. One more rule is not met.")); | |
| 105 | assert_eq!(summary(&[]), None); | |
| 106 | } | |
| 107 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.