| 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 | } |