Detailed changes
@@ -42,10 +42,11 @@ impl GitRemote {
pub async fn avatar_url(
&self,
commit: SharedString,
+ author_email: Option<SharedString>,
client: Arc<dyn HttpClient>,
) -> Option<Url> {
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<SharedString>,
_http_client: Arc<dyn HttpClient>,
) -> Result<Option<Url>> {
Ok(None)
@@ -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.))
@@ -283,6 +283,7 @@ impl GitHostingProvider for Bitbucket {
repo_owner: &str,
repo: &str,
commit: SharedString,
+ _author_email: Option<SharedString>,
http_client: Arc<dyn HttpClient>,
) -> Result<Option<Url>> {
let commit = commit.to_string();
@@ -169,6 +169,7 @@ impl GitHostingProvider for Chromium {
_repo_owner: &str,
repo: &str,
commit: SharedString,
+ _author_email: Option<SharedString>,
http_client: Arc<dyn HttpClient>,
) -> Result<Option<Url>> {
let commit = commit.to_string();
@@ -233,6 +233,7 @@ impl GitHostingProvider for Forgejo {
repo_owner: &str,
repo: &str,
commit: SharedString,
+ _author_email: Option<SharedString>,
http_client: Arc<dyn HttpClient>,
) -> Result<Option<Url>> {
let commit = commit.to_string();
@@ -182,6 +182,7 @@ impl GitHostingProvider for Gitea {
repo_owner: &str,
repo: &str,
commit: SharedString,
+ _author_email: Option<SharedString>,
http_client: Arc<dyn HttpClient>,
) -> Result<Option<Url>> {
let commit = commit.to_string();
@@ -141,6 +141,7 @@ impl GitHostingProvider for Gitee {
repo_owner: &str,
repo: &str,
commit: SharedString,
+ _author_email: Option<SharedString>,
http_client: Arc<dyn HttpClient>,
) -> Result<Option<Url>> {
let commit = commit.to_string();
@@ -68,6 +68,15 @@ pub struct Github {
base_url: Url,
}
+fn build_cdn_avatar_url(email: &str) -> Result<Url> {
+ 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<String>, base_url: Url) -> Self {
Self {
@@ -255,8 +264,13 @@ impl GitHostingProvider for Github {
repo_owner: &str,
repo: &str,
commit: SharedString,
+ author_email: Option<SharedString>,
http_client: Arc<dyn HttpClient>,
) -> Result<Option<Url>> {
+ 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("<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_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"
+ );
+ }
}
@@ -234,6 +234,7 @@ impl GitHostingProvider for Gitlab {
repo_owner: &str,
repo: &str,
commit: SharedString,
+ _author_email: Option<SharedString>,
http_client: Arc<dyn HttpClient>,
) -> Result<Option<Url>> {
let commit = commit.to_string();
@@ -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("<no name>".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)
@@ -28,14 +28,20 @@ pub struct CommitDetails {
pub struct CommitAvatar<'a> {
sha: &'a SharedString,
+ author_email: Option<SharedString>,
remote: Option<&'a GitRemote>,
size: Option<IconSize>,
}
impl<'a> CommitAvatar<'a> {
- pub fn new(sha: &'a SharedString, remote: Option<&'a GitRemote>) -> Self {
+ pub fn new(
+ sha: &'a SharedString,
+ author_email: Option<SharedString>,
+ 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::<CommitAvatarAsset>(&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<SharedString>,
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<SharedString>) -> 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()))
}
@@ -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)
@@ -283,6 +283,7 @@ impl FileHistoryView {
fn render_commit_avatar(
&self,
sha: &SharedString,
+ author_email: Option<SharedString>,
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::<CommitAvatarAsset>(&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<SharedString>,
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<SharedString>) -> 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