use std::env;
use anyhow::{
Context,
Result,
};
use cargo_metadata::MetadataCommand;
#[allow(clippy::disallowed_methods)] pub fn detect_repo() -> Result<(String, String)> {
if let Ok(repo) = env::var("GITHUB_REPOSITORY") {
let parts: Vec<&str> = repo.split('/').collect();
if parts.len() == 2 {
return Ok((parts[0].to_string(), parts[1].to_string()));
}
}
let repo = gix::discover(".").context("Failed to discover git repository")?;
let remote = repo
.find_default_remote(gix::remote::Direction::Fetch)
.context("Failed to find default remote")?
.context("No default remote found")?;
let remote_url = remote
.url(gix::remote::Direction::Fetch)
.context("Failed to get remote URL")?;
let url_str = remote_url.to_string();
if let Some(rest) = url_str.strip_prefix("git@github.com:") {
let rest_trimmed: &str = rest.strip_suffix(".git").unwrap_or(rest);
let parts: Vec<&str> = rest_trimmed.split('/').collect();
if parts.len() >= 2 {
return Ok((parts[0].to_string(), parts[1].to_string()));
}
} else if let Some(rest) = url_str.strip_prefix("https://github.com/") {
let rest_trimmed: &str = rest.strip_suffix(".git").unwrap_or(rest);
let parts: Vec<&str> = rest_trimmed.split('/').collect();
if parts.len() >= 2 {
return Ok((parts[0].to_string(), parts[1].to_string()));
}
}
anyhow::bail!(
"Could not detect GitHub repository. Set GITHUB_REPOSITORY or use --owner/--repo flags"
);
}
pub fn get_owner_repo(owner: Option<String>, repo: Option<String>) -> Result<(String, String)> {
match (owner, repo) {
(Some(o), Some(r)) => Ok((o, r)),
(Some(_), None) | (None, Some(_)) => {
anyhow::bail!("Both --owner and --repo must be provided together");
}
(None, None) => detect_repo(),
}
}
pub fn find_package(manifest_path: Option<&std::path::Path>) -> Result<cargo_metadata::Package> {
let mut cmd = MetadataCommand::new();
if let Some(path) = manifest_path {
cmd.manifest_path(path);
}
let metadata = cmd.exec().context("Failed to get cargo metadata")?;
let current_dir = std::env::current_dir().context("Failed to get current directory")?;
let canonical_current_dir = current_dir.canonicalize().ok();
let packages_with_dirs: Vec<_> = metadata
.packages
.iter()
.filter_map(|pkg| {
pkg.manifest_path
.as_std_path()
.parent()
.and_then(|p| p.canonicalize().ok())
.map(|p| (pkg.clone(), p))
})
.collect();
if let Some(ref canonical_current) = canonical_current_dir
&& let Some((pkg, _)) = packages_with_dirs
.iter()
.find(|(_, pkg_dir)| pkg_dir == canonical_current)
{
return Ok(pkg.clone());
}
let current_manifest = current_dir.join("Cargo.toml");
let canonical_current_manifest = current_manifest.canonicalize().ok();
let packages_with_manifests: Vec<_> = metadata
.packages
.iter()
.filter_map(|pkg| {
pkg.manifest_path
.as_std_path()
.canonicalize()
.ok()
.map(|p| (pkg.clone(), p))
})
.collect();
if let Some(ref canonical) = canonical_current_manifest
&& let Some((pkg, _)) = packages_with_manifests
.iter()
.find(|(_, pkg_path)| pkg_path == canonical)
{
return Ok(pkg.clone());
}
if let Some(root_package) = metadata.root_package() {
return Ok(root_package.clone());
}
if metadata.workspace_default_members.is_available()
&& !metadata.workspace_default_members.is_empty()
&& let Some(first_default_id) = metadata.workspace_default_members.first()
&& let Some(default_package) = metadata
.packages
.iter()
.find(|pkg| &pkg.id == first_default_id)
{
return Ok(default_package.clone());
}
anyhow::bail!(
"No package found in current directory. Run this command from a package directory, \
or use --manifest-path to specify a package."
)
}
pub fn get_package_version_from_manifest(manifest_path: &std::path::Path) -> Result<String> {
let package = find_package(Some(manifest_path))?;
Ok(package.version.to_string())
}
pub fn get_metadata(manifest_path: Option<&std::path::Path>) -> Result<cargo_metadata::Metadata> {
let mut cmd = MetadataCommand::new();
if let Some(path) = manifest_path {
cmd.manifest_path(path);
}
cmd.exec().context("Failed to get cargo metadata")
}
pub fn get_workspace_packages(
manifest_path: Option<&std::path::Path>,
) -> Result<Vec<cargo_metadata::Package>> {
let metadata = get_metadata(manifest_path)?;
Ok(metadata.packages)
}
#[cfg(test)]
mod tests {
use std::env;
use super::*;
#[test]
fn test_get_owner_repo_both_provided() {
let result = get_owner_repo(Some("owner".to_string()), Some("repo".to_string()));
assert!(result.is_ok());
assert_eq!(result.unwrap(), ("owner".to_string(), "repo".to_string()));
}
#[test]
fn test_get_owner_repo_only_owner() {
let result = get_owner_repo(Some("owner".to_string()), None);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Both --owner and --repo must be provided")
);
}
#[test]
fn test_get_owner_repo_only_repo() {
let result = get_owner_repo(None, Some("repo".to_string()));
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Both --owner and --repo must be provided")
);
}
#[test]
fn test_get_owner_repo_from_env() {
let original = env::var("GITHUB_REPOSITORY").ok();
unsafe {
env::set_var("GITHUB_REPOSITORY", "test-owner/test-repo");
}
let result = get_owner_repo(None, None);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
("test-owner".to_string(), "test-repo".to_string())
);
unsafe {
if let Some(val) = original {
env::set_var("GITHUB_REPOSITORY", &val);
} else {
env::remove_var("GITHUB_REPOSITORY");
}
}
}
#[test]
fn test_get_owner_repo_invalid_env() {
unsafe {
env::set_var("GITHUB_REPOSITORY", "invalid");
}
let _result = get_owner_repo(None, None);
unsafe {
env::remove_var("GITHUB_REPOSITORY");
}
}
#[test]
fn test_find_package_in_current_dir() {
let result = find_package(None);
if let Err(e) = result {
assert!(e.to_string().contains("package") || e.to_string().contains("manifest"));
}
}
#[test]
fn test_find_package_with_manifest_path() {
let result = find_package(Some(std::path::Path::new("/nonexistent/path/Cargo.toml")));
assert!(result.is_err());
}
#[test]
fn test_get_package_version_from_manifest() {
let result =
get_package_version_from_manifest(std::path::Path::new("/nonexistent/path/Cargo.toml"));
assert!(result.is_err());
}
#[test]
fn test_detect_repo_from_env() {
let original = env::var("GITHUB_REPOSITORY").ok();
unsafe {
env::set_var("GITHUB_REPOSITORY", "env-owner/env-repo");
}
let result = detect_repo();
assert!(result.is_ok());
let (owner, repo) = result.unwrap();
assert_eq!(owner, "env-owner");
assert_eq!(repo, "env-repo");
unsafe {
if let Some(val) = original {
env::set_var("GITHUB_REPOSITORY", &val);
} else {
env::remove_var("GITHUB_REPOSITORY");
}
}
}
#[test]
fn test_detect_repo_invalid_env_format() {
unsafe {
env::set_var("GITHUB_REPOSITORY", "invalid-format");
}
let _result = detect_repo();
unsafe {
env::remove_var("GITHUB_REPOSITORY");
}
}
}