use anyhow::{bail, Context, Result};
use url::Url;
use crate::issue::IssueRef;
pub(super) fn parse_gitlab_remote_url(url: &str) -> Option<(String, String)> {
let url = url.trim().trim_end_matches(".git");
let path = url
.strip_prefix("https://gitlab.com/")
.or_else(|| url.strip_prefix("git@gitlab.com:"))?;
let (owner, repo) = path.split_once('/')?;
if owner.is_empty() || repo.is_empty() {
return None;
}
Some((owner.to_string(), repo.to_string()))
}
pub(super) fn parse_gitlab_url(s: &str) -> Result<IssueRef> {
let url = Url::parse(s).with_context(|| format!("Invalid URL: {s}"))?;
let segments: Vec<&str> = url
.path_segments()
.context("URL has no path")?
.filter(|s| !s.is_empty())
.collect();
if segments.len() < 5 || segments[2] != "-" || segments[3] != "issues" {
bail!(
"Expected GitLab issue URL like \
https://gitlab.com/owner/repo/-/issues/42, got: {s}"
);
}
let owner = segments[0].to_string();
let repo = segments[1].to_string();
let number = segments[4]
.parse::<u64>()
.with_context(|| format!("Invalid issue number in URL: {}", segments[4]))?;
Ok(IssueRef::GitLab {
owner,
repo,
number,
})
}
pub(super) fn parse_gl(s: &str) -> Result<IssueRef> {
let num_str = s
.strip_prefix("gl:")
.expect("caller checked starts_with(\"gl:\")");
let Ok(number) = num_str.parse::<u64>() else {
return Err(anyhow::anyhow!(
"Invalid issue number for gl shorthand: {num_str:?} — expected a positive integer"
));
};
let cwd = std::env::current_dir().context("Could not determine current directory")?;
let remote_url = crate::git::get_remote_url(&cwd, "origin").context(
"Could not get GitLab remote URL — is this a git repository with an 'origin' remote?",
)?;
let (owner, repo) = parse_gitlab_remote_url(&remote_url)
.ok_or_else(|| anyhow::anyhow!("Remote URL {remote_url:?} is not a GitLab URL"))?;
Ok(IssueRef::GitLab {
owner,
repo,
number,
})
}
#[cfg(test)]
#[path = "gitlab_parse_tests.rs"]
mod tests;