use crate::error::{ActionError, ProviderError, RepoLensError};
use serde_json::json;
use std::io::Write;
use std::process::{Command, Stdio};
use super::plan::BranchProtectionSettings;
use crate::utils::prerequisites::{get_repo_info, is_gh_available};
pub async fn configure(
branch: &str,
settings: &BranchProtectionSettings,
) -> Result<(), RepoLensError> {
if !is_gh_available() {
return Err(RepoLensError::Provider(
ProviderError::GitHubCliNotAvailable,
));
}
let repo = get_repo_info().map_err(|e| {
RepoLensError::Action(ActionError::ExecutionFailed {
message: format!("Failed to get repository info: {}", e),
})
})?;
let mut payload = json!({
"enforce_admins": settings.enforce_admins,
"required_linear_history": settings.require_linear_history,
"allow_force_pushes": !settings.block_force_push,
"allow_deletions": !settings.block_deletions,
"required_conversation_resolution": settings.require_conversation_resolution,
"restrictions": null,
});
if settings.require_status_checks {
payload["required_status_checks"] = json!({
"strict": true,
"contexts": []
});
} else {
payload["required_status_checks"] = json!(null);
}
if settings.required_approvals > 0 {
payload["required_pull_request_reviews"] = json!({
"required_approving_review_count": settings.required_approvals,
"dismiss_stale_reviews": true
});
} else {
payload["required_pull_request_reviews"] = json!(null);
}
let mut child = Command::new("gh")
.args([
"api",
&format!("repos/{}/branches/{}/protection", repo, branch),
"--method",
"PUT",
"--input",
"-",
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|_| {
RepoLensError::Provider(ProviderError::CommandFailed {
command: format!("gh api repos/{}/branches/{}/protection", repo, branch),
})
})?;
let json_str = serde_json::to_string(&payload).map_err(|e| {
RepoLensError::Action(ActionError::ExecutionFailed {
message: format!("Failed to serialize branch protection payload: {}", e),
})
})?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(json_str.as_bytes()).map_err(|e| {
RepoLensError::Action(ActionError::ExecutionFailed {
message: format!("Failed to write to gh CLI stdin: {}", e),
})
})?;
}
let output = child.wait_with_output().map_err(|_| {
RepoLensError::Provider(ProviderError::CommandFailed {
command: format!("gh api repos/{}/branches/{}/protection", repo, branch),
})
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("Resource not accessible") {
return Err(RepoLensError::Action(ActionError::ExecutionFailed {
message: "Cannot configure branch protection. This may require admin access or \
the repository may not support this feature (e.g., free private repos)."
.to_string(),
}));
}
return Err(RepoLensError::Action(ActionError::ExecutionFailed {
message: format!("Failed to configure branch protection: {}", stderr),
}));
}
if settings.require_signed_commits {
let output = Command::new("gh")
.args([
"api",
&format!(
"repos/{}/branches/{}/protection/required_signatures",
repo, branch
),
"--method",
"POST",
])
.output()
.map_err(|_| {
RepoLensError::Provider(ProviderError::CommandFailed {
command: format!(
"gh api repos/{}/branches/{}/protection/required_signatures",
repo, branch
),
})
})?;
if !output.status.success() {
tracing::warn!("Could not enable signed commits requirement (may require GitHub Pro)");
}
}
Ok(())
}