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!( "Deletion of protected branch '{}' is forbidden", r#ref.name )); } continue; } // Check tag push if r#ref.name.starts_with("refs/tags/") { if protection.forbid_tag_push { return Some(format!( "Tag push to protected branch '{}' is forbidden", 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!( "Force push to protected branch '{}' is forbidden", r#ref.name )); } // Check push if protection.forbid_push { return Some(format!( "Push to protected branch '{}' is forbidden", r#ref.name )); } } None }