diff --git a/Cargo.lock b/Cargo.lock index 456e45e3be..f095d096fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4384,6 +4384,7 @@ name = "git" version = "0.1.0" dependencies = [ "anyhow", + "async-trait", "clock", "collections", "git2", diff --git a/crates/editor/src/blame_entry_tooltip.rs b/crates/editor/src/blame_entry_tooltip.rs index 3732cf4caf..fc728a6d32 100644 --- a/crates/editor/src/blame_entry_tooltip.rs +++ b/crates/editor/src/blame_entry_tooltip.rs @@ -61,7 +61,7 @@ struct CommitAvatarAsset { impl Hash for CommitAvatarAsset { fn hash(&self, state: &mut H) { self.sha.hash(state); - self.remote.host.hash(state); + self.remote.host.name().hash(state); } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 44ef4ca61a..1a151cd4f2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -41,7 +41,7 @@ mod editor_tests; #[cfg(any(test, feature = "test-support"))] pub mod test; use ::git::diff::{DiffHunk, DiffHunkStatus}; -use ::git::permalink::{build_permalink, BuildPermalinkParams}; +use ::git::{parse_git_remote_url, BuildPermalinkParams}; pub(crate) use actions::*; use aho_corasick::AhoCorasick; use anyhow::{anyhow, Context as _, Result}; @@ -9548,17 +9548,22 @@ impl Editor { let selections = self.selections.all::(cx); let selection = selections.iter().peekable().next(); - build_permalink(BuildPermalinkParams { - remote_url: &origin_url, - sha: &sha, - path: &path, - selection: selection.map(|selection| { - let range = selection.range(); - let start = range.start.row; - let end = range.end.row; - start..end - }), - }) + let (provider, remote) = parse_git_remote_url(&origin_url) + .ok_or_else(|| anyhow!("failed to parse Git remote URL"))?; + + Ok(provider.build_permalink( + remote, + BuildPermalinkParams { + sha: &sha, + path: &path, + selection: selection.map(|selection| { + let range = selection.range(); + let start = range.start.row; + let end = range.end.row; + start..end + }), + }, + )) } pub fn copy_permalink_to_line(&mut self, _: &CopyPermalinkToLine, cx: &mut ViewContext) { diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index db786e5d4f..deca89c393 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -4,10 +4,7 @@ use anyhow::Result; use collections::HashMap; use git::{ blame::{Blame, BlameEntry}, - hosting_provider::HostingProvider, - permalink::{build_commit_permalink, parse_git_remote_url}, - pull_request::{extract_pull_request, PullRequest}, - Oid, + parse_git_remote_url, GitHostingProvider, Oid, PullRequest, }; use gpui::{Model, ModelContext, Subscription, Task}; use language::{markdown, Bias, Buffer, BufferSnapshot, Edit, LanguageRegistry, ParsedMarkdown}; @@ -50,13 +47,23 @@ impl<'a> sum_tree::Dimension<'a, GitBlameEntrySummary> for u32 { } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct GitRemote { - pub host: HostingProvider, + pub host: Arc, pub owner: String, pub repo: String, } +impl std::fmt::Debug for GitRemote { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GitRemote") + .field("host", &self.host.name()) + .field("owner", &self.owner) + .field("repo", &self.repo) + .finish() + } +} + impl GitRemote { pub fn host_supports_avatars(&self) -> bool { self.host.supports_avatars() @@ -440,10 +447,10 @@ async fn parse_commit_messages( for (oid, message) in messages { let parsed_message = parse_markdown(&message, &languages).await; - let permalink = if let Some(git_remote) = parsed_remote_url.as_ref() { - Some(build_commit_permalink( - git::permalink::BuildCommitPermalinkParams { - remote: git_remote, + let permalink = if let Some((provider, git_remote)) = parsed_remote_url.as_ref() { + Some(provider.build_commit_permalink( + git_remote, + git::BuildCommitPermalinkParams { sha: oid.to_string().as_str(), }, )) @@ -455,15 +462,17 @@ async fn parse_commit_messages( deprecated_permalinks.get(&oid).cloned() }; - let remote = parsed_remote_url.as_ref().map(|remote| GitRemote { - host: remote.provider.clone(), - owner: remote.owner.to_string(), - repo: remote.repo.to_string(), - }); + let remote = parsed_remote_url + .as_ref() + .map(|(provider, remote)| GitRemote { + host: provider.clone(), + owner: remote.owner.to_string(), + repo: remote.repo.to_string(), + }); let pull_request = parsed_remote_url .as_ref() - .and_then(|remote| extract_pull_request(remote, &message)); + .and_then(|(provider, remote)| provider.extract_pull_request(remote, &message)); commit_details.insert( oid, diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml index 3392fa24e3..72ef4a7254 100644 --- a/crates/git/Cargo.toml +++ b/crates/git/Cargo.toml @@ -13,6 +13,7 @@ path = "src/git.rs" [dependencies] anyhow.workspace = true +async-trait.workspace = true clock.workspace = true collections.workspace = true git2.workspace = true diff --git a/crates/git/src/blame.rs b/crates/git/src/blame.rs index 516918c6ba..57a5963092 100644 --- a/crates/git/src/blame.rs +++ b/crates/git/src/blame.rs @@ -1,6 +1,5 @@ use crate::commit::get_messages; -use crate::permalink::{build_commit_permalink, parse_git_remote_url, BuildCommitPermalinkParams}; -use crate::Oid; +use crate::{parse_git_remote_url, BuildCommitPermalinkParams, Oid}; use anyhow::{anyhow, Context, Result}; use collections::{HashMap, HashSet}; use serde::{Deserialize, Serialize}; @@ -47,12 +46,14 @@ impl Blame { unique_shas.insert(entry.sha); // DEPRECATED (18 Apr 24): Sending permalinks over the wire is deprecated. Clients // now do the parsing. - if let Some(remote) = parsed_remote_url.as_ref() { + if let Some((provider, remote)) = parsed_remote_url.as_ref() { permalinks.entry(entry.sha).or_insert_with(|| { - build_commit_permalink(BuildCommitPermalinkParams { + provider.build_commit_permalink( remote, - sha: entry.sha.to_string().as_str(), - }) + BuildCommitPermalinkParams { + sha: entry.sha.to_string().as_str(), + }, + ) }); } } diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index df82c45940..e7b7611ba1 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -1,3 +1,6 @@ +mod hosting_provider; +mod hosting_providers; + use anyhow::{anyhow, Context, Result}; use serde::{Deserialize, Serialize}; use std::ffi::OsStr; @@ -7,12 +10,12 @@ use std::str::FromStr; pub use git2 as libgit; pub use lazy_static::lazy_static; +pub use crate::hosting_provider::*; +pub use crate::hosting_providers::*; + pub mod blame; pub mod commit; pub mod diff; -pub mod hosting_provider; -pub mod permalink; -pub mod pull_request; pub mod repository; lazy_static! { diff --git a/crates/git/src/hosting_provider.rs b/crates/git/src/hosting_provider.rs index e51e5905c2..e618fc83e0 100644 --- a/crates/git/src/hosting_provider.rs +++ b/crates/git/src/hosting_provider.rs @@ -1,110 +1,116 @@ -use core::fmt; use std::{ops::Range, sync::Arc}; use anyhow::Result; +use async_trait::async_trait; use url::Url; -use util::{codeberg, github, http::HttpClient}; +use util::http::HttpClient; +use crate::hosting_providers::{Bitbucket, Codeberg, Gitee, Github, Gitlab, Sourcehut}; use crate::Oid; -#[derive(Clone, Debug, Hash)] -pub enum HostingProvider { - Github, - Gitlab, - Gitee, - Bitbucket, - Sourcehut, - Codeberg, +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct PullRequest { + pub number: u32, + pub url: Url, } -impl HostingProvider { - pub(crate) fn base_url(&self) -> Url { - let base_url = match self { - Self::Github => "https://github.com", - Self::Gitlab => "https://gitlab.com", - Self::Gitee => "https://gitee.com", - Self::Bitbucket => "https://bitbucket.org", - Self::Sourcehut => "https://git.sr.ht", - Self::Codeberg => "https://codeberg.org", - }; +pub struct BuildCommitPermalinkParams<'a> { + pub sha: &'a str, +} - Url::parse(&base_url).unwrap() - } +pub struct BuildPermalinkParams<'a> { + pub sha: &'a str, + pub path: &'a str, + pub selection: Option>, +} - /// Returns the fragment portion of the URL for the selected lines in - /// the representation the [`GitHostingProvider`] expects. - pub(crate) fn line_fragment(&self, selection: &Range) -> String { +/// A Git hosting provider. +#[async_trait] +pub trait GitHostingProvider { + /// Returns the name of the provider. + fn name(&self) -> String; + + /// Returns the base URL of the provider. + fn base_url(&self) -> Url; + + /// Returns a permalink to a Git commit on this hosting provider. + fn build_commit_permalink( + &self, + remote: &ParsedGitRemote, + params: BuildCommitPermalinkParams, + ) -> Url; + + /// Returns a permalink to a file and/or selection on this hosting provider. + fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url; + + /// Returns whether this provider supports avatars. + fn supports_avatars(&self) -> bool; + + /// Returns a URL fragment to the given line selection. + fn line_fragment(&self, selection: &Range) -> String { if selection.start == selection.end { let line = selection.start + 1; - match self { - Self::Github | Self::Gitlab | Self::Gitee | Self::Sourcehut | Self::Codeberg => { - format!("L{}", line) - } - Self::Bitbucket => format!("lines-{}", line), - } + self.format_line_number(line) } else { let start_line = selection.start + 1; let end_line = selection.end + 1; - match self { - Self::Github | Self::Codeberg => format!("L{}-L{}", start_line, end_line), - Self::Gitlab | Self::Gitee | Self::Sourcehut => { - format!("L{}-{}", start_line, end_line) - } - Self::Bitbucket => format!("lines-{}:{}", start_line, end_line), - } + self.format_line_numbers(start_line, end_line) } } - pub fn supports_avatars(&self) -> bool { - match self { - HostingProvider::Github | HostingProvider::Codeberg => true, - _ => false, - } - } + /// Returns a formatted line number to be placed in a permalink URL. + fn format_line_number(&self, line: u32) -> String; - pub async fn commit_author_avatar_url( + /// Returns a formatted range of line numbers to be placed in a permalink URL. + fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String; + + fn parse_remote_url<'a>(&self, url: &'a str) -> Option>; + + fn extract_pull_request( &self, - repo_owner: &str, - repo: &str, - commit: Oid, - client: Arc, + _remote: &ParsedGitRemote, + _message: &str, + ) -> Option { + None + } + + async fn commit_author_avatar_url( + &self, + _repo_owner: &str, + _repo: &str, + _commit: Oid, + _http_client: Arc, ) -> Result> { - Ok(match self { - HostingProvider::Github => { - let commit = commit.to_string(); - github::fetch_github_commit_author(repo_owner, repo, &commit, &client) - .await? - .map(|author| -> Result { - let mut url = Url::parse(&author.avatar_url)?; - url.set_query(Some("size=128")); - Ok(url) - }) - .transpose() - } - HostingProvider::Codeberg => { - let commit = commit.to_string(); - codeberg::fetch_codeberg_commit_author(repo_owner, repo, &commit, &client) - .await? - .map(|author| Url::parse(&author.avatar_url)) - .transpose() - } - _ => Ok(None), - }?) + Ok(None) } } -impl fmt::Display for HostingProvider { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let name = match self { - HostingProvider::Github => "GitHub", - HostingProvider::Gitlab => "GitLab", - HostingProvider::Gitee => "Gitee", - HostingProvider::Bitbucket => "Bitbucket", - HostingProvider::Sourcehut => "Sourcehut", - HostingProvider::Codeberg => "Codeberg", - }; - write!(f, "{}", name) - } +#[derive(Debug)] +pub struct ParsedGitRemote<'a> { + pub owner: &'a str, + pub repo: &'a str, +} + +pub fn parse_git_remote_url( + url: &str, +) -> Option<( + Arc, + ParsedGitRemote, +)> { + let providers: Vec> = vec![ + Arc::new(Github), + Arc::new(Gitlab), + Arc::new(Bitbucket), + Arc::new(Codeberg), + Arc::new(Gitee), + Arc::new(Sourcehut), + ]; + + providers.into_iter().find_map(|provider| { + provider + .parse_remote_url(&url) + .map(|parsed_remote| (provider, parsed_remote)) + }) } diff --git a/crates/git/src/hosting_providers.rs b/crates/git/src/hosting_providers.rs new file mode 100644 index 0000000000..68541541cf --- /dev/null +++ b/crates/git/src/hosting_providers.rs @@ -0,0 +1,13 @@ +mod bitbucket; +mod codeberg; +mod gitee; +mod github; +mod gitlab; +mod sourcehut; + +pub use bitbucket::*; +pub use codeberg::*; +pub use gitee::*; +pub use github::*; +pub use gitlab::*; +pub use sourcehut::*; diff --git a/crates/git/src/hosting_providers/bitbucket.rs b/crates/git/src/hosting_providers/bitbucket.rs new file mode 100644 index 0000000000..e09dc52c25 --- /dev/null +++ b/crates/git/src/hosting_providers/bitbucket.rs @@ -0,0 +1,169 @@ +use url::Url; + +use crate::{ + BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote, +}; + +pub struct Bitbucket; + +impl GitHostingProvider for Bitbucket { + fn name(&self) -> String { + "Bitbucket".to_string() + } + + fn base_url(&self) -> Url { + Url::parse("https://bitbucket.org").unwrap() + } + + fn supports_avatars(&self) -> bool { + false + } + + fn format_line_number(&self, line: u32) -> String { + format!("lines-{line}") + } + + fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String { + format!("lines-{start_line}:{end_line}") + } + + fn parse_remote_url<'a>(&self, url: &'a str) -> Option> { + if url.contains("bitbucket.org") { + let (_, repo_with_owner) = url.trim_end_matches(".git").split_once("bitbucket.org")?; + let (owner, repo) = repo_with_owner + .trim_start_matches('/') + .trim_start_matches(':') + .split_once('/')?; + + return Some(ParsedGitRemote { owner, repo }); + } + + None + } + + fn build_commit_permalink( + &self, + remote: &ParsedGitRemote, + params: BuildCommitPermalinkParams, + ) -> Url { + let BuildCommitPermalinkParams { sha } = params; + let ParsedGitRemote { owner, repo } = remote; + + self.base_url() + .join(&format!("{owner}/{repo}/commits/{sha}")) + .unwrap() + } + + fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url { + let ParsedGitRemote { owner, repo } = remote; + let BuildPermalinkParams { + sha, + path, + selection, + } = params; + + let mut permalink = self + .base_url() + .join(&format!("{owner}/{repo}/src/{sha}/{path}")) + .unwrap(); + permalink.set_fragment( + selection + .map(|selection| self.line_fragment(&selection)) + .as_deref(), + ); + permalink + } +} + +#[cfg(test)] +mod tests { + use crate::parse_git_remote_url; + + use super::*; + + #[test] + fn test_parse_git_remote_url_bitbucket_https_with_username() { + let url = "https://thorstenballzed@bitbucket.org/thorstenzed/testingrepo.git"; + let (provider, parsed) = parse_git_remote_url(url).unwrap(); + assert_eq!(provider.name(), "Bitbucket"); + assert_eq!(parsed.owner, "thorstenzed"); + assert_eq!(parsed.repo, "testingrepo"); + } + + #[test] + fn test_parse_git_remote_url_bitbucket_https_without_username() { + let url = "https://bitbucket.org/thorstenzed/testingrepo.git"; + let (provider, parsed) = parse_git_remote_url(url).unwrap(); + assert_eq!(provider.name(), "Bitbucket"); + assert_eq!(parsed.owner, "thorstenzed"); + assert_eq!(parsed.repo, "testingrepo"); + } + + #[test] + fn test_parse_git_remote_url_bitbucket_git() { + let url = "git@bitbucket.org:thorstenzed/testingrepo.git"; + let (provider, parsed) = parse_git_remote_url(url).unwrap(); + assert_eq!(provider.name(), "Bitbucket"); + assert_eq!(parsed.owner, "thorstenzed"); + assert_eq!(parsed.repo, "testingrepo"); + } + + #[test] + fn test_build_bitbucket_permalink_from_ssh_url() { + let remote = ParsedGitRemote { + owner: "thorstenzed", + repo: "testingrepo", + }; + let permalink = Bitbucket.build_permalink( + remote, + BuildPermalinkParams { + sha: "f00b4r", + path: "main.rs", + selection: None, + }, + ); + + let expected_url = "https://bitbucket.org/thorstenzed/testingrepo/src/f00b4r/main.rs"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_bitbucket_permalink_from_ssh_url_single_line_selection() { + let remote = ParsedGitRemote { + owner: "thorstenzed", + repo: "testingrepo", + }; + let permalink = Bitbucket.build_permalink( + remote, + BuildPermalinkParams { + sha: "f00b4r", + path: "main.rs", + selection: Some(6..6), + }, + ); + + let expected_url = + "https://bitbucket.org/thorstenzed/testingrepo/src/f00b4r/main.rs#lines-7"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_bitbucket_permalink_from_ssh_url_multi_line_selection() { + let remote = ParsedGitRemote { + owner: "thorstenzed", + repo: "testingrepo", + }; + let permalink = Bitbucket.build_permalink( + remote, + BuildPermalinkParams { + sha: "f00b4r", + path: "main.rs", + selection: Some(23..47), + }, + ); + + let expected_url = + "https://bitbucket.org/thorstenzed/testingrepo/src/f00b4r/main.rs#lines-24:48"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } +} diff --git a/crates/git/src/hosting_providers/codeberg.rs b/crates/git/src/hosting_providers/codeberg.rs new file mode 100644 index 0000000000..01c133e3f8 --- /dev/null +++ b/crates/git/src/hosting_providers/codeberg.rs @@ -0,0 +1,219 @@ +use std::sync::Arc; + +use anyhow::Result; +use async_trait::async_trait; +use url::Url; +use util::codeberg; +use util::http::HttpClient; + +use crate::{ + BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, Oid, ParsedGitRemote, +}; + +pub struct Codeberg; + +#[async_trait] +impl GitHostingProvider for Codeberg { + fn name(&self) -> String { + "Codeberg".to_string() + } + + fn base_url(&self) -> Url { + Url::parse("https://codeberg.org").unwrap() + } + + fn supports_avatars(&self) -> bool { + true + } + + fn format_line_number(&self, line: u32) -> String { + format!("L{line}") + } + + fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String { + format!("L{start_line}-L{end_line}") + } + + fn parse_remote_url<'a>(&self, url: &'a str) -> Option> { + if url.starts_with("git@codeberg.org:") || url.starts_with("https://codeberg.org/") { + let repo_with_owner = url + .trim_start_matches("git@codeberg.org:") + .trim_start_matches("https://codeberg.org/") + .trim_end_matches(".git"); + + let (owner, repo) = repo_with_owner.split_once('/')?; + + return Some(ParsedGitRemote { owner, repo }); + } + + None + } + + fn build_commit_permalink( + &self, + remote: &ParsedGitRemote, + params: BuildCommitPermalinkParams, + ) -> Url { + let BuildCommitPermalinkParams { sha } = params; + let ParsedGitRemote { owner, repo } = remote; + + self.base_url() + .join(&format!("{owner}/{repo}/commit/{sha}")) + .unwrap() + } + + fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url { + let ParsedGitRemote { owner, repo } = remote; + let BuildPermalinkParams { + sha, + path, + selection, + } = params; + + let mut permalink = self + .base_url() + .join(&format!("{owner}/{repo}/src/commit/{sha}/{path}")) + .unwrap(); + permalink.set_fragment( + selection + .map(|selection| self.line_fragment(&selection)) + .as_deref(), + ); + permalink + } + + async fn commit_author_avatar_url( + &self, + repo_owner: &str, + repo: &str, + commit: Oid, + http_client: Arc, + ) -> Result> { + let commit = commit.to_string(); + let avatar_url = + codeberg::fetch_codeberg_commit_author(repo_owner, repo, &commit, &http_client) + .await? + .map(|author| Url::parse(&author.avatar_url)) + .transpose()?; + Ok(avatar_url) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build_codeberg_permalink_from_ssh_url() { + let remote = ParsedGitRemote { + owner: "rajveermalviya", + repo: "zed", + }; + let permalink = Codeberg.build_permalink( + remote, + BuildPermalinkParams { + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/editor/src/git/permalink.rs", + selection: None, + }, + ); + + let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_codeberg_permalink_from_ssh_url_single_line_selection() { + let remote = ParsedGitRemote { + owner: "rajveermalviya", + repo: "zed", + }; + let permalink = Codeberg.build_permalink( + remote, + BuildPermalinkParams { + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/editor/src/git/permalink.rs", + selection: Some(6..6), + }, + ); + + let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs#L7"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_codeberg_permalink_from_ssh_url_multi_line_selection() { + let remote = ParsedGitRemote { + owner: "rajveermalviya", + repo: "zed", + }; + let permalink = Codeberg.build_permalink( + remote, + BuildPermalinkParams { + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/editor/src/git/permalink.rs", + selection: Some(23..47), + }, + ); + + let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs#L24-L48"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_codeberg_permalink_from_https_url() { + let remote = ParsedGitRemote { + owner: "rajveermalviya", + repo: "zed", + }; + let permalink = Codeberg.build_permalink( + remote, + BuildPermalinkParams { + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/zed/src/main.rs", + selection: None, + }, + ); + + let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/zed/src/main.rs"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_codeberg_permalink_from_https_url_single_line_selection() { + let remote = ParsedGitRemote { + owner: "rajveermalviya", + repo: "zed", + }; + let permalink = Codeberg.build_permalink( + remote, + BuildPermalinkParams { + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/zed/src/main.rs", + selection: Some(6..6), + }, + ); + + let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/zed/src/main.rs#L7"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_codeberg_permalink_from_https_url_multi_line_selection() { + let remote = ParsedGitRemote { + owner: "rajveermalviya", + repo: "zed", + }; + let permalink = Codeberg.build_permalink( + remote, + BuildPermalinkParams { + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/zed/src/main.rs", + selection: Some(23..47), + }, + ); + + let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/zed/src/main.rs#L24-L48"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } +} diff --git a/crates/git/src/hosting_providers/gitee.rs b/crates/git/src/hosting_providers/gitee.rs new file mode 100644 index 0000000000..76fb92e566 --- /dev/null +++ b/crates/git/src/hosting_providers/gitee.rs @@ -0,0 +1,196 @@ +use url::Url; + +use crate::{ + BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote, +}; + +pub struct Gitee; + +impl GitHostingProvider for Gitee { + fn name(&self) -> String { + "Gitee".to_string() + } + + fn base_url(&self) -> Url { + Url::parse("https://gitee.com").unwrap() + } + + fn supports_avatars(&self) -> bool { + false + } + + fn format_line_number(&self, line: u32) -> String { + format!("L{line}") + } + + fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String { + format!("L{start_line}-{end_line}") + } + + fn parse_remote_url<'a>(&self, url: &'a str) -> Option> { + if url.starts_with("git@gitee.com:") || url.starts_with("https://gitee.com/") { + let repo_with_owner = url + .trim_start_matches("git@gitee.com:") + .trim_start_matches("https://gitee.com/") + .trim_end_matches(".git"); + + let (owner, repo) = repo_with_owner.split_once('/')?; + + return Some(ParsedGitRemote { owner, repo }); + } + + None + } + + fn build_commit_permalink( + &self, + remote: &ParsedGitRemote, + params: BuildCommitPermalinkParams, + ) -> Url { + let BuildCommitPermalinkParams { sha } = params; + let ParsedGitRemote { owner, repo } = remote; + + self.base_url() + .join(&format!("{owner}/{repo}/commit/{sha}")) + .unwrap() + } + + fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url { + let ParsedGitRemote { owner, repo } = remote; + let BuildPermalinkParams { + sha, + path, + selection, + } = params; + + let mut permalink = self + .base_url() + .join(&format!("{owner}/{repo}/blob/{sha}/{path}")) + .unwrap(); + permalink.set_fragment( + selection + .map(|selection| self.line_fragment(&selection)) + .as_deref(), + ); + permalink + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build_gitee_permalink_from_ssh_url() { + let remote = ParsedGitRemote { + owner: "libkitten", + repo: "zed", + }; + let permalink = Gitee.build_permalink( + remote, + BuildPermalinkParams { + sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194", + path: "crates/editor/src/git/permalink.rs", + selection: None, + }, + ); + + let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_gitee_permalink_from_ssh_url_single_line_selection() { + let remote = ParsedGitRemote { + owner: "libkitten", + repo: "zed", + }; + let permalink = Gitee.build_permalink( + remote, + BuildPermalinkParams { + sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194", + path: "crates/editor/src/git/permalink.rs", + selection: Some(6..6), + }, + ); + + let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L7"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_gitee_permalink_from_ssh_url_multi_line_selection() { + let remote = ParsedGitRemote { + owner: "libkitten", + repo: "zed", + }; + let permalink = Gitee.build_permalink( + remote, + BuildPermalinkParams { + sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194", + path: "crates/editor/src/git/permalink.rs", + selection: Some(23..47), + }, + ); + + let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L24-48"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_gitee_permalink_from_https_url() { + let remote = ParsedGitRemote { + owner: "libkitten", + repo: "zed", + }; + let permalink = Gitee.build_permalink( + remote, + BuildPermalinkParams { + sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194", + path: "crates/zed/src/main.rs", + selection: None, + }, + ); + + let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_gitee_permalink_from_https_url_single_line_selection() { + let remote = ParsedGitRemote { + owner: "libkitten", + repo: "zed", + }; + let permalink = Gitee.build_permalink( + remote, + BuildPermalinkParams { + sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194", + path: "crates/zed/src/main.rs", + selection: Some(6..6), + }, + ); + + let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs#L7"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_gitee_permalink_from_https_url_multi_line_selection() { + let remote = ParsedGitRemote { + owner: "libkitten", + repo: "zed", + }; + let permalink = Gitee.build_permalink( + remote, + BuildPermalinkParams { + sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194", + path: "crates/zed/src/main.rs", + selection: Some(23..47), + }, + ); + + let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs#L24-48"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } +} diff --git a/crates/git/src/hosting_providers/github.rs b/crates/git/src/hosting_providers/github.rs new file mode 100644 index 0000000000..c3219278d9 --- /dev/null +++ b/crates/git/src/hosting_providers/github.rs @@ -0,0 +1,287 @@ +use std::sync::{Arc, OnceLock}; + +use anyhow::Result; +use async_trait::async_trait; +use regex::Regex; +use url::Url; +use util::github; +use util::http::HttpClient; + +use crate::{ + BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, Oid, ParsedGitRemote, + PullRequest, +}; + +fn pull_request_number_regex() -> &'static Regex { + static PULL_REQUEST_NUMBER_REGEX: OnceLock = OnceLock::new(); + + PULL_REQUEST_NUMBER_REGEX.get_or_init(|| Regex::new(r"\(#(\d+)\)$").unwrap()) +} + +pub struct Github; + +#[async_trait] +impl GitHostingProvider for Github { + fn name(&self) -> String { + "GitHub".to_string() + } + + fn base_url(&self) -> Url { + Url::parse("https://github.com").unwrap() + } + + fn supports_avatars(&self) -> bool { + true + } + + fn format_line_number(&self, line: u32) -> String { + format!("L{line}") + } + + fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String { + format!("L{start_line}-L{end_line}") + } + + fn parse_remote_url<'a>(&self, url: &'a str) -> Option> { + if url.starts_with("git@github.com:") || url.starts_with("https://github.com/") { + let repo_with_owner = url + .trim_start_matches("git@github.com:") + .trim_start_matches("https://github.com/") + .trim_end_matches(".git"); + + let (owner, repo) = repo_with_owner.split_once('/')?; + + return Some(ParsedGitRemote { owner, repo }); + } + + None + } + + fn build_commit_permalink( + &self, + remote: &ParsedGitRemote, + params: BuildCommitPermalinkParams, + ) -> Url { + let BuildCommitPermalinkParams { sha } = params; + let ParsedGitRemote { owner, repo } = remote; + + self.base_url() + .join(&format!("{owner}/{repo}/commit/{sha}")) + .unwrap() + } + + fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url { + let ParsedGitRemote { owner, repo } = remote; + let BuildPermalinkParams { + sha, + path, + selection, + } = params; + + let mut permalink = self + .base_url() + .join(&format!("{owner}/{repo}/blob/{sha}/{path}")) + .unwrap(); + permalink.set_fragment( + selection + .map(|selection| self.line_fragment(&selection)) + .as_deref(), + ); + permalink + } + + fn extract_pull_request(&self, remote: &ParsedGitRemote, message: &str) -> Option { + let line = message.lines().next()?; + let capture = pull_request_number_regex().captures(line)?; + let number = capture.get(1)?.as_str().parse::().ok()?; + + let mut url = self.base_url(); + let path = format!("/{}/{}/pull/{}", remote.owner, remote.repo, number); + url.set_path(&path); + + Some(PullRequest { number, url }) + } + + async fn commit_author_avatar_url( + &self, + repo_owner: &str, + repo: &str, + commit: Oid, + http_client: Arc, + ) -> Result> { + let commit = commit.to_string(); + let avatar_url = + github::fetch_github_commit_author(repo_owner, repo, &commit, &http_client) + .await? + .map(|author| -> Result { + let mut url = Url::parse(&author.avatar_url)?; + url.set_query(Some("size=128")); + Ok(url) + }) + .transpose()?; + Ok(avatar_url) + } +} + +#[cfg(test)] +mod tests { + // TODO: Replace with `indoc`. + use unindent::Unindent; + + use super::*; + + #[test] + fn test_build_github_permalink_from_ssh_url() { + let remote = ParsedGitRemote { + owner: "zed-industries", + repo: "zed", + }; + let permalink = Github.build_permalink( + remote, + BuildPermalinkParams { + sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7", + path: "crates/editor/src/git/permalink.rs", + selection: None, + }, + ); + + let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_github_permalink_from_ssh_url_single_line_selection() { + let remote = ParsedGitRemote { + owner: "zed-industries", + repo: "zed", + }; + let permalink = Github.build_permalink( + remote, + BuildPermalinkParams { + sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7", + path: "crates/editor/src/git/permalink.rs", + selection: Some(6..6), + }, + ); + + let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_github_permalink_from_ssh_url_multi_line_selection() { + let remote = ParsedGitRemote { + owner: "zed-industries", + repo: "zed", + }; + let permalink = Github.build_permalink( + remote, + BuildPermalinkParams { + sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7", + path: "crates/editor/src/git/permalink.rs", + selection: Some(23..47), + }, + ); + + let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-L48"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_github_permalink_from_https_url() { + let remote = ParsedGitRemote { + owner: "zed-industries", + repo: "zed", + }; + let permalink = Github.build_permalink( + remote, + BuildPermalinkParams { + sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa", + path: "crates/zed/src/main.rs", + selection: None, + }, + ); + + let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_github_permalink_from_https_url_single_line_selection() { + let remote = ParsedGitRemote { + owner: "zed-industries", + repo: "zed", + }; + let permalink = Github.build_permalink( + remote, + BuildPermalinkParams { + sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa", + path: "crates/zed/src/main.rs", + selection: Some(6..6), + }, + ); + + let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_github_permalink_from_https_url_multi_line_selection() { + let remote = ParsedGitRemote { + owner: "zed-industries", + repo: "zed", + }; + let permalink = Github.build_permalink( + remote, + BuildPermalinkParams { + sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa", + path: "crates/zed/src/main.rs", + selection: Some(23..47), + }, + ); + + let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-L48"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_github_pull_requests() { + let remote = ParsedGitRemote { + owner: "zed-industries", + repo: "zed", + }; + + let message = "This does not contain a pull request"; + assert!(Github.extract_pull_request(&remote, message).is_none()); + + // Pull request number at end of first line + let message = r#" + project panel: do not expand collapsed worktrees on "collapse all entries" (#10687) + + Fixes #10597 + + Release Notes: + + - Fixed "project panel: collapse all entries" expanding collapsed worktrees. + "# + .unindent(); + + assert_eq!( + Github + .extract_pull_request(&remote, &message) + .unwrap() + .url + .as_str(), + "https://github.com/zed-industries/zed/pull/10687" + ); + + // Pull request number in middle of line, which we want to ignore + let message = r#" + Follow-up to #10687 to fix problems + + See the original PR, this is a fix. + "# + .unindent(); + assert_eq!(Github.extract_pull_request(&remote, &message), None); + } +} diff --git a/crates/git/src/hosting_providers/gitlab.rs b/crates/git/src/hosting_providers/gitlab.rs new file mode 100644 index 0000000000..8ca99f3a61 --- /dev/null +++ b/crates/git/src/hosting_providers/gitlab.rs @@ -0,0 +1,196 @@ +use url::Url; + +use crate::{ + BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote, +}; + +pub struct Gitlab; + +impl GitHostingProvider for Gitlab { + fn name(&self) -> String { + "GitLab".to_string() + } + + fn base_url(&self) -> Url { + Url::parse("https://gitlab.com").unwrap() + } + + fn supports_avatars(&self) -> bool { + false + } + + fn format_line_number(&self, line: u32) -> String { + format!("L{line}") + } + + fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String { + format!("L{start_line}-{end_line}") + } + + fn parse_remote_url<'a>(&self, url: &'a str) -> Option> { + if url.starts_with("git@gitlab.com:") || url.starts_with("https://gitlab.com/") { + let repo_with_owner = url + .trim_start_matches("git@gitlab.com:") + .trim_start_matches("https://gitlab.com/") + .trim_end_matches(".git"); + + let (owner, repo) = repo_with_owner.split_once('/')?; + + return Some(ParsedGitRemote { owner, repo }); + } + + None + } + + fn build_commit_permalink( + &self, + remote: &ParsedGitRemote, + params: BuildCommitPermalinkParams, + ) -> Url { + let BuildCommitPermalinkParams { sha } = params; + let ParsedGitRemote { owner, repo } = remote; + + self.base_url() + .join(&format!("{owner}/{repo}/-/commit/{sha}")) + .unwrap() + } + + fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url { + let ParsedGitRemote { owner, repo } = remote; + let BuildPermalinkParams { + sha, + path, + selection, + } = params; + + let mut permalink = self + .base_url() + .join(&format!("{owner}/{repo}/-/blob/{sha}/{path}")) + .unwrap(); + permalink.set_fragment( + selection + .map(|selection| self.line_fragment(&selection)) + .as_deref(), + ); + permalink + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build_gitlab_permalink_from_ssh_url() { + let remote = ParsedGitRemote { + owner: "zed-industries", + repo: "zed", + }; + let permalink = Gitlab.build_permalink( + remote, + BuildPermalinkParams { + sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7", + path: "crates/editor/src/git/permalink.rs", + selection: None, + }, + ); + + let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_gitlab_permalink_from_ssh_url_single_line_selection() { + let remote = ParsedGitRemote { + owner: "zed-industries", + repo: "zed", + }; + let permalink = Gitlab.build_permalink( + remote, + BuildPermalinkParams { + sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7", + path: "crates/editor/src/git/permalink.rs", + selection: Some(6..6), + }, + ); + + let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_gitlab_permalink_from_ssh_url_multi_line_selection() { + let remote = ParsedGitRemote { + owner: "zed-industries", + repo: "zed", + }; + let permalink = Gitlab.build_permalink( + remote, + BuildPermalinkParams { + sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7", + path: "crates/editor/src/git/permalink.rs", + selection: Some(23..47), + }, + ); + + let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-48"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_gitlab_permalink_from_https_url() { + let remote = ParsedGitRemote { + owner: "zed-industries", + repo: "zed", + }; + let permalink = Gitlab.build_permalink( + remote, + BuildPermalinkParams { + sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa", + path: "crates/zed/src/main.rs", + selection: None, + }, + ); + + let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_gitlab_permalink_from_https_url_single_line_selection() { + let remote = ParsedGitRemote { + owner: "zed-industries", + repo: "zed", + }; + let permalink = Gitlab.build_permalink( + remote, + BuildPermalinkParams { + sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa", + path: "crates/zed/src/main.rs", + selection: Some(6..6), + }, + ); + + let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_gitlab_permalink_from_https_url_multi_line_selection() { + let remote = ParsedGitRemote { + owner: "zed-industries", + repo: "zed", + }; + let permalink = Gitlab.build_permalink( + remote, + BuildPermalinkParams { + sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa", + path: "crates/zed/src/main.rs", + selection: Some(23..47), + }, + ); + + let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-48"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } +} diff --git a/crates/git/src/hosting_providers/sourcehut.rs b/crates/git/src/hosting_providers/sourcehut.rs new file mode 100644 index 0000000000..93bd8fbf7a --- /dev/null +++ b/crates/git/src/hosting_providers/sourcehut.rs @@ -0,0 +1,217 @@ +use url::Url; + +use crate::{ + BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote, +}; + +pub struct Sourcehut; + +impl GitHostingProvider for Sourcehut { + fn name(&self) -> String { + "Gitee".to_string() + } + + fn base_url(&self) -> Url { + Url::parse("https://git.sr.ht").unwrap() + } + + fn supports_avatars(&self) -> bool { + false + } + + fn format_line_number(&self, line: u32) -> String { + format!("L{line}") + } + + fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String { + format!("L{start_line}-{end_line}") + } + + fn parse_remote_url<'a>(&self, url: &'a str) -> Option> { + if url.starts_with("git@git.sr.ht:") || url.starts_with("https://git.sr.ht/") { + // sourcehut indicates a repo with '.git' suffix as a separate repo. + // For example, "git@git.sr.ht:~username/repo" and "git@git.sr.ht:~username/repo.git" + // are two distinct repositories. + let repo_with_owner = url + .trim_start_matches("git@git.sr.ht:~") + .trim_start_matches("https://git.sr.ht/~"); + + let (owner, repo) = repo_with_owner.split_once('/')?; + + return Some(ParsedGitRemote { owner, repo }); + } + + None + } + + fn build_commit_permalink( + &self, + remote: &ParsedGitRemote, + params: BuildCommitPermalinkParams, + ) -> Url { + let BuildCommitPermalinkParams { sha } = params; + let ParsedGitRemote { owner, repo } = remote; + + self.base_url() + .join(&format!("~{owner}/{repo}/commit/{sha}")) + .unwrap() + } + + fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url { + let ParsedGitRemote { owner, repo } = remote; + let BuildPermalinkParams { + sha, + path, + selection, + } = params; + + let mut permalink = self + .base_url() + .join(&format!("~{owner}/{repo}/tree/{sha}/item/{path}")) + .unwrap(); + permalink.set_fragment( + selection + .map(|selection| self.line_fragment(&selection)) + .as_deref(), + ); + permalink + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build_sourcehut_permalink_from_ssh_url() { + let remote = ParsedGitRemote { + owner: "rajveermalviya", + repo: "zed", + }; + let permalink = Sourcehut.build_permalink( + remote, + BuildPermalinkParams { + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/editor/src/git/permalink.rs", + selection: None, + }, + ); + + let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_sourcehut_permalink_from_ssh_url_with_git_prefix() { + let remote = ParsedGitRemote { + owner: "rajveermalviya", + repo: "zed.git", + }; + let permalink = Sourcehut.build_permalink( + remote, + BuildPermalinkParams { + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/editor/src/git/permalink.rs", + selection: None, + }, + ); + + let expected_url = "https://git.sr.ht/~rajveermalviya/zed.git/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_sourcehut_permalink_from_ssh_url_single_line_selection() { + let remote = ParsedGitRemote { + owner: "rajveermalviya", + repo: "zed", + }; + let permalink = Sourcehut.build_permalink( + remote, + BuildPermalinkParams { + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/editor/src/git/permalink.rs", + selection: Some(6..6), + }, + ); + + let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L7"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_sourcehut_permalink_from_ssh_url_multi_line_selection() { + let remote = ParsedGitRemote { + owner: "rajveermalviya", + repo: "zed", + }; + let permalink = Sourcehut.build_permalink( + remote, + BuildPermalinkParams { + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/editor/src/git/permalink.rs", + selection: Some(23..47), + }, + ); + + let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L24-48"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_sourcehut_permalink_from_https_url() { + let remote = ParsedGitRemote { + owner: "rajveermalviya", + repo: "zed", + }; + let permalink = Sourcehut.build_permalink( + remote, + BuildPermalinkParams { + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/zed/src/main.rs", + selection: None, + }, + ); + + let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/zed/src/main.rs"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_sourcehut_permalink_from_https_url_single_line_selection() { + let remote = ParsedGitRemote { + owner: "rajveermalviya", + repo: "zed", + }; + let permalink = Sourcehut.build_permalink( + remote, + BuildPermalinkParams { + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/zed/src/main.rs", + selection: Some(6..6), + }, + ); + + let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/zed/src/main.rs#L7"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_sourcehut_permalink_from_https_url_multi_line_selection() { + let remote = ParsedGitRemote { + owner: "rajveermalviya", + repo: "zed", + }; + let permalink = Sourcehut.build_permalink( + remote, + BuildPermalinkParams { + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/zed/src/main.rs", + selection: Some(23..47), + }, + ); + + let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/zed/src/main.rs#L24-48"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } +} diff --git a/crates/git/src/permalink.rs b/crates/git/src/permalink.rs deleted file mode 100644 index 3b256a03c3..0000000000 --- a/crates/git/src/permalink.rs +++ /dev/null @@ -1,680 +0,0 @@ -use std::ops::Range; - -use anyhow::{anyhow, Result}; -use url::Url; - -use crate::hosting_provider::HostingProvider; - -pub struct BuildPermalinkParams<'a> { - pub remote_url: &'a str, - pub sha: &'a str, - pub path: &'a str, - pub selection: Option>, -} - -pub fn build_permalink(params: BuildPermalinkParams) -> Result { - let BuildPermalinkParams { - remote_url, - sha, - path, - selection, - } = params; - - let ParsedGitRemote { - provider, - owner, - repo, - } = parse_git_remote_url(remote_url) - .ok_or_else(|| anyhow!("failed to parse Git remote URL"))?; - - let path = match provider { - HostingProvider::Github => format!("{owner}/{repo}/blob/{sha}/{path}"), - HostingProvider::Gitlab => format!("{owner}/{repo}/-/blob/{sha}/{path}"), - HostingProvider::Gitee => format!("{owner}/{repo}/blob/{sha}/{path}"), - HostingProvider::Bitbucket => format!("{owner}/{repo}/src/{sha}/{path}"), - HostingProvider::Sourcehut => format!("~{owner}/{repo}/tree/{sha}/item/{path}"), - HostingProvider::Codeberg => format!("{owner}/{repo}/src/commit/{sha}/{path}"), - }; - let line_fragment = selection.map(|selection| provider.line_fragment(&selection)); - - let mut permalink = provider.base_url().join(&path).unwrap(); - permalink.set_fragment(line_fragment.as_deref()); - Ok(permalink) -} - -#[derive(Debug)] -pub struct ParsedGitRemote<'a> { - pub provider: HostingProvider, - pub owner: &'a str, - pub repo: &'a str, -} - -pub struct BuildCommitPermalinkParams<'a> { - pub remote: &'a ParsedGitRemote<'a>, - pub sha: &'a str, -} - -pub fn build_commit_permalink(params: BuildCommitPermalinkParams) -> Url { - let BuildCommitPermalinkParams { sha, remote } = params; - - let ParsedGitRemote { - provider, - owner, - repo, - } = remote; - - let path = match provider { - HostingProvider::Github => format!("{owner}/{repo}/commit/{sha}"), - HostingProvider::Gitlab => format!("{owner}/{repo}/-/commit/{sha}"), - HostingProvider::Gitee => format!("{owner}/{repo}/commit/{sha}"), - HostingProvider::Bitbucket => format!("{owner}/{repo}/commits/{sha}"), - HostingProvider::Sourcehut => format!("~{owner}/{repo}/commit/{sha}"), - HostingProvider::Codeberg => format!("{owner}/{repo}/commit/{sha}"), - }; - - provider.base_url().join(&path).unwrap() -} - -pub fn parse_git_remote_url(url: &str) -> Option { - if url.starts_with("git@github.com:") || url.starts_with("https://github.com/") { - let repo_with_owner = url - .trim_start_matches("git@github.com:") - .trim_start_matches("https://github.com/") - .trim_end_matches(".git"); - - let (owner, repo) = repo_with_owner.split_once('/')?; - - return Some(ParsedGitRemote { - provider: HostingProvider::Github, - owner, - repo, - }); - } - - if url.starts_with("git@gitlab.com:") || url.starts_with("https://gitlab.com/") { - let repo_with_owner = url - .trim_start_matches("git@gitlab.com:") - .trim_start_matches("https://gitlab.com/") - .trim_end_matches(".git"); - - let (owner, repo) = repo_with_owner.split_once('/')?; - - return Some(ParsedGitRemote { - provider: HostingProvider::Gitlab, - owner, - repo, - }); - } - - if url.starts_with("git@gitee.com:") || url.starts_with("https://gitee.com/") { - let repo_with_owner = url - .trim_start_matches("git@gitee.com:") - .trim_start_matches("https://gitee.com/") - .trim_end_matches(".git"); - - let (owner, repo) = repo_with_owner.split_once('/')?; - - return Some(ParsedGitRemote { - provider: HostingProvider::Gitee, - owner, - repo, - }); - } - - if url.contains("bitbucket.org") { - let (_, repo_with_owner) = url.trim_end_matches(".git").split_once("bitbucket.org")?; - let (owner, repo) = repo_with_owner - .trim_start_matches('/') - .trim_start_matches(':') - .split_once('/')?; - - return Some(ParsedGitRemote { - provider: HostingProvider::Bitbucket, - owner, - repo, - }); - } - - if url.starts_with("git@git.sr.ht:") || url.starts_with("https://git.sr.ht/") { - // sourcehut indicates a repo with '.git' suffix as a separate repo. - // For example, "git@git.sr.ht:~username/repo" and "git@git.sr.ht:~username/repo.git" - // are two distinct repositories. - let repo_with_owner = url - .trim_start_matches("git@git.sr.ht:~") - .trim_start_matches("https://git.sr.ht/~"); - - let (owner, repo) = repo_with_owner.split_once('/')?; - - return Some(ParsedGitRemote { - provider: HostingProvider::Sourcehut, - owner, - repo, - }); - } - - if url.starts_with("git@codeberg.org:") || url.starts_with("https://codeberg.org/") { - let repo_with_owner = url - .trim_start_matches("git@codeberg.org:") - .trim_start_matches("https://codeberg.org/") - .trim_end_matches(".git"); - - let (owner, repo) = repo_with_owner.split_once('/')?; - - return Some(ParsedGitRemote { - provider: HostingProvider::Codeberg, - owner, - repo, - }); - } - - None -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_build_github_permalink_from_ssh_url() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "git@github.com:zed-industries/zed.git", - sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7", - path: "crates/editor/src/git/permalink.rs", - selection: None, - }) - .unwrap(); - - let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_github_permalink_from_ssh_url_single_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "git@github.com:zed-industries/zed.git", - sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7", - path: "crates/editor/src/git/permalink.rs", - selection: Some(6..6), - }) - .unwrap(); - - let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_github_permalink_from_ssh_url_multi_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "git@github.com:zed-industries/zed.git", - sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7", - path: "crates/editor/src/git/permalink.rs", - selection: Some(23..47), - }) - .unwrap(); - - let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-L48"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_github_permalink_from_https_url() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "https://github.com/zed-industries/zed.git", - sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa", - path: "crates/zed/src/main.rs", - selection: None, - }) - .unwrap(); - - let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_github_permalink_from_https_url_single_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "https://github.com/zed-industries/zed.git", - sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa", - path: "crates/zed/src/main.rs", - selection: Some(6..6), - }) - .unwrap(); - - let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_github_permalink_from_https_url_multi_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "https://github.com/zed-industries/zed.git", - sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa", - path: "crates/zed/src/main.rs", - selection: Some(23..47), - }) - .unwrap(); - - let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-L48"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_gitlab_permalink_from_ssh_url() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "git@gitlab.com:zed-industries/zed.git", - sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7", - path: "crates/editor/src/git/permalink.rs", - selection: None, - }) - .unwrap(); - - let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_gitlab_permalink_from_ssh_url_single_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "git@gitlab.com:zed-industries/zed.git", - sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7", - path: "crates/editor/src/git/permalink.rs", - selection: Some(6..6), - }) - .unwrap(); - - let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_gitlab_permalink_from_ssh_url_multi_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "git@gitlab.com:zed-industries/zed.git", - sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7", - path: "crates/editor/src/git/permalink.rs", - selection: Some(23..47), - }) - .unwrap(); - - let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-48"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_gitlab_permalink_from_https_url() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "https://gitlab.com/zed-industries/zed.git", - sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa", - path: "crates/zed/src/main.rs", - selection: None, - }) - .unwrap(); - - let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_gitlab_permalink_from_https_url_single_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "https://gitlab.com/zed-industries/zed.git", - sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa", - path: "crates/zed/src/main.rs", - selection: Some(6..6), - }) - .unwrap(); - - let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_gitlab_permalink_from_https_url_multi_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "https://gitlab.com/zed-industries/zed.git", - sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa", - path: "crates/zed/src/main.rs", - selection: Some(23..47), - }) - .unwrap(); - - let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-48"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_gitee_permalink_from_ssh_url() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "git@gitee.com:libkitten/zed.git", - sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194", - path: "crates/editor/src/git/permalink.rs", - selection: None, - }) - .unwrap(); - - let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_gitee_permalink_from_ssh_url_single_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "git@gitee.com:libkitten/zed.git", - sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194", - path: "crates/editor/src/git/permalink.rs", - selection: Some(6..6), - }) - .unwrap(); - - let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L7"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_gitee_permalink_from_ssh_url_multi_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "git@gitee.com:libkitten/zed.git", - sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194", - path: "crates/editor/src/git/permalink.rs", - selection: Some(23..47), - }) - .unwrap(); - - let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L24-48"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_gitee_permalink_from_https_url() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "https://gitee.com/libkitten/zed.git", - sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194", - path: "crates/zed/src/main.rs", - selection: None, - }) - .unwrap(); - - let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_gitee_permalink_from_https_url_single_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "https://gitee.com/libkitten/zed.git", - sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194", - path: "crates/zed/src/main.rs", - selection: Some(6..6), - }) - .unwrap(); - - let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs#L7"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_gitee_permalink_from_https_url_multi_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "https://gitee.com/libkitten/zed.git", - sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194", - path: "crates/zed/src/main.rs", - selection: Some(23..47), - }) - .unwrap(); - let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs#L24-48"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_parse_git_remote_url_bitbucket_https_with_username() { - let url = "https://thorstenballzed@bitbucket.org/thorstenzed/testingrepo.git"; - let parsed = parse_git_remote_url(url).unwrap(); - assert!(matches!(parsed.provider, HostingProvider::Bitbucket)); - assert_eq!(parsed.owner, "thorstenzed"); - assert_eq!(parsed.repo, "testingrepo"); - } - - #[test] - fn test_parse_git_remote_url_bitbucket_https_without_username() { - let url = "https://bitbucket.org/thorstenzed/testingrepo.git"; - let parsed = parse_git_remote_url(url).unwrap(); - assert!(matches!(parsed.provider, HostingProvider::Bitbucket)); - assert_eq!(parsed.owner, "thorstenzed"); - assert_eq!(parsed.repo, "testingrepo"); - } - - #[test] - fn test_parse_git_remote_url_bitbucket_git() { - let url = "git@bitbucket.org:thorstenzed/testingrepo.git"; - let parsed = parse_git_remote_url(url).unwrap(); - assert!(matches!(parsed.provider, HostingProvider::Bitbucket)); - assert_eq!(parsed.owner, "thorstenzed"); - assert_eq!(parsed.repo, "testingrepo"); - } - - #[test] - fn test_build_bitbucket_permalink_from_ssh_url() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "git@bitbucket.org:thorstenzed/testingrepo.git", - sha: "f00b4r", - path: "main.rs", - selection: None, - }) - .unwrap(); - - let expected_url = "https://bitbucket.org/thorstenzed/testingrepo/src/f00b4r/main.rs"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_bitbucket_permalink_from_ssh_url_single_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "git@bitbucket.org:thorstenzed/testingrepo.git", - sha: "f00b4r", - path: "main.rs", - selection: Some(6..6), - }) - .unwrap(); - - let expected_url = - "https://bitbucket.org/thorstenzed/testingrepo/src/f00b4r/main.rs#lines-7"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_bitbucket_permalink_from_ssh_url_multi_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "git@bitbucket.org:thorstenzed/testingrepo.git", - sha: "f00b4r", - path: "main.rs", - selection: Some(23..47), - }) - .unwrap(); - - let expected_url = - "https://bitbucket.org/thorstenzed/testingrepo/src/f00b4r/main.rs#lines-24:48"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_sourcehut_permalink_from_ssh_url() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "git@git.sr.ht:~rajveermalviya/zed", - sha: "faa6f979be417239b2e070dbbf6392b909224e0b", - path: "crates/editor/src/git/permalink.rs", - selection: None, - }) - .unwrap(); - - let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_sourcehut_permalink_from_ssh_url_with_git_prefix() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "git@git.sr.ht:~rajveermalviya/zed.git", - sha: "faa6f979be417239b2e070dbbf6392b909224e0b", - path: "crates/editor/src/git/permalink.rs", - selection: None, - }) - .unwrap(); - - let expected_url = "https://git.sr.ht/~rajveermalviya/zed.git/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_sourcehut_permalink_from_ssh_url_single_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "git@git.sr.ht:~rajveermalviya/zed", - sha: "faa6f979be417239b2e070dbbf6392b909224e0b", - path: "crates/editor/src/git/permalink.rs", - selection: Some(6..6), - }) - .unwrap(); - - let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L7"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_sourcehut_permalink_from_ssh_url_multi_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "git@git.sr.ht:~rajveermalviya/zed", - sha: "faa6f979be417239b2e070dbbf6392b909224e0b", - path: "crates/editor/src/git/permalink.rs", - selection: Some(23..47), - }) - .unwrap(); - - let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L24-48"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_sourcehut_permalink_from_https_url() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "https://git.sr.ht/~rajveermalviya/zed", - sha: "faa6f979be417239b2e070dbbf6392b909224e0b", - path: "crates/zed/src/main.rs", - selection: None, - }) - .unwrap(); - - let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/zed/src/main.rs"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_sourcehut_permalink_from_https_url_single_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "https://git.sr.ht/~rajveermalviya/zed", - sha: "faa6f979be417239b2e070dbbf6392b909224e0b", - path: "crates/zed/src/main.rs", - selection: Some(6..6), - }) - .unwrap(); - - let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/zed/src/main.rs#L7"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_sourcehut_permalink_from_https_url_multi_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "https://git.sr.ht/~rajveermalviya/zed", - sha: "faa6f979be417239b2e070dbbf6392b909224e0b", - path: "crates/zed/src/main.rs", - selection: Some(23..47), - }) - .unwrap(); - - let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/zed/src/main.rs#L24-48"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_codeberg_permalink_from_ssh_url() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "git@codeberg.org:rajveermalviya/zed.git", - sha: "faa6f979be417239b2e070dbbf6392b909224e0b", - path: "crates/editor/src/git/permalink.rs", - selection: None, - }) - .unwrap(); - - let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_codeberg_permalink_from_ssh_url_single_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "git@codeberg.org:rajveermalviya/zed.git", - sha: "faa6f979be417239b2e070dbbf6392b909224e0b", - path: "crates/editor/src/git/permalink.rs", - selection: Some(6..6), - }) - .unwrap(); - - let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs#L7"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_codeberg_permalink_from_ssh_url_multi_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "git@codeberg.org:rajveermalviya/zed.git", - sha: "faa6f979be417239b2e070dbbf6392b909224e0b", - path: "crates/editor/src/git/permalink.rs", - selection: Some(23..47), - }) - .unwrap(); - - let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs#L24-L48"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_codeberg_permalink_from_https_url() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "https://codeberg.org/rajveermalviya/zed.git", - sha: "faa6f979be417239b2e070dbbf6392b909224e0b", - path: "crates/zed/src/main.rs", - selection: None, - }) - .unwrap(); - - let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/zed/src/main.rs"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_codeberg_permalink_from_https_url_single_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "https://codeberg.org/rajveermalviya/zed.git", - sha: "faa6f979be417239b2e070dbbf6392b909224e0b", - path: "crates/zed/src/main.rs", - selection: Some(6..6), - }) - .unwrap(); - - let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/zed/src/main.rs#L7"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } - - #[test] - fn test_build_codeberg_permalink_from_https_url_multi_line_selection() { - let permalink = build_permalink(BuildPermalinkParams { - remote_url: "https://codeberg.org/rajveermalviya/zed.git", - sha: "faa6f979be417239b2e070dbbf6392b909224e0b", - path: "crates/zed/src/main.rs", - selection: Some(23..47), - }) - .unwrap(); - - let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/zed/src/main.rs#L24-L48"; - assert_eq!(permalink.to_string(), expected_url.to_string()) - } -} diff --git a/crates/git/src/pull_request.rs b/crates/git/src/pull_request.rs deleted file mode 100644 index a0876de7ab..0000000000 --- a/crates/git/src/pull_request.rs +++ /dev/null @@ -1,83 +0,0 @@ -use lazy_static::lazy_static; -use url::Url; - -use crate::{hosting_provider::HostingProvider, permalink::ParsedGitRemote}; - -lazy_static! { - static ref GITHUB_PULL_REQUEST_NUMBER: regex::Regex = - regex::Regex::new(r"\(#(\d+)\)$").unwrap(); -} - -#[derive(Clone, Debug)] -pub struct PullRequest { - pub number: u32, - pub url: Url, -} - -pub fn extract_pull_request(remote: &ParsedGitRemote, message: &str) -> Option { - match remote.provider { - HostingProvider::Github => { - let line = message.lines().next()?; - let capture = GITHUB_PULL_REQUEST_NUMBER.captures(line)?; - let number = capture.get(1)?.as_str().parse::().ok()?; - - let mut url = remote.provider.base_url(); - let path = format!("/{}/{}/pull/{}", remote.owner, remote.repo, number); - url.set_path(&path); - - Some(PullRequest { number, url }) - } - _ => None, - } -} - -#[cfg(test)] -mod tests { - use unindent::Unindent; - - use crate::{ - hosting_provider::HostingProvider, permalink::ParsedGitRemote, - pull_request::extract_pull_request, - }; - - #[test] - fn test_github_pull_requests() { - let remote = ParsedGitRemote { - provider: HostingProvider::Github, - owner: "zed-industries", - repo: "zed", - }; - - let message = "This does not contain a pull request"; - assert!(extract_pull_request(&remote, message).is_none()); - - // Pull request number at end of first line - let message = r#" - project panel: do not expand collapsed worktrees on "collapse all entries" (#10687) - - Fixes #10597 - - Release Notes: - - - Fixed "project panel: collapse all entries" expanding collapsed worktrees. - "# - .unindent(); - - assert_eq!( - extract_pull_request(&remote, &message) - .unwrap() - .url - .as_str(), - "https://github.com/zed-industries/zed/pull/10687" - ); - - // Pull request number in middle of line, which we want to ignore - let message = r#" - Follow-up to #10687 to fix problems - - See the original PR, this is a fix. - "# - .unindent(); - assert!(extract_pull_request(&remote, &message).is_none()); - } -}