ref:main
use crate::client::Client;
use crate::config::{self, Config};
use crate::output;
use clap::{Args, Subcommand};
use serde::Deserialize;
#[derive(Args)]
pub struct RepoArgs {
#[command(subcommand)]
pub command: RepoCommand,
}
#[derive(Subcommand)]
pub enum RepoCommand {
/// List repositories in an organization
List {
/// Organization slug (defaults to org from default repo)
#[arg(long)]
org: Option<String>,
/// Maximum results
#[arg(long, default_value = "30")]
limit: u32,
},
/// Show repository details
View {
/// Repository (org/repo), or detected from git remote
repo: Option<String>,
},
/// Create a new repository
Create {
/// Repository name
#[arg(long)]
name: String,
/// Organization slug (defaults to org from default repo)
#[arg(long)]
org: Option<String>,
/// Description
#[arg(long, default_value = "")]
description: String,
/// Visibility: private or public
#[arg(long, default_value = "private")]
visibility: String,
},
/// Clone a repository via SSH
Clone {
/// Repository (org/repo)
repo: String,
/// Local directory name
dir: Option<String>,
},
/// Set the default repository for commands
SetDefault {
/// Repository (org/repo)
repo: String,
},
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Repo {
name: Option<String>,
slug: Option<String>,
description: Option<String>,
default_branch: Option<String>,
visibility: Option<String>,
fork_count: Option<i64>,
inserted_at: Option<String>,
}
pub async fn run(args: RepoArgs) -> Result<(), Box<dyn std::error::Error>> {
match args.command {
RepoCommand::List { org, limit } => list(org.as_deref(), limit).await,
RepoCommand::View { repo } => view(repo.as_deref()).await,
RepoCommand::Create {
name,
org,
description,
visibility,
} => create(&name, org.as_deref(), &description, &visibility).await,
RepoCommand::Clone { repo, dir } => clone(&repo, dir.as_deref()).await,
RepoCommand::SetDefault { repo } => set_default(&repo).await,
}
}
async fn list(org: Option<&str>, limit: u32) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let org_slug = match org {
Some(o) => o.to_string(),
None => {
let (resolved_org, _) = config::resolve_repo(None)?;
resolved_org
}
};
let resp: serde_json::Value = client
.get_with_query(
&format!("/{org_slug}/repos"),
&[("per_page", &limit.to_string())],
)
.await?;
if output::is_json() {
output::print_json(&resp);
return Ok(());
}
let repos: Vec<Repo> = if let Some(arr) = resp.get("repositories") {
serde_json::from_value(arr.clone()).unwrap_or_default()
} else if let Some(arr) = resp.get("repos") {
serde_json::from_value(arr.clone()).unwrap_or_default()
} else if let Some(arr) = resp.get("data") {
serde_json::from_value(arr.clone()).unwrap_or_default()
} else {
serde_json::from_value(resp).unwrap_or_default()
};
output::header(&format!("Repositories ({org_slug})"));
let rows: Vec<Vec<String>> = repos
.iter()
.map(|r| {
let desc = r.description.clone().unwrap_or_default();
let desc = if desc.chars().count() > 60 {
format!("{}…", desc.chars().take(59).collect::<String>())
} else {
desc
};
vec![
r.slug
.clone()
.or_else(|| r.name.clone())
.unwrap_or_default(),
output::colorize_status(r.visibility.as_deref().unwrap_or("?")),
r.default_branch.clone().unwrap_or_default(),
desc,
]
})
.collect();
output::print_table(&["NAME", "VISIBILITY", "DEFAULT", "DESCRIPTION"], &rows);
Ok(())
}
async fn view(repo: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
if output::is_json() {
let resp: serde_json::Value = client.get(&format!("/{org}/{name}")).await?;
output::print_json(&resp);
return Ok(());
}
let repo: Repo = client.get(&format!("/{org}/{name}")).await?;
output::header(&format!("{org}/{}", repo.slug.as_deref().unwrap_or(&name)));
if let Some(ref desc) = repo.description {
if !desc.is_empty() {
output::detail("Description", desc);
}
}
if let Some(ref branch) = repo.default_branch {
output::detail("Default branch", branch);
}
if let Some(ref vis) = repo.visibility {
output::detail("Visibility", vis);
}
if let Some(forks) = repo.fork_count {
output::detail("Forks", &forks.to_string());
}
if let Some(ref ts) = repo.inserted_at {
output::detail("Created", &output::format_time(ts));
}
Ok(())
}
async fn create(
name: &str,
org: Option<&str>,
description: &str,
visibility: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
// Resolve org: explicit flag, or first part of default repo
let org_slug = match org {
Some(o) => o.to_string(),
None => {
let (resolved_org, _) = config::resolve_repo(None)?;
resolved_org
}
};
let resp: serde_json::Value = client
.post(
&format!("/{org_slug}/repos"),
&serde_json::json!({
"name": name,
"description": description,
"visibility": visibility,
}),
)
.await?;
let slug = resp
.pointer("/slug")
.or_else(|| resp.pointer("/data/slug"))
.and_then(|v| v.as_str())
.unwrap_or(name);
output::success(&format!("Created repository {org_slug}/{slug}"));
let config = Config::load()?;
if let Some(ref url) = config.server_url {
println!("\n {url}/{org_slug}/{slug}");
}
Ok(())
}
async fn clone(repo: &str, dir: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
let config = Config::load()?;
let server = config.server_url()?;
// Parse server URL to get hostname for SSH
let url = url::Url::parse(server)?;
let host = url.host_str().unwrap_or("localhost");
let ssh_url = format!("git@{host}:{repo}.git");
let target = dir.unwrap_or_else(|| repo.rsplit('/').next().unwrap_or(repo));
output::info(&format!("Cloning {ssh_url} into {target}/"));
let status = std::process::Command::new("git")
.args(["clone", &ssh_url, target])
.status()?;
if status.success() {
output::success(&format!("Cloned {repo} into {target}/"));
} else {
return Err(format!("git clone exited with status {status}").into());
}
Ok(())
}
async fn set_default(repo: &str) -> Result<(), Box<dyn std::error::Error>> {
// Validate format
config::resolve_repo(Some(repo))?;
let mut config = Config::load()?;
config.default_repo = Some(repo.to_string());
config.save()?;
output::success(&format!("Default repository set to {repo}"));
Ok(())
}