Skip to main content

gor/cmd/
org.rs

1//! Implementation of the `gor org` subcommand.
2//!
3//! Provides organization listing for the authenticated user.
4
5#![allow(clippy::print_stdout)]
6
7use crate::cli::OrgCommand;
8use crate::client::Client;
9use crate::output::print_json;
10use anyhow::Context;
11
12/// Run the `gor org` subcommand.
13///
14/// # Errors
15///
16/// Returns an error if the command execution fails.
17pub fn run(cmd: OrgCommand) -> anyhow::Result<()> {
18    match cmd {
19        OrgCommand::List {
20            limit,
21            json,
22            hostname,
23        } => list(limit, json, hostname.as_deref()),
24        OrgCommand::View {
25            org,
26            web,
27            json,
28            hostname,
29        } => view(&org, web, json, hostname.as_deref()),
30    }
31}
32
33/// Execute `gor org list`.
34///
35/// Lists organizations the authenticated user belongs to.
36///
37/// # Errors
38///
39/// Returns an error if the API request fails.
40fn list(limit: u32, json: Option<Vec<String>>, hostname: Option<&str>) -> anyhow::Result<()> {
41    let host = hostname.unwrap_or("github.com");
42    let client = Client::new(host).context("failed to create HTTP client")?;
43
44    let path = format!("/user/orgs?per_page={}", limit.min(100));
45    let response = client.get(&path).context("failed to fetch organizations")?;
46
47    let status = response.status();
48    if !status.is_success() {
49        anyhow::bail!("failed to list organizations: HTTP {status}");
50    }
51
52    let mut orgs: Vec<serde_json::Value> =
53        response.json().context("failed to parse orgs response")?;
54
55    orgs.truncate(limit as usize);
56
57    if let Some(fields) = json {
58        let fields_ref: Option<&[String]> = if fields.is_empty() {
59            None
60        } else {
61            Some(&fields)
62        };
63        print_json(&orgs, fields_ref);
64        return Ok(());
65    }
66
67    print_org_table(&orgs);
68    Ok(())
69}
70
71/// Execute `gor org view`.
72///
73/// Views an organization's profile and metadata.
74///
75/// # Errors
76///
77/// Returns an error if the API request fails.
78fn view(
79    org: &str,
80    web: bool,
81    json: Option<Vec<String>>,
82    hostname: Option<&str>,
83) -> anyhow::Result<()> {
84    let host = hostname.unwrap_or("github.com");
85    let client = Client::new(host).context("failed to create HTTP client")?;
86
87    let path = format!("/orgs/{org}");
88    let response = client.get(&path).context("failed to fetch organization")?;
89
90    let status = response.status();
91    if status == reqwest::StatusCode::NOT_FOUND {
92        anyhow::bail!("organization '{org}' not found");
93    }
94    if !status.is_success() {
95        anyhow::bail!("failed to view organization: HTTP {status}");
96    }
97
98    let org_data: serde_json::Value = response.json().context("failed to parse response")?;
99
100    // --web / -w: open in browser
101    if web {
102        if let Some(url) = org_data["html_url"].as_str() {
103            crate::cmd::browse::open_in_browser(url);
104            return Ok(());
105        }
106    }
107
108    // --json: output as JSON
109    if let Some(fields) = json {
110        let fields_ref: Option<&[String]> = if fields.is_empty() {
111            None
112        } else {
113            Some(&fields)
114        };
115        print_json(&org_data, fields_ref);
116        return Ok(());
117    }
118
119    // Default: print details
120    let name = org_data["name"].as_str().unwrap_or("—");
121    let description = org_data["description"].as_str().unwrap_or("No description");
122    let location = org_data["location"].as_str().unwrap_or("—");
123    let blog = org_data["blog"].as_str().unwrap_or("—");
124    let email = org_data["email"].as_str().unwrap_or("—");
125    let members = org_data["members_count"].as_u64().unwrap_or(0);
126    let repos = org_data["public_repos"].as_u64().unwrap_or(0);
127
128    println!("  Name: {name}");
129    println!("  Description: {description}");
130    println!("  Location: {location}");
131    println!("  Website: {blog}");
132    println!("  Email: {email}");
133    println!("  Members: {members}");
134    println!("  Public repos: {repos}");
135
136    Ok(())
137}
138
139/// Print a formatted organization list table.
140fn print_org_table(orgs: &[serde_json::Value]) {
141    if orgs.is_empty() {
142        println!("No organizations found.");
143        return;
144    }
145
146    let login_width = 20;
147    let desc_width = 50;
148
149    println!("{:<login_width$}  {:<desc_width$}", "LOGIN", "DESCRIPTION");
150
151    for org in orgs {
152        let login = org["login"].as_str().unwrap_or("—");
153        let description = org["description"].as_str().unwrap_or("—");
154
155        let desc_truncated = crate::cmd::util::truncate(description, desc_width);
156
157        println!("{login:<login_width$}  {desc_truncated:<desc_width$}");
158    }
159}
160
161#[cfg(test)]
162#[allow(clippy::expect_used)]
163mod tests {
164    use super::*;
165    use serde_json::json;
166
167    #[test]
168    fn print_org_table_basic() {
169        let orgs = vec![json!({
170            "login": "my-org",
171            "description": "My organization"
172        })];
173        print_org_table(&orgs);
174    }
175
176    #[test]
177    fn print_org_table_empty() {
178        let orgs: Vec<serde_json::Value> = vec![];
179        print_org_table(&orgs);
180    }
181}