git blame: Display GitHub avatars in blame tooltips, if available (#10767)
Release Notes: - Added GitHub avatars to tooltips that appear when hovering over a `git blame` entry (either inline or in the blame gutter). Demo: https://github.com/zed-industries/zed/assets/1185253/295c5aee-3a4e-46aa-812d-495439d8840d
This commit is contained in:
parent
37e4f83a78
commit
9247da77a3
12 changed files with 514 additions and 239 deletions
|
@ -23,9 +23,9 @@ sum_tree.workspace = true
|
|||
text.workspace = true
|
||||
time.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
serde.workspace = true
|
||||
rope.workspace = true
|
||||
util.workspace = true
|
||||
parking_lot.workspace = true
|
||||
windows.workspace = true
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ pub use lazy_static::lazy_static;
|
|||
pub mod blame;
|
||||
pub mod commit;
|
||||
pub mod diff;
|
||||
pub mod hosting_provider;
|
||||
pub mod permalink;
|
||||
pub mod repository;
|
||||
|
||||
|
|
107
crates/git/src/hosting_provider.rs
Normal file
107
crates/git/src/hosting_provider.rs
Normal file
|
@ -0,0 +1,107 @@
|
|||
use core::fmt;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use url::Url;
|
||||
use util::{github, http::HttpClient};
|
||||
|
||||
use crate::Oid;
|
||||
|
||||
#[derive(Clone, Debug, Hash)]
|
||||
pub enum HostingProvider {
|
||||
Github,
|
||||
Gitlab,
|
||||
Gitee,
|
||||
Bitbucket,
|
||||
Sourcehut,
|
||||
Codeberg,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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),
|
||||
}
|
||||
} 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn supports_avatars(&self) -> bool {
|
||||
match self {
|
||||
HostingProvider::Github => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn commit_author_avatar_url(
|
||||
&self,
|
||||
repo_owner: &str,
|
||||
repo: &str,
|
||||
commit: Oid,
|
||||
client: Arc<dyn HttpClient>,
|
||||
) -> Result<Option<Url>> {
|
||||
match self {
|
||||
HostingProvider::Github => {
|
||||
let commit = commit.to_string();
|
||||
|
||||
let author =
|
||||
github::fetch_github_commit_author(repo_owner, repo, &commit, &client).await?;
|
||||
|
||||
let url = if let Some(author) = author {
|
||||
let mut url = Url::parse(&author.avatar_url)?;
|
||||
url.set_query(Some("size=128"));
|
||||
Some(url)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(url)
|
||||
}
|
||||
_ => 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)
|
||||
}
|
||||
}
|
|
@ -3,55 +3,7 @@ use std::ops::Range;
|
|||
use anyhow::{anyhow, Result};
|
||||
use url::Url;
|
||||
|
||||
pub enum GitHostingProvider {
|
||||
Github,
|
||||
Gitlab,
|
||||
Gitee,
|
||||
Bitbucket,
|
||||
Sourcehut,
|
||||
Codeberg,
|
||||
}
|
||||
|
||||
impl GitHostingProvider {
|
||||
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()
|
||||
}
|
||||
|
||||
/// Returns the fragment portion of the URL for the selected lines in
|
||||
/// the representation the [`GitHostingProvider`] expects.
|
||||
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),
|
||||
}
|
||||
} 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::hosting_provider::HostingProvider;
|
||||
|
||||
pub struct BuildPermalinkParams<'a> {
|
||||
pub remote_url: &'a str,
|
||||
|
@ -76,12 +28,12 @@ pub fn build_permalink(params: BuildPermalinkParams) -> Result<Url> {
|
|||
.ok_or_else(|| anyhow!("failed to parse Git remote URL"))?;
|
||||
|
||||
let path = match provider {
|
||||
GitHostingProvider::Github => format!("{owner}/{repo}/blob/{sha}/{path}"),
|
||||
GitHostingProvider::Gitlab => format!("{owner}/{repo}/-/blob/{sha}/{path}"),
|
||||
GitHostingProvider::Gitee => format!("{owner}/{repo}/blob/{sha}/{path}"),
|
||||
GitHostingProvider::Bitbucket => format!("{owner}/{repo}/src/{sha}/{path}"),
|
||||
GitHostingProvider::Sourcehut => format!("~{owner}/{repo}/tree/{sha}/item/{path}"),
|
||||
GitHostingProvider::Codeberg => format!("{owner}/{repo}/src/commit/{sha}/{path}"),
|
||||
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));
|
||||
|
||||
|
@ -90,8 +42,9 @@ pub fn build_permalink(params: BuildPermalinkParams) -> Result<Url> {
|
|||
Ok(permalink)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParsedGitRemote<'a> {
|
||||
pub provider: GitHostingProvider,
|
||||
pub provider: HostingProvider,
|
||||
pub owner: &'a str,
|
||||
pub repo: &'a str,
|
||||
}
|
||||
|
@ -111,12 +64,12 @@ pub fn build_commit_permalink(params: BuildCommitPermalinkParams) -> Url {
|
|||
} = remote;
|
||||
|
||||
let path = match provider {
|
||||
GitHostingProvider::Github => format!("{owner}/{repo}/commit/{sha}"),
|
||||
GitHostingProvider::Gitlab => format!("{owner}/{repo}/-/commit/{sha}"),
|
||||
GitHostingProvider::Gitee => format!("{owner}/{repo}/commit/{sha}"),
|
||||
GitHostingProvider::Bitbucket => format!("{owner}/{repo}/commits/{sha}"),
|
||||
GitHostingProvider::Sourcehut => format!("~{owner}/{repo}/commit/{sha}"),
|
||||
GitHostingProvider::Codeberg => format!("{owner}/{repo}/commit/{sha}"),
|
||||
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()
|
||||
|
@ -132,7 +85,7 @@ pub fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
|
|||
let (owner, repo) = repo_with_owner.split_once('/')?;
|
||||
|
||||
return Some(ParsedGitRemote {
|
||||
provider: GitHostingProvider::Github,
|
||||
provider: HostingProvider::Github,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
|
@ -147,7 +100,7 @@ pub fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
|
|||
let (owner, repo) = repo_with_owner.split_once('/')?;
|
||||
|
||||
return Some(ParsedGitRemote {
|
||||
provider: GitHostingProvider::Gitlab,
|
||||
provider: HostingProvider::Gitlab,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
|
@ -162,7 +115,7 @@ pub fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
|
|||
let (owner, repo) = repo_with_owner.split_once('/')?;
|
||||
|
||||
return Some(ParsedGitRemote {
|
||||
provider: GitHostingProvider::Gitee,
|
||||
provider: HostingProvider::Gitee,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
|
@ -176,7 +129,7 @@ pub fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
|
|||
.split_once('/')?;
|
||||
|
||||
return Some(ParsedGitRemote {
|
||||
provider: GitHostingProvider::Bitbucket,
|
||||
provider: HostingProvider::Bitbucket,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
|
@ -193,7 +146,7 @@ pub fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
|
|||
let (owner, repo) = repo_with_owner.split_once('/')?;
|
||||
|
||||
return Some(ParsedGitRemote {
|
||||
provider: GitHostingProvider::Sourcehut,
|
||||
provider: HostingProvider::Sourcehut,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
|
@ -208,7 +161,7 @@ pub fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
|
|||
let (owner, repo) = repo_with_owner.split_once('/')?;
|
||||
|
||||
return Some(ParsedGitRemote {
|
||||
provider: GitHostingProvider::Codeberg,
|
||||
provider: HostingProvider::Codeberg,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
|
@ -476,7 +429,7 @@ mod tests {
|
|||
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, GitHostingProvider::Bitbucket));
|
||||
assert!(matches!(parsed.provider, HostingProvider::Bitbucket));
|
||||
assert_eq!(parsed.owner, "thorstenzed");
|
||||
assert_eq!(parsed.repo, "testingrepo");
|
||||
}
|
||||
|
@ -485,7 +438,7 @@ mod tests {
|
|||
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, GitHostingProvider::Bitbucket));
|
||||
assert!(matches!(parsed.provider, HostingProvider::Bitbucket));
|
||||
assert_eq!(parsed.owner, "thorstenzed");
|
||||
assert_eq!(parsed.repo, "testingrepo");
|
||||
}
|
||||
|
@ -494,7 +447,7 @@ mod tests {
|
|||
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, GitHostingProvider::Bitbucket));
|
||||
assert!(matches!(parsed.provider, HostingProvider::Bitbucket));
|
||||
assert_eq!(parsed.owner, "thorstenzed");
|
||||
assert_eq!(parsed.repo, "testingrepo");
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue