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:
Thorsten Ball 2024-04-19 15:15:19 +02:00 committed by GitHub
parent 37e4f83a78
commit 9247da77a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 514 additions and 239 deletions

View file

@ -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");
}