pub(crate) fn parse_github_remote(url: &str) -> Option<(String, String)> {
let url = url.trim();
let rest = url
.strip_prefix("https://github.com/")
.or_else(|| url.strip_prefix("ssh://git@github.com/"))
.or_else(|| url.strip_prefix("git@github.com:"))?;
let rest = rest.strip_suffix(".git").unwrap_or(rest);
let segments: Vec<&str> = rest.split('/').filter(|s| !s.is_empty()).collect();
match segments.as_slice() {
[owner, repo] => Some((owner.to_string(), repo.to_string())),
_ => None,
}
}
pub(crate) fn github_remote_matches(remote_url: &str, owner: &str, repo: &str) -> bool {
match parse_github_remote(remote_url) {
Some((remote_owner, remote_repo)) => {
remote_owner.eq_ignore_ascii_case(owner) && remote_repo.eq_ignore_ascii_case(repo)
}
None => false,
}
}
pub(crate) fn git_remote_origin_url(
cwd: Option<&std::path::Path>,
) -> anyhow::Result<Option<String>> {
let mut command = std::process::Command::new("git");
command.args(["remote", "get-url", "origin"]);
if let Some(cwd) = cwd {
command.current_dir(cwd);
}
let output = command.output()?;
if !output.status.success() {
return Ok(None);
}
Ok(Some(String::from_utf8(output.stdout)?.trim().to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::{init_repo_with_committed_file, run_git};
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
#[case::should_parse_https_url(
"https://github.com/octocat/hello-world",
Some(("octocat".to_string(), "hello-world".to_string()))
)]
#[case::should_parse_https_url_with_dot_git_suffix(
"https://github.com/octocat/hello-world.git",
Some(("octocat".to_string(), "hello-world".to_string()))
)]
#[case::should_parse_scp_like_ssh_url(
"git@github.com:octocat/hello-world.git",
Some(("octocat".to_string(), "hello-world".to_string()))
)]
#[case::should_parse_scp_like_ssh_url_without_dot_git_suffix(
"git@github.com:octocat/hello-world",
Some(("octocat".to_string(), "hello-world".to_string()))
)]
#[case::should_parse_explicit_ssh_url(
"ssh://git@github.com/octocat/hello-world.git",
Some(("octocat".to_string(), "hello-world".to_string()))
)]
#[case::should_parse_explicit_ssh_url_without_dot_git_suffix(
"ssh://git@github.com/octocat/hello-world",
Some(("octocat".to_string(), "hello-world".to_string()))
)]
#[case::should_trim_surrounding_whitespace(
" https://github.com/octocat/hello-world.git \n",
Some(("octocat".to_string(), "hello-world".to_string()))
)]
#[case::should_reject_non_github_host("https://gitlab.com/octocat/hello-world.git", None)]
#[case::should_reject_url_missing_repo_segment("https://github.com/octocat", None)]
#[case::should_reject_url_with_extra_path_segment(
"https://github.com/octocat/hello-world/extra",
None
)]
#[case::should_reject_empty_string("", None)]
fn should_parse_github_remote(#[case] url: &str, #[case] expected: Option<(String, String)>) {
let actual = parse_github_remote(url);
assert_eq!(expected, actual);
}
#[rstest]
#[case::should_match_identical_owner_and_repo(
"https://github.com/octocat/hello-world.git",
"octocat",
"hello-world",
true
)]
#[case::should_match_case_insensitively(
"https://github.com/Octocat/Hello-World.git",
"octocat",
"hello-world",
true
)]
#[case::should_not_match_different_repo(
"https://github.com/octocat/hello-world.git",
"octocat",
"other-repo",
false
)]
#[case::should_not_match_different_owner(
"https://github.com/octocat/hello-world.git",
"someone-else",
"hello-world",
false
)]
#[case::should_not_match_non_github_remote(
"https://gitlab.com/octocat/hello-world.git",
"octocat",
"hello-world",
false
)]
fn should_check_github_remote_match(
#[case] remote_url: &str,
#[case] owner: &str,
#[case] repo: &str,
#[case] expected: bool,
) {
let actual = github_remote_matches(remote_url, owner, repo);
assert_eq!(expected, actual);
}
#[test]
fn should_return_origin_url_when_repository_has_an_origin_remote() {
let dir = tempfile::TempDir::new().expect("create tempdir");
init_repo_with_committed_file(dir.path(), "fn foo() {}\n");
run_git(
dir.path(),
&[
"remote",
"add",
"origin",
"https://github.com/octocat/hello-world.git",
],
);
let actual =
git_remote_origin_url(Some(dir.path())).expect("git remote get-url should not error");
assert_eq!(
Some("https://github.com/octocat/hello-world.git".to_string()),
actual
);
}
#[test]
fn should_return_none_when_repository_has_no_origin_remote() {
let dir = tempfile::TempDir::new().expect("create tempdir");
init_repo_with_committed_file(dir.path(), "fn foo() {}\n");
let actual = git_remote_origin_url(Some(dir.path()))
.expect("missing origin remote should not error");
assert_eq!(None, actual);
}
#[test]
fn should_return_none_when_directory_is_not_a_git_repository() {
let dir = tempfile::TempDir::new().expect("create tempdir");
let actual = git_remote_origin_url(Some(dir.path()))
.expect("a non-repository directory should not error");
assert_eq!(None, actual);
}
}