Cargo.lock 🔗
@@ -4384,6 +4384,7 @@ name = "git"
version = "0.1.0"
dependencies = [
"anyhow",
+ "async-trait",
"clock",
"collections",
"git2",
Marshall Bowers created
This PR refactors the code pertaining to Git hosting providers to make
it more uniform and easy to add support for new providers.
There is now a `GitHostingProvider` trait that contains the
functionality specific to an individual Git hosting provider. Each
provider we support has an implementation of this trait.
Release Notes:
- N/A
Cargo.lock | 1
crates/editor/src/blame_entry_tooltip.rs | 2
crates/editor/src/editor.rs | 29
crates/editor/src/git/blame.rs | 41
crates/git/Cargo.toml | 1
crates/git/src/blame.rs | 13
crates/git/src/git.rs | 9
crates/git/src/hosting_provider.rs | 170 ++--
crates/git/src/hosting_providers.rs | 13
crates/git/src/hosting_providers/bitbucket.rs | 169 +++++
crates/git/src/hosting_providers/codeberg.rs | 219 ++++++
crates/git/src/hosting_providers/gitee.rs | 196 ++++++
crates/git/src/hosting_providers/github.rs | 287 ++++++++
crates/git/src/hosting_providers/gitlab.rs | 196 ++++++
crates/git/src/hosting_providers/sourcehut.rs | 217 ++++++
crates/git/src/permalink.rs | 680 ---------------------
crates/git/src/pull_request.rs | 83 --
17 files changed, 1,443 insertions(+), 883 deletions(-)
@@ -4384,6 +4384,7 @@ name = "git"
version = "0.1.0"
dependencies = [
"anyhow",
+ "async-trait",
"clock",
"collections",
"git2",
@@ -61,7 +61,7 @@ struct CommitAvatarAsset {
impl Hash for CommitAvatarAsset {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.sha.hash(state);
- self.remote.host.hash(state);
+ self.remote.host.name().hash(state);
}
}
@@ -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::<Point>(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<Self>) {
@@ -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<dyn GitHostingProvider + Send + Sync + 'static>,
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,
@@ -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
@@ -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(),
+ },
+ )
});
}
}
@@ -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! {
@@ -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",
- };
-
- Url::parse(&base_url).unwrap()
- }
+pub struct BuildCommitPermalinkParams<'a> {
+ pub sha: &'a str,
+}
+
+pub struct BuildPermalinkParams<'a> {
+ pub sha: &'a str,
+ pub path: &'a str,
+ pub selection: Option<Range<u32>>,
+}
+
+/// 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 the fragment portion of the URL for the selected lines in
- /// the representation the [`GitHostingProvider`] expects.
- pub(crate) fn line_fragment(&self, selection: &Range<u32>) -> String {
+ /// Returns a URL fragment to the given line selection.
+ fn line_fragment(&self, selection: &Range<u32>) -> 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;
+
+ /// 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<ParsedGitRemote<'a>>;
+
+ fn extract_pull_request(
+ &self,
+ _remote: &ParsedGitRemote,
+ _message: &str,
+ ) -> Option<PullRequest> {
+ None
}
- pub async fn commit_author_avatar_url(
+ async fn commit_author_avatar_url(
&self,
- repo_owner: &str,
- repo: &str,
- commit: Oid,
- client: Arc<dyn HttpClient>,
+ _repo_owner: &str,
+ _repo: &str,
+ _commit: Oid,
+ _http_client: Arc<dyn HttpClient>,
) -> Result<Option<Url>> {
- Ok(match self {
- HostingProvider::Github => {
- let commit = commit.to_string();
- github::fetch_github_commit_author(repo_owner, repo, &commit, &client)
- .await?
- .map(|author| -> Result<Url, url::ParseError> {
- 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<dyn GitHostingProvider + Send + Sync + 'static>,
+ ParsedGitRemote,
+)> {
+ let providers: Vec<Arc<dyn GitHostingProvider + Send + Sync + 'static>> = 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))
+ })
}
@@ -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::*;
@@ -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<ParsedGitRemote<'a>> {
+ 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())
+ }
+}
@@ -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<ParsedGitRemote<'a>> {
+ 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<dyn HttpClient>,
+ ) -> Result<Option<Url>> {
+ 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())
+ }
+}
@@ -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<ParsedGitRemote<'a>> {
+ 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())
+ }
+}
@@ -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<Regex> = 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<ParsedGitRemote<'a>> {
+ 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<PullRequest> {
+ let line = message.lines().next()?;
+ let capture = pull_request_number_regex().captures(line)?;
+ let number = capture.get(1)?.as_str().parse::<u32>().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<dyn HttpClient>,
+ ) -> Result<Option<Url>> {
+ let commit = commit.to_string();
+ let avatar_url =
+ github::fetch_github_commit_author(repo_owner, repo, &commit, &http_client)
+ .await?
+ .map(|author| -> Result<Url, url::ParseError> {
+ 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);
+ }
+}
@@ -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<ParsedGitRemote<'a>> {
+ 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())
+ }
+}
@@ -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<ParsedGitRemote<'a>> {
+ 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())
+ }
+}
@@ -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<Range<u32>>,
-}
-
-pub fn build_permalink(params: BuildPermalinkParams) -> Result<Url> {
- 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<ParsedGitRemote> {
- 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())
- }
-}
@@ -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<PullRequest> {
- 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::<u32>().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());
- }
-}