use anyhow::{Result, bail};
use std::path::Path;
use super::remote::{detect_github_repo_in, detect_owner_repo_in};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RepoSlug {
owner: String,
name: String,
}
impl RepoSlug {
pub fn owner(&self) -> &str {
&self.owner
}
pub fn name(&self) -> &str {
&self.name
}
pub fn slug(&self) -> String {
format!("{}/{}", self.owner, self.name)
}
fn validated(owner: String, name: String) -> Result<Self> {
if owner.trim().is_empty() || name.trim().is_empty() {
bail!(
"repository slug requires a non-empty owner and name (got {:?}/{:?})",
owner,
name
);
}
Ok(Self { owner, name })
}
#[cfg(any(test, feature = "test-helpers"))]
pub fn for_test(owner: impl Into<String>, name: impl Into<String>) -> Self {
Self {
owner: owner.into(),
name: name.into(),
}
}
}
fn override_pair<'a>(owner: Option<&'a str>, name: Option<&'a str>) -> Option<(&'a str, &'a str)> {
let owner = owner.filter(|v| !v.trim().is_empty())?;
let name = name.filter(|v| !v.trim().is_empty())?;
Some((owner, name))
}
pub fn resolve_github_slug_in(
override_owner: Option<&str>,
override_name: Option<&str>,
cwd: &Path,
) -> Result<RepoSlug> {
if let Some((owner, name)) = override_pair(override_owner, override_name) {
return RepoSlug::validated(owner.to_string(), name.to_string());
}
let (owner, name) = detect_github_repo_in(cwd)?;
RepoSlug::validated(owner, name)
}
pub fn resolve_github_slug(
override_owner: Option<&str>,
override_name: Option<&str>,
) -> Result<RepoSlug> {
resolve_github_slug_in(override_owner, override_name, &std::env::current_dir()?)
}
pub fn resolve_repo_slug_in(
override_owner: Option<&str>,
override_name: Option<&str>,
cwd: &Path,
) -> Result<RepoSlug> {
if let Some((owner, name)) = override_pair(override_owner, override_name) {
return RepoSlug::validated(owner.to_string(), name.to_string());
}
let (owner, name) = detect_owner_repo_in(cwd)?;
RepoSlug::validated(owner, name)
}
pub fn resolve_repo_slug(
override_owner: Option<&str>,
override_name: Option<&str>,
) -> Result<RepoSlug> {
resolve_repo_slug_in(override_owner, override_name, &std::env::current_dir()?)
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
fn git(dir: &Path, args: &[&str]) {
let out = crate::test_helpers::output_with_spawn_retry(
|| {
let mut cmd = Command::new("git");
cmd.args(args)
.current_dir(dir)
.env("GIT_TERMINAL_PROMPT", "0")
.env("LC_ALL", "C");
cmd
},
"git",
);
assert!(out.status.success(), "git {args:?} failed");
}
fn repo_with_origin(url: &str) -> tempfile::TempDir {
let tmp = tempfile::tempdir().unwrap();
git(tmp.path(), &["init", "-q"]);
git(tmp.path(), &["remote", "add", "origin", url]);
tmp
}
#[test]
fn override_wins_over_remote_without_touching_git() {
let tmp = tempfile::tempdir().unwrap();
let slug = resolve_github_slug_in(Some("cfg-owner"), Some("cfg-name"), tmp.path()).unwrap();
assert_eq!(slug.owner(), "cfg-owner");
assert_eq!(slug.name(), "cfg-name");
assert_eq!(slug.slug(), "cfg-owner/cfg-name");
}
#[test]
fn empty_override_falls_through_to_remote() {
let tmp = repo_with_origin("https://github.com/remote-owner/remote-repo.git");
let slug = resolve_github_slug_in(Some(""), Some(" "), tmp.path()).unwrap();
assert_eq!(slug.owner(), "remote-owner");
assert_eq!(slug.name(), "remote-repo");
}
#[test]
fn partial_override_falls_through_to_remote() {
let tmp = repo_with_origin("https://github.com/remote-owner/remote-repo.git");
let slug = resolve_github_slug_in(Some("cfg-owner"), None, tmp.path()).unwrap();
assert_eq!(slug.owner(), "remote-owner");
assert_eq!(slug.name(), "remote-repo");
}
#[test]
fn github_remote_derivation() {
let tmp = repo_with_origin("git@github.com:gh-owner/gh-repo.git");
let slug = resolve_github_slug_in(None, None, tmp.path()).unwrap();
assert_eq!(slug.slug(), "gh-owner/gh-repo");
}
#[test]
fn host_agnostic_derivation_preserves_nested_groups() {
let tmp = repo_with_origin("https://gitlab.com/group/subgroup/repo.git");
assert!(resolve_github_slug_in(None, None, tmp.path()).is_err());
let slug = resolve_repo_slug_in(None, None, tmp.path()).unwrap();
assert_eq!(slug.owner(), "group/subgroup");
assert_eq!(slug.name(), "repo");
}
#[test]
fn missing_remote_is_an_error_not_an_empty_slug() {
let tmp = tempfile::tempdir().unwrap();
git(tmp.path(), &["init", "-q"]);
assert!(resolve_github_slug_in(None, None, tmp.path()).is_err());
assert!(resolve_repo_slug_in(None, None, tmp.path()).is_err());
}
}