pub mod api;
pub mod auth;
pub mod backup;
pub mod issue;
pub mod org;
pub mod pr;
pub mod release;
pub mod repo;
pub mod run;
use anyhow::{anyhow, Result};
pub(crate) fn parse_slug(slug: &str) -> Result<(String, String)> {
let (owner, name) = slug
.split_once('/')
.ok_or_else(|| anyhow!("expected `<owner>/<name>`, got `{slug}`"))?;
if owner.is_empty() || name.is_empty() {
return Err(anyhow!("expected `<owner>/<name>`, got `{slug}`"));
}
Ok((owner.to_string(), name.to_string()))
}
pub(crate) fn resolve_repo_slug(arg: &str) -> Result<(String, String)> {
if !arg.is_empty() && arg != "-" {
return parse_slug(arg);
}
let url = git_remote_origin_url().ok_or_else(|| {
anyhow!("no repo argument and no git remote in cwd — pass `<owner>/<name>` explicitly")
})?;
parse_remote_url(&url).ok_or_else(|| {
anyhow!(
"cwd's git remote `{url}` doesn't match the runtime URL shape; pass `<owner>/<name>` explicitly"
)
})
}
fn git_remote_origin_url() -> Option<String> {
let out = std::process::Command::new("git")
.args(["config", "--get", "remote.origin.url"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8(out.stdout).ok()?;
let t = s.trim();
if t.is_empty() {
None
} else {
Some(t.to_string())
}
}
pub(crate) fn parse_remote_url(url: &str) -> Option<(String, String)> {
let stripped = url.trim_end_matches(".git").trim_end_matches('/');
if let Some(rest) = stripped.split_once(':').and_then(|(_, r)| {
if stripped.contains('@') && !stripped.contains("://") {
Some(r)
} else {
None
}
}) {
return split_owner_name(rest);
}
let path = match stripped.split_once("://") {
Some((_scheme, rest)) => rest.split_once('/')?.1,
None => return None,
};
split_owner_name(path)
}
fn split_owner_name(path: &str) -> Option<(String, String)> {
let trimmed = path.trim_matches('/');
let parts: Vec<&str> = trimmed.split('/').collect();
if parts.len() < 2 {
return None;
}
let owner = parts[parts.len() - 2];
let name = parts[parts.len() - 1];
if owner.is_empty() || name.is_empty() {
return None;
}
Some((owner.to_string(), name.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_slug_happy() {
let (o, n) = parse_slug("alice/r1").unwrap();
assert_eq!(o, "alice");
assert_eq!(n, "r1");
}
#[test]
fn parse_slug_rejects_bad_shapes() {
assert!(parse_slug("alice").is_err());
assert!(parse_slug("/r1").is_err());
assert!(parse_slug("alice/").is_err());
}
#[test]
fn parse_remote_url_https_with_dot_git() {
assert_eq!(
parse_remote_url("https://example.com/alice/r1.git"),
Some(("alice".into(), "r1".into()))
);
}
#[test]
fn parse_remote_url_https_without_dot_git() {
assert_eq!(
parse_remote_url("https://example.com:8443/alice/r1"),
Some(("alice".into(), "r1".into()))
);
}
#[test]
fn parse_remote_url_ssh_url() {
assert_eq!(
parse_remote_url("ssh://git@example.com:2222/alice/r1.git"),
Some(("alice".into(), "r1".into()))
);
}
#[test]
fn parse_remote_url_scp_style() {
assert_eq!(
parse_remote_url("git@example.com:alice/r1.git"),
Some(("alice".into(), "r1".into()))
);
}
#[test]
fn parse_remote_url_rejects_non_url() {
assert_eq!(parse_remote_url("hello-world"), None);
assert_eq!(parse_remote_url("https://example.com/onlypath"), None);
}
}