use std::io::Write;
use anyhow::{anyhow, Context, Result};
use clap::{Args, Subcommand};
use serde_json::{json, Value};
use crate::client;
use crate::commands::resolve_repo_slug;
use crate::output::{render_record, render_table, FormatChoice};
#[derive(Debug, Subcommand)]
pub enum RepoCmd {
List(ListArgs),
View(ViewArgs),
Clone(CloneArgs),
}
#[derive(Debug, Args)]
pub struct ListArgs {
#[arg(long)]
pub all: bool,
#[arg(long, default_value_t = false)]
pub json: bool,
#[arg(long, default_value_t = false)]
pub tsv: bool,
}
#[derive(Debug, Args)]
pub struct ViewArgs {
pub slug: String,
#[arg(long, default_value_t = false)]
pub json: bool,
#[arg(long, default_value_t = false)]
pub tsv: bool,
}
#[derive(Debug, Args)]
pub struct CloneArgs {
pub slug: String,
pub dest: Option<String>,
}
pub fn run(cmd: RepoCmd) -> Result<()> {
match cmd {
RepoCmd::List(a) => list(a),
RepoCmd::View(a) => view(a),
RepoCmd::Clone(a) => clone(a),
}
}
fn list(args: ListArgs) -> Result<()> {
let (cli, cfg) = client::authenticated()?;
let data = cli.execute(LIST_REPOS, json!({}))?;
let repos = data["repositories"]
.as_array()
.ok_or_else(|| anyhow!("repositories field missing"))?;
let rows = repo_rows(repos, &cfg.username, args.all);
let mut stdout = std::io::stdout().lock();
let fmt = FormatChoice {
json: args.json,
tsv: args.tsv,
}
.resolve();
stdout
.write_all(render_table(&["repo", "visibility", "description"], &rows, fmt).as_bytes())?;
Ok(())
}
fn view(args: ViewArgs) -> Result<()> {
let (cli, _cfg) = client::authenticated()?;
let (owner, name) = resolve_repo_slug(&args.slug)?;
let data = cli.execute(LIST_REPOS, json!({}))?;
let repos = data["repositories"]
.as_array()
.ok_or_else(|| anyhow!("repositories field missing"))?;
let r = repos
.iter()
.find(|r| r["ownerSlug"].as_str() == Some(&owner) && r["name"].as_str() == Some(&name))
.ok_or_else(|| anyhow!("no repository named {}/{}", owner, name))?;
let pairs = repo_record_pairs(r, &owner, &name);
let mut stdout = std::io::stdout().lock();
let fmt = FormatChoice {
json: args.json,
tsv: args.tsv,
}
.resolve();
stdout.write_all(render_record(&pairs, fmt).as_bytes())?;
Ok(())
}
fn clone(args: CloneArgs) -> Result<()> {
let (cli, _cfg) = client::authenticated()?;
let (owner, name) = crate::commands::parse_slug(&args.slug)?;
let url = format!("{}/{}/{}.git", cli.host(), owner, name);
let dest = args.dest.unwrap_or_else(|| name.clone());
let status = std::process::Command::new("git")
.arg("clone")
.arg(&url)
.arg(&dest)
.status()
.context("run `git clone`")?;
if !status.success() {
return Err(anyhow!("git clone exited with status {status}"));
}
Ok(())
}
const LIST_REPOS: &str =
"{ repositories { id ownerSlug name description visibility defaultBranch updatedAt } }";
fn repo_rows(repos: &[Value], username: &str, include_all: bool) -> Vec<Vec<String>> {
repos
.iter()
.filter(|r| include_all || r["ownerSlug"].as_str() == Some(username))
.map(|r| {
vec![
format!(
"{}/{}",
r["ownerSlug"].as_str().unwrap_or(""),
r["name"].as_str().unwrap_or("")
),
r["visibility"].as_str().unwrap_or("").to_string(),
r["description"].as_str().unwrap_or("").to_string(),
]
})
.collect()
}
fn repo_record_pairs(repo: &Value, owner: &str, name: &str) -> Vec<(&'static str, String)> {
vec![
("Repo", format!("{owner}/{name}")),
(
"Visibility",
repo["visibility"].as_str().unwrap_or("").to_string(),
),
(
"Default branch",
repo["defaultBranch"].as_str().unwrap_or("").to_string(),
),
(
"Description",
repo["description"].as_str().unwrap_or("").to_string(),
),
(
"Updated",
repo["updatedAt"].as_str().unwrap_or("").to_string(),
),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn repo_rows_filter_to_viewer_unless_all_is_set() {
let repos = vec![
json!({
"ownerSlug": "bri",
"name": "runtime",
"visibility": "PUBLIC",
"description": "Runtime platform"
}),
json!({
"ownerSlug": "runtime-labs",
"name": "ops",
"visibility": "PRIVATE",
"description": "Operations"
}),
json!({}),
];
assert_eq!(
repo_rows(&repos, "bri", false),
vec![vec!["bri/runtime", "PUBLIC", "Runtime platform"]]
);
assert_eq!(
repo_rows(&repos, "bri", true),
vec![
vec!["bri/runtime", "PUBLIC", "Runtime platform"],
vec!["runtime-labs/ops", "PRIVATE", "Operations"],
vec!["/", "", ""],
]
);
}
#[test]
fn repo_record_pairs_format_defaults_without_leaking_api_shape() {
let repo = json!({
"visibility": "PRIVATE",
"defaultBranch": "trunk",
"description": "A repo",
"updatedAt": "2026-06-18T01:00:00Z"
});
assert_eq!(
repo_record_pairs(&repo, "bri", "runtime"),
vec![
("Repo", "bri/runtime".to_string()),
("Visibility", "PRIVATE".to_string()),
("Default branch", "trunk".to_string()),
("Description", "A repo".to_string()),
("Updated", "2026-06-18T01:00:00Z".to_string()),
]
);
assert_eq!(
repo_record_pairs(&json!({}), "owner", "repo"),
vec![
("Repo", "owner/repo".to_string()),
("Visibility", String::new()),
("Default branch", String::new()),
("Description", String::new()),
("Updated", String::new()),
]
);
}
}