use crate::ssh::ref_update::RefUpdate; use models::repos::repo_branch_protect; /// Ref name matches a protection rule exactly, or as a directory prefix /// (e.g. "refs/heads/main" matches "refs/heads/main" and "refs/heads/main/*" /// but NOT "refs/heads/main-v2"). fn ref_matches_protection(ref_name: &str, protection_branch: &str) -> bool { ref_name == protection_branch || ref_name.starts_with(&format!("{}/", protection_branch)) } /// Granular branch protection check (same logic as HTTP handler). /// Returns `Some(error_message)` if the push should be rejected. pub fn check_branch_protection( branch_protects: &[repo_branch_protect::Model], r#ref: &RefUpdate, ) -> Option { for protection in branch_protects { if !ref_matches_protection(&r#ref.name, &protection.branch) { continue; } // Check deletion (new_oid is all zeros) if r#ref.new_oid == "0000000000000000000000000000000000000000" { if protection.forbid_deletion { return Some(format!( "GitData: 🛡️ protected branch rejected. Deletion of '{}' is forbidden. Create a PR or ask a maintainer to update branch protection.", r#ref.name )); } continue; } // Check tag push if r#ref.name.starts_with("refs/tags/") { if protection.forbid_tag_push { return Some(format!( "GitData: 🛡️ protected ref rejected. Tag push to '{}' is forbidden by branch protection.", r#ref.name )); } continue; } // Check force push: old != new AND old is non-zero (non-fast-forward) let is_new_branch = r#ref.old_oid == "0000000000000000000000000000000000000000"; if !is_new_branch && r#ref.old_oid != r#ref.new_oid && r#ref.name.starts_with("refs/heads/") && protection.forbid_force_push { return Some(format!( "GitData: 🛡️ protected branch rejected. Force push to '{}' is forbidden. Create a PR instead of rewriting protected history.", r#ref.name )); } // Check push if protection.forbid_push { return Some(format!( "GitData: 🛡️ protected branch rejected. Direct push to '{}' is forbidden. Please push to a feature branch and create a PR.", r#ref.name )); } } None }