1#![allow(clippy::print_stdout)]
6
7use crate::cli::RulesetCommand;
8use crate::client::Client;
9use crate::output::print_json;
10use crate::repository::{detect_remote, parse_repo_spec};
11use anyhow::Context;
12
13pub fn run(cmd: RulesetCommand) -> anyhow::Result<()> {
19 match cmd {
20 RulesetCommand::List {
21 repo,
22 json,
23 hostname,
24 } => list(repo.as_deref(), json, hostname.as_deref()),
25 RulesetCommand::View {
26 id,
27 repo,
28 web,
29 json,
30 hostname,
31 } => view(id, repo.as_deref(), web, json, hostname.as_deref()),
32 }
33}
34
35fn list(
36 repo: Option<&str>,
37 json: Option<Vec<String>>,
38 hostname: Option<&str>,
39) -> anyhow::Result<()> {
40 let spec = match repo {
41 Some(s) => parse_repo_spec(s).context("invalid repository spec")?,
42 None => detect_remote().ok_or_else(|| {
43 anyhow::anyhow!("could not detect repository; specify OWNER/REPO with --repo")
44 })?,
45 };
46
47 let host = hostname.unwrap_or("github.com");
48 let client = Client::new(host).context("failed to create HTTP client")?;
49
50 let path = format!("/repos/{}/{}/rulesets?per_page=100", spec.owner, spec.repo);
51
52 let response = client.get(&path).context("failed to fetch rulesets")?;
53 let status = response.status();
54 if !status.is_success() {
55 anyhow::bail!("failed to list rulesets: HTTP {status}");
56 }
57
58 let rulesets: Vec<serde_json::Value> = response.json().context("failed to parse response")?;
59
60 if let Some(fields) = json {
61 let fields_ref: Option<&[String]> = if fields.is_empty() {
62 None
63 } else {
64 Some(&fields)
65 };
66 print_json(&rulesets, fields_ref);
67 return Ok(());
68 }
69
70 if rulesets.is_empty() {
71 println!("No rulesets found.");
72 return Ok(());
73 }
74
75 println!("{:<8} {:<30} ENFORCEMENT", "ID", "NAME");
76 for r in &rulesets {
77 let id = r["id"].as_u64().unwrap_or(0);
78 let name = r["name"].as_str().unwrap_or("—");
79 let enforcement = r["enforcement"].as_str().unwrap_or("—");
80 let name_truncated = crate::cmd::util::truncate(name, 30);
81 println!("{id:<8} {name_truncated:<30} {enforcement}");
82 }
83
84 Ok(())
85}
86
87fn view(
88 id: u32,
89 repo: Option<&str>,
90 web: bool,
91 json: Option<Vec<String>>,
92 hostname: Option<&str>,
93) -> anyhow::Result<()> {
94 let spec = match repo {
95 Some(s) => parse_repo_spec(s).context("invalid repository spec")?,
96 None => detect_remote().ok_or_else(|| {
97 anyhow::anyhow!("could not detect repository; specify OWNER/REPO with --repo")
98 })?,
99 };
100
101 let host = hostname.unwrap_or("github.com");
102 let client = Client::new(host).context("failed to create HTTP client")?;
103
104 let path = format!("/repos/{}/{}/rulesets/{id}", spec.owner, spec.repo);
105 let response = client.get(&path).context("failed to fetch ruleset")?;
106
107 let status = response.status();
108 if status == reqwest::StatusCode::NOT_FOUND {
109 anyhow::bail!("ruleset #{id} not found in repository '{spec}'");
110 }
111 if !status.is_success() {
112 anyhow::bail!("failed to view ruleset: HTTP {status}");
113 }
114
115 let ruleset: serde_json::Value = response.json().context("failed to parse response")?;
116
117 if web {
118 let url = format!(
119 "https://{host}/{}/{}/settings/rules/{id}",
120 spec.owner, spec.repo
121 );
122 println!("Open {url} in your browser");
123 return Ok(());
124 }
125
126 if let Some(fields) = json {
127 let fields_ref: Option<&[String]> = if fields.is_empty() {
128 None
129 } else {
130 Some(&fields)
131 };
132 print_json(&ruleset, fields_ref);
133 return Ok(());
134 }
135
136 let name = ruleset["name"].as_str().unwrap_or("—");
137 let enforcement = ruleset["enforcement"].as_str().unwrap_or("—");
138 let target = ruleset["target"].as_str().unwrap_or("—");
139
140 println!(" Name: {name}");
141 println!(" Enforcement: {enforcement}");
142 println!(" Target: {target}");
143
144 if let Some(rules) = ruleset["rules"].as_array() {
145 println!(" Rules:");
146 for rule in rules {
147 let rule_type = rule["type"].as_str().unwrap_or("—");
148 println!(" - {rule_type}");
149 }
150 }
151
152 Ok(())
153}