Skip to main content

gor/cmd/
ruleset.rs

1//! Implementation of the `gor ruleset` subcommand.
2//!
3//! Provides repository ruleset listing and viewing.
4
5#![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
13/// Run the `gor ruleset` subcommand.
14///
15/// # Errors
16///
17/// Returns an error if the command execution fails.
18pub 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}