diff --git a/crates/git/src/hosting_provider.rs b/crates/git/src/hosting_provider.rs index 6c69d7a714a69289a858f51837da35abebbb4a4a..958c2d088600f1c869e9544855cd2a7ef3a947c9 100644 --- a/crates/git/src/hosting_provider.rs +++ b/crates/git/src/hosting_provider.rs @@ -42,10 +42,11 @@ impl GitRemote { pub async fn avatar_url( &self, commit: SharedString, + author_email: Option, client: Arc, ) -> Option { self.host - .commit_author_avatar_url(&self.owner, &self.repo, commit, client) + .commit_author_avatar_url(&self.owner, &self.repo, commit, author_email, client) .await .ok() .flatten() @@ -139,6 +140,7 @@ pub trait GitHostingProvider { _repo_owner: &str, _repo: &str, _commit: SharedString, + _author_email: Option, _http_client: Arc, ) -> Result> { Ok(None) diff --git a/crates/git_graph/src/git_graph.rs b/crates/git_graph/src/git_graph.rs index 8ca21071814d7406c5c7e45301fa078d0eb30b6a..fb59826397d432c63479fbba550b9d5fadb72b13 100644 --- a/crates/git_graph/src/git_graph.rs +++ b/crates/git_graph/src/git_graph.rs @@ -975,7 +975,12 @@ impl GitGraph { let remote = repository.update(cx, |repo, cx| self.get_remote(repo, window, cx)); let avatar = { - let avatar = CommitAvatar::new(&full_sha, remote.as_ref()); + let author_email_for_avatar = if author_email.is_empty() { + None + } else { + Some(author_email.clone()) + }; + let avatar = CommitAvatar::new(&full_sha, author_email_for_avatar, remote.as_ref()); v_flex() .w(px(64.)) .h(px(64.)) diff --git a/crates/git_hosting_providers/src/providers/bitbucket.rs b/crates/git_hosting_providers/src/providers/bitbucket.rs index 07c6898d4e0affc3bdde4d8290607897cf2cd5be..df216d1801bbb4a96a1765b3a4eac81a4ce425e8 100644 --- a/crates/git_hosting_providers/src/providers/bitbucket.rs +++ b/crates/git_hosting_providers/src/providers/bitbucket.rs @@ -283,6 +283,7 @@ impl GitHostingProvider for Bitbucket { repo_owner: &str, repo: &str, commit: SharedString, + _author_email: Option, http_client: Arc, ) -> Result> { let commit = commit.to_string(); diff --git a/crates/git_hosting_providers/src/providers/chromium.rs b/crates/git_hosting_providers/src/providers/chromium.rs index 0826e31b309918fb0967f7b3019b53fe483837b9..4f20ce0403f48ca610fa9706789b2874643cf405 100644 --- a/crates/git_hosting_providers/src/providers/chromium.rs +++ b/crates/git_hosting_providers/src/providers/chromium.rs @@ -169,6 +169,7 @@ impl GitHostingProvider for Chromium { _repo_owner: &str, repo: &str, commit: SharedString, + _author_email: Option, http_client: Arc, ) -> Result> { let commit = commit.to_string(); diff --git a/crates/git_hosting_providers/src/providers/forgejo.rs b/crates/git_hosting_providers/src/providers/forgejo.rs index 3944b7a165b724d96bbf6fc315a7b3f213457bf4..d31288655e4de87ac9f96cab6056e6f86ab46983 100644 --- a/crates/git_hosting_providers/src/providers/forgejo.rs +++ b/crates/git_hosting_providers/src/providers/forgejo.rs @@ -233,6 +233,7 @@ impl GitHostingProvider for Forgejo { repo_owner: &str, repo: &str, commit: SharedString, + _author_email: Option, http_client: Arc, ) -> Result> { let commit = commit.to_string(); diff --git a/crates/git_hosting_providers/src/providers/gitea.rs b/crates/git_hosting_providers/src/providers/gitea.rs index d3e62fe6a85c0f2b126dfd4bdb703195c7ec4b38..6196e56b96cfacc5164977d0c2a10b7c7eb79f57 100644 --- a/crates/git_hosting_providers/src/providers/gitea.rs +++ b/crates/git_hosting_providers/src/providers/gitea.rs @@ -182,6 +182,7 @@ impl GitHostingProvider for Gitea { repo_owner: &str, repo: &str, commit: SharedString, + _author_email: Option, http_client: Arc, ) -> Result> { let commit = commit.to_string(); diff --git a/crates/git_hosting_providers/src/providers/gitee.rs b/crates/git_hosting_providers/src/providers/gitee.rs index 120a360cb19615e11e0ea4829a6fcd68665e4fcc..5b832a47464c1e8cb5600fdfa4efa63ba0678029 100644 --- a/crates/git_hosting_providers/src/providers/gitee.rs +++ b/crates/git_hosting_providers/src/providers/gitee.rs @@ -141,6 +141,7 @@ impl GitHostingProvider for Gitee { repo_owner: &str, repo: &str, commit: SharedString, + _author_email: Option, http_client: Arc, ) -> Result> { let commit = commit.to_string(); diff --git a/crates/git_hosting_providers/src/providers/github.rs b/crates/git_hosting_providers/src/providers/github.rs index 77e297d52bc6e9b0c669e32d591b9e77fcb17aa4..23aff64de93b89f04cdf2113af0df9667a335afc 100644 --- a/crates/git_hosting_providers/src/providers/github.rs +++ b/crates/git_hosting_providers/src/providers/github.rs @@ -68,6 +68,15 @@ pub struct Github { base_url: Url, } +fn build_cdn_avatar_url(email: &str) -> Result { + let email = email.trim_start_matches('<').trim_end_matches('>'); + Url::parse(&format!( + "https://avatars.githubusercontent.com/u/e?email={}&s=128", + encode(email) + )) + .context("failed to construct avatar URL") +} + impl Github { pub fn new(name: impl Into, base_url: Url) -> Self { Self { @@ -255,8 +264,13 @@ impl GitHostingProvider for Github { repo_owner: &str, repo: &str, commit: SharedString, + author_email: Option, http_client: Arc, ) -> Result> { + if let Some(email) = author_email { + return Ok(Some(build_cdn_avatar_url(&email)?)); + } + let commit = commit.to_string(); let avatar_url = self .fetch_github_commit_author(repo_owner, repo, &commit, &http_client) @@ -588,4 +602,31 @@ mod tests { "https://github.zed.com/zed-industries/zed/pull/new/feature%2Fnew-feature" ); } + + #[test] + fn test_build_cdn_avatar_url_simple_email() { + let url = build_cdn_avatar_url("user@example.com").unwrap(); + assert_eq!( + url.as_str(), + "https://avatars.githubusercontent.com/u/e?email=user%40example.com&s=128" + ); + } + + #[test] + fn test_build_cdn_avatar_url_with_angle_brackets() { + let url = build_cdn_avatar_url("").unwrap(); + assert_eq!( + url.as_str(), + "https://avatars.githubusercontent.com/u/e?email=user%40example.com&s=128" + ); + } + + #[test] + fn test_build_cdn_avatar_url_with_special_chars() { + let url = build_cdn_avatar_url("user+tag@example.com").unwrap(); + assert_eq!( + url.as_str(), + "https://avatars.githubusercontent.com/u/e?email=user%2Btag%40example.com&s=128" + ); + } } diff --git a/crates/git_hosting_providers/src/providers/gitlab.rs b/crates/git_hosting_providers/src/providers/gitlab.rs index 80171e2d3cbb092876e7c1e2d23b66cef839943c..ec81d6d7f61bddb287e935374e5154487818b240 100644 --- a/crates/git_hosting_providers/src/providers/gitlab.rs +++ b/crates/git_hosting_providers/src/providers/gitlab.rs @@ -234,6 +234,7 @@ impl GitHostingProvider for Gitlab { repo_owner: &str, repo: &str, commit: SharedString, + _author_email: Option, http_client: Arc, ) -> Result> { let commit = commit.to_string(); diff --git a/crates/git_ui/src/blame_ui.rs b/crates/git_ui/src/blame_ui.rs index e62a7c76677b1406bbfc00234f92ec7cb5a8ca5d..56d1a993cc4cc62d95c97f6a39d41b16fc4a4c1c 100644 --- a/crates/git_ui/src/blame_ui.rs +++ b/crates/git_ui/src/blame_ui.rs @@ -44,9 +44,18 @@ impl BlameRenderer for GitBlameRenderer { let name = util::truncate_and_trailoff(author_name, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED); let avatar = if ProjectSettings::get_global(cx).git.blame.show_avatar { + let author_email = blame_entry.author_mail.as_ref().map(|email| { + SharedString::from( + email + .trim_start_matches('<') + .trim_end_matches('>') + .to_string(), + ) + }); Some( CommitAvatar::new( &blame_entry.sha.to_string().into(), + author_email, details.as_ref().and_then(|it| it.remote.as_ref()), ) .render(window, cx), @@ -186,8 +195,20 @@ impl BlameRenderer for GitBlameRenderer { .unwrap_or("".to_string()) .into(); let author_email = blame.author_mail.as_deref().unwrap_or_default(); - let avatar = CommitAvatar::new(&sha, details.as_ref().and_then(|it| it.remote.as_ref())) - .render(window, cx); + let author_email_for_avatar = blame.author_mail.as_ref().map(|email| { + SharedString::from( + email + .trim_start_matches('<') + .trim_end_matches('>') + .to_string(), + ) + }); + let avatar = CommitAvatar::new( + &sha, + author_email_for_avatar, + details.as_ref().and_then(|it| it.remote.as_ref()), + ) + .render(window, cx); let short_commit_id = sha .get(..8) diff --git a/crates/git_ui/src/commit_tooltip.rs b/crates/git_ui/src/commit_tooltip.rs index d79f98747c689c58ded35315a1cfe4eafb245579..114a185542f719b4779a7dccded34819b82701ef 100644 --- a/crates/git_ui/src/commit_tooltip.rs +++ b/crates/git_ui/src/commit_tooltip.rs @@ -28,14 +28,20 @@ pub struct CommitDetails { pub struct CommitAvatar<'a> { sha: &'a SharedString, + author_email: Option, remote: Option<&'a GitRemote>, size: Option, } impl<'a> CommitAvatar<'a> { - pub fn new(sha: &'a SharedString, remote: Option<&'a GitRemote>) -> Self { + pub fn new( + sha: &'a SharedString, + author_email: Option, + remote: Option<&'a GitRemote>, + ) -> Self { Self { sha, + author_email, remote, size: None, } @@ -44,6 +50,7 @@ impl<'a> CommitAvatar<'a> { pub fn from_commit_details(details: &'a CommitDetails) -> Self { Self { sha: &details.sha, + author_email: Some(details.author_email.clone()), remote: details .message .as_ref() @@ -75,7 +82,8 @@ impl<'a> CommitAvatar<'a> { let remote = self .remote .filter(|remote| remote.host_supports_avatars())?; - let avatar_url = CommitAvatarAsset::new(remote.clone(), self.sha.clone()); + let avatar_url = + CommitAvatarAsset::new(remote.clone(), self.sha.clone(), self.author_email.clone()); let url = window.use_asset::(&avatar_url, cx)??; Some(Avatar::new(url.to_string())) @@ -85,6 +93,7 @@ impl<'a> CommitAvatar<'a> { #[derive(Clone, Debug)] struct CommitAvatarAsset { sha: SharedString, + author_email: Option, remote: GitRemote, } @@ -96,8 +105,12 @@ impl Hash for CommitAvatarAsset { } impl CommitAvatarAsset { - fn new(remote: GitRemote, sha: SharedString) -> Self { - Self { remote, sha } + fn new(remote: GitRemote, sha: SharedString, author_email: Option) -> Self { + Self { + remote, + sha, + author_email, + } } } @@ -114,7 +127,7 @@ impl Asset for CommitAvatarAsset { async move { source .remote - .avatar_url(source.sha, client) + .avatar_url(source.sha, source.author_email, client) .await .map(|url| SharedString::from(url.to_string())) } diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index 37512e4644ba260e2b28dba151ab7a707cf3dbf0..7b460b53d7205cb8375cdd871f12b3973c06676b 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -406,7 +406,11 @@ impl CommitView { cx: &mut App, ) -> AnyElement { let size = size.into(); - let avatar = CommitAvatar::new(sha, self.remote.as_ref()); + let avatar = CommitAvatar::new( + sha, + Some(self.commit.author_email.clone()), + self.remote.as_ref(), + ); v_flex() .w(size) diff --git a/crates/git_ui/src/file_history_view.rs b/crates/git_ui/src/file_history_view.rs index 121e44e29eb8ff3bc829522cb0a6b1d00f799c8f..6b49d45add15155692649eeb025c41654d10fe95 100644 --- a/crates/git_ui/src/file_history_view.rs +++ b/crates/git_ui/src/file_history_view.rs @@ -283,6 +283,7 @@ impl FileHistoryView { fn render_commit_avatar( &self, sha: &SharedString, + author_email: Option, window: &mut Window, cx: &mut App, ) -> impl IntoElement { @@ -290,7 +291,7 @@ impl FileHistoryView { let size = rems_from_px(20.); if let Some(remote) = remote { - let avatar_asset = CommitAvatarAsset::new(remote.clone(), sha.clone()); + let avatar_asset = CommitAvatarAsset::new(remote.clone(), sha.clone(), author_email); if let Some(Some(url)) = window.use_asset::(&avatar_asset, cx) { Avatar::new(url.to_string()).size(size) } else { @@ -349,7 +350,12 @@ impl FileHistoryView { .flex_none() .child(Chip::new(pr_number)), ) - .child(self.render_commit_avatar(&entry.sha, window, cx)) + .child(self.render_commit_avatar( + &entry.sha, + Some(entry.author_email.clone()), + window, + cx, + )) .child( h_flex() .min_w_0() @@ -395,6 +401,7 @@ impl FileHistoryView { #[derive(Clone, Debug)] struct CommitAvatarAsset { sha: SharedString, + author_email: Option, remote: GitRemote, } @@ -406,8 +413,12 @@ impl std::hash::Hash for CommitAvatarAsset { } impl CommitAvatarAsset { - fn new(remote: GitRemote, sha: SharedString) -> Self { - Self { remote, sha } + fn new(remote: GitRemote, sha: SharedString, author_email: Option) -> Self { + Self { + remote, + sha, + author_email, + } } } @@ -428,6 +439,7 @@ impl Asset for CommitAvatarAsset { &source.remote.owner, &source.remote.repo, source.sha.clone(), + source.author_email.clone(), client, ) .await