diff --git a/src/features/hyperlinks.rs b/src/features/hyperlinks.rs index b72ff01a9..b5f60af9d 100644 --- a/src/features/hyperlinks.rs +++ b/src/features/hyperlinks.rs @@ -29,7 +29,7 @@ pub fn format_commit_line_with_osc8_commit_hyperlink<'a>( let commit = captures.get(2).unwrap().as_str(); format_osc8_hyperlink(&commit_link_format.replace("{commit}", commit), commit) }) - } else if let Some(GitConfigEntry::GitRemote(GitRemoteRepo::GitHubRepo(repo))) = + } else if let Some(GitConfigEntry::GitRemote(repo)) = config.git_config.as_ref().and_then(get_remote_url) { COMMIT_LINE_REGEX.replace(line, |captures: &Captures| { @@ -93,12 +93,12 @@ lazy_static! { fn format_commit_line_captures_with_osc8_commit_hyperlink( captures: &Captures, - github_repo: &str, + repo: &GitRemoteRepo, ) -> String { let commit = captures.get(2).unwrap().as_str(); format!( "{prefix}{osc}8;;{url}{st}{commit}{osc}8;;{st}{suffix}", - url = format_github_commit_url(commit, github_repo), + url = repo.format_commit_url(commit), commit = commit, prefix = captures.get(1).map(|m| m.as_str()).unwrap_or(""), suffix = captures.get(3).unwrap().as_str(), @@ -107,10 +107,6 @@ fn format_commit_line_captures_with_osc8_commit_hyperlink( ) } -fn format_github_commit_url(commit: &str, github_repo: &str) -> String { - format!("https://github.com/{}/commit/{}", github_repo, commit) -} - #[cfg(not(target_os = "windows"))] #[cfg(test)] pub mod tests { diff --git a/src/git_config/git_config_entry.rs b/src/git_config/git_config_entry.rs index 2f026cf2e..ff5670a5d 100644 --- a/src/git_config/git_config_entry.rs +++ b/src/git_config/git_config_entry.rs @@ -14,7 +14,21 @@ pub enum GitConfigEntry { #[derive(Clone, Debug, PartialEq)] pub enum GitRemoteRepo { - GitHubRepo(String), + GitHubRepo { repo_slug: String }, + GitLabRepo { repo_slug: String }, +} + +impl GitRemoteRepo { + pub fn format_commit_url(&self, commit: &str) -> String { + match self { + Self::GitHubRepo { repo_slug } => { + format!("https://github.com/{}/commit/{}", repo_slug, commit) + } + Self::GitLabRepo { repo_slug } => { + format!("https://gitlab.com/{}/-/commit/{}", repo_slug, commit) + } + } + } } lazy_static! { @@ -32,19 +46,45 @@ lazy_static! { " ) .unwrap(); + static ref GITLAB_REMOTE_URL: Regex = Regex::new( + r"(?x) + ^ + (?:https://|git@)? # Support both HTTPS and SSH URLs, SSH URLs optionally omitting the git@ + gitlab\.com + [:/] # This separator differs between SSH and HTTPS URLs + ([^/]+) # Capture the user/org name + (/.*)? # Capture group(s), if any + / + (.+?) # Capture the repo name (lazy to avoid consuming '.git' if present) + (?:\.git)? # Non-capturing group to consume '.git' if present + $ + " + ) + .unwrap(); } impl FromStr for GitRemoteRepo { type Err = Error; fn from_str(s: &str) -> Result { if let Some(caps) = GITHUB_REMOTE_URL.captures(s) { - Ok(Self::GitHubRepo(format!( - "{user}/{repo}", - user = caps.get(1).unwrap().as_str(), - repo = caps.get(2).unwrap().as_str() - ))) + Ok(Self::GitHubRepo { + repo_slug: format!( + "{user}/{repo}", + user = caps.get(1).unwrap().as_str(), + repo = caps.get(2).unwrap().as_str() + ), + }) + } else if let Some(caps) = GITLAB_REMOTE_URL.captures(s) { + Ok(Self::GitLabRepo { + repo_slug: format!( + "{user}{groups}/{repo}", + user = caps.get(1).unwrap().as_str(), + groups = caps.get(2).map(|x| x.as_str()).unwrap_or_default(), + repo = caps.get(3).unwrap().as_str() + ), + }) } else { - Err("Not a GitHub repo.".into()) + Err("Not a GitHub or GitLab repo.".into()) } } } @@ -68,8 +108,67 @@ mod tests { assert!(parsed.is_ok()); assert_eq!( parsed.unwrap(), - GitRemoteRepo::GitHubRepo("dandavison/delta".to_string()) + GitRemoteRepo::GitHubRepo { + repo_slug: "dandavison/delta".to_string() + } + ); + } + } + + #[test] + fn test_format_github_commit_link() { + let repo = GitRemoteRepo::GitHubRepo { + repo_slug: "dandavison/delta".to_string(), + }; + let commit_hash = "d3b07384d113edec49eaa6238ad5ff00"; + assert_eq!( + repo.format_commit_url(commit_hash), + format!("https://github.com/dandavison/delta/commit/{}", commit_hash) + ) + } + + #[test] + fn test_parse_gitlab_urls() { + let urls = &[ + ( + "https://gitlab.com/proj/grp/subgrp/repo.git", + "proj/grp/subgrp/repo", + ), + ("https://gitlab.com/proj/grp/repo.git", "proj/grp/repo"), + ("https://gitlab.com/proj/repo.git", "proj/repo"), + ("https://gitlab.com/proj/repo", "proj/repo"), + ( + "git@gitlab.com:proj/grp/subgrp/repo.git", + "proj/grp/subgrp/repo", + ), + ("git@gitlab.com:proj/repo.git", "proj/repo"), + ("git@gitlab.com:proj/repo", "proj/repo"), + ("gitlab.com:proj/grp/repo.git", "proj/grp/repo"), + ("gitlab.com:proj/repo.git", "proj/repo"), + ("gitlab.com:proj/repo", "proj/repo"), + ]; + + for (url, expected) in urls { + let parsed = GitRemoteRepo::from_str(url); + assert!(parsed.is_ok()); + assert_eq!( + parsed.unwrap(), + GitRemoteRepo::GitLabRepo { + repo_slug: expected.to_string() + } ); } } + + #[test] + fn test_format_gitlab_commit_link() { + let repo = GitRemoteRepo::GitLabRepo { + repo_slug: "proj/grp/repo".to_string(), + }; + let commit_hash = "d3b07384d113edec49eaa6238ad5ff00"; + assert_eq!( + repo.format_commit_url(commit_hash), + format!("https://gitlab.com/proj/grp/repo/-/commit/{}", commit_hash) + ) + } }